Fixed syntactic errors in Onis::Plugins::Core, Onis::Data::Persistent::None, Onis...
[onis.git] / lib / Onis / Data / Core.pm
1 package Onis::Data::Core;
2
3 =head1 NAME
4
5 Onis::Data::Core - User management
6
7 =head1 DESCRIPTION
8
9 Store data to the internal structure, care about users, nicks and idents and
10 dispatch to plugins. The core of the data even..
11
12 =cut
13
14 use strict;
15 use warnings;
16
17 use Exporter;
18 use Onis::Config qw(get_config);
19 use Onis::Users qw(ident_to_name);
20 use Onis::Data::Persistent;
21 use Onis::Parser::Persistent qw(get_absolute_time);
22
23 =head1 NAMING CONVENTION
24
25 Each and every person in the IRC can be identified by a three-tupel: B<nick>,
26 B<user> and B<host>, most often seen as I<nick!user@host>.
27
28 The combination of B<user> and B<host> is called an B<ident> here and written
29 I<user@host>. The combination of all three parts is called a B<chatter> here,
30 though it's rarely used.
31
32 A B<name> is the name of the "user" as defined in the F<users.conf>. Therefore,
33 the F<users.conf> defines a mapping of B<chatter> -E<gt> B<name>.
34
35 =cut
36
37 our $Nick2Ident   = Onis::Data::Persistent->new ('Nick2Ident', 'nick', 'ident');
38 our $ChatterList  = Onis::Data::Persistent->new ('ChatterList', 'chatter', 'counter');
39 our $ChannelNames = Onis::Data::Persistent->new ('ChannelNames', 'channel', 'counter');
40
41 @Onis::Data::Core::EXPORT_OK =
42 qw(
43         store unsharp calculate_nicks 
44
45         get_all_nicks get_channel get_main_nick nick_to_ident ident_to_nick
46         get_total_lines nick_rename print_output register_plugin
47 );
48 @Onis::Data::Core::ISA = ('Exporter');
49
50 our $PluginCallbacks = {};
51 our $OUTPUT   = [];
52 our @AllNicks = ();
53 our @ALLNAMES = ();
54
55 our %NickToNick = ();
56 our %NickToIdent = ();
57 our %IdentToNick = ();
58
59 our $LASTRUN_DAYS = 0;
60
61
62
63 our $UNSHARP = 'MEDIUM';
64 if (get_config ('unsharp'))
65 {
66         my $tmp = get_config ('unsharp');
67         $tmp = uc ($tmp);
68         $tmp =~ s/\W//g;
69
70         if ($tmp eq 'NONE' or $tmp eq 'LIGHT'
71                         or $tmp eq 'MEDIUM'
72                         or $tmp eq 'HARD')
73         {
74                 $UNSHARP = $tmp;
75         }
76         else
77         {
78                 print STDERR $/, __FILE__, ": ``$tmp'' is not a valid value for config option ``unsharp''.",
79                 $/, __FILE__, ": Using standard value ``MEDIUM''.";
80         }
81 }
82
83 # TODO
84 # - lastrun
85 # - total lines
86
87 my $VERSION = '$Id: Core.pm,v 1.14 2004/10/31 15:00:32 octo Exp $';
88 print STDERR $/, __FILE__, ": $VERSION" if ($::DEBUG);
89
90 return (1);
91
92 =head1 EXPORTED FUNCTIONS
93
94 =over 4
95
96 =item B<store> (I<$type>, I<$data>)
97
98 Passes I<$data> (a hashref) to all plugins which registered for I<$type>. This
99 is the actual workhorse when parsing the file since it will be called once for
100 every line found.
101
102 It will fill I<$data> with I<host>, I<user> and I<ident> if these fields are
103 missing but have been seen for this nick before.
104
105 =cut
106
107 sub store
108 {
109         my $data = shift;
110         my $type = $data->{'type'};
111         my ($nick, $user, $host);
112         my $ident;
113
114         if (!defined ($type))
115         {
116                 print STDERR $/, __FILE__, ": Plugin data did not include a type. This line will be skipped." if ($::DEBUG & 0x20);
117                 return (undef);
118         }
119
120         if (!defined ($data->{'nick'}))
121         {
122                 print STDERR $/, __FILE__, ": Plugin data did not include a nick. This line will be skipped." if ($::DEBUG & 0x20);
123                 return (undef);
124         }
125
126         $nick = $data->{'nick'};
127
128         if (defined ($data->{'host'}))
129         {
130                 my $chatter;
131                 my $counter;
132
133                 ($user, $host) = unsharp ($data->{'host'});
134                 $ident = "$user\@$host";
135
136                 $data->{'host'} = $host;
137                 $data->{'user'} = $user;
138                 $data->{'ident'} = $ident;
139                 
140                 $Nick2Ident->put ($nick, $ident);
141
142                 $chatter = "$nick!$ident";
143                 ($counter) = $ChatterList->get ($chatter);
144                 $counter ||= 0; $counter++;
145                 $ChatterList->put ($chatter, $counter);
146         }
147         elsif (($ident) = $Nick2Ident->get ($nick))
148         {
149                 my $chatter = "$nick!$ident";
150                 my $counter;
151                 ($user, $host) = split (m/@/, $ident);
152
153                 $data->{'host'} = $host;
154                 $data->{'user'} = $user;
155                 $data->{'ident'} = $ident;
156
157                 ($counter) = $ChatterList->get ($chatter);
158                 $counter ||= 0; $counter++;
159                 $ChatterList->put ($chatter, $counter);
160         }
161         else
162         {
163                 $data->{'host'}  = $host  = '';
164                 $data->{'user'}  = $user  = '';
165                 $data->{'ident'} = $ident = '';
166         }
167
168         if ($::DEBUG & 0x0100)
169         {
170                 print STDERR $/, __FILE__, ": id ($nick) = ", $ident;
171         }
172
173         if (defined ($data->{'channel'}))
174         {
175                 my $chan = lc ($data->{'channel'});
176                 my ($count) = $ChannelNames->get ($chan);
177                 $count ||= 0; $count++;
178                 $ChannelNames->put ($chan, $count);
179         }
180
181         if (!defined ($data->{'epoch'}))
182         {
183                 $data->{'epoch'} = get_absolute_time ();
184         }
185
186         if ($::DEBUG & 0x400)
187         {
188                 my @keys = keys (%$data);
189                 for (sort (@keys))
190                 {
191                         my $key = $_;
192                         my $val = $data->{$key};
193                         print STDERR $/, __FILE__, ': ';
194                         printf STDERR ("%10s: %s", $key, $val);
195                 }
196         }
197
198         # TODO
199         #$DATA->{'total_lines'}++;
200
201         if (defined ($PluginCallbacks->{$type}))
202         {
203                 for (@{$PluginCallbacks->{$type}})
204                 {
205                         $_->($data);
206                 }
207         }
208
209         return (1);
210 }
211
212 =item (I<$user>, I<$host>) = B<unsharp> (I<$ident>)
213
214 Takes an ident (i.e. a user-host-pair, e.g. I<user@host.domain.com> or
215 I<login@123.123.123.123>) and "unsharps it". The unsharp version is then
216 returned.
217
218 What unsharp exactly does is described in the F<README>.
219
220 =cut
221
222 sub unsharp
223 {
224         my $ident = shift;
225
226         my $user;
227         my $host;
228         my @parts;
229         my $num_parts;
230         my $i;
231
232         print STDERR $/, __FILE__, ": Unsharp ``$ident''" if ($::DEBUG & 0x100);
233         
234         ($user, $host) = split (m/@/, $ident, 2);
235
236         @parts = split (m/\./, $host);
237         $num_parts = scalar (@parts);
238         
239         if (($UNSHARP ne 'NONE')
240                         and ($user =~ m/^[\~\^\-\+\=](.+)$/))
241         {
242                 $user = $1;
243         }
244         
245         if ($UNSHARP eq 'NONE')
246         {
247                 return ($user, $host);
248         }
249         elsif ($host =~ m/^[\d\.]{7,15}$/)
250         {
251                 if ($UNSHARP ne 'LIGHT')
252                 {
253                         $parts[-1] = '*';
254                 }
255         }
256         else
257         {
258                 for ($i = 0; $i < ($num_parts - 2); $i++)
259                 {
260                         if ($UNSHARP eq 'LIGHT')
261                         {
262                                 if ($parts[$i] !~ s/\d+/*/g)
263                                 {
264                                         last;
265                                 }
266                         }
267                         elsif ($UNSHARP eq 'MEDIUM')
268                         {
269                                 if ($parts[$i] =~ m/\d/)
270                                 {
271                                         $parts[$i] = '*';
272                                 }
273                                 else
274                                 {
275                                         last;
276                                 }
277                         }
278                         else # ($UNSHARP eq 'HARD')
279                         {
280                                 $parts[$i] = '*';
281                         }
282                 }
283         }
284
285         $host = lc (join ('.', @parts));
286         $host =~ s/\*(?:\.\*)+/*/;
287         
288         print STDERR " -> ``$user\@$host''" if ($::DEBUG & 0x100);
289         return ($user, $host);
290 }
291
292 =item B<calculate_nicks> ()
293
294 Iterates over all chatters found so far, trying to figure out which belong to
295 the same person. This function has to be called before any calls to
296 B<get_all_nicks>, B<get_main_nick>, B<get_print_name> and B<nick_to_ident>.
297
298 This is normally the step after having parsed all files and before doing any
299 output. After this function has been run all the other informative functions
300 return actually usefull information..
301
302 It does the following: First, it iterates over all chatters and splits them up
303 into nicks and idents. If a (user)name is found for the ident it (the ident) is
304 replaced with it (the name). 
305
306 In the second step we iterate over all nicks that have been found and
307 determines the most active ident for each nick. After this has been done each
308 nick is associated with exactly one ident, but B<not> vice versa. 
309
310 The final step is to iterate over all idents and determine the most active nick
311 for each ident. After some thought you will agree that now each ident exists
312 only once and so does every nick.
313
314 =cut
315
316 sub calculate_nicks
317 {
318         my $nicks      = {};
319         my $idents     = {};
320         my $name2nick  = {};
321         my $name2ident = {};
322         
323         for ($ChatterList->keys ())
324         {
325                 my $chatter = shift;
326                 my ($nick, $ident) = split (m/!/, $chatter);
327                 my $name = ident_to_name ($ident);
328                 my ($counter) = $ChatterList->get ($chatter);
329
330                 $nicks->{$nick}{$ident} = 0 unless (defined ($nicks->{$nick}{$ident}));
331                 $nicks->{$nick}{$ident} += $counter;
332         }
333
334         for (keys %$nicks)
335         {
336                 my $this_nick = $_;
337                 my $this_ident = 'unidentified';
338                 my $this_name = '';
339                 my $this_total = 0;
340                 my $this_max = 0;
341
342                 for (keys %{$nicks->{$this_nick}})
343                 {
344                         my $ident = $_;
345                         my $name = ident_to_name ($ident);
346                         my $num = $nicks->{$this_nick}{$ident};
347                         
348                         $this_total += $num;
349
350                         if ($name)
351                         {
352                                 if (($num >= $this_max) or !$this_name)
353                                 {
354                                         $this_max = $num;
355                                         $this_ident = $ident;
356                                         $this_name = $name;
357                                 }
358                         }
359                         else
360                         {
361                                 if (($num >= $this_max) and !$this_name)
362                                 {
363                                         $this_max = $num;
364                                         $this_ident = $ident;
365                                 }
366                         }
367                 }
368
369                 print $/, __FILE__, ": max_ident ($this_nick) = $this_ident" if ($::DEBUG & 0x100);
370
371                 if ($this_ident ne 'unidentified')
372                 {
373                         if ($this_name)
374                         {
375                                 $name2nick->{$this_name}{$this_nick} = 0 unless (defined ($name2nick->{$this_name}{$this_nick}));
376                                 $name2nick->{$this_name}{$this_nick} += $this_total;
377
378                                 $name2ident->{$this_name}{$this_ident} = 0 unless (defined ($name2nick->{$this_name}{$this_ident}));
379                                 $name2ident->{$this_name}{$this_ident} += $this_total;
380                         }
381                         else
382                         {
383                                 $idents->{$this_ident}{$this_nick} = 0 unless (defined ($idents->{$this_ident}{$this_nick}));
384                                 $idents->{$this_ident}{$this_nick} += $this_total;
385                         }
386                 }
387                 elsif ($::DEBUG & 0x100)
388                 {
389                         print STDERR $/, __FILE__, ": Ignoring unidentified nick ``$this_nick''";
390                 }
391         }
392
393         for (keys %$idents)
394         {
395                 my $this_ident = $_;
396                 my $this_nick = '';
397                 my $this_max = 0;
398                 my @other_nicks = ();
399
400                 my @nicks = keys (%{$idents->{$this_ident}});
401
402                 for (@nicks)
403                 {
404                         my $nick = $_;
405                         my $num = $idents->{$this_ident}{$nick};
406
407                         if ($num > $this_max)
408                         {
409                                 if ($this_nick) { push (@other_nicks, $this_nick); }
410                                 $this_nick = $nick;
411                                 $this_max = $num;
412                         }
413                         else
414                         {
415                                 push (@other_nicks, $nick);
416                         }
417                 }
418
419                 print STDERR $/, __FILE__, ": max_nick ($this_ident) = $this_nick" if ($::DEBUG & 0x100);
420
421                 for (@other_nicks, $this_nick)
422                 {
423                         push (@AllNicks, $_);
424                         $NickToNick{$_} = $this_nick;
425                         $NickToIdent{$_} = $this_ident;
426                 }
427
428                 $IdentToNick{$this_ident} = $this_nick;
429         }
430
431         for (keys %$name2nick)
432         {
433                 my $name = $_;
434                 my $max_num = 0;
435                 my $max_nick = '';
436                 my $max_ident = '';
437
438                 my @other_nicks = ();
439                 my @other_idents = ();
440
441                 for (keys %{$name2nick->{$name}})
442                 {
443                         my $nick = $_;
444                         my $num = $name2nick->{$name}{$nick};
445
446                         if ($num > $max_num)
447                         {
448                                 push (@other_nicks, $max_nick) if ($max_nick);
449                                 $max_nick = $nick;
450                                 $max_num  = $num;
451                         }
452                         else
453                         {
454                                 push (@other_nicks, $nick);
455                         }
456                 }
457
458                 $max_num = 0;
459                 for (keys %{$name2ident->{$name}})
460                 {
461                         my $ident = $_;
462                         my $num = $name2ident->{$name}{$ident};
463
464                         if ($num > $max_num)
465                         {
466                                 push (@other_idents, $max_ident) if ($max_ident);
467                                 $max_ident = $ident;
468                                 $max_num  = $num;
469                         }
470                         else
471                         {
472                                 push (@other_idents, $ident);
473                         }
474                 }
475
476                 for (@other_nicks, $max_nick)
477                 {
478                         push (@AllNicks, $_);
479                         $NickToNick{$_} = $max_nick;
480                         $NickToIdent{$_} = $max_ident;
481                 }
482
483                 for (@other_idents, $max_ident)
484                 {
485                         $IdentToNick{$_} = $max_nick;
486                 }
487         }
488 }
489
490 =item I<@nicks> = B<get_all_nicks> ()
491
492 Returns an array of all seen nicks.
493
494 =cut
495
496 sub get_all_nicks
497 {
498         return (@AllNicks);
499 }
500
501 =item I<$channel> = B<get_channel> ()
502
503 Returns the name of the channel we're generating stats for.
504
505 =cut
506
507 sub get_channel
508 {
509         my $chan = '#unknown'
510         ;
511         if (get_config ('channel'))
512         {
513                 $chan = get_config ('channel');
514         }
515         else
516         {
517                 my $max = 0;
518                 for ($ChannelNames->keys ())
519                 {
520                         my $c = $_;
521                         my ($num) = $ChannelNames->get ($c);
522                         if (defined ($num) and ($num > $max))
523                         {
524                                 $max = $num;
525                                 $chan = $c;
526                         }
527                 }
528         }
529
530         # Fix network-safe channel named (RFC 2811)
531         if ($chan =~ m/^![A-Z0-9]{5}.+/)
532         {
533                 $chan =~ s/[A-Z0-9]{5}//;
534         }
535
536         return ($chan);
537 }
538
539 =item I<$main> = B<get_main_nick> (I<$nick>)
540
541 Returns the main nick for I<$nick> or an empty string if the nick is unknown..
542
543 =cut
544
545 sub get_main_nick
546 {
547         my $nick = shift;
548         if (defined ($NickToNick{$nick}))
549         {
550                 return ($NickToNick{$nick});
551         }
552         else
553         {
554                 return ('');
555         }
556 }
557
558 =item I<$ident> = B<nick_to_ident> (I<$nick>)
559
560 Returns the ident for this nick or an empty string if unknown. Before
561 B<calculate_nicks> is run it will use the database to find the most recent
562 mapping. After B<calculate_nicks> is run the calculated mapping will be used.
563
564 =cut
565
566 sub nick_to_ident
567 {
568         my $nick = shift;
569         my $ident = '';
570
571         if (%NickToIdent)
572         {
573                 if (defined ($NickToIdent{$nick}))
574                 {
575                         $ident = $NickToIdent{$nick};
576                 }
577         }
578         else
579         {
580                 ($ident) = $Nick2Ident->get ($nick);
581                 $ident ||= '';
582         }
583
584         return ($ident);
585 }
586
587 =item I<$nick> = B<ident_to_nick> (I<$ident>)
588
589 Returns the nick for the given ident or an empty string if unknown.
590
591 =cut
592
593 sub ident_to_nick
594 {
595         my $ident = shift;
596
597         if (defined ($IdentToNick{$ident}))
598         {
599                 return ($IdentToNick{$ident});
600         }
601         else
602         {
603                 return ('');
604         }
605 }
606
607 =item I<$lines> = B<get_total_lines> ()
608
609 Returns the total number of lines parsed so far.
610
611 =cut
612
613 sub get_total_lines
614 {
615         # TODO
616         #return ($DATA->{'total_lines'});
617 }
618
619 =item B<nick_rename> (I<$old_nick>, I<$new_nick>)
620
621 Keeps track of a nick's hostname if the nick changes.
622
623 =cut
624
625 sub nick_rename
626 {
627         my $old_nick = shift;
628         my $new_nick = shift;
629         my $ident;
630
631         ($ident) = $Nick2Ident->get ($old_nick);
632
633         if (defined ($ident) and ($ident))
634         {
635                 $Nick2Ident->put ($new_nick, $ident);
636         }
637 }
638
639 =item B<print_output> ()
640
641 Print the output. Should be called only once..
642
643 =cut
644
645 sub print_output
646 {
647         if (!get_total_lines ())
648         {
649                 print STDERR <<'MESSAGE';
650
651 ERROR: No data found
652
653 The most common reasons for this are:
654 - The logfile used was empty.
655 - The ``logtype'' setting did not match the logfile.
656 - The logfile did not include a date.
657
658 MESSAGE
659                 return;
660         }
661         
662         calculate_nicks ();
663
664         for (@$OUTPUT)
665         {
666                 &$_ ();
667         }
668 }
669
670 =item I<$data> = B<register_plugin> (I<$type>, I<$sub_ref>)
671
672 Register a subroutine for the given type. Returns a reference to the internal
673 data object. This will change soon, don't use it anymore if possible.
674
675 =cut
676
677 sub register_plugin
678 {
679         my $type = shift;
680         my $sub_ref = shift;
681
682         $type = uc ($type);
683         if (ref ($sub_ref) ne "CODE")
684         {
685                 print STDERR $/, __FILE__, ": Plugin tried to register a non-code reference. Ignoring it.";
686                 return (undef);
687         }
688
689         if ($type eq 'OUTPUT')
690         {
691                 push (@$OUTPUT, $sub_ref);
692         }
693         else
694         {
695                 if (!defined ($PluginCallbacks->{$type}))
696                 {
697                         $PluginCallbacks->{$type} = [];
698                 }
699         }
700
701         push (@{$PluginCallbacks->{$type}}, $sub_ref);
702
703         print STDERR $/, __FILE__, ': ', scalar (caller ()), " registered for ``$type''." if ($::DEBUG & 0x800);
704 }
705
706 =back
707
708 =head1 AUTHOR
709
710 Florian octo Forster E<lt>octo at verplant.orgE<gt>
711
712 =cut