version 0.2
[irssistats] / irssistats.c
1 /* Usage: cat /path/to/file.log | ./irssistats channel maintainer language theme > /path/to/file.html */
2
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <time.h>
6 #include <string.h>
7
8 /* Config */
9 #define MAXUSERS 2000
10 #define MAXNICKLENGTH 50
11 #define MAXLINELENGTH 2000
12 #define MAXQUOTELENGTH 100
13 #define NBUSERS 50
14 #define NBURLS 5
15 #define NBTOPICS 5
16 #define NBWORDS 20
17 #define MINWORDLENGTH 5
18
19 /* irssistats */
20 #define VERSION "0.2"
21 #define URL "http://royale.zerezo.com/programmation/irssistats/"
22
23 /* Languages */
24 #define NBLANGUAGES 2
25 #define NBKEYS 22
26 char *keys[NBLANGUAGES][NBKEYS+1][2]= /* first key used for language name and abbreviation */
27 {
28   { /* English language */
29     { "English",     "en" },
30     { "HEADER",      "Statistics for #%s by %s" },
31     { "LEGEND",      "Legend" },
32     { "LASTDAYS",    "Lastdays statistics" },
33     { "TOPHOURS",    "Hourly statistics" },
34     { "TOPUSERS",    "Most active people" },
35     { "OTHERS",      "There are %d left not ranked..." },
36     { "NBLINES",     "lines" },
37     { "NICK",        "nick" },
38     { "AVGLETTERS",  "letters/lines" },
39     { "HOURS",       "hours" },
40     { "QUOTE",       "random message" },
41     { "RANDTOPICS",  "Some topics" },
42     { "CHANGEDBY",   "changed by" },
43     { "NEWTOPIC",    "new topic" },
44     { "RANDURLS",    "Some URLs" },
45     { "POSTEDBY",    "posted by" },
46     { "POSTEDURL",   "URL" },
47     { "TOPWORDS",    "Most used words" },
48     { "WORD",        "word" },
49     { "OCCURRENCES", "occurrences" },
50     { "TIME",        "%d lines parsed in %d seconds" },
51     { "FOOTER",      "Statistics generated by" }
52   },
53   { /* French language */
54     { "Fran├žais",    "fr" },
55     { "HEADER",      "Statistiques de #%s par %s" },
56     { "LEGEND",      "L&eacute;gende" },
57     { "LASTDAYS",    "Statistiques des derniers jours" },
58     { "TOPHOURS",    "Statistiques horaires" },
59     { "TOPUSERS",    "Personnes les plus actives" },
60     { "OTHERS",      "Il reste %d personnes non class&eacute;es..." },
61     { "NBLINES",     "lignes" },
62     { "NICK",        "nick" },
63     { "AVGLETTERS",  "lettres/lignes" },
64     { "HOURS",       "heures" },
65     { "QUOTE",       "message al&eacute;atoire" },
66     { "RANDTOPICS",  "Quelques topics" },
67     { "CHANGEDBY",   "chang&eacute; par" },
68     { "NEWTOPIC",    "nouveau topic" },
69     { "RANDURLS",    "Quelques URLs" },
70     { "POSTEDBY",    "post&eacute;e par" },
71     { "POSTEDURL",   "URL" },
72     { "TOPWORDS",    "Mots les plus utilis&eacute;s" },
73     { "WORD",        "mot" },
74     { "OCCURRENCES", "occurrences" },
75     { "TIME",        "%d lignes trait&eacute;es en %d secondes" },
76     { "FOOTER",      "Statistiques g&eacute;n&eacute;r&eacute;es par" }
77   }
78 };
79
80 int language;
81
82 char *L(char *key)
83 {
84   int i;
85   for (i=1;i<=NBKEYS;i++) if (strcmp(key,keys[language][i][0])==0) return(keys[language][i][1]);
86   fprintf(stderr,"unknown language key: %s\n",key);
87   return("");
88 }
89
90 /* Themes */
91 #define NBTHEMES 3
92 #define NBCOLORS 9
93 char *colors[NBTHEMES][NBCOLORS+1][2]= /* first key used for theme name/description and abbreviation */
94 {
95   { /* Default theme */
96     { "White background", "default" },
97     { "BGCOLOR", "#FFFFFF" },
98     { "TEXT",    "#000000" },
99     { "LINK",    "#0000EE" },
100     { "VLINK",   "#551A8B" },
101     { "ALINK",   "#FF0000" },
102     { "TITLE1",  "#000000" },
103     { "TITLE2",  "#000000" },
104     { "BGTABLE", "#EEEEEE" },
105     { "BGTITLE", "#FFEEEE" }
106   },
107   { /* Dark theme */
108     { "Black background", "dark" },
109     { "BGCOLOR", "#000000" },
110     { "TEXT",    "#FFFFFF" },
111     { "LINK",    "#AAAAFF" },
112     { "VLINK",   "#CCCCDD" },
113     { "ALINK",   "#FFAAAA" },
114     { "TITLE1",  "#AAAAFF" },
115     { "TITLE2",  "#FFAAAA" },
116     { "BGTABLE", "#225522" },
117     { "BGTITLE", "#552222" }
118   },
119   { /* zeRezo theme */
120     { "Green theme...", "zerezo" },
121     { "BGCOLOR", "#000000" },
122     { "TEXT",    "#FFFFFF" },
123     { "LINK",    "#14F024" },
124     { "VLINK",   "#0CBA18" },
125     { "ALINK",   "#FFFFFF" },
126     { "TITLE1",  "#0CBA18" },
127     { "TITLE2",  "#84DB8C" },
128     { "BGTABLE", "#085D10" },
129     { "BGTITLE", "#0B810B" }
130   }
131 };
132
133 int theme;
134
135 char *T(char *color)
136 {
137   int i;
138   for (i=1;i<=NBCOLORS;i++) if (strcmp(color,colors[theme][i][0])==0) return(colors[theme][i][1]);
139   fprintf(stderr,"unknown theme color: %s\n",color);
140   return("");
141 }
142
143 /* Variables */
144
145 char *channel;
146 char *maintainer;
147
148 struct 
149 {
150   char nick[MAXNICKLENGTH];
151   int lines;
152   int letters;
153   int hours[4];
154   char quote[MAXLINELENGTH];
155 } users[MAXUSERS];
156 int nbusers=0;
157
158 struct
159 {
160   char nick[MAXNICKLENGTH];
161   char url[MAXLINELENGTH];
162 } urls[5];
163 int nburls=0;
164
165 struct
166
167   char nick[MAXNICKLENGTH];
168   char topic[MAXLINELENGTH];
169 } topics[5];
170 int nbtopics=0;
171
172 struct
173 {
174   int lines;
175   int hours[4];
176 } lastdays[31];
177 int days=0;
178
179 int hours[24];
180 int lines=0;
181
182 struct letter
183 {
184   int nb;
185   struct letter *next[26];
186 } words;
187
188 struct
189 {
190   int nb;
191   char word[MAXLINELENGTH];
192 } topwords[NBWORDS];
193
194 #define isletter(c) (((c>='a')&&(c<='z'))||((c>='A')&&(c<='Z')))
195 #define lowercase(c) (((c>='A')&&(c<='Z'))?c-'A'+'a':c)
196 void findwords(char *message)
197 {
198   int i,c;
199   struct letter *pos,*tmp;
200   for (;;)
201   {
202     while (!isletter(*message)) if (*message=='\0') return; else message++;
203     pos=&words;
204     while (isletter(*message))
205     {
206       c=lowercase(*message)-'a';
207       if ((*pos).next[(int)c]==NULL)
208       {
209         tmp=malloc(sizeof(struct letter));
210         (*tmp).nb=0;
211         for (i=0;i<26;i++) (*tmp).next[i]=NULL;
212         (*pos).next[(int)c]=tmp;
213       }
214       pos=(*pos).next[(int)c];
215       message++; 
216     }
217     (*pos).nb++;
218   }
219   return;
220 }
221
222 char tempword[MAXLINELENGTH];
223 void bestwords(struct letter pos,int cur)
224 {
225   int i,j;
226   if ((cur>=MINWORDLENGTH)&&(pos.nb>topwords[NBWORDS-1].nb))
227   {
228     for (i=0;pos.nb<topwords[i].nb;i++);
229     for (j=NBWORDS-1;j>i;j--)
230     {
231       topwords[j].nb=topwords[j-1].nb;
232       strcpy(topwords[j].word,topwords[j-1].word);
233     }
234     topwords[i].nb=pos.nb;
235     strcpy(topwords[i].word,tempword);
236   }
237   for (i=0;i<26;i++) if (pos.next[i]!=NULL)
238   {
239     tempword[cur]='a'+i;
240     bestwords(*(pos.next[i]),cur+1);
241   }
242   tempword[cur]='\0';
243 }
244
245 void unhtml(char *string) /* replace < and > by { and } */
246 {
247   while (*string!='\0')
248   {
249     if (*string=='<') *string='{';
250     if (*string=='>') *string='}';
251     string++;
252   }
253   return;
254 }
255
256 int dichotomic(char *nick)
257 {
258   int i,j,start=0,end=nbusers-1,middle;
259   while (start<=end)
260   {
261     middle=(start+end)/2;
262     if (strcmp(nick,users[middle].nick)>0) start=middle+1; else end=middle-1;
263   }
264   if (strcmp(nick,users[start].nick)!=0)
265   {
266     nbusers++;
267     if (nbusers>=MAXUSERS) { fprintf(stderr,"too many users\n"); exit(1); }
268     for (i=nbusers-1;i>start;i--)
269     {
270       strcpy(users[i].nick,users[i-1].nick);
271       users[i].lines=users[i-1].lines;
272       users[i].letters=users[i-1].letters;
273       for (j=0;j<4;j++) users[i].hours[j]=users[i-1].hours[j];
274       strcpy(users[i].quote,users[i-1].quote);
275     }
276     strcpy(users[start].nick,nick);
277     users[start].lines=0;
278     users[start].letters=0;
279     for (j=0;j<4;j++) users[start].hours[j]=0;
280     users[start].quote[0]='\0';
281   }
282   return(start);
283 }
284
285 int main(int argc,char *argv[])
286 {
287   int i,j;
288   int max,user,hour;
289   time_t debut;
290   int totallines=0;
291   int pos=0;
292   char c;
293   char *nick,*message;
294   char line[MAXLINELENGTH];
295   
296   /*** INIT ***/
297   if (argc!=5)
298   {
299     fprintf(stderr,"Usage: cat /path/to/file.log | ./irssistats channel maintainer language theme > /path/to/file.html\n\n");
300     fprintf(stderr,"Supported languages :\n");
301     for (i=0;i<NBLANGUAGES;i++) fprintf(stderr,"%s = %s\n",keys[i][0][1],keys[i][0][0]);
302     fprintf(stderr,"\nSupported themes :\n");
303     for (i=0;i<NBTHEMES;i++) fprintf(stderr,"%s = %s\n",colors[i][0][1],colors[i][0][0]);
304     return(1);
305   }
306   channel=argv[1];
307   maintainer=argv[2];
308   for (i=0;i<NBLANGUAGES;i++) if (strcmp(argv[3],keys[i][0][1])==0) { language=i; break; }
309   if (i==NBLANGUAGES)
310   {
311     fprintf(stderr,"Invalid language : %s\n",argv[3]);
312     return(1);
313   }
314   for (i=0;i<NBTHEMES;i++) if (strcmp(argv[4],colors[i][0][1])==0) { theme=i; break; }
315   if (i==NBTHEMES)
316   {
317     fprintf(stderr,"Invalid theme : %s\n",argv[4]);
318     return(1);
319   }
320
321   /*** LOG ***/
322   
323   srand(debut=time(NULL));
324   fprintf(stderr,"working:");
325   while (!feof(stdin))
326   {
327     c=getchar();
328     line[pos++]=c;
329     if (pos>=MAXLINELENGTH) { fprintf(stderr,"line too long\n"); exit(1); }
330     if (c=='\n')
331     {
332       totallines++;
333       if (totallines%10000==0) { fprintf(stderr,"."); fflush(stdout); }
334       if (strncmp("--- Day changed",line,15)==0) /* --- Day changed Wed May 01 2002 */
335       {
336         for (i=30;i>0;i--)
337         {
338           lastdays[i].lines=lastdays[i-1].lines;
339           for (j=0;j<4;j++) lastdays[i].hours[j]=lastdays[i-1].hours[j];
340         }
341         lastdays[0].lines=0;
342         for (j=0;j<4;j++) lastdays[0].hours[j]=0;
343         days++;
344       }
345       else if (strncmp("-!-",&line[6],3)==0) /* 00:00 -!- Nick changed the topic of #channel to: new topic */
346       {
347         for (i=10;line[i]!=' ';i++);
348         line[i]='\0';
349         nick=&line[10];
350         unhtml(nick);
351         message=&line[i+1];
352         unhtml(message);
353         if (strncmp("changed the topic of",message,20)==0)
354         {
355           for (i=21;message[i]!=':';i++);
356           message=&message[i+2];
357           line[pos-1]='\0';
358           nbtopics++;
359           if ((nbtopics<=5) || (rand()%nbtopics==0))
360           {
361             for (i=4;i>0;i--)
362             {
363               strcpy(topics[i].nick,topics[i-1].nick);
364               strcpy(topics[i].topic,topics[i-1].topic);
365             }
366             strcpy(topics[0].nick,nick);
367             strcpy(topics[0].topic,message);
368           }
369         }
370       }
371       else if ((line[6]=='<') || (line[7]=='*'))
372       {
373         line[2]='\0';
374         hour=atoi(line);
375         if (line[7]=='*') /* 00:00  * Nick the message */
376         {
377           for (i=9;line[i]!=' ';i++);
378           nick=&line[9];
379           message=&line[i+1];
380         }
381         else if (line[7]=='>') /* 00:00 <>>>?Nick<<<> the personal message */
382         {
383           for (i=11;line[i]!='<';i++);
384           nick=&line[11];
385           message=&line[i+5];
386         }
387         else /* 00:00 <?Nick> the message */
388         {
389           for (i=8;line[i]!='>';i++);
390           nick=&line[8];
391           message=&line[i+2];
392         }
393         line[i]='\0';
394         line[pos-1]='\0';
395         unhtml(nick);
396         unhtml(message);        
397         i=dichotomic(nick);
398         users[i].lines++;
399         users[i].letters+=strlen(message);
400         users[i].hours[hour/6]++;
401         lastdays[0].lines++;
402         lastdays[0].hours[hour/6]++;
403         lines++;
404         hours[hour]++;
405         if (rand()%users[i].lines==0) strncpy(users[i].quote,message,MAXQUOTELENGTH);
406         if (strncmp("http://",message,7)==0)
407         {
408           for (i=0;(message[i]!=' ') && (i<strlen(message));i++);
409           message[i]='\0';
410           nburls++;
411           if ((nburls<=5) || (rand()%nburls==0))
412           {
413             for (i=4;i>0;i--)
414             {
415               strcpy(urls[i].nick,urls[i-1].nick);
416               strcpy(urls[i].url,urls[i-1].url);
417             }
418             strcpy(urls[0].nick,nick);
419             strcpy(urls[0].url,message);
420           }
421         }
422         findwords(message);
423       }
424       pos=0;
425     }
426   }
427   fprintf(stderr,"done\n");
428
429   bestwords(words,0);
430
431   /*** HTML ***/
432
433   /* header */
434   printf("<!-- Generated by irssistats %s : %s -->\n\n",VERSION,URL);
435   printf("<html>\n\n<head>\n<base target=\"_blank\">\n<title>");
436   printf(L("HEADER"),channel,maintainer);
437   printf("</title>\n</head>\n\n");
438   printf("<body bgcolor=\"%s\" text=\"%s\" link=\"%s\" vlink=\"%s\" alink=\"%s\">\n\n<center>\n\n<font color=\"%s\"><h1>",T("BGCOLOR"),T("TEXT"),T("LINK"),T("VLINK"),T("ALINK"),T("TITLE1"));
439   printf(L("HEADER"),channel,maintainer);
440   printf("</h1></font>\n%s<br>\n<br><br>\n\n",ctime(&debut));
441
442   /* legend */
443   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table bgcolor=%s>\n<tr>\n",T("TITLE2"),L("LEGEND"),T("BGTABLE"));
444   for (i=0;i<4;i++) printf("<td><img src=\"h%d.png\" width=\"40\" height=\"15\"></td><td> : %s %d-%d&nbsp;</td><td width=\"10\"></td>\n",i+1,L("HOURS"),i*6,i*6+5);
445   printf("</tr>\n</table>\n<br><br>\n\n");
446   
447   /* last days */
448   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table>\n<tr>\n",T("TITLE2"),L("LASTDAYS"));
449   max=-1;
450   for (i=30;i>=0;i--) if (lastdays[i].lines>max) max=lastdays[i].lines;
451   for (i=30;i>=0;i--)
452   {
453     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%d</font><br>",lastdays[i].lines);
454     for (j=0;j<4;j++) if (lastdays[i].hours[j]!=0) printf("<img src=\"v%d.png\" width=\"15\" height=\"%d\"><br>",j+1,150*lastdays[i].hours[j]/max);
455     printf("</td>\n");
456   }
457   printf("</tr>\n<tr>\n");
458   for (i=30;i>=0;i--)
459     printf("<td align=\"center\" valign=\"center\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
460   printf("</tr>\n</table>\n<br><br>\n\n");
461   
462   /* top hours */
463   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table>\n<tr>\n",T("TITLE2"),L("TOPHOURS"));
464   max=-1;
465   for (i=0;i<24;i++) if (hours[i]>max) max=hours[i];
466   for (i=0;i<24;i++)
467   {
468     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%.1f%%</font><br>",lines!=0?(float)100*hours[i]/lines:0);
469     if (hours[i]!=0) printf("<img src=\"v%d.png\" width=\"15\" height=\"%d\"><br>",i/6+1,150*hours[i]/max);
470     printf("</td>\n");
471   }
472   printf("</tr>\n<tr>\n");
473   for (i=0;i<24;i++)
474     printf("<td align=\"center\" valign=\"center\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
475   printf("</tr>\n</table>\n<br><br>\n\n");
476   
477   /* top users */
478   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("TOPUSERS"));
479   printf("<table>\n<tr><td></td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\" colspan=\"2\">%s</td><td bgcolor=\"%s\">%s</td>\n",T("BGTITLE"),L("NICK"),T("BGTITLE"),L("NBLINES"),T("BGTITLE"),L("HOURS"),T("BGTITLE"),L("AVGLETTERS"),T("BGTITLE"),L("QUOTE"));
480   for (i=1;i<=NBUSERS;i++)
481   {
482     user=-1;
483     max=-1;
484     for (j=0;j<nbusers;j++) if (users[j].lines>max) max=users[user=j].lines;
485     if (user!=-1)
486     {
487       printf("<tr><td bgcolor=\"%s\" align=\"right\">%d</td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%d</td><td bgcolor=\"%s\">",T("BGTABLE"),i,T("BGTABLE"),users[user].nick,T("BGTABLE"),users[user].lines,T("BGTABLE"));
488       for (j=0;j<4;j++) if (users[user].hours[j]!=0) printf("<img src=\"h%d.png\" width=\"%d\" height=\"15\">",j+1,100*users[user].hours[j]/users[user].lines);
489       printf("</td><td bgcolor=\"%s\">%d</td><td bgcolor=\"%s\"><img src=\"hm.png\" width=\"%d\" height=\"15\"></td><td bgcolor=\"%s\">\"%s\"</td></tr>\n",T("BGTABLE"),users[user].letters/users[user].lines,T("BGTABLE"),users[user].letters/users[user].lines,T("BGTABLE"),users[user].quote);
490       users[user].lines=-1;
491     }    
492   }
493   printf("</table>\n");
494   if (nbusers>NBUSERS)
495   {
496     printf("<br>");
497     printf(L("OTHERS"),nbusers-50);
498     printf("<br>\n");
499   }
500   printf("<br><br>\n\n");
501
502   /* random topics */
503   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("RANDTOPICS"));
504   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("CHANGEDBY"),T("BGTITLE"),L("NEWTOPIC"));
505   for (i=nbtopics<NBTOPICS?nbtopics-1:NBTOPICS-1;i>=0;i--)
506     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"%s\"</td></tr>\n",T("BGTABLE"),topics[i].nick,T("BGTABLE"),topics[i].topic);
507   printf("</table>\n<br><br>\n\n");
508   
509   /* random urls */
510   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("RANDURLS"));
511   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("POSTEDBY"),T("BGTITLE"),L("POSTEDURL"));
512   for (i=nburls<NBURLS?nburls-1:NBURLS-1;i>=0;i--)
513     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"<a href=\"%s\">%s</a>\"</td></tr>\n",T("BGTABLE"),urls[i].nick,T("BGTABLE"),urls[i].url,urls[i].url);
514   printf("</table>\n<br><br>\n\n");
515   
516   /* top words */
517   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("TOPWORDS"));
518   printf("<table>\n<tr><td></td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("WORD"),T("BGTITLE"),L("OCCURRENCES"));
519   for (i=0;i<NBWORDS;i++)
520     if (topwords[i].nb!=0) printf("<tr><td bgcolor=\"%s\" align=\"right\">%d</td><td bgcolor=\"%s\">\"%s\"</td><td bgcolor=\"%s\">%d</td></tr>\n",T("BGTABLE"),i+1,T("BGTABLE"),topwords[i].word,T("BGTABLE"),topwords[i].nb);
521   printf("</table>\n<br><br>\n\n");
522   
523   /* footer */
524   printf(L("TIME"),totallines,(int)(time(NULL)-debut));
525   printf("<br>\n%s <a href=\"%s\">irssistats %s</a>",L("FOOTER"),URL,VERSION);
526   printf("\n\n</center>\n\n</body>\n\n</html>\n");
527   
528   return(0);
529 }