pgplus/bin/
pgplus/help_files/
pgplus/port_redirector/
pgplus/src/configure/makefiles/
/*
 * Playground+ - news.c
 * NuNews system, enhanced news with groups and more (c) phypor 1998
 * ---------------------------------------------------------------------------
 *
 * Modifications to original release:
 *  Include paths
 *  varible argument functions
 *  changed ADC to LOWER_ADMIN
 *  changed pager calls to be pg+ conformant
 *  cleaned up presentation (used LINE and pstack_mid)
 *  added number of times read to "news check"
 *  added   news next  command to read next unread article
 */



#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <memory.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <errno.h>

#include "include/config.h"
#include "include/player.h"
#include "include/proto.h"



/* for sync streamlining, so we dont sync unless there is some change */
#define NEWS_CHANGED		newschanged = 1
#define NEWS_CHANGE_RESET	newschanged = 0
#define NEWS_HAS_CHANGED	newschanged == 1


extern command_func news_command;
extern int news_sync;
extern void help(player *, char *);

int newschanged = 0;


newsgroup NewsGroups[] =
{
  {"main", 0, 7, "Default news group", "files/news/main", 0},
  {"sus", PSU, 10, "Staff news group", "files/news/sus", 0},
  {"admin", LOWER_ADMIN, 15, "Admin news groups", "files/news/admin", 0},
  {"flames", 0, 20, "Flames. keep em to This Board Only", "files/news/flames", 0},
  {"songs", 0, 20, "Songs, Poems, Things of that nature", "files/news/songs", 0},
  {"humor", 0, 20, "Humor, jokes, funny stories.", "files/news/humor", 0},
  {"", 0, 0, "", "", 0}
};




/*** io functs ***/

char *load_file_to_string(char *filename)
{
  int fd, len;
  char *loaded;

  fd = open(filename, O_RDONLY);
  if (fd < 0)
  {
    LOGF("error", "failed to load file to string [%s]", filename);
    return (char *) NULL;
  }
  len = lseek(fd, 0, SEEK_END);
  lseek(fd, 0, SEEK_SET);

  loaded = (char *) MALLOC(len + 1);
  if (!loaded)
  {
    close(fd);
    LOGF("error", "failed to malloc in load_file_to_string %d.", len);
    return (char *) NULL;
  }
  memset(loaded, 0, len + 1);
  if (read(fd, loaded, len) < 0)
  {
    close(fd);
    LOGF("error", "failed to read, load_file_to_string [%s]", filename);
    return (char *) NULL;
  }
  close(fd);
  if (sys_flags & VERBOSE)
    LOGF("verbose", "load_file_to_string [%s]", filename);

  return loaded;
}

void sync_news(newsgroup * group, news_header * nh, char *body)
{
  int fd;
  FILE *of;

  /* allows us to just sync header, if no body is passed */
  if (*body)
  {
    sprintf(stack, "%s/%d.body", group->path, nh->id);
#ifdef BSDISH
    fd = open(stack, O_CREAT | O_WRONLY | S_IRUSR | S_IWUSR);
#else
    fd = open(stack, O_CREAT | O_WRONLY | O_SYNC, S_IRUSR | S_IWUSR);
#endif /* BSDISH */
    if (fd < 0)
    {
      LOGF("error", "sync_news() failed to open "
	   "body file, %s", strerror(errno));
      return;
    }
    write(fd, body, strlen(body));
    close(fd);
  }

  sprintf(stack, "%s/%d.head", group->path, nh->id);
  of = fopen(stack, "w");
  if (!of)
  {
    LOGF("error", "sync_news() failed to open "
	 "head file, %s", strerror(errno));
    return;
  }
  fwrite(nh, sizeof(news_header), 1, of);
  fclose(of);
}

void sync_group_news_headers(newsgroup * group)
{
  news_header *scan;
  int fd;
  char *oldstack = stack;
  char path[160];

  *stack = '\0';

  for (scan = group->top; scan; scan = scan->next)
    sync_news(group, scan, "");

  /* update the .newslist file */
  for (scan = group->top; scan; scan = scan->next)
    stack += sprintf(stack, "%d.head\n", scan->id);
  stack = end_string(stack);

  memset(path, 0, 160);
  sprintf(path, "%s/.newslist", group->path);
#ifdef BSDISH
  fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
#else
  fd = open(path, O_CREAT | O_WRONLY | O_SYNC | O_TRUNC, S_IRUSR | S_IWUSR);
#endif /* BSDISH */
  if (fd < 0)
  {
    LOGF("error", "failed to open .newslist in "
	 "sync_group_news_headers() [%s], %s", group->name,
	 strerror(errno));
    return;
  }

  write(fd, oldstack, strlen(oldstack));
  close(fd);
  stack = oldstack;
}


void sync_all_news_headers(void)
{
  int i;

  if (!(NEWS_HAS_CHANGED))
    return;
  for (i = 0; NewsGroups[i].name[0]; i++)
    sync_group_news_headers(&NewsGroups[i]);

  NEWS_CHANGE_RESET;

}





