File Coverage

blib/lib/App/Statsbot.pm
Criterion Covered Total %
statement 74 98 75.5
branch 10 18 55.5
condition 2 2 100.0
subroutine 18 22 81.8
pod 0 5 0.0
total 104 145 71.7


line stmt bran cond sub pod time code
1             package App::Statsbot;
2              
3 1     1   36848 use 5.014000;
  1         3  
4 1     1   6 use strict;
  1         2  
  1         38  
5 1     1   6 use warnings;
  1         7  
  1         78  
6              
7             our $VERSION = '1.000';
8              
9 1     1   784 use POE;
  1         108192  
  1         8  
10 1     1   111574 use POE::Component::IRC::State;
  1         191505  
  1         72  
11 1     1   837 use POE::Component::IRC::Plugin::AutoJoin;
  1         2291  
  1         60  
12 1     1   795 use POE::Component::IRC::Plugin::Connector;
  1         23070  
  1         51  
13 1     1   872 use POE::Component::IRC::Plugin::CTCP;
  1         1753  
  1         52  
14 1     1   13 use IRC::Utils qw/parse_user/;
  1         1  
  1         67  
15              
16 1     1   6 use Carp;
  1         1  
  1         66  
17 1     1   2074 use DBI;
  1         21527  
  1         118  
18 1     1   14627 use DBD::SQLite;
  1         22780  
  1         72  
19 1     1   830 use Text::ParseWords qw/shellwords/;
  1         1669  
  1         92  
20 1     1   857 use Time::Duration qw/duration duration_exact/;
  1         2319  
  1         118  
21 1     1   712 use Time::Duration::Parse qw/parse_duration/;
  1         14865  
  1         10  
