version 0.31
[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 5000
10 #define MAXNICKLENGTH 50
11 #define MAXLINELENGTH 2000
12 #define MAXQUOTELENGTH 100
13 #define NBUSERS 50
14 #define NBUSERSTIME 10
15 #define NBURLS 5
16 #define NBTOPICS 5
17 #define NBWORDS 20
18 #define MINWORDLENGTH 5
19
20 /* irssistats */
21 #define VERSION "0.31"
22 #define URL "http://royale.zerezo.com/programmation/irssistats/"
23
24 /* Counters */
25 #define D_SMILE     0
26 #define D_FROWN     1
27 #define D_EXCLAM    2
28 #define D_QUESTION  3
29 #define D_ME        4
30 #define D_TOPIC     5
31 #define D_MODE      6
32 #define D_KICK      7
33 #define D_KICKED    8
34 #define D_URL       9
35 #define D_JOIN      10
36 #define D_NICK      11
37 #define D_MONOLOGUE 12
38 #define NBCOUNTERS  13
39 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"};
40
41 /* Languages */
42 #define NBLANGUAGES 3
43 #define NBKEYS 38
44 char *keys[NBLANGUAGES][NBKEYS+1][2]= /* first key used for language name and abbreviation */
45 {
46   { /* English language */
47     { "English",      "en" },
48     { "HEADER",       "Statistics for #%s by %s" },
49     { "LEGEND",       "Legend" },
50     { "LASTDAYS",     "Lastdays statistics" },
51     { "TOPHOURS",     "Hourly statistics" },
52     { "TOPUSERS",     "Most active people" },
53     { "OTHERS",       "There are %d left not ranked..." },
54     { "NBLINES",      "lines" },
55     { "NICK",         "nick" },
56     { "AVGLETTERS",   "letters/lines" },
57     { "HOURS",        "hours" },
58     { "QUOTE",        "random message" },
59     { "TOPUSERSTIME", "Most active people by time of day" },
60     { "RANDTOPICS",   "Some topics" },
61     { "CHANGEDBY",    "changed by" },
62     { "NEWTOPIC",     "new topic" },
63     { "RANDURLS",     "Some URLs" },
64     { "POSTEDBY",     "posted by" },
65     { "POSTEDURL",    "URL" },
66     { "TOPWORDS",     "Most used words" },
67     { "WORD",         "word" },
68     { "OCCURRENCES",  "occurrences" },
69     { "BIGNUMBERS",   "Some big numbers..." },
70     { "NUMBERS",      "numbers" },
71     { "TIME",         "%d lines parsed in %d seconds" },
72     { "FOOTER",       "Statistics generated by" },
73     { "C_SMILE",      "is often happy :)" },
74     { "C_FROWN",      "is often sad :(" },
75     { "C_EXCLAM",     "yells a lot !" },
76     { "C_QUESTION",   "asks a lot of questions ?" },
77     { "C_ME",         "likes /me command" },
78     { "C_TOPIC",      "often changes the topic" },
79     { "C_MODE",       "often changes the modes" },
80     { "C_KICK",       "likes to /kick" },
81     { "C_KICKED",     "is often kicked" },
82     { "C_URL",        "posts many URLs" },
83     { "C_JOIN",       "doesn't know wether to stay or quit" },
84     { "C_NICK",       "often changes his nick" },
85     { "C_MONOLOGUE",  "speaks a lot of monologues" }
86   },
87   { /* French language */
88     { "Fran├žais",     "fr" },
89     { "HEADER",       "Statistiques de #%s par %s" },
90     { "LEGEND",       "L&eacute;gende" },
91     { "LASTDAYS",     "Statistiques des derniers jours" },
92     { "TOPHOURS",     "Statistiques horaires" },
93     { "TOPUSERS",     "Personnes les plus actives" },
94     { "OTHERS",       "Il reste %d personnes non class&eacute;es..." },
95     { "NBLINES",      "lignes" },
96     { "NICK",         "nick" },
97     { "AVGLETTERS",   "lettres/lignes" },
98     { "HOURS",        "heures" },
99     { "QUOTE",        "message al&eacute;atoire" },
100     { "TOPUSERSTIME", "Personnes les plus actives par p&eacute;riode de la journ&eacutee" },
101     { "RANDTOPICS",   "Quelques topics" },
102     { "CHANGEDBY",    "chang&eacute; par" },
103     { "NEWTOPIC",     "nouveau topic" },
104     { "RANDURLS",     "Quelques URLs" },
105     { "POSTEDBY",     "post&eacute;e par" },
106     { "POSTEDURL",    "URL" },
107     { "TOPWORDS",     "Mots les plus utilis&eacute;s" },
108     { "WORD",         "mot" },
109     { "OCCURRENCES",  "occurrences" },
110     { "BIGNUMBERS",   "Quelques grands nombres..." },
111     { "NUMBERS",      "nombres" },
112     { "TIME",         "%d lignes trait&eacute;es en %d secondes" },
113     { "FOOTER",       "Statistiques g&eacute;n&eacute;r&eacute;es par" },
114     { "C_SMILE",      "est souvent heureux :)" },
115     { "C_FROWN",      "est souvent triste :(" },
116     { "C_EXCLAM",     "hurle beaucoup !" },
117     { "C_QUESTION",   "pose beaucoup de questions ?" },
118     { "C_ME",         "aime la commande /me" },
119     { "C_TOPIC",      "change souvent le topic" },
120     { "C_MODE",       "change souvent les modes" },
121     { "C_KICK",       "aime la commande /kick" },
122     { "C_KICKED",     "est souvent kick&eacute;" },
123     { "C_URL",        "poste beaucoup d'URLs" },
124     { "C_JOIN",       "ne sait pas s'il doit rester ou partir" },
125     { "C_NICK",       "change souvent de nick" },
126     { "C_MONOLOGUE",  "parle beaucoup de monologues" }
127   },
128   { /* Spanish language */
129     /* Submitted by Alex <ainaker@gmx.net> */
130     { "Spanish",      "es" },
131     { "HEADER",       "Estad&iacute;sticas de #%s por %s" },
132     { "LEGEND",       "Leyenda" },
133     { "LASTDAYS",     "Estad&iacute;sticas de los &uacute;ltimos d&iacute;as" },
134     { "TOPHOURS",     "Estad&iacute;sticas por horas" },
135     { "TOPUSERS",     "Los que m&aacute;s escriben" },
136     { "OTHERS",       "Hay %d m&aacute;s que no llegaron..." },
137     { "NBLINES",      "l&iacute;neas" },
138     { "NICK",         "nick" },
139     { "AVGLETTERS",   "letras por l&iacute;nea" },
140     { "HOURS",        "horas" },
141     { "QUOTE",        "Frase aleatoria" },
142     { "TOPUSERSTIME", "Los que m&aacute;s escriben seg&uacute;n la hora" },
143     { "RANDTOPICS",   "Algunos topics" },
144     { "CHANGEDBY",    "Puestos por" },
145     { "NEWTOPIC",     "topic" },
146     { "RANDURLS",     "Algunas URLs" },
147     { "POSTEDBY",     "puestas por" },
148     { "POSTEDURL",    "URL" },
149     { "TOPWORDS",     "Palabras m&aacute;s usadas" },
150     { "WORD",         "Palabra" },
151     { "OCCURRENCES",  "Frecuencia" },
152     { "BIGNUMBERS",   "Algunos datos..." },
153     { "NUMBERS",      "N&uacute;mero de veces" },
154     { "TIME",         "%d lineas procesadas en %d segundos" },
155     { "FOOTER",       "Estad&iacute;sticas generadas por" },
156     { "C_SMILE",      "Suele estar fel&iacute;z :)" },
157     { "C_FROWN",      "Suele estar triste :(" },
158     { "C_EXCLAM",     "Grita mucho !" },
159     { "C_QUESTION",   "Hace muchas preguntas ?" },
160     { "C_ME",         "Abusa del comando /me" },
161     { "C_TOPIC",      "Suele cambiar el topic" },
162     { "C_MODE",       "Cambia a veces los modos del canal" },
163     { "C_KICK",       "Le gusta patear" },
164     { "C_KICKED",     "Es pateado con frecuencia" },
165     { "C_URL",        "Pone muchas URLs" },
166     { "C_JOIN",       "No sabe si irse o quedarse" },
167     { "C_NICK",       "Cambia mucho de nick" },
168     { "C_MONOLOGUE",  "Habla solo" }
169   }
170 };
171
172 int language;
173
174 char *L(char *key)
175 {
176   int i;
177   for (i=1;i<=NBKEYS;i++) if (strcmp(key,keys[language][i][0])==0) return(keys[language][i][1]);
178   fprintf(stderr,"unknown language key: %s\n",key);
179   return("");
180 }
181
182 /* Themes */
183 #define NBTHEMES 5
184 #define NBCOLORS 9
185 char *colors[NBTHEMES][NBCOLORS+1][2]= /* first key used for theme name/description and abbreviation */
186 {
187   { /* Default theme */
188     { "White background", "default" },
189     { "BGCOLOR", "#FFFFFF" },
190     { "TEXT",    "#000000" },
191     { "LINK",    "#0000EE" },
192     { "VLINK",   "#551A8B" },
193     { "ALINK",   "#FF0000" },
194     { "TITLE1",  "#000000" },
195     { "TITLE2",  "#000000" },
196     { "BGTABLE", "#EEEEEE" },
197     { "BGTITLE", "#FFEEEE" }
198   },
199   { /* Dark theme */
200     { "Black background", "dark" },
201     { "BGCOLOR", "#000000" },
202     { "TEXT",    "#FFFFFF" },
203     { "LINK",    "#AAAAFF" },
204     { "VLINK",   "#CCCCDD" },
205     { "ALINK",   "#FFAAAA" },
206     { "TITLE1",  "#AAAAFF" },
207     { "TITLE2",  "#FFAAAA" },
208     { "BGTABLE", "#225522" },
209     { "BGTITLE", "#552222" }
210   },
211   { /* zeRezo theme */
212     { "Green theme...", "zerezo" },
213     { "BGCOLOR", "#000000" },
214     { "TEXT",    "#FFFFFF" },
215     { "LINK",    "#14F024" },
216     { "VLINK",   "#0CBA18" },
217     { "ALINK",   "#FFFFFF" },
218     { "TITLE1",  "#0CBA18" },
219     { "TITLE2",  "#84DB8C" },
220     { "BGTABLE", "#085D10" },
221     { "BGTITLE", "#0B810B" }
222   },
223   { /* tit-namour theme */
224     { "Purple and Pink", "namour" },
225     { "BGCOLOR", "#9933CC" },
226     { "TEXT",    "#DDAAFF" },
227     { "LINK",    "#CC99FF" },
228     { "VLINK",   "#999999" },
229     { "ALINK",   "#FFC8C8" },
230     { "TITLE1",  "#FFC8C8" },
231     { "TITLE2",  "#FFC8C8" },
232     { "BGTABLE", "#7711AA" },
233     { "BGTITLE", "#550088" }
234   },
235   { /* zeDuel theme */
236     { "Orange theme...", "zeduel" },
237     { "BGCOLOR", "#FFFFFF" },
238     { "TEXT",    "#000000" },
239     { "LINK",    "#FF7700" },
240     { "VLINK",   "#C05A00" },
241     { "ALINK",   "#FF9A41" },
242     { "TITLE1",  "#C05A00" },
243     { "TITLE2",  "#FF7700" },
244     { "BGTABLE", "#FFEEEE" },
245     { "BGTITLE", "#FF7700" }
246   }
247 };
248
249 int theme;
250
251 char *T(char *color)
252 {
253   int i;
254   for (i=1;i<=NBCOLORS;i++) if (strcmp(color,colors[theme][i][0])==0) return(colors[theme][i][1]);
255   fprintf(stderr,"unknown theme color: %s\n",color);
256   return("");
257 }
258
259 /* Variables */
260
261 char *channel;
262 char *maintainer;
263
264 struct
265 {
266   char nick[MAXNICKLENGTH];
267   int lines;
268   int letters;
269   int hours[4];
270   char quote[MAXQUOTELENGTH+1];
271   int counters[NBCOUNTERS];
272 } users[MAXUSERS];
273 int nbusers=0;
274
275 struct
276 {
277   char nick[MAXNICKLENGTH];
278   char url[MAXLINELENGTH];
279   char shorturl[MAXQUOTELENGTH+1];
280 } urls[5];
281 int nburls=0;
282
283 struct
284
285   char nick[MAXNICKLENGTH];
286   char topic[MAXQUOTELENGTH+1];
287 } topics[5];
288 int nbtopics=0;
289
290 struct
291 {
292   int lines;
293   int hours[4];
294 } lastdays[31];
295 int days=0;
296
297 int hours[24];
298 int lines=0;
299
300 struct letter
301 {
302   int nb;
303   struct letter *next[26];
304 } words;
305
306 struct
307 {
308   int nb;
309   char word[MAXLINELENGTH];
310 } topwords[NBWORDS];
311
312 #define isletter(c) (((c>='a')&&(c<='z'))||((c>='A')&&(c<='Z')))
313 #define lowercase(c) (((c>='A')&&(c<='Z'))?c-'A'+'a':c)
314 void findwords(char *message)
315 {
316   int i,c;
317   struct letter *pos,*tmp;
318   for (;;)
319   {
320     while (!isletter(*message)) if (*message=='\0') return; else message++;
321     pos=&words;
322     while (isletter(*message))
323     {
324       c=lowercase(*message)-'a';
325       if (pos->next[(int)c]==NULL)
326       {
327         tmp=malloc(sizeof(struct letter));
328         tmp->nb=0;
329         for (i=0;i<26;i++) tmp->next[i]=NULL;
330         pos->next[(int)c]=tmp;
331       }
332       pos=pos->next[(int)c];
333       message++; 
334     }
335     pos->nb++;
336   }
337   return;
338 }
339
340 char tempword[MAXLINELENGTH];
341 void bestwords(struct letter pos,int cur)
342 {
343   int i,j;
344   if ((cur>=MINWORDLENGTH)&&(pos.nb>topwords[NBWORDS-1].nb))
345   {
346     for (i=0;pos.nb<topwords[i].nb;i++);
347     for (j=NBWORDS-1;j>i;j--)
348     {
349       topwords[j].nb=topwords[j-1].nb;
350       strcpy(topwords[j].word,topwords[j-1].word);
351     }
352     topwords[i].nb=pos.nb;
353     strcpy(topwords[i].word,tempword);
354   }
355   for (i=0;i<26;i++) if (pos.next[i]!=NULL)
356   {
357     tempword[cur]='a'+i;
358     bestwords(*(pos.next[i]),cur+1);
359   }
360   tempword[cur]='\0';
361 }
362
363 void printhtml(char *string) /* replace < and > by &lt; and &gt; */
364 {
365   while (*string!='\0')
366   {
367     switch (*string)
368     {
369       case '<':printf("&lt;"); break;
370       case '>':printf("&gt;"); break;
371       case '&':printf("&amp;"); break;
372       default:printf("%c",*string); break;
373     }
374     string++;
375   }
376   return;
377 }
378
379 int dichotomic(char *nick)
380 {
381   int i,j,start=0,end=nbusers-1,middle;
382   while (start<=end)
383   {
384     middle=(start+end)/2;
385     if (strcmp(nick,users[middle].nick)>0) start=middle+1; else end=middle-1;
386   }
387   if (strcmp(nick,users[start].nick)!=0)
388   {
389     nbusers++;
390     if (nbusers>=MAXUSERS) { fprintf(stderr,"too many users\n"); exit(1); }
391     for (i=nbusers-1;i>start;i--)
392     {
393       strcpy(users[i].nick,users[i-1].nick);
394       users[i].lines=users[i-1].lines;
395       users[i].letters=users[i-1].letters;
396       for (j=0;j<4;j++) users[i].hours[j]=users[i-1].hours[j];
397       strcpy(users[i].quote,users[i-1].quote);
398       for (j=0;j<NBCOUNTERS;j++) users[i].counters[j]=users[i-1].counters[j];
399     }
400     strcpy(users[start].nick,nick);
401     users[start].lines=0;
402     users[start].letters=0;
403     for (j=0;j<4;j++) users[start].hours[j]=0;
404     users[start].quote[0]='\0';
405     for (j=0;j<NBCOUNTERS;j++) users[start].counters[j]=0;
406   }
407   return(start);
408 }
409
410 int main(int argc,char *argv[])
411 {
412   int i,j,k;
413   int max,user,temp,hour;
414   int mononick,monolines;
415   time_t debut;
416   int totallines=0;
417   int pos=0;
418   char c;
419   char *nick,*message;
420   char line[MAXLINELENGTH];
421   
422   /*** INIT ***/
423   
424   if (argc!=5)
425   {
426     fprintf(stderr,"Usage: cat /path/to/file.log | ./irssistats channel maintainer language theme > /path/to/file.html\n\n");
427     fprintf(stderr,"Version :\nirssistats %s\n\n",VERSION);
428     fprintf(stderr,"Supported languages :\n");
429     for (i=0;i<NBLANGUAGES;i++) fprintf(stderr,"%s = %s\n",keys[i][0][1],keys[i][0][0]);
430     fprintf(stderr,"\nSupported themes :\n");
431     for (i=0;i<NBTHEMES;i++) fprintf(stderr,"%s = %s\n",colors[i][0][1],colors[i][0][0]);
432     return(1);
433   }
434   channel=argv[1];
435   maintainer=argv[2];
436   for (i=0;i<NBLANGUAGES;i++) if (strcmp(argv[3],keys[i][0][1])==0) { language=i; break; }
437   if (i==NBLANGUAGES)
438   {
439     fprintf(stderr,"Invalid language : %s\n",argv[3]);
440     return(1);
441   }
442   for (i=0;i<NBTHEMES;i++) if (strcmp(argv[4],colors[i][0][1])==0) { theme=i; break; }
443   if (i==NBTHEMES)
444   {
445     fprintf(stderr,"Invalid theme : %s\n",argv[4]);
446     return(1);
447   }
448
449   /*** LOG ***/
450   
451   srand(debut=time(NULL));
452   fprintf(stderr,"working:");
453   while (!feof(stdin))
454   {
455     c=getchar();
456     line[pos++]=c;
457     if (pos>=MAXLINELENGTH) { fprintf(stderr,"line too long\n"); exit(1); }
458     if (c=='\n')
459     {
460       line[pos-1]='\0';
461       totallines++;
462       if (totallines%10000==0) { fprintf(stderr,"."); fflush(stdout); }
463       if (strncmp("--- Day changed",line,15)==0) /* --- Day changed Wed May 01 2002 */
464       {
465         for (i=30;i>0;i--)
466         {
467           lastdays[i].lines=lastdays[i-1].lines;
468           for (j=0;j<4;j++) lastdays[i].hours[j]=lastdays[i-1].hours[j];
469         }
470         lastdays[0].lines=0;
471         for (j=0;j<4;j++) lastdays[0].hours[j]=0;
472         days++;
473       }
474       else if (strncmp("-!- mode/",&line[6],9)==0) /* 00:00 -!- mode/#channel [...] by Nick(, Nick2, Nick3...) */
475       {
476         for (i=15;(line[i]!=']')||(line[i+1]!=' ');i++);
477         nick=&line[i+5];
478         for (i=0;(nick[i]!='\0')&&(nick[i]!=',');i++);
479         nick[i]='\0';
480         users[dichotomic(nick)].counters[D_MODE]++;
481       }
482       else if (strncmp("-!-",&line[6],3)==0) /* 00:00 -!- Nick something... */
483       {
484         for (i=10;line[i]!=' ';i++);
485         line[i]='\0';
486         nick=&line[10];
487         message=&line[i+1];
488         if (strncmp("changed the topic of",message,20)==0) /* 00:00 -!- Nick changed the topic of #channel to: new topic */
489         {
490           users[dichotomic(nick)].counters[D_TOPIC]++;
491           for (i=21;message[i]!=':';i++);
492           message=&message[i+2];
493           nbtopics++;
494           if ((nbtopics<=NBTOPICS) || (rand()%(nbtopics/NBTOPICS)==0))
495           {
496             temp=nbtopics<=NBTOPICS?nbtopics-1:rand()%NBTOPICS;
497             strcpy(topics[temp].nick,nick);
498             strncpy(topics[temp].topic,message,MAXQUOTELENGTH);
499           }
500         }
501         else if (strncmp("was kicked from",message,15)==0) /* 00:00 -!- Nick was kicked from #channel by Nick [Reason] */
502         {
503           users[dichotomic(nick)].counters[D_KICKED]++;
504           for (i=16;message[i]!=' ';i++);
505           message=&message[i+4];
506           for (i=0;message[i]!=' ';i++);
507           message[i]='\0';
508           users[dichotomic(message)].counters[D_KICK]++;
509         }
510         else if (strncmp("is now known as",message,15)==0) /* 00:00 -!- Nick is now known as Nick */
511           users[dichotomic(nick)].counters[D_NICK]++;
512         else if (message[0]=='[') /* 00:00 -!- Nick [user@host] something... */
513         {
514           for (i=0;message[i]!=']';i++);
515           message=&message[i+2];
516           if (strncmp("has joined",message,10)==0) /* 00:00 -!- Nick [user@host] has joined #channel */
517             users[dichotomic(nick)].counters[D_JOIN]++;
518           else if (strncmp("has quit",message,8)==0); /* 00:00 -!- Nick [user@host] has quit [Reason] */
519           else if (strncmp("has left",message,8)==0); /* 00:00 -!- Nick [user@host] has left #channel [Reason] */
520           else;
521         }
522       }
523       else if ((line[6]=='<') || (line[7]=='*'))
524       {
525         line[2]='\0';
526         hour=atoi(line);
527         if (line[7]=='*') /* 00:00  * Nick the message */
528         {
529           for (i=9;line[i]!=' ';i++);
530           nick=&line[9];
531           message=&line[i+1];
532         }
533         else if (line[7]=='>') /* 00:00 <>>>?Nick<<<> the personal message */
534         {
535           for (i=11;line[i]!='<';i++);
536           nick=&line[11];
537           message=&line[i+5];
538         }
539         else /* 00:00 <?Nick> the message */
540         {
541           for (i=8;line[i]!='>';i++);
542           nick=&line[8];
543           message=&line[i+2];
544         }
545         line[i]='\0';
546         i=dichotomic(nick);
547         if (line[7]=='*') users[i].counters[D_ME]++;
548         if (i==mononick)
549         {
550           monolines++;
551           if (monolines==5) users[i].counters[D_MONOLOGUE]++;
552         }
553         else
554         {
555           mononick=i;
556           monolines=1;
557         }
558         j=strlen(message);
559         users[i].lines++;
560         users[i].letters+=j;
561         users[i].hours[hour/6]++;
562         lastdays[0].lines++;
563         lastdays[0].hours[hour/6]++;
564         lines++;
565         hours[hour]++;
566         if (message[j-1]=='?') users[i].counters[D_QUESTION]++;
567         else if (message[j-1]=='!') users[i].counters[D_EXCLAM]++;
568         else if ((message[j-3]==' ')&&(message[j-2]==':'))
569         {
570           if (message[j-1]==')') users[i].counters[D_SMILE]++;
571           else if (message[j-1]=='(') users[i].counters[D_FROWN]++;
572         }
573         if (rand()%users[i].lines==0) strncpy(users[i].quote,message,MAXQUOTELENGTH);
574         if (strncmp("http://",message,7)==0)
575         {
576           users[i].counters[D_URL]++;
577           for (i=0;(message[i]!=' ') && (i<strlen(message));i++);
578           message[i]='\0';
579           nburls++;
580           if ((nburls<=NBURLS) || (rand()%(nburls/NBURLS)==0))
581           {
582             temp=nburls<=NBURLS?nburls-1:rand()%NBURLS;
583             strcpy(urls[temp].nick,nick);
584             strcpy(urls[temp].url,message);
585             strncpy(urls[temp].shorturl,message,MAXQUOTELENGTH);
586           }
587         }
588         findwords(message);
589       }
590       pos=0;
591     }
592   }
593   fprintf(stderr,"done\n");
594
595   bestwords(words,0);
596
597   /*** HTML ***/
598
599   /* header */
600   printf("<!-- Generated by irssistats %s : %s -->\n\n",VERSION,URL);
601   printf("<html>\n\n<head>\n<base target=\"_blank\">\n<title>");
602   printf(L("HEADER"),channel,maintainer);
603   printf("</title>\n</head>\n\n");
604   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"));
605   printf(L("HEADER"),channel,maintainer);
606   printf("</h1></font>\n%s<br>\n<br><br>\n\n",ctime(&debut));
607
608   /* legend */
609   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table bgcolor=%s>\n<tr>\n",T("TITLE2"),L("LEGEND"),T("BGTABLE"));
610   for (i=0;i<4;i++) printf("<td><img src=\"h%d.png\" width=\"40\" height=\"15\"></td><td> : %s %d-%d</td><td width=\"10\"></td>\n",i+1,L("HOURS"),i*6,i*6+5);
611   printf("</tr>\n</table>\n<br><br>\n\n");
612   
613   /* last days */
614   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table>\n<tr>\n",T("TITLE2"),L("LASTDAYS"));
615   max=-1;
616   for (i=30;i>=0;i--) if (lastdays[i].lines>max) max=lastdays[i].lines;
617   for (i=30;i>=0;i--)
618   {
619     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%d</font><br>",lastdays[i].lines);
620     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);
621     printf("</td>\n");
622   }
623   printf("</tr>\n<tr>\n");
624   for (i=30;i>=0;i--)
625     printf("<td align=\"center\" valign=\"center\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
626   printf("</tr>\n</table>\n<br><br>\n\n");
627   
628   /* top hours */
629   printf("<font color=\"%s\"><h3>%s</h3></font>\n<table>\n<tr>\n",T("TITLE2"),L("TOPHOURS"));
630   max=-1;
631   for (i=0;i<24;i++) if (hours[i]>max) max=hours[i];
632   for (i=0;i<24;i++)
633   {
634     printf("<td width=\"15\" align=\"center\" valign=\"bottom\"><font size=\"-2\">%.1f%%</font><br>",lines!=0?(float)100*hours[i]/lines:0);
635     if (hours[i]!=0) printf("<img src=\"v%d.png\" width=\"15\" height=\"%d\"><br>",i/6+1,150*hours[i]/max);
636     printf("</td>\n");
637   }
638   printf("</tr>\n<tr>\n");
639   for (i=0;i<24;i++)
640     printf("<td align=\"center\" valign=\"center\" bgcolor=\"%s\">%d</td>\n",T("BGTABLE"),i);
641   printf("</tr>\n</table>\n<br><br>\n\n");
642   
643   /* top users */
644   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("TOPUSERS"));
645   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"));
646   for (i=1;i<=NBUSERS;i++)
647   {
648     user=-1;
649     max=-1;
650     for (j=0;j<nbusers;j++) if (users[j].lines>max) max=users[user=j].lines;
651     if (user!=-1)
652     {
653       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"));
654       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);
655       printf("</td><td bgcolor=\"%s\">%d</td><td bgcolor=\"%s\"><img src=\"hm.png\" width=\"%d\" height=\"15\"></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"));
656       printhtml(users[user].quote);
657       printf("\"</td></tr>\n");
658       users[user].lines=-1;
659     }    
660   }
661   printf("</table>\n");
662   if (nbusers>NBUSERS)
663   {
664     printf("<br>");
665     printf(L("OTHERS"),nbusers-50);
666     printf("<br>\n");
667   }
668   printf("<br><br>\n\n");
669   
670   /* top users by time */
671   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("TOPUSERSTIME"));
672   printf("<table>\n<tr><td></td>");
673   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);
674   printf("</tr>\n");
675   for (i=1;i<=NBUSERSTIME;i++)
676   {
677     printf("<tr><td bgcolor=\"%s\" align=\"right\">%d</td>",T("BGTABLE"),i);
678     for (j=0;j<4;j++)
679     {
680       user=-1;
681       max=-1;
682       for (k=0;k<nbusers;k++) if (users[k].hours[j]>max) max=users[user=k].hours[j];
683       if (user!=-1)
684       {
685         printf("<td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%d</td>",T("BGTABLE"),users[user].nick,T("BGTABLE"),users[user].hours[j]);
686         users[user].hours[j]=-1;
687       }
688       else printf("<td></td><td></td>");
689     }
690     printf("</tr>\n");
691   }
692   printf("</table>\n<br><br>\n\n");
693
694   /* random topics */
695   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("RANDTOPICS"));
696   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("CHANGEDBY"),T("BGTITLE"),L("NEWTOPIC"));
697   for (i=nbtopics<NBTOPICS?nbtopics-1:NBTOPICS-1;i>=0;i--)
698   {
699     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"",T("BGTABLE"),topics[i].nick,T("BGTABLE"));
700     printhtml(topics[i].topic);
701     printf("\"</td></tr>\n");
702   }
703   printf("</table>\n<br><br>\n\n");
704   
705   /* random urls */
706   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("RANDURLS"));
707   printf("<table>\n<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">%s</td></tr>\n",T("BGTITLE"),L("POSTEDBY"),T("BGTITLE"),L("POSTEDURL"));
708   for (i=nburls<NBURLS?nburls-1:NBURLS-1;i>=0;i--)
709   {
710     printf("<tr><td bgcolor=\"%s\">%s</td><td bgcolor=\"%s\">\"<a href=\"%s\">",T("BGTABLE"),urls[i].nick,T("BGTABLE"),urls[i].url);
711     printhtml(urls[i].shorturl);
712     printf("</a>\"</td></tr>\n");
713   }
714   printf("</table>\n<br><br>\n\n");
715   
716   /* top words */
717   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("TOPWORDS"));
718   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"));
719   for (i=0;i<NBWORDS;i++)
720     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);
721   printf("</table>\n<br><br>\n\n");
722
723   /* big numbers */
724   printf("<font color=\"%s\"><h3>%s</h3></font>\n",T("TITLE2"),L("BIGNUMBERS"));
725   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"));
726   for (i=0;i<NBCOUNTERS;i++)
727   {
728     user=-1;
729     max=0;
730     for (j=0;j<nbusers;j++) if (users[j].counters[i]>max) max=users[user=j].counters[i];
731     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]);
732   }
733   printf("</table>\n<br><br>\n\n");
734   
735   /* footer */
736   printf(L("TIME"),totallines,(int)(time(NULL)-debut));
737   printf("<br>\n%s <a href=\"%s\">irssistats %s</a>",L("FOOTER"),URL,VERSION);
738   printf("\n\n</center>\n\n</body>\n\n</html>\n");
739   
740   return(0);
741 }