news_header *load_news_header(newsgroup * group, char *fname)
{
  FILE *ni;
  news_header *nh;


  sprintf(stack, "%s/%s", group->path, fname);
  ni = fopen(stack, "r");
  if (!ni)
  {
    LOGF("error", "failed to fopen() in load_news_article() [%s:%s], %s",
	 group->name, fname, strerror(errno));
    return (news_header *) NULL;
  }
  nh = (news_header *) MALLOC(sizeof(news_header));
  if (!nh)
  {
    LOGF("error", "failed to malloc() in load_news_article() [%s], %s",
	 fname, strerror(errno));
    return (news_header *) NULL;
  }
  fread(nh, sizeof(news_header), 1, ni);
  fclose(ni);

  return nh;
}

void load_all_news_for_group(newsgroup * group)
{
  news_header *nh = 0;
  news_header *pe = 0;
  FILE *lf;
  char li[160];

  /* open the file with news list */
  memset(li, 0, 160);
  sprintf(li, "%s/.newslist", group->path);
  lf = fopen(li, "r");
  if (!lf)
  {
    LOGF("error", "failed to fopen() in load_all_news_for_group()[%s], %s",
	 group->name, strerror(errno));
    return;
  }

  /* read through each line of file and load that header */
  while (fgets(li, 159, lf))
  {
    if (li[strlen(li) - 1] == '\n')
      li[strlen(li) - 1] = '\0';	/* spank off the newlines */

    nh = load_news_header(group, li);
    if (nh)
    {
      if (!(group->top))
	group->top = nh;
      else
	pe->next = nh;
      pe = nh;			/* set this one to the previous article */
    }
    memset(li, 0, 160);
  }
}


