add hope again
[fillybot.git] / mod.c
blobb1da9ce148bdb4a93824bb5e92823dc90f5b8ca1
1 #include "mod.h"
3 #include <libxml/xmlstring.h>
4 #include <libxml/HTMLparser.h>
6 #include <stdint.h>
7 #include <inttypes.h>
8 #include <sys/wait.h>
9 #include <curl/curl.h>
11 #include <magic.h>
13 #include "entities.h"
14 #include "quotes.h"
16 static const char *nick_self;
17 static unsigned ponify;
18 #define elements(x) (sizeof(x)/sizeof(*x))
20 static struct tdb_context *feed_db, *chan_db, *mail_db, *seen_db;
22 static struct command_hash command_channel = { "#", NULL };
24 #define sstrncmp(a, b) (strncmp(a, b, sizeof(b)-1))
25 #define sstrncasecmp(a, b) (strncasecmp(a, b, sizeof(b)-1))
27 #define SPAM_CUTOFF 5
29 static int pipe_command(struct bio *b, const char *target, const char *nick, int redirect_stdout, int redirect_stderr, char *argv[])
31 int fd[2];
32 int lines = 0;
33 if (pipe2(fd, O_CLOEXEC) < 0) {
34 privmsg(b, target, "Could not create pipe: %m");
35 return -1;
37 pid_t pid = fork();
38 if (pid < 0) {
39 privmsg(b, target, "Could not fork: %m");
40 close(fd[0]);
41 close(fd[1]);
42 return -1;
43 } else if (!pid) {
44 int fdnull = open("/dev/null", O_WRONLY|O_CLOEXEC);
45 close(fd[0]);
46 if (dup3(redirect_stdout ? fd[1] : fdnull, 1, 0) < 0)
47 exit(errno);
48 if (dup3(redirect_stderr ? fd[1] : fdnull, 2, 0) < 0)
49 exit(errno);
50 exit(execv(argv[0], argv));
51 } else {
52 int loc = -1;
53 char buffer[0x100];
54 int bufptr = 0, ret;
55 close(fd[1]);
56 fcntl(fd[0], F_SETFL, O_NONBLOCK);
57 while ((ret = waitpid(pid, &loc, WNOHANG)) >= 0) {
58 while (read(fd[0], buffer+bufptr, 1) == 1) {
59 if (buffer[bufptr] != '\n' && bufptr < sizeof(buffer)-1) {
60 bufptr++;
61 continue;
62 } else if (bufptr) {
63 buffer[bufptr] = 0;
64 bufptr = 0;
65 lines++;
66 if (lines < SPAM_CUTOFF)
67 privmsg(b, nick, "%s", buffer);
68 else
69 fprintf(stderr, "%s\n", buffer);
73 if (ret) {
74 if (bufptr) {
75 buffer[bufptr] = 0;
76 if (lines < SPAM_CUTOFF)
77 privmsg(b, nick, "%s", buffer);
78 else
79 fprintf(stderr, "%s\n", buffer);
81 if (lines >= SPAM_CUTOFF)
82 privmsg(b, nick, "%i lines suppressed", lines - SPAM_CUTOFF + 1);
83 break;
86 if (ret < 0)
87 privmsg(b, target, "error on waitpid: %m");
88 else
89 ret = loc;
90 close(fd[0]);
91 return ret;
95 #include "local.c"
97 static struct command_hash commands[2048];
99 static void command_abort(struct bio *b, const char *nick, const char *host, const char *target, char *args)
101 abort();
102 return;
105 static void command_crash(struct bio *b, const char *nick, const char *host, const char *target, char *args)
107 *(char*)0 = 0;
110 static void command_coinflip(struct bio *b, const char *nick, const char *host, const char *target, char *args)
112 if (getrand() & 1)
113 action(b, target, "whirrrs and clicks excitedly at %s", nick);
114 else
115 action(b, target, "eyes %s as nothing happens", nick);
118 static void command_shesaid(struct bio *b, const char *nick, const char *host, const char *target, char *args)
120 privmsg(b, target, "%s: \"%s\"", args ? args : nick, women_quotes[getrand() % elements(women_quotes)]);
123 static void squash(char *title)
125 char *start = title;
126 goto start;
127 while (*title) {
128 while (*title != '\n' && *title != '\r' && *title != '\t' && *title != ' ' && *title) {
129 *(start++) = *(title++);
131 if (*title)
132 *(start++) = ' ';
133 start:
134 while (*title == '\n' || *title == '\r' || *title == '\t' || *title == ' ')
135 title++;
137 *start = 0;
140 struct curl_download_context
142 char *data;
143 size_t len;
146 static size_t write_data(void *ptr, size_t size, size_t nmemb, void *member) {
147 struct curl_download_context *ctx = member;
148 size *= nmemb;
149 ctx->data = realloc(ctx->data, ctx->len + size);
150 memcpy(ctx->data + ctx->len, ptr, size);
151 ctx->len += size;
152 return size;
155 static const char *get_text(xmlNode *cur)
157 for (; cur; cur = cur->next) {
158 if (cur->type == XML_TEXT_NODE)
159 return cur->content;
161 return NULL;
164 static const char *get_link(xmlAttr *cur, const char *which)
166 for (; cur; cur = cur->next) {
167 if (!strcasecmp(cur->name, which))
168 return get_text(cur->children);
170 return NULL;
173 static int get_feed_entry(struct bio *b, const char *nick, const char *target, const char *category, xmlNode *entry, const char **title, const char **link)
175 const char *cur_title = NULL, *cur_link = NULL;
176 xmlNode *cur;
177 int cur_match = !category;
178 for (cur = entry->children; cur; cur = cur->next) {
179 const char *name = cur->name, *cur_cat;
180 if (cur->type != XML_ELEMENT_NODE)
181 continue;
182 else if (!strcasecmp(name, "link")) {
183 const char *ishtml = get_link(cur->properties, "type");
184 const char *rel = get_link(cur->properties, "rel");
185 if ((!ishtml || !strcasecmp(ishtml, "text/html")) &&
186 (!rel || !strcasecmp(rel, "alternate")))
187 cur_link = get_link(cur->properties, "href");
188 } else if (!strcasecmp(name, "title"))
189 cur_title = get_text(cur->children);
190 else if (!cur_match && !strcasecmp(name, "category") &&
191 (cur_cat = get_link(cur->properties, "term")) &&
192 strcasestr(cur_cat, category))
193 cur_match = 1;
196 if (cur_title)
197 *title = cur_title;
198 else
199 *title = "<no title>";
200 *link = cur_link;
201 return !cur_link ? -1 : cur_match;
204 static const char *walk_feed(struct bio *b, const char *nick, const char *target, const char *url, const char *category, xmlNode *root, const char *last_link)
206 const char *main_title = NULL, *main_subtitle = NULL, *main_link = NULL, *title, *link, *prev_link = NULL, *prev_title = NULL;
207 char *t;
208 int match, updates = 0;
209 xmlNode *cur, *entry = NULL;
210 for (cur = root->children; cur; cur = cur->next) {
211 const char *name = cur->name;
212 if (cur->type != XML_ELEMENT_NODE)
213 continue;
214 else if (!strcasecmp(name, "category"))
215 continue;
216 else if (!strcasecmp(name, "entry"))
217 entry = entry ? entry : cur;
218 else if (!strcasecmp(name, "title"))
219 main_title = get_text(cur->children);
220 else if (!strcasecmp(name, "subtitle"))
221 main_subtitle = get_text(cur->children);
222 else if (!strcasecmp(name, "link")) {
223 const char *ishtml = get_link(cur->properties, "type");
224 const char *rel = get_link(cur->properties, "rel");
225 if ((!ishtml || !strcasecmp(ishtml, "text/html")) && (!rel || !strcasecmp(rel, "alternate")))
226 main_link = get_link(cur->properties, "href");
229 if (!main_link || !main_title) {
230 privmsg(b, target, "%s: Failed to parse main: %s %s", nick, main_link, main_title);
231 return NULL;
233 if (!entry)
234 return NULL;
236 match = get_feed_entry(b, nick, target, category, entry, &title, &link);
237 for (; !match && entry; entry = entry->next) {
238 if (!strcasecmp(entry->name, "entry"))
239 match = get_feed_entry(b, nick, target, category, entry, &title, &link);
242 if (match < 0)
243 return NULL;
244 if (!last_link) {
245 if (link) {
246 if (main_subtitle)
247 privmsg(b, target, "adding feed %s \"%s\": %s", main_link, main_title, main_subtitle);
248 else
249 privmsg(b, target, "adding feed %s \"%s\"", main_link, main_title);
251 if (match > 0) {
252 t = strdup(title);
253 squash(t);
254 privmsg(b, target, "Most recent entry: %s %s", link, t);
255 free(t);
256 } else
257 privmsg(b, target, "Currently having no entries for this feed that matches the filter");
258 return link;
260 if (!strcmp(last_link, link) || !match || !entry)
261 return link;
263 for (entry = entry->next; entry; entry = entry->next) {
264 const char *cur_link, *cur_title;
265 if (strcasecmp(entry->name, "entry"))
266 continue;
267 match = get_feed_entry(b, nick, target, category, entry, &cur_title, &cur_link);
268 if (match < 0 || !strcmp(last_link, cur_link))
269 break;
270 if (match) {
271 prev_link = cur_link;
272 prev_title = cur_title;
273 updates++;
276 if (updates == 1) {
277 t = strdup(prev_title);
278 squash(t);
279 privmsg(b, target, "( %s ): %s", prev_link, t);
280 free(t);
281 } else if (updates > 1)
282 privmsg(b, target, "( %s ): %u updates, linking most recent", main_link, 1+updates);
283 t = strdup(title);
284 squash(t);
285 privmsg(b, target, "( %s ): %s", link, t);
286 free(t);
287 return link;
290 static const char *walk_rss(struct bio *b, const char *nick, const char *target, const char *url, xmlNode *root, const char *last_link)
292 const char *main_title = NULL, *main_link = NULL, *title = NULL, *link = NULL, *ver;
293 char *t;
294 xmlNode *cur, *entry = NULL;
295 ver = get_link(root->properties, "version");
296 if (!ver || strcmp(ver, "2.0")) {
297 if (!ver)
298 privmsg(b, target, "%s: Could not parse rss feed", nick);
299 else
300 privmsg(b, target, "%s: Invalid rss version \"%s\"", nick, ver);
301 return NULL;
303 for (cur = root->children; cur && cur->type != XML_ELEMENT_NODE; cur = cur->next);
304 if (!cur)
305 return NULL;
306 for (cur = cur->children; cur; cur = cur->next) {
307 const char *name = cur->name;
308 if (cur->type != XML_ELEMENT_NODE)
309 continue;
310 if (!strcasecmp(name, "title"))
311 main_title = get_text(cur->children);
312 else if (!strcasecmp(name, "link"))
313 main_link = main_link ? main_link : get_text(cur->children);
314 else if (!strcasecmp(name, "item"))
315 entry = entry ? entry : cur;
317 if (!main_link || !main_title) {
318 privmsg(b, target, "%s: Failed to parse main: %s %s", nick, main_link, main_title);
319 return NULL;
321 if (!entry)
322 return NULL;
324 link = title = NULL;
325 for (cur = entry->children; cur; cur = cur->next) {
326 const char *name = cur->name;
327 if (cur->type != XML_ELEMENT_NODE)
328 continue;
329 if (!strcasecmp(name, "title"))
330 title = get_text(cur->children);
331 else if (!strcasecmp(name, "link") && !link)
332 link = get_text(cur->children);
334 if (!title)
335 title = "<no title>";
336 if (!link) {
337 privmsg(b, target, "%s: Failed to parse entry: %s %s", nick, link, title);
338 return NULL;
340 if (!last_link) {
341 t = strdup(title);
342 privmsg(b, target, "adding feed %s \"%s\"", main_link, main_title);
343 squash(t);
344 privmsg(b, target, "Most recent entry: %s %s", link, t);
345 free(t);
346 } else if (strcmp(last_link, link)) {
347 int updates = 0;
348 const char *prev_title = NULL, *prev_link = NULL, *cur_title = NULL, *cur_link = NULL;
349 for (entry = entry->next; entry; entry = entry->next) {
350 if (strcasecmp(entry->name, "item"))
351 continue;
352 prev_title = cur_title;
353 prev_link = cur_link;
354 cur_title = cur_link = NULL;
355 for (cur = entry->children; cur; cur = cur->next) {
356 const char *name = cur->name;
357 if (cur->type != XML_ELEMENT_NODE)
358 continue;
359 if (!strcasecmp(name, "title"))
360 cur_title = get_text(cur->children);
361 else if (!strcasecmp(name, "link") && !cur_link)
362 cur_link = get_text(cur->children);
364 if (!cur_title)
365 cur_title = "<no title>";
366 if (!cur_link || !strcmp(last_link, cur_link))
367 break;
368 updates++;
370 if (updates == 1) {
371 t = strdup(prev_title);
372 squash(t);
373 privmsg(b, target, "( %s ): %s", prev_link, t);
374 free(t);
375 } else if (updates > 1)
376 privmsg(b, target, "( %s ): %u updates, linking most recent", main_link, 1+updates);
377 t = strdup(title);
378 squash(t);
379 privmsg(b, target, "( %s ): %s", link, t);
380 free(t);
382 return link;
385 // HTML is a mess, so I'm just walking the tree depth first until I find the next element..
386 static xmlNode *next_link(xmlNode *cur_node)
388 if (cur_node->children)
389 return cur_node->children;
390 while (cur_node) {
391 if (cur_node->next)
392 return cur_node->next;
393 cur_node = cur_node->parent;
395 return NULL;
398 static const char *get_atom_link(xmlNode *cur)
400 for (; cur; cur = next_link(cur)) {
401 if (cur->type != XML_ELEMENT_NODE)
402 continue;
403 if (!strcasecmp(cur->name, "link")) {
404 const char *isxml = get_link(cur->properties, "type");
405 if (isxml && !strcasecmp(isxml, "application/atom+xml"))
406 return get_link(cur->properties, "href");
409 return NULL;
412 static const char *get_rss_link(xmlNode *cur)
414 for (; cur; cur = next_link(cur)) {
415 if (cur->type != XML_ELEMENT_NODE)
416 continue;
417 if (!strcasecmp(cur->name, "link")) {
418 const char *isxml = get_link(cur->properties, "type");
419 if (isxml && !strcasecmp(isxml, "application/rss+xml"))
420 return get_link(cur->properties, "href");
423 return NULL;
426 static void do_html(struct bio *b, const char *nick, const char *target, const char *url, const char *data, unsigned len)
428 htmlDocPtr ctx = htmlReadMemory(data, len, 0, url, HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING);
429 xmlNode *root = xmlDocGetRootElement(ctx);
430 const char *link = get_atom_link(root);
431 if (link)
432 privmsg(b, target, "%s: not a valid feed link, try atom: %s", nick, link);
433 else if ((link = get_rss_link(root)))
434 privmsg(b, target, "%s: not a valid feed link, try rss: %s", nick, link);
435 else
436 privmsg(b, target, "%s: not a valid feed link, no suggestion found", nick);
437 xmlFreeDoc(ctx);
440 static size_t get_time_from_header(void *data, size_t size, size_t size2, void *ptr)
442 char *d, *e;
443 size *= size2;
444 if (sstrncmp(data, "Last-Modified: "))
445 return size;
446 data += sizeof("Last-Modified: ")-1;
447 *(char**)ptr = d = strdup(data);
448 if ((e = strchr(d, '\r')))
449 *e = 0;
450 return size;
453 static int check_single_feed(struct bio *b, const char *target, TDB_DATA key, const char *last_modified, const char *url, const char *link, const char *nick)
455 struct curl_download_context curl_ctx = {};
456 struct curl_slist *headers = NULL;
457 char error[CURL_ERROR_SIZE], *category = strchr(url, '#');
458 int retval = -1;
459 headers = curl_slist_append(headers, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
460 headers = curl_slist_append(headers, "Accept: */*");
461 if (category)
462 *(category++) = 0;
464 CURL *h = curl_easy_init();
465 curl_easy_setopt(h, CURLOPT_HTTPHEADER, headers);
466 curl_easy_setopt(h, CURLOPT_URL, url);
467 curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, write_data);
468 curl_easy_setopt(h, CURLOPT_WRITEDATA, &curl_ctx);
469 curl_easy_setopt(h, CURLOPT_ERRORBUFFER, error);
470 curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1);
471 curl_easy_setopt(h, CURLOPT_MAXREDIRS, 3);
472 curl_easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0);
473 curl_easy_setopt(h, CURLOPT_TIMEOUT, 8);
474 curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT, 8);
475 curl_easy_setopt(h, CURLOPT_FILETIME, 1);
476 curl_easy_setopt(h, CURLOPT_HEADERFUNCTION, get_time_from_header);
477 curl_easy_setopt(h, CURLOPT_WRITEHEADER, &last_modified);
478 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
480 if (last_modified) {
481 char *tmp;
482 asprintf(&tmp, "If-Modified-Since: %s", last_modified);
483 headers = curl_slist_append(headers, tmp);
484 free(tmp);
487 int success = curl_easy_perform(h);
488 curl_slist_free_all(headers);
489 if (success == CURLE_OK) {
490 char *mime = NULL;
491 long code;
492 curl_easy_getinfo(h, CURLINFO_CONTENT_TYPE, &mime);
493 curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &code);
494 if (code == 304)
496 else if (!mime ||
497 !sstrncmp(mime, "application/xml") ||
498 !sstrncmp(mime, "text/xml") ||
499 !sstrncmp(mime, "application/rss+xml") ||
500 !sstrncmp(mime, "application/atom+xml")) {
501 const char *ret_link = NULL;
502 xmlDocPtr ctx = xmlReadMemory(curl_ctx.data, curl_ctx.len, 0, url, XML_PARSE_NOWARNING | XML_PARSE_NOERROR);
503 xmlNode *root = xmlDocGetRootElement(ctx);
505 if (!root || !root->name)
506 fprintf(stderr, "Failed to parse feed %s %p", url, root);
507 else if (!strcasecmp(root->name, "feed"))
508 ret_link = walk_feed(b, nick, target, url, category, root, link);
509 else if (!strcasecmp(root->name, "rss"))
510 ret_link = walk_rss(b, nick, target, url, root, link);
511 else {
512 privmsg(b, target, "Unknown feed type \"%s\"", root->name);
513 goto free_ctx;
515 if (category)
516 category[-1] = '#';
518 if (ret_link && (!link || strcmp(ret_link, link))) {
519 TDB_DATA val;
520 asprintf((char**)&val.dptr, "%s\001%s", last_modified ? last_modified : "", ret_link);
521 val.dsize = strlen(val.dptr)+1;
522 if (tdb_store(feed_db, key, val, 0) < 0)
523 privmsg(b, target, "updating returns %s", tdb_errorstr(feed_db));
524 free(val.dptr);
525 retval = 1;
527 else
528 retval = 0;
530 free_ctx:
531 xmlFreeDoc(ctx);
532 } else if (link)
534 else if (!sstrncmp(mime, "text/html") ||
535 !sstrncmp(mime, "application/xhtml+xml"))
536 do_html(b, nick, target, url, curl_ctx.data, curl_ctx.len);
537 else
538 privmsg(b, target, "unhandled content type %s", mime);
539 } else if (!link)
540 privmsg(b, target, "Error %s (%u)", error, success);
542 free(curl_ctx.data);
543 curl_easy_cleanup(h);
544 return retval;
547 static void command_follow(struct bio *b, const char *nick, const char *host, const char *target, char *args)
549 char *space, *last_link = NULL;
550 TDB_DATA key, val;
551 int ret;
552 if (!feed_db)
553 return;
554 if (target[0] != '#') {
555 privmsg(b, target, "%s: Can only follow on channels", nick);
556 return;
558 if (!args || !*args) {
559 privmsg(b, target, "%s: Usage: !follow <url>", nick);
560 return;
563 if (((space = strchr(args, ' ')) && space < strchr(args, '#')) ||
564 (sstrncmp(args, "http://") && sstrncmp(args, "https://"))) {
565 privmsg(b, target, "%s: Invalid url", nick);
566 return;
569 key.dsize = asprintf((char**)&key.dptr, "%s,%s", target, args)+1;
570 val = tdb_fetch(feed_db, key);
571 if (val.dptr)
572 last_link = strchr(val.dptr, '\001');
573 ret = check_single_feed(b, target, key, NULL, args, last_link ? last_link+1 : NULL, nick);
574 free(val.dptr);
575 if (!ret)
576 privmsg(b, target, "%s: Not updated", nick);
577 free(key.dptr);
580 static void channel_feed_check(struct bio *b, const char *target, int64_t now)
582 int len = strlen(target);
583 int64_t save[] = { now, 0, 0 };
585 TDB_DATA chan, res;
586 if (!feed_db || !chan_db)
587 return;
588 chan.dptr = (char*)target;
589 chan.dsize = len+1;
590 res = tdb_fetch(chan_db, chan);
591 if (res.dptr && res.dsize >= 8) {
592 uint64_t then = *(uint64_t*)res.dptr;
593 if (now - then <= 8000)
594 return;
595 if (res.dsize >= 16)
596 save[1] = ((uint64_t*)res.dptr)[1];
597 if (res.dsize >= 24)
598 save[2] = ((uint64_t*)res.dptr)[2];
600 free(res.dptr);
601 res.dptr = (unsigned char*)save;
602 res.dsize = sizeof(save);
603 if (tdb_store(chan_db, chan, res, 0) < 0) {
604 static int complain_db;
605 if (!complain_db++)
606 privmsg(b, target, "updating database: %s", tdb_errorstr(feed_db));
607 return;
610 for (TDB_DATA d = tdb_firstkey(feed_db); d.dptr;) {
611 TDB_DATA f = tdb_fetch(feed_db, d);
612 TDB_DATA next = tdb_nextkey(feed_db, d);
614 if (!strncmp(d.dptr, target, len) && d.dptr[len] == ',') {
615 const char *url = (char*)d.dptr + len + 1;
616 char *sep;
617 if ((sep = strchr(f.dptr, '\001'))) {
618 *(sep++) = 0;
619 check_single_feed(b, target, d, f.dptr, url, sep, target);
622 free(d.dptr);
623 free(f.dptr);
624 d = next;
628 static void command_unfollow(struct bio *b, const char *nick, const char *host, const char *target, char *args)
630 TDB_DATA key;
631 char *url;
633 if (!feed_db)
634 return;
636 if (!(url = token(&args, ' ')) || (sstrncmp(url, "http://") && sstrncmp(url, "https://"))) {
637 privmsg(b, target, "%s: Invalid url", nick);
638 return;
640 if (target[0] != '#') {
641 privmsg(b, target, "%s: Can only unfollow on channels", nick);
642 return;
644 key.dsize = asprintf((char**)&key.dptr, "%s,%s", target, url)+1;
645 if (tdb_delete(feed_db, key) < 0) {
646 if (tdb_error(feed_db) == TDB_ERR_NOEXIST)
647 privmsg(b, target, "%s: Not following %s on this channel", nick, url);
648 else
649 privmsg(b, target, "%s: Could not delete: %s", nick, tdb_errorstr(feed_db));
650 } else
651 privmsg(b, target, "%s: No longer following %s", nick, url);
652 free(key.dptr);
655 static void command_g(struct bio *b, const char *nick, const char *host, const char *target, char *args)
657 int ret = 0;
658 int64_t g = 0, g_total = 0;
659 TDB_DATA chan, res;
661 if (!chan_db || target[0] != '#')
662 return;
664 chan.dptr = (char*)target;
665 chan.dsize = strlen(chan.dptr)+1;
666 res = tdb_fetch(chan_db, chan);
667 if (res.dptr && res.dsize >= 16) {
668 g = ((int64_t*)res.dptr)[1];
669 ((int64_t*)res.dptr)[1] = 0;
670 if (res.dsize >= 24)
671 g_total = ((int64_t*)res.dptr)[2];
672 ret = tdb_store(chan_db, chan, res, 0);
674 free(res.dptr);
675 if (ret < 0)
676 fprintf(stderr, "updating database: %s", tdb_errorstr(feed_db));
677 else
678 privmsg(b, target, "%s: %"PRIi64" g's since last check, %"PRIi64" total", nick, g, g_total);
681 static void command_feeds(struct bio *b, const char *nick, const char *host, const char *target, char *args)
683 int len = strlen(target), found = 0;
684 if (!feed_db)
685 return;
686 if (target[0] != '#') {
687 privmsg(b, target, "%s: Only useful in channels..", nick);
688 return;
690 for (TDB_DATA d = tdb_firstkey(feed_db); d.dptr;) {
691 TDB_DATA f = tdb_fetch(feed_db, d);
692 TDB_DATA next = tdb_nextkey(feed_db, d);
694 if (!strncmp(d.dptr, target, len) && d.dptr[len] == ',') {
695 privmsg(b, target, "%s: following %s", nick, d.dptr + len + 1);
696 found++;
698 free(d.dptr);
699 free(f.dptr);
700 d = next;
702 if (!found)
703 privmsg(b, target, "%s: not following any feed on %s", nick, target);
706 static void command_feed_get(struct bio *b, const char *nick, const char *host, const char *target, char *args)
708 if (!feed_db)
709 return;
710 for (TDB_DATA d = tdb_firstkey(feed_db); d.dptr;) {
711 TDB_DATA next = tdb_nextkey(feed_db, d);
712 if (!args || strcasestr(d.dptr, args)) {
713 TDB_DATA f = tdb_fetch(feed_db, d);
714 privmsg(b, target, "%s: %s = %s", nick, d.dptr, f.dptr);
715 free(f.dptr);
717 if (strlen(d.dptr)+1 < d.dsize) {
718 privmsg(b, target, "%s: removed buggy entry", nick);
719 tdb_delete(feed_db, d);
721 free(d.dptr);
722 d = next;
726 static void command_feed_set(struct bio *b, const char *nick, const char *host, const char *target, char *args)
728 if (!feed_db)
729 return;
730 TDB_DATA key, val;
731 key.dptr = token(&args, ' ');
732 char *value = token(&args, ' ');
733 if (!key.dptr || !value)
734 return;
735 key.dsize = strlen(key.dptr) + 1;
736 val.dsize = strlen(value) + 2;
737 val.dptr = malloc(val.dsize);
738 strcpy(val.dptr+1, value);
739 val.dptr[0] = '\001';
740 if (tdb_store(feed_db, key, val, 0) < 0)
741 privmsg(b, target, "%s: setting failed: %s", nick, tdb_errorstr(feed_db));
742 else
743 privmsg(b, target, "%s: burp", nick);
744 free(val.dptr);
747 static void command_feed_rem(struct bio *b, const char *nick, const char *host, const char *target, char *args)
749 if (!feed_db || !args)
750 return;
751 TDB_DATA key = { .dptr = (unsigned char*)args, .dsize = strlen(args)+1 };
752 if (tdb_delete(feed_db, key) < 0)
753 privmsg(b, target, "%s: removing failed: %s", nick, tdb_errorstr(feed_db));
754 else
755 privmsg(b, target, "%s: burp", nick);
758 static void command_feed_xxx(struct bio *b, const char *nick, const char *host, const char *target, char *args)
760 if (!feed_db)
761 return;
763 tdb_wipe_all(feed_db);
764 privmsg(b, target, "%s: all evidence erased", nick);
767 static void command_seen_xxx(struct bio *b, const char *nick, const char *host, const char *target, char *args)
769 if (!seen_db)
770 return;
772 tdb_wipe_all(seen_db);
773 privmsg(b, target, "%s: all evidence erased", nick);
776 static void command_feed_counter(struct bio *b, const char *nick, const char *host, const char *target, char *args)
778 if (!chan_db)
779 return;
780 tdb_wipe_all(chan_db);
781 privmsg(b, target, "%s: All update counters reset", nick);
784 static char *get_text_appended(xmlNode *cur)
786 for (; cur; cur = cur->next) {
787 if (cur->type != XML_TEXT_NODE)
788 continue;
789 return strdup(cur->content);
791 return NULL;
794 static char *get_title(struct bio *b, xmlNode *cur_node)
796 for (; cur_node; cur_node = next_link(cur_node)) {
797 if (cur_node->type == XML_ELEMENT_NODE && !strcasecmp(cur_node->name, "title")) {
798 if (!cur_node->children)
799 return NULL;
800 return get_text_appended(cur_node->children);
803 return NULL;
806 static void internal_link(struct bio *b, const char *nick, const char *host, const char *target, char *args, unsigned verbose)
808 CURL *h;
809 struct curl_slist *headers = NULL;
810 char error[CURL_ERROR_SIZE];
811 int success, sent = verbose;
812 struct curl_download_context curl_ctx = {};
814 if (!args)
815 return;
816 int64_t stop, start = get_time(b, target);
818 h = curl_easy_init();
819 headers = curl_slist_append(headers, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
820 headers = curl_slist_append(headers, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
821 headers = curl_slist_append(headers, "Accept-Language: en-us,en;q=0.7");
822 headers = curl_slist_append(headers, "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
823 headers = curl_slist_append(headers, "DNT: 1");
824 headers = curl_slist_append(headers, "Connection: keep-alive");
825 curl_easy_setopt(h, CURLOPT_HTTPHEADER, headers);
826 curl_easy_setopt(h, CURLOPT_URL, args);
827 curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, write_data);
828 curl_easy_setopt(h, CURLOPT_WRITEDATA, &curl_ctx);
829 curl_easy_setopt(h, CURLOPT_ERRORBUFFER, error);
830 curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 1);
831 curl_easy_setopt(h, CURLOPT_MAXREDIRS, 3);
832 curl_easy_setopt(h, CURLOPT_SSL_VERIFYPEER, 0);
833 curl_easy_setopt(h, CURLOPT_TIMEOUT, 8);
834 curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT, 8);
835 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
836 success = curl_easy_perform(h);
837 curl_easy_cleanup(h);
838 curl_slist_free_all(headers);
839 if (success == CURLE_OK) {
840 magic_t m = magic_open(MAGIC_MIME_TYPE);
841 magic_load(m, NULL);
842 const char *mime = magic_buffer(m, curl_ctx.data, curl_ctx.len);
843 if (strstr(mime, "text/html") || strstr(mime, "application/xml") || strstr(mime, "application/xhtml+xml")) {
844 htmlDocPtr ctx = htmlReadMemory(curl_ctx.data, curl_ctx.len, 0, args, HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING);
845 xmlNode *root_element = xmlDocGetRootElement(ctx);
846 char *title = get_title(b, root_element);
847 if (title) {
848 char *nuke;
849 squash(title);
850 decode_html_entities_utf8(title, NULL);
851 if ((nuke = strstr(title, " on SoundCloud - Create")))
852 *nuke = 0;
853 if (*title) {
854 privmsg(b, target, "%s linked %s", nick, title);
855 sent = 1;
857 free(title);
859 if (verbose && !title)
860 privmsg(b, target, "%s linked %s page with invalid title", nick, mime);
861 xmlFreeDoc(ctx);
862 } else if (verbose) {
863 magic_setflags(m, MAGIC_COMPRESS);
864 const char *desc = magic_buffer(m, curl_ctx.data, curl_ctx.len);
865 privmsg(b, target, "%s linked type %s", nick, desc);
867 magic_close(m);
869 if (verbose && success != CURLE_OK)
870 privmsg(b, target, "Error %s (%u)", error, success);
871 else if (!sent && (stop = get_time(b, target)) - start >= 15) {
872 static uint64_t last_timeout;
874 if (stop - last_timeout < 45) {
875 privmsg(b, target, "Link (%s) by %s timed out, disabling links for 10 seconds", args, nick);
876 commands[strhash("get") % elements(commands)].disabled_until = stop + 10;
878 last_timeout = stop;
880 free(curl_ctx.data);
883 static void command_get(struct bio *b, const char *nick, const char *host, const char *target, char *args)
885 if (!args || (sstrncmp(args, "http://") && sstrncmp(args, "https://")))
886 return;
887 internal_link(b, nick, host, target, token(&args, ' '), 1);
890 static void command_fabric(struct bio *b, const char *nick, const char *host, const char *target, char *args)
892 privmsg(b, target, "Dumb fabric!");
895 static void command_fun(struct bio *b, const char *nick, const char *host, const char *target, char *args)
897 privmsg(b, target, "Fun?");
900 static void command_admire(struct bio *b, const char *nick, const char *host, const char *target, char *args)
902 char *arg = token(&args, ' ');
903 privmsg(b, target, "%s: I really like your mane!", arg ? arg : nick);
906 static void command_hugs(struct bio *b, const char *nick, const char *host, const char *target, char *args)
908 action(b, target, "gives a lunar hug to %s", args ? args : nick);
911 static void command_hug(struct bio *b, const char *nick, const char *host, const char *target, char *args)
913 if ((host && !strcmp(host, "life.on.the.moon.is.great.I.know.all.about.it")) ||
914 (args && !sstrncasecmp(args, "lu")))
915 command_hugs(b, nick, host, target, args);
916 else
917 action(b, target, "gives a robotic hug to %s", args ? args : nick);
920 static void command_snuggles(struct bio *b, const char *nick, const char *host, const char *target, char *args)
922 action(b, target, "gives a lunar snuggle to %s", args ? args : nick);
925 static void command_snuggle(struct bio *b, const char *nick, const char *host, const char *target, char *args)
927 if ((host && !strcmp(host, "life.on.the.moon.is.great.I.know.all.about.it")) ||
928 (args && !sstrncasecmp(args, "lu")))
929 command_snuggles(b, nick, host, target, args);
930 else
931 action(b, target, "gives a robotic snuggle to %s", args ? args : nick);
934 static void command_cuddles(struct bio *b, const char *nick, const char *host, const char *target, char *args)
936 action(b, target, "gives cuddles to %s in a lunaresque way", args ? args : nick);
939 static void command_cuddle(struct bio *b, const char *nick, const char *host, const char *target, char *args)
941 if ((host && !strcmp(host, "life.on.the.moon.is.great.I.know.all.about.it")) ||
942 (args && !sstrncasecmp(args, "lu")))
943 command_cuddles(b, nick, host, target, args);
944 else
945 action(b, target, "gives cuddles to %s in a robotic way", args ? args : nick);
949 static void command_cookie(struct bio *b, const char *nick, const char *host, const char *target, char *args)
951 if (args && !strncasecmp(args, nick_self, strlen(nick_self)))
952 action(b, target, "eats the cookie offered by %s", nick);
953 else
954 action(b, target, "hands a metallic looking cookie to %s", args ? args : nick);
957 static void command_derpy(struct bio *b, const char *nick, const char *host, const char *target, char *args)
959 static const char *msgs[] = {
960 "{ SweetieBelle sad! Beep, boop.. beep. }",
962 static const char *insults[] = {
963 "accidentally shocks herself",
964 "tumbles down the stairs like a slinky",
965 "whirrrs and clicks in a screeching way",
966 "had problems executing this command",
967 "breaks down entirely",
968 "uses her magic to levitate herself off the ground, then hits it face first"
970 int n = getrand() % (elements(msgs) + elements(insults));
971 if (n < elements(msgs))
972 privmsg(b, target, "%s", msgs[n]);
973 else
974 action(b, target, "%s", insults[n - elements(msgs)]);
977 static void command_inspect(struct bio *b, const char *nick, const char *host, const char *target, char *args)
979 struct command_hash *c;
980 unsigned leave, crash;
981 char *cmd;
982 if (!args || !(cmd = token(&args, ' ')))
983 return;
985 if (strcmp(cmd, "#")) {
986 c = &commands[strhash(cmd) % elements(commands)];
987 if (!c->string || strcasecmp(c->string, cmd)) {
988 privmsg(b, target, "Command %s not valid", cmd);
989 return;
991 } else
992 c = &command_channel;
994 leave = c->left + (c->cmd == command_inspect);
995 crash = c->enter - leave;
996 if (c->enter != leave)
997 privmsg(b, target, "%s: %u successes and %u crash%s, last crashing command: %s", c->string, leave, crash, crash == 1 ? "" : "es", c->failed_command);
998 else
999 privmsg(b, target, "%s: %u time%s executed succesfully", c->string, leave, leave == 1 ? "" : "s");
1002 static void command_rebuild(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1004 int ret;
1005 char *make[] = { "/usr/bin/make", "-j4", NULL };
1006 char *git_reset[] = { "/usr/bin/git", "reset", "--hard", "master", NULL };
1007 ret = pipe_command(b, target, nick, 0, 1, git_reset);
1008 if (ret) {
1009 action(b, target, "could not rebuild");
1010 return;
1012 ret = pipe_command(b, target, nick, 0, 1, make);
1013 if (!ret)
1014 kill(getpid(), SIGUSR1);
1015 else if (ret > 0)
1016 action(b, target, "displays an ominous %i", ret);
1019 static void command_swear(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1021 static const char *insults[] = {
1022 "featherbrain",
1023 "ponyfeathers",
1024 "What in the hey?",
1025 "What are you, a dictionary?",
1026 "TAR-DY!",
1027 "[BUY SOME APPLES]",
1028 "{ Your lack of bloodlust on the battlefield is proof positive that you are a soulless automaton! }",
1029 "Your royal snootiness"
1031 privmsg(b, target, "%s: %s", args ? args : nick, insults[getrand() % elements(insults)]);
1034 static const char *perty(int64_t *t)
1036 if (*t >= 14 * 24 * 3600) {
1037 *t /= 7 * 24 * 3600;
1038 return "weeks";
1039 } else if (*t >= 48 * 3600) {
1040 *t /= 24 * 3600;
1041 return "days";
1042 } else if (*t >= 7200) {
1043 *t /= 3600;
1044 return "hours";
1045 } else if (*t >= 120) {
1046 *t /= 60;
1047 return "minutes";
1049 return *t == 1 ? "second" : "seconds";
1052 static void command_timeout(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1054 struct command_hash *c;
1055 int64_t t = get_time(b, target);
1056 int64_t howlong;
1057 if (t < 0)
1058 return;
1059 char *arg = token(&args, ' ');
1060 if (!arg || !args || !(howlong = atoi(args))) {
1061 action(b, target, "pretends to time out");;
1062 return;
1064 c = &commands[strhash(arg) % elements(commands)];
1065 if (c->string && !strcasecmp(c->string, arg)) {
1066 c->disabled_until = t + howlong;
1067 const char *str = perty(&howlong);
1068 action(b, target, "disables %s for %"PRIi64" %s", arg, howlong, str);
1069 } else
1070 action(b, target, "clicks sadly at %s for not being able to find that command", nick);
1073 static void command_mfw(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1075 char error[CURL_ERROR_SIZE], *new_url;
1076 CURL *h = curl_easy_init();
1077 struct curl_slist *headers = NULL;
1078 struct curl_download_context curl_ctx = {};
1079 headers = curl_slist_append(headers, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
1080 headers = curl_slist_append(headers, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
1081 headers = curl_slist_append(headers, "Accept-Language: en-us,en;q=0.7");
1082 headers = curl_slist_append(headers, "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
1083 headers = curl_slist_append(headers, "DNT: 1");
1084 headers = curl_slist_append(headers, "Connection: keep-alive");
1085 curl_easy_setopt(h, CURLOPT_HTTPHEADER, headers);
1086 curl_easy_setopt(h, CURLOPT_URL, "http://mylittlefacewhen.com/random/");
1087 curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, write_data);
1088 curl_easy_setopt(h, CURLOPT_WRITEDATA, &curl_ctx);
1089 curl_easy_setopt(h, CURLOPT_ERRORBUFFER, error);
1090 curl_easy_setopt(h, CURLOPT_TIMEOUT, 8);
1091 curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT, 8);
1092 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
1093 CURLcode ret = curl_easy_perform(h);
1094 if (ret == CURLE_OK && curl_easy_getinfo(h, CURLINFO_REDIRECT_URL, &new_url) == CURLE_OK)
1095 privmsg(b, target, "%s: %s", nick, new_url);
1096 curl_slist_free_all(headers);
1097 curl_easy_cleanup(h);
1098 if (ret != CURLE_OK)
1099 privmsg(b, target, "%s: You have no face", nick);
1102 static TDB_DATA get_mail_key(const char *nick)
1104 TDB_DATA d;
1105 int i;
1106 d.dsize = strlen(nick)+1;
1107 d.dptr = malloc(d.dsize);
1108 for (i = 0; i < d.dsize - 1; ++i)
1109 d.dptr[i] = tolower(nick[i]);
1110 d.dptr[i] = 0;
1111 return d;
1114 static void command_mail(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1116 char *victim, *x;
1117 size_t len;
1118 int override = 0;
1119 int64_t last_seen = 0;
1121 if (!mail_db || !seen_db)
1122 return;
1123 TDB_DATA key, val;
1124 victim = token(&args, ' ');
1125 if (victim && !strcasecmp(victim, "-seen")) {
1126 victim = token(&args, ' ');
1127 override = 1;
1129 x = victim;
1130 victim = token(&x, ',');
1131 if (!victim || x || !args || victim[0] == '#' || strchr(victim, '@') || strchr(victim, '.')) {
1132 privmsg(b, target, "%s: Usage: !mail <nick> <message>", nick);
1133 return;
1135 if (!strcasecmp(victim, nick_self)) {
1136 action(b, target, "whirrs and clicks excitedly at the mail she received from %s", nick);
1137 return;
1139 if (!strcasecmp(victim, nick)) {
1140 action(b, target, "echos the words from %s back to them: %s", nick, args);
1141 return;
1143 int64_t now = get_time(b, target);
1144 if (now < 0)
1145 return;
1147 key = get_mail_key(victim);
1148 val = tdb_fetch(seen_db, key);
1149 if (val.dptr && (x = strchr(val.dptr, ','))) {
1150 *(x++) = 0;
1151 last_seen = atoll(val.dptr);
1153 if (x && !admin(host) && (now - last_seen) < 300 && (!sstrncasecmp(x, "in ") || !sstrncasecmp(x, "joining "))) {
1154 action(b, target, "would rather not store mail for someone active so recently");
1155 goto out;
1156 } else if (last_seen && now - last_seen > 14 * 24 * 3600 && !override) {
1157 int64_t delta = now - last_seen;
1158 const char *str = perty(&delta);
1159 privmsg(b, target, "%s: \"%s\" was last seen %"PRIi64" %s ago, use !mail -seen %s <message> to override this check.", nick, victim, delta, str, victim);
1160 return;
1161 } else if (!x && !override) {
1162 privmsg(b, target, "%s: I've never seen \"%s\", use !mail -seen %s <message> to override this check.", nick, victim, victim);
1163 return;
1166 val = tdb_fetch(mail_db, key);
1167 if (!val.dptr)
1168 val.dsize = 0;
1169 else {
1170 unsigned char *cur;
1171 int letters = 0;
1172 for (cur = val.dptr; cur < val.dptr + val.dsize; cur += strlen(cur)+1)
1173 letters++;
1174 if (letters >= 6) {
1175 action(b, target, "looks sadly at %s as she cannot hold any more mail to %s", nick, victim);
1176 goto out;
1179 len = snprintf(NULL, 0, "%"PRIi64 ",%s: %s", now, nick, args) + 1;
1180 val.dptr = realloc(val.dptr, val.dsize + len);
1181 snprintf(val.dptr + val.dsize, len, "%"PRIi64 ",%s: %s", now, nick, args);
1182 val.dsize += len;
1183 if (tdb_store(mail_db, key, val, 0) < 0)
1184 privmsg(b, target, "%s: updating mail returns %s", nick, tdb_errorstr(mail_db));
1185 else
1186 action(b, target, "whirrs and clicks at %s as she stores the mail for %s", nick, victim);
1187 out:
1188 free(val.dptr);
1189 free(key.dptr);
1192 static void single_message(struct bio *b, const char *target, char *cur, int64_t now)
1194 char *endtime, *sep = strchr(cur, ':');
1195 int64_t delta = -1;
1196 if (sep && (endtime = strchr(cur, ',')) && endtime < sep) {
1197 int64_t t = atoll(cur);
1198 if (t > 0)
1199 delta = now - t;
1200 cur = endtime + 1;
1202 if (delta >= 0) {
1203 const char *str = perty(&delta);
1204 privmsg(b, target, "%"PRIi64" %s ago from %s", delta, str, cur);
1205 } else
1206 privmsg(b, target, "From %s", cur);
1209 static void command_deliver(struct bio *b, const char *nick, const char *target)
1211 TDB_DATA key, val;
1212 unsigned char *cur;
1213 static unsigned mail_enter, mail_leave;
1214 if (mail_enter++ != mail_leave)
1215 return;
1217 if (!mail_db)
1218 return;
1219 key = get_mail_key(nick);
1220 val = tdb_fetch(mail_db, key);
1221 if (!val.dptr)
1222 goto end;
1223 int64_t now = get_time(b, NULL);
1224 if (strcasecmp(key.dptr, nick_self)) {
1225 privmsg(b, target, "%s: You've got mail!", nick);
1226 for (cur = val.dptr; cur < val.dptr + val.dsize; cur += strlen(cur)+1)
1227 single_message(b, target, cur, now);
1229 free(val.dptr);
1230 tdb_delete(mail_db, key);
1231 end:
1232 free(key.dptr);
1233 mail_leave++;
1236 static void update_seen(struct bio *b, char *doingwhat, const char *nick)
1238 TDB_DATA key;
1239 key = get_mail_key(nick);
1240 TDB_DATA val = { .dptr = doingwhat, .dsize = strlen(doingwhat)+1 };
1241 if (seen_db)
1242 tdb_store(seen_db, key, val, 0);
1243 free(key.dptr);
1246 static void command_seen(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1248 char *arg = token(&args, ' '), *x;
1249 int64_t now = get_time(b, target);
1250 TDB_DATA key, val;
1251 if (now < 0)
1252 return;
1253 if (!seen_db || !arg) {
1254 privmsg(b, target, "%s: { Error... }", nick);
1255 return;
1257 if (!strcasecmp(arg, nick_self)) {
1258 action(b, target, "whirrs and clicks at %s", nick);
1259 return;
1261 if (!strcasecmp(arg, nick)) {
1262 action(b, target, "circles around %s dancing", nick);
1263 return;
1265 key = get_mail_key(arg);
1266 val = tdb_fetch(seen_db, key);
1267 if (val.dptr && (x = strchr(val.dptr, ','))) {
1268 int64_t delta;
1269 const char *str;
1270 *(x++) = 0;
1271 delta = now - atoll(val.dptr);
1272 str = perty(&delta);
1273 if (delta < 0)
1274 privmsg(b, target, "%s was last seen in the future %s", arg, x);
1275 else
1276 privmsg(b, target, "%s was last seen %"PRIi64" %s ago %s", arg, delta, str, x);
1277 } else
1278 action(b, target, "cannot find any evidence that %s exists", arg);
1279 free(val.dptr);
1282 static int suppress_message(const char *cur, int64_t now)
1284 const char *endtime, *sep = strchr(cur, ':');
1285 int64_t delta = -1;
1286 if (sep && (endtime = strchr(cur, ',')) && endtime < sep) {
1287 int64_t t = atoll(cur);
1288 if (t > 0)
1289 delta = now - t;
1292 return delta == -1 || delta >= 28 * 24 * 3600;
1295 static void command_mailbag(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1297 char buffer[256], *trunc = NULL;
1298 unsigned rem = sizeof(buffer)-1, first = 2, hidden = 0;
1299 int64_t now = get_time(b, NULL);
1300 int show_recent = 1, show_old = 0;
1301 char *show;
1303 if (args && (show = token(&args, ' '))) {
1304 if (!strcasecmp(show, "all"))
1305 show_old = 1;
1306 else if (!strcasecmp(show, "old")) {
1307 show_old = 1;
1308 show_recent = 0;
1312 if (!mail_db)
1313 return;
1314 buffer[rem] = 0;
1316 for (TDB_DATA f = tdb_firstkey(mail_db); f.dptr;) {
1317 TDB_DATA next;
1319 if (f.dsize + 4 > rem) {
1320 privmsg(b, target, "%s: Holding mail for: %s", nick, &buffer[rem]);
1321 first = 2;
1322 rem = sizeof(buffer)-1;
1323 trunc = NULL;
1324 assert(f.dsize + 4 < rem);
1326 if (f.dptr) {
1327 TDB_DATA val = tdb_fetch(mail_db, f);
1328 if (val.dptr) {
1329 int hide = now >= 0 && suppress_message(val.dptr, now);
1330 if ((!show_recent && !hide) || (!show_old && hide)) {
1331 hidden++;
1332 free(val.dptr);
1333 goto skip;
1335 free(val.dptr);
1338 if (first == 2) {
1339 first = 1;
1340 } else if (first) {
1341 rem -= 5;
1342 memcpy(&buffer[rem], " and ", 5);
1343 first = 0;
1344 trunc = &buffer[rem];
1345 } else {
1346 rem -= 2;
1347 memcpy(&buffer[rem], ", ", 2);
1349 rem -= f.dsize - 1;
1350 memcpy(&buffer[rem], f.dptr, f.dsize - 1);
1352 skip:
1353 next = tdb_nextkey(mail_db, f);
1354 free(f.dptr);
1355 f = next;
1357 if (first < 2 && !hidden)
1358 privmsg(b, target, "%s: Holding mail for: %s", nick, &buffer[rem]);
1359 else if (trunc) {
1360 *trunc = 0;
1361 trunc += 5;
1362 privmsg(b, target, "%s: Holding mail for: %s, %s and %u others", nick, &buffer[rem], trunc, hidden);
1363 } else
1364 privmsg(b, target, "%s: Holding mail for: %s and %u others", nick, &buffer[rem], hidden);
1367 static void command_mailread(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1369 TDB_DATA key, val;
1370 char *victim;
1371 if (!mail_db || !(victim = token(&args, ' ')))
1372 return;
1373 key = get_mail_key(victim);
1374 val = tdb_fetch(mail_db, key);
1375 if (!val.dptr)
1376 action(b, target, "ponyshrugs as no mail for %s was found", victim);
1377 else {
1378 unsigned char *cur;
1379 int64_t now = get_time(b, NULL);
1380 action(b, target, "peeks through %s's mail", victim);
1381 for (cur = val.dptr; cur < val.dptr + val.dsize; cur += strlen(cur)+1)
1382 single_message(b, target, cur, now);
1384 free(val.dptr);
1385 free(key.dptr);
1388 static void command_no_deliver(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1390 TDB_DATA key, val;
1391 char *cur;
1392 if (!mail_db || !(cur = token(&args, ' ')))
1393 return;
1394 key = get_mail_key(cur);
1395 val = tdb_fetch(mail_db, key);
1396 if (!val.dptr)
1397 action(b, target, "ponyshrugs as no mail for %s was found", cur);
1398 else {
1399 action(b, target, "deletes all evidence of %s's mail", cur);
1400 tdb_delete(mail_db, key);
1402 free(val.dptr);
1403 free(key.dptr);
1406 static void perform_roll(struct bio *b, const char *nick, const char *target, long sides, long dice, long bonus)
1408 long rem = dice, total = bonus;
1409 while (rem--)
1410 total += 1 + (getrand() % sides);
1411 if (bonus)
1412 action(b, target, "rolls %li %li-sided %s for a total of %li (%+li)", dice, sides, dice == 1 ? "die" : "dice", total, bonus);
1413 else
1414 action(b, target, "rolls %li %li-sided %s for a total of %li", dice, sides, dice == 1 ? "die" : "dice", total);
1417 static void command_roll(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1419 char *cur;
1420 long dice = 1, sides = 20;
1421 if ((cur = token(&args, ' '))) {
1422 char *first = cur;
1423 dice = strtol(first, &cur, 10);
1424 if (first == cur)
1425 dice = 1;
1426 if (*cur && *cur != 'd' && *cur != 'D')
1427 goto syntax;
1429 if (*cur) {
1430 char *last = cur+1;
1431 sides = strtol(last, &cur, 10);
1432 if (last == cur)
1433 goto syntax;
1436 if (dice <= 0 || dice > 25 || sides < 2 || sides > 20)
1437 goto syntax;
1438 perform_roll(b, nick, target, sides, dice, 0);
1439 return;
1441 syntax:
1442 action(b, target, "bleeps at %s in a confused manner", nick);
1445 static void add_g(struct bio *b, const char *target, int g)
1447 int ret = 0;
1448 int64_t total_g = 0;
1449 TDB_DATA chan, res;
1451 if (!chan_db || target[0] != '#')
1452 return;
1454 chan.dptr = (char*)target;
1455 chan.dsize = strlen(chan.dptr)+1;
1456 res = tdb_fetch(chan_db, chan);
1457 if (res.dptr && res.dsize >= 16) {
1458 ((int64_t*)res.dptr)[1] += g;
1459 if (res.dsize >= 24)
1460 total_g = ((int64_t*)res.dptr)[2] += g;
1461 ret = tdb_store(chan_db, chan, res, 0);
1463 free(res.dptr);
1464 if (ret < 0)
1465 fprintf(stderr, "updating database: %s", tdb_errorstr(feed_db));
1466 else if (g < total_g) {
1467 int64_t last_g = total_g - g;
1468 last_g -= last_g % 1000;
1469 if (last_g + 1000 <= total_g)
1470 privmsg(b, target, "g has been said %"PRIi64" times!", total_g);
1474 static int parse_g(const char *cur, int *g)
1476 int this_g = 0;
1477 for (const unsigned char *ptr = cur; *ptr; ptr++) {
1478 if (*ptr >= 0x80)
1479 return 0;
1480 if (*ptr == 'g' || *ptr == 'G')
1481 this_g++;
1482 else if ((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= 'A' && *ptr <= 'Z'))
1483 return 0;
1484 else if (*ptr != '1' && *ptr >= '0' && *ptr <= '9')
1485 return 0;
1487 *g += this_g;
1488 return this_g;
1491 static void channel_msg(struct bio *b, const char *nick, const char *host,
1492 const char *chan, char *msg, int64_t t)
1494 char *cur = NULL, *next;
1495 int is_action = 0;
1496 int g = 0;
1498 int that = 0, what = 0, she = 0, nsfw = 0;
1500 if (!msg || !strcmp(nick, "`Daring_Do`") ||
1501 !sstrncmp(nick, "derpy") || !sstrncmp(nick, "`derpy") ||
1502 !sstrncmp(nick, "`Luna") || !sstrncmp(nick, "`Celestia") ||
1503 !sstrncmp(nick, "GitHub") || !sstrncmp(nick, "CIA-") ||
1504 !sstrncmp(nick, "Termi") || !strcmp(nick, "r0m") || !sstrncasecmp(nick, "Owloysius"))
1505 return;
1507 if (!sstrncasecmp(msg, "\001ACTION ")) {
1508 msg += sizeof("\001ACTION ")-1;
1509 is_action = 1;
1510 asprintf(&cur, "%"PRIi64 ",in %s: * %s %s", t, chan, nick, msg);
1511 } else
1512 asprintf(&cur, "%"PRIi64 ",in %s: <%s> %s", t, chan, nick, msg);
1513 if (t > 0 && strcasecmp(chan, "#themoon") && strcasecmp(chan, "#fluttertreehouse"))
1514 update_seen(b, cur, nick);
1515 free(cur);
1516 (void)is_action;
1518 if (!sstrncasecmp(msg, "fun!") || !strcasecmp(msg, "fun")) {
1519 static int64_t last_fun = -1;
1521 if (!strcasecmp(chan, "#bronymusic") && t - last_fun > 5) {
1522 if (t < 0 || t > commands[strhash("fun") % elements(commands)].disabled_until) {
1523 if (sstrncmp(nick, "EqBot"))
1524 privmsg(b, chan, "Fun!");
1525 privmsg(b, chan, "Fun!");
1528 last_fun = t;
1529 return;
1532 next = msg;
1533 while ((cur = token(&next, ' '))) {
1534 if (!strcasecmp(cur, ">mfw") || !strcasecmp(cur, "mfw")) {
1535 if (!strcasecmp(chan, "#brony") || !ponify)
1536 continue;
1537 if (t < 0 || t > commands[strhash("mfw") % elements(commands)].disabled_until)
1538 command_mfw(b, nick, host, chan, NULL);
1539 return;
1540 } else if (!sstrncasecmp(cur, "http://") || !sstrncasecmp(cur, "https://")) {
1541 static char last_url[512];
1542 char *part;
1543 if (!strcmp(cur, last_url))
1544 return;
1545 strncpy(last_url, cur, sizeof(last_url)-1);
1547 if (!sstrncmp(nick, "EqBot") || !sstrncasecmp(chan, "#mlpsurvival"))
1548 return;
1550 if (t >= 0 && t < commands[strhash("get") % elements(commands)].disabled_until)
1551 return;
1553 else if (strcasestr(cur, "youtube.com/user") && (part = strstr(cur, "#p/"))) {
1554 char *foo;
1555 part = strrchr(part, '/') + 1;
1556 asprintf(&foo, "http://youtube.com/watch?v=%s", part);
1557 if (foo)
1558 internal_link(b, nick, host, chan, foo, 0);
1559 free(foo);
1560 return;
1561 } else if (strcasestr(cur, "twitter.com/") || strcasestr(cur, "mlfw.info") || strcasestr(cur, "mylittlefacewhen.com") || !strcasecmp(chan, "#geek"))
1562 return;
1563 if (!strcasecmp(chan, "#bronymusic") &&
1564 (nsfw || (next && strcasestr(next, "nsfw"))))
1565 continue;
1566 internal_link(b, nick, host, chan, cur, 0);
1567 return;
1569 else if (!sstrncasecmp(cur, "that") || !sstrncasecmp(cur, "dat"))
1570 that = 1;
1571 else if (that && (!sstrncasecmp(cur, "what") || !sstrncasecmp(cur, "wat")))
1572 what = 1;
1573 else if (what && !sstrncasecmp(cur, "she"))
1574 she = 1;
1575 else if (she && !sstrncasecmp(cur, "said")) {
1576 if (t <= 0 || t >= commands[strhash("shesaid") % elements(commands)].disabled_until)
1577 privmsg(b, chan, "%s: \"%s\"", nick, women_quotes[getrand() % elements(women_quotes)]);
1578 return;
1579 } else if (parse_g(cur, &g))
1580 continue;
1581 else if (strcasestr(cur, "nsfw"))
1582 nsfw = 1;
1584 if (g)
1585 add_g(b, chan, g);
1588 static void command_commands(struct bio *b, const char *nick, const char *host, const char *target, char *args);
1590 static struct command_hash unhashed[] = {
1591 { "1", command_coinflip },
1592 { "fabric", command_fabric },
1593 { "fun", command_fun },
1594 { "get", command_get },
1595 { "hug", command_hug },
1596 { "hugs", command_hugs, -1 },
1597 { "admire", command_admire },
1598 { "snug", command_snuggle, -1 },
1599 { "snugs", command_snuggles, -1 },
1600 { "snuggle", command_snuggle },
1601 { "snuggles", command_snuggles, -1 },
1602 { "cuddle", command_cuddle },
1603 { "cuddles", command_cuddles, -1 },
1604 { "cookie", command_cookie },
1605 { "mfw", command_mfw },
1606 { "swear", command_swear },
1607 { "mail", command_mail },
1608 { "m", command_mail, -1 },
1609 { "seen", command_seen },
1610 { "derpy", command_derpy },
1611 { "g", command_g, -1 },
1612 { "shesaid", command_shesaid },
1613 { "roll", command_roll },
1615 { "rebuild", command_rebuild, 1 },
1616 { "abort", command_abort, 1 },
1617 { "crash", command_crash, 1 },
1618 { "inspect", command_inspect, 1 },
1619 { "timeout", command_timeout, 1 },
1621 { "follow", command_follow },
1622 { "unfollow", command_unfollow },
1623 { "feeds", command_feeds },
1625 // DEBUG
1626 { "feed_get", command_feed_get, 1 },
1627 { "feed_set", command_feed_set, 1 },
1628 { "feed_rem", command_feed_rem, 1 },
1629 { "feed_xxx", command_feed_xxx, 1 },
1630 { "feed_counter", command_feed_counter, 1 },
1631 { "seen_xxx", command_seen_xxx, 1 },
1632 { "mailbag", command_mailbag },
1633 { "mailread", command_mailread, 1 },
1634 { "\"deliver\"", command_no_deliver, 1 },
1635 { "commands", command_commands, -1 }
1638 static void command_commands(struct bio *b, const char *nick, const char *host, const char *target, char *args)
1640 char buffer[256];
1641 unsigned rem = sizeof(buffer)-1, first = 2, i;
1642 buffer[rem] = 0;
1644 for (i = 0; i < sizeof(unhashed)/sizeof(*unhashed); ++i) {
1645 const char *str;
1646 unsigned long len;
1647 if (unhashed[i].admin)
1648 continue;
1650 str = unhashed[i].string;
1651 len = strlen(str);
1653 if (len + 5 > rem) {
1654 privmsg(b, target, "%s: Valid commands: %s", nick, &buffer[rem]);
1655 first = 2;
1656 rem = sizeof(buffer)-1;
1657 assert(len < rem);
1659 if (str) {
1660 if (first == 2) {
1661 first = 1;
1662 } else if (first) {
1663 rem -= 5;
1664 memcpy(&buffer[rem], " and ", 5);
1665 first = 0;
1666 } else {
1667 rem -= 2;
1668 memcpy(&buffer[rem], ", ", 2);
1670 rem -= len;
1671 memcpy(&buffer[rem], str, len);
1674 if (first < 2)
1675 privmsg(b, target, "%s: Valid commands: %s", nick, &buffer[rem]);
1678 static void init_hash(struct bio *b, const char *target)
1680 int i;
1681 for (i = 0; i < elements(unhashed); ++i) {
1682 unsigned h = strhash(unhashed[i].string) % elements(commands);
1683 if (commands[h].string)
1684 privmsg(b, target, "%s is a duplicate command with %s", commands[h].string, unhashed[i].string);
1685 else
1686 commands[h] = unhashed[i];
1688 #ifdef local_commands
1689 for (i = 0; i < elements(local_commands); ++i) {
1690 unsigned h = strhash(local_commands[i].string) % elements(commands);
1691 if (commands[h].string)
1692 privmsg(b, target, "%s is a duplicate command with %s", commands[h].string, local_commands[i].string);
1693 else
1694 commands[h] = local_commands[i];
1696 #endif
1699 void init_hook(struct bio *b, const char *target, const char *nick, unsigned is_ponified)
1701 char *cwd, *path = NULL;
1702 nick_self = nick;
1703 static const char *messages[] = {
1704 "feels circuits being activated that weren't before",
1705 "suddenly gets a better feel of her surroundings",
1706 "looks the same, yet there's definitely something different",
1707 "emits a beep as her lights begin to pulse slowly",
1708 "whirrrs and bleeps like never before",
1709 "bleeps a few times happily",
1710 "excitedly peeks at her surroundings"
1712 init_hash(b, target);
1713 ponify = is_ponified;
1715 cwd = getcwd(NULL, 0);
1716 asprintf(&path, "%s/db/feed.tdb", cwd);
1717 feed_db = tdb_open(path, 0, 0, O_RDWR|O_CREAT|O_CLOEXEC|O_NOCTTY, 0644);
1718 free(path);
1719 if (!feed_db)
1720 privmsg(b, target, "Opening feed db failed: %m");
1722 asprintf(&path, "%s/db/chan.tdb", cwd);
1723 chan_db = tdb_open(path, 0, 0, O_RDWR|O_CREAT|O_CLOEXEC|O_NOCTTY, 0644);
1724 free(path);
1725 if (!chan_db)
1726 privmsg(b, target, "Opening chan db failed: %m");
1728 asprintf(&path, "%s/db/mail.tdb", cwd);
1729 mail_db = tdb_open(path, 0, 0, O_RDWR|O_CREAT|O_CLOEXEC|O_NOCTTY, 0644);
1730 free(path);
1731 if (!mail_db)
1732 privmsg(b, target, "Opening mail db failed: %m");
1734 asprintf(&path, "%s/db/seen.tdb", cwd);
1735 seen_db = tdb_open(path, 0, 0, O_RDWR|O_CREAT|O_CLOEXEC|O_NOCTTY, 0644);
1736 free(path);
1737 if (!seen_db)
1738 privmsg(b, target, "Opening seen db failed: %m");
1740 free(cwd);
1741 action(b, target, "%s", messages[getrand() % elements(messages)]);
1744 static void __attribute__((destructor)) shutdown_hook(void)
1746 if (seen_db)
1747 tdb_close(seen_db);
1748 if (mail_db)
1749 tdb_close(mail_db);
1750 if (feed_db)
1751 tdb_close(feed_db);
1752 if (chan_db)
1753 tdb_close(chan_db);
1756 static char *nom_special(char *line)
1758 while (*line) {
1759 if (*line == 0x03) {
1760 line++;
1761 if (*line >= '0' && *line <= '9')
1762 line++;
1763 else continue;
1764 if (*line >= '0' && *line <= '9')
1765 line++;
1766 if (line[0] == ',' && line[1] >= '0' && line[1] <= '9')
1767 line += 2;
1768 else continue;
1769 if (*line >= '0' && *line <= '9')
1770 line++;
1771 } else if (*line >= 0x02 && *line <= 0x1f)
1772 /* some IRC control code that's not CTCP */
1773 line++;
1774 else
1775 return line;
1777 return line;
1780 static char *cleanup_special(char *line)
1782 char *cur, *start = nom_special(line);
1783 if (!*start)
1784 return NULL;
1785 for (line = cur = start; *line; line = nom_special(line))
1786 *(cur++) = *(line++);
1788 for (cur--; cur >= start; --cur)
1789 if (*cur != ' ' && *cur != '\001')
1790 break;
1792 if (cur < start)
1793 return NULL;
1794 cur[1] = 0;
1795 return start;
1798 static void rss_check(struct bio *b, const char *channel, int64_t t)
1800 static unsigned rss_enter, rss_leave;
1801 if (t >= 0 && rss_enter == rss_leave) {
1802 rss_enter++;
1803 channel_feed_check(b, channel, t);
1804 rss_leave++;
1808 static const char *privileged_command[] = {
1809 "{ Rarity, I love you so much! }",
1810 "{ Rarity, have I ever told you that I love you? }",
1811 "{ Yes, I love my sister, Rarity. }",
1812 "{ Raaaaaaaaaaaaaarity. }",
1813 "{ You do not fool me, Rari...bot! }"
1816 static int cmd_check(struct bio *b, struct command_hash *c, int is_admin, int64_t t, const char *prefix, const char *target)
1818 if (c->left != c->enter)
1819 privmsg(b, target, "Command %s is disabled because of a crash", c->string);
1820 else if (t > 0 && t < c->disabled_until && !is_admin) {
1821 int64_t delta = c->disabled_until - t;
1822 const char *str = perty(&delta);
1823 b->writeline(b, "NOTICE %s :Command %s is on timeout for the next %"PRIi64 " %s", prefix, c->string, delta, str);
1824 } else if (c->admin > 0 && !is_admin)
1825 privmsg(b, target, "%s: %s", prefix, privileged_command[getrand() % elements(privileged_command)]);
1826 else
1827 return 1;
1828 return 0;
1831 void privmsg_hook(struct bio *b, const char *prefix, const char *ident, const char *host,
1832 const char *const *args, unsigned nargs)
1834 char *cmd_args, *cmd;
1835 const char *target = args[0][0] == '#' ? args[0] : prefix;
1836 unsigned chan, nick_len;
1837 struct command_hash *c;
1838 int64_t t = get_time(b, target);
1839 int is_admin = admin(host);
1841 chan = args[0][0] == '#';
1842 if (chan) {
1843 rss_check(b, args[0], t);
1844 command_deliver(b, prefix, args[0]);
1846 cmd_args = cleanup_special((char*)args[1]);
1847 if (!cmd_args)
1848 return;
1850 if (ident && (!strcasecmp(ident, "Revolver") || !strcasecmp(ident, "Rev")))
1851 return;
1853 if (chan && cmd_args[0] == '!') {
1854 cmd_args++;
1855 } else if (chan && (nick_len = strlen(nick_self)) &&
1856 !strncasecmp(cmd_args, nick_self, nick_len) &&
1857 (cmd_args[nick_len] == ':' || cmd_args[nick_len] == ',') && cmd_args[nick_len+1] == ' ') {
1858 cmd_args += nick_len + 2;
1859 if (!cmd_args[0])
1860 return;
1861 chan = 2;
1862 } else if (chan) {
1863 if (command_channel.enter == command_channel.left) {
1864 command_channel.enter++;
1865 snprintf(command_channel.failed_command,
1866 sizeof(command_channel) - offsetof(struct command_hash, failed_command) - 1,
1867 "%s:%s (%s@%s) \"#\" %s", target, prefix, ident, host, cmd_args);
1868 channel_msg(b, prefix, host, args[0], cmd_args, t);
1869 command_channel.left++;
1871 return;
1873 cmd = token(&cmd_args, ' ');
1874 if (!chan && cmd_args && cmd_args[0] == '#') {
1875 if (!is_admin) {
1876 privmsg(b, target, "%s: %s", prefix, privileged_command[getrand() % elements(privileged_command)]);
1877 return;
1879 target = token(&cmd_args, ' ');
1882 c = &commands[strhash(cmd) % elements(commands)];
1883 if (c->string && !strcasecmp(c->string, cmd)) {
1884 if (!sstrncasecmp(prefix, "EqBot")) {
1885 action(b, target, "adoringly pulses some light back to %s", prefix);
1886 return;
1888 if (cmd_check(b, c, is_admin, t, prefix, target)) {
1889 c->enter++;
1890 snprintf(c->failed_command, sizeof(*c) - offsetof(struct command_hash, failed_command) - 1,
1891 "%s:%s (%s@%s) \"%s\" %s", target, prefix, ident, host, cmd, cmd_args);
1892 c->cmd(b, prefix, host, target, cmd_args);
1893 c->left++;
1895 return;
1896 } else if (cmd[0] == 'd' && cmd[1] >= '0' && cmd[1] <= '9') {
1897 char *end;
1898 long base;
1899 base = strtol(cmd+1, &end, 10);
1900 if (base > 1 && base <= 20 && !*end) {
1901 long dice = 1, bonus = 0;
1902 char *strdice;
1903 c = &commands[strhash("roll") % elements(commands)];
1904 if (!cmd_check(b, c, is_admin, t, prefix, target))
1905 return;
1906 c->enter++;
1907 snprintf(c->failed_command, sizeof(*c) - offsetof(struct command_hash, failed_command) - 1,
1908 "%s:%s (%s@%s) \"%s\" %s", target, prefix, ident, host, cmd, cmd_args);
1910 strdice = token(&cmd_args, ' ');
1911 if (strdice) {
1912 dice = strtol(strdice, &end, 10);
1913 if (*end || !dice || dice > 25)
1914 goto syntax;
1915 strdice = token(&cmd_args, ' ');
1916 if (strdice) {
1917 bonus = strtol(strdice, &end, 10);
1918 if (*end || bonus > 1000 || bonus < -1000)
1919 goto syntax;
1922 perform_roll(b, prefix, target, base, dice, bonus);
1923 c->left++;
1924 return;
1926 syntax:
1927 action(b, target, "bleeps at %s in a confused manner", prefix);
1928 c->left++;
1929 return;
1931 } else if (cmd[0] >= '0' && cmd[0] <= '9') {
1932 long dice, sides, bonus = 0;
1933 char *end;
1934 dice = strtol(cmd, &end, 10);
1935 if (dice >= 1 && dice <= 25 && *end == 'd' &&
1936 (sides = strtol(end+1, &end, 10)) &&
1937 sides > 1 && sides <= 20 && !*end) {
1938 char *strbonus;
1940 c = &commands[strhash("roll") % elements(commands)];
1941 if (!cmd_check(b, c, is_admin, t, prefix, target))
1942 return;
1943 c->enter++;
1944 snprintf(c->failed_command, sizeof(*c) - offsetof(struct command_hash, failed_command) - 1,
1945 "%s:%s (%s@%s) \"%s\" %s", target, prefix, ident, host, cmd, cmd_args);
1947 strbonus = token(&cmd_args, ' ');
1948 if (strbonus) {
1949 bonus = strtol(strbonus, &end, 10);
1950 if (*end || bonus > 1000 || bonus < -1000)
1951 goto syntax;
1954 perform_roll(b, prefix, target, sides, dice, bonus);
1955 c->left++;
1956 return;
1960 if (chan == 2 && t > 0) {
1961 static int64_t last_t;
1962 if (t - last_t > 3)
1963 privmsg(b, target, "%s: { I love you! }", prefix);
1964 last_t = t;
1968 void command_hook(struct bio *b, const char *prefix, const char *ident, const char *host,
1969 const char *command, const char *const *args, unsigned nargs)
1971 char *buf = NULL;
1972 int64_t t = -1;
1973 if (!strcasecmp(command, "NOTICE")) {
1974 if (nargs < 2 || args[0][0] == '#')
1975 return;
1976 if (admin(host))
1977 b->writeline(b, "%s", args[1]);
1978 else
1979 fprintf(stderr, "%s: %s\n", prefix, args[1]);
1980 } else if (!strcasecmp(command, "JOIN")) {
1981 t = get_time(b, args[0]);
1982 rss_check(b, args[0], t);
1983 command_deliver(b, prefix, args[0]);
1984 asprintf(&buf, "%"PRIi64",joining %s", t, args[0]);
1985 } else if (!strcasecmp(command, "PART")) {
1986 t = get_time(b, args[0]);
1987 rss_check(b, args[0], t);
1988 asprintf(&buf, "%"PRIi64",leaving %s", t, args[0]);
1989 } else if (!strcasecmp(command, "QUIT")) {
1990 t = get_time(b, NULL);
1991 asprintf(&buf, "%"PRIi64",quitting with the message \"%s\"", t, args[0]);
1992 } else if (!strcasecmp(command, "NICK")) {
1993 t = get_time(b, NULL);
1994 if (t >= 0) {
1995 asprintf(&buf, "%"PRIi64",changing nick from %s", t, prefix);
1996 if (buf)
1997 update_seen(b, buf, args[0]);
1998 free(buf);
1999 buf = NULL;
2001 asprintf(&buf, "%"PRIi64",changing nick to %s", t, args[0]);
2002 } else if (0) {
2003 int i;
2004 fprintf(stderr, ":%s!%s%s %s", prefix, ident, host, command);
2005 for (i = 0; i < nargs; ++i)
2006 fprintf(stderr, " %s", args[i]);
2007 fprintf(stderr, "\n");
2009 if (t >= 0 && buf)
2010 update_seen(b, buf, prefix);
2011 free(buf);
2012 return;