22              
23 1     1   90 use List::Util qw/max/;
  1         2  
  1         1466  
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             {
35             my %cfg = (debug => \$DEBUG, tick => \$TICK, nickname => \$NICKNAME, server => \$SERVER, port => \$PORT, ssl => \$SSL, channels => \@CHANNELS, db => \$DB);
36             for my $var (keys %cfg) {
37             my $key = "STATSBOT_\U$var";
38             ${$cfg{$var}} = $ENV{$key} if exists $ENV{$key} && ref $cfg{$var} eq 'SCALAR';
39             @{$cfg{$var}} = split ' ', $ENV{$key} if exists $ENV{$key} && ref $cfg{$var} eq 'ARRAY';
40             }
41             }
42              
43             my $dbh;
44             my $insert;
45             my $update;
46             my $irc;
47              
48             my %state;
49              
50             sub _yield { $irc->yield(@_) }
51             sub _nick_name { $irc->nick_name }
52              
53             sub _uptime {
54             my ($starttime, $nick) = @_;
55             my $sth=$dbh->prepare('SELECT start,end FROM presence WHERE end > ? AND nick == ?');
56             $sth->execute($starttime, $nick);
57              
58             my $uptime=0;
59             while (my ($start, $end)=$sth->fetchrow_array) {
60             $uptime+=$end-max($start,$starttime)
61             }
62             return $uptime
63             }
64              
65             sub run {
66 0     0 0 0 $irc=POE::Component::IRC::State->spawn;
67 0         0 POE::Session->create(
68             inline_states => {
69             _start => \&bot_start,
70             irc_public => \&on_public,
71              
72             irc_chan_sync => \&tick,
73             tick => \&tick,
74              
75             irc_disconnected => \&on_fatal,
76             irc_error => \&on_fatal,
77             },
78             options => { trace => $DEBUG },
79             );
80              
81 0 0       0 $dbh=DBI->connect("dbi:SQLite:dbname=$DB") or croak "Cannot connect to database: $!";
82 0         0 $dbh->do('CREATE TABLE presence (start INTEGER, end INTEGER, nick TEXT)');
83 0 0       0 $insert=$dbh->prepare('INSERT INTO presence (start, end, nick) VALUES (?,?,?)') or croak "Cannot prepare query: $!";
84 0 0       0 $update=$dbh->prepare('UPDATE presence SET end = ? WHERE start == ? AND nick == ?') or croak "Cannot prepare query: $!";
85 0         0 $poe_kernel->run();
86             };
87              
88             sub tick{
89 0     0 0 0 my %nicks = map {$_ => 1} $irc->nicks;
  0         0  
90 0         0 for my $nick (keys %state) {
91 0         0 $update->execute(time, $state{$nick}, $nick);
92 0 0       0 delete $state{$nick} unless (exists $nicks{$nick});
93 0         0 delete $nicks{$nick};
94             }
95              
96 0         0 for (keys %nicks) {
97 0         0 $state{$_}=time;
98 0         0 $insert->execute($state{$_}, $state{$_}, $_);
99             }
100 0         0 $_[KERNEL]->delay(tick => $TICK);
101             }
102              
103             sub bot_start{ ## no critic (RequireArgUnpacking)
104 0     0 0 0 $_[KERNEL]->delay(tick => $TICK);
105              
106 0         0 $irc->plugin_add(CTCP => POE::Component::IRC::Plugin::CTCP->new(
107             version => "Statsbot/$VERSION",
108             source => 'https://metacpan.org/pod/App::Statsbot',
109             userinfo => 'A bot which keeps logs and computes channel statistics',
110             clientinfo => 'PING VERSION CLIENTINFO USERINFO SOURCE',
111             ));
112 0         0 $irc->plugin_add(AutoJoin => POE::Component::IRC::Plugin::AutoJoin->new(
113             Channels => [ @CHANNELS ],
114             RejoinOnKick => 1,
115             Rejoin_delay => 20,
116             Retry_when_banned => 60,
117             ));
118 0         0 $irc->plugin_add(Connecter => POE::Component::IRC::Plugin::Connector->new(
119             servers => [ $SERVER ],
120             ));
121              
122 0         0 _yield(register => 'all');
123 0         0 _yield(
124             connect => {
125             Nick => $NICKNAME,
126             Username => 'statsbot',
127             Ircname => 'Logging and statistics bot',
128             Server => $SERVER,
129             Port => $PORT,
130             UseSSL => $SSL,
131             }
132             );
133             }
134              
135 0     0 0 0 sub on_fatal{ croak "Fatal error: $_[ARG0]" }
136              
137             sub on_public{
138 11     11 0 6238 my ($targets,$message)=@_[ARG1,ARG2];
139 11         31 my $botnick = _nick_name;
140              
141 11 100       169 if ($message =~ /^(?:$botnick[:,]\s*!?|\s*!)help/sx) {
142 4         12 _yield(privmsg => $targets, 'Try !presence username interval [truncate]');
143 4         13 _yield(privmsg => $targets, q/For example, !presence mgv '2 days'/);
144 4         12 _yield(privmsg => $targets, q/or !presence mgv '1 year' 4/);
145 4         12 return;
146             }
147              
148 7 100       87 return unless $message =~ /^(?:$botnick[:,])?\s*!?presence\s*(.*)/sx;
149 6         26 my ($nick, $time, $truncate) = shellwords $1;
150              
151 6   100     617 $truncate//=-1;
152              
153 6 100       338 unless (defined $time) {
154 1         4 $time='1 days';
155 1         3 $truncate=-1;
156             }
157              
158             eval {
159 6         22 $time = parse_duration $time;
160 6 100       10 } or do {
161 1         250 _yield(privmsg => $targets, "cannot parse timespec: $time");
162 1         5 return;
163             };
164              
165 5         188 my $uptime=_uptime time-$time, $nick;
166              
167 5         13 my $ret;
168 5 100       14 if ($truncate == -1) {
169 1     1   9 use integer;
  1         2  
  1         11  
170 2         7 $ret=($uptime/3600).' hours';
171             } else {
172 3         11 $ret=duration $uptime,$truncate;
173             }
174              
175 5         281 $time=duration_exact $time;
176              
177 5         228 _yield(privmsg => $targets, "$nick was here $ret during the last $time");
178             }
179              
180              
181             1;
182             __END__