]> jfr.im git - irc/SurrealServices/srsv.git/blob - branches/erry-devel/modules/serviceslibs/memoserv.pm
020621d7cd9345ade57c0710347b1becc5846afb
[irc/SurrealServices/srsv.git] / branches / erry-devel / modules / serviceslibs / memoserv.pm
1 # This file is part of SurrealServices.
2 #
3 # SurrealServices is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # SurrealServices is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with SurrealServices; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16 package memoserv;
17
18 use strict;
19 use DBI qw(:sql_types);
20 #use constant {
21 # READ => 1,
22 # DEL => 2,
23 # ACK => 4,
24 # NOEXP => 8
25 #};
26
27 use SrSv::Agent qw(is_agent);
28
29 use SrSv::Time;
30 use SrSv::Text::Format qw(columnar);
31 use SrSv::Errors;
32
33 use SrSv::User qw(get_user_nick get_user_id :flood);
34 use SrSv::User::Notice;
35 use SrSv::Help qw( sendhelp );
36
37 use SrSv::NickReg::Flags;
38 use SrSv::NickReg::User qw(is_identified get_nick_user_nicks);
39
40 use SrSv::MySQL '$dbh';
41
42 use SrSv::Util qw( makeSeqList );
43
44 use constant (
45 MAX_MEMO_LEN => 400
46 );
47
48 our $msnick_default = 'MemoServ';
49 our $msnick = $msnick_default;
50
51 our (
52 $send_memo, $send_chan_memo, $get_chan_recipients,
53
54 $get_memo_list,
55
56 $get_memo, $get_memo_full, $get_memo_count, $get_unread_memo_count,
57
58 $set_flag,
59
60 $delete_memo, $purge_memos, $delete_all_memos,
61 $memo_chgroot,
62
63 $add_ignore, $get_ignore_num, $del_ignore_nick, $list_ignore, $chk_ignore,
64 $wipe_ignore, $purge_ignore,
65 );
66
67 sub init() {
68 $send_memo = $dbh->prepare("INSERT INTO memo SELECT ?, id, NULL, UNIX_TIMESTAMP(), NULL, ? FROM nickreg WHERE nick=?");
69 $send_chan_memo = $dbh->prepare("INSERT INTO memo SELECT ?, nickreg.id, ?, ?, NULL, ? FROM chanacc, nickreg
70 WHERE chanacc.chan=? AND chanacc.level >= ? AND chanacc.nrid=nickreg.id
71 AND !(nickreg.flags & ". NRF_NOMEMO() . ")");
72 $get_chan_recipients = $dbh->prepare("SELECT user.nick FROM user, nickid, nickreg, chanacc WHERE
73 user.id=nickid.id AND nickid.nrid=chanacc.nrid AND chanacc.nrid=nickreg.id AND chanacc.chan=?
74 AND level >= ? AND
75 !(nickreg.flags & ". NRF_NOMEMO() . ")");
76
77 $get_memo_list = $dbh->prepare("SELECT memo.src, memo.chan, memo.time, memo.flag, memo.msg FROM memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id ORDER BY memo.time ASC");
78
79 $get_memo = $dbh->prepare("SELECT memo.src, memo.chan, memo.time
80 FROM memo JOIN nickreg ON (memo.dstid=nickreg.id) WHERE nickreg.nick=? ORDER BY memo.time ASC LIMIT 1 OFFSET ?");
81 $get_memo->bind_param(2, 0, SQL_INTEGER);
82 $get_memo_full = $dbh->prepare("SELECT memo.src, memo.chan, memo.time, memo.flag, memo.msg FROM memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id ORDER BY memo.time ASC LIMIT 1 OFFSET ?");
83 $get_memo_full->bind_param(2, 0, SQL_INTEGER);
84 $get_memo_count = $dbh->prepare("SELECT COUNT(*) FROM memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id");
85 $get_unread_memo_count = $dbh->prepare("SELECT COUNT(*) FROM memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id AND memo.flag=0");
86
87 $set_flag = $dbh->prepare("UPDATE memo, nickreg SET memo.flag=? WHERE memo.src=? AND nickreg.nick=? AND memo.dstid=nickreg.id AND memo.chan=? AND memo.time=?");
88
89 $delete_memo = $dbh->prepare("DELETE FROM memo USING memo, nickreg WHERE memo.src=? AND nickreg.nick=? AND memo.dstid=nickreg.id AND memo.chan=? AND memo.time=?");
90 $purge_memos = $dbh->prepare("DELETE FROM memo USING memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id AND memo.flag=1");
91 $delete_all_memos = $dbh->prepare("DELETE FROM memo USING memo, nickreg WHERE nickreg.nick=? AND memo.dstid=nickreg.id");
92
93 $add_ignore = $dbh->prepare("INSERT INTO ms_ignore (ms_ignore.nrid, ms_ignore.ignoreid, time)
94 SELECT nickreg.id, ignorenick.id, UNIX_TIMESTAMP() FROM nickreg, nickreg AS ignorenick
95 WHERE nickreg.nick=? AND ignorenick.nick=?");
96 $del_ignore_nick = $dbh->prepare("DELETE FROM ms_ignore USING ms_ignore
97 JOIN nickreg ON (ms_ignore.nrid=nickreg.id)
98 JOIN nickreg AS ignorenick ON(ms_ignore.ignoreid=ignorenick.id)
99 WHERE nickreg.nick=? AND ignorenick.nick=?");
100 $get_ignore_num = $dbh->prepare("SELECT ignorenick.nick FROM ms_ignore
101 JOIN nickreg ON (ms_ignore.nrid=nickreg.id)
102 JOIN nickreg AS ignorenick ON(ms_ignore.ignoreid=ignorenick.id)
103 WHERE nickreg.nick=?
104 ORDER BY ms_ignore.time LIMIT 1 OFFSET ?");
105 $get_ignore_num->bind_param(2, 0, SQL_INTEGER);
106
107 $list_ignore = $dbh->prepare("SELECT ignorenick.nick, ms_ignore.time
108 FROM ms_ignore, nickreg, nickreg AS ignorenick
109 WHERE nickreg.nick=? AND ms_ignore.nrid=nickreg.id AND ms_ignore.ignoreid=ignorenick.id
110 ORDER BY ms_ignore.time");
111 $chk_ignore = $dbh->prepare("SELECT 1
112 FROM ms_ignore, nickreg, nickreg AS ignorenick
113 WHERE nickreg.nick=? AND ms_ignore.nrid=nickreg.id AND ignorenick.nick=? AND ms_ignore.ignoreid=ignorenick.id");
114
115 $wipe_ignore = $dbh->prepare("DELETE FROM ms_ignore USING ms_ignore JOIN nickreg ON(ms_ignore.nrid=nickreg.id) WHERE nickreg.nick=?");
116 $purge_ignore = $dbh->prepare("DELETE FROM ms_ignore USING ms_ignore JOIN nickreg ON(ms_ignore.ignoreid=nickreg.id) WHERE nickreg.nick=?");
117 }
118
119 ### MEMOSERV COMMANDS ###
120
121 sub dispatch($$$) {
122 my ($src, $dst, $msg) = @_;
123 $msg =~ s/^\s+//;
124 my @args = split(/\s+/, $msg);
125 my $cmd = shift @args;
126
127 my $user = { NICK => $src, AGENT => $dst };
128
129 return if flood_check($user);
130
131 if($cmd =~ /^send$/i) {
132 if(@args >= 2) {
133 my @args = split(/\s+/, $msg, 3);
134 ms_send($user, $args[1], $args[2], 0);
135 } else {
136 notice($user, 'Syntax: SEND <recipient> <message>');
137 }
138 }
139 elsif($cmd =~ /^csend$/i) {
140 if(@args >= 3 and $args[1] =~ /^(?:[uvhas]op|co?f(ounder)?|founder)$/i) {
141 my @args = split(/\s+/, $msg, 4);
142 my $level = chanserv::xop_byname($args[2]);
143 ms_send($user, $args[1], $args[3], $level);
144 } else {
145 notice($user, 'Syntax: CSEND <recipient> <uop|vop|hop|aop|sop|cf|founder> <message>');
146 }
147 }
148 elsif($cmd =~ /^read$/i) {
149 if(@args == 1 and (lc($args[0]) eq 'last' or $args[0] > 0)) {
150 ms_read($user, $args[0]);
151 } else {
152 notice($user, 'Syntax: READ <num|LAST>');
153 }
154 }
155 elsif($cmd =~ /^list$/i) {
156 ms_list($user);
157 }
158 elsif($cmd =~ /^del(ete)?$/i) {
159 if(@args >= 1 and (lc($args[0]) eq 'all' or $args[0] > 0)) {
160 ms_delete($user, $args[0]);
161 } else {
162 notice($user, 'Syntax: DELETE <num|num1-num2|ALL>');
163 }
164 }
165 elsif($cmd =~ /^ign(ore)?$/i) {
166 my $cmd2 = shift @args;
167 if($cmd2 =~ /^a(dd)?$/i) {
168 if(@args == 1) {
169 ms_ignore_add($user, $args[0]);
170 }
171 else {
172 notice($user, 'Syntax: IGNORE ADD <nick>');
173 }
174 }
175 elsif($cmd2 =~ /^d(el)?$/i) {
176 if(@args == 1) {
177 ms_ignore_del($user, $args[0]);
178 }
179 else {
180 notice($user, 'Syntax: IGNORE DEL [nick|num]');
181 }
182 }
183 elsif($cmd2 =~ /^l(ist)?$/i) {
184 ms_ignore_list($user);
185 }
186 else {
187 notice($user, 'Syntax: IGNORE <ADD|DEL|LIST> [nick|num]');
188 }
189 }
190 elsif($cmd =~ /^help$/i) {
191 sendhelp($user, 'memoserv', @args);
192 }
193 else {
194 notice($user, "Unrecognized command. For help, type: \002/ms help\002");
195 }
196 }
197
198 sub ms_send($$$$) {
199 my ($user, $dst, $msg, $level) = @_;
200 my $src = get_user_nick($user);
201
202 my $root = auth($user) or return;
203
204 if(length($msg) > MAX_MEMO_LEN()) {
205 notice($user, 'Memo too long. Maximum memo length is '.MAX_MEMO_LEN().' characters.');
206 return;
207 }
208
209 if($dst =~ /^#/) {
210 my $chan = { CHAN => $dst };
211 unless(chanserv::is_registered($chan)) {
212 notice($user, "$dst is not registered");
213 return;
214 }
215
216 my $srcnick = chanserv::can_do($chan, 'MEMO', $user) or return;
217
218 send_chan_memo($srcnick, $chan, $msg, $level);
219 } else {
220 nickserv::chk_registered($user, $dst) or return;
221
222 if (nr_chk_flag($dst, NRF_NOMEMO(), +1)) {
223 notice($user, "\002$dst\002 is not accepting memos.");
224 return;
225 }
226 $chk_ignore->execute(nickserv::get_root_nick($dst), $root);
227 if ($chk_ignore->fetchrow_array) {
228 notice($user, "\002$dst\002 is not accepting memos.");
229 return;
230 }
231
232 send_memo($src, $dst, $msg);
233 }
234
235 notice($user, "Your memo has been sent.");
236 }
237
238 sub ms_read($$) {
239 my ($user, $num) = @_;
240 my ($from, $chan, $time, $flag, $msg);
241 my $src = get_user_nick($user);
242
243 my $root = auth($user) or return;
244
245 my @nums;
246 if(lc($num) eq 'last') {
247 $get_memo_count->execute($root);
248 ($num) = $get_memo_count->fetchrow_array;
249 if (!$num) {
250 notice($user, "Memo \002$num\002 not found.");
251 return;
252 }
253 @nums = ($num);
254 } else {
255 @nums = makeSeqList($num);
256 }
257
258 my $count = 0;
259 my @reply;
260 while (my $num = shift @nums) {
261 if (++$count > 5) {
262 push @reply, "You can only read 5 memos at a time.";
263 last;
264 }
265 $get_memo_full->execute($root, $num-1);
266 unless(($from, $chan, $time, $flag, $msg) = $get_memo_full->fetchrow_array) {
267 push @reply, "Memo \002$num\002 not found.";
268 next;
269 }
270 $set_flag->execute(1, $from, $root, $chan, $time);
271 push @reply, "Memo \002$num\002 from \002$from\002 ".
272 ($chan ? "to \002$chan\002 " : "to \002$root\002 ").
273 "at ".gmtime2($time), ' ', ' '.$msg, ' --';
274 }
275 notice($user, @reply);
276 }
277
278 sub ms_list($) {
279 my ($user) = @_;
280 my ($i, @data, $mnlen, $mclen);
281 my $src = get_user_nick($user);
282
283 my $root = auth($user) or return;
284
285 $get_memo_list->execute($root);
286 while(my ($from, $chan, $time, $flag, $msg) = $get_memo_list->fetchrow_array) {
287 $i++;
288
289 push @data, [
290 ($flag ? '' : "\002") . $i,
291 $from, $chan, gmtime2($time),
292 (length($msg) > 20 ? substr($msg, 0, 17) . '...' : $msg)
293 ];
294 }
295
296 unless(@data) {
297 notice($user, "You have no memos.");
298 return;
299 }
300
301 notice($user, columnar( { TITLE => "Memo list for \002$root\002. To read, type \002/ms read <num>\002",
302 NOHIGHLIGHT => nr_chk_flag_user($user, NRF_NOHIGHLIGHT) }, @data));
303 }
304
305 sub ms_delete($@) {
306 my ($user, @args) = @_;
307 my $src = get_user_nick($user);
308
309 my $root = auth($user) or return;
310
311 if(scalar(@args) == 1 and lc($args[0]) eq 'all') {
312 $delete_all_memos->execute($root);
313 notice($user, 'All of your memos have been deleted.');
314 return;
315 }
316 my (@deleted, @notDeleted);
317 foreach my $num (reverse makeSeqList(@args)) {
318 if(int($num) ne $num) { # can this happen, given makeSeqList?
319 notice($user, "\002$num\002 is not an integer number");
320 next;
321 }
322 my ($from, $chan, $time);
323 $get_memo->execute($root, $num-1);
324 if(my ($from, $chan, $time) = $get_memo->fetchrow_array) {
325 $delete_memo->execute($from, $root, $chan, $time);
326 push @deleted, $num;
327 } else {
328 push @notDeleted, $num;
329 }
330 }
331 if(scalar(@deleted)) {
332 my $plural = (scalar(@deleted) == 1);
333 my $msg = sprintf("Memo%s deleted: ".join(', ', @deleted), ($plural ? '' : 's'));
334 notice($user, $msg);
335 }
336 if(scalar(@notDeleted)) {
337 my $msg = sprintf("Memos not found: ".join(', ', @notDeleted));
338 notice($user, $msg);
339 }
340 }
341
342 sub ms_ignore_add($$) {
343 my ($user, $nick) = @_;
344 my $src = get_user_nick($user);
345
346 unless(is_identified($user, $src) or adminserv::can_do($user, 'SERVOP')) {
347 notice($user, $err_deny);
348 return;
349 }
350
351 my $nickroot = nickserv::get_root_nick($nick);
352 unless ($nickroot) {
353 notice($user, "$nick is not registered");
354 return;
355 }
356
357 my $srcroot = nickserv::get_root_nick($src);
358
359 $add_ignore->execute($srcroot, $nickroot);
360
361 notice($user, "\002$nick\002 (\002$nickroot\002) added to \002$src\002 (\002$srcroot\002) memo ignore list.");
362 }
363
364 sub ms_ignore_del($$) {
365 my ($user, $entry) = @_;
366 my $src = get_user_nick($user);
367
368 unless(is_identified($user, $src) or adminserv::can_do($user, 'SERVOP')) {
369 notice($user, $err_deny);
370 return;
371 }
372 my $srcroot = nickserv::get_root_nick($src);
373
374 my $ignorenick;
375 if (misc::isint($entry)) {
376 $get_ignore_num->execute($srcroot, $entry - 1);
377 ($ignorenick) = $get_ignore_num->fetchrow_array();
378 $get_ignore_num->finish();
379 }
380 my $ret = $del_ignore_nick->execute($srcroot, ($ignorenick ? $ignorenick : $entry));
381 if($ret == 1) {
382 notice($user, "Delete succeeded for ($srcroot): $entry");
383 }
384 else {
385 notice($user, "Delete failed for ($srcroot): $entry. entry does not exist?");
386 }
387 }
388
389 sub ms_ignore_list($) {
390 my ($user) = @_;
391 my $src = get_user_nick($user);
392
393 unless(is_identified($user, $src) or adminserv::can_do($user, 'SERVOP')) {
394 notice($user, $err_deny);
395 return;
396 }
397 my $srcroot = nickserv::get_root_nick($src);
398
399 my @data;
400 $list_ignore->execute($srcroot);
401 while (my ($nick, $time) = $list_ignore->fetchrow_array) {
402 push @data, [$nick, '('.gmtime2($time).')'];
403 }
404
405 notice($user, columnar({TITLE => "Memo ignore list for \002$src\002:",
406 NOHIGHLIGHT => nr_chk_flag_user($user, NRF_NOHIGHLIGHT)}, @data));
407 }
408
409 sub notify($;$) {
410 my ($user, $root) = @_;
411 my (@nicks);
412
413 unless(ref($user)) {
414 $user = { NICK => $user };
415 }
416
417 if($root) { @nicks = ($root) }
418 else { @nicks = nickserv::get_id_nicks($user) }
419
420 my $hasmemos;
421 foreach my $n (@nicks) {
422 $get_unread_memo_count->execute($n);
423 my ($c) = $get_unread_memo_count->fetchrow_array;
424 next unless $c;
425 notice($user, "You have \002$c\002 unread memo(s). " . (@nicks > 1 ? "(\002$n\002) " : ''));
426 $hasmemos = 1;
427 }
428
429 notice($user, "To view them, type: \002/ms list\002") if $hasmemos;
430 }
431
432 ### DATABASE UTILITY FUNCTIONS ###
433
434 sub send_memo($$$) {
435 my ($src, $dst, $msg) = @_;
436
437 # This construct is intended to allow agents to send memos.
438 # Unfortunately this is raceable against %nickserv::enforcers.
439 # I don't want to change the %nickserv::enforcers decl tho, s/my/our/
440 $src = (is_agent($src) ? $src : nickserv::get_root_nick($src));
441 $dst = nickserv::get_root_nick($dst);
442
443 $send_memo->execute($src, $msg, $dst);
444 notice_all_nicks($dst, "You have a new memo from \002$src\002. To read it, type: \002/ms read last\002");
445 }
446
447 sub send_chan_memo($$$$) {
448 my ($src, $chan, $msg, $level) = @_;
449 my $cn = $chan->{CHAN};
450 $src = (is_agent($src) ? $src : nickserv::get_root_nick($src));
451
452 $send_chan_memo->execute($src, $cn, time(), $msg, $cn, $level);
453 # "INSERT INTO memo SELECT ?, nick, ?, ?, 0, ? FROM chanacc WHERE chan=? AND level >= ?"
454
455 $get_chan_recipients->execute($cn, $level);
456 while(my ($u) = $get_chan_recipients->fetchrow_array) {
457 notice({ NICK => $u, AGENT => $msnick },
458 "You have a new memo from \002$src\002 to \002$cn\002. To read it, type: \002/ms read last\002");
459 }
460 }
461
462 sub notice_all_nicks($$) {
463 my ($nick, $msg) = @_;
464
465 foreach my $u (get_nick_user_nicks $nick) {
466 notice({ NICK => $u, AGENT => $msnick }, $msg);
467 }
468 }
469
470 sub auth($) {
471 my ($user) = @_;
472 my $src = get_user_nick($user);
473
474 my $root = nickserv::get_root_nick($src);
475 unless($root) {
476 notice($user, "Your nick is not registered.");
477 return 0;
478 }
479
480 unless(is_identified($user, $root)) {
481 notice($user, $err_deny);
482 return 0;
483 }
484
485 return $root;
486 }
487
488 ### IRC EVENTS ###
489
490 1;