File Coverage

blib/lib/Bot/Cobalt/Plugin/Seen.pm
Criterion Covered Total %
statement 9 11 81.8
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 13 15 86.6


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Plugin::Seen;
2             $Bot::Cobalt::Plugin::Seen::VERSION = '0.021001';
3 1     1   832 use v5.10;
  1         3  
4 1     1   3 use Bot::Cobalt;
  1         1  
  1         5  
5 1     1   549 use Bot::Cobalt::Common;
  1         1  
  1         5  
6 1     1   37 use Bot::Cobalt::DB;
  0            
  0            
7              
8             use Path::Tiny;
9              
10             sub SDB () { 0 }
11             sub BUF () { 1 }
12              
13             sub new {
14             bless [
15             undef, # SDB
16             +{}, # BUF
17             ], shift
18             }
19              
20             sub _parse_nick {
21             my ($context, $nickname) = @_;
22             lc_irc $nickname, (core->get_irc_casemap($context) || 'rfc1459')
23             }
24              
25             ## FIXME method to retrieve users w/ similar hosts
26             ## !seen search ... ?
27              
28             sub retrieve {
29             my ($self, $context, $nickname) = @_;
30             $nickname = _parse_nick($context, $nickname);
31              
32             my $ref = $self->[BUF]->{$context}->{$nickname}; # intentional autoviv
33             unless (defined $ref) {
34             my $db = $self->[SDB];
35             unless ($db->dbopen) {
36             logger->warn("dbopen failed in retrieve; cannot open SeenDB");
37             return
38             }
39             my $thiskey = $context .'%'. $nickname;
40             $ref = $db->get($thiskey);
41             $db->dbclose;
42             }
43              
44             ref $ref ? $ref : ()
45             }
46              
47             sub Cobalt_register {
48             my ($self, $core) = splice @_, 0, 2;
49            
50             my $pcfg = $core->get_plugin_cfg($self);
51             my $seendb_path = path(
52             $core->var .'/'. ($pcfg->{SeenDB} || "seen.db")
53             );
54            
55             logger->info("Opening SeenDB at $seendb_path");
56              
57             $self->[BUF] = +{};
58             $self->[SDB] = Bot::Cobalt::DB->new(file => $seendb_path);
59            
60             my $rc = $self->[SDB]->dbopen;
61             $self->[SDB]->dbclose;
62             unless ($rc) {
63             logger->warn("Failed to open SeenDB at $seendb_path");
64             die "Unable to open SeenDB at $seendb_path"
65             }
66              
67             register( $self, 'SERVER',
68             qw/
69            
70             public_cmd_seen
71            
72             nick_changed
73             chan_sync
74             user_joined
75             user_left
76             user_quit
77            
78             seendb_update
79            
80             seenplug_deferred_list
81            
82             /,
83             );
84            
85             core->timer_set( 6,
86             +{ Event => 'seendb_update' },
87             'SEENDB_WRITE'
88             );
89            
90             logger->info("Loaded");
91            
92             PLUGIN_EAT_NONE
93             }
94              
95             sub Cobalt_unregister {
96             my ($self, $core) = splice @_, 0, 2;
97             $self->Bot_seendb_update($core, \1);
98             $core->log->info("Unloaded");
99             PLUGIN_EAT_NONE
100             }
101              
102             sub Bot_seendb_update {
103             my ($self, $core) = splice @_, 0, 2;
104             my $force_flush = @_ ? ${ $_[0] } : 0;
105              
106             my $buf = $self->[BUF];
107             unless (keys %$buf) {
108             $core->timer_set( 2, +{ Event => 'seendb_update' } );
109             return PLUGIN_EAT_ALL
110             }
111              
112             my $db = $self->[SDB];
113              
114             CONTEXT: for my $context (keys %$buf) {
115             unless ($db->dbopen) {
116             logger->warn("dbopen failed in update; cannot update SeenDB");
117             # FIXME exponential back-off?
118             $core->timer_set( 6, +{ Event => 'seendb_update' } );
119             return PLUGIN_EAT_ALL
120             }
121              
122             my $writes;
123             NICK: for my $nickname (keys %{ $buf->{$context} }) {
124             ## if we've done a lot of writes, yield back (unless we're cleaning up)
125             if (!$force_flush && $writes && $writes % 50 == 0) {
126             $db->dbclose;
127             broadcast 'seendb_update';
128             return PLUGIN_EAT_ALL
129             }
130             ## .. else flush this item to disk
131             my $thisbuf = delete $buf->{$context}->{$nickname};
132             my $thiskey = $context .'%'. $nickname;
133             $db->put($thiskey, $thisbuf);
134             ++$writes;
135             } ## NICK
136             $db->dbclose;
137            
138             delete $buf->{$context} unless keys %{ $buf->{$context} };
139            
140             } ## CONTEXT
141            
142             $core->timer_set( 2, +{ Event => 'seendb_update' } );
143              
144             return PLUGIN_EAT_ALL
145             }
146              
147             sub Bot_user_joined {
148             my ($self, $core) = splice @_, 0, 2;
149             my $join = ${ $_[0] };
150             my $context = $join->context;
151              
152             my $nick = $join->src_nick;
153             my $user = $join->src_user;
154             my $host = $join->src_host;
155             my $chan = $join->channel;
156              
157             $nick = _parse_nick($context, $nick);
158             $self->[BUF]->{$context}->{$nick} = +{
159             TS => time(),
160             Action => 'join',
161             Channel => $chan,
162             Username => $user,
163             Host => $host,
164             };
165            
166             PLUGIN_EAT_NONE
167             }
168              
169             sub Bot_chan_sync {
170             my ($self, $core) = splice @_, 0, 2;
171             my $context = ${$_[0]};
172             my $channel = ${$_[1]};
173              
174             broadcast seenplug_deferred_list => $context, $channel;
175              
176             PLUGIN_EAT_NONE
177             }
178              
179             sub Bot_seenplug_deferred_list {
180             my ($self, $core) = splice @_, 0, 2;
181             my $context = ${$_[0]};
182             my $channel = ${$_[1]};
183            
184             my $irc = $core->get_irc_object($context);
185             my @nicks = $irc->channel_list($channel);
186             for my $nick (@nicks) {
187             $nick = _parse_nick($context, $nick);
188             $self->[BUF]->{$context}->{$nick} = +{
189             TS => time(),
190             Action => 'present',
191             Channel => $channel,
192             Username => '',
193             Host => '',
194             };
195             }
196            
197             PLUGIN_EAT_ALL
198             }
199              
200             sub Bot_user_left {
201             my ($self, $core) = splice @_, 0, 2;
202             my $part = ${ $_[0] };
203             my $context = $part->context;
204            
205             my $nick = $part->src_nick;
206             my $user = $part->src_user;
207             my $host = $part->src_host;
208             my $chan = $part->channel;
209              
210             $nick = _parse_nick($context, $nick);
211             $self->[BUF]->{$context}->{$nick} = +{
212             TS => time(),
213             Action => 'part',
214             Channel => $chan,
215             Username => $user,
216             Host => $host,
217             };
218              
219             PLUGIN_EAT_NONE
220             }
221              
222             sub Bot_user_quit {
223             my ($self, $core) = splice @_, 0, 2;
224             my $quit = ${ $_[0] };
225             my $context = $quit->context;
226            
227             my $nick = $quit->src_nick;
228             my $user = $quit->src_user;
229             my $host = $quit->src_host;
230             my $common = $quit->common;
231              
232             $nick = _parse_nick($context, $nick);
233             $self->[BUF]->{$context}->{$nick} = +{
234             TS => time(),
235             Action => 'quit',
236             Channel => $common->[0],
237             Username => $user,
238             Host => $host,
239             };
240            
241             PLUGIN_EAT_NONE
242             }
243              
244             sub Bot_nick_changed {
245             my ($self, $core) = splice @_, 0, 2;
246             my $nchange = ${ $_[0] };
247             my $context = $nchange->context;
248             return PLUGIN_EAT_NONE if $nchange->equal;
249            
250             my $old = $nchange->old_nick;
251             my $new = $nchange->new_nick;
252            
253             my $irc = $core->get_irc_obj($context);
254             my $src = $irc->nick_long_form($new) || $new;
255             my ($nick, $user, $host) = parse_user($src);
256            
257             my $first_common = $nchange->channels->[0];
258              
259             $self->[BUF]->{$context}->{$old} = +{
260             TS => time(),
261             Action => 'nchange',
262             Channel => $first_common,
263             Username => $user || 'unknown',
264             Host => $host || 'unknown',
265             Meta => { To => $new },
266             };
267            
268             $self->[BUF]->{$context}->{$new} = +{
269             TS => time(),
270             Action => 'nchange',
271             Channel => $first_common,
272             Username => $user || 'unknown',
273             Host => $host || 'unknown',
274             Meta => { From => $old },
275             };
276            
277             PLUGIN_EAT_NONE
278             }
279              
280             sub Bot_public_cmd_seen {
281             my ($self, $core) = splice @_, 0, 2;
282             my $msg = ${ $_[0] };
283             my $context = $msg->context;
284            
285             my $channel = $msg->channel;
286             my $nick = $msg->src_nick;
287            
288             my $targetnick = $msg->message_array->[0];
289            
290             unless ($targetnick) {
291             broadcast message => $context, $channel,
292             "Need a nickname to look for, $nick";
293             return PLUGIN_EAT_NONE
294             }
295            
296             my $ref = $self->retrieve($context, $targetnick);
297            
298             unless ($ref) {
299             broadcast message => $context, $channel,
300             "${nick}: I don't know anything about $targetnick";
301              
302             return PLUGIN_EAT_NONE
303             }
304            
305             my $last_ts = $ref->{TS};
306             my $last_act = $ref->{Action} // '';
307             my $last_chan = $ref->{Channel};
308             my $last_user = $ref->{Username};
309             my $last_host = $ref->{Host};
310             my $meta = $ref->{Meta} // {};
311              
312             my $ts_delta = time - $last_ts ;
313             my $ts_str = secs_to_str_y($ts_delta);
314              
315             my $resp;
316             ACTION: {
317             if ($last_act eq 'quit') {
318             $resp =
319             "$targetnick was last seen quitting IRC $ts_str ago";
320             last ACTION
321             }
322            
323             if ($last_act eq 'join') {
324             $resp =
325             "$targetnick was last seen joining $last_chan $ts_str ago";
326             last ACTION
327             }
328            
329             if ($last_act eq 'part') {
330             $resp =
331             "$targetnick was last seen leaving $last_chan $ts_str ago";
332             last ACTION
333             }
334            
335             if ($last_act eq 'present') {
336             $resp =
337             "$targetnick was last seen when I joined $last_chan $ts_str ago";
338             last ACTION
339             }
340            
341             if ($last_act eq 'nchange') {
342             if ($meta->{From}) {
343             $resp = "$targetnick was last seen changing nicknames from "
344             . $meta->{From} .
345             " $ts_str ago";
346              
347             } elsif ($meta->{To}) {
348             $resp = "$targetnick was last seen changing nicknames to "
349             . $meta->{To} .
350             " $ts_str ago";
351             } else {
352             logger->warn("BUG; no To/From recorded for nick change");
353             $resp = 'Something weird happened; check log file for details.';
354             }
355              
356             last ACTION
357             }
358              
359             logger->warn("BUG; unknown action '$last_act'");
360             $resp = 'Something weird happened; check log file for details.';
361             }
362              
363             broadcast message => $context, $channel, $resp;
364            
365             PLUGIN_EAT_NONE
366             }
367              
368             1;
369             __END__