void init_news(void)
{
  struct stat sbuf;
  char *oldaction = action;
  char *oldstack = stack;
  int i, fd;

  action = "News Initation";
  for (i = 0; NewsGroups[i].name[0]; i++)
  {
    /* make sure the directory for the group exists */
    if (stat(NewsGroups[i].path, &sbuf) < 0)
    {
      if (mkdir(NewsGroups[i].path, S_IRUSR | S_IWUSR | S_IXUSR) < 0)
      {
	LOGF("error", "failed to create diretory for news [%s], %s",
	     NewsGroups[i].path, strerror(errno));
	continue;
      }
      sprintf(stack, "%s/.newslist", NewsGroups[i].path);
      stack = end_string(stack);
      fd = open(oldstack, O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
      if (fd < 0)
	LOGF("error", "failed to create dummy newslist file [%s], %s",
	     NewsGroups[i].name, strerror(errno));
      else
	close(fd);
      stack = oldstack;

      LOGF("boot", "Created new directory for newsgroup '%s'",
	   NewsGroups[i].name);
    }

    load_all_news_for_group(&NewsGroups[i]);
  }
  action = oldaction;

}


/*** misc functions ***/

void destroy_news(newsgroup * group, news_header * nh)
{
  char path[160];

  NEWS_CHANGED;

  memset(path, 0, 160);
  sprintf(path, "%s/%d.body", group->path, nh->id);
  unlink(path);
  memset(path, 0, 160);
  sprintf(path, "%s/%d.head", group->path, nh->id);
  unlink(path);

  memset(nh, 0, sizeof(news_header));
  FREE(nh);
}


void scan_news(void)
{
  news_header *scan, *current;
  int t, i;
  int d = 0;			/* for sanity, only delete 5 articles per loop */


  t = time(0);

  for (i = 0; NewsGroups[i].name[0]; i++)
  {
    while (NewsGroups[i].top && NewsGroups[i].top->flags & DELETE_ME && d < 5)
    {
      current = NewsGroups[i].top;
      NewsGroups[i].top = NewsGroups[i].top->next;
      destroy_news(&NewsGroups[i], current);
      d++;
    }
    for (scan = NewsGroups[i].top; (d < 5 && scan); scan = scan->next)
    {
      if (((t - (scan->date)) > NEWS_TIMEOUT) &&
          !(scan->flags & STICKY_ARTICLE))
      {
        LOGF("news", "Timeout of %s %s posting, %s", scan->name,
             NewsGroups[i].name, scan->header);
        scan->flags |= DELETE_ME;
      }


      /* keep this the last thing done, so we can have other
         things in the loop set the DELETE_ME flag and handle it promptly
       */
      if (scan->next && scan->next->flags & DELETE_ME)
      {
	NEWS_CHANGED;
	current = scan->next;
	scan->next = scan->next->next;
	destroy_news(&NewsGroups[i], current);
	d++;
      }
    }
  }
}

int remove_all_news(player * p, char *rm_name)
{
  news_header *scan;
  int rmd = 0, i;

  for (i = 0; NewsGroups[i].name[0]; i++)
    for (scan = NewsGroups[i].top; scan; scan = scan->next)
      if (!strcasecmp(scan->name, rm_name))
      {
	scan->flags |= DELETE_ME;
	rmd++;
      }
  return rmd;
}

newsgroup *find_news_group(char *str)
{
  int i;

  for (i = 0; NewsGroups[i].name[0]; i++)
    if (!strcasecmp(str, NewsGroups[i].name))
      if (!NewsGroups[i].required_priv || (current_player &&
		   current_player->residency & NewsGroups[i].required_priv))
	return &NewsGroups[i];

  return (newsgroup *) NULL;
}

int find_news_group_number(newsgroup * group)
{
  int i;

  for (i = 0; NewsGroups[i].name[0]; i++)
    if (&NewsGroups[i] == group)
      return i;

  LOGF("error", "news group not in NewsGroups array?!? [%s]", group->name);
  return -1;
}


int count_player_postings(player * p, newsgroup * group)
{
  int i = 0;
  news_header *scan;

  for (scan = group->top; scan; scan = scan->next)
    if (!strcasecmp(scan->name, p->name))
      i++;
  return i;
}



int get_next_news_id(newsgroup * group)
{
  int lowest = 1;
  news_header *scan;

  scan = group->top;
  while (scan)
  {
    for (scan = group->top; scan; scan = scan->next)
      if (scan->id == lowest)
      {
	lowest++;
	break;
      }
  }
  return lowest;
}

int count_news_articles(newsgroup * group)
{
  news_header *nh = group->top;
  int i = 0;

  while (nh)
  {
    i++;
    nh = nh->next;
  }
  return i;
}

news_header *find_news_article(newsgroup * group, int i)
{
  news_header *scan = group->top;
  int ret = 1;

  for (; (scan && ret < i); ret++, scan = scan->next);

  return scan;
}



char *id_or_not(player * p, news_header * nh)
{
  static char fawn[40];

  memset(fawn, 0, 40);
  if (p->residency & ADMIN)
    sprintf(fawn, "(%d)", nh->id);
  else
    return "";
  return fawn;
}

char *spaces10(int i)
{
  if (i < 10)
    return " ";
  return "";
}

char *spaces_followups(news_header * nh)
{
  switch (nh->followups)
  {
    case 0:
      return "";
    case 1:
      return " ";
    case 2:
      return "  ";
    case 3:
      return "   ";
    case 4:
      return "    ";
    case 5:
      return "     ";
    case 6:
      return "      ";
    case 7:
      return "       ";
    case 8:
      return "        ";
    case 9:
      return "         ";
  }
  return "         ";
}


char *sender_str(player * p, news_header * nh)
{
  static char isat[80];

  if (nh->flags & ANONYMOUS)
  {
    if (p && (p->residency & ADMIN || !strcasecmp(nh->name, p->name)))
      sprintf(isat, "?%s?", nh->name);
    else
      sprintf(isat, "(?????)");
  }
  else
    sprintf(isat, "(%s)", nh->name);
  return isat;
}

char *read_count_string(int i)
{
  static char s[16];

  memset(s, 0, 16);
  sprintf(s, "<%d>", i);
  for (i = strlen(s); i < 5; i++)
    strcat(s, " ");

  return s;
}

int next_unread(player * p, newsgroup * g)
{
  news_header *nh = g->top;
  int i = 0, gn = find_news_group_number(g);

  if (gn < 0)
    return 0;

  while (nh && nh->date > p->news_last[gn])
  {
    nh = nh->next;
    i++;
  }
  return i;
}



void save_str_public(char *filename, char *str)
{
  int fd;

#ifdef BSDISH
  fd = open(filename, O_CREAT | O_WRONLY |
	    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
#else
  fd = open(filename, O_CREAT | O_WRONLY | O_SYNC,
	    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
#endif /* BSDISH */

  if (fd < 0)
  {
    LOGF("error", "save_str() failed to open [%s], %s", filename,
	 strerror(errno));
    return;
  }
  write(fd, str, strlen(str));
  /* make sure we get the right privs ..no matter whut umask */
  fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
  close(fd);
}


/* new version for walling to news inform And enough privs */

void news_wall_priv_but(player * p, char *str, int priv)
{
  player *scan;

  for (scan = flatlist_start; scan; scan = scan->flat_next)
  {
    if (scan->custom_flags & NEWS_INFORM && scan != p &&
	(!priv || scan->residency & priv))
    {
      command_type |= HIGHLIGHT;
      tell_player(scan, str);
      command_type &= ~HIGHLIGHT;
    }
  }
}

int count_new_news(newsgroup * group, int cutoff)
{
  news_header *scan;
  int newcnt = 0;

  for (scan = group->top; scan; scan = scan->next)
    if (cutoff < scan->date)
      newcnt++;

  return newcnt;
}




void new_news_inform(player * p)
{
  int i, h = 0;
  char *oldstack = stack;

  stack += sprintf(stack, " Unread news : ");

  for (i = 0; (NewsGroups[i].name[0] && i < MAX_LAST_NEWS_INTS); i++)
    if (!(NewsGroups[i].required_priv) ||
	p->residency & NewsGroups[i].required_priv)
      if (NewsGroups[i].top && NewsGroups[i].top->date > p->news_last[i])
      {
	h++;
	stack += sprintf(stack, "%s (%d)   ", NewsGroups[i].name,
			 count_new_news(&NewsGroups[i], p->news_last[i]));
      }

  stack += sprintf(stack, "\n");
  stack = end_string(stack);
  if (h)
    tell_player(p, oldstack);
  stack = oldstack;
}



/*** final functs ***/
void quit_news_posting(player * p)
{
  tell_player(p, " Article NOT posted.\n");
  FREE(p->edit_info->misc);
  p->mode &= ~NEWSEDIT;
}

void end_news_posting(player * p)
{
  news_header *nh;
  char *oldstack = stack;
  int ngn;

  if (!*(p->edit_info->buffer))
  {
    quit_news_posting(p);
    return;
  }
  NEWS_CHANGED;

  nh = (news_header *) p->edit_info->misc;

  nh->id = get_next_news_id(nh->group);
  nh->date = time(0);

  /* add it to the linked news list */
  if (nh->group->top)
    nh->next = nh->group->top;
  nh->group->top = nh;

  /* save it to disk */
  sync_news(nh->group, nh, p->edit_info->buffer);

  /* sync the news to disk */
  sync_group_news_headers(nh->group);

  /* inform the player */
  tell_player(p, " Article posted ...\n");

  /* inform the masses */
  if (nh->group != &NewsGroups[0])
    stack += sprintf(stack, " -=*> A new news article has been posted in %s",
		     nh->group->name);
  else
    stack += sprintf(stack, " -=*> A new news article has been posted");

  if (nh->flags & ANONYMOUS)
    stack += sprintf(stack, " anonymously");
  else
    stack += sprintf(stack, " by %s", p->name);

  stack += sprintf(stack, " entitled ...\n      %s^N\n", nh->header);

  stack = end_string(oldstack);
  news_wall_priv_but(p, oldstack, nh->group->required_priv);
  stack = oldstack;

  /* update the players read count, so they dont get it as unread */
  ngn = find_news_group_number(nh->group);
  if (ngn > -1 && ngn < MAX_LAST_NEWS_INTS)
    p->news_last[ngn] = nh->date;

  /* just make a log for whutever */
  LOGF("news", "%s posts in %s [%s]", nh->name,
       nh->group->name, nh->header);

  /* preserve (or not) the players mode */
  if (p->edit_info->input_copy == news_command)
  {
    do_prompt(p, "News Mode >");
    p->mode |= NEWSEDIT;
  }
  else
    p->mode &= ~NEWSEDIT;

}




/*** command functs ***/


/* the news command */

void news_command(player * p, char *str)
{
  if (p->edit_info)
  {
    tell_player(p, " Can't do news commands whilst in the editor or pager.\n");
    return;
  }
  if ((*str == '/') && (p->input_to_fn == news_command))
  {
    match_commands(p, str + 1);
    if (!(p->flags & PANIC) && (p->input_to_fn == news_command))
    {
      do_prompt(p, "News Mode >");
      p->mode |= NEWSEDIT;
    }
    return;
  }
  if (!*str)
  {
    if (p->input_to_fn == news_command)
    {
      tell_player(p, " Format : news <action>\n");
      if (!(p->flags & PANIC) && (p->input_to_fn == news_command))
      {
	do_prompt(p, "News Mode >");
	p->mode |= NEWSEDIT;
      }
      return;
    }
    else
    {
      tell_player(p, " Entering news mode. Use 'end' to leave.\n"
		  " '/<command>' does normal commands.\n");
      p->flags &= ~PROMPT;
      p->input_to_fn = news_command;
    }
  }
  else
    sub_command(p, str, news_list);

  if (!(p->flags & PANIC) && (p->input_to_fn == news_command))
  {
    do_prompt(p, "News Mode >");
    p->mode |= NEWSEDIT;
  }
}

/* exit news mode */

void exit_news_mode(player * p, char *str)
{
  if (p->input_to_fn != news_command)
  {
    tell_player(p, " Idle git! ;)\n");
    return;
  }

  tell_player(p, " Leaving news mode.\n");
  p->input_to_fn = 0;
  p->flags |= PROMPT;
  p->mode &= ~NEWSEDIT;
}

void view_news_commands(player * p, char *str)
{
  view_sub_commands(p, news_list);
}


/* toggle whether someone gets informed of news */

void toggle_news_inform(player * p, char *str)
{
  if (!strcasecmp("off", str))
    p->custom_flags &= ~NEWS_INFORM;
  else if (!strcasecmp("on", str))
    p->custom_flags |= NEWS_INFORM;
  else
    p->custom_flags ^= NEWS_INFORM;

  if (p->custom_flags & NEWS_INFORM)
    tell_player(p, " You will be informed of new news when posted.\n");
  else
    tell_player(p, " You will not be informed of new news when posted.\n");
}

/* toggle seeing unread news postings on login */

void toggle_news_login(player * p, char *str)
{
  if (!strcasecmp("off", str))
    p->custom_flags |= NO_NEW_NEWS_INFORM;
  else if (!strcasecmp("on", str))
    p->custom_flags &= ~NO_NEW_NEWS_INFORM;
  else
    p->custom_flags ^= NO_NEW_NEWS_INFORM;

  if (p->custom_flags & NO_NEW_NEWS_INFORM)
    tell_player(p, " You will not see unread news on login.\n");
  else
    tell_player(p, " On login, you will be informed of unread news.\n");
}

/* new news post */
void post_news(player * p, char *str)
{
  newsgroup *group;
  news_header *nh;
  char *scan, *first, word[80];

  if (!*str)
  {
    tell_player(p, " Format : post [group] <header>\n");
    return;
  }
  strncpy(word, str, 79);	/* get the first word, for checking group */
  for (scan = word; (*scan && *scan != ' '); scan++);
  *scan = '\0';
  if ((group = find_news_group(word)))
  {
    while (*str && *str != ' ')
      str++;			/* cut off the first word */

    if (!*str)			/* they tried to trick us */
    {
      tell_player(p, " Format : news post [group] <header>\n");
      return;
    }
    str++;			/* get past the space we stopped on */
  }
  else
    group = &NewsGroups[0];	/* use the first group, by default for default */


  if ((count_player_postings(p, group) >= group->max) &&
      !(p->residency & ADMIN))
  {
    tell_player(p, " You have posted your maximum amount of articles in that group.\n");
    return;
  }

  /* get a new news_header */
  nh = (news_header *) MALLOC(sizeof(news_header));
  memset(nh, 0, sizeof(news_header));

  /* setup its stuffs */
  strncpy(nh->name, p->name, MAX_NAME - 1);
  strncpy(nh->header, str, MAX_TITLE - 1);

  nh->flags |= NEWS_ARTICLE;
  nh->read_count = 0;
  nh->group = group;

  first = first_char(p);
  scan = strstr(first, "post");
  if (scan && (scan != first_char(p)) && (*(scan - 1) == 'a'))
    nh->flags |= ANONYMOUS;

  tell_player(p, " Now enter the body for the article.\n");
  *stack = 0;
  start_edit(p, MAX_ARTICLE_SIZE, end_news_posting,
	     quit_news_posting, stack, 1);

  if (p->edit_info)
    p->edit_info->misc = (void *) nh;
  else
  {
    LOGF("error", "failed to enter the editor "
	 "in post_news() for %s", p->name);
    FREE(nh);

  }
}

/* follow up an article */

void followup(player * p, char *str)
{
  newsgroup *group;
  news_header *scan, *nh;
  char *oldstack = stack;
  char *newbody, *body, *indent, *ptr, *first, *word;
  int which;


  if (!*str)
  {
    tell_player(p, " Format : news followup [group] #\n");
    return;
  }
  if (isalpha(*str))		/* they want a news group */
  {
    word = next_space(str);
    *word++ = 0;
    group = find_news_group(str);
    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a group '%s'\n", str);
      return;
    }
    which = atoi(word);
  }
  else
  {
    which = atoi(str);
    group = &NewsGroups[0];
  }

  if (which < 1)
  {
    tell_player(p, " Format : news followup [group] #\n");
    return;
  }

  if ((count_player_postings(p, group) >= group->max) &&
      !(p->residency & ADMIN))
  {
    tell_player(p, " You have posted your maximum amount of articles in that group.\n");
    return;
  }

  scan = find_news_article(group, which);
  if (!scan)
  {
    if (group != &NewsGroups[0])
      TELLPLAYER(p, " No such news posting '%d' in the %s group\n",
		 which, group->name);
    else
      TELLPLAYER(p, " No such news posting '%d'\n", which);
    return;
  }

  sprintf(stack, "%s/%d.body", group->path, scan->id);
  stack = end_string(stack);
  body = load_file_to_string(oldstack);
  stack = oldstack;

  if (!body)
  {
    tell_player(p, " Ergs, no body file found for that news posting.\n");
    LOGF("error", "No news body for header, id %d, group %s",
	 scan->id, group->name);
    return;
  }

  body[strlen(body)] = 0;

  nh = (news_header *) MALLOC(sizeof(news_header));
  if (!nh)
  {
    tell_player(p, " Erg, malloc fails, try again ...\n");
    log("error", "malloc failed in news followup");
    return;
  }
  memset(nh, 0, sizeof(news_header));

  /* setup its stuffs */
  strncpy(nh->name, p->name, MAX_NAME - 1);

  nh->flags |= NEWS_ARTICLE;
  nh->read_count = 0;
  nh->group = group;
  nh->followups = scan->followups + 1;


  strncpy(nh->header, str, MAX_TITLE - 1);

  if (strstr(scan->header, "Re: ") == scan->header)
    strncpy(nh->header, scan->header, MAX_TITLE - 1);
  else
  {
    sprintf(stack, "Re: %s", scan->header);
    strncpy(nh->header, stack, MAX_TITLE - 1);
  }
  nh->flags |= NEWS_ARTICLE;

  first = first_char(p);
  ptr = strstr(first, "followup");
  if (ptr && (ptr != first_char(p)) && (*(ptr - 1) == 'a'))
    nh->flags |= ANONYMOUS;



  indent = body;
  newbody = stack;

  if (scan->flags & ANONYMOUS)
    stack += sprintf(stack, "\nFrom anonymous article written on %s ...\n",
		     convert_time(scan->date));
  else
    stack += sprintf(stack, "\nOn %s, %s wrote ...\n",
		     convert_time(scan->date), scan->name);

  while (*indent)
  {
    *stack++ = '>';
    *stack++ = ' ';
    while (*indent && *indent != '\n')
      *stack++ = *indent++;
    *stack++ = '\n';
    indent++;
  }
  *stack++ = '\n';
  *stack++ = 0;

  tell_player(p, " Please trim article as much as possible ...\n");
  start_edit(p, MAX_ARTICLE_SIZE, end_news_posting,
	     quit_news_posting, newbody, 1);
  if (p->edit_info)
    p->edit_info->misc = (void *) nh;
  else
  {
    tell_player(p, " Erg, failed to enter the editor ...\n");
    FREE(nh);
  }
  stack = oldstack;
  FREE(body);
}

/* list news articles */

void list_news(player * p, char *str)
{
  newsgroup *group;
  news_header *scan;
  char *oldstack = stack;
  char middle[80], *which;
  int amt = 0;
  int page, pages;
  int count, ncount = 1;

  if (*str && isalpha(*str))	/* they want a specfic group */
  {
    which = next_space(str);	/* maybe they want a page too */
    *which++ = 0;
    group = find_news_group(str);
    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a group '%s'\n", str);
      return;
    }
  }
  else
  {
    which = str;
    group = &NewsGroups[0];
  }

  amt = count_news_articles(group);

  if (!amt)
  {
    if (group != &NewsGroups[0])
      TELLPLAYER(p, " There appears to be no news in the %s group.\n",
		 group->name);
    else
      tell_player(p, " There doesn't seem to be any news at all.\n");
    return;
  }

  page = atoi(which);
  if (page <= 0)
    page = 1;
  page--;

  pages = (amt - 1) / (TERM_LINES - 2);
  if (page > pages)
    page = pages;

  /* setup info thinger */
  if (group != &NewsGroups[0])
  {
    if (amt == 1)
      sprintf(middle, "1 article in group %s", group->name);
    else
      sprintf(middle, "%d articles in group %s", amt, group->name);
  }
  else
  {
    if (amt == 1)
      strcpy(middle, "There is one news article");
    else
      sprintf(middle, "There are %s articles", number2string(amt));
  }
  pstack_mid(middle);


  /* set our first article to be the top if the page to be viewed */
  count = page * (TERM_LINES - 2);
  for (scan = group->top; count; count--, ncount++)
    scan = scan->next;


  for (count = 0; ((count < (TERM_LINES - 1)) && scan); count++, ncount++)
  {
    stack += sprintf(stack, "%s [%d] %s%s%s%s^N %s\n", id_or_not(p, scan),
	      ncount, read_count_string(scan->read_count), spaces10(ncount),
		 spaces_followups(scan), scan->header, sender_str(p, scan));

    scan = scan->next;
  }
  sprintf(middle, "Page %d of %d", page + 1, pages + 1);
  pstack_mid(middle);

  *stack++ = 0;
  tell_player(p, oldstack);

  stack = oldstack;
}

void sync_news_command(player * p, char *str)
{
  if (!(NEWS_HAS_CHANGED))
  {
    tell_player(p, " No news changes since last sync ...\n");
    return;
  }
  tell_player(p, "Syncing news ...\n");
  sync_all_news_headers();
  tell_player(p, "Done ...\n");
}


void read_article(player * p, char *str)
{
  newsgroup *group;
  news_header *scan;
  int which, t, ngn;
  char *oldstack = stack;
  char *body, lastreader[MAX_INET_ADDR], head[MAX_TITLE], *word;

  if (!*str)
  {
    tell_player(p, " Format : news read [group] #\n");
    return;
  }
  if (isalpha(*str))		/* they want a news group */
  {
    word = next_space(str);
    *word++ = 0;
    group = find_news_group(str);
    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a group '%s'\n", str);
      return;
    }
    which = atoi(word);
  }
  else
  {
    which = atoi(str);
    group = &NewsGroups[0];
  }

  if (which < 1)
  {
    tell_player(p, " Format : news read [group] #\n");
    return;
  }

  scan = find_news_article(group, which);
  if (!scan)
  {
    if (group != &NewsGroups[0])
      TELLPLAYER(p, " No such news posting '%d' in the %s group\n",
		 which, group->name);
    else
      TELLPLAYER(p, " No such news posting '%d'\n", which);
    return;
  }

  sprintf(stack, "%s/%d.body", group->path, scan->id);
  stack = end_string(stack);
  body = load_file_to_string(oldstack);
  stack = oldstack;

  if (!body)
  {
    tell_player(p, " Ergs, no body file found for that news posting.\n");
    LOGF("error", "No news body for header, id %d, group %s",
	 scan->id, group->name);
    return;
  }

  NEWS_CHANGED;

  ngn = find_news_group_number(group);
  if (ngn > -1 && ngn < MAX_LAST_NEWS_INTS)
  {
    if (p->news_last[ngn] < scan->date)
      p->news_last[ngn] = scan->date;
  }


  strncpy(lastreader, scan->lastreader, MAX_INET_ADDR - 1);
  if (!*scan->lastreader || strcasecmp(p->inet_addr, scan->lastreader))
  {
    scan->read_count++;
    strncpy(scan->lastreader, p->inet_addr, MAX_INET_ADDR - 1);
  }

  t = time(0);
  memset(head, 0, MAX_TITLE);
  if (group != &NewsGroups[0])	/* is it the the defaault ? */
  {
    strncpy(head, group->name, MAX_TITLE - 1);	/* put the group up top */
    pstack_mid(head);
  }
  else				/* just a line */
    stack += sprintf(stack, LINE);

  stack += sprintf(stack, "    Subject: %s^N\n", scan->header);

  if (scan->flags & ANONYMOUS)
  {
    if (!strcasecmp(scan->name, p->name))
      stack += sprintf(stack, "    Posted anonymously by you on %s\n",
		       convert_time(scan->date));
    else if (p->residency & ADMIN)
      stack += sprintf(stack, "    Posted anonymously by %s on %s\n",
		       scan->name, convert_time(scan->date));
    else
      stack += sprintf(stack, "    Posted anonymously on %s\n",
		       convert_time(scan->date));
  }
  else
    stack += sprintf(stack, "    Posted by %s on %s\n",
		     scan->name, convert_time(scan->date));
  if (scan->read_count == 1)
    stack += sprintf(stack, "    Article has been read one time.\n");
  else
  {
    stack += sprintf(stack, "    Article has been read %s times.\n",
		     number2string(scan->read_count));
    if (p->residency & (LOWER_ADMIN | ADMIN))
      stack += sprintf(stack, "    Last read by someone from %s.\n",
		       lastreader);
  }
  if (p->residency & (LOWER_ADMIN | ADMIN))
  {
    if (!(scan->flags & STICKY_ARTICLE))
      stack += sprintf(stack, "    Times out in %s.\n",
		       word_time(NEWS_TIMEOUT + (scan->date - t)));
    else
      stack += sprintf(stack, "    Article will never timeout.\n");
  }


  stack += sprintf(stack, LINE);

  sprintf(stack, "%s\n", body);
  stack = end_string(stack);
  pager(p, oldstack);
  stack = oldstack;
  FREE(body);
}

