switch to RcB2 build sys
[rofl0r-jsbot.git] / jsbot.c
blob87eef413d46e405f3f4e1a4f98325abb0b99a571
1 /* Copyright (C) 2017 rofl0r
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation; either version 2 of the License, or
6 (at your option) any later version.
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
13 You should have received a copy of the GNU General Public License along
14 with this program; if not, write to the Free Software Foundation, Inc.,
15 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 #include "rocksock.h"
18 #include "rsirc.h"
19 #include <string.h>
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <ctype.h>
23 #include <unistd.h>
24 #include <stdarg.h>
25 #include <assert.h>
26 #include <fcntl.h>
27 #include <sys/stat.h>
28 #include <time.h>
29 #include <mujs.h>
30 #pragma RcB2 LINK "-lmujs"
32 #include "simplecfg.c"
33 #ifndef TRANSACT_TIME
34 #define TRANSACT_TIME 500 * 1000
35 #endif
37 static const char hextab[] = "0123456789abcdef";
39 static char* decode(const char *in, char* out) {
40 const unsigned char* pi = (void*)in;
41 char *po = out;
42 while(*pi) {
43 if(*pi > 20 && *pi < 128) *po++ = *pi++;
44 else {
45 *po++='\\';
46 *po++='x';
47 *po++= hextab[(*pi & 0xf0) >> 4];
48 *po++= hextab[*pi++ & 15];
51 *po = 0;
52 return out;
55 static int split(const char *in, char sep, int splitcount, ...) {
56 va_list ap;
57 va_start(ap, splitcount);
58 int r = 1;
59 int i = 0;
60 const char *start = in;
61 while(i < splitcount-1 || !splitcount) {
62 char * out = va_arg(ap, char*);
63 if(!out && !splitcount) break;
64 size_t idx = 0;
65 while(start[idx] && start[idx] != sep) idx++;
66 memcpy(out, start, idx);
67 out[idx] = 0;
68 if(!start[idx]) { r = 0; goto ret; }
69 start += idx + 1;
70 i++;
72 if(splitcount) {
73 char * out = va_arg(ap, char*);
74 strcpy(out, start);
76 ret:
77 va_end(ap);
78 return r;
81 #define chk(X, ACTION) if(X) { \
82 rocksock_error_dprintf(2, s); \
83 ACTION; \
85 #define chk2(X, ACTION) if(X) { ACTION; }
86 /* "" */
89 static rocksock rs, *s = &rs;
90 static rsirc ircb, *irc = &ircb;
91 static rs_proxy proxies[2];
92 static int done_rs_init;
94 //PING :kornbluth.freenode.net
95 static int ping_handler(char *buf) {
96 char b[512];
97 snprintf(b, sizeof(b), "PONG %s", buf+1);
98 return rsirc_sendline(irc, b);
101 static char *own_nick;
102 static char *alternate_nick;
103 static char nick1[32];
104 static char nick2[32];
105 static char nick3[32];
106 static char proxy[512];
107 static char savefile[64];
108 static char host[256];
109 static int port;
110 static int use_ssl;
113 static int want_quit;
115 static js_State *J;
117 static void jscb_strings_command(const char* cmd, int args, ...) {
118 int i;
119 va_list ap;
120 va_start(ap, args);
121 js_getglobal(J, cmd);
122 js_pushnull(J);
123 for (i = 0; i < args; ++i) {
124 const char *a = va_arg(ap, const char *);
125 js_pushstring(J, a);
127 va_end(ap);
128 if (js_pcall(J, args))
129 dprintf(2, "error calling %s: %s\n", cmd, js_tostring(J, -1));
130 js_pop(J, 1);
133 static void jscb_onconnect(void) {
134 jscb_strings_command("connect", 0);
136 static void jscb_botnick(const char* nick) {
137 jscb_strings_command("botnick", 1, nick);
140 /* join: mask, cmd, chan
141 part: mask, cmd, chan, :"msg"
142 quit: mask, cmd, :msg
143 kick: mask, cmd, chan, whom, :msg
144 notice: mask, cmd, dest, :msg
145 privmsg: mask, cmd, dest, :msg
146 353: mask, cmd, who, =, chan, :names
147 366: mask, cmd, nick, chan, :End of /NAMES list.
148 nick: mask, cmd, :newname
150 enum action {
151 a_join = 0, a_part, a_quit, a_kick,
152 a_notice, a_privmsg, a_names, a_endnames,
153 a_nick,
155 /* number of IRC arguments after the obligatory mask, cmd */
156 static const char action_args[] = {
157 [a_join] = 1, [a_part] = 2,
158 [a_quit] = 1, [a_kick] = 3,
159 [a_notice] = 2, [a_privmsg] = 2,
160 [a_names] = 4, [a_endnames] = 3,
161 [a_nick] = 1,
163 /* we care only about up to 4 IRC arguments after mask, cmd */
164 static const char actionarg_msgadd[][4] = { /* this is to add 1 to the msg argument so the leading ':' is skipped */
165 [a_join] = "\0\0\0\0", [a_part] = "\0\1\0\0",
166 [a_quit] = "\1\0\0\0", [a_kick] = "\0\0\1\0",
167 [a_notice] = "\0\1\0\0", [a_privmsg] = "\0\1\0\0",
168 [a_names] = "\0\0\0\1", [a_endnames] = "\0\0\0\0",
169 [a_nick] = "\1\0\0\0",
172 /* we pass up to 5 arguments to dispatch functions.
173 argument 0 is always nick, which is derived from mask.
174 the original CMD argument is dropped (for example KICK or 366)
175 nick+mask always go together in that order, i.e. 0,1. */
176 static const char action_order[][5] = {
177 [a_join] ="\2\0\1\n\n", [a_part] = "\2\0\1\3\n",
178 [a_quit] ="\0\1\2\n\n", [a_kick] = "\0\1\3\2\4",
179 [a_notice]="\2\0\1\3\n", [a_privmsg]= "\2\0\1\3\n",
180 [a_names] ="\4\5\n\n\n", [a_endnames]="\3\2\n\n\n",
181 [a_nick] = "\0\2\n\n\n",
183 static const char dispatchtbl[][14]={
184 [a_join] = "joinhandler", [a_part] = "parthandler",
185 [a_quit] = "quithandler", [a_kick] = "kickhandler",
186 [a_notice] = "noticehandler", [a_privmsg] = "msghandler",
187 [a_names] = "nameshandler", [a_endnames] = "selfjoin", /*we use end of names as a signal that we're now in that chan*/
188 [a_nick] = "nickchange",
190 static unsigned action_dispatch_argcount(enum action a) {
191 unsigned i = 0;
192 while(i < sizeof(action_order[0]) && action_order[a][i]!='\n') i++;
193 return i;
195 static const char *action_arg(enum action a, int pos, const char* args[]) {
196 int l = action_order[a][pos];
197 return l == '\n' ? 0 : args[l];
199 static void action_dispatch(enum action a, const char* args[]) {
200 const char *a0 = action_arg(a, 0, args);
201 const char *a1 = action_arg(a, 1, args);
202 const char *a2 = action_arg(a, 2, args);
203 const char *a3 = action_arg(a, 3, args);
204 const char *a4 = action_arg(a, 4, args);
205 unsigned argcount = action_dispatch_argcount(a);
206 jscb_strings_command(dispatchtbl[a], argcount, a0, a1, a2, a3, a4);
208 static void prep_action_handler(char *buf, size_t cmdpos, enum action a) {
209 char nick[512];
210 char mask[512];
211 char cmd[16];
212 char a1[512];
213 char a2[512];
214 char a3[512];
215 char a4[512];
216 size_t i;
217 a1[0] = a2[0] = a3[0] = a4[0] = i = 0;
218 split(buf+1, ' ', 2+action_args[a], mask, cmd, a1, a2, a3, a4);
219 while(mask[i] != '!' && mask[i] != ' ') i++;
220 memcpy(nick, mask, i);
221 nick[i] = 0;
222 unsigned a1off = a1[0] ? actionarg_msgadd[a][0] : 0;
223 unsigned a2off = a2[0] ? actionarg_msgadd[a][1] : 0;
224 unsigned a3off = a3[0] ? actionarg_msgadd[a][2] : 0;
225 unsigned a4off = a4[0] ? actionarg_msgadd[a][3] : 0;
226 const char* args[6] = {nick, mask, a1+a1off, a2+a2off, a3+a3off, a4+a4off};
227 action_dispatch(a, args);
230 static int motd_finished() {
231 jscb_onconnect();
232 return 0;
235 void switch_names(void) {
236 char *t = own_nick;
237 own_nick = alternate_nick;
238 alternate_nick = t;
239 rsirc_sendlinef(irc, "NICK %s", own_nick);
242 static unsigned atou(const char *n) {
243 unsigned r = 0;
244 while(isspace(*n)) n++;
245 while(isdigit(*n)) r = r*10+(*(n++)-'0');
246 return r;
249 int read_cb(char* buf, size_t bufsize) {
250 if(buf[0] == ':') {
251 size_t i = 0, j;
252 while(!isspace(buf[i])) i++;
253 unsigned cmd = atou(buf+i);
254 switch(cmd) {
255 case 0: /* no number */
256 j = ++i;
257 while(!isspace(buf[j])) j++;
258 switch(j - i) {
259 case 4:
260 if(!memcmp(buf+i,"JOIN", 4))
261 prep_action_handler(buf, i, a_join);
262 else if(!memcmp(buf+i,"PART", 4))
263 prep_action_handler(buf, i, a_part);
264 else if(!memcmp(buf+i,"QUIT", 4))
265 prep_action_handler(buf, i, a_quit);
266 else if(!memcmp(buf+i,"KICK", 4))
267 prep_action_handler(buf, i, a_kick);
268 else if(!memcmp(buf+i,"NICK", 4))
269 prep_action_handler(buf, i, a_nick);
270 break;
271 case 7:
272 if(!memcmp(buf+i,"PRIVMSG", 7))
273 prep_action_handler(buf, i, a_privmsg);
274 break;
275 case 6:
276 if(!memcmp(buf+i,"NOTICE", 6))
277 prep_action_handler(buf, i, a_notice);
278 default:
279 break;
281 break;
282 /* status messages having the bot nickname in it, like:
283 :rajaniemi.freenode.net 255 foobot :I have 8369 clients and 1 servers */
284 case 5: case 250: case 251: case 252: case 253:
285 case 254: case 255: case 265: case 266: case 375:
286 i++;
287 while(!isspace(buf[i])) i++;
288 j = ++i;
289 while(!isspace(buf[i])) i++;
290 buf[i] = 0;
291 jscb_botnick(buf + j);
292 break;
293 case 376: motd_finished(); break;
294 //:kornbluth.freenode.net 433 * foobot :Nickname is already in use.
295 case 433:
296 if(alternate_nick) switch_names();
297 else {
298 rocksock_disconnect(s);
299 sleep(30);
301 break;
302 case 66:
303 if(i >= 512) dprintf(2, "caught canary bird\n");
304 default: break;
305 case 353:
306 prep_action_handler(buf, i, a_names);
307 break;
308 case 366:
309 prep_action_handler(buf, i, a_endnames);
310 break;
312 } else {
313 size_t i = 0;
314 while(!isspace(buf[i])) i++;
315 if(i == 4 && !memcmp(buf, "PING", 4)) ping_handler(buf + 5);
317 return 0;
320 char *cfgfilename;
321 static int load_cfg(void) {
322 FILE *cfg = cfg_open(cfgfilename);
323 if(!cfg) { perror("fopen"); return 0; }
324 cfg_getstr(cfg, "nick1", nick1, sizeof(nick1));
325 *nick2 = 0;
326 cfg_getstr(cfg, "nick2", nick2, sizeof(nick2));
327 cfg_getstr(cfg, "nick3", nick3, sizeof(nick3));
328 cfg_getstr(cfg, "proxy", proxy, sizeof(proxy));
329 int hostnr = (rand()%2)+1;
330 char hb[10];
331 again:
332 snprintf(hb, sizeof hb, "host%d", hostnr);
333 if(!cfg_getstr(cfg, hb, host, sizeof(host)) && hostnr == 2) { hostnr = 1; goto again; }
334 port = cfg_getint(cfg, "port");
335 use_ssl = cfg_getint(cfg, "ssl");
336 cfg_getstr(cfg, "savefile", savefile, sizeof savefile);
337 if(!savefile[0]) {
338 dprintf(2, "error: savefile config item not set!\n");
339 exit(1);
341 cfg_close(cfg);
342 own_nick = nick1;
343 if(*nick2) alternate_nick = nick2;
344 return 1;
347 int connect_it(void) {
348 load_cfg();
349 if(done_rs_init) {
350 rocksock_disconnect(s);
351 rocksock_clear(s);
353 rocksock_init(s, proxies);
354 done_rs_init = 1;
355 rocksock_set_timeout(s, 36000);
356 if(proxy[0])
357 chk(rocksock_add_proxy_fromstring(s, proxy), exit(1));
358 chk(rocksock_connect(s, host, port, use_ssl), goto err);
359 chk(rsirc_init(irc, s), goto err);
360 chk(rsirc_handshake(irc, host, own_nick, "foo"), goto err);
362 return 1;
363 err:
364 usleep(TRANSACT_TIME);
365 return 0;
368 static void js_sendline(js_State *J) {
369 const char* msg = js_tostring(J, 1);
370 int ret = rsirc_sendline(irc, msg);
371 js_pushnumber(J, ret);
374 static void js_privmsg(js_State *J) {
375 const char* chan = js_tostring(J, 1);
376 const const char* msg = js_tostring(J, 2);
377 int ret = rsirc_privmsg(irc, chan, msg);
378 js_pushnumber(J, ret);
381 static void js_errmsg(js_State *J) {
382 js_pushstring(J, rocksock_strerror(s));
385 static int reload_script() {
386 if (js_dofile(J, "ircbot.js"))
387 return 1;
388 js_gc(J, 0);
389 return 0;
392 static void js_reload(js_State *J) {
393 int ret = reload_script();
394 js_pushboolean(J, !ret);
397 static void js_disconnect(js_State *J) {
398 rocksock_disconnect(s);
399 js_pushundefined(J);
402 static void js_debugprint(js_State *J) {
403 const char* msg = js_tostring(J, 1);
404 dprintf(2, "%s\n", msg);
405 js_pushundefined(J);
408 static void js_writesettings(js_State *J) {
409 int fd, fail = 1; size_t l;
410 const char* contents = js_tostring(J, 1);
411 if( 0 == contents) goto err;
412 if( 0 == (l = strlen(contents))) goto err;
413 if(-1 == (fd = open(savefile, O_CREAT | O_WRONLY | O_TRUNC, 0660))) goto err;
414 if( l != write(fd, contents, l)) goto fderr;
415 fail = 0;
416 fderr:
417 close(fd);
418 err:
419 js_pushboolean(J, !fail);
422 static void js_readsettings(js_State *J) {
423 struct stat st;
424 char *contents;
425 int fd, failed = 1;
426 if(stat(savefile, &st)) goto err;
427 if(-1 == (fd = open(savefile, O_RDONLY))) goto err;
428 if( 0 == (contents = malloc(st.st_size + 1))) goto fderr;
429 if(st.st_size != read(fd, contents, st.st_size)) goto mallerr;
430 failed = 0;
431 contents[st.st_size] = 0;
432 js_pushstring(J, contents);
433 mallerr:
434 free(contents);
435 fderr:
436 close(fd);
437 err:
438 if(failed) js_pushundefined(J);
441 static int syntax() { dprintf(2, "need filename of cfg file\n"); return 1; }
442 int main(int argc, char** argv) {
443 if(argc <= 1) return syntax();
444 cfgfilename = argv[1];
445 srand(time(0));
446 if(!load_cfg()) return 1;
448 J = js_newstate(NULL, NULL, JS_STRICT);
450 js_newcfunction(J, js_privmsg, "privmsg", 2);
451 js_setglobal(J, "privmsg");
453 js_newcfunction(J, js_sendline, "send", 1);
454 js_setglobal(J, "send");
456 js_newcfunction(J, js_errmsg, "errmsg", 0);
457 js_setglobal(J, "errmsg");
459 js_newcfunction(J, js_reload, "reload", 0);
460 js_setglobal(J, "reload");
462 js_newcfunction(J, js_writesettings, "writesettings", 1);
463 js_setglobal(J, "writesettings");
465 js_newcfunction(J, js_readsettings, "readsettings", 0);
466 js_setglobal(J, "readsettings");
468 js_newcfunction(J, js_disconnect, "disconnect", 0);
469 js_setglobal(J, "disconnect");
471 js_newcfunction(J, js_debugprint, "debugprint", 1);
472 js_setglobal(J, "debugprint");
474 if(reload_script()) {
475 dprintf(2, "error: loading ircbot.js failed\n");
476 return 1;
479 size_t rcvd;
481 rocksock_init_ssl();
483 conn:
484 while(!connect_it());
486 char line[512+4] = {0};
487 static const char canary[4] = " 66\0";
488 time_t last_packet = time(0);
490 while(!want_quit) {
491 char decodebuf[512*4];
492 chk(rsirc_process(irc, line, &rcvd), goto conn);
493 if(rcvd) {
494 last_packet = time(0);
495 memcpy(line+512, canary, 4); /* protect against evil server */
496 dprintf(2, "LEN %zu - %s\n", rcvd, decode(line, decodebuf));
497 chk(read_cb(line, sizeof line), goto conn);
498 } else {
499 if(last_packet + 5*60 < time(0)) {
500 dprintf(2, "timeout occured, reconnecting\n");
501 goto conn;
505 usleep(10000);
508 rocksock_disconnect(s);
509 rocksock_free_ssl();
510 return 0;