/[irc-logger]/trunk/bin/irc-logger.pl
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Contents of /trunk/bin/irc-logger.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 89 - (show annotations)
Fri Mar 7 00:43:45 2008 UTC (16 years, 1 month ago) by dpavlin
File MIME type: text/plain
File size: 30352 byte(s)
quick bugfix
1 #!/usr/bin/perl -w
2 use strict;
3 $|++;
4
5 =head1 NAME
6
7 irc-logger.pl
8
9 =head1 SYNOPSIS
10
11 ./irc-logger.pl
12
13 =head2 Options
14
15 =over 4
16
17 =item --import-dircproxy=filename
18
19 Import log from C<dircproxy> to C<irc-logger> database
20
21 =item --log=irc-logger.log
22
23 Name of log file
24
25 =item --follow=file.log
26
27 Follows new messages in file
28
29 =back
30
31 =head1 DESCRIPTION
32
33 log all conversation on irc channel
34
35 =cut
36
37 ## CONFIG
38
39 my $HOSTNAME = `hostname -f`;
40 chomp($HOSTNAME);
41
42 my $NICK = 'irc-logger';
43 $NICK .= '-dev' if ($HOSTNAME =~ m/llin/);
44 my $CONNECT =
45 {Server => 'irc.freenode.net',
46 Nick => $NICK,
47 Ircname => "try /msg $NICK help",
48 };
49 my $CHANNEL = '#razmjenavjestina';
50 $CHANNEL = '#irc-logger' if ($HOSTNAME =~ m/llin/);
51 my $IRC_ALIAS = "log";
52
53 # default log to follow and announce messages
54 my $follows_path = 'follows.log';
55
56 my $DSN = 'DBI:Pg:dbname=' . $NICK;
57
58 my $TIMESTAMP = '%Y-%m-%d %H:%M:%S';
59
60 my $sleep_on_error = 5;
61
62 # number of last tags to keep in circular buffer
63 my $last_x_tags = 50;
64
65 # don't pull rss feeds more often than this
66 my $rss_min_delay = 60;
67 $rss_min_delay = 15;
68
69 my $http_port = $NICK =~ m/-dev/ ? 8001 : 8000;
70
71 my $url = "http://$HOSTNAME:$http_port";
72
73 ## END CONFIG
74
75 use POE qw(Component::IRC Wheel::FollowTail Component::Server::HTTP);
76 use HTTP::Status;
77 use DBI;
78 use Regexp::Common qw /URI/;
79 use CGI::Simple;
80 use HTML::TagCloud;
81 use POSIX qw/strftime/;
82 use HTML::CalendarMonthSimple;
83 use Getopt::Long;
84 use DateTime;
85 use URI::Escape;
86 use Data::Dump qw/dump/;
87 use DateTime::Format::ISO8601;
88 use Carp qw/confess/;
89 use XML::Feed;
90 use DateTime::Format::Flexible;
91
92 my $use_twitter = 1;
93 eval { require Net::Twitter; };
94 $use_twitter = 0 if ($@);
95
96 my $import_dircproxy;
97 my $log_path;
98 GetOptions(
99 'import-dircproxy:s' => \$import_dircproxy,
100 'follows:s' => \$follows_path,
101 'log:s' => \$log_path,
102 );
103
104 #$SIG{__DIE__} = sub {
105 # confess "fatal error";
106 #};
107
108 open(STDOUT, '>', $log_path) || warn "can't redirect log to $log_path: $!";
109
110 sub _log {
111 print strftime($TIMESTAMP,localtime()) . ' ' . join(" ",@_) . $/;
112 }
113
114 # LOG following
115
116 my %FOLLOWS =
117 (
118 # ACCESS => "/var/log/apache/access.log",
119 # ERROR => "/var/log/apache/error.log",
120 );
121
122 sub add_follow_path {
123 my $path = shift;
124 my $name = $path;
125 $name =~ s/\..*$//;
126 warn "# using $path to announce messages from $name\n";
127 $FOLLOWS{$name} = $path;
128 }
129
130 add_follow_path( $follows_path ) if ( -e $follows_path );
131
132 # HTML formatters
133
134 my %escape = ('<'=>'&lt;', '>'=>'&gt;', '&'=>'&amp;', '"'=>'&quot;');
135 my $escape_re = join '|' => keys %escape;
136
137 my $tag_regex = '\b([\w-_]+)//';
138
139 my %nick_enumerator;
140 my $max_color = 0;
141
142 my $filter = {
143 message => sub {
144 my $m = shift || return;
145
146 # protect HTML from wiki modifications
147 sub e {
148 my $t = shift;
149 return 'uri_unescape{' . uri_escape($t) . '}';
150 }
151
152 $m =~ s/($escape_re)/$escape{$1}/gs;
153 $m =~ s#($RE{URI}{HTTP})#e(qq{<a href="$1">$1</a>})#egs ||
154 $m =~ s#\/(\w+)\/#<i>$1</i>#gs;
155 $m =~ s#$tag_regex#e(qq{<a href="$url?tag=$1" class="tag">$1</a>})#egs;
156 $m =~ s#\*(\w+)\*#<b>$1</b>#gs;
157 $m =~ s#_(\w+)_#<u>$1</u>#gs;
158
159 $m =~ s#uri_unescape{([^}]+)}#uri_unescape($1)#egs;
160 return $m;
161 },
162 nick => sub {
163 my $n = shift || return;
164 if (! $nick_enumerator{$n}) {
165 my $max = scalar keys %nick_enumerator;
166 $nick_enumerator{$n} = $max + 1;
167 }
168 return '<span class="nick col-' .
169 ( $nick_enumerator{$n} % $max_color ) .
170 '">' . $n . '</span>';
171 },
172 };
173
174 my $dbh = DBI->connect($DSN,"","", { RaiseError => 1, AutoCommit => 1 }) || die $DBI::errstr;
175 $dbh->do( qq{ set client_encoding = 'UTF-8' } );
176
177 my $sql_schema = {
178 log => qq{
179 create table log (
180 id serial,
181 time timestamp default now(),
182 channel text not null,
183 me boolean default false,
184 nick text not null,
185 message text not null,
186 primary key(id)
187 );
188
189 create index log_time on log(time);
190 create index log_channel on log(channel);
191 create index log_nick on log(nick);
192 },
193 meta => q{
194 create table meta (
195 nick text not null,
196 channel text not null,
197 name text not null,
198 value text,
199 changed timestamp default 'now()',
200 primary key(nick,channel,name)
201 );
202 },
203 feeds => qq{
204 create table feeds (
205 id serial,
206 url text not null,
207 name text,
208 delay interval not null default '5 min',
209 active boolean default true,
210 last_update timestamp default 'now()',
211 polls int default 0,
212 updates int default 0
213 );
214 create unique index feeds_url on feeds(url);
215 insert into feeds (url,name) values ('http://wiki.razmjenavjestina.org/feed/workspace/razmjenavjestina?category=Recent%20Changes','wiki');
216 },
217 };
218
219 foreach my $table ( keys %$sql_schema ) {
220
221 eval {
222 $dbh->do(qq{ select count(*) from $table });
223 };
224
225 if ($@) {
226 warn "creating database table $table in $DSN\n";
227 $dbh->do( $sql_schema->{ $table } );
228 }
229 }
230
231
232 =head2 meta
233
234 Set or get some meta data into database
235
236 meta('nick','channel','var_name', $var_value );
237
238 $var_value = meta('nick','channel','var_name');
239 ( $var_value, $changed ) = meta('nick','channel','var_name');
240
241 =cut
242
243 sub meta {
244 my ($nick,$channel,$name,$value) = @_;
245
246 # normalize channel name
247 $channel =~ s/^#//;
248
249 if (defined($value)) {
250
251 my $sth = $dbh->prepare(qq{ update meta set value = ?, changed = now() where nick = ? and channel = ? and name = ? });
252
253 eval { $sth->execute( $value, $nick, $channel, $name ) };
254
255 # error or no result
256 if ( $@ || ! $sth->rows ) {
257 $sth = $dbh->prepare(qq{ insert into meta (value,nick,channel,name,changed) values (?,?,?,?,now()) });
258 $sth->execute( $value, $nick, $channel, $name );
259 _log "created $nick/$channel/$name = $value";
260 } else {
261 _log "updated $nick/$channel/$name = $value ";
262 }
263
264 return $value;
265
266 } else {
267
268 my $sth = $dbh->prepare(qq{ select value,changed from meta where nick = ? and channel = ? and name = ? });
269 $sth->execute( $nick, $channel, $name );
270 my ($v,$c) = $sth->fetchrow_array;
271 _log "fetched $nick/$channel/$name = $v [$c]";
272 return ($v,$c) if wantarray;
273 return $v;
274
275 }
276 }
277
278
279
280 my $sth_insert_log = $dbh->prepare(qq{
281 insert into log
282 (channel, me, nick, message, time)
283 values (?,?,?,?,?)
284 });
285
286
287 my $tags;
288
289 =head2 get_from_log
290
291 my @messages = get_from_log(
292 limit => 42,
293 search => '%what to stuff in ilike%',
294 fmt => {
295 time => '{%s} ',
296 time_channel => '{%s %s} ',
297 nick => '%s: ',
298 me_nick => '***%s ',
299 message => '%s',
300 },
301 filter => {
302 message => sub {
303 # modify message content
304 return shift;
305 }
306 },
307 context => 5,
308 full_rows => 1,
309 );
310
311 Order is important. Fields are first passed through C<filter> (if available) and
312 then throgh C<< sprintf($fmt->{message}, $message >> if available.
313
314 C<context> defines number of messages around each search hit for display.
315
316 C<full_rows> will return database rows for each result with C<date>, C<time>, C<channel>,
317 C<me>, C<nick> and C<message> keys.
318
319 =cut
320
321 sub get_from_log {
322 my $args = {@_};
323
324 if ( ! $args->{fmt} ) {
325 $args->{fmt} = {
326 date => '[%s] ',
327 time => '{%s} ',
328 time_channel => '{%s %s} ',
329 nick => '%s: ',
330 me_nick => '***%s ',
331 message => '%s',
332 };
333 }
334
335 my $sql_message = qq{
336 select
337 time::date as date,
338 time::time as time,
339 channel,
340 me,
341 nick,
342 message
343 from log
344 };
345
346 my $sql_context = qq{
347 select
348 id
349 from log
350 };
351
352 my $context = $1 if ($args->{search} && $args->{search} =~ s/\s*\+(\d+)\s*/ /);
353
354 my $sql = $context ? $sql_context : $sql_message;
355
356 sub check_date {
357 my $date = shift || return;
358 my $new_date = eval { DateTime::Format::ISO8601->parse_datetime( $date )->ymd; };
359 if ( $@ ) {
360 warn "invalid date $date\n";
361 $new_date = DateTime->now->ymd;
362 }
363 return $new_date;
364 }
365
366 my @where;
367 my @args;
368
369 if (my $search = $args->{search}) {
370 $search =~ s/^\s+//;
371 $search =~ s/\s+$//;
372 push @where, 'message ilike ? or nick ilike ?';
373 push @args, ( ( '%' . $search . '%' ) x 2 );
374 _log "search for '$search'";
375 }
376
377 if ($args->{tag} && $tags->{ $args->{tag} }) {
378 push @where, 'id in (' . join(',', @{ $tags->{ $args->{tag} } }) . ')';
379 _log "search for tags $args->{tag}";
380 }
381
382 if (my $date = $args->{date} ) {
383 $date = check_date( $date );
384 push @where, 'date(time) = ?';
385 push @args, $date;
386 _log "search for date $date";
387 }
388
389 $sql .= " where " . join(" and ", @where) if @where;
390
391 $sql .= " order by log.time desc";
392 $sql .= " limit " . $args->{limit} if ($args->{limit});
393
394 #warn "### sql: $sql ", dump( @args );
395
396 my $sth = $dbh->prepare( $sql );
397 eval { $sth->execute( @args ) };
398 return if $@;
399
400 my $last_row = {
401 date => '',
402 time => '',
403 channel => '',
404 nick => '',
405 };
406
407 my @rows;
408
409 while (my $row = $sth->fetchrow_hashref) {
410 unshift @rows, $row;
411 }
412
413 # normalize nick names
414 map {
415 $_->{nick} =~ s/^_*(.*?)_*$/$1/
416 } @rows;
417
418 return @rows if ($args->{full_rows});
419
420 my @msgs = (
421 "Showing " . ($#rows + 1) . " messages..."
422 );
423
424 if ($context) {
425 my @ids = @rows;
426 @rows = ();
427
428 my $last_to = 0;
429
430 my $sth = $dbh->prepare( $sql_message . qq{ where id >= ? and id < ? } );
431 foreach my $row_id (sort { $a->{id} <=> $b->{id} } @ids) {
432 my $id = $row_id->{id} || die "can't find id in row";
433
434 my ($from, $to) = ($id - $context, $id + $context);
435 $from = $last_to if ($from < $last_to);
436 $last_to = $to;
437 $sth->execute( $from, $to );
438
439 #warn "## id: $id from: $from to: $to returned: ", $sth->rows, "\n";
440
441 while (my $row = $sth->fetchrow_hashref) {
442 push @rows, $row;
443 }
444
445 }
446 }
447
448 # sprintf which can take coderef as first parametar
449 sub cr_sprintf {
450 my $fmt = shift || return;
451 if (ref($fmt) eq 'CODE') {
452 $fmt->(@_);
453 } else {
454 sprintf($fmt, @_);
455 }
456 }
457
458 foreach my $row (@rows) {
459
460 $row->{time} =~ s#\.\d+##;
461
462 my $msg = '';
463
464 $msg = cr_sprintf($args->{fmt}->{date}, $row->{date}) . ' ' if ($last_row->{date} ne $row->{date});
465 my $t = $row->{time};
466
467 if ($last_row->{channel} ne $row->{channel}) {
468 $msg .= cr_sprintf($args->{fmt}->{time_channel}, $t, $row->{channel});
469 } else {
470 $msg .= cr_sprintf($args->{fmt}->{time}, $t);
471 }
472
473 my $append = 1;
474
475 my $nick = $row->{nick};
476 # if ($nick =~ s/^_*(.*?)_*$/$1/) {
477 # $row->{nick} = $nick;
478 # }
479
480 if ($last_row->{nick} ne $nick) {
481 # obfu way to find format for me_nick if needed or fallback to default
482 my $fmt = $row->{me} ? ( $args->{fmt}->{me_nick} || $args->{fmt}->{nick} ) : $args->{fmt}->{nick};
483 $fmt ||= '%s';
484
485 $nick = $args->{filter}->{nick}->($nick) if (ref($args->{filter}->{nick}) eq 'CODE');
486
487 $msg .= cr_sprintf( $fmt, $nick );
488 $append = 0;
489 }
490
491 $args->{fmt}->{message} ||= '%s';
492 if (ref($args->{filter}->{message}) eq 'CODE') {
493 $msg .= cr_sprintf($args->{fmt}->{message},
494 $args->{filter}->{message}->(
495 $row->{message}
496 )
497 );
498 } else {
499 $msg .= cr_sprintf($args->{fmt}->{message}, $row->{message});
500 }
501
502 if ($append && @msgs) {
503 $msgs[$#msgs] .= " " . $msg;
504 } else {
505 push @msgs, $msg;
506 }
507
508 $last_row = $row;
509 }
510
511 return @msgs;
512 }
513
514 # tags support
515
516 my $cloud = HTML::TagCloud->new;
517
518 =head2 add_tag
519
520 add_tag( id => 42, message => 'irc message', nick => 'foobar' [, me => 1 ] );
521
522 =cut
523
524 my @last_tags;
525
526 sub add_tag {
527 my $arg = {@_};
528
529 return unless ($arg->{id} && $arg->{message});
530
531 my $m = $arg->{message};
532
533 my @tags;
534
535 while ($m =~ s#$tag_regex##s) {
536 my $tag = $1;
537 next if (! $tag || $tag =~ m/https?:/i);
538 push @{ $tags->{$tag} }, $arg->{id};
539 #warn "+tag $tag: $arg->{id}\n";
540 $cloud->add($tag, "$url?tag=$tag", scalar @{$tags->{$tag}} + 1);
541 push @tags, $tag;
542
543 }
544
545 if ( @tags ) {
546 pop @last_tags if $#last_tags == $last_x_tags;
547 unshift @last_tags, { tags => [ @tags ], %$arg };
548 }
549
550 }
551
552 =head2 seed_tags
553
554 Read all tags from database and create in-memory cache for tags
555
556 =cut
557
558 sub seed_tags {
559 my $sth = $dbh->prepare(qq{ select id,message,nick,me,time from log where message like '%//%' order by time asc });
560 $sth->execute;
561 while (my $row = $sth->fetchrow_hashref) {
562 add_tag( %$row );
563 }
564
565 foreach my $tag (keys %$tags) {
566 $cloud->add($tag, "$url?tag=$tag", scalar @{$tags->{$tag}} + 1);
567 }
568 }
569
570 seed_tags;
571
572
573 =head2 save_message
574
575 save_message(
576 channel => '#foobar',
577 me => 0,
578 nick => 'dpavlin',
579 message => 'test message',
580 time => '2006-06-25 18:57:18',
581 );
582
583 C<time> is optional, it will use C<< now() >> if it's not available.
584
585 C<me> if not specified will be C<0> (not C</me> message)
586
587 =cut
588
589 sub save_message {
590 my $a = {@_};
591 confess "have msg" if $a->{msg};
592 $a->{me} ||= 0;
593 $a->{time} ||= strftime($TIMESTAMP,localtime());
594
595 _log
596 $a->{channel}, " ",
597 $a->{me} ? "***" . $a->{nick} : "<" . $a->{nick} . ">",
598 " " . $a->{message};
599
600 $sth_insert_log->execute($a->{channel}, $a->{me}, $a->{nick}, $a->{message}, $a->{time});
601 add_tag( id => $dbh->last_insert_id(undef,undef,"log",undef), %$a );
602 }
603
604
605 if ($import_dircproxy) {
606 open(my $l, $import_dircproxy) || die "can't open $import_dircproxy: $!";
607 warn "importing $import_dircproxy...\n";
608 my $tz_offset = 1 * 60 * 60; # TZ GMT+2
609 while(<$l>) {
610 chomp;
611 if (/^@(\d+)\s(\S+)\s(.+)$/) {
612 my ($time, $nick, $msg) = ($1,$2,$3);
613
614 my $dt = DateTime->from_epoch( epoch => $time + $tz_offset );
615
616 my $me = 0;
617 $me = 1 if ($nick =~ m/^\[\S+]/);
618 $nick =~ s/^[\[<]([^!]+).*$/$1/;
619
620 $msg =~ s/^ACTION\s+// if ($me);
621
622 save_message(
623 channel => $CHANNEL,
624 me => $me,
625 nick => $nick,
626 message => $msg,
627 time => $dt->ymd . " " . $dt->hms,
628 ) if ($nick !~ m/^-/);
629
630 } else {
631 _log "can't parse: $_";
632 }
633 }
634 close($l);
635 warn "import over\n";
636 exit;
637 }
638
639 #
640 # RSS follow
641 #
642
643 my $_rss;
644
645
646 sub rss_fetch {
647 my ($args) = @_;
648
649 # how many messages to send out when feed is seen for the first time?
650 my $send_rss_msgs = 1;
651
652 _log "RSS fetch", $args->{url};
653
654 my $feed = XML::Feed->parse(URI->new( $args->{url} ));
655 if ( ! $feed ) {
656 _log("can't fetch RSS ", $args->{url});
657 return;
658 }
659 my ( $total, $updates ) = ( 0, 0 );
660 for my $entry ($feed->entries) {
661 $total++;
662
663 # seen allready?
664 return if $_rss->{$feed->link}->{seen}->{$entry->id}++ > 0;
665
666 sub prefix {
667 my ($txt,$var) = @_;
668 $var =~ s/^\s+//g;
669 return $txt . $var if $var;
670 }
671
672 my $msg;
673 $msg .= prefix( 'From: ' , $feed->title );
674 $msg .= prefix( ' by ' , $entry->author );
675 $msg .= prefix( ' -- ' , $entry->link );
676 # $msg .= prefix( ' id ' , $entry->id );
677
678 if ( $args->{kernel} && $send_rss_msgs ) {
679 $send_rss_msgs--;
680 _log('RSS', $msg);
681 $sth_insert_log->execute( $CHANNEL, 1, $NICK, $msg, undef );
682 $args->{kernel}->post( $IRC_ALIAS => notice => $CHANNEL, $msg );
683 $updates++;
684 }
685 }
686
687 my $sql = qq{ update feeds set last_update = now(), polls = polls + 1 };
688 $sql .= qq{, updates = updates + $updates } if $updates;
689 $sql .= qq{where id = } . $args->{id};
690 eval { $dbh->do( $sql ) };
691
692 _log "RSS got $total items of which $updates new";
693
694 return $updates;
695 }
696
697 sub rss_fetch_all {
698 my $kernel = shift;
699 my $sql = qq{
700 select id, url, name
701 from feeds
702 where active is true
703 };
704 # limit to newer feeds only if we are not sending messages out
705 $sql .= qq{ and last_update + delay < now() } if $kernel;
706 my $sth = $dbh->prepare( $sql );
707 $sth->execute();
708 warn "# ",$sth->rows," active RSS feeds\n";
709 my $count = 0;
710 while (my $row = $sth->fetchrow_hashref) {
711 $row->{kernel} = $kernel if $kernel;
712 $count += rss_fetch( $row );
713 }
714 return "OK, fetched $count posts from " . $sth->rows . " feeds";
715 }
716
717
718 sub rss_check_updates {
719 my $kernel = shift;
720 my $last_t = $_rss->{last_poll} || time();
721 my $t = time();
722 if ( $last_t - $t > $rss_min_delay ) {
723 $_rss->{last_poll} = $t;
724 _log rss_fetch_all( $kernel );
725 }
726 }
727
728 # seed rss seen cache so we won't send out all items on startup
729 _log rss_fetch_all;
730
731 #
732 # POE handing part
733 #
734
735 my $SKIPPING = 0; # if skipping, how many we've done
736 my $SEND_QUEUE; # cache
737 my $ping; # ping stats
738
739 POE::Component::IRC->new($IRC_ALIAS);
740
741 POE::Session->create( inline_states => {
742 _start => sub {
743 $_[KERNEL]->post($IRC_ALIAS => register => 'all');
744 $_[KERNEL]->post($IRC_ALIAS => connect => $CONNECT);
745 },
746 irc_255 => sub { # server is done blabbing
747 $_[KERNEL]->post($IRC_ALIAS => join => $CHANNEL);
748 $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "IDENTIFY $NICK" );
749 },
750 irc_public => sub {
751 my $kernel = $_[KERNEL];
752 my $nick = (split /!/, $_[ARG0])[0];
753 my $channel = $_[ARG1]->[0];
754 my $msg = $_[ARG2];
755
756 save_message( channel => $channel, me => 0, nick => $nick, message => $msg);
757 meta( $nick, $channel, 'last-msg', $msg );
758 },
759 irc_ctcp_action => sub {
760 my $kernel = $_[KERNEL];
761 my $nick = (split /!/, $_[ARG0])[0];
762 my $channel = $_[ARG1]->[0];
763 my $msg = $_[ARG2];
764
765 save_message( channel => $channel, me => 1, nick => $nick, message => $msg);
766
767 if ( $use_twitter ) {
768 if ( my $twitter = meta( $nick, $NICK, 'twitter' ) ) {
769 my ($login,$passwd) = split(/\s+/,$twitter,2);
770 _log("sending twitter for $nick/$login on $channel ");
771 my $bot = Net::Twitter->new( username=>$login, password=>$passwd );
772 $bot->update("<${channel}> $msg");
773 }
774 }
775
776 },
777 irc_ping => sub {
778 _log( "pong ", $_[ARG0] );
779 $ping->{ $_[ARG0] }++;
780 rss_check_updates( $_[KERNEL] );
781 },
782 irc_invite => sub {
783 my $kernel = $_[KERNEL];
784 my $nick = (split /!/, $_[ARG0])[0];
785 my $channel = $_[ARG1];
786
787 _log "invited to $channel by $nick";
788
789 $_[KERNEL]->post( $IRC_ALIAS => privmsg => $nick, "how nice of you to invite me to $channel, I'll be right there..." );
790 $_[KERNEL]->post($IRC_ALIAS => join => $channel);
791
792 },
793 irc_msg => sub {
794 my $kernel = $_[KERNEL];
795 my $nick = (split /!/, $_[ARG0])[0];
796 my $msg = $_[ARG2];
797 my $channel = $_[ARG1]->[0];
798
799 my $res = "unknown command '$msg', try /msg $NICK help!";
800 my @out;
801
802 _log "<< $msg";
803
804 if ($msg =~ m/^help/i) {
805
806 $res = "usage: /msg $NICK comand | commands: stat - user/message stat | last - show backtrace | grep foobar - find foobar";
807
808 } elsif ($msg =~ m/^msg\s+(\S+)\s+(.*)$/i) {
809
810 _log ">> /msg $1 $2";
811 $_[KERNEL]->post( $IRC_ALIAS => privmsg => $1, $2 );
812 $res = '';
813
814 } elsif ($msg =~ m/^stat.*?\s*(\d*)/i) {
815
816 my $nr = $1 || 10;
817
818 my $sth = $dbh->prepare(qq{
819 select
820 trim(both '_' from nick) as nick,
821 count(*) as count,
822 sum(length(message)) as len
823 from log
824 group by trim(both '_' from nick)
825 order by len desc,count desc
826 limit $nr
827 });
828 $sth->execute();
829 $res = "Top $nr users: ";
830 my @users;
831 while (my $row = $sth->fetchrow_hashref) {
832 push @users,$row->{nick} . ': ' . $row->{count} . '/' . $row->{len} . '=' . sprintf("%.2f", $row->{len}/$row->{count});
833 }
834 $res .= join(" | ", @users);
835 } elsif ($msg =~ m/^last.*?\s*(\d*)/i) {
836
837 my $limit = $1 || meta( $nick, $channel, 'last-size' ) || 10;
838
839 foreach my $res (get_from_log( limit => $limit )) {
840 _log "last: $res";
841 $_[KERNEL]->post( $IRC_ALIAS => privmsg => $nick, $res );
842 }
843
844 $res = '';
845
846 } elsif ($msg =~ m/^(search|grep)\s+(.*)\s*$/i) {
847
848 my $what = $2;
849
850 foreach my $res (get_from_log(
851 limit => 20,
852 search => $what,
853 )) {
854 _log "search [$what]: $res";
855 $_[KERNEL]->post( $IRC_ALIAS => privmsg => $nick, $res );
856 }
857
858 $res = '';
859
860 } elsif ($msg =~ m/^(?:count|poll)\s+(.*)(?:\s+(\d+))?\s*$/i) {
861
862 my ($what,$limit) = ($1,$2);
863 $limit ||= 100;
864
865 my $stat;
866
867 foreach my $res (get_from_log(
868 limit => $limit,
869 search => $what,
870 full_rows => 1,
871 )) {
872 while ($res->{message} =~ s/\Q$what\E(\+|\-)//) {
873 $stat->{vote}->{$1}++;
874 $stat->{from}->{ $res->{nick} }++;
875 }
876 }
877
878 my @nicks;
879 foreach my $nick (sort { $stat->{from}->{$a} <=> $stat->{from}->{$b} } keys %{ $stat->{from} }) {
880 push @nicks, $nick . ( $stat->{from}->{$nick} == 1 ? '' :
881 "(" . $stat->{from}->{$nick} . ")"
882 );
883 }
884
885 $res =
886 "$what ++ " . ( $stat->{vote}->{'+'} || 0 ) .
887 " : " . ( $stat->{vote}->{'-'} || 0 ) . " --" .
888 " from " . ( join(", ", @nicks) || 'nobody' );
889
890 $_[KERNEL]->post( $IRC_ALIAS => notice => $nick, $res );
891
892 } elsif ($msg =~ m/^ping/) {
893 $res = "ping = " . dump( $ping );
894 } elsif ($msg =~ m/^conf(?:ig)*\s*(last-size|twitter)*\s*(.*)/) {
895 if ( ! defined( $1 ) ) {
896 my $sth = $dbh->prepare(qq{ select name,value,changed from meta where nick = ? and channel = ? });
897 $sth->execute( $nick, $channel );
898 $res = "config for $nick on $channel";
899 while ( my ($n,$v) = $sth->fetchrow_array ) {
900 $res .= " | $n = $v";
901 }
902 } elsif ( ! $2 ) {
903 my $val = meta( $nick, $channel, $1 );
904 $res = "current $1 = " . ( $val ? $val : 'undefined' );
905 } else {
906 my $validate = {
907 'last-size' => qr/^\d+/,
908 'twitter' => qr/^\w+\s+\w+/,
909 };
910
911 my ( $op, $val ) = ( $1, $2 );
912
913 if ( my $regex = $validate->{$op} ) {
914 if ( $val =~ $regex ) {
915 meta( $nick, $channel, $op, $val );
916 $res = "saved $op = $val";
917 } else {
918 $res = "config option $op = $val doesn't validate against $regex";
919 }
920 } else {
921 $res = "config option $op doesn't exist";
922 }
923 }
924 } elsif ($msg =~ m/^rss-update/) {
925 $res = rss_fetch_all( $_[KERNEL] );
926 } elsif ($msg =~ m/^rss-clean/) {
927 $_rss = undef;
928 $res = "OK, cleaned RSS cache";
929 } elsif ($msg =~ m!^rss-(add|remove|stop|start)\s+(http://\S+)\s*(.*)!) {
930 my $sql = {
931 add => qq{ insert into feeds (url,name) values (?,?) },
932 # remove => qq{ delete from feeds where url = ? and name = ? },
933 start => qq{ update feeds set active = true where url = ? -- ? },
934 stop => qq{ update feeds set active = false where url = ? -- ? },
935
936 };
937 if (my $q = $sql->{$1} ) {
938 my $sth = $dbh->prepare( $q );
939 warn "## SQL $q ( $2 | $3 )\n";
940 eval { $sth->execute( $2, $3 ) };
941 }
942
943 $res ||= "OK, RSS $1 : $2 - $3";
944 }
945
946 if ($res) {
947 _log ">> [$nick] $res";
948 $_[KERNEL]->post( $IRC_ALIAS => privmsg => $nick, $res );
949 }
950
951 rss_check_updates( $_[KERNEL] );
952 },
953 irc_477 => sub {
954 _log "# irc_477: ",$_[ARG1];
955 $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "register $NICK" );
956 },
957 irc_505 => sub {
958 _log "# irc_505: ",$_[ARG1];
959 $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "register $NICK" );
960 # $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "set hide email on" );
961 # $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "set email dpavlin\@rot13.org" );
962 },
963 irc_registered => sub {
964 _log "## registrated $NICK";
965 $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "IDENTIFY $NICK" );
966 },
967 irc_disconnected => sub {
968 _log "## disconnected, reconnecting again";
969 $_[KERNEL]->post($IRC_ALIAS => connect => $CONNECT);
970 },
971 irc_socketerr => sub {
972 _log "## socket error... sleeping for $sleep_on_error seconds and retry";
973 sleep($sleep_on_error);
974 $_[KERNEL]->post($IRC_ALIAS => connect => $CONNECT);
975 },
976 # irc_433 => sub {
977 # print "# irc_433: ",$_[ARG1], "\n";
978 # warn "## indetify $NICK\n";
979 # $_[KERNEL]->post( $IRC_ALIAS => privmsg => 'nickserv', "IDENTIFY $NICK" );
980 # },
981 _child => sub {},
982 _default => sub {
983 _log sprintf "sID:%s %s %s",
984 $_[SESSION]->ID, $_[ARG0],
985 ref($_[ARG1]) eq "ARRAY" ? join(",", map { ref($_) eq "ARRAY" ? join(";", @{$_}) : $_ } @{ $_[ARG1] }) :
986 $_[ARG1] ? $_[ARG1] :
987 "";
988 0; # false for signals
989 },
990 },
991 );
992
993 # http server
994
995 my $httpd = POE::Component::Server::HTTP->new(
996 Port => $http_port,
997 PreHandler => {
998 '/' => sub {
999 $_[0]->header(Connection => 'close')
1000 }
1001 },
1002 ContentHandler => { '/' => \&root_handler },
1003 Headers => { Server => 'irc-logger' },
1004 );
1005
1006 my $style = <<'_END_OF_STYLE_';
1007 p { margin: 0; padding: 0.1em; }
1008 .time, .channel { color: #808080; font-size: 60%; }
1009 .date { float: right; background: #e0e0e0; color: #404040; font-size: 120%; padding: 0.25em; border: 1px dashed #808080; }
1010 .nick { color: #000000; font-size: 80%; padding: 2px; font-family: courier, courier new, monospace ; }
1011 .message { color: #000000; font-size: 100%; }
1012 .search { float: right; }
1013 a:link.tag, a:visited.tag { border: 1px dashed #ccc; backgound: #ccc; text-decoration: none }
1014 a:hover.tag { border: 1px solid #eee }
1015 hr { border: 1px dashed #ccc; height: 1px; clear: both; }
1016 /*
1017 .col-0 { background: #ffff66 }
1018 .col-1 { background: #a0ffff }
1019 .col-2 { background: #99ff99 }
1020 .col-3 { background: #ff9999 }
1021 .col-4 { background: #ff66ff }
1022 */
1023 .calendar { border: 1px solid red; width: 100%; }
1024 .month { border: 0px; width: 100%; }
1025 _END_OF_STYLE_
1026
1027 $max_color = 0;
1028
1029 my @cols = qw(
1030 #ffcccc #ccffe6 #ccccff #e6ccff #ffccff #ffcce6 #ff9999 #ffcc99 #ffff99
1031 #ccff99 #99ff99 #99ffcc #99ccff #9999ff #cc99ff #ff6666 #ffb366 #ffff66
1032 #66ff66 #66ffb3 #66b3ff #6666ff #ff3333 #33ff33 #3399ff #3333ff #ff3399
1033 #a0a0a0 #ff0000 #ffff00 #80ff00 #0000ff #8000ff #ff00ff #ff0080 #994d00
1034 #999900 #009900 #cc0066 #c0c0c0 #ccff99 #99ff33 #808080 #660033 #ffffff
1035 );
1036
1037 foreach my $c (@cols) {
1038 $style .= ".col-${max_color} { background: $c }\n";
1039 $max_color++;
1040 }
1041 warn "defined $max_color colors for users...\n";
1042
1043 sub root_handler {
1044 my ($request, $response) = @_;
1045 $response->code(RC_OK);
1046
1047 # this doesn't seem to work, so moved to PreHandler
1048 #$response->header(Connection => 'close');
1049
1050 return RC_OK if $request->uri =~ m/favicon.ico$/;
1051
1052 my $q;
1053
1054 if ( $request->method eq 'POST' ) {
1055 $q = new CGI::Simple( $request->content );
1056 } elsif ( $request->uri =~ /\?(.+)$/ ) {
1057 $q = new CGI::Simple( $1 );
1058 } else {
1059 $q = new CGI::Simple;
1060 }
1061
1062 my $search = $q->param('search') || $q->param('grep') || '';
1063
1064 if ($request->url =~ m#/rss(?:/(tags|last-tag|follow.*)\w*(?:=(\d+))?)?#i) {
1065 my $show = lc($1);
1066 my $nr = $2;
1067
1068 my $type = 'RSS'; # Atom
1069
1070 $response->content_type( 'application/' . lc($type) . '+xml' );
1071
1072 my $html = '<!-- error -->';
1073 #warn "create $type feed from ",dump( @last_tags );
1074
1075 my $feed = XML::Feed->new( $type );
1076 $feed->link( $url );
1077
1078 if ( $show eq 'tags' ) {
1079 $nr ||= 50;
1080 $feed->title( "tags from $CHANNEL" );
1081 $feed->link( "$url/tags" );
1082 $feed->description( "tag cloud created from messages on channel $CHANNEL which have tags// in them" );
1083 my $feed_entry = XML::Feed::Entry->new($type);
1084 $feed_entry->title( "$nr tags from $CHANNEL" );
1085 $feed_entry->author( $NICK );
1086 $feed_entry->link( '/#tags' );
1087
1088 $feed_entry->content(
1089 qq{<![CDATA[<style type="text/css">}
1090 . $cloud->css
1091 . qq{</style>}
1092 . $cloud->html( $nr )
1093 . qq{]]>}
1094 );
1095 $feed->add_entry( $feed_entry );
1096
1097 } elsif ( $show eq 'last-tag' ) {
1098
1099 $nr ||= $last_x_tags;
1100 $nr = $last_x_tags if $nr > $last_x_tags;
1101
1102 $feed->title( "last $nr tagged messages from $CHANNEL" );
1103 $feed->description( "collects messages which have tags// in them" );
1104
1105 foreach my $m ( @last_tags ) {
1106 # warn dump( $m );
1107 #my $tags = join(' ', @{$m->{tags}} );
1108 my $feed_entry = XML::Feed::Entry->new($type);
1109 $feed_entry->title( $m->{nick} . '@' . $m->{time} );
1110 $feed_entry->author( $m->{nick} );
1111 $feed_entry->link( '/#' . $m->{id} );
1112 $feed_entry->issued( DateTime::Format::Flexible->build( $m->{time} ) );
1113
1114 my $message = $filter->{message}->( $m->{message} );
1115 $message .= "<br/>\n" unless $message =~ m!<(/p|br/?)>!;
1116 # warn "## message = $message\n";
1117
1118 #$feed_entry->summary(
1119 $feed_entry->content(
1120 "<![CDATA[$message]]>"
1121 );
1122 $feed_entry->category( join(', ', @{$m->{tags}}) );
1123 $feed->add_entry( $feed_entry );
1124
1125 $nr--;
1126 last if $nr <= 0;
1127
1128 }
1129
1130 } elsif ( $show =~ m/^follow/ ) {
1131
1132 $feed->title( "Feeds which this bot follows" );
1133
1134 my $sth = $dbh->prepare( qq{ select * from feeds order by last_update desc } );
1135 $sth->execute;
1136 while (my $row = $sth->fetchrow_hashref) {
1137 my $feed_entry = XML::Feed::Entry->new($type);
1138 $feed_entry->title( $row->{name} );
1139 $feed_entry->link( $row->{url} );
1140 $feed_entry->issued( DateTime::Format::Flexible->build( $row->{last_update} ) );
1141 $feed_entry->content(
1142 '<![CDATA[<pre>' . dump( $row ) . '</pre>]]>'
1143 );
1144 $feed->add_entry( $feed_entry );
1145 }
1146
1147 } else {
1148 _log "unknown rss request ",$request->url;
1149 return RC_DENY;
1150 }
1151
1152 $response->content( $feed->as_xml );
1153 return RC_OK;
1154 }
1155
1156 if ( $@ ) {
1157 warn "$@";
1158 }
1159
1160 $response->content_type("text/html; charset=UTF-8");
1161
1162 my $html =
1163 qq{<html><head><title>$NICK</title><style type="text/css">$style}
1164 . $cloud->css
1165 . qq{</style></head><body>}
1166 . qq{
1167 <form method="post" class="search" action="/">
1168 <input type="text" name="search" value="$search" size="10">
1169 <input type="submit" value="search">
1170 </form>
1171 }
1172 . $cloud->html(500)
1173 . qq{<p>};
1174
1175 if ($request->url =~ m#/tags?#) {
1176 # nop
1177 } elsif ($request->url =~ m#/history#) {
1178 my $sth = $dbh->prepare(qq{
1179 select date(time) as date,count(*) as nr,sum(length(message)) as len
1180 from log
1181 group by date(time)
1182 order by date(time) desc
1183 });
1184 $sth->execute();
1185 my ($l_yyyy,$l_mm) = (0,0);
1186 $html .= qq{<table class="calendar"><tr>};
1187 my $cal;
1188 my $ord = 0;
1189 while (my $row = $sth->fetchrow_hashref) {
1190 # this is probably PostgreSQL specific, expects ISO date
1191 my ($yyyy,$mm,$dd) = split(/-/, $row->{date});
1192 if ($yyyy != $l_yyyy || $mm != $l_mm) {
1193 if ( $cal ) {
1194 $html .= qq{<td valign="top">} . $cal->as_HTML() . qq{</td>};
1195 $ord++;
1196 $html .= qq{</tr><tr>} if ( $ord % 3 == 0 );
1197 }
1198 $cal = new HTML::CalendarMonthSimple('month'=>$mm,'year'=>$yyyy);
1199 $cal->border(1);
1200 $cal->width('30%');
1201 $cal->cellheight('5em');
1202 $cal->tableclass('month');
1203 #$cal->cellclass('day');
1204 $cal->sunday('SUN');
1205 $cal->saturday('SAT');
1206 $cal->weekdays('MON','TUE','WED','THU','FRI');
1207 ($l_yyyy,$l_mm) = ($yyyy,$mm);
1208 }
1209 $cal->setcontent($dd, qq[
1210 <a href="$url?date=$row->{date}">$row->{nr}</a><br/>$row->{len}
1211 ]) if $cal;
1212
1213 }
1214 $html .= qq{<td valign="top">} . $cal->as_HTML() . qq{</td></tr></table>};
1215
1216 } else {
1217 $html .= join("</p><p>",
1218 get_from_log(
1219 limit => ( $q->param('last') || $q->param('date') ) ? undef : 100,
1220 search => $search || undef,
1221 tag => $q->param('tag') || undef,
1222 date => $q->param('date') || undef,
1223 fmt => {
1224 date => sub {
1225 my $date = shift || return;
1226 qq{<hr/><div class="date"><a href="$url?date=$date">$date</a></div>};
1227 },
1228 time => '<span class="time">%s</span> ',
1229 time_channel => '<span class="channel">%s %s</span> ',
1230 nick => '%s:&nbsp;',
1231 me_nick => '***%s&nbsp;',
1232 message => '<span class="message">%s</span>',
1233 },
1234 filter => $filter,
1235 )
1236 );
1237 }
1238
1239 $html .= qq{</p>
1240 <hr/>
1241 <p>See <a href="/history">history</a> of all messages.</p>
1242 </body></html>};
1243
1244 $response->content( $html );
1245 warn "<< ", $request->method, " ", $request->uri, " created ", length($html), " bytes\n";
1246 return RC_OK;
1247 }
1248
1249 POE::Kernel->run;

Properties

Name Value
svn:executable *

  ViewVC Help
Powered by ViewVC 1.1.26