version 0.44
[irssistats] / irssistats.c
1 /* Usage: cat /path/to/file.log | ./irssistats \#channel maintainer language theme [nickfile] > /path/to/file.html */
2
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <time.h>
6 #include <string.h>
7 #include <regex.h>
8
9 /* Options */
10 #define REFRESH_TIME 3600
11 #define W3C_LINK
12
13 /* Config */
14 #define MAXUSERS 10000
15 #define MAXNICKLENGTH 50
16 #define MAXLINELENGTH 2000
17 #define MAXQUOTELENGTH 100
18 #define NBUSERS 50
19 #define NBUSERSTIME 10
20 #define NBURLS 5
21 #define NBTOPICS 5
22 #define NBWORDS 20
23 #define MINWORDLENGTH 5
24
25 /* irssistats */
26 #define VERSION "0.44"
27 #define URL "http://royale.zerezo.com/irssistats/"
28
29 /* Counters */
30 #define D_SMILE     0
31 #define D_FROWN     1
32 #define D_EXCLAM    2
33 #define D_QUESTION  3
34 #define D_ME        4
35 #define D_TOPIC     5
36 #define D_MODE      6
37 #define D_KICK      7
38 #define D_KICKED    8
39 #define D_URL       9
40 #define D_JOIN      10
41 #define D_NICK      11
42 #define D_MONOLOGUE 12
43 #define NBCOUNTERS  13
44 char *counters[NBCOUNTERS]={"C_SMILE","C_FROWN","C_EXCLAM","C_QUESTION","C_ME","C_TOPIC","C_MODE","C_KICK","C_KICKED","C_URL","C_JOIN","C_NICK","C_MONOLOGUE"};
45
46 /* Languages */
47 #define NBLANGUAGES 6
48 #define NBKEYS 38
49 char *keys[NBLANGUAGES][NBKEYS+1][2]= /* first key used for language name and abbreviation */
50 {
51   { /* English language */
52     { "English",      "en" },
53     { "HEADER",       "Statistics for %s by %s" },
54     { "LEGEND",       "Legend" },
55     { "LASTDAYS",     "Lastdays statistics" },
56     { "TOPHOURS",     "Hourly statistics" },
57     { "TOPUSERS",     "Most active people" },
58     { "OTHERS",       "There are %d left not ranked..." },
59     { "NBLINES",      "lines" },
60     { "NICK",         "nick" },
61     { "AVGLETTERS",   "letters/lines" },
62     { "HOURS",        "hours" },
63     { "QUOTE",        "random message" },
64     { "TOPUSERSTIME", "Most active people by time of day" },
65     { "RANDTOPICS",   "Some topics" },
66     { "CHANGEDBY",    "changed by" },
67     { "NEWTOPIC",     "new topic" },
68     { "RANDURLS",     "Some URLs" },
69     { "POSTEDBY",     "posted by" },
70     { "POSTEDURL",    "URL" },
71     { "TOPWORDS",     "Most used words" },
72     { "WORD",         "word" },
73     { "OCCURRENCES",  "occurrences" },
74     { "BIGNUMBERS",   "Some big numbers..." },
75     { "NUMBERS",      "numbers" },
76     { "TIME",         "%d lines (%d days) parsed in %d seconds" },
77     { "FOOTER",       "Statistics generated by" },
78     { "C_SMILE",      "is often happy :)" },
79     { "C_FROWN",      "is often sad :(" },
80     { "C_EXCLAM",     "yells a lot !" },
81     { "C_QUESTION",   "asks a lot of questions ?" },
82     { "C_ME",         "likes /me command" },
83     { "C_TOPIC",      "often changes the topic" },
84     { "C_MODE",       "often changes the modes" },
85     { "C_KICK",       "likes to /kick" },
86     { "C_KICKED",     "is often kicked" },
87     { "C_URL",        "posts many URLs" },
88     { "C_JOIN",       "doesn't know wether to stay or quit" },
89     { "C_NICK",       "often changes his nick" },
90     { "C_MONOLOGUE",  "speaks a lot of monologues" }
91   },
92   { /* French language */
93     { "French",     "fr" },
94     { "HEADER",       "Statistiques de %s par %s" },
95     { "LEGEND",       "L&eacute;gende" },
96     { "LASTDAYS",     "Statistiques des derniers jours" },
97     { "TOPHOURS",     "Statistiques horaires" },
98     { "TOPUSERS",     "Personnes les plus actives" },
99     { "OTHERS",       "Il reste %d personnes non class&eacute;es..." },
100     { "NBLINES",      "lignes" },
101     { "NICK",         "nick" },
102     { "AVGLETTERS",   "lettres/lignes" },
103     { "HOURS",        "heures" },
104     { "QUOTE",        "message al&eacute;atoire" },
105     { "TOPUSERSTIME", "Personnes les plus actives par p&eacute;riode de la journ&eacute;e" },
106     { "RANDTOPICS",   "Quelques topics" },
107     { "CHANGEDBY",    "chang&eacute; par" },
108     { "NEWTOPIC",     "nouveau topic" },
109     { "RANDURLS",     "Quelques URLs" },
110     { "POSTEDBY",     "post&eacute;e par" },
111     { "POSTEDURL",    "URL" },
112     { "TOPWORDS",     "Mots les plus utilis&eacute;s" },
113     { "WORD",         "mot" },
114     { "OCCURRENCES",  "occurrences" },
115     { "BIGNUMBERS",   "Quelques grands nombres..." },
116     { "NUMBERS",      "nombres" },
117     { "TIME",         "%d lignes (%d jours) trait&eacute;es en %d secondes" },
118     { "FOOTER",       "Statistiques g&eacute;n&eacute;r&eacute;es par" },
119     { "C_SMILE",      "est souvent heureux :)" },
120     { "C_FROWN",      "est souvent triste :(" },
121     { "C_EXCLAM",     "hurle beaucoup !" },
122     { "C_QUESTION",   "pose beaucoup de questions ?" },
123     { "C_ME",         "aime la commande /me" },
124     { "C_TOPIC",      "change souvent le topic" },
125     { "C_MODE",       "change souvent les modes" },
126     { "C_KICK",       "aime la commande /kick" },
127     { "C_KICKED",     "est souvent kick&eacute;" },
128     { "C_URL",        "poste beaucoup d'URLs" },
129     { "C_JOIN",       "ne sait pas s'il doit rester ou partir" },
130     { "C_NICK",       "change souvent de nick" },
131     { "C_MONOLOGUE",  "parle beaucoup de monologues" }
132   },
133   { /* German language */
134     /* contributed by Valentin Gelhorn <valentin.gelhorn@web.de> */
135     { "German",       "de" },
136     { "HEADER",       "Statistiken f&uuml;r %s von %s" },
137     { "LEGEND",       "Legende" },
138     { "LASTDAYS",     "Statistik der letzten Tage" },
139     { "TOPHOURS",     "St&uuml;ndliche Statistik" },
140     { "TOPUSERS",     "Die aktivsten Personen" },
141     { "OTHERS",       "Es bleiben noch %d uneingetragene" },
142     { "NBLINES",      "Zeilen" },
143     { "NICK",         "Nick" },
144     { "AVGLETTERS",   "Buchstaben/Zeile" },
145     { "HOURS",        "Stunden" },
146     { "QUOTE",        "Zuf&auml;llig ausgewaehlte Zitate" },
147     { "TOPUSERSTIME", "Die aktivsten Personen zur bestimmten Tageszeit" },
148     { "RANDTOPICS",   "Ein paar Topics" },
149     { "CHANGEDBY",    "Gesetzt von" },
150     { "NEWTOPIC",     "Neues topic" },
151     { "RANDURLS",     "Ein paar URLs" },
152     { "POSTEDBY",     "Geschrieben von" },
153     { "POSTEDURL",    "URL" },
154     { "TOPWORDS",     "Am h&auml;ufigsten benutze W&ouml;rter" },
155     { "WORD",         "Wort" },
156     { "OCCURRENCES",  "Vorkommen" },
157     { "BIGNUMBERS",   "Ein paar grosse Zahlen" },
158     { "NUMBERS",      "Zahlen" },
159     { "TIME",         "%d Zeilen (%d Tage) analysiert in %d Sekunden" },
160     { "FOOTER",       "Statistiken wurden erstellt von" },
161     { "C_SMILE",      "ist oft gl&uuml;klich :)" },
162     { "C_FROWN",      "ist oft traurig :(" },
163     { "C_EXCLAM",     "schreit oft !" },
164     { "C_QUESTION",   "stellt viele Fragen ?" },
165     { "C_ME",         "mag /me'en" },
166     { "C_TOPIC",      "aendert oft das Topico" },
167     { "C_MODE",       "aendert oft die Modes" },
168     { "C_KICK",       "mag /kick'en" },
169     { "C_KICKED",     "wird oft gekickt"},
170     { "C_URL",        "schreibt viele URLs"},
171     { "C_JOIN",       "kann sich nicht entscheiden ob er bleiben oder gehen soll" },
172     { "C_NICK",       "&auml;ndert oft seinen Nick" },
173     { "C_MONOLOGUE",  "spricht oft Monologe" }
174   },
175   { /* Spanish language */
176     /* contributed by Alex <ainaker@gmx.net> */
177     { "Spanish",      "es" },
178     { "HEADER",       "Estad&iacute;sticas de %s por %s" },
179     { "LEGEND",       "Leyenda" },
180     { "LASTDAYS",     "Estad&iacute;sticas de los &uacute;ltimos d&iacute;as" },
181     { "TOPHOURS",     "Estad&iacute;sticas por horas" },
182     { "TOPUSERS",     "Los que m&aacute;s escriben" },
183     { "OTHERS",       "Hay %d m&aacute;s que no llegaron..." },
184     { "NBLINES",      "l&iacute;neas" },
185     { "NICK",         "nick" },
186     { "AVGLETTERS",   "letras por l&iacute;nea" },
187     { "HOURS",        "horas" },
188     { "QUOTE",        "Frase aleatoria" },
189     { "TOPUSERSTIME", "Los que m&aacute;s escriben seg&uacute;n la hora" },
190     { "RANDTOPICS",   "Algunos topics" },
191     { "CHANGEDBY",    "Puestos por" },
192     { "NEWTOPIC",     "topic" },
193     { "RANDURLS",     "Algunas URLs" },
194     { "POSTEDBY",     "puestas por" },
195     { "POSTEDURL",    "URL" },
196     { "TOPWORDS",     "Palabras m&aacute;s usadas" },
197     { "WORD",         "Palabra" },
198     { "OCCURRENCES",  "Frecuencia" },
199     { "BIGNUMBERS",   "Algunos datos..." },
200     { "NUMBERS",      "N&uacute;mero de veces" },
201     { "TIME",         "%d lineas (%d d&iacute;as) procesadas en %d segundos" },
202     { "FOOTER",       "Estad&iacute;sticas generadas por" },
203     { "C_SMILE",      "Suele estar fel&iacute;z :)" },
204     { "C_FROWN",      "Suele estar triste :(" },
205     { "C_EXCLAM",     "Grita mucho !" },
206     { "C_QUESTION",   "Hace muchas preguntas ?" },
207     { "C_ME",         "Abusa del comando /me" },
208     { "C_TOPIC",      "Suele cambiar el topic" },
209     { "C_MODE",       "Cambia a veces los modos del canal" },
210     { "C_KICK",       "Le gusta patear" },
211     { "C_KICKED",     "Es pateado con frecuencia" },
212     { "C_URL",        "Pone muchas URLs" },
213     { "C_JOIN",       "No sabe si irse o quedarse" },
214     { "C_NICK",       "Cambia mucho de nick" },
215     { "C_MONOLOGUE",  "Habla solo" }
216   },
217   { /* Polish language */
218     /* contributed by Piotr Jarmuz <coreupper@yahoo.com> */
219     { "Polish",       "pl" },
220     { "HEADER",       "Statystyki dla %s przez %s" },
221     { "LEGEND",       "Legenda" },
222     { "LASTDAYS",     "Statystyki z ostatnich dni" },
223     { "TOPHOURS",     "Statystyki godzinne" },
224     { "TOPUSERS",     "Najaktywniejsi ludzie" },
225     { "OTHERS",       "Zostalo jeszcze %d nie sklasyfikowanych..." },
226     { "NBLINES",      "linie" },
227     { "NICK",         "nick" },
228     { "AVGLETTERS",   "litery/linie" },
229     { "HOURS",        "godziny" },
230     { "QUOTE",        "przypadkowa wiadomosc" },
231     { "TOPUSERSTIME", "Najaktywniejsi ludzie wedlug czasu dnia" },
232     { "RANDTOPICS",   "Pare tematow" },
233     { "CHANGEDBY",    "zmienione przez" },
234     { "NEWTOPIC",     "nowy temat" },
235     { "RANDURLS",     "Pare URL-i" },
236     { "POSTEDBY",     "wyslane przez" },
237     { "POSTEDURL",    "URL" },
238     { "TOPWORDS",     "Najczestsze slowa" },
239     { "WORD",         "slowo" },
240     { "OCCURRENCES",  "wystapienia" },
241     { "BIGNUMBERS",   "Pare wielkich liczb..." },
242     { "NUMBERS",      "liczby" },
243     { "TIME",         "%d linii (%d dni) sparsowanych w %d sekund" },
244     { "FOOTER",       "Statystyki wygenerowane przez" },
245     { "C_SMILE",      "jest czesto szczesliwy :)" },
246     { "C_FROWN",      "jest czesto smutny :(" },
247     { "C_EXCLAM",     "duzo krzyczy !" },
248     { "C_QUESTION",   "zadaje duzo pytan ?" },
249     { "C_ME",         "lubi /mnie polecenie" },
250     { "C_TOPIC",      "czesto zmienia temat" },
251     { "C_MODE",       "czesto zmienia tryb" },
252     { "C_KICK",       "lubi /kopac" },
253     { "C_KICKED",     "czesto go wykopuja" },
254     { "C_URL",        "wysyla duzo URL-i" },
255     { "C_JOIN",       "nie wie czy zostac czy wyjsc" },
256     { "C_NICK",       "czesto zmienia swojego nicka" },
257     { "C_MONOLOGUE",  "czesto mowi monologiem" }
258   },
259   { /* Finnish language */
260     /* contributed by Antti Huopana <ahuopana@ratol.fi> */
261     { "Finnish",      "fi" },
262     { "HEADER",       "Kanavan %s tilastot - %s" },
263     { "LEGEND",       "Merkkien selitykset" },
264     { "LASTDAYS",     "Viime päivien tilastot" },
265     { "TOPHOURS",     "Tilastot tunneittain" },
266     { "TOPUSERS",     "Aktiivisimmat ihmiset" },
267     { "OTHERS",       "Jäljelle jäi %d joita ei listattu..." },
268     { "NBLINES",      "rivit" },
269     { "NICK",         "nikki" },
270     { "AVGLETTERS",   "kirjainta/rivi" },
271     { "HOURS",        "tunnit" },
272     { "QUOTE",        "satunnainen viesti" },
273     { "TOPUSERSTIME", "Vuorokauden ajan mukaan aktiivisimmat" },
274     { "RANDTOPICS",   "Joitakin aiheita" },
275     { "CHANGEDBY",    "vaihtaja" },
276     { "NEWTOPIC",     "aihe" },
277     { "RANDURLS",     "Joitakin URLeja" },
278     { "POSTEDBY",     "lähettäjä" },
279     { "POSTEDURL",    "URL" },
280     { "TOPWORDS",     "Eniten käytettyjä sanoja" },
281     { "WORD",         "sana" },
282     { "OCCURRENCES",  "käytetty" },
283     { "BIGNUMBERS",   "Joitakin isoja lukuja..." },
284     { "NUMBERS",      "luvut" },
285     { "TIME",         "%d riviä (%d päivää) parsittu %d sekunnissa" },
286     { "FOOTER",       "Tilastot on generoinut" },
287     { "C_SMILE",      "on usein iloinen :)" },
288     { "C_FROWN",      "on usein surullinen :(" },
289     { "C_EXCLAM",     "möykkää paljon !" },
290     { "C_QUESTION",   "kyselee liikaa ?" },
291     { "C_ME",         "pitää itsestään" },
292     { "C_TOPIC",      "vaihtaa usein aihetta" },
293     { "C_MODE",       "haluaa elää muuttuvassa maailmassa" },
294     { "C_KICK",       "pitää potkimisesta" },
295     { "C_KICKED",     "tykkää tulla potkituksi" },
296     { "C_URL",        "surffailee liikaa" },
297     { "C_JOIN",       "ei tiedä ollakko vai eikö olla" },
298     { "C_NICK",       "kärsii identiteettiongelmista" },
299     { "C_MONOLOGUE",  "höpöttää paljon itsekseen" }
300   }
301 };
302
303 int language;
304
305 char *L(char *key)
306 {
307   int i;
308   for (i=1;i<=NBKEYS;i++) if (strcmp(key,keys[language][i][0])==0) return(keys[language][i][1]);
309   fprintf(stderr,"unknown language key: %s\n",key);
310   return("");
311 }
312
313 /* Themes */
314 #define NBTHEMES 6
315 #define NBCOLORS 9
316 char *colors[NBTHEMES][NBCOLORS+1][2]= /* first key used for theme name/description and abbreviation */
317 {
318   { /* Default theme */
319     { "White background", "default" },
320     { "BGCOLOR", "#FFFFFF" },
321     { "TEXT",    "#000000" },
322     { "LINK",    "#0000EE" },
323     { "VLINK",   "#551A8B" },
324     { "ALINK",   "#FF0000" },
325     { "TITLE1",  "#000000" },
326     { "TITLE2",  "#000000" },
327     { "BGTABLE", "#EEEEEE" },
328     { "BGTITLE", "#FFEEEE" }
329   },
330   { /* Dark theme */
331     { "Black background", "dark" },
332     { "BGCOLOR", "#000000" },
333     { "TEXT",    "#FFFFFF" },
334     { "LINK",    "#AAAAFF" },
335     { "VLINK",   "#CCCCDD" },
336     { "ALINK",   "#FFAAAA" },
337     { "TITLE1",  "#AAAAFF" },
338     { "TITLE2",  "#FFAAAA" },
339     { "BGTABLE", "#225522" },
340     { "BGTITLE", "#552222" }
341   },
342   { /* zeRezo theme */
343     { "Green theme...", "zerezo" },
344     { "BGCOLOR", "#000000" },
345     { "TEXT",    "#FFFFFF" },
346     { "LINK",    "#14F024" },
347     { "VLINK",   "#0CBA18" },
348     { "ALINK",   "#FFFFFF" },
349     { "TITLE1",  "#0CBA18" },
350     { "TITLE2",  "#84DB8C" },
351     { "BGTABLE", "#085D10" },
352     { "BGTITLE", "#0B810B" }
353   },
354   { /* tit-namour theme */
355     { "Purple and Pink", "namour" },
356     { "BGCOLOR", "#9933CC" },
357     { "TEXT",    "#DDAAFF" },
358     { "LINK",    "#CC99FF" },
359     { "VLINK",   "#999999" },
360     { "ALINK",   "#FFC8C8" },
361     { "TITLE1",  "#FFC8C8" },
362     { "TITLE2",  "#FFC8C8" },
363     { "BGTABLE", "#7711AA" },
364     { "BGTITLE", "#550088" }
365   },
366   { /* zeDuel theme */
367     { "Orange theme...", "zeduel" },
368     { "BGCOLOR", "#FFFFFF" },
369     { "TEXT",    "#000000" },
370     { "LINK",    "#FF7700" },
371     { "VLINK",   "#C05A00" },
372     { "ALINK",   "#FF9A41" },
373     { "TITLE1",  "#C05A00" },
374     { "TITLE2",  "#FF7700" },
375     { "BGTABLE", "#FFEEEE" },
376     { "BGTITLE", "#FF7700" }
377   },
378   { /* Blue theme */
379     { "Blue theme...", "blue" },
380     { "BGCOLOR", "#FFFFFF" },
381     { "TEXT",    "#000000" },
382     { "LINK",    "#4444FF" },
383     { "VLINK",   "#8888FF" },
384     { "ALINK",   "#CCCCFF" },
385     { "TITLE1",  "#8888FF" },
386     { "TITLE2",  "#AAAAFF" },
387     { "BGTABLE", "#EEEEFF" },
388     { "BGTITLE", "#CCCCFF" }
389   }
390 };
391
392 int theme;
393
394 char *T(char *color)
395 {
396   int i;
397   for (i=1;i<=NBCOLORS;i++) if (strcmp(color,colors[theme][i][0])==0) return(colors[theme][i][1]);
398   fprintf(stderr,"unknown theme color: %s\n",color);
399   return("");
400 }
401
402 /* Variables */
403
404 char *channel;
405 char *maintainer;
406
407 struct
408 {
409   char nick[MAXNICKLENGTH];
410   int lines;
411   int letters;
412   int hours[4];
413   char quote[MAXQUOTELENGTH+1];
414   int counters[NBCOUNTERS];
415   int temp;
416 } users[MAXUSERS];
417 int nbusers=0;
418
419 struct
420 {
421   char nick[MAXNICKLENGTH];
422   char url[MAXLINELENGTH];
423   char shorturl[MAXQUOTELENGTH+1];
424 } urls[5];
425 int nburls=0;
426
427 struct
428
429   char nick[MAXNICKLENGTH];
430   char topic[MAXQUOTELENGTH+1];
431 } topics[5];
432 int nbtopics=0;
433
434 struct
435 {
436   int lines;
437   int hours[4];
438 } lastdays[31];
439 int days=0;
440
441 int hours[24];
442 int lines=0;
443
444 struct letter
445 {
446   int nb;
447   struct letter *next[26];
448 } words;
449
450 struct
451 {
452   int nb;
453   char word[MAXLINELENGTH];
454 } topwords[NBWORDS];
455
456 #define isletter(c) (((c>='a')&&(c<='z'))||((c>='A')&&(c<='Z')))
457 #define lowercase(c) (((c>='A')&&(c<='Z'))?c-'A'+'a':c)
458 void findwords(char *message)
459 {
460   int i,c;
461   struct letter *pos,*tmp;
462   for (;;)
463   {
464     while (!isletter(*message)) if (*message=='\0') return; else message++;
465     pos=&words;
466     while (isletter(*message))
467     {
468       c=lowercase(*message)-'a';
469       if (pos->next[(int)c]==NULL)
470       {
471         tmp=malloc(sizeof(struct letter));
472         tmp->nb=0;
473         for (i=0;i<26;i++) tmp->next[i]=NULL;
474         pos->next[(int)c]=tmp;
475       }
476       pos=pos->next[(int)c];
477       message++; 
478     }
479     pos->nb++;
480   }
481   return;
482 }
483
484 char tempword[MAXLINELENGTH];
485 void bestwords(struct letter pos,int cur)
486 {
487   int i,j;
488   if ((cur>=MINWORDLENGTH)&&(pos.nb>topwords[NBWORDS-1].nb))
489   {
490     for (i=0;pos.nb<topwords[i].nb;i++);
491     for (j=NBWORDS-1;j>i;j--)
492     {
493       topwords[j].nb=topwords[j-1].nb;
494       strcpy(topwords[j].word,topwords[j-1].word);
495     }
496     topwords[i].nb=pos.nb;
497     strcpy(topwords[i].word,tempword);
498   }
499   for (i=0;i<26;i++) if (pos.next[i]!=NULL)
500   {
501     tempword[cur]='a'+i;
502     bestwords(*(pos.next[i]),cur+1);
503   }
504   tempword[cur]='\0';
505 }
506
507 void printhtml(char *string) /* replace < and > by &lt; and &gt; */
508 {
509   while (*string!='\0')
510   {
511     switch (*string)
512     {
513       case '<':printf("&lt;"); break;
514       case '>':printf("&gt;"); break;
515       case '&':printf("&amp;"); break;
516       default:printf("%c",*string); break;
517     }
518     string++;
519   }
520   return;
521 }
522
523 int dichotomic(char *nick)
524 {
525   int i,j,start=0,end=nbusers-1,middle;
526   while (start<=end)
527   {
528     middle=(start+end)/2;
529     if (strcmp(nick,users[middle].nick)>0) start=middle+1; else end=middle-1;
530   }
531   if (strcmp(nick,users[start].nick)!=0)
532   {
533     nbusers++;
534     if (nbusers>=MAXUSERS) { fprintf(stderr,"too many users\n"); exit(1); }
535     for (i=nbusers-1;i>start;i--)
536     {
537       strcpy(users[i].nick,users[i-1].nick);
538       users[i].lines=users[i-1].lines;
539       users[i].letters=users[i-1].letters;
540       for (j=0;j<4;j++) users[i].hours[j]=users[i-1].hours[j];
541       strcpy(users[i].quote,users[i-1].quote);
542       for (j=0;j<NBCOUNTERS;j++) users[i].counters[j]=users[i-1].counters[j];
543       users[i].temp=users[i-1].temp;
544     }
545     strcpy(users[start].nick,nick);
546     users[start].lines=0;
547     users[start].letters=0;
548     for (j=0;j<4;j++) users[start].hours[j]=0;
549     users[start].quote[0]='\0';
550     for (j=0;j<NBCOUNTERS;j++) users[start].counters[j]=0;
551     users[start].temp=0;
552   }
553   return(start);
554 }
555
556 int main(int argc,char *argv[])
557 {
558   int i,j,k;
559   int max,user,temp,hour;
560   int mononick,monolines;
561   time_t debut;
562   int totallines=0;
563   int pos=0;
564   char c;
565   char *nick,*message;
566   char line[MAXLINELENGTH];
567   FILE *fic;
568   regex_t preg;
569   
570   /*** INIT ***/
571   
572   if ((argc<5) || (argc>6))
573   {
574     fprintf(stderr,"Usage: cat /path/to/file.log | ./irssistats \\#channel maintainer language theme [nickfile] > /path/to/file.html\n\n");
575     fprintf(stderr,"Version :\nirssistats %s\n\n",VERSION);
576     fprintf(stderr,"Supported languages :\n");
577     for (i=0;i<NBLANGUAGES;i++) fprintf(stderr,"%s = %s\n",keys[i][0][1],keys[i][0][0]);
578     fprintf(stderr,"\nSupported themes :\n");
579     for (i=0;i<NBTHEMES;i++) fprintf(stderr,"%s = %s\n",colors[i][0][1],colors[i][0][0]);
580     return(1);
581   }
582   channel=argv[1];
583   maintainer=argv[2];
584   for (i=0;i<NBLANGUAGES;i++) if (strcmp(argv[3],keys[i][0][1])==0) { language=i; break; }
585   if (i==NBLANGUAGES)
586   {
587     fprintf(stderr,"Invalid language : %s\n",argv[3]);
588     return(1);
589   }
590   for (i=0;i<NBTHEMES;i++) if (strcmp(argv[4],colors[i][0][1])==0) { theme=i; break; }
591   if (i==NBTHEMES)
592   {
593     fprintf(stderr,"Invalid theme : %s\n",argv[4]);
594     return(1);
595   }
596
597   /*** LOG ***/
598   
599   srand(debut=time(NULL));
600   fprintf(stderr,"working on %s : ",channel);
601   while (!feof(stdin))
602   {
603     c=getchar();
604     line[pos++]=c;
605     if (pos>=MAXLINELENGTH) { fprintf(stderr,"line %d is too long\n",totallines); exit(1); }
606     if (c=='\n')
607     {
608       line[pos-1]='\0';
609       totallines++;
610       if (totallines%10000==0) { fprintf(stderr,"."); fflush(stderr); }
611       if (strncmp("--- Day changed",line,15)==0) /* --- Day changed Wed May 01 2002 */
612       {
613         for (i=30;i>0;i--)
614         {
615           lastdays[i].lines=lastdays[i-1].lines;
616           for (j=0;j<4;j++) lastdays[i].hours[j]=lastdays[i-1].hours[j];
617         }
618         lastdays[0].lines=0;
619         for (j=0;j<4;j++) lastdays[0].hours[j]=0;
620         days++;
621       }
622       else if (strncmp("-!- mode/",&line[6],9)==0) /* 00:00 -!- mode/#channel [...] by (Nick, Nick2, )Nick3 */
623       {
624         for (i=strlen(line);line[i]!=' ';i--);
625         nick=&line[i+1];
626         users[dichotomic(nick)].counters[D_MODE]++;
627       }
628       else if (strncmp("-!-",&line[6],3)==0) /* 00:00 -!- Nick something... */
629       {
630         for (i=10;line[i]!=' ';i++);
631         line[i]='\0';
632         nick=&line[10];
633         message=&line[i+1];
634         if (strncmp("changed the topic of",message,20)==0) /* 00:00 -!- Nick changed the topic of #channel to: new topic */
635         {
636           users[dichotomic(nick)].counters[D_TOPIC]++;
637           for (i=21;message[i]!=':';i++);
638           message=&message[i+2];
639           nbtopics++;
640           if ((nbtopics<=NBTOPICS) || (rand()%(nbtopics/NBTOPICS)==0))
641           {
642             temp=nbtopics<=NBTOPICS?nbtopics-1:rand()%NBTOPICS;
643             strcpy(topics[temp].nick,nick);
644             strncpy(topics[temp].topic,message,MAXQUOTELENGTH);
645           }
646         }
647         else if (strncmp("was kicked from",message,15)==0) /* 00:00 -!- Nick was kicked from #channel by Nick [Reason] */
648         {
649           users[dichotomic(nick)].counters[D_KICKED]++;
650           for (i=16;message[i]!=' ';i++);
651           message=&message[i+4];
652           for (i=0;message[i]!=' ';i++);
653           message[i]='\0';
654           users[dichotomic(message)].counters[D_KICK]++;
655         }
656         else if (strncmp("is now known as",message,15)==0) /* 00:00 -!- Nick is now known as Nick */
657           users[dichotomic(nick)].counters[D_NICK]++;
658         else if (message[0]=='[') /* 00:00 -!- Nick [user@host] something... */
659         {
660           for (i=0;message[i]!=']';i++);
661           message=&message[i+2];
662           if (strncmp("has joined",message,10)==0) /* 00:00 -!- Nick [user@host] has joined #channel */
663             users[dichotomic(nick)].counters[D_JOIN]++;
664           else if (strncmp("has quit",message,8)==0); /* 00:00 -!- Nick [user@host] has quit [Reason] */
665           else if (strncmp("has left",message,8)==0); /* 00:00 -!- Nick [user@host] has left #channel [Reason] */
666           else;
667         }
668       }
669       else if ((line[6]=='<') || (line[7]=='*'))
670       {
671         line[2]='\0';
672         hour=atoi(line);
673         if (line[7]=='*') /* 00:00  * Nick the message */
674         {
675           for (i=9;line[i]!=' ';i++);
676           nick=&line[9];
677           message=&line[i+1];
678         }
679         else if (line[7]=='>') /* 00:00 <>>>?Nick<<<> the personal message */
680                                /* 00:00 <>>?Nick<<> the personal message */
681         {
682           for (i=10;line[i]!='<';i++);
683           nick=&line[10];
684           if (line[9]=='>') nick++;
685           message=&line[i+5];
686         }
687         else /* 00:00 <?Nick> the message */
688         {
689           for (i=8;line[i]!='>';i++);
690           nick=&line[8];
691           message=&line[i+2];
692         }
693         line[i]='\0';
694         i=dichotomic(nick);
695         if (line[7]=='*') users[i].counters[D_ME]++;
696         if (i==mononick)
697         {
698           monolines++;
699           if (monolines==5) users[i].counters[D_MONOLOGUE]++;
700         }
701         else
702         {
703           mononick=i;
704           monolines=1;
705         }
706         j=strlen(message);
707         users[i].lines++;
708         users[i].letters+=j;
709         users[i].hours[hour/6]++;
710         lastdays[0].lines++;
711         lastdays[0].hours[hour/6]++;
712         lines++;
713         hours[hour]++;
714         if (message[j-1]=='?') users[i].counters[D_QUESTION]++;
715         else if (message[j-1]=='!') users[i].counters[D_EXCLAM]++;
716         else if ((message[j-3]==' ')&&(message[j-2]==':'))
717         {
718           if (message[j-1]==')') users[i].counters[D_SMILE]++;
719           else if (message[j-1]=='(') users[i].counters[D_FROWN]++;
720         }
721         if (rand()%users[i].lines==0) strncpy(users[i].quote,message,MAXQUOTELENGTH);
722         if (strncmp("http://",message,7)==0)
723         {
724           users[i].counters[D_URL]++;
725           for (i=0;(message[i]!=' ') && (i<strlen(message));i++);
726           message[i]='\0';
727           nburls++;
728           if ((nburls<=NBURLS) || (rand()%(nburls/NBURLS)==0))
729           {
730             temp=nburls<=NBURLS?nburls-1:rand()%NBURLS;
731             strcpy(urls[temp].nick,nick);
732             strcpy(urls[temp].url,message);
733             strncpy(urls[temp].shorturl,message,MAXQUOTELENGTH);
734           }
735         }
736         findwords(message);
737       }
738       pos=0;
739     }
740   }
741   fprintf(stderr," done\n");
742
743   bestwords(words,0);
744
745   /*** ALIAS ***/
746   
747   if (argc==6)
748   {
749     for (i=0;i<nbusers;i++) users[i].temp=users[i].lines;
750     if ((fic=fopen(argv[5],"rt"))==NULL) { fprintf(stderr,"can't open nick file\n"); exit(1); }
751     while (fscanf(fic,"%s",line)==1)
752     {
753       user=dichotomic(line);
754       fscanf(fic,"%s",line);
755       if (regcomp(&preg,line,0)!=0) { fprintf(stderr,"error in nick file"); exit(1); }
756       for (i=0;i<nbusers;i++) if ((i!=user) && (regexec(&preg,users[i].nick,0,0,0)==0) && (users[i].lines>=0))
757       {
758         if (users[i].temp>users[user].temp) /* for nick alias, keep the random quote of the most used nick */
759         {
760           strcpy(users[user].quote,users[i].quote);
761           users[user].temp=users[i].temp;
762         }
763         users[user].lines+=users[i].lines;
764         users[user].letters+=users[i].letters;
765         for (j=0;j<4;j++) users[user].hours[j]+=users[i].hours[j];
766         for (j=0;j<NBCOUNTERS;j++) users[user].counters[j]+=users[i].counters[j];
767         /* "remove" old user */
768         users[i].lines=-1;
769         users[i].letters=-1;
770         for (j=0;j<4;j++) users[i].hours[j]=-1;
771         for (j=0;j<NBCOUNTERS;j++) users[i].counters[j]=-1;
772       }
773       regfree(&preg);
774     }
775     fclose(fic);
776     /* "remove" the ignored nicks */
777     i=dichotomic("<NULL>");
778     users[i].lines=-1;
779     users[i].letters=-1;
780     for (j=0;j<4;j++) users[i].hours[j]=-1;
781     for (j=0;j<NBCOUNTERS;j++) users[i].counters[j]=-1;
782   }
783   
784   /*** HTML ***/
785
786   /* header */
787   printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n\n");
788   printf("<!-- Generated by irssistats %s : %s -->\n\n",VERSION,URL);
789   printf("<html>\n\n<head>\n<title>");
790   printf(L("HEADER"),channel,maintainer);
791   printf("</title>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">\n");
792 #ifdef REFRESH_TIME
793   printf("<meta http-equiv=\"Refresh\" content=\"%d\">\n",REFRESH_TIME);
794 #endif
795   printf("<base target=\"_blank\">\n</head>\n\n");
796   printf("<body bgcolor=\"%s\" text=\"%s\" link=\"%s\" vlink=\"%s\" alink=\"%s\">\n\n<center>\n\n<font color=\"%s\" size=\"+3\"><b>",T("BGCOLOR"),T("TEXT"),T("LINK"),T("VLINK"),T("ALINK"),T("TITLE1"));
797   printf(L("HEADER"),channel,maintainer);
798   printf("</b></font><br><br>\n%s<br>\n<br><br>\n\n",ctime(&debut));
799
800   /* legend */
801   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n<table bgcolor=\"%s\">\n<tr>\n",T("TITLE2"),L("LEGEND"),T("BGTABLE"));
802   for (i=0;i<4;i++) printf("<td><img src=\"h%d.png\" width=\"40\" height=\"15\" alt=\"\"></td><td> : %s %d-%d</td><td width=\"10\"></td>\n",i+1,L("HOURS"),i*6,i*6+5);
803   printf("</tr>\n</table>\n<br><br>\n\n");
804   
805   /* last days */
806   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n<table>\n<tr>\n",T("TITLE2"),L("LASTDAYS"));
807   max=-1;
808   for (i=30;i>=0;i--) if (lastdays[i].lines>max) max=lastdays[i].lines;
809   for (i=30;i>=0;i--)
810   {
811     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%d</font><br>",lastdays[i].lines);
812     for (j=0;j<4;j++) if (lastdays[i].hours[j]!=0) printf("<img src=\"v%d.png\" width=\"15\" height=\"%d\" alt=\"\"><br>",j+1,150*lastdays[i].hours[j]/max);
813     printf("</td>\n");
814   }
815   printf("</tr>\n<tr>\n");
816   for (i=30;i>=0;i--)
817     printf("<td align=\"center\" valign=\"middle\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
818   printf("</tr>\n</table>\n<br><br>\n\n");
819   
820   /* top hours */
821   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n<table>\n<tr>\n",T("TITLE2"),L("TOPHOURS"));
822   max=-1;
823   for (i=0;i<24;i++) if (hours[i]>max) max=hours[i];
824   for (i=0;i<24;i++)
825   {
826     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%.1f%%</font><br>",lines!=0?(float)100*hours[i]/lines:0);
827     if (hours[i]!=0) printf("<img src=\"v%d.png\" width=\"15\" height=\"%d\" alt=\"\"><br>",i/6+1,150*hours[i]/max);
828     printf("</td>\n");
829   }
830   printf("</tr>\n<tr>\n");
831   for (i=0;i<24;i++)
832     printf("<td align=\"center\" valign=\"middle\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
833   printf("</tr>\n</table>\n<br><br>\n\n");
834   
835   /* top users */
836   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("TOPUSERS"));
837   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"));
838   for (i=1;i<=NBUSERS;i++)
839   {
840     user=-1;
841     max=0;
842     for (j=0;j<nbusers;j++) if (users[j].lines>max) max=users[user=j].lines;
843     if (user!=-1)
844     {
845       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"));
846       for (j=0;j<4;j++) if (users[user].hours[j]!=0) printf("<img src=\"h%d.png\" width=\"%d\" height=\"15\" alt=\"\">",j+1,100*users[user].hours[j]/users[user].lines);
847       printf("</td><td bgcolor=\"%s\">%d</td><td bgcolor=\"%s\"><img src=\"hm.png\" width=\"%d\" height=\"15\" alt=\"\"></td><td bgcolor=\"%s\">\"",T("BGTABLE"),users[user].lines!=0?users[user].letters/users[user].lines:0,T("BGTABLE"),users[user].lines!=0?users[user].letters/users[user].lines:0,T("BGTABLE"));
848       printhtml(users[user].quote);
849       printf("\"</td></tr>\n");
850       users[user].lines=-1;
851     }    
852   }
853   printf("</table>\n");
854   temp=0;
855   for (i=0;i<=nbusers;i++) if (users[i].lines>=0) temp++;
856   if (temp>0)
857   {
858     printf("<br>");
859     printf(L("OTHERS"),temp);
860     printf("<br>\n");
861   }
862   printf("<br><br>\n\n");
863   
864   /* top users by time */
865   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("TOPUSERSTIME"));
866   printf("<table>\n<tr><td></td>");
867   for (i=0;i<4;i++) printf("<td bgcolor=\"%s\" colspan=\"2\">%s %d-%d</td>",T("BGTITLE"),L("HOURS"),i*6,i*6+5);
868   printf("</tr>\n");
869   for (i=1;i<=NBUSERSTIME;i++)
870   {
871     printf("<tr><td bgcolor=\"%s\" align=\"right\">%d</td>",T("BGTABLE"),i);
872     for (j=0;j<4;j++)
873     {
874       user=-1;
875       max=0;
876       for (k=0;k<nbusers;k++) if (users[k].hours[j]>max) max=users[user=k].hours[j];
877       if (user!=-1)
878       {
879         printf("<td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%d</td>",T("BGTABLE"),users[user].nick,T("BGTABLE"),users[user].hours[j]);
880         users[user].hours[j]=-1;
881       }
882       else printf("<td></td><td></td>");
883     }
884     printf("</tr>\n");
885   }
886   printf("</table>\n<br><br>\n\n");
887
888   /* random topics */
889   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("RANDTOPICS"));
890   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("CHANGEDBY"),T("BGTITLE"),L("NEWTOPIC"));
891   for (i=nbtopics<NBTOPICS?nbtopics-1:NBTOPICS-1;i>=0;i--)
892   {
893     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"",T("BGTABLE"),topics[i].nick,T("BGTABLE"));
894     printhtml(topics[i].topic);
895     printf("\"</td></tr>\n");
896   }
897   printf("</table>\n<br><br>\n\n");
898   
899   /* random urls */
900   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("RANDURLS"));
901   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("POSTEDBY"),T("BGTITLE"),L("POSTEDURL"));
902   for (i=nburls<NBURLS?nburls-1:NBURLS-1;i>=0;i--)
903   {
904     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"<a href=\"",T("BGTABLE"),urls[i].nick,T("BGTABLE"));
905     printhtml(urls[i].url);
906     printf("\">");
907     printhtml(urls[i].shorturl);
908     printf("</a>\"</td></tr>\n");
909   }
910   printf("</table>\n<br><br>\n\n");
911   
912   /* top words */
913   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("TOPWORDS"));
914   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"));
915   for (i=0;i<NBWORDS;i++)
916     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);
917   printf("</table>\n<br><br>\n\n");
918
919   /* big numbers */
920   printf("<font color=\"%s\" size=\"+1\"><b>%s</b></font><br><br>\n",T("TITLE2"),L("BIGNUMBERS"));
921   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("NICK"),T("BGTITLE"),L("NUMBERS"),T("BGTITLE"),L("NBLINES"));
922   for (i=0;i<NBCOUNTERS;i++)
923   {
924     user=-1;
925     max=0;
926     for (j=0;j<nbusers;j++) if (users[j].counters[i]>max) max=users[user=j].counters[i];
927     if (user!=-1) printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%d</td></tr>",T("BGTABLE"),users[user].nick,T("BGTABLE"),L(counters[i]),T("BGTABLE"),users[user].counters[i]);
928   }
929   printf("</table>\n<br><br>\n\n");
930   
931   /* footer */
932   printf(L("TIME"),totallines,days,(int)(time(NULL)-debut));
933   printf("<br>\n%s <a href=\"%s\">irssistats %s</a>",L("FOOTER"),URL,VERSION);
934 #ifdef W3C_LINK
935   printf("<br><br>\n\n<a href=\"http://validator.w3.org/check/referer\"><img src=\"valid-html401.gif\" border=\"0\" height=\"31\" width=\"88\"alt=\"Valid HTML 4.01!\"></a>");
936 #endif
937   printf("\n\n</center>\n\n</body>\n\n</html>\n");
938   
939   return(0);
940 }