3 #include <libxml/xmlstring.h>
4 #include <libxml/HTMLparser.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))
29 static int pipe_command(struct bio
*b
, const char *target
, const char *nick
, int redirect_stdout
, int redirect_stderr
, char *argv
[])
33 if (pipe2(fd
, O_CLOEXEC
) < 0) {
34 privmsg(b
, target
, "Could not create pipe: %m");
39 privmsg(b
, target
, "Could not fork: %m");
44 int fdnull
= open("/dev/null", O_WRONLY
|O_CLOEXEC
);
46 if (dup3(redirect_stdout
? fd
[1] : fdnull
, 1, 0) < 0)
48 if (dup3(redirect_stderr
? fd
[1] : fdnull
, 2, 0) < 0)
50 exit(execv(argv
[0], argv
));
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) {
66 if (lines
< SPAM_CUTOFF
)
67 privmsg(b
, nick
, "%s", buffer
);
69 fprintf(stderr
, "%s\n", buffer
);
76 if (lines
< SPAM_CUTOFF
)
77 privmsg(b
, nick
, "%s", buffer
);
79 fprintf(stderr
, "%s\n", buffer
);
81 if (lines
>= SPAM_CUTOFF
)
82 privmsg(b
, nick
, "%i lines suppressed", lines
- SPAM_CUTOFF
+ 1);
87 privmsg(b
, target
, "error on waitpid: %m");
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
)
105 static void command_crash(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
110 static void command_coinflip(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
113 action(b
, target
, "whirrrs and clicks excitedly at %s", nick
);
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
)
128 while (*title
!= '\n' && *title
!= '\r' && *title
!= '\t' && *title
!= ' ' && *title
) {
129 *(start
++) = *(title
++);
134 while (*title
== '\n' || *title
== '\r' || *title
== '\t' || *title
== ' ')
140 struct curl_download_context
146 static size_t write_data(void *ptr
, size_t size
, size_t nmemb
, void *member
) {
147 struct curl_download_context
*ctx
= member
;
149 ctx
->data
= realloc(ctx
->data
, ctx
->len
+ size
);
150 memcpy(ctx
->data
+ ctx
->len
, ptr
, size
);
155 static const char *get_text(xmlNode
*cur
)
157 for (; cur
; cur
= cur
->next
) {
158 if (cur
->type
== XML_TEXT_NODE
)
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
);
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
;
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
)
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
))
199 *title
= "<no title>";
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
;
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
)
214 else if (!strcasecmp(name
, "category"))
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
);
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
);
247 privmsg(b
, target
, "adding blog %s \"%s\": %s", main_link
, main_title
, main_subtitle
);
249 privmsg(b
, target
, "adding blog %s \"%s\"", main_link
, main_title
);
254 privmsg(b
, target
, "Most recent entry: %s %s", link
, t
);
257 privmsg(b
, target
, "Currently having no entries for this feed that matches the filter");
260 if (!strcmp(last_link
, link
) || !match
|| !entry
)
263 for (entry
= entry
->next
; entry
; entry
= entry
->next
) {
264 const char *cur_link
, *cur_title
;
265 if (strcasecmp(entry
->name
, "entry"))
267 match
= get_feed_entry(b
, nick
, target
, category
, entry
, &cur_title
, &cur_link
);
268 if (match
< 0 || !strcmp(last_link
, cur_link
))
271 prev_link
= cur_link
;
272 prev_title
= cur_title
;
277 t
= strdup(prev_title
);
279 privmsg(b
, target
, "( %s ): %s", prev_link
, t
);
281 } else if (updates
> 1)
282 privmsg(b
, target
, "( %s ): %u updates, linking most recent", main_link
, 1+updates
);
285 privmsg(b
, target
, "( %s ): %s", link
, t
);
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
;
294 xmlNode
*cur
, *entry
= NULL
;
295 ver
= get_link(root
->properties
, "version");
296 if (!ver
|| strcmp(ver
, "2.0")) {
298 privmsg(b
, target
, "%s: Could not parse rss feed", nick
);
300 privmsg(b
, target
, "%s: Invalid rss version \"%s\"", nick
, ver
);
303 for (cur
= root
->children
; cur
&& cur
->type
!= XML_ELEMENT_NODE
; cur
= cur
->next
);
306 for (cur
= cur
->children
; cur
; cur
= cur
->next
) {
307 const char *name
= cur
->name
;
308 if (cur
->type
!= XML_ELEMENT_NODE
)
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
);
325 for (cur
= entry
->children
; cur
; cur
= cur
->next
) {
326 const char *name
= cur
->name
;
327 if (cur
->type
!= XML_ELEMENT_NODE
)
329 if (!strcasecmp(name
, "title"))
330 title
= get_text(cur
->children
);
331 else if (!strcasecmp(name
, "link") && !link
)
332 link
= get_text(cur
->children
);
335 title
= "<no title>";
337 privmsg(b
, target
, "%s: Failed to parse entry: %s %s", nick
, link
, title
);
342 privmsg(b
, target
, "adding blog %s \"%s\"", main_link
, main_title
);
344 privmsg(b
, target
, "Most recent entry: %s %s", link
, t
);
346 } else if (strcmp(last_link
, link
)) {
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"))
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
)
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
);
365 cur_title
= "<no title>";
366 if (!cur_link
|| !strcmp(last_link
, cur_link
))
371 t
= strdup(prev_title
);
373 privmsg(b
, target
, "( %s ): %s", prev_link
, t
);
375 } else if (updates
> 1)
376 privmsg(b
, target
, "( %s ): %u updates, linking most recent", main_link
, 1+updates
);
379 privmsg(b
, target
, "( %s ): %s", link
, t
);
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
;
392 return cur_node
->next
;
393 cur_node
= cur_node
->parent
;
398 static const char *get_atom_link(xmlNode
*cur
)
400 for (; cur
; cur
= next_link(cur
)) {
401 if (cur
->type
!= XML_ELEMENT_NODE
)
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");
412 static const char *get_rss_link(xmlNode
*cur
)
414 for (; cur
; cur
= next_link(cur
)) {
415 if (cur
->type
!= XML_ELEMENT_NODE
)
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");
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
);
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
);
436 privmsg(b
, target
, "%s: not a valid feed link, no suggestion found", nick
);
440 static size_t get_time_from_header(void *data
, size_t size
, size_t size2
, void *ptr
)
444 if (sstrncmp(data
, "Last-Modified: "))
446 data
+= sizeof("Last-Modified: ")-1;
447 *(char**)ptr
= d
= strdup(data
);
448 if ((e
= strchr(d
, '\r')))
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
, '#');
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: */*");
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);
482 asprintf(&tmp
, "If-Modified-Since: %s", last_modified
);
483 headers
= curl_slist_append(headers
, tmp
);
487 int success
= curl_easy_perform(h
);
488 curl_slist_free_all(headers
);
489 if (success
== CURLE_OK
) {
492 curl_easy_getinfo(h
, CURLINFO_CONTENT_TYPE
, &mime
);
493 curl_easy_getinfo(h
, CURLINFO_RESPONSE_CODE
, &code
);
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
);
512 privmsg(b
, target
, "Unknown feed type \"%s\"", root
->name
);
518 if (ret_link
&& (!link
|| strcmp(ret_link
, link
))) {
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
));
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
);
538 privmsg(b
, target
, "unhandled content type %s", mime
);
540 privmsg(b
, target
, "Error %s (%u)", error
, success
);
543 curl_easy_cleanup(h
);
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
;
554 if (target
[0] != '#') {
555 privmsg(b
, target
, "%s: Can only follow on channels", nick
);
558 if (!args
|| !*args
) {
559 privmsg(b
, target
, "%s: Usage: !follow <url>", nick
);
563 if (((space
= strchr(args
, ' ')) && space
< strchr(args
, '#')) ||
564 (sstrncmp(args
, "http://") && sstrncmp(args
, "https://"))) {
565 privmsg(b
, target
, "%s: Invalid url", nick
);
569 key
.dsize
= asprintf((char**)&key
.dptr
, "%s,%s", target
, args
)+1;
570 val
= tdb_fetch(feed_db
, key
);
572 last_link
= strchr(val
.dptr
, '\001');
573 ret
= check_single_feed(b
, target
, key
, NULL
, args
, last_link
? last_link
+1 : NULL
, nick
);
576 privmsg(b
, target
, "%s: Not updated", nick
);
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 };
586 if (!feed_db
|| !chan_db
)
588 chan
.dptr
= (char*)target
;
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)
596 save
[1] = ((uint64_t*)res
.dptr
)[1];
598 save
[2] = ((uint64_t*)res
.dptr
)[2];
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
;
606 privmsg(b
, target
, "updating database: %s", tdb_errorstr(feed_db
));
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;
617 if ((sep
= strchr(f
.dptr
, '\001'))) {
619 check_single_feed(b
, target
, d
, f
.dptr
, url
, sep
, target
);
628 static void command_unfollow(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
636 if (!(url
= token(&args
, ' ')) || (sstrncmp(url
, "http://") && sstrncmp(url
, "https://"))) {
637 privmsg(b
, target
, "%s: Invalid url", nick
);
640 if (target
[0] != '#') {
641 privmsg(b
, target
, "%s: Can only unfollow on channels", nick
);
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
);
649 privmsg(b
, target
, "%s: Could not delete: %s", nick
, tdb_errorstr(feed_db
));
651 privmsg(b
, target
, "%s: No longer following %s", nick
, url
);
655 static void command_g(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
658 int64_t g
= 0, g_total
= 0;
661 if (!chan_db
|| target
[0] != '#')
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;
671 g_total
= ((int64_t*)res
.dptr
)[2];
672 ret
= tdb_store(chan_db
, chan
, res
, 0);
676 fprintf(stderr
, "updating database: %s", tdb_errorstr(feed_db
));
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;
686 if (target
[0] != '#') {
687 privmsg(b
, target
, "%s: Only useful in channels..", nick
);
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);
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
)
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
);
717 if (strlen(d
.dptr
)+1 < d
.dsize
) {
718 privmsg(b
, target
, "%s: removed buggy entry", nick
);
719 tdb_delete(feed_db
, d
);
726 static void command_feed_set(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
731 key
.dptr
= token(&args
, ' ');
732 char *value
= token(&args
, ' ');
733 if (!key
.dptr
|| !value
)
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
));
743 privmsg(b
, target
, "%s: burp", nick
);
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
)
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
));
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
)
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
)
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
)
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
)
789 return strdup(cur
->content
);
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
)
800 return get_text_appended(cur_node
->children
);
806 static void internal_link(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
, unsigned verbose
)
809 struct curl_slist
*headers
= NULL
;
810 char error
[CURL_ERROR_SIZE
];
811 int success
, sent
= verbose
;
812 struct curl_download_context curl_ctx
= {};
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
);
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
);
850 decode_html_entities_utf8(title
, NULL
);
851 if ((nuke
= strstr(title
, " on SoundCloud - Create")))
854 privmsg(b
, target
, "%s linked %s", nick
, title
);
859 if (verbose
&& !title
)
860 privmsg(b
, target
, "%s linked %s page with invalid title", nick
, mime
);
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
);
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;
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://")))
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
);
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
);
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
);
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
);
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 *insults
[] = {
960 "accidentally shocks herself",
961 "tumbles down the stairs like a slinky",
962 "whirrrs and clicks in a screeching way",
963 "had problems executing this command",
964 "breaks down entirely",
965 "uses her magic to levitate herself off the ground, then hits it face first"
967 action(b
, target
, "%s", insults
[getrand() % elements(insults
)]);
970 static void command_inspect(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
972 struct command_hash
*c
;
973 unsigned leave
, crash
;
975 if (!args
|| !(cmd
= token(&args
, ' ')))
978 if (strcmp(cmd
, "#")) {
979 c
= &commands
[strhash(cmd
) % elements(commands
)];
980 if (!c
->string
|| strcasecmp(c
->string
, cmd
)) {
981 privmsg(b
, target
, "Command %s not valid", cmd
);
985 c
= &command_channel
;
987 leave
= c
->left
+ (c
->cmd
== command_inspect
);
988 crash
= c
->enter
- leave
;
989 if (c
->enter
!= leave
)
990 privmsg(b
, target
, "%s: %u successes and %u crash%s, last crashing command: %s", c
->string
, leave
, crash
, crash
== 1 ? "" : "es", c
->failed_command
);
992 privmsg(b
, target
, "%s: %u time%s executed succesfully", c
->string
, leave
, leave
== 1 ? "" : "s");
995 static void command_rebuild(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
998 char *make
[] = { "/usr/bin/make", "-j4", NULL
};
999 char *git_reset
[] = { "/usr/bin/git", "reset", "--hard", "master", NULL
};
1000 ret
= pipe_command(b
, target
, nick
, 0, 1, git_reset
);
1002 action(b
, target
, "could not rebuild");
1005 ret
= pipe_command(b
, target
, nick
, 0, 1, make
);
1007 kill(getpid(), SIGUSR1
);
1009 action(b
, target
, "displays an ominous %i", ret
);
1012 static void command_swear(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1014 static const char *insults
[] = {
1018 "What are you, a dictionary?",
1020 "[BUY SOME APPLES]",
1021 "{ Your lack of bloodlust on the battlefield is proof positive that you are a soulless automaton! }",
1022 "Your royal snootiness"
1024 privmsg(b
, target
, "%s: %s", args
? args
: nick
, insults
[getrand() % elements(insults
)]);
1027 static const char *perty(int64_t *t
)
1029 if (*t
>= 14 * 24 * 3600) {
1030 *t
/= 7 * 24 * 3600;
1032 } else if (*t
>= 48 * 3600) {
1035 } else if (*t
>= 7200) {
1038 } else if (*t
>= 120) {
1042 return *t
== 1 ? "second" : "seconds";
1045 static void command_timeout(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1047 struct command_hash
*c
;
1048 int64_t t
= get_time(b
, target
);
1052 char *arg
= token(&args
, ' ');
1053 if (!arg
|| !args
|| !(howlong
= atoi(args
))) {
1054 action(b
, target
, "pretends to time out");;
1057 c
= &commands
[strhash(arg
) % elements(commands
)];
1058 if (c
->string
&& !strcasecmp(c
->string
, arg
)) {
1059 c
->disabled_until
= t
+ howlong
;
1060 const char *str
= perty(&howlong
);
1061 action(b
, target
, "disables %s for %"PRIi64
" %s", arg
, howlong
, str
);
1063 action(b
, target
, "clicks sadly at %s for not being able to find that command", nick
);
1066 static void command_mfw(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1068 char error
[CURL_ERROR_SIZE
], *new_url
;
1069 CURL
*h
= curl_easy_init();
1070 struct curl_slist
*headers
= NULL
;
1071 struct curl_download_context curl_ctx
= {};
1072 headers
= curl_slist_append(headers
, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
1073 headers
= curl_slist_append(headers
, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
1074 headers
= curl_slist_append(headers
, "Accept-Language: en-us,en;q=0.7");
1075 headers
= curl_slist_append(headers
, "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
1076 headers
= curl_slist_append(headers
, "DNT: 1");
1077 headers
= curl_slist_append(headers
, "Connection: keep-alive");
1078 curl_easy_setopt(h
, CURLOPT_HTTPHEADER
, headers
);
1079 curl_easy_setopt(h
, CURLOPT_URL
, "http://mylittlefacewhen.com/random/");
1080 curl_easy_setopt(h
, CURLOPT_WRITEFUNCTION
, write_data
);
1081 curl_easy_setopt(h
, CURLOPT_WRITEDATA
, &curl_ctx
);
1082 curl_easy_setopt(h
, CURLOPT_ERRORBUFFER
, error
);
1083 curl_easy_setopt(h
, CURLOPT_TIMEOUT
, 8);
1084 curl_easy_setopt(h
, CURLOPT_CONNECTTIMEOUT
, 8);
1085 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
1086 CURLcode ret
= curl_easy_perform(h
);
1087 if (ret
== CURLE_OK
&& curl_easy_getinfo(h
, CURLINFO_REDIRECT_URL
, &new_url
) == CURLE_OK
)
1088 privmsg(b
, target
, "%s: %s", nick
, new_url
);
1089 curl_slist_free_all(headers
);
1090 curl_easy_cleanup(h
);
1091 if (ret
!= CURLE_OK
)
1092 privmsg(b
, target
, "%s: You have no face", nick
);
1095 static TDB_DATA
get_mail_key(const char *nick
)
1099 d
.dsize
= strlen(nick
)+1;
1100 d
.dptr
= malloc(d
.dsize
);
1101 for (i
= 0; i
< d
.dsize
- 1; ++i
)
1102 d
.dptr
[i
] = tolower(nick
[i
]);
1107 static void command_mail(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1112 int64_t last_seen
= 0;
1114 if (!mail_db
|| !seen_db
)
1117 victim
= token(&args
, ' ');
1118 if (victim
&& !strcasecmp(victim
, "-seen")) {
1119 victim
= token(&args
, ' ');
1123 victim
= token(&x
, ',');
1124 if (!victim
|| x
|| !args
|| victim
[0] == '#' || strchr(victim
, '@') || strchr(victim
, '.')) {
1125 privmsg(b
, target
, "%s: Usage: !mail <nick> <message>", nick
);
1128 if (!strcasecmp(victim
, nick_self
)) {
1129 action(b
, target
, "whirrs and clicks excitedly at the mail she received from %s", nick
);
1132 if (!strcasecmp(victim
, nick
)) {
1133 action(b
, target
, "echos the words from %s back to them: %s", nick
, args
);
1136 int64_t now
= get_time(b
, target
);
1140 key
= get_mail_key(victim
);
1141 val
= tdb_fetch(seen_db
, key
);
1142 if (val
.dptr
&& (x
= strchr(val
.dptr
, ','))) {
1144 last_seen
= atoll(val
.dptr
);
1146 if (x
&& !admin(host
) && (now
- last_seen
) < 300 && (!sstrncasecmp(x
, "in ") || !sstrncasecmp(x
, "joining "))) {
1147 action(b
, target
, "would rather not store mail for someone active so recently");
1149 } else if (last_seen
&& now
- last_seen
> 14 * 24 * 3600 && !override
) {
1150 int64_t delta
= now
- last_seen
;
1151 const char *str
= perty(&delta
);
1152 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
);
1154 } else if (!x
&& !override
) {
1155 privmsg(b
, target
, "%s: I've never seen \"%s\", use !mail -seen %s <message> to override this check.", nick
, victim
, victim
);
1159 val
= tdb_fetch(mail_db
, key
);
1165 for (cur
= val
.dptr
; cur
< val
.dptr
+ val
.dsize
; cur
+= strlen(cur
)+1)
1168 action(b
, target
, "looks sadly at %s as she cannot hold any more mail to %s", nick
, victim
);
1172 len
= snprintf(NULL
, 0, "%"PRIi64
",%s: %s", now
, nick
, args
) + 1;
1173 val
.dptr
= realloc(val
.dptr
, val
.dsize
+ len
);
1174 snprintf(val
.dptr
+ val
.dsize
, len
, "%"PRIi64
",%s: %s", now
, nick
, args
);
1176 if (tdb_store(mail_db
, key
, val
, 0) < 0)
1177 privmsg(b
, target
, "%s: updating mail returns %s", nick
, tdb_errorstr(mail_db
));
1179 action(b
, target
, "whirrs and clicks at %s as she stores the mail for %s", nick
, victim
);
1185 static void single_message(struct bio
*b
, const char *target
, char *cur
, int64_t now
)
1187 char *endtime
, *sep
= strchr(cur
, ':');
1189 if (sep
&& (endtime
= strchr(cur
, ',')) && endtime
< sep
) {
1190 int64_t t
= atoll(cur
);
1196 const char *str
= perty(&delta
);
1197 privmsg(b
, target
, "%"PRIi64
" %s ago from %s", delta
, str
, cur
);
1199 privmsg(b
, target
, "From %s", cur
);
1202 static void command_deliver(struct bio
*b
, const char *nick
, const char *target
)
1206 static unsigned mail_enter
, mail_leave
;
1207 if (mail_enter
++ != mail_leave
)
1212 key
= get_mail_key(nick
);
1213 val
= tdb_fetch(mail_db
, key
);
1216 int64_t now
= get_time(b
, NULL
);
1217 if (strcasecmp(key
.dptr
, nick_self
)) {
1218 privmsg(b
, target
, "%s: You've got mail!", nick
);
1219 for (cur
= val
.dptr
; cur
< val
.dptr
+ val
.dsize
; cur
+= strlen(cur
)+1)
1220 single_message(b
, target
, cur
, now
);
1223 tdb_delete(mail_db
, key
);
1229 static void update_seen(struct bio
*b
, char *doingwhat
, const char *nick
)
1232 key
= get_mail_key(nick
);
1233 TDB_DATA val
= { .dptr
= doingwhat
, .dsize
= strlen(doingwhat
)+1 };
1235 tdb_store(seen_db
, key
, val
, 0);
1239 static void command_seen(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1241 char *arg
= token(&args
, ' '), *x
;
1242 int64_t now
= get_time(b
, target
);
1246 if (!seen_db
|| !arg
) {
1247 privmsg(b
, target
, "%s: { Error... }", nick
);
1250 if (!strcasecmp(arg
, nick_self
)) {
1251 action(b
, target
, "whirrs and clicks at %s", nick
);
1254 if (!strcasecmp(arg
, nick
)) {
1255 action(b
, target
, "circles around %s dancing", nick
);
1258 key
= get_mail_key(arg
);
1259 val
= tdb_fetch(seen_db
, key
);
1260 if (val
.dptr
&& (x
= strchr(val
.dptr
, ','))) {
1264 delta
= now
- atoll(val
.dptr
);
1265 str
= perty(&delta
);
1267 privmsg(b
, target
, "%s was last seen in the future %s", arg
, x
);
1269 privmsg(b
, target
, "%s was last seen %"PRIi64
" %s ago %s", arg
, delta
, str
, x
);
1271 action(b
, target
, "cannot find any evidence that %s exists", arg
);
1275 static int suppress_message(const char *cur
, int64_t now
)
1277 const char *endtime
, *sep
= strchr(cur
, ':');
1279 if (sep
&& (endtime
= strchr(cur
, ',')) && endtime
< sep
) {
1280 int64_t t
= atoll(cur
);
1285 return delta
== -1 || delta
>= 28 * 24 * 3600;
1288 static void command_mailbag(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1290 char buffer
[256], *trunc
= NULL
;
1291 unsigned rem
= sizeof(buffer
)-1, first
= 2, hidden
= 0;
1292 int64_t now
= get_time(b
, NULL
);
1295 if (args
&& (show
= token(&args
, ' '))) {
1296 if (strcasecmp(show
, "all"))
1304 for (TDB_DATA f
= tdb_firstkey(mail_db
); f
.dptr
;) {
1307 if (f
.dsize
+ 4 > rem
) {
1308 privmsg(b
, target
, "%s: Holding mail for: %s", nick
, &buffer
[rem
]);
1310 rem
= sizeof(buffer
)-1;
1312 assert(f
.dsize
+ 4 < rem
);
1315 TDB_DATA val
= tdb_fetch(mail_db
, f
);
1317 if (now
>= 0 && !show
&& suppress_message(val
.dptr
, now
)) {
1329 memcpy(&buffer
[rem
], " and ", 5);
1331 trunc
= &buffer
[rem
];
1334 memcpy(&buffer
[rem
], ", ", 2);
1337 memcpy(&buffer
[rem
], f
.dptr
, f
.dsize
- 1);
1340 next
= tdb_nextkey(mail_db
, f
);
1344 if (first
< 2 && !hidden
)
1345 privmsg(b
, target
, "%s: Holding mail for: %s", nick
, &buffer
[rem
]);
1349 privmsg(b
, target
, "%s: Holding mail for: %s, %s and %u others", nick
, &buffer
[rem
], trunc
, hidden
);
1351 privmsg(b
, target
, "%s: Holding mail for: %s and %u others", nick
, &buffer
[rem
], hidden
);
1354 static void command_mailread(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1358 if (!mail_db
|| !(victim
= token(&args
, ' ')))
1360 key
= get_mail_key(victim
);
1361 val
= tdb_fetch(mail_db
, key
);
1363 action(b
, target
, "ponyshrugs as no mail for %s was found", victim
);
1366 int64_t now
= get_time(b
, NULL
);
1367 action(b
, target
, "peeks through %s's mail", victim
);
1368 for (cur
= val
.dptr
; cur
< val
.dptr
+ val
.dsize
; cur
+= strlen(cur
)+1)
1369 single_message(b
, target
, cur
, now
);
1375 static void command_no_deliver(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1379 if (!mail_db
|| !(cur
= token(&args
, ' ')))
1381 key
= get_mail_key(cur
);
1382 val
= tdb_fetch(mail_db
, key
);
1384 action(b
, target
, "ponyshrugs as no mail for %s was found", cur
);
1386 action(b
, target
, "deletes all evidence of %s's mail", cur
);
1387 tdb_delete(mail_db
, key
);
1393 static void perform_roll(struct bio
*b
, const char *nick
, const char *target
, long sides
, long dice
, long bonus
)
1395 long rem
= dice
, total
= bonus
;
1397 total
+= 1 + (getrand() % sides
);
1399 action(b
, target
, "rolls %li %li-sided %s for a total of %li (%+li)", dice
, sides
, dice
== 1 ? "die" : "dice", total
, bonus
);
1401 action(b
, target
, "rolls %li %li-sided %s for a total of %li", dice
, sides
, dice
== 1 ? "die" : "dice", total
);
1404 static void command_roll(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1407 long dice
= 1, sides
= 20;
1408 if ((cur
= token(&args
, ' '))) {
1410 dice
= strtol(first
, &cur
, 10);
1413 if (*cur
&& *cur
!= 'd' && *cur
!= 'D')
1418 sides
= strtol(last
, &cur
, 10);
1423 if (dice
<= 0 || dice
> 25 || sides
< 2 || sides
> 20)
1425 perform_roll(b
, nick
, target
, sides
, dice
, 0);
1429 action(b
, target
, "bleeps at %s in a confused manner", nick
);
1432 static void add_g(struct bio
*b
, const char *target
, int g
)
1435 int64_t total_g
= 0;
1438 if (!chan_db
|| target
[0] != '#')
1441 chan
.dptr
= (char*)target
;
1442 chan
.dsize
= strlen(chan
.dptr
)+1;
1443 res
= tdb_fetch(chan_db
, chan
);
1444 if (res
.dptr
&& res
.dsize
>= 16) {
1445 ((int64_t*)res
.dptr
)[1] += g
;
1446 if (res
.dsize
>= 24)
1447 total_g
= ((int64_t*)res
.dptr
)[2] += g
;
1448 ret
= tdb_store(chan_db
, chan
, res
, 0);
1452 fprintf(stderr
, "updating database: %s", tdb_errorstr(feed_db
));
1453 else if (g
< total_g
) {
1454 int64_t last_g
= total_g
- g
;
1455 last_g
-= last_g
% 1000;
1456 if (last_g
+ 1000 <= total_g
)
1457 privmsg(b
, target
, "g has been said %"PRIi64
" times!", total_g
);
1461 static int parse_g(const char *cur
, int *g
)
1464 for (const unsigned char *ptr
= cur
; *ptr
; ptr
++) {
1467 if (*ptr
== 'g' || *ptr
== 'G')
1469 else if ((*ptr
>= 'a' && *ptr
<= 'z') || (*ptr
>= 'A' && *ptr
<= 'Z'))
1471 else if (*ptr
!= '1' && *ptr
>= '0' && *ptr
<= '9')
1478 static void channel_msg(struct bio
*b
, const char *nick
, const char *host
,
1479 const char *chan
, char *msg
, int64_t t
)
1481 char *cur
= NULL
, *next
;
1485 int that
= 0, what
= 0, she
= 0, nsfw
= 0;
1487 if (!msg
|| !strcmp(nick
, "`Daring_Do`") ||
1488 !sstrncmp(nick
, "derpy") || !sstrncmp(nick
, "`derpy") ||
1489 !sstrncmp(nick
, "`Luna") || !sstrncmp(nick
, "`Celestia") ||
1490 !sstrncmp(nick
, "GitHub") || !sstrncmp(nick
, "CIA-") ||
1491 !sstrncmp(nick
, "Termi") || !strcmp(nick
, "r0m") || !sstrncasecmp(nick
, "Owloysius"))
1494 if (!sstrncasecmp(msg
, "\001ACTION ")) {
1495 msg
+= sizeof("\001ACTION ")-1;
1497 asprintf(&cur
, "%"PRIi64
",in %s: * %s %s", t
, chan
, nick
, msg
);
1499 asprintf(&cur
, "%"PRIi64
",in %s: <%s> %s", t
, chan
, nick
, msg
);
1500 if (t
> 0 && strcasecmp(chan
, "#themoon") && strcasecmp(chan
, "#fluttertreehouse"))
1501 update_seen(b
, cur
, nick
);
1505 if (!sstrncasecmp(msg
, "fun!") || !strcasecmp(msg
, "fun")) {
1506 static int64_t last_fun
= -1;
1508 if (!strcasecmp(chan
, "#bronymusic") && t
- last_fun
> 5) {
1509 if (t
< 0 || t
> commands
[strhash("fun") % elements(commands
)].disabled_until
) {
1510 if (sstrncmp(nick
, "EqBot"))
1511 privmsg(b
, chan
, "Fun!");
1512 privmsg(b
, chan
, "Fun!");
1520 while ((cur
= token(&next
, ' '))) {
1521 if (!strcasecmp(cur
, ">mfw") || !strcasecmp(cur
, "mfw")) {
1522 if (!strcasecmp(chan
, "#brony") || !ponify
)
1524 if (t
< 0 || t
> commands
[strhash("mfw") % elements(commands
)].disabled_until
)
1525 command_mfw(b
, nick
, host
, chan
, NULL
);
1527 } else if (!sstrncasecmp(cur
, "http://") || !sstrncasecmp(cur
, "https://")) {
1528 static char last_url
[512];
1530 if (!strcmp(cur
, last_url
))
1532 strncpy(last_url
, cur
, sizeof(last_url
)-1);
1534 if (!sstrncmp(nick
, "EqBot") || !sstrncasecmp(chan
, "#mlpsurvival"))
1537 if (t
>= 0 && t
< commands
[strhash("get") % elements(commands
)].disabled_until
)
1540 else if (strcasestr(cur
, "youtube.com/user") && (part
= strstr(cur
, "#p/"))) {
1542 part
= strrchr(part
, '/') + 1;
1543 asprintf(&foo
, "http://youtube.com/watch?v=%s", part
);
1545 internal_link(b
, nick
, host
, chan
, foo
, 0);
1548 } else if (strcasestr(cur
, "twitter.com/") || strcasestr(cur
, "mlfw.info") || strcasestr(cur
, "mylittlefacewhen.com") || !strcasecmp(chan
, "#geek"))
1550 if (!strcasecmp(chan
, "#bronymusic") &&
1551 (nsfw
|| (next
&& strcasestr(next
, "nsfw"))))
1553 internal_link(b
, nick
, host
, chan
, cur
, 0);
1556 else if (!sstrncasecmp(cur
, "that") || !sstrncasecmp(cur
, "dat"))
1558 else if (that
&& (!sstrncasecmp(cur
, "what") || !sstrncasecmp(cur
, "wat")))
1560 else if (what
&& !sstrncasecmp(cur
, "she"))
1562 else if (she
&& !sstrncasecmp(cur
, "said")) {
1563 if (t
<= 0 || t
>= commands
[strhash("shesaid") % elements(commands
)].disabled_until
)
1564 privmsg(b
, chan
, "%s: \"%s\"", nick
, women_quotes
[getrand() % elements(women_quotes
)]);
1566 } else if (parse_g(cur
, &g
))
1568 else if (strcasestr(cur
, "nsfw"))
1575 static void command_commands(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
);
1577 static struct command_hash unhashed
[] = {
1578 { "1", command_coinflip
},
1579 { "fabric", command_fabric
},
1580 { "fun", command_fun
},
1581 { "get", command_get
},
1582 { "hug", command_hug
},
1583 { "hugs", command_hugs
, -1 },
1584 { "admire", command_admire
},
1585 { "snug", command_snuggle
, -1 },
1586 { "snugs", command_snuggles
, -1 },
1587 { "snuggle", command_snuggle
},
1588 { "snuggles", command_snuggles
, -1 },
1589 { "cuddle", command_cuddle
},
1590 { "cuddles", command_cuddles
, -1 },
1591 { "cookie", command_cookie
},
1592 { "mfw", command_mfw
},
1593 { "swear", command_swear
},
1594 { "mail", command_mail
},
1595 { "m", command_mail
, -1 },
1596 { "seen", command_seen
},
1597 { "derpy", command_derpy
},
1598 { "g", command_g
, -1 },
1599 { "shesaid", command_shesaid
},
1600 { "roll", command_roll
},
1602 { "rebuild", command_rebuild
, 1 },
1603 { "abort", command_abort
, 1 },
1604 { "crash", command_crash
, 1 },
1605 { "inspect", command_inspect
, 1 },
1606 { "timeout", command_timeout
, 1 },
1608 { "follow", command_follow
},
1609 { "unfollow", command_unfollow
},
1610 { "feeds", command_feeds
},
1613 { "feed_get", command_feed_get
, 1 },
1614 { "feed_set", command_feed_set
, 1 },
1615 { "feed_rem", command_feed_rem
, 1 },
1616 { "feed_xxx", command_feed_xxx
, 1 },
1617 { "feed_counter", command_feed_counter
, 1 },
1618 { "seen_xxx", command_seen_xxx
, 1 },
1619 { "mailbag", command_mailbag
},
1620 { "mailread", command_mailread
, 1 },
1621 { "\"deliver\"", command_no_deliver
, 1 },
1622 { "commands", command_commands
, -1 }
1625 static void command_commands(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
1628 unsigned rem
= sizeof(buffer
)-1, first
= 2, i
;
1631 for (i
= 0; i
< sizeof(unhashed
)/sizeof(*unhashed
); ++i
) {
1634 if (unhashed
[i
].admin
)
1637 str
= unhashed
[i
].string
;
1640 if (len
+ 5 > rem
) {
1641 privmsg(b
, target
, "%s: Valid commands: %s", nick
, &buffer
[rem
]);
1643 rem
= sizeof(buffer
)-1;
1651 memcpy(&buffer
[rem
], " and ", 5);
1655 memcpy(&buffer
[rem
], ", ", 2);
1658 memcpy(&buffer
[rem
], str
, len
);
1662 privmsg(b
, target
, "%s: Valid commands: %s", nick
, &buffer
[rem
]);
1665 static void init_hash(struct bio
*b
, const char *target
)
1668 for (i
= 0; i
< elements(unhashed
); ++i
) {
1669 unsigned h
= strhash(unhashed
[i
].string
) % elements(commands
);
1670 if (commands
[h
].string
)
1671 privmsg(b
, target
, "%s is a duplicate command with %s", commands
[h
].string
, unhashed
[i
].string
);
1673 commands
[h
] = unhashed
[i
];
1675 #ifdef local_commands
1676 for (i
= 0; i
< elements(local_commands
); ++i
) {
1677 unsigned h
= strhash(local_commands
[i
].string
) % elements(commands
);
1678 if (commands
[h
].string
)
1679 privmsg(b
, target
, "%s is a duplicate command with %s", commands
[h
].string
, local_commands
[i
].string
);
1681 commands
[h
] = local_commands
[i
];
1686 void init_hook(struct bio
*b
, const char *target
, const char *nick
, unsigned is_ponified
)
1688 char *cwd
, *path
= NULL
;
1690 static const char *messages
[] = {
1691 "feels circuits being activated that weren't before",
1692 "suddenly gets a better feel of her surroundings",
1693 "looks the same, yet there's definitely something different",
1694 "emits a beep as her lights begin to pulse slowly",
1695 "whirrrs and bleeps like never before",
1696 "bleeps a few times happily",
1697 "excitedly peeks at her surroundings"
1699 init_hash(b
, target
);
1700 ponify
= is_ponified
;
1702 cwd
= getcwd(NULL
, 0);
1703 asprintf(&path
, "%s/db/feed.tdb", cwd
);
1704 feed_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1707 privmsg(b
, target
, "Opening feed db failed: %m");
1709 asprintf(&path
, "%s/db/chan.tdb", cwd
);
1710 chan_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1713 privmsg(b
, target
, "Opening chan db failed: %m");
1715 asprintf(&path
, "%s/db/mail.tdb", cwd
);
1716 mail_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1719 privmsg(b
, target
, "Opening mail db failed: %m");
1721 asprintf(&path
, "%s/db/seen.tdb", cwd
);
1722 seen_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1725 privmsg(b
, target
, "Opening seen db failed: %m");
1728 action(b
, target
, "%s", messages
[getrand() % elements(messages
)]);
1731 static void __attribute__((destructor
)) shutdown_hook(void)
1743 static char *nom_special(char *line
)
1746 if (*line
== 0x03) {
1748 if (*line
>= '0' && *line
<= '9')
1751 if (*line
>= '0' && *line
<= '9')
1753 if (line
[0] == ',' && line
[1] >= '0' && line
[1] <= '9')
1756 if (*line
>= '0' && *line
<= '9')
1758 } else if (*line
>= 0x02 && *line
<= 0x1f)
1759 /* some IRC control code that's not CTCP */
1767 static char *cleanup_special(char *line
)
1769 char *cur
, *start
= nom_special(line
);
1772 for (line
= cur
= start
; *line
; line
= nom_special(line
))
1773 *(cur
++) = *(line
++);
1775 for (cur
--; cur
>= start
; --cur
)
1776 if (*cur
!= ' ' && *cur
!= '\001')
1785 static void rss_check(struct bio
*b
, const char *channel
, int64_t t
)
1787 static unsigned rss_enter
, rss_leave
;
1788 if (t
>= 0 && rss_enter
== rss_leave
) {
1790 channel_feed_check(b
, channel
, t
);
1795 static const char *privileged_command
[] = {
1796 "{ Rarity, I love you so much! }",
1797 "{ Rarity, have I ever told you that I love you? }",
1798 "{ Yes, I love my sister, Rarity. }",
1799 "{ Raaaaaaaaaaaaaarity. }",
1800 "{ You do not fool me, Rari...bot! }"
1803 static int cmd_check(struct bio
*b
, struct command_hash
*c
, int is_admin
, int64_t t
, const char *prefix
, const char *target
)
1805 if (c
->left
!= c
->enter
)
1806 privmsg(b
, target
, "Command %s is disabled because of a crash", c
->string
);
1807 else if (t
> 0 && t
< c
->disabled_until
&& !is_admin
) {
1808 int64_t delta
= c
->disabled_until
- t
;
1809 const char *str
= perty(&delta
);
1810 b
->writeline(b
, "NOTICE %s :Command %s is on timeout for the next %"PRIi64
" %s", prefix
, c
->string
, delta
, str
);
1811 } else if (c
->admin
> 0 && !is_admin
)
1812 privmsg(b
, target
, "%s: %s", prefix
, privileged_command
[getrand() % elements(privileged_command
)]);
1818 void privmsg_hook(struct bio
*b
, const char *prefix
, const char *ident
, const char *host
,
1819 const char *const *args
, unsigned nargs
)
1821 char *cmd_args
, *cmd
;
1822 const char *target
= args
[0][0] == '#' ? args
[0] : prefix
;
1823 unsigned chan
, nick_len
;
1824 struct command_hash
*c
;
1825 int64_t t
= get_time(b
, target
);
1826 int is_admin
= admin(host
);
1828 chan
= args
[0][0] == '#';
1830 rss_check(b
, args
[0], t
);
1831 command_deliver(b
, prefix
, args
[0]);
1833 cmd_args
= cleanup_special((char*)args
[1]);
1837 if (ident
&& (!strcasecmp(ident
, "Revolver") || !strcasecmp(ident
, "Rev")))
1840 if (chan
&& cmd_args
[0] == '!') {
1842 } else if (chan
&& (nick_len
= strlen(nick_self
)) &&
1843 !strncasecmp(cmd_args
, nick_self
, nick_len
) &&
1844 (cmd_args
[nick_len
] == ':' || cmd_args
[nick_len
] == ',') && cmd_args
[nick_len
+1] == ' ') {
1845 cmd_args
+= nick_len
+ 2;
1850 if (command_channel
.enter
== command_channel
.left
) {
1851 command_channel
.enter
++;
1852 snprintf(command_channel
.failed_command
,
1853 sizeof(command_channel
) - offsetof(struct command_hash
, failed_command
) - 1,
1854 "%s:%s (%s@%s) \"#\" %s", target
, prefix
, ident
, host
, cmd_args
);
1855 channel_msg(b
, prefix
, host
, args
[0], cmd_args
, t
);
1856 command_channel
.left
++;
1860 cmd
= token(&cmd_args
, ' ');
1861 if (!chan
&& cmd_args
&& cmd_args
[0] == '#') {
1863 privmsg(b
, target
, "%s: %s", prefix
, privileged_command
[getrand() % elements(privileged_command
)]);
1866 target
= token(&cmd_args
, ' ');
1869 c
= &commands
[strhash(cmd
) % elements(commands
)];
1870 if (c
->string
&& !strcasecmp(c
->string
, cmd
)) {
1871 if (!sstrncasecmp(prefix
, "EqBot")) {
1872 action(b
, target
, "adoringly pulses some light back to %s", prefix
);
1875 if (cmd_check(b
, c
, is_admin
, t
, prefix
, target
)) {
1877 snprintf(c
->failed_command
, sizeof(*c
) - offsetof(struct command_hash
, failed_command
) - 1,
1878 "%s:%s (%s@%s) \"%s\" %s", target
, prefix
, ident
, host
, cmd
, cmd_args
);
1879 c
->cmd(b
, prefix
, host
, target
, cmd_args
);
1883 } else if (cmd
[0] == 'd' && cmd
[1] >= '0' && cmd
[1] <= '9') {
1886 base
= strtol(cmd
+1, &end
, 10);
1887 if (base
> 1 && base
<= 20 && !*end
) {
1888 long dice
= 1, bonus
= 0;
1890 c
= &commands
[strhash("roll") % elements(commands
)];
1891 if (!cmd_check(b
, c
, is_admin
, t
, prefix
, target
))
1894 snprintf(c
->failed_command
, sizeof(*c
) - offsetof(struct command_hash
, failed_command
) - 1,
1895 "%s:%s (%s@%s) \"%s\" %s", target
, prefix
, ident
, host
, cmd
, cmd_args
);
1897 strdice
= token(&cmd_args
, ' ');
1899 dice
= strtol(strdice
, &end
, 10);
1900 if (*end
|| !dice
|| dice
> 25)
1902 strdice
= token(&cmd_args
, ' ');
1904 bonus
= strtol(strdice
, &end
, 10);
1905 if (*end
|| bonus
> 1000 || bonus
< -1000)
1909 perform_roll(b
, prefix
, target
, base
, dice
, bonus
);
1914 action(b
, target
, "bleeps at %s in a confused manner", prefix
);
1918 } else if (cmd
[0] >= '0' && cmd
[0] <= '9') {
1919 long dice
, sides
, bonus
= 0;
1921 dice
= strtol(cmd
, &end
, 10);
1922 if (dice
>= 1 && dice
<= 25 && *end
== 'd' &&
1923 (sides
= strtol(end
+1, &end
, 10)) &&
1924 sides
> 1 && sides
<= 20 && !*end
) {
1927 c
= &commands
[strhash("roll") % elements(commands
)];
1928 if (!cmd_check(b
, c
, is_admin
, t
, prefix
, target
))
1931 snprintf(c
->failed_command
, sizeof(*c
) - offsetof(struct command_hash
, failed_command
) - 1,
1932 "%s:%s (%s@%s) \"%s\" %s", target
, prefix
, ident
, host
, cmd
, cmd_args
);
1934 strbonus
= token(&cmd_args
, ' ');
1936 bonus
= strtol(strbonus
, &end
, 10);
1937 if (*end
|| bonus
> 1000 || bonus
< -1000)
1941 perform_roll(b
, prefix
, target
, sides
, dice
, bonus
);
1947 if (chan
== 2 && t
> 0) {
1948 static int64_t last_t
;
1950 privmsg(b
, target
, "%s: { I love you! }", prefix
);
1955 void command_hook(struct bio
*b
, const char *prefix
, const char *ident
, const char *host
,
1956 const char *command
, const char *const *args
, unsigned nargs
)
1960 if (!strcasecmp(command
, "NOTICE")) {
1961 if (nargs
< 2 || args
[0][0] == '#')
1964 b
->writeline(b
, "%s", args
[1]);
1966 fprintf(stderr
, "%s: %s\n", prefix
, args
[1]);
1967 } else if (!strcasecmp(command
, "JOIN")) {
1968 t
= get_time(b
, args
[0]);
1969 rss_check(b
, args
[0], t
);
1970 command_deliver(b
, prefix
, args
[0]);
1971 asprintf(&buf
, "%"PRIi64
",joining %s", t
, args
[0]);
1972 } else if (!strcasecmp(command
, "PART")) {
1973 t
= get_time(b
, args
[0]);
1974 rss_check(b
, args
[0], t
);
1975 asprintf(&buf
, "%"PRIi64
",leaving %s", t
, args
[0]);
1976 } else if (!strcasecmp(command
, "QUIT")) {
1977 t
= get_time(b
, NULL
);
1978 asprintf(&buf
, "%"PRIi64
",quitting with the message \"%s\"", t
, args
[0]);
1979 } else if (!strcasecmp(command
, "NICK")) {
1980 t
= get_time(b
, NULL
);
1982 asprintf(&buf
, "%"PRIi64
",changing nick from %s", t
, prefix
);
1984 update_seen(b
, buf
, args
[0]);
1988 asprintf(&buf
, "%"PRIi64
",changing nick to %s", t
, args
[0]);
1991 fprintf(stderr
, ":%s!%s%s %s", prefix
, ident
, host
, command
);
1992 for (i
= 0; i
< nargs
; ++i
)
1993 fprintf(stderr
, " %s", args
[i
]);
1994 fprintf(stderr
, "\n");
1997 update_seen(b
, buf
, prefix
);