void remove_article(player * p, char *str)
{
  newsgroup *group;
  news_header *nh;
  char *word;
  int which;

  if (!*str)
  {
    tell_player(p, " Format : news remove #\n");
    return;
  }
  if (isalpha(*str))		/* they want a news group */
  {
    word = next_space(str);
    *word++ = 0;
    group = find_news_group(str);
    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a group '%s'\n", str);
      return;
    }
    which = atoi(word);
  }
  else
  {
    which = atoi(str);
    group = &NewsGroups[0];
  }
  if (which < 1)
  {
    tell_player(p, " Format : news remove [group] #\n");
    return;
  }

  nh = find_news_article(group, which);
  if (!nh)
  {
    TELLPLAYER(p, " No such news posting '%d'\n", which);
    return;
  }
  if (strcasecmp(nh->name, p->name) && !(p->residency & ADMIN))
  {
    tell_player(p, " You may only remove posts you yourself have made.\n");
    return;
  }
  NEWS_CHANGED;

  nh->flags |= DELETE_ME;
  if (strcasecmp(nh->name, p->name))
    LOGF("remove", "%s removed news in %s from %s entitled %s", p->name,
	 group->name, nh->name, nh->header);

  tell_player(p, " Article removed ...\n");
}


void news_setsticky_command(player * p, char *str)
{
  newsgroup *group;
  news_header *nh;
  char *word;
  int which;

  if (!*str)
  {
    tell_player(p, " Format : news sticky #\n");
    return;
  }
  if (isalpha(*str))		/* they want a news group */
  {
    word = next_space(str);
    *word++ = 0;
    group = find_news_group(str);
    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a group '%s'\n", str);
      return;
    }
    which = atoi(word);
  }
  else
  {
    which = atoi(str);
    group = &NewsGroups[0];
  }
  if (which < 1)
  {
    tell_player(p, " Format : news sticky [group] #\n");
    return;
  }

  nh = find_news_article(group, which);
  if (!nh)
  {
    TELLPLAYER(p, " No such news posting '%d'\n", which);
    return;
  }
  nh->flags ^= STICKY_ARTICLE;

  if (nh->flags & STICKY_ARTICLE)
    tell_player(p, " Sticky bit set ...\n");
  else
    tell_player(p, " Sticky bit removed ...\n");
}


