Initial import (again)
[srvx.git] / src / mod-memoserv.c
1 /* memoserv.c - MemoServ module for srvx
2  * Copyright 2003-2004 Martijn Smit and srvx Development Team
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.  Important limitations are
8  * listed in the COPYING file that accompanies this software.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, email srvx-maintainers@srvx.net.
17  */
18
19 /* Adds new section to srvx.conf:
20  * "modules" {
21  *     "memoserv" {
22  *         "bot" "NickServ";
23  *         "message_expiry" "30d"; // age when messages are deleted; set
24  *                                 // to 0 to disable message expiration
25  *     };
26  *  };
27  *
28  * After that, to make the module active on an existing bot:
29  * /msg opserv bind nickserv * *memoserv.*
30  *
31  * If you want a dedicated MemoServ bot, make sure the service control
32  * commands are bound to OpServ:
33  * /msg opserv bind opserv service *modcmd.joiner
34  * /msg opserv bind opserv service\ add *modcmd.service\ add
35  * /msg opserv bind opserv service\ rename *modcmd.service\ rename
36  * /msg opserv bind opserv service\ trigger *modcmd.service\ trigger
37  * /msg opserv bind opserv service\ remove *modcmd.service\ remove
38  * Add the bot:
39  * /msg opserv service add MemoServ User-to-user Memorandum Service
40  * /msg opserv bind memoserv help *modcmd.help
41  * Restart srvx with the updated conf file (as above, butwith "bot"
42  * "MemoServ"), and bind the commands to it:
43  * /msg opserv bind memoserv * *memoserv.*
44  * /msg opserv bind memoserv set *modcmd.joiner
45  */
46
47 #include "chanserv.h"
48 #include "conf.h"
49 #include "modcmd.h"
50 #include "saxdb.h"
51 #include "timeq.h"
52
53 #define KEY_SENT "sent"
54 #define KEY_RECIPIENT "to"
55 #define KEY_FROM "from"
56 #define KEY_MESSAGE "msg"
57 #define KEY_READ "read"
58
59 static const struct message_entry msgtab[] = {
60     { "MSMSG_CANNOT_SEND", "You cannot send to account $b%s$b." },
61     { "MSMSG_MEMO_SENT", "Message sent to $b%s$b." },
62     { "MSMSG_NO_MESSAGES", "You have no messages." },
63     { "MSMSG_MEMOS_FOUND", "Found $b%d$b matches.\nUse /msg $S READ <ID> to read a message." },
64     { "MSMSG_CLEAN_INBOX", "You have $b%d$b or more messages, please clean out your inbox.\nUse /msg $S READ <ID> to read a message." },
65     { "MSMSG_LIST_HEAD", "$bID$b   $bFrom$b       $bTime Sent$b" },
66     { "MSMSG_LIST_FORMAT", "%-2u     %s           %s" },
67     { "MSMSG_MEMO_HEAD", "Memo %u From $b%s$b, received on %s:" },
68     { "MSMSG_BAD_MESSAGE_ID", "$b%s$b is not a valid message ID (it should be a number between 0 and %u)." },
69     { "MSMSG_NO_SUCH_MEMO", "You have no memo with that ID." },
70     { "MSMSG_MEMO_DELETED", "Memo $b%d$b deleted." },
71     { "MSMSG_EXPIRY_OFF", "I am currently not expiring messages. (turned off)" },
72     { "MSMSG_EXPIRY", "Messages will be expired when they are %s old (%d seconds)." },
73     { "MSMSG_MESSAGES_EXPIRED", "$b%lu$b message(s) expired." },
74     { "MSMSG_MEMOS_INBOX", "You have $b%d$b new message(s) in your inbox and %d old messages.  Use /msg $S LIST to list them." },
75     { "MSMSG_NEW_MESSAGE", "You have a new message from $b%s$b." },
76     { "MSMSG_DELETED_ALL", "Deleted all of your messages." },
77     { "MSMSG_USE_CONFIRM", "Please use /msg $S DELETE * $bCONFIRM$b to delete $uall$u of your messages." },
78     { "MSMSG_STATUS_TOTAL", "I have $b%u$b memos in my database." },
79     { "MSMSG_STATUS_EXPIRED", "$b%ld$b memos expired during the time I am awake." },
80     { "MSMSG_STATUS_SENT", "$b%ld$b memos have been sent." },
81     { "MSMSG_SET_NOTIFY",     "$bNotify:       $b %s" },
82     { "MSMSG_SET_AUTHNOTIFY", "$bAuthNotify:   $b %s" },
83     { "MSMSG_SET_PRIVATE",    "$bPrivate:      $b %s" },
84     { NULL, NULL }
85 };
86
87 struct memo {
88     struct memo_account *recipient;
89     struct memo_account *sender;
90     char *message;
91     time_t sent;
92     unsigned int is_read : 1;
93 };
94
95 DECLARE_LIST(memoList, struct memo*);
96 DEFINE_LIST(memoList, struct memo*);
97
98 /* memo_account.flags fields */
99 #define MEMO_NOTIFY_NEW   1
100 #define MEMO_NOTIFY_LOGIN 2
101 #define MEMO_DENY_NONCHANNEL 4
102
103 struct memo_account {
104     struct handle_info *handle;
105     unsigned int flags;
106     struct memoList sent;
107     struct memoList recvd;
108 };
109
110 static struct {
111     struct userNode *bot;
112     int message_expiry;
113 } memoserv_conf;
114
115 const char *memoserv_module_deps[] = { NULL };
116 static struct module *memoserv_module;
117 static struct log_type *MS_LOG;
118 static unsigned long memosSent, memosExpired;
119 static struct dict *memos; /* memo_account->handle->handle -> memo_account */
120
121 static struct memo_account *
122 memoserv_get_account(struct handle_info *hi)
123 {
124     struct memo_account *ma;
125     if (!hi)
126         return NULL;
127     ma = dict_find(memos, hi->handle, NULL);
128     if (ma)
129         return ma;
130     ma = calloc(1, sizeof(*ma));
131     if (!ma)
132         return ma;
133     ma->handle = hi;
134     ma->flags = MEMO_NOTIFY_NEW | MEMO_NOTIFY_LOGIN;
135     dict_insert(memos, ma->handle->handle, ma);
136     return ma;
137 }
138
139 static void
140 delete_memo(struct memo *memo)
141 {
142     memoList_remove(&memo->recipient->recvd, memo);
143     memoList_remove(&memo->sender->sent, memo);
144     free(memo->message);
145     free(memo);
146 }
147
148 static void
149 delete_memo_account(void *data)
150 {
151     struct memo_account *ma = data;
152
153     while (ma->recvd.used)
154         delete_memo(ma->recvd.list[0]);
155     while (ma->sent.used)
156         delete_memo(ma->sent.list[0]);
157     memoList_clean(&ma->recvd);
158     memoList_clean(&ma->sent);
159     free(ma);
160 }
161
162 void
163 do_expire(void)
164 {
165     dict_iterator_t it;
166     for (it = dict_first(memos); it; it = iter_next(it)) {
167         struct memo_account *acct = iter_data(it);
168         unsigned int ii;
169         for (ii = 0; ii < acct->sent.used; ++ii) {
170             struct memo *memo = acct->sent.list[ii];
171             if ((now - memo->sent) > memoserv_conf.message_expiry) {
172                 delete_memo(memo);
173                 memosExpired++;
174                 ii--;
175             }
176         }
177     }
178 }
179
180 static void
181 expire_memos(UNUSED_ARG(void *data))
182 {
183     if (memoserv_conf.message_expiry) {
184         do_expire();
185         timeq_add(now + memoserv_conf.message_expiry, expire_memos, NULL);
186     }
187 }
188
189 static struct memo*
190 add_memo(time_t sent, struct memo_account *recipient, struct memo_account *sender, char *message)
191 {
192     struct memo *memo;
193
194     memo = calloc(1, sizeof(*memo));
195     if (!memo)
196         return NULL;
197
198     memo->recipient = recipient;
199     memoList_append(&recipient->recvd, memo);
200     memo->sender = sender;
201     memoList_append(&sender->sent, memo);
202     memo->sent = sent;
203     memo->message = strdup(message);
204     memosSent++;
205     return memo;
206 }
207
208 static int
209 memoserv_can_send(struct userNode *bot, struct userNode *user, struct memo_account *acct)
210 {
211     extern struct userData *_GetChannelUser(struct chanData *channel, struct handle_info *handle, int override, int allow_suspended);
212     struct userData *dest;
213
214     if (!user->handle_info)
215         return 0;
216     if (!(acct->flags & MEMO_DENY_NONCHANNEL))
217         return 1;
218     for (dest = acct->handle->channels; dest; dest = dest->u_next)
219         if (_GetChannelUser(dest->channel, user->handle_info, 1, 0))
220             return 1;
221     send_message(user, bot, "MSMSG_CANNOT_SEND", acct->handle->handle);
222     return 0;
223 }
224
225 static struct memo *find_memo(struct userNode *user, struct svccmd *cmd, struct memo_account *ma, const char *msgid, unsigned int *id)
226 {
227     unsigned int memoid;
228     if (!isdigit(msgid[0])) {
229         if (ma->recvd.used)
230             reply("MSMSG_BAD_MESSAGE_ID", msgid, ma->recvd.used - 1);
231         else
232             reply("MSMSG_NO_MESSAGES");
233         return NULL;
234     }
235     memoid = atoi(msgid);
236     if (memoid >= ma->recvd.used) {
237         reply("MSMSG_NO_SUCH_MEMO");
238         return NULL;
239     }
240     return ma->recvd.list[*id = memoid];
241 }
242
243 static MODCMD_FUNC(cmd_send)
244 {
245     char *message;
246     struct handle_info *hi;
247     struct memo_account *ma, *sender;
248
249     if (!(hi = modcmd_get_handle_info(user, argv[1])))
250         return 0;
251     if (!(sender = memoserv_get_account(user->handle_info))
252         || !(ma = memoserv_get_account(hi))) {
253         reply("MSG_INTERNAL_FAILURE");
254         return 0;
255     }
256     if (!(memoserv_can_send(cmd->parent->bot, user, ma)))
257         return 0;
258     message = unsplit_string(argv + 2, argc - 2, NULL);
259     add_memo(now, ma, sender, message);
260     if (ma->flags & MEMO_NOTIFY_NEW) {
261         struct userNode *other;
262         for (other = ma->handle->users; other; other = other->next_authed)
263             send_message(other, cmd->parent->bot, "MSMSG_NEW_MESSAGE", user->nick);
264     }
265     reply("MSMSG_MEMO_SENT", ma->handle->handle);
266     return 1;
267 }
268
269 static MODCMD_FUNC(cmd_list)
270 {
271     struct memo_account *ma;
272     struct memo *memo;
273     unsigned int ii;
274     char posted[24];
275     struct tm tm;
276
277     if (!(ma = memoserv_get_account(user->handle_info)))
278         return 0;
279     reply("MSMSG_LIST_HEAD");
280     for (ii = 0; (ii < ma->recvd.used) && (ii < 15); ++ii) {
281         memo = ma->recvd.list[ii];
282         localtime_r(&memo->sent, &tm);
283         strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
284         reply("MSMSG_LIST_FORMAT", ii, memo->sender->handle->handle, posted);
285     }
286     if (ii == 0)
287         reply("MSG_NONE");
288     else if (ii == 15)
289         reply("MSMSG_CLEAN_INBOX", ii);
290     else
291         reply("MSMSG_MEMOS_FOUND", ii);
292     return 1;
293 }
294
295 static MODCMD_FUNC(cmd_read)
296 {
297     struct memo_account *ma;
298     unsigned int memoid;
299     struct memo *memo;
300     char posted[24];
301     struct tm tm;
302
303     if (!(ma = memoserv_get_account(user->handle_info)))
304         return 0;
305     if (!(memo = find_memo(user, cmd, ma, argv[1], &memoid)))
306         return 0;
307     localtime_r(&memo->sent, &tm);
308     strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
309     reply("MSMSG_MEMO_HEAD", memoid, memo->sender->handle->handle, posted);
310     send_message_type(4, user, cmd->parent->bot, "%s", memo->message);
311     memo->is_read = 1;
312     return 1;
313 }
314
315 static MODCMD_FUNC(cmd_delete)
316 {
317     struct memo_account *ma;
318     struct memo *memo;
319     unsigned int memoid;
320
321     if (!(ma = memoserv_get_account(user->handle_info)))
322         return 0;
323     if (!irccasecmp(argv[1], "*") || !irccasecmp(argv[1], "all")) {
324         if ((argc < 3) || irccasecmp(argv[2], "confirm")) {
325             reply("MSMSG_USE_CONFIRM");
326             return 0;
327         }
328         while (ma->recvd.used)
329             delete_memo(ma->recvd.list[0]);
330         reply("MSMSG_DELETED_ALL");
331         return 1;
332     }
333
334     if (!(memo = find_memo(user, cmd, ma, argv[1], &memoid)))
335         return 0;
336     delete_memo(memo);
337     reply("MSMSG_MEMO_DELETED", memoid);
338     return 1;
339 }
340
341 static MODCMD_FUNC(cmd_expire)
342 {
343     unsigned long old_expired = memosExpired;
344     do_expire();
345     reply("MSMSG_MESSAGES_EXPIRED", memosExpired - old_expired);
346     return 1;
347 }
348
349 static MODCMD_FUNC(cmd_expiry)
350 {
351     char interval[INTERVALLEN];
352
353     if (!memoserv_conf.message_expiry) {
354         reply("MSMSG_EXPIRY_OFF");
355         return 1;
356     }
357
358     intervalString(interval, memoserv_conf.message_expiry);
359     reply("MSMSG_EXPIRY", interval, memoserv_conf.message_expiry);
360     return 1;
361 }
362
363 static MODCMD_FUNC(cmd_set_notify)
364 {
365     struct memo_account *ma;
366     char *choice;
367
368     if (!(ma = memoserv_get_account(user->handle_info)))
369         return 0;
370     if (argc > 1) {
371         choice = argv[1];
372         if (enabled_string(choice)) {
373             ma->flags |= MEMO_NOTIFY_NEW;
374         } else if (disabled_string(choice)) {
375             ma->flags &= ~MEMO_NOTIFY_NEW;
376         } else {
377             reply("MSG_INVALID_BINARY", choice);
378             return 0;
379         }
380     }
381
382     choice = (ma->flags & MEMO_NOTIFY_NEW) ? "on" : "off";
383     reply("MSMSG_SET_NOTIFY", choice);
384     return 1;
385 }
386
387 static MODCMD_FUNC(cmd_set_authnotify)
388 {
389     struct memo_account *ma;
390     char *choice;
391
392     if (!(ma = memoserv_get_account(user->handle_info)))
393         return 0;
394     if (argc > 1) {
395         choice = argv[1];
396         if (enabled_string(choice)) {
397             ma->flags |= MEMO_NOTIFY_LOGIN;
398         } else if (disabled_string(choice)) {
399             ma->flags &= ~MEMO_NOTIFY_LOGIN;
400         } else {
401             reply("MSG_INVALID_BINARY", choice);
402             return 0;
403         }
404     }
405
406     choice = (ma->flags & MEMO_NOTIFY_LOGIN) ? "on" : "off";
407     reply("MSMSG_SET_AUTHNOTIFY", choice);
408     return 1;
409 }
410
411 static MODCMD_FUNC(cmd_set_private)
412 {
413     struct memo_account *ma;
414     char *choice;
415
416     if (!(ma = memoserv_get_account(user->handle_info)))
417         return 0;
418     if (argc > 1) {
419         choice = argv[1];
420         if (enabled_string(choice)) {
421             ma->flags |= MEMO_DENY_NONCHANNEL;
422         } else if (disabled_string(choice)) {
423             ma->flags &= ~MEMO_DENY_NONCHANNEL;
424         } else {
425             reply("MSG_INVALID_BINARY", choice);
426             return 0;
427         }
428     }
429
430     choice = (ma->flags & MEMO_DENY_NONCHANNEL) ? "on" : "off";
431     reply("MSMSG_SET_PRIVATE", choice);
432     return 1;
433 }
434
435 static MODCMD_FUNC(cmd_status)
436 {
437     reply("MSMSG_STATUS_TOTAL", dict_size(memos));
438     reply("MSMSG_STATUS_EXPIRED", memosExpired);
439     reply("MSMSG_STATUS_SENT", memosSent);
440     return 1;
441 }
442
443 static void
444 memoserv_conf_read(void)
445 {
446     dict_t conf_node;
447     const char *str;
448
449     str = "modules/memoserv";
450     if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
451         log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
452         return;
453     }
454
455     str = database_get_data(conf_node, "message_expiry", RECDB_QSTRING);
456     memoserv_conf.message_expiry = str ? ParseInterval(str) : 60*24*30;
457 }
458
459 static int
460 memoserv_saxdb_read(struct dict *db)
461 {
462     char *str;
463     struct handle_info *sender, *recipient;
464     struct record_data *hir;
465     struct memo *memo;
466     dict_iterator_t it;
467     time_t sent;
468
469     for (it = dict_first(db); it; it = iter_next(it)) {
470         hir = iter_data(it);
471         if (hir->type != RECDB_OBJECT) {
472             log_module(MS_LOG, LOG_WARNING, "Unexpected rectype %d for %s.", hir->type, iter_key(it));
473             continue;
474         }
475
476         if (!(str = database_get_data(hir->d.object, KEY_SENT, RECDB_QSTRING))) {
477             log_module(MS_LOG, LOG_ERROR, "Date sent not present in memo %s; skipping", iter_key(it));
478             continue;
479         }
480         sent = atoi(str);
481
482         if (!(str = database_get_data(hir->d.object, KEY_RECIPIENT, RECDB_QSTRING))) {
483             log_module(MS_LOG, LOG_ERROR, "Recipient not present in memo %s; skipping", iter_key(it));
484             continue;
485         } else if (!(recipient = get_handle_info(str))) {
486             log_module(MS_LOG, LOG_ERROR, "Invalid recipient %s in memo %s; skipping", str, iter_key(it));
487             continue;
488         }
489
490         if (!(str = database_get_data(hir->d.object, KEY_FROM, RECDB_QSTRING))) {
491             log_module(MS_LOG, LOG_ERROR, "Sender not present in memo %s; skipping", iter_key(it));
492             continue;
493         } else if (!(sender = get_handle_info(str))) {
494             log_module(MS_LOG, LOG_ERROR, "Invalid sender %s in memo %s; skipping", str, iter_key(it));
495             continue;
496         }
497
498         if (!(str = database_get_data(hir->d.object, KEY_MESSAGE, RECDB_QSTRING))) {
499             log_module(MS_LOG, LOG_ERROR, "Message not present in memo %s; skipping", iter_key(it));
500             continue;
501         }
502
503         memo = add_memo(sent, memoserv_get_account(recipient), memoserv_get_account(sender), str);
504         if ((str = database_get_data(hir->d.object, KEY_READ, RECDB_QSTRING)))
505             memo->is_read = 1;
506     }
507     return 0;
508 }
509
510 static int
511 memoserv_saxdb_write(struct saxdb_context *ctx)
512 {
513     dict_iterator_t it;
514     struct memo_account *ma;
515     struct memo *memo;
516     char str[7];
517     unsigned int id = 0, ii;
518
519     for (it = dict_first(memos); it; it = iter_next(it)) {
520         ma = iter_data(it);
521         for (ii = 0; ii < ma->recvd.used; ++ii) {
522             memo = ma->recvd.list[ii];
523             saxdb_start_record(ctx, inttobase64(str, id++, sizeof(str)), 0);
524             saxdb_write_int(ctx, KEY_SENT, memo->sent);
525             saxdb_write_string(ctx, KEY_RECIPIENT, memo->recipient->handle->handle);
526             saxdb_write_string(ctx, KEY_FROM, memo->sender->handle->handle);
527             saxdb_write_string(ctx, KEY_MESSAGE, memo->message);
528             if (memo->is_read)
529                 saxdb_write_int(ctx, KEY_READ, 1);
530             saxdb_end_record(ctx);
531         }
532     }
533     return 0;
534 }
535
536 static void
537 memoserv_cleanup(void)
538 {
539     dict_delete(memos);
540 }
541
542 static void
543 memoserv_check_messages(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
544 {
545     unsigned int ii, unseen;
546     struct memo_account *ma;
547     struct memo *memo;
548
549     if (!(ma = memoserv_get_account(user->handle_info))
550         || !(ma->flags & MEMO_NOTIFY_LOGIN))
551         return;
552     for (ii = unseen = 0; ii < ma->recvd.used; ++ii) {
553         memo = ma->recvd.list[ii];
554         if (!memo->is_read)
555             unseen++;
556     }
557     if (ma->recvd.used && memoserv_conf.bot)
558         send_message(user, memoserv_conf.bot, "MSMSG_MEMOS_INBOX", unseen, ma->recvd.used - unseen);
559 }
560
561 static void
562 memoserv_rename_account(struct handle_info *hi, const char *old_handle)
563 {
564     struct memo_account *ma;
565     if (!(ma = dict_find(memos, old_handle, NULL)))
566         return;
567     dict_remove2(memos, old_handle, 1);
568     dict_insert(memos, hi->handle, ma);
569 }
570
571 static void
572 memoserv_unreg_account(UNUSED_ARG(struct userNode *user), struct handle_info *handle)
573 {
574     dict_remove(memos, handle->handle);
575 }
576
577 int
578 memoserv_init(void)
579 {
580     MS_LOG = log_register_type("MemoServ", "file:memoserv.log");
581     memos = dict_new();
582     dict_set_free_data(memos, delete_memo_account);
583     reg_auth_func(memoserv_check_messages);
584     reg_handle_rename_func(memoserv_rename_account);
585     reg_unreg_func(memoserv_unreg_account);
586     conf_register_reload(memoserv_conf_read);
587     reg_exit_func(memoserv_cleanup);
588     saxdb_register("MemoServ", memoserv_saxdb_read, memoserv_saxdb_write);
589
590     memoserv_module = module_register("MemoServ", MS_LOG, "mod-memoserv.help", NULL);
591     modcmd_register(memoserv_module, "send", cmd_send, 3, MODCMD_REQUIRE_AUTHED, NULL);
592     modcmd_register(memoserv_module, "list", cmd_list, 1, MODCMD_REQUIRE_AUTHED, NULL);
593     modcmd_register(memoserv_module, "read", cmd_read, 2, MODCMD_REQUIRE_AUTHED, NULL);
594     modcmd_register(memoserv_module, "delete", cmd_delete, 2, MODCMD_REQUIRE_AUTHED, NULL);
595     modcmd_register(memoserv_module, "expire", cmd_expire, 1, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
596     modcmd_register(memoserv_module, "expiry", cmd_expiry, 1, 0, NULL);
597     modcmd_register(memoserv_module, "status", cmd_status, 1, 0, NULL);
598     modcmd_register(memoserv_module, "set notify", cmd_set_notify, 1, 0, NULL);
599     modcmd_register(memoserv_module, "set authnotify", cmd_set_authnotify, 1, 0, NULL);
600     modcmd_register(memoserv_module, "set private", cmd_set_private, 1, 0, NULL);
601     message_register_table(msgtab);
602
603     if (memoserv_conf.message_expiry)
604         timeq_add(now + memoserv_conf.message_expiry, expire_memos, NULL);
605     return 1;
606 }
607
608 int
609 memoserv_finalize(void) {
610     dict_t conf_node;
611     const char *str;
612
613     str = "modules/memoserv";
614     if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
615         log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
616         return 0;
617     }
618
619     str = database_get_data(conf_node, "bot", RECDB_QSTRING);
620     if (str)
621         memoserv_conf.bot = GetUserH(str);
622     return 1;
623 }