plan9fox/acme/news/src/news.c
2011-04-14 17:27:24 +00:00

1008 lines
18 KiB
C

/*
* Acme interface to nntpfs.
*/
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <thread.h>
#include "win.h"
#include <ctype.h>
int canpost;
int debug;
int nshow = 20;
int lo; /* next message to look at in dir */
int hi; /* current hi message in dir */
char *dir = "/mnt/news";
char *group;
char *from;
typedef struct Article Article;
struct Article {
Ref;
Article *prev;
Article *next;
Window *w;
int n;
int dead;
int dirtied;
int sayspost;
int headers;
int ispost;
};
Article *mlist;
Window *root;
int
cistrncmp(char *a, char *b, int n)
{
while(n-- > 0){
if(tolower(*a++) != tolower(*b++))
return -1;
}
return 0;
}
int
cistrcmp(char *a, char *b)
{
for(;;){
if(tolower(*a) != tolower(*b++))
return -1;
if(*a++ == 0)
break;
}
return 0;
}
char*
skipwhite(char *p)
{
while(isspace(*p))
p++;
return p;
}
int
gethi(void)
{
Dir *d;
int hi;
if((d = dirstat(dir)) == nil)
return -1;
hi = d->qid.vers;
free(d);
return hi;
}
char*
fixfrom(char *s)
{
char *r, *w;
s = estrdup(s);
/* remove quotes */
for(r=w=s; *r; r++)
if(*r != '"')
*w++ = *r;
*w = '\0';
return s;
}
char *day[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", };
char *mon[] = {
"Jan", "Feb", "Mar", "Apr",
"May", "Jun", "Jul", "Aug",
"Sep", "Oct", "Nov", "Dec",
};
char*
fixdate(char *s)
{
char *f[10], *m, *t, *wd, tmp[40];
int d, i, j, nf, hh, mm;
nf = tokenize(s, f, nelem(f));
wd = nil;
d = 0;
m = nil;
t = nil;
for(i=0; i<nf; i++){
for(j=0; j<7; j++)
if(cistrncmp(day[j], f[i], 3)==0)
wd = day[j];
for(j=0; j<12; j++)
if(cistrncmp(mon[j], f[i], 3)==0)
m = mon[j];
j = atoi(f[i]);
if(1 <= j && j <= 31 && d != 0)
d = j;
if(strchr(f[i], ':'))
t = f[i];
}
if(d==0 || wd==nil || m==nil || t==nil)
return nil;
hh = strtol(t, 0, 10);
mm = strtol(strchr(t, ':')+1, 0, 10);
sprint(tmp, "%s %d %s %d:%.2d", wd, d, m, hh, mm);
return estrdup(tmp);
}
void
msgheadline(Biobuf *bin, int n, Biobuf *bout)
{
char *p, *q;
char *date;
char *from;
char *subject;
date = nil;
from = nil;
subject = nil;
while(p = Brdline(bin, '\n')){
p[Blinelen(bin)-1] = '\0';
if((q = strchr(p, ':')) == nil)
continue;
*q++ = '\0';
if(cistrcmp(p, "from")==0)
from = fixfrom(skipwhite(q));
else if(cistrcmp(p, "subject")==0)
subject = estrdup(skipwhite(q));
else if(cistrcmp(p, "date")==0)
date = fixdate(skipwhite(q));
}
Bprint(bout, "%d/\t%s", n, from ? from : "");
if(date)
Bprint(bout, "\t%s", date);
if(subject)
Bprint(bout, "\n\t%s", subject);
Bprint(bout, "\n");
free(date);
free(from);
free(subject);
}
/*
* Write the headers for at most nshow messages to b,
* starting with hi and working down to lo.
* Return number of first message not scanned.
*/
int
adddir(Biobuf *body, int hi, int lo, int nshow)
{
char *p, *q, tmp[40];
int i, n;
Biobuf *b;
n = 0;
for(i=hi; i>=lo && n<nshow; i--){
sprint(tmp, "%d", i);
p = estrstrdup(dir, tmp);
if(access(p, OREAD) < 0){
free(p);
break;
}
q = estrstrdup(p, "/header");
free(p);
b = Bopen(q, OREAD);
free(q);
if(b == nil)
continue;
msgheadline(b, i, body);
Bterm(b);
n++;
}
return i;
}
/*
* Show the first nshow messages in the window.
* This depends on nntpfs presenting contiguously
* numbered directories, and on the qid version being
* the topmost numbered directory.
*/
void
dirwindow(Window *w)
{
if((hi=gethi()) < 0)
return;
if(w->data < 0)
w->data = winopenfile(w, "data");
fprint(w->ctl, "dirty\n");
winopenbody(w, OWRITE);
lo = adddir(w->body, hi, 0, nshow);
winclean(w);
}
/* return 1 if handled, 0 otherwise */
static int
iscmd(char *s, char *cmd)
{
int len;
len = strlen(cmd);
return strncmp(s, cmd, len)==0 && (s[len]=='\0' || s[len]==' ' || s[len]=='\t' || s[len]=='\n');
}
static char*
skip(char *s, char *cmd)
{
s += strlen(cmd);
while(*s==' ' || *s=='\t' || *s=='\n')
s++;
return s;
}
void
unlink(Article *m)
{
if(mlist == m)
mlist = m->next;
if(m->next)
m->next->prev = m->prev;
if(m->prev)
m->prev->next = m->next;
m->next = m->prev = nil;
}
int mesgopen(char*);
int fillmesgwindow(int, Article*);
Article *newpost(void);
void replywindow(Article*);
void mesgpost(Article*);
/*
* Depends on d.qid.vers being highest numbered message in dir.
*/
void
acmetimer(Article *m, Window *w)
{
Biobuf *b;
Dir *d;
assert(m==nil && w==root);
if((d = dirstat(dir))==nil | hi==d->qid.vers){
free(d);
return;
}
if(w->data < 0)
w->data = winopenfile(w, "data");
if(winsetaddr(w, "0", 0))
write(w->data, "", 0);
b = emalloc(sizeof(*b));
Binit(b, w->data, OWRITE);
adddir(b, d->qid.vers, hi+1, d->qid.vers);
hi = d->qid.vers;
Bterm(b);
free(b);
free(d);
winselect(w, "0,.", 0);
}
int
acmeload(Article*, Window*, char *s)
{
int nopen;
//fprint(2, "load %s\n", s);
nopen = 0;
while(*s){
/* skip directory name */
if(strncmp(s, dir, strlen(dir))==0)
s += strlen(dir);
nopen += mesgopen(s);
if((s = strchr(s, '\n')) == nil)
break;
s = skip(s, "");
}
return nopen;
}
int
acmecmd(Article *m, Window *w, char *s)
{
int n;
Biobuf *b;
//fprint(2, "cmd %s\n", s);
s = skip(s, "");
if(iscmd(s, "Del")){
if(m == nil){ /* don't close dir until messages close */
if(mlist != nil){
ctlprint(mlist->w->ctl, "show\n");
return 1;
}
if(windel(w, 0))
threadexitsall(nil);
return 1;
}else{
if(windel(w, 0))
m->dead = 1;
return 1;
}
}
if(m==nil && iscmd(s, "More")){
s = skip(s, "More");
if(n = atoi(s))
nshow = n;
if(w->data < 0)
w->data = winopenfile(w, "data");
winsetaddr(w, "$", 1);
fprint(w->ctl, "dirty\n");
b = emalloc(sizeof(*b));
Binit(b, w->data, OWRITE);
lo = adddir(b, lo, 0, nshow);
Bterm(b);
free(b);
winclean(w);
winsetaddr(w, ".,", 0);
}
if(m!=nil && !m->ispost && iscmd(s, "Headers")){
m->headers = !m->headers;
fillmesgwindow(-1, m);
return 1;
}
if(iscmd(s, "Newpost")){
m = newpost();
winopenbody(m->w, OWRITE);
Bprint(m->w->body, "%s\nsubject: \n\n", group);
winclean(m->w);
winselect(m->w, "$", 0);
return 1;
}
if(m!=nil && !m->ispost && iscmd(s, "Reply")){
replywindow(m);
return 1;
}
// if(m!=nil && iscmd(s, "Replymail")){
// fprint(2, "no replymail yet\n");
// return 1;
// }
if(iscmd(s, "Post")){
mesgpost(m);
return 1;
}
return 0;
}
void
acmeevent(Article *m, Window *w, Event *e)
{
Event *ea, *e2, *eq;
char *s, *t, *buf;
int na;
//int n;
//ulong q0, q1;
switch(e->c1){ /* origin of action */
default:
Unknown:
fprint(2, "unknown message %c%c\n", e->c1, e->c2);
break;
case 'T': /* bogus timer event! */
acmetimer(m, w);
break;
case 'F': /* generated by our actions; ignore */
break;
case 'E': /* write to body or tag; can't affect us */
break;
case 'K': /* type away; we don't care */
if(m && (e->c2 == 'I' || e->c2 == 'D')){
m->dirtied = 1;
if(!m->sayspost){
wintagwrite(w, "Post ", 5);
m->sayspost = 1;
}
}
break;
case 'M': /* mouse event */
switch(e->c2){ /* type of action */
case 'x': /* mouse: button 2 in tag */
case 'X': /* mouse: button 2 in body */
ea = nil;
//e2 = nil;
s = e->b;
if(e->flag & 2){ /* null string with non-null expansion */
e2 = recvp(w->cevent);
if(e->nb==0)
s = e2->b;
}
if(e->flag & 8){ /* chorded argument */
ea = recvp(w->cevent); /* argument */
na = ea->nb;
recvp(w->cevent); /* ignore origin */
}else
na = 0;
/* append chorded arguments */
if(na){
t = emalloc(strlen(s)+1+na+1);
sprint(t, "%s %s", s, ea->b);
s = t;
}
/* if it's a known command, do it */
/* if it's a long message, it can't be for us anyway */
// DPRINT(2, "exec: %s\n", s);
if(!acmecmd(m, w, s)) /* send it back */
winwriteevent(w, e);
if(na)
free(s);
break;
case 'l': /* mouse: button 3 in tag */
case 'L': /* mouse: button 3 in body */
//buf = nil;
eq = e;
if(e->flag & 2){
e2 = recvp(w->cevent);
eq = e2;
}
s = eq->b;
if(eq->q1>eq->q0 && eq->nb==0){
buf = emalloc((eq->q1-eq->q0)*UTFmax+1);
winread(w, eq->q0, eq->q1, buf);
s = buf;
}
if(!acmeload(m, w, s))
winwriteevent(w, e);
break;
case 'i': /* mouse: text inserted in tag */
case 'd': /* mouse: text deleted from tag */
break;
case 'I': /* mouse: text inserted in body */
case 'D': /* mouse: text deleted from body */
if(m == nil)
break;
m->dirtied = 1;
if(!m->sayspost){
wintagwrite(w, "Post ", 5);
m->sayspost = 1;
}
break;
default:
goto Unknown;
}
}
}
void
dirthread(void *v)
{
Event *e;
Window *w;
w = v;
while(e = recvp(w->cevent))
acmeevent(nil, w, e);
threadexitsall(nil);
}
void
mesgthread(void *v)
{
Event *e;
Article *m;
m = v;
while(!m->dead && (e = recvp(m->w->cevent)))
acmeevent(m, m->w, e);
//fprint(2, "msg %p exits\n", m);
unlink(m);
free(m->w);
free(m);
threadexits(nil);
}
/*
Xref: news.research.att.com comp.os.plan9:7360
Newsgroups: comp.os.plan9
Path: news.research.att.com!batch0!uunet!ffx.uu.net!finch!news.mindspring.net!newsfeed.mathworks.com!fu-berlin.de!server1.netnews.ja.net!hgmp.mrc.ac.uk!pegasus.csx.cam.ac.uk!bath.ac.uk!ccsdhd
From: Stephen Adam <saadam@bigpond.com>
Subject: Future of Plan9
Approved: plan9mod@bath.ac.uk
X-Newsreader: Microsoft Outlook Express 5.00.2014.211
X-Mimeole: Produced By Microsoft MimeOLE V5.00.2014.211
Sender: ccsdhd@bath.ac.uk (Dennis Davis)
Nntp-Posting-Date: Wed, 13 Dec 2000 21:28:45 EST
NNTP-Posting-Host: 203.54.121.233
Organization: Telstra BigPond Internet Services (http://www.bigpond.com)
X-Date: Wed, 13 Dec 2000 20:43:37 +1000
Lines: 12
Message-ID: <xbIZ5.157945$e5.114349@newsfeeds.bigpond.com>
References: <95pghu$3lf$1@news.fas.harvard.edu> <95ph36$3m9$1@news.fas.harvard.edu> <slrn980iic.u5q.mperrin@hcs.harvard.edu> <95png6$4ln$1@news.fas.harvard.edu> <95poqg$4rq$1@news.fas.harvard.edu> <slrn980vh8.2gb.myLastName@is07.fas.harvard.edu> <95q40h$66c$2@news.fas.harvard.edu> <95qjhu$8ke$1@news.fas.harvard.edu> <95riue$bu2$1@news.fas.harvard.edu> <95rnar$cbu$1@news.fas.harvard.edu>
X-Msmail-Priority: Normal
X-Trace: newsfeeds.bigpond.com 976703325 203.54.121.233 (Wed, 13 Dec 2000 21:28:45 EST)
X-Priority: 3
Date: Wed, 13 Dec 2000 10:49:50 GMT
*/
char *skipheader[] =
{
"x-",
"path:",
"xref:",
"approved:",
"sender:",
"nntp-",
"organization:",
"lines:",
"message-id:",
"references:",
"reply-to:",
"mime-",
"content-",
};
int
fillmesgwindow(int fd, Article *m)
{
Biobuf *b;
char *p, tmp[40];
int i, inhdr, copy, xfd;
Window *w;
xfd = -1;
if(fd == -1){
sprint(tmp, "%d/article", m->n);
p = estrstrdup(dir, tmp);
if((xfd = open(p, OREAD)) < 0){
free(p);
return 0;
}
free(p);
fd = xfd;
}
w = m->w;
if(w->data < 0)
w->data = winopenfile(w, "data");
if(winsetaddr(w, ",", 0))
write(w->data, "", 0);
winopenbody(m->w, OWRITE);
b = emalloc(sizeof(*b));
Binit(b, fd, OREAD);
inhdr = 1;
copy = 1;
while(p = Brdline(b, '\n')){
if(Blinelen(b)==1)
inhdr = 0, copy=1;
if(inhdr && !isspace(p[0])){
copy = 1;
if(!m->headers){
if(cistrncmp(p, "from:", 5)==0){
p[Blinelen(b)-1] = '\0';
p = fixfrom(skip(p, "from:"));
Bprint(m->w->body, "From: %s\n", p);
free(p);
copy = 0;
continue;
}
for(i=0; i<nelem(skipheader); i++)
if(cistrncmp(p, skipheader[i], strlen(skipheader[i]))==0)
copy=0;
}
}
if(copy)
Bwrite(m->w->body, p, Blinelen(b));
}
Bterm(b);
free(b);
winclean(m->w);
if(xfd != -1)
close(xfd);
return 1;
}
Article*
newpost(void)
{
Article *m;
char *p, tmp[40];
static int nnew;
m = emalloc(sizeof *m);
sprint(tmp, "Post%d", ++nnew);
p = estrstrdup(dir, tmp);
m->w = newwindow();
proccreate(wineventproc, m->w, STACK);
winname(m->w, p);
wintagwrite(m->w, "Post ", 5);
m->sayspost = 1;
m->ispost = 1;
threadcreate(mesgthread, m, STACK);
if(mlist){
m->next = mlist;
mlist->prev = m;
}
mlist = m;
return m;
}
void
replywindow(Article *m)
{
Biobuf *b;
char *p, *ep, *q, tmp[40];
int fd, copy;
Article *reply;
sprint(tmp, "%d/article", m->n);
p = estrstrdup(dir, tmp);
if((fd = open(p, OREAD)) < 0){
free(p);
return;
}
free(p);
reply = newpost();
winopenbody(reply->w, OWRITE);
b = emalloc(sizeof(*b));
Binit(b, fd, OREAD);
copy = 0;
while(p = Brdline(b, '\n')){
if(Blinelen(b)==1)
break;
ep = p+Blinelen(b);
if(!isspace(*p)){
copy = 0;
if(cistrncmp(p, "newsgroups:", 11)==0){
for(q=p+11; *q!='\n'; q++)
if(*q==',')
*q = ' ';
copy = 1;
}else if(cistrncmp(p, "subject:", 8)==0){
if(!strstr(p, " Re:") && !strstr(p, " RE:") && !strstr(p, " re:")){
p = skip(p, "subject:");
ep[-1] = '\0';
Bprint(reply->w->body, "Subject: Re: %s\n", p);
}else
copy = 1;
}else if(cistrncmp(p, "message-id:", 11)==0){
Bprint(reply->w->body, "References: ");
p += 11;
copy = 1;
}
}
if(copy)
Bwrite(reply->w->body, p, ep-p);
}
Bterm(b);
close(fd);
free(b);
Bprint(reply->w->body, "\n");
winclean(reply->w);
winselect(reply->w, "$", 0);
}
char*
skipbl(char *s, char *e)
{
while(s < e){
if(*s!=' ' && *s!='\t' && *s!=',')
break;
s++;
}
return s;
}
char*
findbl(char *s, char *e)
{
while(s < e){
if(*s==' ' || *s=='\t' || *s==',')
break;
s++;
}
return s;
}
/*
* comma-separate possibly blank-separated strings in line; e points before newline
*/
void
commas(char *s, char *e)
{
char *t;
/* may have initial blanks */
s = skipbl(s, e);
while(s < e){
s = findbl(s, e);
if(s == e)
break;
t = skipbl(s, e);
if(t == e) /* no more words */
break;
/* patch comma */
*s++ = ',';
while(s < t)
*s++ = ' ';
}
}
void
mesgpost(Article *m)
{
Biobuf *b;
char *p, *ep;
int isfirst, ishdr, havegroup, havefrom;
p = estrstrdup(dir, "post");
if((b = Bopen(p, OWRITE)) == nil){
fprint(2, "cannot open %s: %r\n", p);
free(p);
return;
}
free(p);
winopenbody(m->w, OREAD);
ishdr = 1;
isfirst = 1;
havegroup = havefrom = 0;
while(p = Brdline(m->w->body, '\n')){
ep = p+Blinelen(m->w->body);
if(ishdr && p+1==ep){
if(!havegroup)
Bprint(b, "Newsgroups: %s\n", group);
if(!havefrom)
Bprint(b, "From: %s\n", from);
ishdr = 0;
}
if(ishdr){
ep[-1] = '\0';
if(isfirst && strchr(p, ':')==0){ /* group list */
commas(p, ep);
Bprint(b, "newsgroups: %s\n", p);
havegroup = 1;
isfirst = 0;
continue;
}
if(cistrncmp(p, "newsgroup:", 10)==0){
commas(skip(p, "newsgroup:"), ep);
Bprint(b, "newsgroups: %s\n", skip(p, "newsgroup:"));
havegroup = 1;
continue;
}
if(cistrncmp(p, "newsgroups:", 11)==0){
commas(skip(p, "newsgroups:"), ep);
Bprint(b, "newsgroups: %s\n", skip(p, "newsgroups:"));
havegroup = 1;
continue;
}
if(cistrncmp(p, "from:", 5)==0)
havefrom = 1;
ep[-1] = '\n';
}
Bwrite(b, p, ep-p);
}
winclosebody(m->w);
Bflush(b);
if(write(Bfildes(b), "", 0) == 0)
winclean(m->w);
else
fprint(2, "post: %r\n");
Bterm(b);
}
int
mesgopen(char *s)
{
char *p, tmp[40];
int fd, n;
Article *m;
n = atoi(s);
if(n==0)
return 0;
for(m=mlist; m; m=m->next){
if(m->n == n){
ctlprint(m->w->ctl, "show\n");
return 1;
}
}
sprint(tmp, "%d/article", n);
p = estrstrdup(dir, tmp);
if((fd = open(p, OREAD)) < 0){
free(p);
return 0;
}
m = emalloc(sizeof(*m));
m->w = newwindow();
m->n = n;
proccreate(wineventproc, m->w, STACK);
p[strlen(p)-strlen("article")] = '\0';
winname(m->w, p);
if(canpost)
wintagwrite(m->w, "Reply ", 6);
wintagwrite(m->w, "Headers ", 8);
free(p);
if(mlist){
m->next = mlist;
mlist->prev = m;
}
mlist = m;
threadcreate(mesgthread, m, STACK);
fillmesgwindow(fd, m);
close(fd);
windormant(m->w);
return 1;
}
void
usage(void)
{
fprint(2, "usage: News [-d /mnt/news] comp.os.plan9\n");
exits("usage");
}
void
timerproc(void *v)
{
Event e;
Window *w;
memset(&e, 0, sizeof e);
e.c1 = 'T';
w = v;
for(;;){
sleep(60*1000);
sendp(w->cevent, &e);
}
}
char*
findfrom(void)
{
char *p, *u;
Biobuf *b;
u = getuser();
if(u==nil)
return "glenda";
p = estrstrstrdup("/usr/", u, "/lib/newsfrom");
b = Bopen(p, OREAD);
free(p);
if(b){
p = Brdline(b, '\n');
if(p){
p[Blinelen(b)-1] = '\0';
p = estrdup(p);
Bterm(b);
return p;
}
Bterm(b);
}
p = estrstrstrdup("/mail/box/", u, "/headers");
b = Bopen(p, OREAD);
free(p);
if(b){
while(p = Brdline(b, '\n')){
p[Blinelen(b)-1] = '\0';
if(cistrncmp(p, "from:", 5)==0){
p = estrdup(skip(p, "from:"));
Bterm(b);
return p;
}
}
Bterm(b);
}
return u;
}
void
threadmain(int argc, char **argv)
{
char *p, *q;
Dir *d;
Window *w;
ARGBEGIN{
case 'D':
debug++;
break;
case 'd':
dir = EARGF(usage());
break;
default:
usage();
break;
}ARGEND
if(argc != 1)
usage();
from = findfrom();
group = estrdup(argv[0]); /* someone will be cute */
while(q=strchr(group, '/'))
*q = '.';
p = estrdup(argv[0]);
while(q=strchr(p, '.'))
*q = '/';
p = estrstrstrdup(dir, "/", p);
cleanname(p);
if((d = dirstat(p)) == nil){ /* maybe it is a new group */
if((d = dirstat(dir)) == nil){
fprint(2, "dirstat(%s) fails: %r\n", dir);
threadexitsall(nil);
}
if((d->mode&DMDIR)==0){
fprint(2, "%s not a directory\n", dir);
threadexitsall(nil);
}
free(d);
if((d = dirstat(p)) == nil){
fprint(2, "stat %s: %r\n", p);
threadexitsall(nil);
}
}
if((d->mode&DMDIR)==0){
fprint(2, "%s not a directory\n", dir);
threadexitsall(nil);
}
free(d);
dir = estrstrdup(p, "/");
q = estrstrdup(dir, "post");
canpost = access(q, AWRITE)==0;
w = newwindow();
root = w;
proccreate(wineventproc, w, STACK);
proccreate(timerproc, w, STACK);
winname(w, dir);
if(canpost)
wintagwrite(w, "Newpost ", 8);
wintagwrite(w, "More ", 5);
dirwindow(w);
threadcreate(dirthread, w, STACK);
threadexits(nil);
}