File Coverage

blib/lib/Bot/Cobalt/Plugin/Alarmclock.pm
Criterion Covered Total %
statement 12 14 85.7
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 17 19 89.4


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Plugin::Alarmclock;
2             $Bot::Cobalt::Plugin::Alarmclock::VERSION = '0.021001';
3 2     2   13100 use strictures 2;
  2         1105  
  2         60  
4 2     2   290 use v5.10;
  2         4  
5              
6 2     2   7 use File::Spec ();
  2         1  
  2         21  
7              
8 2     2   683 use Bot::Cobalt;
  2         3  
  2         9  
9 2     2   1856 use Bot::Cobalt::DB;
  0            
  0            
10             use Bot::Cobalt::Utils 'timestr_to_secs';
11              
12             use Object::Pluggable::Constants ':ALL';
13              
14              
15             sub new {
16             bless +{
17             # $self->timers->{$timerid} = [ $context, $username ]
18             _timers => +{},
19             _db => undef,
20             }, shift
21             }
22              
23             sub timers { shift->{_timers} }
24             sub clear_timers { shift->{_timers} = +{} }
25              
26             sub _init_from_db {
27             my ($self) = @_;
28             my $db = $self->{_db};
29             unless ($db->dbopen) {
30             logger->error("dbopen failure for alarmclock db in _init_from_db");
31             logger->error("persistent alarms may be broken!");
32             return
33             }
34             my $count = 0;
35             ID: for my $id ($db->dbkeys) {
36             my $alarm = $db->get($id);
37             unless ($alarm && ref $alarm eq 'HASH') {
38             logger->warn(
39             defined $alarm ?
40             "Alarm '$id' not a HASH; alarmclock db may be broken!"
41             : "Could not retrieve alarm '$id'; alarmclock db may be broken!"
42             );
43             next ID
44             }
45             my $expires_at = $alarm->{At};
46             unless ($expires_at) {
47             logger->warn("No 'At' expiry for timer id '$id', removing");
48             $db->del($id);
49             next ID
50             }
51             if ($expires_at <= time) {
52             logger->debug("Expiring stale alarmclock '$id'");
53             $db->del($id);
54             next ID
55             }
56             $alarm->{Alias} = plugin_alias($self);
57             my $secs = $expires_at - time;
58             my $new_id = core->timer_set( $secs, $alarm );
59             if ($new_id) {
60             $self->timers->{$new_id} = [ $alarm->{Context}, $alarm->{User} ];
61             $db->put($new_id => $alarm);
62             $db->del($id);
63             ++$count
64             } else {
65             logger->warn("Failed to readd alarmclock timer '$id'");
66             }
67             }
68             $db->dbclose;
69             $count
70             }
71              
72             sub _delete_alarm {
73             my ($self, $id) = @_;
74             my $db = $self->{_db};
75             unless ($db->dbopen) {
76             logger->error("dbopen failure for alarmclock db in _delete_alarm");
77             logger->error("persistent alarms may be broken!");
78             }
79             my $ret = $db->del($id);
80             unless ($ret) {
81             logger->warn("attempted to delete nonexistant alarm ID '$id'");
82             }
83             $db->dbclose;
84             $ret
85             }
86              
87              
88             sub Cobalt_register {
89             my ($self, $core) = splice @_, 0, 2;
90              
91             # FIXME config option for persistent alarms?
92             my $dbpath = File::Spec->catfile( $core->var, 'alarmclock.db' );
93             $self->{_db} = Bot::Cobalt::DB->new(
94             file => $dbpath,
95             );
96             my $count = $self->_init_from_db;
97              
98             register( $self, SERVER => qw/
99             public_cmd_alarmclock
100             public_cmd_alarmdelete
101             public_cmd_alarmdel
102             public_cmd_alarmclear
103             executed_timer
104             / );
105              
106             logger->info("Loaded alarm clock ($count existing alarms loaded)");
107              
108             PLUGIN_EAT_NONE
109             }
110              
111             sub Cobalt_unregister {
112             my ($self, $core) = splice @_, 0, 2;
113             logger->info("Unregistering core IRC plugin");
114             core->timer_del_alias( core->get_plugin_alias($self) );
115             $self->clear_timers;
116             PLUGIN_EAT_NONE
117             }
118              
119             sub Bot_deleted_timer { Bot_executed_timer(@_) }
120              
121             sub Bot_executed_timer {
122             my ($self, $core) = splice @_, 0, 2;
123             my $timerid = ${$_[0]};
124              
125             return PLUGIN_EAT_NONE
126             unless exists $self->timers->{$timerid};
127              
128             logger->debug("clearing timer state for $timerid")
129             if core->debug > 1;
130              
131             delete $self->timers->{$timerid};
132             $self->_delete_alarm($timerid);
133              
134             PLUGIN_EAT_NONE
135             }
136              
137             sub Bot_public_cmd_alarmclear {
138             my ($self, $core) = splice @_, 0, 2;
139             my $msg = ${ $_[0] };
140              
141             my $context = $msg->context;
142             my $nick = $msg->src_nick;
143              
144             my $auth_usr = core->auth->username($context, $nick);
145             return PLUGIN_EAT_NONE unless $auth_usr
146             and core->auth->has_flag($context, $nick, 'SUPERUSER');
147              
148             my $target_ctxt = $msg->message_array->[0];
149              
150             logger->info(
151             "Clearing all alarms"
152             . ($target_ctxt ? " for context $target_ctxt" : "")
153             . " per $nick ($auth_usr)"
154             );
155              
156             my $count = 0;
157             DELETE: for my $timerid (keys %{ $self->timers }) {
158             if ($target_ctxt) {
159             my $ctxt_set = $self->timers->{$timerid}->[0];
160             next DELETE unless $target_ctxt eq $ctxt_set;
161             }
162             core->timer_del($timerid);
163             delete $self->timers->{$timerid};
164             $self->_delete_alarm($timerid);
165             ++$count
166             }
167              
168             broadcast( 'message', $context, $msg->channel,
169             core->rpl( q{ALARMCLOCK_DELETED},
170             nick => $nick,
171             timerid => (
172             $target_ctxt ? "ALL [$target_ctxt] ($count)" : "ALL ($count)"
173             ),
174             )
175             );
176              
177             PLUGIN_EAT_ALL
178             }
179              
180             sub Bot_public_cmd_alarmdelete { Bot_public_cmd_alarmdel(@_) }
181              
182             sub Bot_public_cmd_alarmdel {
183             my ($self, $core) = splice @_, 0, 2;
184             my $msg = ${$_[0]};
185              
186             my $context = $msg->context;
187             my $nick = $msg->src_nick;
188              
189             my $auth_usr = core->auth->username($context, $nick);
190             return PLUGIN_EAT_NONE unless $auth_usr;
191              
192             my $timerid = $msg->message_array->[0];
193             return PLUGIN_EAT_ALL unless $timerid;
194              
195             my $channel = $msg->channel;
196              
197             unless (exists $self->timers->{$timerid}) {
198             broadcast( 'message', $context, $channel,
199             core->rpl( q{ALARMCLOCK_NOSUCH},
200             nick => $nick,
201             timerid => $timerid,
202             )
203             );
204              
205             return PLUGIN_EAT_ALL
206             }
207              
208             my $thistimer = $self->timers->{$timerid};
209             my ($ctxt_set, $ctxt_by) = @$thistimer;
210             ## did this user set this timer?
211             ## original user may've been undef if LevelRequired == 0
212             unless ($ctxt_set eq $context && defined $ctxt_by && $auth_usr eq $ctxt_by) {
213             my $auth_lev = core->auth->level($context, $nick);
214             ## superusers can override:
215             unless ($auth_lev == 9999) {
216             broadcast( 'message', $context, $channel,
217             core->rpl( q{ALARMCLOCK_NOTYOURS},
218             nick => $nick,
219             timerid => $timerid,
220             )
221             );
222             return PLUGIN_EAT_ALL
223             }
224             }
225              
226             core->timer_del($timerid);
227             delete $self->timers->{$timerid};
228             $self->_delete_alarm($timerid);
229              
230             broadcast( 'message', $context, $channel,
231             core->rpl( q{ALARMCLOCK_DELETED},
232             nick => $nick,
233             timerid => $timerid,
234             )
235             );
236              
237             PLUGIN_EAT_ALL
238             }
239              
240              
241             sub Bot_public_cmd_alarmclock {
242             my ($self, $core) = splice @_, 0, 2;
243             my $msg = ${$_[0]};
244              
245             my $context = $msg->context;
246             my $setter = $msg->src_nick;
247              
248             my $cfg = plugin_cfg( $self );
249              
250             my $minlevel = $cfg->{LevelRequired} // 1;
251              
252             ## quietly do nothing for unauthorized users
253             unless (core->auth->level($context, $setter) >= $minlevel) {
254             logger->debug(
255             "ignoring unauthorized alarmclock from '$setter' on '$context'"
256             );
257             return PLUGIN_EAT_NONE
258             }
259            
260             ## undef if $minlevel == 0
261             my $auth_usr = core->auth->username($context, $setter);
262              
263             ## This is the array of (format-stripped) args to the _public_cmd_
264             my $args = $msg->message_array;
265             ## -> f.ex.: split ' ', !alarmclock 1h10m things and stuff
266             my $timestr = shift @$args;
267             ## the rest of this string is the alarm text:
268             my $txtstr = join ' ', @$args;
269              
270             $txtstr = "$setter: ALARMCLOCK: ".$txtstr ;
271              
272             ## set a timer
273             my $secs = timestr_to_secs($timestr) || 1;
274             my $channel = $msg->channel;
275              
276             my $alarm = +{
277             Type => 'msg',
278             User => $auth_usr,
279             Context => $context,
280             Target => $channel,
281             Text => $txtstr,
282             Alias => plugin_alias($self),
283             At => time + $secs,
284             };
285             my $id = core->timer_set( $secs, $alarm );
286              
287             my $resp;
288             if ($id) {
289             $self->timers->{$id} = [ $context, $auth_usr ];
290             $resp = core->rpl( q{ALARMCLOCK_SET},
291             nick => $setter,
292             secs => $secs,
293             timerid => $id,
294             timestr => $timestr,
295             );
296             my $db = $self->{_db};
297             if ($db->dbopen) {
298             $db->put($id => $alarm);
299             $db->dbclose;
300             } else {
301             logger->error("dbopen failure for alarmclock db during add");
302             }
303             } else {
304             $resp = core->rpl( q{RPL_TIMER_ERR} );
305             }
306              
307             if ($resp) {
308             broadcast( 'message', $context, $channel, $resp );
309             }
310              
311             PLUGIN_EAT_ALL
312             }
313              
314             1;
315             __END__