File Coverage

blib/lib/App/Statsbot.pm
Criterion Covered Total %
statement 68 98 69.3
branch 7 18 38.8
condition 2 2 100.0
subroutine 18 22 81.8
pod 0 5 0.0
total 95 145 65.5


line stmt bran cond sub pod time code
1             package App::Statsbot;
2              
3 1     1   21326 use 5.014000;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         21  
5 1     1   10 use warnings;
  1         4  
  1         49  
6              
7             our $VERSION = '0.001002';
8              
9 1     1   852 use POE;
  1         49698  
  1         6  
10 1     1   70815 use POE::Component::IRC::State;
  1         169834  
  1         46  
11 1     1   923 use POE::Component::IRC::Plugin::AutoJoin;
  1         1834  
  1         33  
12 1     1   813 use POE::Component::IRC::Plugin::Connector;
  1         1945  
  1         28  
13 1     1   795 use POE::Component::IRC::Plugin::CTCP;
  1         1301  
  1         35  
14 1     1   6 use IRC::Utils qw/parse_user/;
  1         2  
  1         57  
15              
16 1     1   5 use Carp;
  1         2  
  1         110  
17 1     1   2327 use DBI;
  1         20782  
  1         62  
18 1     1   1312 use DBD::SQLite;
  1         8417  
  1         39  
19 1     1   798 use Text::ParseWords qw/shellwords/;
  1         1268  
  1         61  
20 1     1   771 use Time::Duration qw/duration duration_exact/;
  1         1895  
  1         67  
21 1     1   779 use Time::Duration::Parse qw/parse_duration/;
  1         3120  
  1         6  
22              
23 1     1   57 use List::Util qw/max/;
  1         2  
  1         1034  
24              
25             our $DEBUG = '';
26             our $TICK = 10;
27             our $NICKNAME = 'statsbot';
28             our $SERVER = 'irc.freenode.net';
29             our $PORT = 6667;
30             our $SSL = '';
31             our @CHANNELS;
32             our $DB = '/var/lib/statsbot/db';
33              
34             my $dbh;
35             my $insert;
36             my $update;
37             my $irc;
38              
39             my %state;
40              
41             sub _yield { $irc->yield(@_) }
42             sub _nick_name { $irc->nick_name }
43              
44             sub _uptime {
45             my ($starttime, $nick) = @_;
46             my $sth=$dbh->prepare('SELECT start,end FROM presence WHERE end > ? AND nick == ?');
47             $sth->execute($starttime, $nick);
48              
49             my $uptime=0;
50             while (my ($start, $end)=$sth->fetchrow_array) {
51             $uptime+=$end-max($start,$starttime)
52             }
53             return $uptime
54             }
55              
56             sub run {
57 0     0 0 0 $irc=POE::Component::IRC::State->spawn;
58 0         0 POE::Session->create(
59             inline_states => {
60             _start => \&bot_start,
61             irc_public => \&on_public,
62              
63             irc_chan_sync => \&tick,
64             tick => \&tick,
65              
66             irc_disconnected => \&on_fatal,
67             irc_error => \&on_fatal,
68             },
69             options => { trace => $DEBUG },
70             );
71              
72 0 0       0 $dbh=DBI->connect("dbi:SQLite:dbname=$DB") or croak "Cannot connect to database: $!";
73 0         0 $dbh->do('CREATE TABLE presence (start INTEGER, end INTEGER, nick TEXT)');
74 0 0       0 $insert=$dbh->prepare('INSERT INTO presence (start, end, nick) VALUES (?,?,?)') or croak "Cannot prepare query: $!";
75 0 0       0 $update=$dbh->prepare('UPDATE presence SET end = ? WHERE start == ? AND nick == ?') or croak "Cannot prepare query: $!";
76 0         0 $poe_kernel->run();
77             };
78              
79             sub tick{
80 0     0 0 0 my %nicks = map {$_ => 1} $irc->nicks;
  0         0  
81 0         0 for my $nick (keys %state) {
82 0         0 $update->execute(time, $state{$nick}, $nick);
83 0 0       0 delete $state{$nick} unless (exists $nicks{$nick});
84 0         0 delete $nicks{$nick};
85             }
86              
87 0         0 for (keys %nicks) {
88 0         0 $state{$_}=time;
89 0         0 $insert->execute($state{$_}, $state{$_}, $_);
90             }
91 0         0 $_[KERNEL]->delay(tick => $TICK);
92             }
93              
94             sub bot_start{ ## no critic (RequireArgUnpacking)
95 0     0 0 0 $_[KERNEL]->delay(tick => $TICK);
96              
97 0         0 $irc->plugin_add(CTCP => POE::Component::IRC::Plugin::CTCP->new(
98             userinfo => 'A bot which keeps logs and computes channel statistics',
99             clientinfo => 'PING VERSION CLIENTINFO USERINFO SOURCE',
100             ));
101 0         0 $irc->plugin_add(AutoJoin => POE::Component::IRC::Plugin::AutoJoin->new(
102             Channels => [ @CHANNELS ],
103             RejoinOnKick => 1,
104             Rejoin_delay => 20,
105             Retry_when_banned => 60,
106             ));
107 0         0 $irc->plugin_add(Connecter => POE::Component::IRC::Plugin::Connector->new(
108             servers => [ $SERVER ],
109             ));
110              
111 0         0 _yield(register => 'all');
112 0         0 _yield(
113             connect => {
114             Nick => $NICKNAME,
115             Username => 'statsbot',
116             Ircname => 'Logging and statistics bot',
117             Server => $SERVER,
118             Port => $PORT,
119             UseSSL => $SSL,
120             }
121             );
122             }
123              
124 0     0 0 0 sub on_fatal{ croak "Fatal error: $_[ARG0]" }
125              
126             sub on_public{
127 5     5 0 1609 my ($targets,$message)=@_[ARG1,ARG2];
128 5         13 my $botnick = _nick_name;
129              
130 5 50       62 if ($message =~ /(?:$botnick[:,])?\s*!?help\s*(.*)/sx) {
131 0         0 _yield(privmsg => $targets, 'Try !presence username interval [truncate]');
132 0         0 _yield(privmsg => $targets, q/For example, !presence mgv '2 days'/);
133 0         0 _yield(privmsg => $targets, q/or !presence mgv '1 year' 4/);
134 0         0 return;
135             }
136              
137 5 50       55 return unless $message =~ /(?:$botnick[:,])?\s*!?presence\s*(.*)/sx;
138 5         16 my ($nick, $time, $truncate) = shellwords $1;
139              
140 5   100     498 $truncate//=-1;
141              
142 5 100       11 unless (defined $time) {
143 1         2 $time='1 days';
144 1         26 $truncate=-1;
145             }
146              
147             eval {
148 5         16 $time = parse_duration $time;
149 5 50       6 } or do {
150 0         0 _yield(privmsg => $targets, "cannot parse timespec: $time");
151 0         0 return;
152             };
153              
154 5         145 my $uptime=_uptime time-$time, $nick;
155              
156 5         14 my $ret;
157 5 100       12 if ($truncate == -1) {
158 1     1   6 use integer;
  1         2  
  1         8  
159 2         5 $ret=($uptime/3600).' hours';
160             } else {
161 3         9 $ret=duration $uptime,$truncate;
162             }
163              
164 5         330 $time=duration_exact $time;
165              
166 5         209 _yield(privmsg => $targets, "$nick was here $ret during the last $time");
167             }
168              
169              
170             1;
171             __END__