1 /* mod-watchdog.c - Watchdog module for srvx
2 * Copyright 2003-2004 Martijn Smit and srvx Development Team
4 * This file is part of srvx.
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.
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.
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.
21 /* Adds new section to srvx.conf:
26 "ban_duration" "2h"; //only if the channel is registered with chanserv
27 "gline_duration" "1h";
28 "punishment_reason" "Your message contained a forbidden word.";
42 #define KEY_BADWORDS "badwords"
43 #define KEY_BADWORD_MASK "mask"
44 #define KEY_BADWORD_TRIGGERED "count"
45 #define KEY_BADWORD_ACTION "action"
46 #define KEY_BADWOR_ALERT "alert"
47 #define KEY_CHANNELS "channel"
48 #define KEY_BADWORDID "badwordid"
50 static const struct message_entry msgtab[] = {
51 { "WDMSG_REGISTER_SUCCESS", "$b%s$b is now registered with %s." },
52 { "WDMSG_NOT_REGISTERED", "$b%s$b is not registered with %s." },
53 { "WDMSG_ALREADY_ADDED", "$b%s$b is already added. (ID: %s)" },
54 { "WDMSG_BADWORD_ADDED", "added '$b%s$b' to the badword list with ID %s." },
55 { "WDMSG_BADWORD_NOT_FOUND", "badword with ID %s does not exist." },
56 { "WDMSG_BADWORD_REMOVED", "badword ID $b%s$b has been removed (mask: '%s')" },
57 { "WDMSG_BADWORD_SET_DONE", "Done." },
58 { "WDMSG_BADWORD_SET_INVALID", "Invalid Option for setting %s" },
59 { "OSMSG_BADWORD_SETTING_INVALID", "unknown setting $b%s$b." },
60 { "WDMSG_BADWORD_SET", "Settings for BadWord entry $b%s$b" },
61 { "WDMSG_BADWORD_SET_MASK", "$bMASK$b: %s" },
62 { "WDMSG_BADWORD_SET_ACTION", "$bACTION$b: %s" },
63 { "WDMSG_BADWORD_ALERT", "%s used badword '%s' in channel: %s" },
70 unsigned int triggered : 29;
71 unsigned int action : 3;
75 struct watchdog_channel {
76 struct chanNode *channel;
77 //struct shitList *shitlist;
80 /* badword.action fields */
81 #define BADACTION_KICK 0
82 #define BADACTION_BAN 1
83 #define BADACTION_KILL 2
84 #define BADACTION_GLINE 3
89 const char *punishment_reason;
90 unsigned long ban_duration;
91 unsigned long gline_duration;
94 const char *watchdog_module_deps[] = { NULL };
95 struct userNode *watchdog;
96 static struct module *watchdog_module;
97 static struct service *watchdog_service;
98 static dict_t shitlist;
99 static dict_t chanlist;
100 static struct log_type *MS_LOG;
101 static unsigned int last_badword_id = 0;
103 static struct watchdog_channel *add_channel(const char *name);
104 static struct badword *add_badword(const char *badword_mask, unsigned int triggered, unsigned int action, const char *id);
105 #define watchdog_notice(target, format...) send_message(target , watchdog , ## format)
107 static MODCMD_FUNC(cmd_addbad)
110 char *mask = unsplit_string(argv + 1, argc - 1, NULL);
111 for (it = dict_first(shitlist); it; it = iter_next(it)) {
112 struct badword *badword = iter_data(it);
113 if(match_ircglob(mask,badword->badword_mask)) {
114 reply("WDMSG_ALREADY_ADDED", mask, badword->id);
119 struct badword *new_badword = add_badword(mask, 0, BADACTION_KICK, NULL);
120 for (it = dict_first(shitlist); it; it = iter_next(it)) {
121 struct badword *badword = iter_data(it);
122 if(match_ircglob(badword->badword_mask, new_badword->badword_mask) && badword != new_badword) {
123 dict_remove(shitlist, badword->id);
127 reply("WDMSG_BADWORD_ADDED", new_badword->badword_mask, new_badword->id);
131 static MODCMD_FUNC(cmd_delbad)
135 for (n=1; n<argc; n++) {
136 struct badword *badword = dict_find(shitlist, argv[n], NULL);
138 reply("WDMSG_BADWORD_NOT_FOUND", argv[n]);
141 reply("WDMSG_BADWORD_REMOVED", argv[n], badword->badword_mask);
142 dict_remove(shitlist, argv[n]);
147 static MODCMD_FUNC(cmd_setbad)
149 struct badword *badword;
150 if ((badword = dict_find(shitlist, argv[1], NULL))) {
153 char *setting = argv[2];
154 char *value = argv[3];
155 for( ii = 0; setting[ ii ]; ii++)
156 setting[ ii ] = toupper( setting[ ii ] );
157 for( ii = 0; value[ ii ]; ii++)
158 value[ ii ] = toupper( value[ ii ] );
159 if(!strcmp("MASK",setting)) {
160 free(badword->badword_mask);
161 badword->badword_mask = strdup(argv[3]);
162 badword->triggered = 0;
163 reply("WDMSG_BADWORD_SET_DONE");
165 else if(!strcmp("ACTION",setting)) {
166 if (!strcmp("1",value) || !strcmp("KICK",value)) {
167 badword->action = BADACTION_KICK;
168 reply("WDMSG_BADWORD_SET_DONE");
169 } else if (!strcmp("2",value) || !strcmp("BAN",value)) {
170 badword->action = BADACTION_BAN;
171 reply("WDMSG_BADWORD_SET_DONE");
172 } else if (!strcmp("3",value) || !strcmp("KILL",value)) {
173 badword->action = BADACTION_KILL;
174 reply("WDMSG_BADWORD_SET_DONE");
175 } else if (!strcmp("4",value) || !strcmp("GLINE",value)) {
176 badword->action = BADACTION_GLINE;
177 reply("WDMSG_BADWORD_SET_DONE");
179 reply("WDMSG_BADWORD_SET_INVALID", setting);
182 else if(!strcmp("ALERT",setting)) {
183 if (!strcmp("0",value)) {
185 } else if (!strcmp("1",value)) {
188 reply("WDMSG_BADWORD_SET_INVALID", setting);
191 reply("WDMSG_BADWORD_SETTING_INVALID", setting);
195 reply("WDMSG_BADWORD_SET", badword->id);
196 reply("WDMSG_BADWORD_SET_MASK", badword->badword_mask);
197 switch(badword->action) {
199 reply("WDMSG_BADWORD_SET_ACTION", "KICK");
202 reply("WDMSG_BADWORD_SET_ACTION", "BAN");
205 reply("WDMSG_BADWORD_SET_ACTION", "KILL");
207 case BADACTION_GLINE:
208 reply("WDMSG_BADWORD_SET_ACTION", "GLINE");
211 reply("WDMSG_BADWORD_SET_ACTION", "*undef*");
215 reply("WDMSG_BADWORD_NOT_FOUND", argv[1]);
222 badwords_sort(const void *pa, const void *pb)
224 struct badword *a = *(struct badword**)pa;
225 struct badword *b = *(struct badword**)pb;
227 return strtoul(a->id, NULL, 0) - strtoul(b->id, NULL, 0);
230 static MODCMD_FUNC(cmd_listbad)
232 struct helpfile_table tbl;
233 unsigned int count = 0, ii = 0;
234 struct badword **badwords;
237 for (it = dict_first(shitlist); it; it = iter_next(it)) {
240 tbl.length = count+1;
243 tbl.flags = TABLE_NO_FREE;
244 tbl.contents = malloc(tbl.length * sizeof(tbl.contents[0]));
245 tbl.contents[0] = malloc(tbl.width * sizeof(tbl.contents[0][0]));
246 tbl.contents[0][0] = "#";
247 tbl.contents[0][1] = "Badword";
248 tbl.contents[0][2] = "Action";
249 tbl.contents[0][3] = "(Triggered)";
250 tbl.contents[0][4] = "Alert";
253 table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
255 free(tbl.contents[0]);
259 badwords = alloca(count * sizeof(badwords[0]));
260 for (it = dict_first(shitlist); it; it = iter_next(it)) {
261 struct badword *bw = iter_data(it);
264 qsort(badwords, count, sizeof(badwords[0]), badwords_sort);
265 for (ii = 1; ii <= count; ii++) {
266 struct badword *bw = badwords[ii-1];
267 tbl.contents[ii] = malloc(tbl.width * sizeof(tbl.contents[0][0]));
268 tbl.contents[ii][0] = strdup(bw->id);
269 tbl.contents[ii][1] = strdup(bw->badword_mask);
272 tbl.contents[ii][2] = "KICK";
275 tbl.contents[ii][2] = "BAN";
278 tbl.contents[ii][2] = "KILL";
280 case BADACTION_GLINE:
281 tbl.contents[ii][2] = "GLINE";
284 tbl.contents[ii][2] = "*undef*";
286 tbl.contents[ii][3] = strtab(bw->triggered);
287 tbl.contents[ii][4] = strtab(bw->alert);
289 table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
290 for(ii = 1; ii < tbl.length; ++ii)
292 free(tbl.contents[ii]);
294 free(tbl.contents[0]);
299 static MODCMD_FUNC(cmd_register)
307 if(channel->bad_channel)
309 reply("CSMSG_ILLEGAL_CHANNEL", channel->name);
314 && (!(mn = GetUserMode(channel, user)) || !(mn->modes & MODE_CHANOP)))
316 reply("CSMSG_MUST_BE_OPPED", channel->name);
324 if((argc < 2) || !IsChannelName(argv[1]))
326 reply("MSG_NOT_CHANNEL_NAME");
330 if(opserv_bad_channel(argv[1]))
332 reply("CSMSG_ILLEGAL_CHANNEL", argv[1]);
336 channel = AddChannel(argv[1], now, NULL, NULL);
339 for (it = dict_first(chanlist); it; it = iter_next(it)) {
340 struct watchdog_channel *chan = iter_data(it);
341 if(chan->channel == channel) {
342 reply("CSMSG_ALREADY_REGGED", channel->name);
347 add_channel(channel->name);
348 reply("WDMSG_REGISTER_SUCCESS", channel->name, watchdog->nick);
352 static MODCMD_FUNC(cmd_unregister)
354 struct watchdog_channel *chan = NULL;
357 for (it = dict_first(chanlist); it; it = iter_next(it)) {
358 chan = iter_data(it);
359 if(chan->channel == channel)
363 if(chan && chan->channel == channel) {
364 //found, unregister it!
366 sprintf(reason, "Unregistered by %s.", user->handle_info->handle);
367 DelChannelUser(watchdog, channel, reason, 0);
368 dict_remove(chanlist, channel->name);
369 reply("CSMSG_UNREG_SUCCESS", channel->name);
372 reply("WDMSG_NOT_REGISTERED", channel->name, watchdog->nick);
379 watchdog_detected_badword(struct userNode *user, struct chanNode *chan, struct badword *badword)
382 char *reason = watchdog_conf.punishment_reason;
383 char mask[IRC_NTOP_MAX_SIZE+3] = { '*', '@', '\0' };
385 if(badword->alert == 1) {
386 log_module(MS_LOG, LOG_WARNING, "WDMSG_BADWORD_ALERT", user->nick, badword->badword_mask, channel->name);
388 switch(badword->action) {
390 hostmask = generate_hostmask(user, GENMASK_STRICT_HOST | GENMASK_ANY_IDENT);
391 sanitize_ircmask(hostmask);
392 if(chan->channel_info) {
394 add_channel_ban(chan->channel_info, hostmask, watchdog->nick, now, now, now + watchdog_conf.ban_duration, reason);
396 struct mod_chanmode change;
397 mod_chanmode_init(&change);
399 change.args[0].mode = MODE_BAN;
400 change.args[0].u.hostmask = hostmask;
401 mod_chanmode_announce(watchdog, chan, &change);
405 if(GetUserMode(chan, user))
406 KickChannelUser(user, chan, watchdog, reason);
409 DelUser(user, watchdog, 1, reason);
411 case BADACTION_GLINE:
412 irc_ntop(mask + 2, sizeof(mask) - 2, &user->ip);
413 gline_add(watchdog->nick, mask, watchdog_conf.gline_duration, reason, now, now, 0, 1);
423 watchdog_channel_message(struct userNode *user, struct chanNode *chan, const char *text, UNUSED_ARG(struct userNode *bot), UNUSED_ARG(unsigned int is_notice))
427 if(!watchdog || !dict_find(chanlist, chan->name, NULL))
430 for (it = dict_first(shitlist); it; it = iter_next(it)) {
431 struct badword *badword = iter_data(it);
432 if(match_ircglob(text, badword->badword_mask)) {
433 watchdog_detected_badword(user, chan, badword);
438 static struct badword*
439 add_badword(const char *badword_mask, unsigned int triggered, unsigned int action, unsigned int alert, const char *id)
441 struct badword *badword;
443 badword = calloc(1, sizeof(*badword));
449 badword->id = strtab(last_badword_id);
451 badword->id = strdup(id);
452 badword->badword_mask = strdup(badword_mask);
453 badword->triggered = triggered;
454 badword->action = action;
455 badword->alert = alert;
456 dict_insert(shitlist, badword->id, badword);
461 free_shitlist_entry(void *data)
463 struct badword *badword = data;
465 free(badword->badword_mask);
469 static struct watchdog_channel*
470 add_channel(const char *name)
472 struct watchdog_channel *wc;
473 struct mod_chanmode *change;
475 if(!watchdog) //module disabled
478 wc = calloc(1, sizeof(*wc));
482 wc->channel = AddChannel(name, now, NULL, NULL);
483 change = mod_chanmode_alloc(1);
485 change->args[0].mode = MODE_CHANOP;
486 change->args[0].u.member = AddChannelUser(watchdog, wc->channel);
487 mod_chanmode_announce(watchdog, wc->channel, change);
488 mod_chanmode_free(change);
489 dict_insert(chanlist, wc->channel->name, wc);
494 free_chanlist_entry(void *data)
496 struct watchdog_channel *wc = data;
502 watchdog_conf_read(void)
507 str = "modules/watchdog";
508 if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
509 log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
513 str = database_get_data(conf_node, "nick", RECDB_QSTRING);
514 if(watchdog_conf.nick && strcmp(watchdog_conf.nick, str)) {
517 watchdog_conf.nick = str;
519 str = database_get_data(conf_node, "modes", RECDB_QSTRING);
520 watchdog_conf.modes = (str ? str : NULL);
522 str = database_get_data(conf_node, "ban_duration", RECDB_QSTRING);
523 watchdog_conf.ban_duration = str ? ParseInterval(str) : ParseInterval("2h");
525 str = database_get_data(conf_node, "gline_duration", RECDB_QSTRING);
526 watchdog_conf.gline_duration = str ? ParseInterval(str) : ParseInterval("1h");
528 str = database_get_data(conf_node, "punishment_reason", RECDB_QSTRING);
529 watchdog_conf.punishment_reason = (str ? str : "Your message contained a forbidden word.");
534 watchdog_saxdb_read_shitlist(const char *name, void *data, UNUSED_ARG(void *extra))
536 struct record_data *rd = data;
538 char *triggered, *action;
540 if (rd->type == RECDB_OBJECT) {
541 dict_t obj = GET_RECORD_OBJECT(rd);
542 /* new style structure */
543 badword = database_get_data(obj, KEY_BADWORD_MASK, RECDB_QSTRING);
544 triggered = database_get_data(obj, KEY_BADWORD_TRIGGERED, RECDB_QSTRING);
545 action = database_get_data(obj, KEY_BADWORD_ACTION, RECDB_QSTRING);
546 alert = database_get_data(obj, KEY_BADWOR_ALERT, RECDB_QSTRING);
548 add_badword(badword, strtoul(triggered, NULL, 0), strtoul(action, NULL, 0), strtoul(alert, NULL, 0), name);
554 watchdog_saxdb_read_chanlist(const char *name, void *data, UNUSED_ARG(void *extra))
556 struct record_data *rd = data;
558 if (rd->type == RECDB_OBJECT) {
559 //dict_t obj = GET_RECORD_OBJECT(rd);
560 /* nothing in here, yet */
568 watchdog_saxdb_read(struct dict *db)
572 str = database_get_data(db, KEY_BADWORDID, RECDB_QSTRING);
573 last_badword_id = str ? strtoul(str, NULL, 0) : 0;
575 if ((object = database_get_data(db, KEY_BADWORDS, RECDB_OBJECT)))
576 dict_foreach(object, watchdog_saxdb_read_shitlist, NULL);
578 if ((object = database_get_data(db, KEY_CHANNELS, RECDB_OBJECT)))
579 dict_foreach(object, watchdog_saxdb_read_chanlist, NULL);
585 watchdog_saxdb_write(struct saxdb_context *ctx)
589 saxdb_write_int(ctx, KEY_BADWORDID, last_badword_id);
591 if (dict_size(shitlist)) {
592 saxdb_start_record(ctx, KEY_BADWORDS, 1);
593 for (it = dict_first(shitlist); it; it = iter_next(it)) {
594 struct badword *badword = iter_data(it);
595 if(badword && badword->badword_mask) {
596 saxdb_start_record(ctx, iter_key(it), 0);
598 saxdb_write_string(ctx, KEY_BADWORD_MASK, badword->badword_mask);
599 saxdb_write_int(ctx, KEY_BADWORD_TRIGGERED, badword->triggered);
600 saxdb_write_int(ctx, KEY_BADWORD_ACTION, badword->action);
601 saxdb_write_int(ctx, KEY_BADWOR_ALERT, badword->alert);
603 saxdb_end_record(ctx);
606 saxdb_end_record(ctx);
609 if (dict_size(chanlist)) {
610 saxdb_start_record(ctx, KEY_CHANNELS, 1);
611 for (it = dict_first(chanlist); it; it = iter_next(it)) {
612 struct watchdog_channel *wc = iter_data(it);
613 if(wc && wc->channel && wc->channel->name) {
614 saxdb_start_record(ctx, wc->channel->name, 0);
616 saxdb_end_record(ctx);
619 saxdb_end_record(ctx);
626 watchdog_cleanup(void)
628 dict_delete(shitlist);
629 dict_delete(chanlist);
635 MS_LOG = log_register_type("Watchdog", "file:watchdog.log");
637 /* set up shitlist dict */
638 dict_delete(shitlist);
639 shitlist = dict_new();
640 dict_set_free_data(shitlist, free_shitlist_entry);
641 /* set up chanlist dict */
642 dict_delete(chanlist);
643 chanlist = dict_new();
644 dict_set_free_data(chanlist, free_chanlist_entry);
646 const char *nick, *modes;
647 if((nick = conf_get_data("modules/watchdog/nick", RECDB_QSTRING))) {
648 modes = conf_get_data("modules/watchdog/modes", RECDB_QSTRING);
649 watchdog = AddLocalUser(nick, nick, NULL, "Watchdog Service", modes);
650 watchdog_service = service_register(watchdog);
651 watchdog_service->trigger = ',';
652 reg_allchanmsg_func(watchdog, watchdog_channel_message);
655 conf_register_reload(watchdog_conf_read);
656 reg_exit_func(watchdog_cleanup);
657 saxdb_register("Watchdog", watchdog_saxdb_read, watchdog_saxdb_write);
659 watchdog_module = module_register("Watchdog", MS_LOG, "mod-watchdog.help", NULL);
660 modcmd_register(watchdog_module, "addbad", cmd_addbad, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
661 modcmd_register(watchdog_module, "delbad", cmd_delbad, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
662 modcmd_register(watchdog_module, "setbad", cmd_setbad, 2, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
663 modcmd_register(watchdog_module, "listbad", cmd_listbad, 1, MODCMD_REQUIRE_AUTHED, "flags", "+oper", NULL);
664 modcmd_register(watchdog_module, "register", cmd_register, 1, MODCMD_REQUIRE_AUTHED, "flags", "+acceptchan,+helping", NULL);
665 modcmd_register(watchdog_module, "unregister", cmd_unregister, 1, MODCMD_REQUIRE_AUTHED | MODCMD_REQUIRE_CHANNEL, "flags", "+helping", NULL);
666 message_register_table(msgtab);
672 watchdog_finalize(void) {
676 str = "modules/watchdog";
677 if (!(conf_node = conf_get_data(str, RECDB_OBJECT))) {
678 log_module(MS_LOG, LOG_ERROR, "config node `%s' is missing or has wrong type.", str);
682 str = database_get_data(conf_node, "nick", RECDB_QSTRING);
683 if (str) watchdog_conf.nick = str;
685 str = database_get_data(conf_node, "modes", RECDB_QSTRING);
686 if (str) watchdog_conf.modes = str;
688 str = database_get_data(conf_node, "ban_duration", RECDB_QSTRING);
689 if (str) watchdog_conf.ban_duration = ParseInterval(str);
691 str = database_get_data(conf_node, "gline_duration", RECDB_QSTRING);
692 if (str) watchdog_conf.gline_duration = ParseInterval(str);
694 str = database_get_data(conf_node, "punishment_reason", RECDB_QSTRING);
695 if (str) watchdog_conf.punishment_reason = str;