Fixes this bug:
[onis.git] / onis
1 #!/usr/bin/perl
2 ##########################################################################
3 #    onis 0.8.2                                               2005-06-07 #
4 #---=============--------------------------------------------------------#
5 # Language: Perl                                                         #
6 # Purpose:  Generating statistics                                        #
7 # Input:    IRC-Logfiles                                                 #
8 # Output:   One HTML file                                                #
9 # Version:  0.8.2 (unstable)                                             #
10 # License:  GPL                                                          #
11 # Homepage: http://verplant.org/onis/                                    #
12 # Authors:  Florian octo Forster <octo@verplant.org>                     #
13 #           Contributions are listed in THANKS                           #
14 ##########################################################################
15
16 BEGIN
17 {
18         if ($0 =~ m#^(.*)[/\\]#) { chdir ($1); }
19
20         unshift (@INC, 'lib');
21
22         # 0x0010   Language (make not-translated lines red/yellow)
23         # 0x0020   Parser (dropped lines)
24         # 0x0040   Parser (time information)
25         # 0x0100   Data::Core (host unsharp)
26         # 0x0200   Data::Persistent
27         # 0x0400   Data::Core (dump incoming data to stderr)
28         # 0x0800   Data::Core (initializing)
29         # 0x1000   Onis::Users
30         $::DEBUG = 0x0000;
31 }
32
33 use strict;
34 use warnings;
35 use vars qw/$VERSION $REVISION/;
36
37 use Onis::Config qw/get_config parse_argv read_config/;
38 use File::Basename qw/dirname/;
39 use Fcntl qw/:flock/;
40
41 =head1 NAME
42
43 onis - onis not irs stats
44
45 =head1 SYNOPSIS
46
47 B<onis> [I<options>] I<logfile>...
48
49 =head1 DESCRIPTION
50
51 onis is a script that converts IRC logfiles into an HTML statistics page. It
52 provides information about daily channel usage, user activity, and channel
53 trivia. It provides a configurable customization and supports Dancer,
54 dircproxy, eggdrop, irssi, mIRC, and XChat logs. Persistent data (history
55 files) and automatic log purging make onis applicable for a large number of
56 logfiles. It also features a powerful translation infrastructure.
57
58 =cut
59
60 $VERSION = '';
61 $REVISION = '$LastChangedRevision$';
62
63 if (!$VERSION)
64 {
65         $VERSION = $REVISION;
66         $VERSION =~ s/^\D*(\d+).*/r$1/;
67 }
68
69 print STDERR $/, __FILE__, ': $Id$' if ($::DEBUG);
70
71 our $FileInfo;
72 our $PurgeLogs = 0;
73
74 parse_argv (@ARGV);
75 read_config (get_config ('config') ? get_config ('config') : 'onis.conf');
76 read_config (scalar get_config ('theme')) if (get_config ('theme'));
77
78 my $output = get_config ('output');
79 if (!$output)
80 {
81         $output = "reports/onis.html";
82 }
83
84 foreach ('Core', get_config ('plugin'))
85 {
86         my $module = ucfirst (lc ($_));
87         require "Onis/Plugins/$module.pm";
88 }
89
90 if (!get_config ('input'))
91 {
92         print STDERR <<EOF;
93
94 Usage: $0 [options] <logfile> [logfile logfile ..]
95
96 Options:
97         --config                Specify alternate config file
98         --output <file>         Defines the file to write the HTML to.
99         --overwrite <bool>      Overwrites files without prompting.
100         --channel <channel>     Defines the channel's name.
101         --logtype <type>        Defines the logfile's type.
102                                 See 'config' for a complete list.
103         --user <name>           Define's the generator's name.
104
105 For a full list of all options please read the onis(1) manpage.
106 EOF
107         exit (1);
108 }
109
110 if (-e $output)
111 {
112         my $overwrite = 0;
113         if (get_config ('overwrite'))
114         {
115                 my $tmp = lc (get_config ('overwrite'));
116                 if ($tmp eq 'true' or $tmp eq 'yes' or $tmp eq 'on')
117                 {
118                         $overwrite = 1;
119                 }
120         }
121         
122         if (!$overwrite)
123         {
124                 print STDERR <<MESSAGE;
125
126 WARNING: The output file ``$output'' already exists
127
128   You can set the ``overwrite'' option in the config
129   file to disable this dialog.
130
131 MESSAGE
132                 print STDERR 'Are you sure you want to overwrite it? [Y|n] ';
133                 my $answer = <STDIN>;
134                 exit (1) if ($answer =~ m/n/i);
135         }
136 }
137
138 my $logtype = 'Eggdrop';
139 if (get_config ('logtype'))
140 {
141         $logtype = ucfirst (lc (get_config ('logtype')));
142 }
143
144 require "Onis/Parser/$logtype.pm";
145 require Onis::Parser::Persistent;
146 require Onis::Data::Persistent;
147 import Onis::Parser (qw(parse last_date));
148 import Onis::Parser::Persistent (qw(newfile));
149 import Onis::Data::Persistent ();
150
151 $FileInfo = Onis::Data::Persistent->new ('FileInfo', 'inode', qw(mtime));
152
153 if (get_config ('purge_logs'))
154 {
155         my $temp = lc (get_config ('purge_logs'));
156         if (($temp eq 'truncate') or ($temp eq 'shorten'))
157         {
158                 $PurgeLogs = 1;
159         }
160         elsif (($temp eq 'delete') or ($temp eq 'remove')
161                         or ($temp eq 'del'))
162         {
163                 $PurgeLogs = 2;
164         }
165 }
166
167 for (get_config ('input'))
168 {
169         my $file = $_;
170         my $logfile;
171         my $status = 4;
172         my $position = 0;
173         my $mtime;
174         my $size;
175         my $inode;
176
177         ($inode, $size, $mtime) = (stat ($file))[1,7,9];
178
179         print STDERR $/, $/, __FILE__, " --- New File ``$file'' ---" if ($::DEBUG & 0x200);
180         
181         if (!defined ($mtime))
182         {
183                 print STDERR $/, __FILE__, ": Unable to stat file ``$file''";
184                 next;
185         }
186         else
187         {
188                 my ($old_mtime) = $FileInfo->get ($inode);
189
190                 print STDERR $/, __FILE__, ": ``$file'': " if ($::DEBUG & 0x200);
191
192                 if (defined ($old_mtime))
193                 {
194                         if ($old_mtime == $mtime)
195                         {
196                                 print STDERR "File did not change. Skipping." if ($::DEBUG & 0x200);
197                                 next;
198                         }
199                         elsif ($old_mtime < $mtime)
200                         {
201                                 print STDERR "File changed. Reading it again." if ($::DEBUG & 0x200);
202                         }
203                         else
204                         {
205                                 print STDERR "File ``$file'' is older than expected. There might be a problem!";
206                         }
207                 }
208                 else
209                 {
210                         print STDERR "File appears to be new. Reading it." if ($::DEBUG & 0x200);
211                 }
212                 $FileInfo->put ($inode, $mtime);
213         }
214         
215         # truncate
216         if ($PurgeLogs == 1)
217         {
218                 unless (open ($logfile, '+< ' . $file))
219                 {
220                         print STDERR $/, __FILE__, ": Unable to open file ``$file'': $!";
221                         next;
222                 }
223         }
224         else
225         {
226                 unless (open ($logfile, '< ' . $file))
227                 {
228                         print STDERR $/, __FILE__, ": Unable to open file ``$file'': $!";
229                         next;
230                 }
231         }
232         
233         if ($PurgeLogs)
234         {
235                 unless (flock ($logfile, LOCK_EX))
236                 {
237                         print STDERR $/, __FILE__, ": Unable to get an exclusive lock for file ``$file'': $!";
238                         close ($logfile);
239                         next;
240                 }
241         }
242         else
243         {
244                 unless (flock ($logfile, LOCK_SH))
245                 {
246                         print STDERR $/, __FILE__, ": Unable to get a shared lock for file ``$file'': $!";
247                         close ($logfile);
248                         next;
249                 }
250         }
251         
252         newfile ($inode);
253         while (<$logfile>)
254         {
255                 s/\n|\r//g;
256                 $status = parse ($_);
257
258                 # 0 == rewind file
259                 # 1 == line parsed
260                 # 2 == unable to parse
261                 # 3 == line old
262                 # 4 == don't have date
263
264                 if ($status == 0)
265                 {
266                         print STDERR $/, __FILE__, ": Rewinding file ``$file''" if ($::DEBUG & 0x200);
267                         seek ($logfile, 0, 0);
268                         $position = 0;
269                 }
270                 elsif (($status == 1) or ($status == 2)
271                                 or ($status == 3))
272                 {
273                         $position = tell ($logfile);
274                 }
275                 elsif ($status == 4)
276                 {
277                         # void
278                 }
279                 else
280                 {
281                         print STDERR $/, __FILE__, ": Parser returned unknown status code: ``$status''";
282                 }
283         }
284
285         if ($PurgeLogs and (($status == 1)
286                                 or ($status == 2)
287                                 or ($status == 3)))
288         {
289                 if (($PurgeLogs > 1)
290                         #and (($position + 1) >= $size)
291                         )
292                 {
293                         # delete file
294                         print STDERR $/, __FILE__, ": Deleting empty file ``$file''" if ($::DEBUG & 0x200);
295                         close ($logfile);
296
297                         if (-w $file)
298                         {
299                                 unless (unlink ($file))
300                                 {
301                                         print STDERR $/, __FILE__, ": Unable to delete empty file ``$file'': $!";
302                                 }
303                                 delete ($FileInfo->{$inode});
304                         }
305                         else
306                         {
307                                 print STDERR $/, __FILE__, ": Won't delete ``$file''. Set it to writeable first!";
308                         }
309                 }
310                 else
311                 {
312                         seek ($logfile, 0, 0);
313                         if (truncate ($logfile, 0))
314                         {
315                                 print $logfile &last_date ();
316                                 print STDERR $/, __FILE__, ": Truncated ``$file''" if ($::DEBUG & 0x200);
317                         }
318                         else
319                         {
320                                 print STDERR $/, __FILE__, ": Couldn't truncate file ``$file'': $!";
321                         }
322                         
323                         close ($logfile);
324                 }
325         }
326         else
327         {       
328                 close ($logfile);
329         }
330 }
331
332 require Onis::Data::Core;
333 require Onis::Html;
334 import Onis::Data::Core qw#print_output#;
335 import Onis::Html qw#open_file close_file#;
336
337 if (open_file ($output))
338 {
339         print_output ();
340         close_file ();
341 }
342 else
343 {
344         # Fail and make noise! ;)
345         print STDERR <<MESSAGE;
346
347 ERROR: Unable to open output file
348
349 The output file ``$output'' could not be opened. Please make sure to set
350 the permissions right and try again.
351
352 MESSAGE
353         exit (1);
354 }
355
356 exit (0);
357
358 END
359 {
360         print $/ if ($::DEBUG);
361 }
362
363 =head1 OPTIONS
364
365 =head2 Core options
366
367 =over 4
368
369 =item B<config>: I<file>;
370
371 Load the config from this file. B<(command line only)>
372
373 =item B<users_config>: I<file>;
374
375 Sets the file from which to read the user configuration.
376
377 =item B<language_file>: I<file>;
378
379 Sets the language file/translation to use.
380
381 =item B<plugin>: I<string>;
382
383 Sets the plugins to load. The plugin B<Core> will always be loaded.
384
385 =item B<input>: I<file>;
386
387 Read and parse this file(s). B<(config file only)>
388
389 =item B<logtype>: I<string>;
390
391 Sets the parser to use for parsing the input file.
392
393 =item B<output>: I<file>;
394
395 Write the generated output to this file.
396
397 =item B<overwrite>: I<bool>;
398
399 Sets wether or not to overwrite the output-file if it exists.
400
401 =item B<purge_logs>: "I<false>" | "I<truncate>" | "I<delete>";
402
403 Sets wether logs should be truncated or even removes after they have been
404 parsed.
405
406 =item B<user>: I<string>;
407
408 Sets the user that created the page. Defaults to the environment variable
409 B<USER> or "onis", if it is not set.
410
411 =item B<channel>: I<string>;
412
413 Sets the name of the channel being parsed. Normally this is auto-detected.
414
415 =item B<unsharp>: "I<none>" | "I<light>" | "I<medium>" | "I<hard>";
416
417 Sets how to do unsharping. What each setting actually does is described in the
418 readme and in L<Onis::Data::Core>.
419
420 =back
421
422 =head2 Appearance
423
424 =over 4
425
426 =item B<theme>: I<file>;
427
428 Theme file to load.
429
430 =item B<stylesheet>: I<file>;
431
432 Sets the stylesheet to use. This is included in the HTML-file as-is, so you
433 have to take care of absolute/relative paths yourself..
434
435 =item B<color_codes>: I<bool>;
436
437 Wether or not to print the color codes (introduced by mIRC, used by idiots and
438 ignored by the rest) in the generated HTML-file. Of course this defaults to not
439 print the codes..
440
441 =item B<display_images>: I<bool>;
442
443 Sets if user-images should be displayed.
444
445 =item B<default_image>: I<file>;
446
447 Sets the default image to use if no user-defined image is available.
448
449 =item B<display_lines>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";
450
451 =item B<display_words>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";
452
453 =item B<display_chars>: "I<none>" | "I<number>" | "I<bar>" | "I<both>";
454
455 Sets if and how lines, words and/or characters should be displayed.
456
457 =item B<sort_by>: "I<lines>" | "I<words>" | "I<chars>";
458
459 Sets wether to sort by lines, words or characters written.
460
461 =item B<display_times>: I<bool>;
462
463 Wether or not to display a fixed width bar that shows when a user is most
464 active.
465
466 =item B<horizontal_images>: I<file>, I<file>, I<file>, I<file>;
467
468 =item B<vertical_images>:   I<file>, I<file>, I<file>, I<file>;
469
470 Sets the images to use for horizontal and vertical bars. This should be used in
471 the theme-file.
472
473 =item B<encoding>: I<string>;
474
475 Sets the encoding to include in the HTML-file. If you don't know what this is,
476 don't change it..
477
478 =item B<public_page>: I<bool>;
479
480 Wether or not this is a public page. Public pages may be linked on the onis
481 homepage at some point in the fututre..
482
483 =back
484
485 =head2 Storage / Persistency
486
487 =over 4
488
489 =item B<storage_module>: I<string>;
490
491 Sets the storage-module to use.
492
493 =item B<storage_dir>: I<directory>;
494
495 Sets the directory to store persistency information under.
496
497 =item B<storage_file>: I<file>;
498
499 Sets the file to write persistency data to, if applicable by the
500 storage-module.
501
502 =back
503
504 =head2 Plugins
505
506 =over 4
507
508 =item B<min_word_length>: I<number>;
509
510 Substring containing only word-characters needs to be this long to be
511 considered a word.
512
513 =item B<plugin_max>: I<number>;
514
515 Sets the number of "most referenced nicks", "most used words" and the like to
516 be displayed. This option will be removed in the future.
517
518 =item B<longlines>: I<number>;
519
520 =item B<shortlines>: I<number>;
521
522 The number of lines in the big and the small table. While in the big table one
523 line is dedicated for one person the small table displays six persons per line.
524
525 =item B<quote_cache_size>: I<number>;
526
527 Sets how many quotes are to be cached for each nick. At the end of the run one
528 of the quotes in the cache will be chosen at random and displayed.
529
530 =item B<quote_min>: I<number>;
531
532 =item B<quote_max>: I<number>;
533
534 Sets the minimum and maximum length of a quote. Too short quotes may be not
535 very typical for a person, too long quotes may clutter the layout.
536
537 =item B<conversations_number>: I<number>;
538
539 =item B<userdetails_conversations_number>: I<number>;
540
541 Number of conversations partners to include in the output (or in the
542 conversations section of the userdetails plugin).
543
544 =item B<soliloquies_count>: I<number>;
545
546 Sets how many lines without interruption are considered a soliloquy.
547
548 =item B<longterm_days>: I<number>;
549
550 =item B<userdetails_longterm_days>: I<number>;
551
552 Sets the number of days shown in the longterm-plugin (or the longter-section of
553 the userdetails-plugin).
554
555 =item B<ignore_words>: I<number>;
556
557 The Words-Plugin will ignore words with less than this characters.
558
559 =item B<userdetails_number>: I<number>;
560
561 The number of nicks to print userdetails for.
562
563 =back
564
565 =head1 AUTHOR
566
567 Florian Forster E<lt>octo at verplant.orgE<gt>
568
569 =cut