call kickhandler hook as well
[rofl0r-jsbot.git] / jsbot.c
blob9c1940ff10ece65136cf6ef5c3c7a080931d225e
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 //RcB: 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_onjoinself(const char* chan, const char* nick) {
137 jscb_strings_command("selfjoin", 2, chan, nick);
139 static void jscb_botnick(const char* nick) {
140 jscb_strings_command("botnick", 1, nick);
142 static int on_joinself(const char* chan, const char* nick) {
143 jscb_onjoinself(chan, nick);
144 return 0;
147 /* join: mask, cmd, chan
148 part: mask, cmd, chan, :"msg"
149 quit: mask, cmd, :msg
150 kick: mask, cmd, chan, whom, :msg
151 notice: mask, cmd, dest, :msg
152 privmsg: mask, cmd, dest, :msg
154 enum action {
155 a_join = 0, a_part, a_quit, a_kick,
156 a_notice, a_privmsg
158 static const char action_args[] = {
159 [a_join] = 1, [a_part] = 2,
160 [a_quit] = 1, [a_kick] = 3,
161 [a_notice] = 2, [a_privmsg] = 2,
163 static const char actionarg_msgadd[][3] = { /* this is to add 1 to the msg argument so the leading ':' is skipped */
164 [a_join] = "\0\0\0", [a_part] = "\0\1\0",
165 [a_quit] = "\1\0\0", [a_kick] = "\0\0\1",
166 [a_notice] = "\0\1\0", [a_privmsg] = "\0\1\0",
169 /* nick+mask always go together in that order, i.e. 0,1. */
170 static const char action_order[][5] = {
171 [a_join] ="\2\0\1\n\n", [a_part] = "\2\0\1\3\n",
172 [a_quit] ="\0\1\2\n\n", [a_kick] = "\0\1\3\2\4",
173 [a_notice]="\2\0\1\3\n", [a_privmsg]= "\2\0\1\3\n",
175 static const char dispatchtbl[][14]={
176 [a_join] = "joinhandler", [a_part] = "parthandler",
177 [a_quit] = "quithandler", [a_kick] = "kickhandler",
178 [a_notice] = "noticehandler", [a_privmsg] = "msghandler",
180 static const char *action_arg(enum action a, int pos, const char* args[]) {
181 int l = action_order[a][pos];
182 return l == '\n' ? 0 : args[l];
184 static void action_dispatch(enum action a, const char* args[]) {
185 const char *a0 = action_arg(a, 0, args);
186 const char *a1 = action_arg(a, 1, args);
187 const char *a2 = action_arg(a, 2, args);
188 const char *a3 = action_arg(a, 3, args);
189 const char *a4 = action_arg(a, 4, args);
190 jscb_strings_command(dispatchtbl[a], action_args[a]+2, a0, a1, a2, a3, a4);
192 static void prep_action_handler(char *buf, size_t cmdpos, enum action a) {
193 char nick[512];
194 char mask[512];
195 char cmd[16];
196 char a1[512];
197 char a2[512];
198 char a3[512];
199 size_t i;
200 a1[0] = a2[0] = a3[0] = i = 0;
201 split(buf+1, ' ', 2+action_args[a], mask, cmd, a1, a2, a3);
202 while(mask[i] != '!' && mask[i] != ' ') i++;
203 memcpy(nick, mask, i);
204 nick[i] = 0;
205 unsigned a1off = a1[0] ? actionarg_msgadd[a][0] : 0;
206 unsigned a2off = a2[0] ? actionarg_msgadd[a][1] : 0;
207 unsigned a3off = a3[0] ? actionarg_msgadd[a][2] : 0;
208 const char* args[5] = {nick, mask, a1+a1off, a2+a2off, a3+a3off};
209 action_dispatch(a, args);
212 static int motd_finished() {
213 jscb_onconnect();
214 return 0;
217 void switch_names(void) {
218 char *t = own_nick;
219 own_nick = alternate_nick;
220 alternate_nick = t;
221 rsirc_sendlinef(irc, "NICK %s", own_nick);
224 int read_cb(char* buf, size_t bufsize) {
225 if(buf[0] == ':') {
226 size_t i = 0, j, k;
227 while(!isspace(buf[i])) i++;
228 int cmd = atoi(buf+i);
229 switch(cmd) {
230 case 0: /* no number */
231 j = ++i;
232 while(!isspace(buf[j])) j++;
233 switch(j - i) {
234 case 4:
235 if(!memcmp(buf+i,"JOIN", 4))
236 prep_action_handler(buf, i, a_join);
237 else if(!memcmp(buf+i,"PART", 4))
238 prep_action_handler(buf, i, a_part);
239 else if(!memcmp(buf+i,"QUIT", 4))
240 prep_action_handler(buf, i, a_quit);
241 else if(!memcmp(buf+i,"KICK", 4))
242 prep_action_handler(buf, i, a_kick);
243 break;
244 case 7:
245 if(!memcmp(buf+i,"PRIVMSG", 7))
246 prep_action_handler(buf, i, a_privmsg);
247 break;
248 case 6:
249 if(!memcmp(buf+i,"NOTICE", 6))
250 prep_action_handler(buf, i, a_notice);
251 default:
252 break;
254 break;
255 /* status messages having the bot nickname in it, like:
256 :rajaniemi.freenode.net 255 foobot :I have 8369 clients and 1 servers */
257 case 5: case 250: case 251: case 252: case 253:
258 case 254: case 255: case 265: case 266: case 375:
259 i++;
260 while(!isspace(buf[i])) i++;
261 j = ++i;
262 while(!isspace(buf[i])) i++;
263 buf[i] = 0;
264 jscb_botnick(buf + j);
265 break;
266 case 376: motd_finished(); break;
267 //:kornbluth.freenode.net 433 * foobot :Nickname is already in use.
268 case 433:
269 if(alternate_nick) switch_names();
270 else {
271 rocksock_disconnect(s);
272 sleep(30);
274 break;
275 default: break;
276 case 366:
277 while(!isspace(buf[++i]));
278 i++;
279 k = i;
280 while(!isspace(buf[++i]));
281 buf[i] = 0;
282 j = ++i;
283 while(!isspace(buf[++i]));
284 buf[i] = 0;
285 on_joinself(buf + j, buf + k);
286 break;
288 } else {
289 size_t i = 0;
290 while(!isspace(buf[i])) i++;
291 if(i == 4 && !memcmp(buf, "PING", 4)) ping_handler(buf + 5);
293 return 0;
296 char *cfgfilename;
297 static int load_cfg(void) {
298 FILE *cfg = cfg_open(cfgfilename);
299 if(!cfg) { perror("fopen"); return 0; }
300 cfg_getstr(cfg, "nick1", nick1, sizeof(nick1));
301 *nick2 = 0;
302 cfg_getstr(cfg, "nick2", nick2, sizeof(nick2));
303 cfg_getstr(cfg, "nick3", nick3, sizeof(nick3));
304 cfg_getstr(cfg, "proxy", proxy, sizeof(proxy));
305 int hostnr = (rand()%2)+1;
306 char hb[10];
307 again:
308 snprintf(hb, sizeof hb, "host%d", hostnr);
309 if(!cfg_getstr(cfg, hb, host, sizeof(host)) && hostnr == 2) { hostnr = 1; goto again; }
310 port = cfg_getint(cfg, "port");
311 use_ssl = cfg_getint(cfg, "ssl");
312 cfg_getstr(cfg, "savefile", savefile, sizeof savefile);
313 if(!savefile[0]) {
314 dprintf(2, "error: savefile config item not set!\n");
315 exit(1);
317 cfg_close(cfg);
318 own_nick = nick1;
319 if(*nick2) alternate_nick = nick2;
320 return 1;
323 int connect_it(void) {
324 load_cfg();
325 if(done_rs_init) {
326 rocksock_disconnect(s);
327 rocksock_clear(s);
329 rocksock_init(s, proxies);
330 done_rs_init = 1;
331 rocksock_set_timeout(s, 36000);
332 if(proxy[0])
333 chk(rocksock_add_proxy_fromstring(s, proxy), exit(1));
334 chk(rocksock_connect(s, host, port, use_ssl), goto err);
335 chk(rsirc_init(irc, s), goto err);
336 chk(rsirc_handshake(irc, host, own_nick, "foo"), goto err);
338 return 1;
339 err:
340 usleep(TRANSACT_TIME);
341 return 0;
344 static void js_sendline(js_State *J) {
345 const char* msg = js_tostring(J, 1);
346 int ret = rsirc_sendline(irc, msg);
347 js_pushnumber(J, ret);
350 static void js_privmsg(js_State *J) {
351 const char* chan = js_tostring(J, 1);
352 const const char* msg = js_tostring(J, 2);
353 int ret = rsirc_privmsg(irc, chan, msg);
354 js_pushnumber(J, ret);
357 static void js_errmsg(js_State *J) {
358 js_pushstring(J, rocksock_strerror(s));
361 static int reload_script() {
362 if (js_dofile(J, "ircbot.js"))
363 return 1;
364 js_gc(J, 0);
365 return 0;
368 static void js_reload(js_State *J) {
369 int ret = reload_script();
370 js_pushboolean(J, !ret);
373 static void js_disconnect(js_State *J) {
374 rocksock_disconnect(s);
375 js_pushundefined(J);
378 static void js_debugprint(js_State *J) {
379 const char* msg = js_tostring(J, 1);
380 dprintf(2, "%s\n", msg);
381 js_pushundefined(J);
384 static void js_writesettings(js_State *J) {
385 int fd, fail = 1; size_t l;
386 const char* contents = js_tostring(J, 1);
387 if( 0 == contents) goto err;
388 if( 0 == (l = strlen(contents))) goto err;
389 if(-1 == (fd = open(savefile, O_CREAT | O_WRONLY | O_TRUNC, 0660))) goto err;
390 if( l != write(fd, contents, l)) goto fderr;
391 fail = 0;
392 fderr:
393 close(fd);
394 err:
395 js_pushboolean(J, !fail);
398 static void js_readsettings(js_State *J) {
399 struct stat st;
400 char *contents;
401 int fd, failed = 1;
402 if(stat(savefile, &st)) goto err;
403 if(-1 == (fd = open(savefile, O_RDONLY))) goto err;
404 if( 0 == (contents = malloc(st.st_size + 1))) goto fderr;
405 if(st.st_size != read(fd, contents, st.st_size)) goto mallerr;
406 failed = 0;
407 contents[st.st_size] = 0;
408 js_pushstring(J, contents);
409 mallerr:
410 free(contents);
411 fderr:
412 close(fd);
413 err:
414 if(failed) js_pushundefined(J);
417 static int syntax() { dprintf(2, "need filename of cfg file\n"); return 1; }
418 int main(int argc, char** argv) {
419 if(argc <= 1) return syntax();
420 cfgfilename = argv[1];
421 srand(time(0));
422 if(!load_cfg()) return 1;
424 J = js_newstate(NULL, NULL, JS_STRICT);
426 js_newcfunction(J, js_privmsg, "privmsg", 2);
427 js_setglobal(J, "privmsg");
429 js_newcfunction(J, js_sendline, "send", 1);
430 js_setglobal(J, "send");
432 js_newcfunction(J, js_errmsg, "errmsg", 0);
433 js_setglobal(J, "errmsg");
435 js_newcfunction(J, js_reload, "reload", 0);
436 js_setglobal(J, "reload");
438 js_newcfunction(J, js_writesettings, "writesettings", 1);
439 js_setglobal(J, "writesettings");
441 js_newcfunction(J, js_readsettings, "readsettings", 0);
442 js_setglobal(J, "readsettings");
444 js_newcfunction(J, js_disconnect, "disconnect", 0);
445 js_setglobal(J, "disconnect");
447 js_newcfunction(J, js_debugprint, "debugprint", 1);
448 js_setglobal(J, "debugprint");
450 if(reload_script()) {
451 dprintf(2, "error: loading ircbot.js failed\n");
452 return 1;
455 size_t rcvd;
457 rocksock_init_ssl();
459 conn:
460 while(!connect_it());
462 while(!want_quit) {
463 char line[512];
464 char decodebuf[512*4];
465 chk(rsirc_process(irc, line, &rcvd), goto conn);
466 if(rcvd) {
467 dprintf(2, "LEN %zu - %s\n", rcvd, decode(line, decodebuf));
468 chk(read_cb(line, sizeof line), goto conn);
471 usleep(10000);
474 rocksock_disconnect(s);
475 rocksock_free_ssl();
476 return 0;