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