void list_news_groups(player * p, char *str)
{
  int i;
  char *oldstack = stack;
  char temp[70];

  sprintf(temp, "%s News Groups", get_config_msg("talker_name"));
  pstack_mid(temp);

  for (i = 1; NewsGroups[i].name[0]; i++)
  {
    if (NewsGroups[i].required_priv &&
	!(p->residency & NewsGroups[i].required_priv))
      continue;

    stack += sprintf(stack, "%-20s %s\n", NewsGroups[i].name,
		     NewsGroups[i].desc);
  }
  stack += sprintf(stack, LINE "\n");
  stack = end_string(stack);
  pager(p, oldstack);
  stack = oldstack;
}

void news_checkown_command(player * p, char *str)
{
  newsgroup *group;
  news_header *scan;
  int cnt, i, tot = 0;
  char *oldstack = stack;

  if (!*str || (*str && strcasecmp(str, "all")))
  {
    if (*str)
      group = find_news_group(str);
    else
      group = &NewsGroups[0];

    if (!group)
    {
      TELLPLAYER(p, " There doesn't appear to be a newsgroup '%s'.\n",
		 str);
      return;
    }
    for (scan = group->top, cnt = 1; scan; scan = scan->next, cnt++)
      if (!strcasecmp(scan->name, p->name))
      {
	stack += sprintf(stack, "[%d] %s\n", cnt, scan->header);
	tot++;
      }
    stack = end_string(stack);
    if (tot)
      tell_player(p, oldstack);
    else
      tell_player(p, " You have posted no news ...\n");
    stack = oldstack;
    return;
  }
  for (i = 0; NewsGroups[i].name[0]; i++)
  {
    if (NewsGroups[i].required_priv &&
	!(p->residency & NewsGroups[i].required_priv))
      continue;

    stack += sprintf(stack, "---- %s group\n", NewsGroups[i].name);

    for (scan = NewsGroups[i].top, cnt = 1; scan; scan = scan->next, cnt++)
      if (!strcasecmp(scan->name, p->name))
      {
	stack += sprintf(stack, "[%d] %s\n", cnt, scan->header);
	tot++;
      }
  }

  stack = end_string(stack);
  if (tot)
    pager(p, oldstack);
  else
    tell_player(p, " You have posted no news ...\n");
  stack = oldstack;
}


