License update
[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         {
446             intervalString(buffer, message->posted + message->duration - now);
447         }
448         else
449         {
450             strcpy(buffer, "Never.");
451         }
452         table.contents[nn][2] = strdup(buffer);
453         table.contents[nn][3] = message->from;
454         length = strlen(message->message);
455         safestrncpy(buffer, message->message, sizeof(buffer));
456         if(length > (sizeof(buffer) - 4))
457         {
458             buffer[sizeof(buffer) - 1] = 0;
459             buffer[sizeof(buffer) - 2] = buffer[sizeof(buffer) - 3] = buffer[sizeof(buffer) - 4] = '.';
460         }
461         table.contents[nn][4] = strdup(buffer);
462     }
463     table_send(global, user->nick, 0, NULL, table);
464     for (nn=1; nn<table.length; nn++)
465     {
466         free((char*)table.contents[nn][0]);
467         free((char*)table.contents[nn][2]);
468         free((char*)table.contents[nn][4]);
469         free(table.contents[nn]);
470     }
471     free(table.contents[0]);
472     free(table.contents);
473
474     return 1;
475 }
476
477 static GLOBAL_FUNC(cmd_remove)
478 {
479     struct globalMessage *message = NULL;
480     unsigned long id;
481
482     assert(argc >= 2);
483     id = strtoul(argv[1], NULL, 0);
484
485     for(message = messageList; message; message = message->next)
486     {
487         if(message->id == id)
488         {
489             message_del(message);
490             global_notice(user, "GMSG_MESSAGE_DELETED", argv[1]);
491             return 1;
492         }
493     }
494
495     global_notice(user, "GMSG_ID_INVALID", argv[1]);
496     return 0;
497 }
498
499 static unsigned int
500 send_messages(struct userNode *user, long mask, int obstreperize)
501 {
502     struct globalMessage *message = messageList;
503     unsigned int count = 0;
504
505     while(message)
506     {
507         if(message->flags & mask)
508         {
509             if (obstreperize && !count)
510                 send_target_message(0, user->nick, global, "GMSG_MOTD_HEADER");
511             notice_target(user->nick, message);
512             count++;
513         }
514
515         message = message->next;
516     }
517     if (obstreperize && count)
518         send_target_message(0, user->nick, global, "GMSG_MOTD_FOOTER");
519     return count;
520 }
521
522 static GLOBAL_FUNC(cmd_messages)
523 {
524     long mask = MESSAGE_RECIPIENT_LUSERS | MESSAGE_RECIPIENT_CHANNELS;
525     unsigned int count;
526
527     if(IsOper(user))
528         mask |= MESSAGE_RECIPIENT_OPERS;
529
530     if(IsHelper(user))
531         mask |= MESSAGE_RECIPIENT_HELPERS;
532
533     count = send_messages(user, mask, 0);
534     if(count)
535         global_notice(user, "GMSG_MESSAGE_COUNT", count);
536     else
537         global_notice(user, "GMSG_NO_MESSAGES");
538
539     return 1;
540 }
541
542 static int
543 global_process_user(struct userNode *user)
544 {
545     if(IsLocal(user) || self->uplink->burst || user->uplink->burst)
546         return 0;
547     send_messages(user, MESSAGE_RECIPIENT_LUSERS, 1);
548
549     /* only alert on new usercount if the record was broken in the last
550      * 30 seconds, and no alert had been sent in that time.
551      */
552     if((now - max_clients_time) <= 30 && (now - last_max_alert) > 30)
553     {
554         char *message;
555         message = alloca(36);
556         sprintf(message, "New user count record: %d", max_clients);
557         global_message(MESSAGE_RECIPIENT_OPERS, message);
558         last_max_alert = now;
559     }
560
561     return 0;
562 }
563
564 static void
565 global_process_auth(struct userNode *user, UNUSED_ARG(struct handle_info *old_handle))
566 {
567     if(IsHelper(user))
568         send_messages(user, MESSAGE_RECIPIENT_HELPERS, 0);
569 }
570
571 static void
572 global_process_oper(struct userNode *user)
573 {
574     if(user->uplink->burst)
575         return;
576     send_messages(user, MESSAGE_RECIPIENT_OPERS, 0);
577 }
578
579 static void
580 global_conf_read(void)
581 {
582     dict_t conf_node;
583     const char *str;
584
585     if (!(conf_node = conf_get_data(GLOBAL_CONF_NAME, RECDB_OBJECT))) {
586         log_module(G_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", GLOBAL_CONF_NAME);
587         return;
588     }
589
590     str = database_get_data(conf_node, KEY_DB_BACKUP_FREQ, RECDB_QSTRING);
591     global_conf.db_backup_frequency = str ? ParseInterval(str) : 7200;
592     str = database_get_data(conf_node, KEY_ANNOUNCEMENTS_DEFAULT, RECDB_QSTRING);
593     global_conf.announcements_default = str ? enabled_string(str) : 1;
594
595     str = database_get_data(conf_node, KEY_NICK, RECDB_QSTRING);
596     if(global && str)
597         NickChange(global, str, 0);
598 }
599
600 static int
601 global_saxdb_read(struct dict *db)
602 {
603     struct record_data *hir;
604     time_t posted;
605     long flags;
606     unsigned long duration;
607     char *str, *from, *message;
608     dict_iterator_t it;
609
610     for(it=dict_first(db); it; it=iter_next(it))
611     {
612         hir = iter_data(it);
613         if(hir->type != RECDB_OBJECT)
614         {
615             log_module(G_LOG, LOG_WARNING, "Unexpected rectype %d for %s.", hir->type, iter_key(it));
616             continue;
617         }
618
619         str = database_get_data(hir->d.object, KEY_FLAGS, RECDB_QSTRING);
620         flags = str ? strtoul(str, NULL, 0) : 0;
621
622         str = database_get_data(hir->d.object, KEY_POSTED, RECDB_QSTRING);
623         posted = str ? strtoul(str, NULL, 0) : 0;
624
625         str = database_get_data(hir->d.object, KEY_DURATION, RECDB_QSTRING);
626         duration = str ? strtoul(str, NULL, 0) : 0;
627
628         from = database_get_data(hir->d.object, KEY_FROM, RECDB_QSTRING);
629         message = database_get_data(hir->d.object, KEY_MESSAGE, RECDB_QSTRING);
630
631         message_add(flags, posted, duration, from, message);
632     }
633     return 0;
634 }
635
636 static int
637 global_saxdb_write(struct saxdb_context *ctx)
638 {
639     struct globalMessage *message;
640     char str[16];
641
642     for(message = messageList; message; message = message->next) {
643         snprintf(str, sizeof(str), "%li", message->id);
644         saxdb_start_record(ctx, str, 0);
645         saxdb_write_int(ctx, KEY_FLAGS, message->flags);
646         saxdb_write_int(ctx, KEY_POSTED, message->posted);
647         saxdb_write_int(ctx, KEY_DURATION, message->duration);
648         saxdb_write_string(ctx, KEY_FROM, message->from);
649         saxdb_write_string(ctx, KEY_MESSAGE, message->message);
650         saxdb_end_record(ctx);
651     }
652     return 0;
653 }
654
655 static void
656 global_db_cleanup(void)
657 {
658     while(messageList)
659         message_del(messageList);
660 }
661
662 void
663 init_global(const char *nick)
664 {
665     G_LOG = log_register_type("Global", "file:global.log");
666     reg_new_user_func(global_process_user);
667     reg_auth_func(global_process_auth);
668     reg_oper_func(global_process_oper);
669
670     conf_register_reload(global_conf_read);
671
672     global_module = module_register("Global", G_LOG, "global.help", NULL);
673     modcmd_register(global_module, "LIST", cmd_list, 1, 0, "flags", "+oper", NULL);
674     modcmd_register(global_module, "MESSAGE", cmd_message, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
675     modcmd_register(global_module, "MESSAGES", cmd_messages, 1, 0, NULL);
676     modcmd_register(global_module, "NOTICE", cmd_notice, 3, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
677     modcmd_register(global_module, "REMOVE", cmd_remove, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
678
679     if(nick)
680     {
681         global = AddService(nick, "Global Services");
682         global_service = service_register(global, 0);
683     }
684     saxdb_register("Global", global_saxdb_read, global_saxdb_write);
685     reg_exit_func(global_db_cleanup);
686     message_register_table(msgtab);
687 }