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 $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');
41 @Onis::Data::Core::EXPORT_OK =
43 store unsharp calculate_nicks
45 get_all_nicks get_channel get_main_nick nick_to_ident ident_to_nick nick_to_name
46 get_total_lines nick_rename print_output register_plugin
48 @Onis::Data::Core::ISA = ('Exporter');
50 our $PluginCallbacks = {};
51 our $OutputCallbacks = [];
55 our %NickToIdent = ();
56 our %IdentToNick = ();
58 our $UNSHARP = 'MEDIUM';
59 if (get_config ('unsharp'))
61 my $tmp = get_config ('unsharp');
65 if ($tmp eq 'NONE' or $tmp eq 'LIGHT'
73 print STDERR $/, __FILE__, ": ``$tmp'' is not a valid value for config option ``unsharp''.",
74 $/, __FILE__, ": Using standard value ``MEDIUM''.";
82 my $VERSION = '$Id: Core.pm,v 1.14 2004/10/31 15:00:32 octo Exp $';
83 print STDERR $/, __FILE__, ": $VERSION" if ($::DEBUG);
87 =head1 EXPORTED FUNCTIONS
91 =item B<store> (I<$type>, I<$data>)
93 Passes I<$data> (a hashref) to all plugins which registered for I<$type>. This
94 is the actual workhorse when parsing the file since it will be called once for
97 It will fill I<$data> with I<host>, I<user> and I<ident> if these fields are
98 missing but have been seen for this nick before.
105 my $type = $data->{'type'};
106 my ($nick, $user, $host);
109 if (!defined ($type))
111 print STDERR $/, __FILE__, ": Plugin data did not include a type. This line will be skipped." if ($::DEBUG & 0x20);
115 if (!defined ($data->{'nick'}))
117 print STDERR $/, __FILE__, ": Plugin data did not include a nick. This line will be skipped." if ($::DEBUG & 0x20);
121 $nick = $data->{'nick'};
123 if (defined ($data->{'host'}))
128 ($user, $host) = unsharp ($data->{'host'});
129 $ident = "$user\@$host";
131 $data->{'host'} = $host;
132 $data->{'user'} = $user;
133 $data->{'ident'} = $ident;
135 $Nick2Ident->put ($nick, $ident);
137 $chatter = "$nick!$ident";
138 ($counter) = $ChatterList->get ($chatter);
139 $counter ||= 0; $counter++;
140 $ChatterList->put ($chatter, $counter);
142 elsif (($ident) = $Nick2Ident->get ($nick))
144 my $chatter = "$nick!$ident";
146 ($user, $host) = split (m/@/, $ident);
148 $data->{'host'} = $host;
149 $data->{'user'} = $user;
150 $data->{'ident'} = $ident;
152 ($counter) = $ChatterList->get ($chatter);
153 $counter ||= 0; $counter++;
154 $ChatterList->put ($chatter, $counter);
158 $data->{'host'} = $host = '';
159 $data->{'user'} = $user = '';
160 $data->{'ident'} = $ident = '';
163 if ($::DEBUG & 0x0100)
165 print STDERR $/, __FILE__, ": id ($nick) = ", $ident;
168 if (defined ($data->{'channel'}))
170 my $chan = lc ($data->{'channel'});
171 my ($count) = $ChannelNames->get ($chan);
172 $count ||= 0; $count++;
173 $ChannelNames->put ($chan, $count);
176 if (!defined ($data->{'epoch'}))
178 $data->{'epoch'} = get_absolute_time ();
181 if ($::DEBUG & 0x400)
183 my @keys = keys (%$data);
187 my $val = $data->{$key};
188 print STDERR $/, __FILE__, ': ';
189 printf STDERR ("%10s: %s", $key, $val);
194 #$DATA->{'total_lines'}++;
196 if (defined ($PluginCallbacks->{$type}))
198 for (@{$PluginCallbacks->{$type}})
207 =item (I<$user>, I<$host>) = B<unsharp> (I<$ident>)
209 Takes an ident (i.e. a user-host-pair, e.g. I<user@host.domain.com> or
210 I<login@123.123.123.123>) and "unsharps it". The unsharp version is then
213 What unsharp exactly does is described in the F<README>.
227 print STDERR $/, __FILE__, ": Unsharp ``$ident''" if ($::DEBUG & 0x100);
229 ($user, $host) = split (m/@/, $ident, 2);
231 @parts = split (m/\./, $host);
232 $num_parts = scalar (@parts);
234 if (($UNSHARP ne 'NONE')
235 and ($user =~ m/^[\~\^\-\+\=](.+)$/))
240 if ($UNSHARP eq 'NONE')
242 return ($user, $host);
244 elsif ($host =~ m/^[\d\.]{7,15}$/)
246 if ($UNSHARP ne 'LIGHT')
253 for ($i = 0; $i < ($num_parts - 2); $i++)
255 if ($UNSHARP eq 'LIGHT')
257 if ($parts[$i] !~ s/\d+/*/g)
262 elsif ($UNSHARP eq 'MEDIUM')
264 if ($parts[$i] =~ m/\d/)
273 else # ($UNSHARP eq 'HARD')
280 $host = lc (join ('.', @parts));
281 $host =~ s/\*(?:\.\*)+/*/;
283 print STDERR " -> ``$user\@$host''" if ($::DEBUG & 0x100);
284 return ($user, $host);
287 =item B<calculate_nicks> ()
289 Iterates over all chatters found so far, trying to figure out which belong to
290 the same person. This function has to be called before any calls to
291 B<get_all_nicks>, B<get_main_nick>, B<get_print_name> and B<nick_to_ident>.
293 This is normally the step after having parsed all files and before doing any
294 output. After this function has been run all the other informative functions
295 return actually usefull information..
297 It does the following: First, it iterates over all chatters and splits them up
298 into nicks and idents. If a (user)name is found for the ident it (the ident) is
299 replaced with it (the name).
301 In the second step we iterate over all nicks that have been found and
302 determines the most active ident for each nick. After this has been done each
303 nick is associated with exactly one ident, but B<not> vice versa.
305 The final step is to iterate over all idents and determine the most active nick
306 for each ident. After some thought you will agree that now each ident exists
307 only once and so does every nick.
318 for ($ChatterList->keys ())
321 my ($nick, $ident) = split (m/!/, $chatter);
322 my $name = ident_to_name ($ident);
323 my ($counter) = $ChatterList->get ($chatter);
325 $nicks->{$nick}{$ident} = 0 unless (defined ($nicks->{$nick}{$ident}));
326 $nicks->{$nick}{$ident} += $counter;
332 my $this_ident = 'unidentified';
337 for (keys %{$nicks->{$this_nick}})
340 my $name = ident_to_name ($ident);
341 my $num = $nicks->{$this_nick}{$ident};
347 if (($num >= $this_max) or !$this_name)
350 $this_ident = $ident;
356 if (($num >= $this_max) and !$this_name)
359 $this_ident = $ident;
364 print $/, __FILE__, ": max_ident ($this_nick) = $this_ident" if ($::DEBUG & 0x100);
366 if ($this_ident ne 'unidentified')
370 $name2nick->{$this_name}{$this_nick} = 0 unless (defined ($name2nick->{$this_name}{$this_nick}));
371 $name2nick->{$this_name}{$this_nick} += $this_total;
373 $name2ident->{$this_name}{$this_ident} = 0 unless (defined ($name2nick->{$this_name}{$this_ident}));
374 $name2ident->{$this_name}{$this_ident} += $this_total;
378 $idents->{$this_ident}{$this_nick} = 0 unless (defined ($idents->{$this_ident}{$this_nick}));
379 $idents->{$this_ident}{$this_nick} += $this_total;
382 elsif ($::DEBUG & 0x100)
384 print STDERR $/, __FILE__, ": Ignoring unidentified nick ``$this_nick''";
393 my @other_nicks = ();
395 my @nicks = keys (%{$idents->{$this_ident}});
400 my $num = $idents->{$this_ident}{$nick};
402 if ($num > $this_max)
404 if ($this_nick) { push (@other_nicks, $this_nick); }
410 push (@other_nicks, $nick);
414 print STDERR $/, __FILE__, ": max_nick ($this_ident) = $this_nick" if ($::DEBUG & 0x100);
416 for (@other_nicks, $this_nick)
418 push (@AllNicks, $_);
419 $NickToNick{$_} = $this_nick;
420 $NickToIdent{$_} = $this_ident;
423 $IdentToNick{$this_ident} = $this_nick;
426 for (keys %$name2nick)
433 my @other_nicks = ();
434 my @other_idents = ();
436 for (keys %{$name2nick->{$name}})
439 my $num = $name2nick->{$name}{$nick};
443 push (@other_nicks, $max_nick) if ($max_nick);
449 push (@other_nicks, $nick);
454 for (keys %{$name2ident->{$name}})
457 my $num = $name2ident->{$name}{$ident};
461 push (@other_idents, $max_ident) if ($max_ident);
467 push (@other_idents, $ident);
471 for (@other_nicks, $max_nick)
473 push (@AllNicks, $_);
474 $NickToNick{$_} = $max_nick;
475 $NickToIdent{$_} = $max_ident;
478 for (@other_idents, $max_ident)
480 $IdentToNick{$_} = $max_nick;
485 =item I<@nicks> = B<get_all_nicks> ()
487 Returns an array of all seen nicks.
496 =item I<$channel> = B<get_channel> ()
498 Returns the name of the channel we're generating stats for.
504 my $chan = '#unknown'
506 if (get_config ('channel'))
508 $chan = get_config ('channel');
513 for ($ChannelNames->keys ())
516 my ($num) = $ChannelNames->get ($c);
517 if (defined ($num) and ($num > $max))
525 # Fix network-safe channel named (RFC 2811)
526 if ($chan =~ m/^![A-Z0-9]{5}.+/)
528 $chan =~ s/[A-Z0-9]{5}//;
534 =item I<$main> = B<get_main_nick> (I<$nick>)
536 Returns the main nick for I<$nick> or an empty string if the nick is unknown..
543 if (defined ($NickToNick{$nick}))
545 return ($NickToNick{$nick});
553 =item I<$ident> = B<nick_to_ident> (I<$nick>)
555 Returns the ident for this nick or an empty string if unknown. Before
556 B<calculate_nicks> is run it will use the database to find the most recent
557 mapping. After B<calculate_nicks> is run the calculated mapping will be used.
568 if (defined ($NickToIdent{$nick}))
570 $ident = $NickToIdent{$nick};
575 ($ident) = $Nick2Ident->get ($nick);
582 =item I<$nick> = B<ident_to_nick> (I<$ident>)
584 Returns the nick for the given ident or an empty string if unknown.
592 if (defined ($IdentToNick{$ident}))
594 return ($IdentToNick{$ident});
602 =item I<$name> = B<nick_to_name> (I<$nick>)
604 Return the name associated with I<$nick>. This function uses B<ident_to_name>
605 (see L<Onis::Users>).
612 my $ident = nick_to_ident ($nick);
616 return (ident_to_name ($ident));
624 =item I<$lines> = B<get_total_lines> ()
626 Returns the total number of lines parsed so far.
633 #return ($DATA->{'total_lines'});
636 =item B<nick_rename> (I<$old_nick>, I<$new_nick>)
638 Keeps track of a nick's hostname if the nick changes.
644 my $old_nick = shift;
645 my $new_nick = shift;
648 ($ident) = $Nick2Ident->get ($old_nick);
650 if (defined ($ident) and ($ident))
652 $Nick2Ident->put ($new_nick, $ident);
656 =item B<print_output> ()
658 Print the output. Should be called only once..
665 if (!get_total_lines () and 0)
667 print STDERR <<'MESSAGE';
671 The most common reasons for this are:
672 - The logfile used was empty.
673 - The ``logtype'' setting did not match the logfile.
674 - The logfile did not include a date.
682 for (@$OutputCallbacks)
688 =item I<$data> = B<register_plugin> (I<$type>, I<$sub_ref>)
690 Register a subroutine for the given type. Returns a reference to the internal
691 data object. This will change soon, don't use it anymore if possible.
701 if (ref ($sub_ref) ne "CODE")
703 print STDERR $/, __FILE__, ": Plugin tried to register a non-code reference. Ignoring it.";
707 if ($type eq 'OutputCallbacks')
709 push (@$OutputCallbacks, $sub_ref);
713 if (!defined ($PluginCallbacks->{$type}))
715 $PluginCallbacks->{$type} = [];
719 push (@{$PluginCallbacks->{$type}}, $sub_ref);
721 print STDERR $/, __FILE__, ': ', scalar (caller ()), " registered for ``$type''." if ($::DEBUG & 0x800);
728 Florian octo Forster E<lt>octo at verplant.orgE<gt>