void remove_all_news_command(player * p, char *str)
{
  int rmd;

  if (!*str)
  {
    tell_player(p, " Format : remove_all_news <player>\n");
    return;
  }
  rmd = remove_all_news(p, str);
  if (rmd)
  {
    NEWS_CHANGED;
    TELLPLAYER(p, " Removed %d articles ...\n", rmd);
    LOGF("remove", "%s removed %d postings of %s", p->name, rmd, str);
  }
  else
    TELLPLAYER(p, " No articles found posted by %s to be removed.\n",
	       str);
}

void news_help(player * p, char *str)
{
  char holder[15];		/* use instead of taking a chance with a constant */

  memset(holder, 0, 15);
  strcpy(holder, "news");
  help(p, holder);
}


 /* lil stuffs for sus shortcutting */
void sus_news_post(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    tell_player(p, " Format : spost <subject>\n");
    return;
  }
  sprintf(stack, "sus %s", str);
  stack = end_string(stack);

  post_news(p, oldstack);
  stack = oldstack;
}

void sus_news_list(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    sprintf(stack, "sus");	/* use stack so we dont muck constants */
    stack = end_string(stack);
    list_news(p, oldstack);
    stack = oldstack;
    return;
  }
  sprintf(stack, "sus %s", str);
  stack = end_string(stack);

  list_news(p, oldstack);
  stack = oldstack;
}

