0e9c2b1034a110a4b8b35415aa6cdafecc7ab939
[srvx.git] / src / global.c
1 /* global.c - Global notice service
2  * Copyright 2000-2004 srvx Development Team
3  *
4  * This file is part of srvx.
5  *
6  * srvx is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with srvx; if not, write to the Free Software Foundation,
18  * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
19  */
20
21 #include "conf.h"
22 #include "global.h"
23 #include "modcmd.h"
24 #include "nickserv.h"
25 #include "saxdb.h"
26 #include "timeq.h"
27
28 #define GLOBAL_CONF_NAME        "services/global"
29
30 #define GLOBAL_DB               "global.db"
31 #define GLOBAL_TEMP_DB          "global.db.new"
32
33 /* Global options */
34 #define KEY_DB_BACKUP_FREQ      "db_backup_freq"
35 #define KEY_ANNOUNCEMENTS_DEFAULT "announcements_default"
36 #define KEY_NICK                "nick"
37
38 /* Message data */
39 #define KEY_FLAGS               "flags"
40 #define KEY_POSTED              "posted"
41 #define KEY_DURATION            "duration"
42 #define KEY_FROM                "from"
43 #define KEY_MESSAGE             "message"
44
45 /* Clarification: Notices are immediate, they are sent to matching users
46    _once_, then forgotten. Messages are stored in Global's database and
47    continually sent to users as they match the target specification until
48    they are deleted. */
49 static const struct message_entry msgtab[] = {
50     { "GMSG_INVALID_TARGET", "$b%s$b is an invalid message target." },
51     { "GMSG_MESSAGE_REQUIRED", "You $bmust$b provide a message to send." },
52     { "GMSG_MESSAGE_SENT", "Message to $b%s$b sent." },
53     { "GMSG_MESSAGE_ADDED", "Message to $b%s$b with ID %ld added." },
54     { "GMSG_MESSAGE_DELETED", "Message $b%s$b deleted." },
55     { "GMSG_ID_INVALID", "$b%s$b is an invalid message ID." },
56     { "GMSG_MESSAGE_COUNT", "$b%d$b messages found." },
57     { "GMSG_NO_MESSAGES", "There are no messages for you." },
58     { "GMSG_NOTICE_SOURCE", "[$b%s$b] Notice from %s:" },
59     { "GMSG_MESSAGE_SOURCE", "[$b%s$b] Notice from %s, posted %s:" },
60     { "GMSG_MOTD_HEADER", "$b------------- MESSAGE(S) OF THE DAY --------------$b" },
61     { "GMSG_MOTD_FOOTER", "$b---------- END OF MESSAGE(S) OF THE DAY ----------$b" },
62     { NULL, NULL }
63 };
64
65 #define GLOBAL_SYNTAX()   svccmd_send_help(user, global, cmd)
66 #define GLOBAL_FUNC(NAME) MODCMD_FUNC(NAME)
67
68 struct userNode *global;
69
70 static struct module *global_module;
71 static struct service *global_service;
72 static struct globalMessage *messageList;
73 static long messageCount;
74 static time_t last_max_alert;
75 static struct log_type *G_LOG;
76
77 static struct
78 {
79     unsigned long db_backup_frequency;
80     unsigned int announcements_default : 1;
81 } global_conf;
82
83 #define global_notice(target, format...) send_message(target , global , ## format)
84
85 void message_expire(void *data);
86
87 static struct globalMessage*
88 message_add(long flags, time_t posted, unsigned long duration, char *from, const char *msg)
89 {
90     struct globalMessage *message;
91
92     message = malloc(sizeof(struct globalMessage));
93
94     if(!message)
95     {
96         return NULL;
97     }
98
99     message->id = messageCount++;
100     message->flags = flags;
101     message->posted = posted;
102     message->duration = duration;
103     message->from = strdup(from);
104     message->message = strdup(msg);
105
106     if(messageList)
107     {
108         messageList->prev = message;
109     }
110
111     message->prev = NULL;
112     message->next = messageList;
113
114     messageList = message;
115
116     if(duration)
117     {
118         timeq_add(now + duration, message_expire, message);
119     }
120
121     return message;
122 }
123
124 static void
125 message_del(struct globalMessage *message)
126 {
127     if(message->duration)
128     {
129         timeq_del(0, NULL, message, TIMEQ_IGNORE_FUNC | TIMEQ_IGNORE_WHEN);
130     }
131
132     if(message->prev) message->prev->next = message->next;
133     else messageList = message->next;
134
135     if(message->next) message->next->prev = message->prev;
136
137     free(message->from);
138     free(message->message);
139     free(message);
140 }
141
142 void message_expire(void *data)
143 {
144     struct globalMessage *message = data;
145
146     message->duration = 0;
147     message_del(message);
148 }
149
150 static struct globalMessage*
151 message_create(struct userNode *user, unsigned int argc, char *argv[])
152 {
153     unsigned long duration = 0;
154     char *text = NULL;
155     long flags = 0;
156     unsigned int i;
157
158     for(i = 0; i < argc; i++)
159     {
160         if((i + 1) > argc)
161         {
162             global_notice(user, "MSG_MISSING_PARAMS", argv[argc]);
163             return NULL;
164         }
165
166         if(!irccasecmp(argv[i], "text"))
167         {
168             i++;
169             text = unsplit_string(argv + i, argc - i, NULL);
170             break;
171         } else if (!irccasecmp(argv[i], "sourceless")) {
172             i++;
173             flags |= MESSAGE_OPTION_SOURCELESS;
174         } else if (!irccasecmp(argv[i], "target")) {
175             i++;
176
177             if(!irccasecmp(argv[i], "all")) {
178                 flags |= MESSAGE_RECIPIENT_ALL;
179             } else if(!irccasecmp(argv[i], "users")) {
180                 flags |= MESSAGE_RECIPIENT_LUSERS;
181             } else if(!irccasecmp(argv[i], "helpers")) {
182                 flags |= MESSAGE_RECIPIENT_HELPERS;
183             } else if(!irccasecmp(argv[i], "opers")) {
184                 flags |= MESSAGE_RECIPIENT_OPERS;
185             } else if(!irccasecmp(argv[i], "staff") || !irccasecmp(argv[i], "privileged")) {
186                 flags |= MESSAGE_RECIPIENT_STAFF;
187             } else if(!irccasecmp(argv[i], "channels")) {
188                 flags |= MESSAGE_RECIPIENT_CHANNELS;
189             } else if(!irccasecmp(argv[i], "announcement") || !irccasecmp(argv[i], "announce")) {
190                 flags |= MESSAGE_RECIPIENT_ANNOUNCE;
191             } else {
192                 global_notice(user, "GMSG_INVALID_TARGET", argv[i]);
193                 return NULL;
194             }
195         } else if (irccasecmp(argv[i], "duration") == 0) {
196             duration = ParseInterval(argv[++i]);
197         } else {
198             global_notice(user, "MSG_INVALID_CRITERIA", argv[i]);
199             return NULL;
200         }
201     }
202
203     if(!flags)
204     {
205         flags = MESSAGE_RECIPIENT_LUSERS;
206     }
207
208     if(!text) {
209         global_notice(user, "GMSG_MESSAGE_REQUIRED");
210         return NULL;
211     }
212
213     return message_add(flags, now, duration, user->handle_info->handle, text);
214 }
215
216 static const char *
217 messageType(const struct globalMessage *message)
218 {
219     if((message->flags & MESSAGE_RECIPIENT_ALL) == MESSAGE_RECIPIENT_ALL)
220     {
221         return "all";
222     }
223     if((message->flags & MESSAGE_RECIPIENT_STAFF) == MESSAGE_RECIPIENT_STAFF)
224     {
225         return "staff";
226     }
227     else if(message->flags & MESSAGE_RECIPIENT_ANNOUNCE)
228     {
229         return "announcement";
230     }
231     else if(message->flags & MESSAGE_RECIPIENT_OPERS)
232     {
233         return "opers";
234     }
235     else if(message->flags & MESSAGE_RECIPIENT_HELPERS)
236     {
237         return "helpers";
238     }
239     else if(message->flags & MESSAGE_RECIPIENT_LUSERS)
240     {
241         return "users";
242     }
243     else
244     {
245         return "channels";
246     }
247 }
248
249 static void
250 notice_target(const char *target, struct globalMessage *message)
251 {
252     if(!(message->flags & MESSAGE_OPTION_SOURCELESS))
253     {
254         if(message->flags & MESSAGE_OPTION_IMMEDIATE)
255         {
256             send_target_message(0, target, global, "GMSG_NOTICE_SOURCE", messageType(message), message->from);
257         }
258         else
259         {
260             char posted[24];
261             struct tm tm;
262
263             localtime_r(&message->posted, &tm);
264             strftime(posted, sizeof(posted), "%I:%M %p, %m/%d/%Y", &tm);
265             send_target_message(0, target, global, "GMSG_MESSAGE_SOURCE", messageType(message), message->from, posted);
266         }
267     }
268
269     send_target_message(4, target, global, "%s", message->message);
270 }
271
272 static int
273 notice_channel(const char *key, void *data, void *extra)
274 {
275     struct chanNode *channel = data;
276     /* It should be safe to assume channel is not NULL. */
277     if(channel->channel_info)
278         notice_target(key, extra);
279     return 0;
280 }
281
282 static void
283 message_send(struct globalMessage *message)
284 {
285     struct userNode *user;
286     unsigned long n;
287     dict_iterator_t it;
288
289     if(message->flags & MESSAGE_RECIPIENT_CHANNELS)
290     {
291         dict_foreach(channels, notice_channel, message);
292     }
293
294     if(message->flags & MESSAGE_RECIPIENT_LUSERS)
295     {
296         notice_target("$*", message);
297         return;
298     }
299
300     if(message->flags & MESSAGE_RECIPIENT_ANNOUNCE)
301     {
302         char announce;
303
304         for (it = dict_first(clients); it; it = iter_next(it)) {
305             user = iter_data(it);
306             if (user->uplink == self) continue;
307             announce = user->handle_info ? user->handle_info->announcements : '?';
308             if (announce == 'n') continue;
309             if ((announce == '?') && !global_conf.announcements_default) continue;
310             notice_target(user->nick, message);
311         }
312     }
313
314     if(message->flags & MESSAGE_RECIPIENT_OPERS)
315     {
316         for(n = 0; n < curr_opers.used; n++)
317         {
318             user = curr_opers.list[n];
319
320             if(user->uplink != self)
321             {
322                 notice_target(user->nick, message);
323             }
324         }
325     }
326
327     if(message->flags & MESSAGE_RECIPIENT_HELPERS)
328     {
329         for(n = 0; n < curr_helpers.used; n++)
330         {
331             user = curr_helpers.list[n];
332             if (IsOper(user))
333                 continue;
334             notice_target(user->nick, message);
335         }
336     }
337 }
338
339 void
340 global_message(long targets, char *text)
341 {
342     struct globalMessage *message;
343
344     if(!targets || !global)
345         return;
346
347     message = message_add(targets | MESSAGE_OPTION_SOURCELESS, now, 0, "", text);
348     if(!message)
349         return;
350
351     message_send(message);
352     message_del(message);
353 }
354
355 static GLOBAL_FUNC(cmd_notice)
356 {
357     struct globalMessage *message = NULL;
358     const char *recipient = NULL, *text;
359     long target = 0;
360
361     assert(argc >= 3);
362     if(!irccasecmp(argv[1], "all")) {
363         target = MESSAGE_RECIPIENT_ALL;
364     } else if(!irccasecmp(argv[1], "users")) {
365         target = MESSAGE_RECIPIENT_LUSERS;
366     } else if(!irccasecmp(argv[1], "helpers")) {
367         target = MESSAGE_RECIPIENT_HELPERS;
368     } else if(!irccasecmp(argv[1], "opers")) {
369         target = MESSAGE_RECIPIENT_OPERS;
370     } else if(!irccasecmp(argv[1], "staff") || !irccasecmp(argv[1], "privileged")) {
371         target |= MESSAGE_RECIPIENT_HELPERS | MESSAGE_RECIPIENT_OPERS;
372     } else if(!irccasecmp(argv[1], "announcement") || !irccasecmp(argv[1], "announce")) {
373         target |= MESSAGE_RECIPIENT_ANNOUNCE;
374     } else if(!irccasecmp(argv[1], "channels")) {
375         target = MESSAGE_RECIPIENT_CHANNELS;
376     } else {
377         global_notice(user, "GMSG_INVALID_TARGET", argv[1]);
378         return 0;
379     }
380
381     text = unsplit_string(argv + 2, argc - 2, NULL);
382     message = message_add(target | MESSAGE_OPTION_IMMEDIATE, now, 0, user->handle_info->handle, text);
383
384     if(!message)
385     {
386         return 0;
387     }
388
389     recipient = messageType(message);
390
391     message_send(message);
392     message_del(message);
393
394     global_notice(user, "GMSG_MESSAGE_SENT", recipient);
395     return 1;
396 }
397
398 static GLOBAL_FUNC(cmd_message)
399 {
400     struct globalMessage *message = NULL;
401     const char *recipient = NULL;
402
403     assert(argc >= 3);
404     message = message_create(user, argc - 1, argv + 1);
405     if(!message)
406         return 0;
407     recipient = messageType(message);
408     global_notice(user, "GMSG_MESSAGE_ADDED", recipient, message->id);
409     return 1;
410 }
411
412 static GLOBAL_FUNC(cmd_list)
413 {
414     struct globalMessage *message;
415     struct helpfile_table table;
416     unsigned int length, nn;
417
418     if(!messageList)
419     {
420         global_notice(user, "GMSG_NO_MESSAGES");
421         return 1;
422     }
423
424     for(nn=0, message = messageList; message; nn++, message=message->next) ;
425     table.length = nn+1;
426     table.width = 5;
427     table.flags = TABLE_NO_FREE;
428     table.contents = calloc(table.length, sizeof(char**));
429     table.contents[0] = calloc(table.width, sizeof(char*));
430     table.contents[0][0] = "ID";
431     table.contents[0][1] = "Target";
432     table.contents[0][2] = "Expires";
433     table.contents[0][3] = "From";
434     table.contents[0][4] = "Message";
435
436     for(nn=1, message = messageList; message; nn++, message = message->next)
437     {
438         char buffer[64];
439
440         table.contents[nn] = calloc(table.width, sizeof(char*));
441         snprintf(buffer, sizeof(buffer), "%lu", message->id);
442         table.contents[nn][0] = strdup(buffer);
443         table.contents[nn][1] = messageType(message);
444         if(message->duration)
445             intervalString(buffer, message->posted + message->duration - now, user->handle_info);
446         else
447             strcpy(buffer, "Never.");
448         table.contents[nn][2] = strdup(buffer);
449         table.contents[nn][3] = message->from;
450         length = strlen(message->message);
451         safestrncpy(buffer, message->message, sizeof(buffer));
452         if(length > (sizeof(buffer) - 4))
453         {
454             buffer[sizeof(buffer) - 1] = 0;
455             buffer[sizeof(buffer) - 2] = buffer[sizeof(buffer) - 3] = buffer[sizeof(buffer) - 4] = '.';
456         }
457         table.contents[nn][4] = strdup(buffer);
458     }
459     table_send(global, user->nick, 0, NULL, table);
460     for (nn=1; nn<table.length; nn++)
461     {
462         free((char*)table.contents[nn][0]);
463         free((char*)table.contents[nn][2]);
464         free((char*)table.contents[nn][4]);
465         free(table.contents[nn]);
466     }
467     free(table.contents[0]);
468     free(table.contents);
469
470     return 1;
471 }
472
473 static GLOBAL_FUNC(cmd_remove)
474 {
475     struct globalMessage *message = NULL;
476     unsigned long id;
477
478     assert(argc >= 2);
479     id = strtoul(argv[1], NULL, 0);
480
481     for(message = messageList; message; message = message->next)
482     {
483         if(message->id == id)
484         {
485             message_del(message);
486             global_notice(user, "GMSG_MESSAGE_DELETED", argv[1]);
487             return 1;
488         }
489     }
490
491     global_notice(user, "GMSG_ID_INVALID", argv[1]);
492     return 0;
493 }
494
495 static unsigned int
496 send_messages(struct userNode *user, long mask, int obstreperize)
497 {
498     struct globalMessage *message = messageList;
499     unsigned int count = 0;
500
501     while(message)
502     {
503         if(message->flags & mask)
504         {
505             if (obstreperize && !count)
506                 send_target_message(0, user->nick, global, "GMSG_MOTD_HEADER");
507             notice_target(user->nick, message);
508             count++;
509         }
510
511         message = message->next;
512     }
513     if (obstreperize && count)
514         send_target_message(0, user->nick, global, "GMSG_MOTD_FOOTER");
515     return count;
516 }
517
518 static GLOBAL_FUNC(cmd_messages)
519 {
520     long mask = MESSAGE_RECIPIENT_LUSERS | MESSAGE_RECIPIENT_CHANNELS;
521     unsigned int count;
522
523     if(IsOper(user))
524         mask |= MESSAGE_RECIPIENT_OPERS;
525
526     if(IsHelper(user))
527         mask |= MESSAGE_RECIPIENT_HELPERS;
528
529     count = send_messages(user, mask, 0);
530     if(count)
531         global_notice(user, "GMSG_MESSAGE_COUNT", count);
532     else
533         global_notice(user, "GMSG_NO_MESSAGES");
534
535     return 1;
536 }
537
538 static int
539 global_process_user(struct userNode *user)
540 {
541     if(IsLocal(user) || self->uplink->burst || user->uplink->burst)
542         return 0;
543     send_messages(user, MESSAGE_RECIPIENT_LUSERS, 1);
544
545     /* only alert on new usercount if the record was broken in the last
546      * 30 seconds, and no alert had been sent in that time.
547      */
548     if((now - max_clients_time) <= 30 && (now - last_max_alert) > 30)
549     {
550         char *message;
551         message = alloca(36);
552         sprintf(message, "New user count record: %d", max_clients);
553         global_message(MESSAGE_RECIPIENT_OPERS, message);
554         last_max_alert = now;
555     }
556
557     return 0;
558 }
559
560 static void
561 global_process_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
562 {
563     if(IsHelper(user))
564         send_messages(user, MESSAGE_RECIPIENT_HELPERS, 0);
565 }
566
567 static void
568 global_process_oper(struct userNode *user)
569 {
570     if(user->uplink->burst)
571         return;
572     send_messages(user, MESSAGE_RECIPIENT_OPERS, 0);
573 }
574
575 static void
576 global_conf_read(void)
577 {
578     dict_t conf_node;
579     const char *str;
580
581     if (!(conf_node = conf_get_data(GLOBAL_CONF_NAME, RECDB_OBJECT))) {
582         log_module(G_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", GLOBAL_CONF_NAME);
583         return;
584     }
585
586     str = database_get_data(conf_node, KEY_DB_BACKUP_FREQ, RECDB_QSTRING);
587     global_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
588     str = database_get_data(conf_node, KEY_ANNOUNCEMENTS_DEFAULT, RECDB_QSTRING);
589     global_conf.announcements_default = str ? enabled_string(str) : 1;
590
591     str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
592     if(global && str)
593         NickChange(global, str, 0);
594 }
595
596 static int
597 global_saxdb_read(struct dict *db)
598 {
599     struct record_data *hir;
600     time_t posted;
601     long flags;
602     unsigned long duration;
603     char *str, *from, *message;
604     dict_iterator_t it;
605
606     for(it=dict_first(db); it; it=iter_next(it))
607     {
608         hir = iter_data(it);
609         if(hir->type != RECDB_OBJECT)
610         {
611             log_module(G_LOG, LOG_WARNING, "Unexpected rectype %d for %s.", hir->type, iter_key(it));
612             continue;
613         }
614
615         str = database_get_data(hir->d.object, KEY_FLAGS, RECDB_QSTRING);
616         flags = str ? strtoul(str, NULL, 0) : 0;
617
618         str = database_get_data(hir->d.object, KEY_POSTED, RECDB_QSTRING);
619         posted = str ? strtoul(str, NULL, 0) : 0;
620
621         str = database_get_data(hir->d.object, KEY_DURATION, RECDB_QSTRING);
622         duration = str ? strtoul(str, NULL, 0) : 0;
623
624         from = database_get_data(hir->d.object, KEY_FROM, RECDB_QSTRING);
625         message = database_get_data(hir->d.object, KEY_MESSAGE, RECDB_QSTRING);
626
627         message_add(flags, posted, duration, from, message);
628     }
629     return 0;
630 }
631
632 static int
633 global_saxdb_write(struct saxdb_context *ctx)
634 {
635     struct globalMessage *message;
636     char str[16];
637
638     for(message = messageList; message; message = message->next) {
639         snprintf(str, sizeof(str), "%li", message->id);
640         saxdb_start_record(ctx, str, 0);
641         saxdb_write_int(ctx, KEY_FLAGS, message->flags);
642         saxdb_write_int(ctx, KEY_POSTED, message->posted);
643         saxdb_write_int(ctx, KEY_DURATION, message->duration);
644         saxdb_write_string(ctx, KEY_FROM, message->from);
645         saxdb_write_string(ctx, KEY_MESSAGE, message->message);
646         saxdb_end_record(ctx);
647     }
648     return 0;
649 }
650
651 static void
652 global_db_cleanup(void)
653 {
654     while(messageList)
655         message_del(messageList);
656 }
657
658 void
659 init_global(const char *nick)
660 {
661     G_LOG = log_register_type("Global", "file:global.log");
662     reg_new_user_func(global_process_user);
663     reg_auth_func(global_process_auth);
664     reg_oper_func(global_process_oper);
665
666     conf_register_reload(global_conf_read);
667
668     global_module = module_register("Global", G_LOG, "global.help", NULL);
669     modcmd_register(global_module, "LIST", cmd_list, 1, 0, "flags", "+oper", NULL);
670     modcmd_register(global_module, "MESSAGE", cmd_message, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
671     modcmd_register(global_module, "MESSAGES", cmd_messages, 1, 0, NULL);
672     modcmd_register(global_module, "NOTICE", cmd_notice, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
673     modcmd_register(global_module, "REMOVE", cmd_remove, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
674
675     if(nick)
676     {
677         global = AddService(nick, "Global Services");
678         global_service = service_register(global, 0);
679     }
680     saxdb_register("Global", global_saxdb_read, global_saxdb_write);
681     reg_exit_func(global_db_cleanup);
682     message_register_table(msgtab);
683 }