1 package Onis::Data::Core;
5 Onis::Data::Core - User management
9 Store data to the internal structure, care about users, nicks and idents and
10 dispatch to plugins. The core of the data even..
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);
23 =head1 NAMING CONVENTION
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>.
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.
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>.
37 our $GeneralCounters = Onis::Data::Persistent->new ('GeneralCounters', 'key', 'value');
38 our $NickToIdentCache = Onis::Data::Persistent->new ('NickToIdentCache', 'nick', 'ident');
39 our $ChatterList = Onis::Data::Persistent->new ('ChatterList', 'chatter', 'counter');
40 our $ChannelNames = Onis::Data::Persistent->new ('ChannelNames', 'channel', 'counter');
42 @Onis::Data::Core::EXPORT_OK =
44 store unsharp calculate_nicks
46 get_all_nicks get_channel get_main_nick nick_to_ident ident_to_nick nick_to_name
47 get_total_lines get_most_recent_time nick_rename print_output register_plugin
49 @Onis::Data::Core::ISA = ('Exporter');
51 our $LinesThisRun = 0;
53 our $PluginCallbacks = {};
54 our $OutputCallbacks = [];
58 our %NickToIdent = ();
59 our %IdentToNick = ();
61 our $UNSHARP = 'MEDIUM';
62 if (get_config ('unsharp'))
64 my $tmp = get_config ('unsharp');
68 if ($tmp eq 'NONE' or $tmp eq 'LIGHT'
76 print STDERR $/, __FILE__, ": ``$tmp'' is not a valid value for config option ``unsharp''.",
77 $/, __FILE__, ": Using standard value ``MEDIUM''.";
85 my $VERSION = '$Id: Core.pm,v 1.14 2004/10/31 15:00:32 octo Exp $';
86 print STDERR $/, __FILE__, ": $VERSION" if ($::DEBUG);
90 =head1 EXPORTED FUNCTIONS
94 =item B<store> (I<$type>, I<$data>)
96 Passes I<$data> (a hashref) to all plugins which registered for I<$type>. This
97 is the actual workhorse when parsing the file since it will be called once for
100 It will fill I<$data> with I<host>, I<user> and I<ident> if these fields are
101 missing but have been seen for this nick before.
108 my $type = $data->{'type'};
109 my ($nick, $user, $host);
112 if (!defined ($type))
114 print STDERR $/, __FILE__, ": Plugin data did not include a type. This line will be skipped." if ($::DEBUG & 0x20);
118 if (!defined ($data->{'nick'}))
120 print STDERR $/, __FILE__, ": Plugin data did not include a nick. This line will be skipped." if ($::DEBUG & 0x20);
124 $nick = $data->{'nick'};
126 if (defined ($data->{'host'}))
131 ($user, $host) = unsharp ($data->{'host'});
132 $ident = "$user\@$host";
134 $data->{'host'} = $host;
135 $data->{'user'} = $user;
136 $data->{'ident'} = $ident;
138 $NickToIdentCache->put ($nick, $ident);
140 $chatter = "$nick!$ident";
141 ($counter) = $ChatterList->get ($chatter);
142 $counter ||= 0; $counter++;
143 $ChatterList->put ($chatter, $counter);
145 elsif (($ident) = $NickToIdentCache->get ($nick))
147 my $chatter = "$nick!$ident";
149 ($user, $host) = split (m/@/, $ident);
151 $data->{'host'} = $host;
152 $data->{'user'} = $user;
153 $data->{'ident'} = $ident;
155 ($counter) = $ChatterList->get ($chatter);
156 $counter ||= 0; $counter++;
157 $ChatterList->put ($chatter, $counter);
161 $data->{'host'} = $host = '';
162 $data->{'user'} = $user = '';
163 $data->{'ident'} = $ident = '';
166 if ($::DEBUG & 0x0100)
168 print STDERR $/, __FILE__, ": id ($nick) = ", $ident;
171 if (defined ($data->{'channel'}))
173 my $chan = lc ($data->{'channel'});
174 my ($count) = $ChannelNames->get ($chan);
175 $count ||= 0; $count++;
176 $ChannelNames->put ($chan, $count);
179 if (!defined ($data->{'epoch'}))
181 $data->{'epoch'} = get_absolute_time ();
184 if ($::DEBUG & 0x400)
186 my @keys = keys (%$data);
190 my $val = $data->{$key};
191 print STDERR $/, __FILE__, ': ';
192 printf STDERR ("%10s: %s", $key, $val);
197 my ($counter) = $GeneralCounters->get ('lines_total');
200 $GeneralCounters->put ('lines_total', $counter);
202 my ($time) = $GeneralCounters->get ('most_recent_time');
204 $time = $data->{'epoch'} if ($time < $data->{'epoch'});
205 $GeneralCounters->put ('most_recent_time', $time);
210 if (defined ($PluginCallbacks->{$type}))
212 for (@{$PluginCallbacks->{$type}})
221 =item (I<$user>, I<$host>) = B<unsharp> (I<$ident>)
223 Takes an ident (i.e. a user-host-pair, e.g. I<user@host.domain.com> or
224 I<login@123.123.123.123>) and "unsharps it". The unsharp version is then
227 What unsharp exactly does is described in the F<README>.
241 print STDERR $/, __FILE__, ": Unsharp ``$ident''" if ($::DEBUG & 0x100);
243 ($user, $host) = split (m/@/, $ident, 2);
245 @parts = split (m/\./, $host);
246 $num_parts = scalar (@parts);
248 if (($UNSHARP ne 'NONE')
249 and ($user =~ m/^[\~\^\-\+\=](.+)$/))
254 if ($UNSHARP eq 'NONE')
256 return ($user, $host);
258 elsif ($host =~ m/^[\d\.]{7,15}$/)
260 if ($UNSHARP ne 'LIGHT')
267 for ($i = 0; $i < ($num_parts - 2); $i++)
269 if ($UNSHARP eq 'LIGHT')
271 if ($parts[$i] !~ s/\d+/*/g)
276 elsif ($UNSHARP eq 'MEDIUM')
278 if ($parts[$i] =~ m/\d/)
287 else # ($UNSHARP eq 'HARD')
294 $host = lc (join ('.', @parts));
295 $host =~ s/\*(?:\.\*)+/*/;
297 print STDERR " -> ``$user\@$host''" if ($::DEBUG & 0x100);
298 return ($user, $host);
301 =item B<calculate_nicks> ()
303 Iterates over all chatters found so far, trying to figure out which belong to
304 the same person. This function has to be called before any calls to
305 B<get_all_nicks>, B<get_main_nick>, B<get_print_name> and B<nick_to_ident>.
307 This is normally the step after having parsed all files and before doing any
308 output. After this function has been run all the other informative functions
309 return actually usefull information..
311 It does the following: First, it iterates over all chatters and splits them up
312 into nicks and idents. If a (user)name is found for the ident it (the ident) is
313 replaced with it (the name).
315 In the second step we iterate over all nicks that have been found and
316 determines the most active ident for each nick. After this has been done each
317 nick is associated with exactly one ident, but B<not> vice versa.
319 The final step is to iterate over all idents and determine the most active nick
320 for each ident. After some thought you will agree that now each ident exists
321 only once and so does every nick.
332 for ($ChatterList->keys ())
335 my ($nick, $ident) = split (m/!/, $chatter);
336 my $name = ident_to_name ($ident);
337 my ($counter) = $ChatterList->get ($chatter);
339 $nicks->{$nick}{$ident} = 0 unless (defined ($nicks->{$nick}{$ident}));
340 $nicks->{$nick}{$ident} += $counter;
346 my $this_ident = 'unidentified';
351 for (keys %{$nicks->{$this_nick}})
354 my $name = ident_to_name ($ident);
355 my $num = $nicks->{$this_nick}{$ident};
361 if (($num >= $this_max) or !$this_name)
364 $this_ident = $ident;
370 if (($num >= $this_max) and !$this_name)
373 $this_ident = $ident;
378 print $/, __FILE__, ": max_ident ($this_nick) = $this_ident" if ($::DEBUG & 0x100);
380 if ($this_ident ne 'unidentified')
384 $name2nick->{$this_name}{$this_nick} = 0 unless (defined ($name2nick->{$this_name}{$this_nick}));
385 $name2nick->{$this_name}{$this_nick} += $this_total;
387 $name2ident->{$this_name}{$this_ident} = 0 unless (defined ($name2nick->{$this_name}{$this_ident}));
388 $name2ident->{$this_name}{$this_ident} += $this_total;
392 $idents->{$this_ident}{$this_nick} = 0 unless (defined ($idents->{$this_ident}{$this_nick}));
393 $idents->{$this_ident}{$this_nick} += $this_total;
396 elsif ($::DEBUG & 0x100)
398 print STDERR $/, __FILE__, ": Ignoring unidentified nick ``$this_nick''";
407 my @other_nicks = ();
409 my @nicks = keys (%{$idents->{$this_ident}});
414 my $num = $idents->{$this_ident}{$nick};
416 if ($num > $this_max)
418 if ($this_nick) { push (@other_nicks, $this_nick); }
424 push (@other_nicks, $nick);
428 print STDERR $/, __FILE__, ": max_nick ($this_ident) = $this_nick" if ($::DEBUG & 0x100);
430 for (@other_nicks, $this_nick)
432 push (@AllNicks, $_);
433 $NickToNick{$_} = $this_nick;
434 $NickToIdent{$_} = $this_ident;
437 $IdentToNick{$this_ident} = $this_nick;
440 for (keys %$name2nick)
447 my @other_nicks = ();
448 my @other_idents = ();
450 for (keys %{$name2nick->{$name}})
453 my $num = $name2nick->{$name}{$nick};
457 push (@other_nicks, $max_nick) if ($max_nick);
463 push (@other_nicks, $nick);
468 for (keys %{$name2ident->{$name}})
471 my $num = $name2ident->{$name}{$ident};
475 push (@other_idents, $max_ident) if ($max_ident);
481 push (@other_idents, $ident);
485 for (@other_nicks, $max_nick)
487 push (@AllNicks, $_);
488 $NickToNick{$_} = $max_nick;
489 $NickToIdent{$_} = $max_ident;
492 for (@other_idents, $max_ident)
494 $IdentToNick{$_} = $max_nick;
499 =item I<@nicks> = B<get_all_nicks> ()
501 Returns an array of all seen nicks.
510 =item I<$channel> = B<get_channel> ()
512 Returns the name of the channel we're generating stats for.
518 my $chan = '#unknown'
520 if (get_config ('channel'))
522 $chan = get_config ('channel');
527 for ($ChannelNames->keys ())
530 my ($num) = $ChannelNames->get ($c);
531 if (defined ($num) and ($num > $max))
539 # Fix network-safe channel named (RFC 2811)
540 if ($chan =~ m/^![A-Z0-9]{5}.+/)
542 $chan =~ s/[A-Z0-9]{5}//;
548 =item I<$main> = B<get_main_nick> (I<$nick>)
550 Returns the main nick for I<$nick> or an empty string if the nick is unknown..
557 if (defined ($NickToNick{$nick}))
559 return ($NickToNick{$nick});
567 =item I<$ident> = B<nick_to_ident> (I<$nick>)
569 Returns the ident for this nick or an empty string if unknown. Before
570 B<calculate_nicks> is run it will use the database to find the most recent
571 mapping. After B<calculate_nicks> is run the calculated mapping will be used.
582 if (defined ($NickToIdent{$nick}))
584 $ident = $NickToIdent{$nick};
589 ($ident) = $NickToIdentCache->get ($nick);
596 =item I<$nick> = B<ident_to_nick> (I<$ident>)
598 Returns the nick for the given ident or an empty string if unknown.
606 if (defined ($IdentToNick{$ident}))
608 return ($IdentToNick{$ident});
616 =item I<$name> = B<nick_to_name> (I<$nick>)
618 Return the name associated with I<$nick>. This function uses B<ident_to_name>
619 (see L<Onis::Users>).
626 my $ident = nick_to_ident ($nick);
630 return (ident_to_name ($ident));
638 =item I<$lines> = B<get_total_lines> ()
640 Returns the total number of lines parsed so far.
646 my ($total) = $GeneralCounters->get ('lines_total');
648 return (qw()) unless ($total);
650 return ($total, $LinesThisRun);
653 =item I<$epoch> = B<get_most_recent_time> ()
655 Returns the epoch of the most recent line received from the parser.
659 sub get_most_recent_time
661 my ($time) = $GeneralCounters->get ('most_recent_time');
667 =item B<nick_rename> (I<$old_nick>, I<$new_nick>)
669 Keeps track of a nick's hostname if the nick changes.
675 my $old_nick = shift;
676 my $new_nick = shift;
679 ($ident) = $NickToIdentCache->get ($old_nick);
681 if (defined ($ident) and ($ident))
683 $NickToIdentCache->put ($new_nick, $ident);
687 =item B<print_output> ()
689 Print the output. Should be called only once..
696 if (!get_total_lines ())
698 print STDERR <<'MESSAGE';
702 The most common reasons for this are:
703 - The logfile used was empty.
704 - The ``logtype'' setting did not match the logfile.
705 - The logfile did not include a date.
713 for (@$OutputCallbacks)
719 =item I<$data> = B<register_plugin> (I<$type>, I<$sub_ref>)
721 Register a subroutine for the given type. Returns a reference to the internal
722 data object. This will change soon, don't use it anymore if possible.
732 if (ref ($sub_ref) ne "CODE")
734 print STDERR $/, __FILE__, ": Plugin tried to register a non-code reference. Ignoring it.";
738 if ($type eq 'OUTPUT')
740 push (@$OutputCallbacks, $sub_ref);
744 if (!defined ($PluginCallbacks->{$type}))
746 $PluginCallbacks->{$type} = [];
750 push (@{$PluginCallbacks->{$type}}, $sub_ref);
752 print STDERR $/, __FILE__, ': ', scalar (caller ()), " registered for ``$type''." if ($::DEBUG & 0x800);
759 Florian octo Forster E<lt>octo at verplant.orgE<gt>