void sus_news_read(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    tell_player(p, " Format : sread #\n");
    return;
  }
  sprintf(stack, "sus %s", str);
  stack = end_string(stack);

  read_article(p, oldstack);
  stack = oldstack;
}

 /* and for admin ... */

void ad_news_post(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    tell_player(p, " Format : adpost <subject>\n");
    return;
  }
  sprintf(stack, "admin %s", str);
  stack = end_string(stack);

  post_news(p, oldstack);
  stack = oldstack;
}

void ad_news_list(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    sprintf(stack, "admin");
    stack = end_string(stack);
    list_news(p, oldstack);
    stack = oldstack;
    return;
  }
  sprintf(stack, "admin %s", str);
  stack = end_string(stack);

  list_news(p, oldstack);
  stack = oldstack;
}

void ad_news_read(player * p, char *str)
{
  char *oldstack = stack;

  if (!*str)
  {
    tell_player(p, " Format : adread #\n");
    return;
  }
  sprintf(stack, "admin %s", str);
  stack = end_string(stack);

  read_article(p, oldstack);
  stack = oldstack;
}




void news_stats(player * p, char *str)
{
  char *oldstack = stack;
  int curcnt, totalcnt = 0, i;

  stack += sprintf(stack, " The NewsGroups array is %d bytes in size.\n",
		   sizeof(NewsGroups));

  for (i = 0; NewsGroups[i].name[0]; i++)
  {
    curcnt = count_news_articles(&NewsGroups[i]);
    stack += sprintf(stack, " Newsgroup %-20s - %6d posting/s, %d bytes\n",
		  NewsGroups[i].name, curcnt, curcnt * sizeof(news_header));

    totalcnt += sizeof(news_header) * curcnt;
  }
  stack += sprintf(stack, " %d bytes is total resident memory used"
		   " by all news headers\n", totalcnt);
  stack += sprintf(stack, " %s til the next news sync",
		   word_time(news_sync));
  stack += sprintf(stack, ", %s interval.\n", word_time(NEWS_SYNC_INTERVAL));


  stack = end_string(stack);
  pager(p, oldstack);
  stack = oldstack;

}

void news_read_next(player * p, char *str)
{
  newsgroup *ng;
  int n;
  char poppy[16];

  if (*str)
  {
    ng = find_news_group(str);
    if (!ng)
    {
      TELLPLAYER(p, " No such news group '%s' ...\n", str);
      return;
    }
  }
  else
    ng = &NewsGroups[0];

  n = next_unread(p, ng);

  if (!n)
  {
    if (ng == &NewsGroups[0])
      tell_player(p, " No more unread articles.\n");
    else
      TELLPLAYER(p, " No more unread articles in '%s' group.\n", str);
    return;
  }
  sprintf(poppy, "%d", n);
  read_article(p, poppy);
}


void nunews_version(void)
{
  stack += sprintf(stack, " -=*> NuNews v0.5 (by phypor) enabled.\n");
}