3 #include <libxml/xmlstring.h>
4 #include <libxml/HTMLparser.h>
15 static const char *nick_self
;
16 static unsigned ponify
;
17 #define elements(x) (sizeof(x)/sizeof(*x))
19 static struct tdb_context
*feed_db
, *chan_db
, *mail_db
, *seen_db
;
21 #define sstrncmp(a, b) (strncmp(a, b, sizeof(b)-1))
22 #define sstrncasecmp(a, b) (strncasecmp(a, b, sizeof(b)-1))
26 static int pipe_command(struct bio
*b
, const char *target
, const char *nick
, int redirect_stdout
, int redirect_stderr
, char *argv
[])
30 if (pipe2(fd
, O_CLOEXEC
) < 0) {
31 privmsg(b
, target
, "Could not create pipe: %m");
36 privmsg(b
, target
, "Could not fork: %m");
41 int fdnull
= open("/dev/null", O_WRONLY
|O_CLOEXEC
);
43 if (dup3(redirect_stdout
? fd
[1] : fdnull
, 1, 0) < 0)
45 if (dup3(redirect_stderr
? fd
[1] : fdnull
, 2, 0) < 0)
47 exit(execv(argv
[0], argv
));
53 fcntl(fd
[0], F_SETFL
, O_NONBLOCK
);
54 while ((ret
= waitpid(pid
, &loc
, WNOHANG
)) >= 0) {
55 while (read(fd
[0], buffer
+bufptr
, 1) == 1) {
56 if (buffer
[bufptr
] != '\n' && bufptr
< sizeof(buffer
)-1) {
63 if (lines
< SPAM_CUTOFF
)
64 privmsg(b
, nick
, "%s", buffer
);
66 fprintf(stderr
, "%s\n", buffer
);
73 if (lines
< SPAM_CUTOFF
)
74 privmsg(b
, nick
, "%s", buffer
);
76 fprintf(stderr
, "%s\n", buffer
);
78 if (lines
>= SPAM_CUTOFF
)
79 privmsg(b
, nick
, "%i lines suppressed", lines
- SPAM_CUTOFF
+ 1);
84 privmsg(b
, target
, "error on waitpid: %m");
94 static struct command_hash commands
[2048];
96 static void command_abort(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
101 static void command_coinflip(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
104 action(b
, target
, "whirrrs and clicks excitedly at %s", nick
);
106 action(b
, target
, "eyes %s as nothing happens", nick
);
109 static void command_crash(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
114 static void squash(char *title
)
119 while (*title
!= '\n' && *title
!= '\r' && *title
!= '\t' && *title
!= ' ' && *title
) {
120 *(start
++) = *(title
++);
125 while (*title
== '\n' || *title
== '\r' || *title
== '\t' || *title
== ' ')
131 struct curl_download_context
137 static size_t write_data(void *ptr
, size_t size
, size_t nmemb
, void *member
) {
138 struct curl_download_context
*ctx
= member
;
140 ctx
->data
= realloc(ctx
->data
, ctx
->len
+ size
);
141 memcpy(ctx
->data
+ ctx
->len
, ptr
, size
);
146 static const char *get_text(xmlNode
*cur
)
148 for (; cur
; cur
= cur
->next
) {
149 if (cur
->type
== XML_TEXT_NODE
)
155 static const char *get_link(xmlAttr
*cur
, const char *which
)
157 for (; cur
; cur
= cur
->next
) {
158 if (!strcasecmp(cur
->name
, which
))
159 return get_text(cur
->children
);
164 static const char *walk_feed(struct bio
*b
, const char *nick
, const char *target
, const char *url
, xmlNode
*root
, const char *last_link
)
166 const char *main_title
= NULL
, *main_subtitle
= NULL
, *main_link
= NULL
;
167 const char *title
= NULL
, *link
= NULL
;
168 xmlNode
*cur
, *entry
= NULL
;
169 for (cur
= root
->children
; cur
; cur
= cur
->next
) {
170 const char *name
= cur
->name
;
171 if (cur
->type
!= XML_ELEMENT_NODE
)
173 else if (!strcasecmp(name
, "category"))
175 else if (!strcasecmp(name
, "entry"))
176 entry
= entry
? entry
: cur
;
177 else if (!strcasecmp(name
, "title"))
178 main_title
= get_text(cur
->children
);
179 else if (!strcasecmp(name
, "subtitle"))
180 main_subtitle
= get_text(cur
->children
);
181 else if (!strcasecmp(name
, "link")) {
182 const char *ishtml
= get_link(cur
->properties
, "type");
183 const char *rel
= get_link(cur
->properties
, "rel");
184 if ((!ishtml
|| !strcasecmp(ishtml
, "text/html")) && rel
&& !strcasecmp(rel
, "alternate"))
185 main_link
= get_link(cur
->properties
, "href");
188 if (!main_link
|| !main_title
) {
189 privmsg(b
, target
, "%s: Failed to parse main: %s %s", nick
, main_link
, main_title
);
194 for (cur
= entry
->children
; cur
; cur
= cur
->next
) {
195 const char *name
= cur
->name
;
196 if (cur
->type
!= XML_ELEMENT_NODE
)
198 else if (!strcasecmp(name
, "link")) {
199 const char *ishtml
= get_link(cur
->properties
, "type");
200 const char *rel
= get_link(cur
->properties
, "rel");
201 if ((!ishtml
|| !strcasecmp(ishtml
, "text/html")) && rel
&& !strcasecmp(rel
, "alternate"))
202 link
= get_link(cur
->properties
, "href");
203 } else if (!strcasecmp(name
, "title"))
204 title
= get_text(cur
->children
);
207 title
= "<no title>";
209 privmsg(b
, target
, "%s: Failed to parse entry: %s %s", nick
, link
, title
);
213 privmsg(b
, target
, "adding blog %s \"%s\": %s", main_link
, main_title
, main_subtitle
);
214 privmsg(b
, target
, "Most recent entry: %s %s", link
, title
);
215 } else if (strcmp(last_link
, link
)) {
217 const char *prev_title
= NULL
, *prev_link
= NULL
, *cur_title
= NULL
, *cur_link
= NULL
;
218 for (entry
= entry
->next
; entry
; entry
= entry
->next
) {
219 if (strcasecmp(entry
->name
, "entry"))
221 prev_title
= cur_title
;
222 prev_link
= cur_link
;
223 cur_title
= cur_link
= NULL
;
224 for (cur
= entry
->children
; cur
; cur
= cur
->next
) {
225 const char *name
= cur
->name
;
226 if (cur
->type
!= XML_ELEMENT_NODE
)
228 else if (!strcasecmp(name
, "link")) {
229 const char *ishtml
= get_link(cur
->properties
, "type");
230 const char *rel
= get_link(cur
->properties
, "rel");
231 if ((!ishtml
|| !strcasecmp(ishtml
, "text/html")) &&
232 rel
&& !strcasecmp(rel
, "alternate"))
233 cur_link
= get_link(cur
->properties
, "href");
234 } else if (!strcasecmp(name
, "title"))
235 cur_title
= get_text(cur
->children
);
238 cur_title
= "<no title>";
239 if (!cur_link
|| !strcmp(last_link
, cur_link
))
244 privmsg(b
, target
, "( %s ): %s", prev_link
, prev_title
);
245 else if (updates
> 1)
246 privmsg(b
, target
, "( %s ): %u updates, linking most recent", main_link
, 1+updates
);
247 privmsg(b
, target
, "( %s ): %s", link
, title
);
252 static const char *walk_rss(struct bio
*b
, const char *nick
, const char *target
, const char *url
, xmlNode
*root
, const char *last_link
)
254 const char *main_title
= NULL
, *main_link
= NULL
, *title
= NULL
, *link
= NULL
, *ver
;
255 xmlNode
*cur
, *entry
= NULL
;
256 ver
= get_link(root
->properties
, "version");
257 if (!ver
|| strcmp(ver
, "2.0")) {
259 privmsg(b
, target
, "%s: Could not parse rss feed", nick
);
261 privmsg(b
, target
, "%s: Invalid rss version \"%s\"", nick
, ver
);
264 for (cur
= root
->children
; cur
&& cur
->type
!= XML_ELEMENT_NODE
; cur
= cur
->next
);
267 for (cur
= cur
->children
; cur
; cur
= cur
->next
) {
268 const char *name
= cur
->name
;
269 if (cur
->type
!= XML_ELEMENT_NODE
)
271 if (!strcasecmp(name
, "title"))
272 main_title
= get_text(cur
->children
);
273 else if (!strcasecmp(name
, "link"))
274 main_link
= main_link
? main_link
: get_text(cur
->children
);
275 else if (!strcasecmp(name
, "item"))
276 entry
= entry
? entry
: cur
;
278 if (!main_link
|| !main_title
) {
279 privmsg(b
, target
, "%s: Failed to parse main: %s %s", nick
, main_link
, main_title
);
286 for (cur
= entry
->children
; cur
; cur
= cur
->next
) {
287 const char *name
= cur
->name
;
288 if (cur
->type
!= XML_ELEMENT_NODE
)
290 if (!strcasecmp(name
, "title"))
291 title
= get_text(cur
->children
);
292 else if (!strcasecmp(name
, "link"))
293 link
= get_text(cur
->children
);
296 title
= "<no title>";
298 privmsg(b
, target
, "%s: Failed to parse entry: %s %s", nick
, link
, title
);
302 privmsg(b
, target
, "adding blog %s \"%s\"", main_link
, main_title
);
303 privmsg(b
, target
, "Most recent entry: %s %s", link
, title
);
304 } else if (strcmp(last_link
, link
)) {
306 const char *prev_title
= NULL
, *prev_link
= NULL
, *cur_title
= NULL
, *cur_link
= NULL
;
307 for (entry
= entry
->next
; entry
; entry
= entry
->next
) {
308 if (strcasecmp(entry
->name
, "item"))
310 prev_title
= cur_title
;
311 prev_link
= cur_link
;
312 cur_title
= cur_link
= NULL
;
313 for (cur
= entry
->children
; cur
; cur
= cur
->next
) {
314 const char *name
= cur
->name
;
315 if (cur
->type
!= XML_ELEMENT_NODE
)
317 if (!strcasecmp(name
, "title"))
318 cur_title
= get_text(cur
->children
);
319 else if (!strcasecmp(name
, "link"))
320 cur_link
= get_text(cur
->children
);
323 cur_title
= "<no title>";
324 if (!cur_link
|| !strcmp(last_link
, cur_link
))
329 privmsg(b
, target
, "( %s ): %s", prev_link
, prev_title
);
330 else if (updates
> 1)
331 privmsg(b
, target
, "( %s ): %u updates, linking most recent", main_link
, 1+updates
);
332 privmsg(b
, target
, "( %s ): %s", link
, title
);
337 // HTML is a mess, so I'm just walking the tree depth first until I find the next element..
338 static xmlNode
*next_link(xmlNode
*cur_node
)
340 if (cur_node
->children
)
341 return cur_node
->children
;
344 return cur_node
->next
;
345 cur_node
= cur_node
->parent
;
350 static const char *get_atom_link(xmlNode
*cur
)
352 for (; cur
; cur
= next_link(cur
)) {
353 if (cur
->type
!= XML_ELEMENT_NODE
)
355 if (!strcasecmp(cur
->name
, "link")) {
356 const char *isxml
= get_link(cur
->properties
, "type");
357 if (isxml
&& !strcasecmp(isxml
, "application/atom+xml"))
358 return get_link(cur
->properties
, "href");
364 static const char *get_rss_link(xmlNode
*cur
)
366 for (; cur
; cur
= next_link(cur
)) {
367 if (cur
->type
!= XML_ELEMENT_NODE
)
369 if (!strcasecmp(cur
->name
, "link")) {
370 const char *isxml
= get_link(cur
->properties
, "type");
371 if (isxml
&& !strcasecmp(isxml
, "application/rss+xml"))
372 return get_link(cur
->properties
, "href");
378 static void do_html(struct bio
*b
, const char *nick
, const char *target
, const char *url
, const char *data
, unsigned len
)
380 htmlDocPtr ctx
= htmlReadMemory(data
, len
, 0, url
, HTML_PARSE_RECOVER
|HTML_PARSE_NOERROR
|HTML_PARSE_NOWARNING
);
381 xmlNode
*root
= xmlDocGetRootElement(ctx
);
382 const char *link
= get_atom_link(root
);
384 privmsg(b
, target
, "%s: not a valid feed link, try atom: %s", nick
, link
);
385 else if ((link
= get_rss_link(root
)))
386 privmsg(b
, target
, "%s: not a valid feed link, try rss: %s", nick
, link
);
388 privmsg(b
, target
, "%s: not a valid feed link, no suggestion found", nick
);
392 static size_t get_time_from_header(void *data
, size_t size
, size_t size2
, void *ptr
)
396 if (sstrncmp(data
, "Last-Modified: "))
398 data
+= sizeof("Last-Modified: ")-1;
399 *(char**)ptr
= d
= strdup(data
);
400 if ((e
= strchr(d
, '\r')))
405 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
)
407 struct curl_download_context curl_ctx
= {};
408 struct curl_slist
*headers
= NULL
;
409 char error
[CURL_ERROR_SIZE
];
411 headers
= curl_slist_append(headers
, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
412 headers
= curl_slist_append(headers
, "Accept: */*");
414 CURL
*h
= curl_easy_init();
415 curl_easy_setopt(h
, CURLOPT_HTTPHEADER
, headers
);
416 curl_easy_setopt(h
, CURLOPT_URL
, url
);
417 curl_easy_setopt(h
, CURLOPT_WRITEFUNCTION
, write_data
);
418 curl_easy_setopt(h
, CURLOPT_WRITEDATA
, &curl_ctx
);
419 curl_easy_setopt(h
, CURLOPT_ERRORBUFFER
, error
);
420 curl_easy_setopt(h
, CURLOPT_FOLLOWLOCATION
, 1);
421 curl_easy_setopt(h
, CURLOPT_MAXREDIRS
, 3);
422 curl_easy_setopt(h
, CURLOPT_SSL_VERIFYPEER
, 0);
423 curl_easy_setopt(h
, CURLOPT_TIMEOUT
, 8);
424 curl_easy_setopt(h
, CURLOPT_FILETIME
, 1);
425 curl_easy_setopt(h
, CURLOPT_HEADERFUNCTION
, get_time_from_header
);
426 curl_easy_setopt(h
, CURLOPT_WRITEHEADER
, &last_modified
);
427 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
431 asprintf(&tmp
, "If-Modified-Since: %s", last_modified
);
432 headers
= curl_slist_append(headers
, tmp
);
436 int success
= curl_easy_perform(h
);
437 curl_slist_free_all(headers
);
438 if (success
== CURLE_OK
) {
441 curl_easy_getinfo(h
, CURLINFO_CONTENT_TYPE
, &mime
);
442 curl_easy_getinfo(h
, CURLINFO_RESPONSE_CODE
, &code
);
445 else if (!mime
|| !sstrncmp(mime
, "application/xml") || !sstrncmp(mime
, "text/xml")) {
446 const char *ret_link
= NULL
;
447 xmlDocPtr ctx
= xmlReadMemory(curl_ctx
.data
, curl_ctx
.len
, 0, url
, XML_PARSE_NOWARNING
| XML_PARSE_NOERROR
);
448 xmlNode
*root
= xmlDocGetRootElement(ctx
);
450 if (!strcasecmp(root
->name
, "feed"))
451 ret_link
= walk_feed(b
, nick
, target
, url
, root
, link
);
452 else if (!strcasecmp(root
->name
, "rss"))
453 ret_link
= walk_rss(b
, nick
, target
, url
, root
, link
);
455 privmsg(b
, target
, "Unknown feed type \"%s\"", root
->name
);
460 privmsg(b
, target
, "Could not feed parse correctly");
461 else if (ret_link
&& (!link
|| strcmp(ret_link
, link
))) {
463 asprintf((char**)&val
.dptr
, "%s\001%s", last_modified
? last_modified
: "", ret_link
);
464 val
.dsize
= strlen(val
.dptr
)+1;
465 if (tdb_store(feed_db
, key
, val
, 0) < 0)
466 privmsg(b
, target
, "updating returns %s", tdb_errorstr(feed_db
));
475 } else if (!sstrncmp(mime
, "text/html") || !sstrncmp(mime
, "application/xhtml+xml"))
476 do_html(b
, nick
, target
, url
, curl_ctx
.data
, curl_ctx
.len
);
478 privmsg(b
, target
, "unhandled content type %s", mime
);
480 privmsg(b
, target
, "Error %s (%u)\n", error
, success
);
483 curl_easy_cleanup(h
);
487 static void command_follow(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
495 if (!(url
= token(&args
, ' ')) || (sstrncmp(url
, "http://") && sstrncmp(url
, "https://"))) {
496 privmsg(b
, target
, "%s: Invalid url", nick
);
499 if (target
[0] != '#') {
500 privmsg(b
, target
, "%s: Can only follow on channels", nick
);
504 key
.dsize
= asprintf((char**)&key
.dptr
, "%s,%s", target
, url
)+1;
505 val
= tdb_fetch(feed_db
, key
);
507 char *foo
= strchr(val
.dptr
, '\001');
508 ret
= check_single_feed(b
, target
, key
, NULL
, url
, foo
? foo
+ 1 : NULL
, nick
);
511 ret
= check_single_feed(b
, target
, key
, NULL
, url
, NULL
, nick
);
513 privmsg(b
, target
, "%s: Not updated", nick
);
518 static void channel_feed_check(struct bio
*b
, const char *target
, int64_t now
)
520 int len
= strlen(target
);
522 if (!feed_db
|| !chan_db
)
524 chan
.dptr
= (char*)target
;
526 res
= tdb_fetch(chan_db
, chan
);
527 if (res
.dptr
&& res
.dsize
== 8) {
528 uint64_t then
= *(uint64_t*)res
.dptr
;
529 if (now
- then
<= 2000)
533 res
.dptr
= (unsigned char*)&now
;
535 if (tdb_store(chan_db
, chan
, res
, 0) < 0) {
536 static int complain_db
;
538 privmsg(b
, target
, "updating database: %s", tdb_errorstr(feed_db
));
542 /* Reset the alarm on every get, we are not actually in danger of doing an infinite loop, probably */
543 for (TDB_DATA d
= tdb_firstkey(feed_db
); d
.dptr
;) {
544 TDB_DATA f
= tdb_fetch(feed_db
, d
);
545 TDB_DATA next
= tdb_nextkey(feed_db
, d
);
547 if (!strncmp(d
.dptr
, target
, len
) && d
.dptr
[len
] == ',') {
548 const char *url
= (char*)d
.dptr
+ len
+ 1;
550 if ((sep
= strchr(f
.dptr
, '\001'))) {
553 check_single_feed(b
, target
, d
, f
.dptr
, url
, sep
, target
);
563 static void command_unfollow(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
571 if (!(url
= token(&args
, ' ')) || (sstrncmp(url
, "http://") && sstrncmp(url
, "https://"))) {
572 privmsg(b
, target
, "%s: Invalid url", nick
);
575 if (target
[0] != '#') {
576 privmsg(b
, target
, "%s: Can only unfollow on channels", nick
);
579 key
.dsize
= asprintf((char**)&key
.dptr
, "%s,%s", target
, url
)+1;
580 if (tdb_delete(feed_db
, key
) < 0) {
581 if (tdb_error(feed_db
) == TDB_ERR_NOEXIST
)
582 privmsg(b
, target
, "%s: Not following %s on this channel", nick
, url
);
584 privmsg(b
, target
, "%s: Could not delete: %s", nick
, tdb_errorstr(feed_db
));
586 privmsg(b
, target
, "%s: No longer following %s", nick
, url
);
590 static void command_feeds(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
592 int len
= strlen(target
), found
= 0;
595 if (target
[0] != '#') {
596 privmsg(b
, target
, "%s: Only useful in channels..", nick
);
599 for (TDB_DATA d
= tdb_firstkey(feed_db
); d
.dptr
;) {
600 TDB_DATA f
= tdb_fetch(feed_db
, d
);
601 TDB_DATA next
= tdb_nextkey(feed_db
, d
);
603 if (!strncmp(d
.dptr
, target
, len
) && d
.dptr
[len
] == ',') {
604 privmsg(b
, target
, "%s: following %s", nick
, d
.dptr
+ len
+ 1);
612 privmsg(b
, target
, "%s: not following any feed on %s", nick
, target
);
615 static void command_feed_get(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
619 for (TDB_DATA d
= tdb_firstkey(feed_db
); d
.dptr
;) {
620 TDB_DATA f
= tdb_fetch(feed_db
, d
);
621 TDB_DATA next
= tdb_nextkey(feed_db
, d
);
622 privmsg(b
, target
, "%s: %s = %s", nick
, d
.dptr
, f
.dptr
);
629 static void command_feed_set(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
634 key
.dptr
= token(&args
, ' ');
635 char *value
= token(&args
, ' ');
636 if (!key
.dptr
|| !value
)
638 key
.dsize
= strlen(key
.dptr
) + 1;
639 val
.dsize
= strlen(value
) + 2;
640 val
.dptr
= malloc(val
.dsize
);
641 strcpy(val
.dptr
+1, value
);
642 val
.dptr
[0] = '\001';
643 if (tdb_store(feed_db
, key
, val
, 0) < 0)
644 privmsg(b
, target
, "%s: setting failed: %s", nick
, tdb_errorstr(feed_db
));
646 privmsg(b
, target
, "%s: burp", nick
);
650 static void command_feed_rem(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
652 if (!feed_db
|| !args
)
654 TDB_DATA key
= { .dptr
= (unsigned char*)args
, .dsize
= strlen(args
)+1 };
655 if (tdb_delete(feed_db
, key
) < 0)
656 privmsg(b
, target
, "%s: removing failed: %s", nick
, tdb_errorstr(feed_db
));
658 privmsg(b
, target
, "%s: burp", nick
);
661 static void command_feed_xxx(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
666 tdb_wipe_all(feed_db
);
667 privmsg(b
, target
, "%s: all evidence erased", nick
);
670 static void command_seen_xxx(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
675 tdb_wipe_all(seen_db
);
676 privmsg(b
, target
, "%s: all evidence erased", nick
);
679 static void command_feed_counter(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
683 tdb_wipe_all(chan_db
);
684 privmsg(b
, target
, "%s: All update counters reset", nick
);
687 static char *get_text_appended(xmlNode
*cur
)
689 for (; cur
; cur
= cur
->next
) {
690 if (cur
->type
!= XML_TEXT_NODE
)
692 return strdup(cur
->content
);
697 static char *get_title(struct bio
*b
, xmlNode
*cur_node
)
699 for (; cur_node
; cur_node
= next_link(cur_node
)) {
700 if (cur_node
->type
== XML_ELEMENT_NODE
&& !strcasecmp(cur_node
->name
, "title")) {
701 if (!cur_node
->children
)
703 return get_text_appended(cur_node
->children
);
709 static void internal_link(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
, unsigned verbose
)
712 struct curl_slist
*headers
= NULL
;
713 char error
[CURL_ERROR_SIZE
];
715 struct curl_download_context curl_ctx
= {};
720 h
= curl_easy_init();
721 headers
= curl_slist_append(headers
, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
722 headers
= curl_slist_append(headers
, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
723 headers
= curl_slist_append(headers
, "Accept-Language: en-us,en;q=0.7");
724 headers
= curl_slist_append(headers
, "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
725 headers
= curl_slist_append(headers
, "DNT: 1");
726 headers
= curl_slist_append(headers
, "Connection: keep-alive");
727 curl_easy_setopt(h
, CURLOPT_HTTPHEADER
, headers
);
728 curl_easy_setopt(h
, CURLOPT_URL
, args
);
729 curl_easy_setopt(h
, CURLOPT_WRITEFUNCTION
, write_data
);
730 curl_easy_setopt(h
, CURLOPT_WRITEDATA
, &curl_ctx
);
731 curl_easy_setopt(h
, CURLOPT_ERRORBUFFER
, error
);
732 curl_easy_setopt(h
, CURLOPT_FOLLOWLOCATION
, 1);
733 curl_easy_setopt(h
, CURLOPT_MAXREDIRS
, 20);
734 curl_easy_setopt(h
, CURLOPT_SSL_VERIFYPEER
, 0);
735 curl_easy_setopt(h
, CURLOPT_TIMEOUT
, 8);
736 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
737 success
= curl_easy_perform(h
);
738 curl_easy_cleanup(h
);
739 curl_slist_free_all(headers
);
740 if (success
== CURLE_OK
) {
741 magic_t m
= magic_open(MAGIC_MIME_TYPE
);
743 const char *mime
= magic_buffer(m
, curl_ctx
.data
, curl_ctx
.len
);
744 if (strstr(mime
, "text/html") || strstr(mime
, "application/xml") || strstr(mime
, "application/xhtml+xml")) {
745 htmlDocPtr ctx
= htmlReadMemory(curl_ctx
.data
, curl_ctx
.len
, 0, args
, HTML_PARSE_RECOVER
|HTML_PARSE_NOERROR
|HTML_PARSE_NOWARNING
);
746 xmlNode
*root_element
= xmlDocGetRootElement(ctx
);
747 char *title
= get_title(b
, root_element
);
751 decode_html_entities_utf8(title
, NULL
);
752 if ((nuke
= strstr(title
, " on SoundCloud - Create")))
755 privmsg(b
, target
, "%s linked %s", nick
, title
);
758 if (verbose
&& !title
)
759 privmsg(b
, target
, "%s linked %s page with invalid title", nick
, mime
);
761 } else if (verbose
) {
762 magic_setflags(m
, MAGIC_COMPRESS
);
763 const char *desc
= magic_buffer(m
, curl_ctx
.data
, curl_ctx
.len
);
764 privmsg(b
, target
, "%s linked type %s", nick
, desc
);
768 if (verbose
&& success
!= CURLE_OK
)
769 privmsg(b
, target
, "Error %s (%u)\n", error
, success
);
773 static void command_get(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
775 if (!args
|| (sstrncmp(args
, "http://") && sstrncmp(args
, "https://")))
778 internal_link(b
, nick
, host
, target
, token(&args
, ' '), 1);
781 static void command_hug(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
783 action(b
, target
, "gives a robotic hug to %s", args
? args
: nick
);
786 static void command_inspect(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
788 struct command_hash
*c
;
789 unsigned leave
, crash
;
792 c
= &commands
[strhash(args
) % elements(commands
)];
793 if (!c
->string
|| strcasecmp(c
->string
, args
)) {
794 privmsg(b
, target
, "Command %s not valid", args
);
797 leave
= c
->left
+ (c
->cmd
== command_inspect
);
798 crash
= c
->enter
- leave
;
799 if (c
->enter
!= leave
)
800 privmsg(b
, target
, "%s: %u successes and %u crash%s, last crashing command: %s", c
->string
, leave
, crash
, crash
== 1 ? "" : "es", c
->failed_command
);
802 privmsg(b
, target
, "%s: %u time%s executed succesfully", c
->string
, leave
, leave
== 1 ? "" : "s");
805 static void command_rebuild(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
808 char *make
[] = { "/usr/bin/make", "-j4", NULL
};
809 char *git_reset
[] = { "/usr/bin/git", "reset", "--hard", "master", NULL
};
810 ret
= pipe_command(b
, target
, nick
, 0, 1, git_reset
);
812 action(b
, target
, "could not rebuild");
815 alarm(40); // sigh, swapping to death
816 ret
= pipe_command(b
, target
, nick
, 0, 1, make
);
818 kill(getpid(), SIGUSR1
);
820 action(b
, target
, "displays an ominous %i", ret
);
823 static void command_swear(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
825 static const char *insults
[] = {
829 "What are you, a dictionary?",
832 "{ Your lack of bloodlust on the battlefield is proof positive that you are a soulless automaton! }",
833 "Your royal snootiness"
835 privmsg(b
, target
, "%s: %s", args
? args
: nick
, insults
[getrand() % elements(insults
)]);
838 static const char *perty(int64_t *t
)
840 if (*t
>= 14 * 24 * 3600) {
843 } else if (*t
>= 48 * 3600) {
846 } else if (*t
>= 7200) {
849 } else if (*t
>= 120) {
853 return *t
== 1 ? "second" : "seconds";
856 static void command_timeout(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
858 struct command_hash
*c
;
859 int64_t t
= get_time(b
, target
);
863 char *arg
= token(&args
, ' ');
864 if (!arg
|| !args
|| !(howlong
= atoi(args
))) {
865 action(b
, target
, "pretends to time out");;
868 c
= &commands
[strhash(arg
) % elements(commands
)];
869 if (c
->string
&& !strcasecmp(c
->string
, arg
)) {
870 c
->disabled_until
= t
+ howlong
;
871 const char *str
= perty(&howlong
);
872 action(b
, target
, "disables %s for %"PRIi64
" %s", arg
, howlong
, str
);
874 action(b
, target
, "clicks sadly at %s for not being able to find that command", nick
);
877 static void command_mfw(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
879 char error
[CURL_ERROR_SIZE
], *new_url
;
880 CURL
*h
= curl_easy_init();
881 struct curl_slist
*headers
= NULL
;
882 struct curl_download_context curl_ctx
= {};
883 headers
= curl_slist_append(headers
, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:9.0.1) Gecko/20100101 Firefox/9.0.1");
884 headers
= curl_slist_append(headers
, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
885 headers
= curl_slist_append(headers
, "Accept-Language: en-us,en;q=0.7");
886 headers
= curl_slist_append(headers
, "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7");
887 headers
= curl_slist_append(headers
, "DNT: 1");
888 headers
= curl_slist_append(headers
, "Connection: keep-alive");
889 curl_easy_setopt(h
, CURLOPT_HTTPHEADER
, headers
);
890 curl_easy_setopt(h
, CURLOPT_URL
, "http://mylittlefacewhen.com/random/");
891 curl_easy_setopt(h
, CURLOPT_WRITEFUNCTION
, write_data
);
892 curl_easy_setopt(h
, CURLOPT_WRITEDATA
, &curl_ctx
);
893 curl_easy_setopt(h
, CURLOPT_ERRORBUFFER
, error
);
894 curl_easy_setopt(h
, CURLOPT_TIMEOUT
, 8);
895 //curl_easy_setopt(h, CURLOPT_VERBOSE, 1);
896 CURLcode ret
= curl_easy_perform(h
);
897 if (ret
== CURLE_OK
&& curl_easy_getinfo(h
, CURLINFO_REDIRECT_URL
, &new_url
) == CURLE_OK
)
898 privmsg(b
, target
, "%s: %s", nick
, new_url
);
899 curl_slist_free_all(headers
);
900 curl_easy_cleanup(h
);
902 privmsg(b
, target
, "%s: You have no face", nick
);
905 static TDB_DATA
get_mail_key(const char *nick
)
909 d
.dsize
= strlen(nick
)+1;
910 d
.dptr
= malloc(d
.dsize
);
911 for (i
= 0; i
< d
.dsize
- 1; ++i
)
912 d
.dptr
[i
] = tolower(nick
[i
]);
917 static void command_mail(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
924 victim
= token(&args
, ' ');
925 if (!victim
|| !args
|| victim
[0] == '#' || strchr(victim
, '@') || strchr(victim
, '.')) {
926 privmsg(b
, target
, "%s: Usage: !mail <nick> <message>", nick
);
929 if (!strcasecmp(victim
, nick_self
)) {
930 action(b
, target
, "whirrs and clicks excitedly at the mail she received from %s", nick
);
934 key
= get_mail_key(victim
);
935 val
= tdb_fetch(mail_db
, key
);
941 for (cur
= val
.dptr
; cur
< val
.dptr
+ val
.dsize
; cur
+= strlen(cur
)+1)
944 action(b
, target
, "looks sadly at %s as she cannot hold any more mail to %s", nick
, victim
);
948 len
= strlen(nick
) + strlen(args
)+3;
949 val
.dptr
= realloc(val
.dptr
, val
.dsize
+ len
);
950 sprintf(val
.dptr
+ val
.dsize
, "%s: %s", nick
, args
);
952 if (tdb_store(mail_db
, key
, val
, 0) < 0)
953 privmsg(b
, target
, "%s: updating mail returns %s", nick
, tdb_errorstr(mail_db
));
955 action(b
, target
, "whirrs and clicks at %s as she stores the mail for %s", nick
, victim
);
961 static void command_deliver(struct bio
*b
, const char *nick
, const char *target
)
967 key
= get_mail_key(nick
);
968 val
= tdb_fetch(mail_db
, key
);
973 if (strcasecmp(key
.dptr
, nick_self
)) {
974 privmsg(b
, target
, "%s: You've got mail!", nick
);
975 for (cur
= val
.dptr
; cur
< val
.dptr
+ val
.dsize
; cur
+= strlen(cur
)+1)
976 privmsg(b
, target
, "%s", cur
);
979 tdb_delete(mail_db
, key
);
983 static void update_seen(struct bio
*b
, char *doingwhat
, const char *nick
)
985 TDB_DATA key
= get_mail_key(nick
);
986 TDB_DATA val
= { .dptr
= doingwhat
, .dsize
= strlen(doingwhat
)+1 };
988 tdb_store(seen_db
, key
, val
, 0);
992 static void command_seen(struct bio
*b
, const char *nick
, const char *host
, const char *target
, char *args
)
994 char *arg
= token(&args
, ' '), *x
;
995 int64_t now
= get_time(b
, target
);
999 if (!seen_db
|| !arg
) {
1000 privmsg(b
, target
, "%s: { Error... }", nick
);
1003 if (!strcasecmp(arg
, nick_self
)) {
1004 action(b
, target
, "whirrs and clicks at %s", nick
);
1007 key
= get_mail_key(arg
);
1008 val
= tdb_fetch(seen_db
, key
);
1009 if (val
.dptr
&& (x
= strchr(val
.dptr
, ','))) {
1013 delta
= now
- atoll(val
.dptr
);
1014 str
= perty(&delta
);
1016 privmsg(b
, target
, "%s was last seen in the future %s", arg
, x
);
1018 privmsg(b
, target
, "%s was last seen %"PRIi64
" %s ago %s", arg
, delta
, str
, x
);
1020 action(b
, target
, "cannot find any evidence that %s exists", arg
);
1023 static void channel_msg(struct bio
*b
, const char *nick
, const char *host
,
1024 const char *chan
, const char *msg
, int64_t t
)
1026 char *cur
= NULL
, *next
;
1028 if (!msg
|| !strcmp(nick
, "`Celestia`") || !strcmp(nick
, "derpy") || !strcmp(nick
, "`Luna`") || !sstrncmp(nick
, "GitHub") || !sstrncmp(nick
, "CIA-"))
1031 if (!sstrncasecmp(msg
, "\001ACTION ")) {
1032 msg
+= sizeof("\001ACTION ")-1;
1033 asprintf(&cur
, "%"PRIi64
",in %s: * %s %s", t
, chan
, nick
, msg
);
1035 asprintf(&cur
, "%"PRIi64
",in %s: <%s> %s", t
, chan
, nick
, msg
);
1037 update_seen(b
, cur
, nick
);
1041 while (next
&& (cur
= token(&next
, ' '))) {
1042 if (!strcasecmp(cur
, ">mfw") || !strcasecmp(cur
, "mfw")) {
1043 if (!strcasecmp(chan
, "#brony") || !ponify
)
1045 if (t
< 0 || t
> commands
[strhash("mfw") % elements(commands
)].disabled_until
)
1046 command_mfw(b
, nick
, host
, chan
, NULL
);
1048 } else if (!sstrncasecmp(cur
, "http://") || !sstrncasecmp(cur
, "https://")) {
1050 if (!sstrncasecmp(chan
, "#brony") && !sstrncasecmp(cur
, "http://www.youtube.com/watch?v="))
1052 if (t
>= 0 && t
< commands
[strhash("get") % elements(commands
)].disabled_until
)
1055 else if (strcasestr(cur
, "youtube.com/user") && (part
= strstr(cur
, "#p/"))) {
1057 part
= strrchr(part
, '/') + 1;
1058 asprintf(&foo
, "http://youtube.com/watch?v=%s", part
);
1060 internal_link(b
, nick
, host
, chan
, foo
, 0);
1063 } else if (strcasestr(cur
, "twitter.com/") || strcasestr(cur
, "mlfw.info") || strcasestr(cur
, "mylittlefacewhen.com"))
1065 internal_link(b
, nick
, host
, chan
, cur
, 0);
1071 static struct command_hash unhashed
[] = {
1072 { "1", command_coinflip
},
1074 { "get", command_get
},
1075 { "hug", command_hug
},
1076 { "mfw", command_mfw
},
1077 { "swear", command_swear
},
1078 { "mail", command_mail
},
1079 { "seen", command_seen
},
1081 { "rebuild", command_rebuild
, 1 },
1082 { "abort", command_abort
, 1 },
1083 { "crash", command_crash
, 1 },
1084 { "inspect", command_inspect
, 1 },
1085 { "timeout", command_timeout
, 1 },
1087 { "follow", command_follow
},
1088 { "unfollow", command_unfollow
},
1089 { "feeds", command_feeds
},
1092 { "feed_get", command_feed_get
, 1 },
1093 { "feed_set", command_feed_set
, 1 },
1094 { "feed_rem", command_feed_rem
, 1 },
1095 { "feed_xxx", command_feed_xxx
, 1 },
1096 { "feed_counter", command_feed_counter
, 1 },
1097 { "seen_xxx", command_seen_xxx
, 1 },
1100 static void init_hash(struct bio
*b
, const char *target
)
1103 for (i
= 0; i
< elements(unhashed
); ++i
) {
1104 unsigned h
= strhash(unhashed
[i
].string
) % elements(commands
);
1105 if (commands
[h
].string
)
1106 privmsg(b
, target
, "%s is a duplicate command with %s", commands
[h
].string
, unhashed
[i
].string
);
1108 commands
[h
] = unhashed
[i
];
1110 #ifdef local_commands
1111 for (i
= 0; i
< elements(local_commands
); ++i
) {
1112 unsigned h
= strhash(local_commands
[i
].string
) % elements(commands
);
1113 if (commands
[h
].string
)
1114 privmsg(b
, target
, "%s is a duplicate command with %s", commands
[h
].string
, local_commands
[i
].string
);
1116 commands
[h
] = local_commands
[i
];
1121 void init_hook(struct bio
*b
, const char *target
, const char *nick
, unsigned is_ponified
)
1123 char *cwd
, *path
= NULL
;
1125 static const char *messages
[] = {
1126 "feels circuits being activated that weren't before",
1127 "suddenly gets a better feel of her surroundings",
1128 "looks the same, yet there's definitely something different",
1129 "her lights begin to pulse slowly",
1130 "whirrrs and bleeps like never before",
1131 "bleeps a few times happily",
1132 "excitedly peeks at her surroundings"
1134 init_hash(b
, target
);
1135 ponify
= is_ponified
;
1137 cwd
= getcwd(NULL
, 0);
1138 asprintf(&path
, "%s/db/feed.tdb", cwd
);
1139 feed_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1142 privmsg(b
, target
, "Opening feed db failed: %m");
1144 asprintf(&path
, "%s/db/chan.tdb", cwd
);
1145 chan_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1148 privmsg(b
, target
, "Opening chan db failed: %m");
1150 asprintf(&path
, "%s/db/mail.tdb", cwd
);
1151 mail_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1154 privmsg(b
, target
, "Opening mail db failed: %m");
1156 asprintf(&path
, "%s/db/seen.tdb", cwd
);
1157 seen_db
= tdb_open(path
, 0, 0, O_RDWR
|O_CREAT
|O_CLOEXEC
|O_NOCTTY
, 0644);
1160 privmsg(b
, target
, "Opening seen db failed: %m");
1163 action(b
, target
, "%s", messages
[getrand() % elements(messages
)]);
1166 static void __attribute__((destructor
)) shutdown_hook(void)
1178 static char *nom_special(char *line
)
1181 if (*line
== 0x03) {
1183 if (*line
>= '0' && *line
<= '9')
1186 if (*line
>= '0' && *line
<= '9')
1188 if (line
[0] == ',' && line
[1] >= '0' && line
[1] <= '9')
1191 if (*line
>= '0' && *line
<= '9')
1193 } else if (*line
!= 0x02 && *line
!= 0x1f && *line
!= 0x16 && *line
!= 0x06 && *line
!= 0x07)
1201 static char *cleanup_special(char *line
)
1203 char *cur
, *start
= nom_special(line
);
1206 for (line
= cur
= start
; *line
; line
= nom_special(line
))
1207 *(cur
++) = *(line
++);
1209 for (cur
--; cur
>= start
; --cur
)
1210 if (*cur
!= ' ' && *cur
!= '\001')
1219 static void rss_check(struct bio
*b
, const char *channel
, int64_t t
)
1221 static unsigned rss_enter
, rss_leave
;
1222 if (t
>= 0 && rss_enter
== rss_leave
) {
1224 channel_feed_check(b
, channel
, t
);
1229 void privmsg_hook(struct bio
*b
, const char *prefix
, const char *ident
, const char *host
,
1230 const char *const *args
, unsigned nargs
)
1232 char *cmd_args
, *cmd
;
1233 const char *target
= args
[0][0] == '#' ? args
[0] : prefix
;
1234 unsigned chan
, nick_len
;
1235 struct command_hash
*c
;
1236 int64_t t
= get_time(b
, target
);
1238 chan
= args
[0][0] == '#';
1240 rss_check(b
, args
[0], t
);
1241 command_deliver(b
, prefix
, args
[0]);
1243 cmd_args
= cleanup_special((char*)args
[1]);
1246 if (chan
&& cmd_args
[0] == '!') {
1248 } else if (chan
&& (nick_len
= strlen(nick_self
)) &&
1249 !strncasecmp(cmd_args
, nick_self
, nick_len
) &&
1250 (cmd_args
[nick_len
] == ':' || cmd_args
[nick_len
] == ',') && cmd_args
[nick_len
+1] == ' ') {
1251 cmd_args
+= nick_len
+ 2;
1256 static unsigned channel_enter
, channel_left
;
1257 if (channel_enter
== channel_left
) {
1259 channel_msg(b
, prefix
, host
, args
[0], cmd_args
, t
);
1264 cmd
= token(&cmd_args
, ' ');
1265 if (!chan
&& cmd_args
&& cmd_args
[0] == '#') {
1267 privmsg(b
, prefix
, "%s: { Error... }", prefix
);
1270 target
= token(&cmd_args
, ' ');
1273 c
= &commands
[strhash(cmd
) % elements(commands
)];
1274 if (c
->string
&& !strcasecmp(c
->string
, cmd
)) {
1275 if (c
->left
!= c
->enter
)
1276 privmsg(b
, target
, "Command %s is disabled because of a crash", c
->string
);
1277 else if (t
> 0 && t
< c
->disabled_until
) {
1278 int64_t delta
= c
->disabled_until
- t
;
1279 const char *str
= perty(&delta
);
1280 b
->writeline(b
, "NOTICE %s :Command %s is on timeout for the next %"PRIi64
" %s", prefix
, c
->string
, delta
, str
);
1281 } else if (c
->admin
&& !admin(host
)) {
1282 static const char *insults
[] = {
1283 "{ Rarity, I love you so much! }",
1284 "{ Rarity, have I ever told you that I love you? }",
1285 "{ Yes, I love my sister, Rarity. }",
1286 "{ Raaaaaaaaaaaaaarity. }",
1287 "{ You do not fool me, Rari...bot! }"
1289 privmsg(b
, target
, "%s: %s", prefix
, insults
[getrand() % elements(insults
)]);
1292 snprintf(c
->failed_command
, sizeof(*c
) - offsetof(struct command_hash
, failed_command
) - 1,
1293 "%s:%s (%s@%s) \"%s\" %s", target
, prefix
, ident
, host
, cmd
, cmd_args
);
1294 c
->cmd(b
, prefix
, host
, target
, cmd_args
);
1297 } else if (chan
== 2)
1298 privmsg(b
, target
, "%s: { I love you! }", prefix
);
1301 void command_hook(struct bio
*b
, const char *prefix
, const char *ident
, const char *host
,
1302 const char *command
, const char *const *args
, unsigned nargs
)
1306 if (!strcasecmp(command
, "NOTICE")) {
1307 if (nargs
< 2 || args
[0][0] == '#')
1310 b
->writeline(b
, "%s", args
[1]);
1312 fprintf(stderr
, "%s: %s\n", prefix
, args
[1]);
1313 } else if (!strcasecmp(command
, "JOIN")) {
1314 t
= get_time(b
, args
[0]);
1315 rss_check(b
, args
[0], t
);
1316 command_deliver(b
, prefix
, args
[0]);
1317 asprintf(&buf
, "%"PRIi64
",joining %s", t
, args
[0]);
1318 } else if (!strcasecmp(command
, "PART")) {
1319 t
= get_time(b
, args
[0]);
1320 rss_check(b
, args
[0], t
);
1321 asprintf(&buf
, "%"PRIi64
",leaving %s", t
, args
[0]);
1322 } else if (!strcasecmp(command
, "QUIT")) {
1323 t
= get_time(b
, NULL
);
1324 asprintf(&buf
, "%"PRIi64
",quitting with the message \"%s\"", t
, args
[0]);
1325 } else if (!strcasecmp(command
, "NICK")) {
1326 t
= get_time(b
, NULL
);
1327 asprintf(&buf
, "%"PRIi64
",changing nick to %s", t
, args
[0]);
1330 fprintf(stderr
, ":%s!%s%s %s", prefix
, ident
, host
, command
);
1331 for (i
= 0; i
< nargs
; ++i
)
1332 fprintf(stderr
, " %s", args
[i
]);
1333 fprintf(stderr
, "\n");
1336 update_seen(b
, buf
, prefix
);