Download
jcs
/wallops
/irc.c
(View History)
jcs focusable: Let each dictate the minimum sleep time for WaitNextEvent | Latest amendment: 28 on 2022-02-10 |
1 | /* |
2 | * Copyright (c) 2021-2022 joshua stein <jcs@jcs.org> |
3 | * |
4 | * Permission to use, copy, modify, and distribute this software for any |
5 | * purpose with or without fee is hereby granted, provided that the above |
6 | * copyright notice and this permission notice appear in all copies. |
7 | * |
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
15 | */ |
16 | |
17 | #include <ctype.h> |
18 | #include <string.h> |
19 | #include <stdarg.h> |
20 | |
21 | #include "chatter.h" |
22 | #include "irc.h" |
23 | |
24 | void irc_init(struct chatter *chatter); |
25 | short irc_verify_state(struct chatter *chatter, int state); |
26 | short irc_recv(struct chatter *chatter); |
27 | size_t irc_send(struct chatter *chatter, char *line, size_t size); |
28 | size_t irc_printf(struct chatter *chatter, const char *format, ...); |
29 | char * irc_get_line(struct chatter *chatter, size_t *retsize); |
30 | struct irc_user * irc_parse_user(char *str); |
31 | bool irc_can_send(struct chatter *chatter); |
32 | void irc_login(struct chatter *chatter); |
33 | bool irc_process_server(struct chatter *chatter); |
34 | void irc_set_active_channel(struct chatter *chatter, char *channame); |
35 | void irc_parse_names(struct chatter *chatter, char *line); |
36 | void irc_add_nick_to_channel(struct chatter *chatter, char *nick, |
37 | short flags); |
38 | void irc_remove_nick_from_channel(struct chatter *chatter, char *nick); |
39 | void irc_change_user_nick(struct chatter *chatter, struct irc_user *user, |
40 | char *nick); |
41 | void irc_parse_channel_mode_change(struct chatter *chatter, char *mode, |
42 | char *args); |
43 | |
44 | void |
45 | irc_process(struct chatter *chatter) |
46 | { |
47 | short oldstate; |
48 | |
49 | if (chatter->irc_state >= IRC_STATE_CONNECTED) |
50 | irc_recv(chatter); |
51 | |
52 | oldstate = chatter->irc_state; |
53 | |
54 | switch (chatter->irc_state) { |
55 | case IRC_STATE_UNINITED: |
56 | chatter_printf(chatter, "$B***$0 Welcome to %s", PROGRAM_NAME); |
57 | chatter->irc_state = IRC_STATE_CONNECTING; |
58 | irc_init(chatter); |
59 | break; |
60 | case IRC_STATE_CONNECTED: |
61 | irc_login(chatter); |
62 | break; |
63 | case IRC_STATE_IDLE: |
64 | irc_process_server(chatter); |
65 | break; |
66 | } |
67 | |
68 | if (chatter->irc_state != oldstate) |
69 | chatter_update_titlebar(chatter); |
70 | } |
71 | |
72 | short |
73 | irc_verify_state(struct chatter *chatter, int state) |
74 | { |
75 | if (chatter->irc_state != state) { |
76 | warn("Bad IRC state (in %d, expected %d)", chatter->irc_state, |
77 | state); |
78 | chatter->irc_state = IRC_STATE_DISCONNECTED; |
79 | return 0; |
80 | } |
81 | |
82 | return 1; |
83 | } |
84 | |
85 | void |
86 | irc_init(struct chatter *chatter) |
87 | { |
88 | char ip_str[] = "255.255.255.255"; |
89 | char *retname; |
90 | ip_addr ip = 0, local_ip = 0; |
91 | ip_port port, local_port = 0; |
92 | short err; |
93 | |
94 | if (!irc_verify_state(chatter, IRC_STATE_CONNECTING)) |
95 | return; |
96 | |
97 | chatter_printf(chatter, "$B***$0 Connecting to $B%s:%d$0...", |
98 | chatter->irc_hostname, chatter->irc_port); |
99 | |
100 | if ((err = _TCPCreate(&chatter->irc_send_pb, &chatter->irc_stream, |
101 | (Ptr)chatter->irc_tcp_buf, sizeof(chatter->irc_tcp_buf), |
102 | nil, nil, nil, false)) != noErr) { |
103 | chatter_printf(chatter, "%B*!* TCPCreate failed: %d$0", err); |
104 | chatter->irc_state = IRC_STATE_DISCONNECTED; |
105 | return; |
106 | } |
107 | |
108 | if ((err = TCPResolveName(&chatter->irc_hostname, &ip)) != 0) { |
109 | chatter_printf(chatter, "$B*!* Couldn't resolve host %s (%d)$0", |
110 | chatter->irc_hostname, err); |
111 | chatter->irc_state = IRC_STATE_DISCONNECTED; |
112 | return; |
113 | } |
114 | |
115 | long2ip(ip, ip_str); |
116 | |
117 | if ((err = _TCPActiveOpen(&chatter->irc_send_pb, chatter->irc_stream, |
118 | ip, chatter->irc_port, &local_ip, &local_port, nil, nil, |
119 | false)) != noErr) { |
120 | chatter_printf(chatter, "$B*!* Failed connecting to %s (%s) " |
121 | "port %d: %d$0", chatter->irc_hostname, ip_str, |
122 | chatter->irc_port, err); |
123 | chatter->irc_state = IRC_STATE_DISCONNECTED; |
124 | return; |
125 | } |
126 | |
127 | chatter_printf(chatter, "$B***$0 Connected to $B%s$0 (%s) port %d", |
128 | chatter->irc_hostname, ip_str, chatter->irc_port); |
129 | |
130 | chatter->irc_state = IRC_STATE_CONNECTED; |
131 | } |
132 | |
133 | void |
134 | irc_abort(struct chatter *chatter) |
135 | { |
136 | chatter->irc_state = IRC_STATE_DISCONNECTED; |
137 | irc_set_active_channel(chatter, NULL); |
138 | |
139 | _TCPClose(&chatter->irc_close_pb, chatter->irc_stream, nil, nil, |
140 | false); |
141 | _TCPAbort(&chatter->irc_close_pb, chatter->irc_stream, nil, nil, |
142 | false); |
143 | _TCPRelease(&chatter->irc_close_pb, chatter->irc_stream, nil, nil, |
144 | false); |
145 | |
146 | chatter->irc_ibuflen = 0; |
147 | } |
148 | |
149 | void |
150 | irc_close(struct chatter *chatter) |
151 | { |
152 | if (chatter->irc_state == IRC_STATE_DISCONNECTED) |
153 | return; |
154 | |
155 | chatter_printf(chatter, "$B***$0 Disconnecting"); |
156 | if (chatter->irc_state == IRC_STATE_IDLE) |
157 | irc_printf(chatter, "QUIT :Cmd+Q\r\n"); |
158 | irc_abort(chatter); |
159 | } |
160 | |
161 | short |
162 | irc_recv(struct chatter *chatter) |
163 | { |
164 | unsigned short rlen; |
165 | short error, rerror, n; |
166 | |
167 | error = _TCPStatus(&chatter->irc_rcv_pb, chatter->irc_stream, |
168 | &chatter->irc_status_pb, nil, nil, false); |
169 | |
170 | if (chatter->irc_status_pb.amtUnreadData > 0 && |
171 | chatter->irc_ibuflen < sizeof(chatter->irc_ibuf)) { |
172 | rlen = chatter->irc_status_pb.amtUnreadData; |
173 | if (chatter->irc_ibuflen + rlen > sizeof(chatter->irc_ibuf)) |
174 | rlen = sizeof(chatter->irc_ibuf) - chatter->irc_ibuflen; |
175 | |
176 | rerror = _TCPRcv(&chatter->irc_rcv_pb, chatter->irc_stream, |
177 | (Ptr)(chatter->irc_ibuf + chatter->irc_ibuflen), &rlen, nil, nil, |
178 | false); |
179 | if (rerror) { |
180 | chatter_printf(chatter, "$B*!* TCPRecv failed: %d$0", error); |
181 | irc_abort(chatter); |
182 | return 0; |
183 | } |
184 | |
185 | chatter->irc_ibuflen += rlen; |
186 | } |
187 | |
188 | if (error) { |
189 | /* let already-consumed buffer finish processing */ |
190 | while (irc_process_server(chatter)) |
191 | ; |
192 | chatter_printf(chatter, "$B*!* TCPStatus failed: %d$0", error); |
193 | irc_abort(chatter); |
194 | return 0; |
195 | } |
196 | |
197 | return rlen; |
198 | } |
199 | |
200 | bool |
201 | irc_can_send(struct chatter *chatter) |
202 | { |
203 | return (chatter->irc_send_pb.ioResult <= 0); |
204 | } |
205 | |
206 | size_t |
207 | irc_send(struct chatter *chatter, char *line, size_t size) |
208 | { |
209 | short error; |
210 | |
211 | if (size > sizeof(chatter->irc_obuf)) |
212 | panic("irc_send: too much data %lu", size); |
213 | |
214 | if (chatter->irc_state == IRC_STATE_DISCONNECTED) |
215 | return 0; |
216 | |
217 | while (!irc_can_send(chatter)) |
218 | SystemTask(); |
219 | |
220 | if (chatter->irc_send_pb.ioResult < 0) { |
221 | chatter_printf(chatter, "$B*!* TCPSend failed: %d$0", |
222 | chatter->irc_send_pb.ioResult); |
223 | irc_abort(chatter); |
224 | return 0; |
225 | } |
226 | |
227 | memcpy(&chatter->irc_obuf, line, size); |
228 | |
229 | /* |
230 | * _TCPSend only knows how many wds pointers were passed in when it |
231 | * reads the next one and its pointer is zero (or size is zero?), so |
232 | * even though we're only sending one wds, memory after wds[0] |
233 | * has to be zeroed out. |
234 | */ |
235 | memset(&chatter->irc_wds, 0, sizeof(chatter->irc_wds)); |
236 | chatter->irc_wds[0].ptr = (Ptr)&chatter->irc_obuf; |
237 | chatter->irc_wds[0].length = size; |
238 | |
239 | error = _TCPSend(&chatter->irc_send_pb, chatter->irc_stream, |
240 | chatter->irc_wds, nil, nil, true); |
241 | if (error) { |
242 | chatter_printf(chatter, "$B*!* TCPSend failed: %d$0", error); |
243 | irc_abort(chatter); |
244 | return 0; |
245 | } |
246 | |
247 | return size; |
248 | } |
249 | |
250 | size_t |
251 | irc_printf(struct chatter *chatter, const char *format, ...) |
252 | { |
253 | static char buf[512]; |
254 | va_list argptr; |
255 | size_t len = 0; |
256 | |
257 | va_start(argptr, format); |
258 | len = vsnprintf(buf, sizeof(buf), format, argptr); |
259 | va_end(argptr); |
260 | |
261 | if (len > sizeof(buf)) { |
262 | warn("irc_printf overflow!"); |
263 | len = sizeof(buf); |
264 | buf[len - 1] = '\0'; |
265 | } |
266 | |
267 | irc_send(chatter, buf, len); |
268 | |
269 | return len; |
270 | } |
271 | |
272 | void |
273 | irc_login(struct chatter *chatter) |
274 | { |
275 | size_t len; |
276 | |
277 | if (!irc_verify_state(chatter, IRC_STATE_CONNECTED)) |
278 | return; |
279 | |
280 | if (!irc_can_send(chatter)) |
281 | return; |
282 | |
283 | if (chatter->irc_password && chatter->irc_password[0]) { |
284 | len = snprintf(chatter->irc_line, sizeof(chatter->irc_line), |
285 | "PASS %s\r\n", chatter->irc_password); |
286 | irc_send(chatter, chatter->irc_line, len); |
287 | } |
288 | |
289 | len = snprintf(chatter->irc_line, sizeof(chatter->irc_line), |
290 | "NICK %s\r\n", chatter->irc_nick); |
291 | irc_send(chatter, chatter->irc_line, len); |
292 | |
293 | len = snprintf(chatter->irc_line, sizeof(chatter->irc_line), |
294 | "USER %s 8 * :%s\r\n", chatter->irc_ident, chatter->irc_realname); |
295 | irc_send(chatter, chatter->irc_line, len); |
296 | |
297 | if (chatter->irc_channel_autojoin && chatter->irc_channel_autojoin[0]) { |
298 | len = snprintf(chatter->irc_line, sizeof(chatter->irc_line), |
299 | "JOIN %s\r\n", chatter->irc_channel_autojoin); |
300 | irc_send(chatter, chatter->irc_line, len); |
301 | } |
302 | |
303 | chatter->irc_state = IRC_STATE_IDLE; |
304 | } |
305 | |
306 | struct irc_user * |
307 | irc_parse_user(char *str) |
308 | { |
309 | static struct irc_user parsed_user; |
310 | short ret; |
311 | |
312 | memset(&parsed_user, 0, sizeof(parsed_user)); |
313 | ret = sscanf(str, "%[^!]!%[^@]@%s", &parsed_user.nick, |
314 | &parsed_user.username, &parsed_user.hostname); |
315 | if (ret != 3) { |
316 | /* probably just a hostname */ |
317 | strlcpy(parsed_user.nick, str, sizeof(parsed_user.nick)); |
318 | strlcpy(parsed_user.username, str, sizeof(parsed_user.username)); |
319 | strlcpy(parsed_user.hostname, str, sizeof(parsed_user.hostname)); |
320 | } |
321 | return &parsed_user; |
322 | } |
323 | |
324 | char * |
325 | irc_get_line(struct chatter *chatter, size_t *retsize) |
326 | { |
327 | size_t n; |
328 | |
329 | if (chatter->irc_ibuflen == 0) { |
330 | if (retsize != NULL) |
331 | *retsize = 0; |
332 | return NULL; |
333 | } |
334 | |
335 | for (n = 0; n < chatter->irc_ibuflen - 1; n++) { |
336 | if (chatter->irc_ibuf[n] != '\r') |
337 | continue; |
338 | if (chatter->irc_ibuf[n + 1] != '\n') |
339 | continue; |
340 | |
341 | memcpy(chatter->irc_line, chatter->irc_ibuf, n + 1); |
342 | chatter->irc_line[n] = '\0'; |
343 | if (retsize != NULL) |
344 | *retsize = n + 1; |
345 | |
346 | if (n == chatter->irc_ibuflen - 2) { |
347 | chatter->irc_ibuflen = 0; |
348 | } else { |
349 | chatter->irc_ibuflen -= n + 2; |
350 | if (chatter->irc_ibuflen < 0) |
351 | panic("bogus irc_ibuflen %d", chatter->irc_ibuflen); |
352 | memmove(chatter->irc_ibuf, chatter->irc_ibuf + n + 2, |
353 | chatter->irc_ibuflen); |
354 | } |
355 | return chatter->irc_line; |
356 | } |
357 | |
358 | return NULL; |
359 | } |
360 | |
361 | bool |
362 | irc_process_server(struct chatter *chatter) |
363 | { |
364 | struct irc_msg msg; |
365 | struct irc_user *user; |
366 | char *line, *word; |
367 | size_t size, n, lastbreak; |
368 | short state, curarg; |
369 | |
370 | line = irc_get_line(chatter, &size); |
371 | if (size == 0 || line == NULL) |
372 | return false; |
373 | |
374 | memset(&msg, 0, sizeof(msg)); |
375 | |
376 | if (strstr(line, "services")) |
377 | msg.code = 1; |
378 | |
379 | word = strsep(&line, " "); |
380 | |
381 | /* extract source before command */ |
382 | if (word[0] == ':') { |
383 | /* ":server.name 001 jcs :Hi" -> msg.source=server.name */ |
384 | strlcpy(msg.source, word + 1, sizeof(msg.source)); |
385 | word = strsep(&line, " "); |
386 | } |
387 | |
388 | /* code or a command name */ |
389 | if (isdigit(word[0]) && isdigit(word[1]) && isdigit(word[2])) |
390 | /* ":server.name 001 jcs :Hi" -> msg.code=1 */ |
391 | msg.code = atoi(word); |
392 | else |
393 | /* "PING :server.name" -> msg.cmd=PING */ |
394 | /* ":jcs!~jcs@jcs JOIN #wallops" -> msg.cmd=JOIN */ |
395 | strlcpy(msg.cmd, word, sizeof(msg.cmd)); |
396 | |
397 | /* parameters, put into args until we see one starting with : */ |
398 | curarg = 0; |
399 | while (line != NULL && line[0] != '\0') { |
400 | if (line[0] == ':') { |
401 | strlcpy(msg.msg, line + 1, sizeof(msg.msg)); |
402 | |
403 | /* some irc servers put the first arg as the message */ |
404 | if (curarg == 0) |
405 | strlcpy(msg.arg[0], line + 1, sizeof(msg.arg[0])); |
406 | break; |
407 | } |
408 | |
409 | word = strsep(&line, " "); |
410 | if (word == NULL) { |
411 | strlcpy(msg.msg, line, sizeof(msg.msg)); |
412 | break; |
413 | } |
414 | |
415 | strlcpy(msg.arg[curarg], word, sizeof(msg.arg[curarg])); |
416 | curarg++; |
417 | |
418 | if (curarg >= IRC_MSG_MAX_ARGS) { |
419 | /* just stick the rest in msg */ |
420 | strlcpy(msg.msg, line, sizeof(msg.msg)); |
421 | break; |
422 | } |
423 | } |
424 | |
425 | if (msg.cmd[0]) { |
426 | /* this one will fire most often, keep at the top */ |
427 | if (strcmp(msg.cmd, "PRIVMSG") == 0) { |
428 | user = irc_parse_user(msg.source); |
429 | size = strlen(msg.msg); |
430 | |
431 | if (msg.msg[0] == '\1' && msg.msg[size - 1] == '\1') { |
432 | /* CTCP or a /me action */ |
433 | msg.msg[size - 1] = '\0'; |
434 | if (strncmp(msg.msg + 1, "ACTION", 6) == 0) { |
435 | chatter_printf(chatter, "* %s$/ %s", user->nick, |
436 | msg.msg + 8); |
437 | } else if (strncmp(msg.msg + 1, "VERSION", 7) == 0) { |
438 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 " |
439 | "requested CTCP $BVERSION$0 from $B%s$0", |
440 | user->nick, user->username, user->hostname, |
441 | msg.arg[0]); |
442 | |
443 | /* only respond if it was sent directly to us */ |
444 | if (strcmp(msg.arg[0], chatter->irc_nick) == 0) { |
445 | irc_printf(chatter, |
446 | "NOTICE %s :\1VERSION %s %s on a %s\1\r\n", |
447 | user->nick, PROGRAM_NAME, get_version(false), |
448 | gestalt_machine_type()); |
449 | } |
450 | } else { |
451 | goto unknown; |
452 | } |
453 | } else if (strcmp(msg.arg[0], chatter->irc_nick) == 0) { |
454 | chatter_printf(chatter, "$B[$0%s$B($0%s@%s$B)]$0$/ %s", |
455 | user->nick, user->username, user->hostname, msg.msg); |
456 | } else if (chatter->irc_channel && |
457 | strcmp(msg.arg[0], chatter->irc_channel->name) == 0) { |
458 | size = strlen(chatter->irc_nick); |
459 | if (strncmp(msg.msg, chatter->irc_nick, size) == 0 && |
460 | (msg.msg[size] == ' ' || msg.msg[size] == ':' || |
461 | msg.msg[size] == ',' || msg.msg[size] == '\0')) { |
462 | /* highlight message */ |
463 | chatter_printf(chatter, "<$U%s$0>$/ %s", user->nick, |
464 | msg.msg); |
465 | if (!chatter->focusable->visible) |
466 | notify(); |
467 | } else |
468 | chatter_printf(chatter, "$/<%s> %s", user->nick, |
469 | msg.msg); |
470 | } else |
471 | chatter_printf(chatter, "$B[%s($0%s@%s$B):%s]$0$/ %s", |
472 | user->nick, user->username, user->hostname, msg.arg[0], |
473 | msg.msg); |
474 | return true; |
475 | } |
476 | if (strcmp(msg.cmd, "ERROR") == 0) { |
477 | chatter_printf(chatter, "$B*!* Disconnected$0 from $B%s$0:$/ %s", |
478 | chatter->irc_hostname, msg.msg); |
479 | irc_abort(chatter); |
480 | return true; |
481 | } |
482 | if (strcmp(msg.cmd, "JOIN") == 0) { |
483 | user = irc_parse_user(msg.source); |
484 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has joined " |
485 | "$B%s$0", user->nick, user->username, user->hostname, |
486 | msg.arg[0]); |
487 | if (strcmp(user->nick, chatter->irc_nick) == 0) |
488 | irc_set_active_channel(chatter, msg.arg[0]); |
489 | else |
490 | irc_add_nick_to_channel(chatter, user->nick, 0); |
491 | return true; |
492 | } |
493 | if (strcmp(msg.cmd, "KICK") == 0) { |
494 | user = irc_parse_user(msg.source); |
495 | chatter_printf(chatter, "$B*** %s$0 was kicked " |
496 | "from $B%s$0 by $B%s%0:$/ %s", msg.arg[1], msg.arg[0], |
497 | user->nick, msg.msg); |
498 | if (strcmp(msg.arg[1], chatter->irc_nick) == 0) |
499 | irc_set_active_channel(chatter, NULL); |
500 | else |
501 | irc_remove_nick_from_channel(chatter, msg.arg[1]); |
502 | return true; |
503 | } |
504 | if (strcmp(msg.cmd, "KILL") == 0) { |
505 | user = irc_parse_user(msg.source); |
506 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has been " |
507 | "killed:$/ %s", user->nick, user->username, user->hostname, |
508 | msg.msg); |
509 | if (strcmp(user->nick, chatter->irc_nick) == 0) |
510 | irc_set_active_channel(chatter, NULL); |
511 | else |
512 | irc_remove_nick_from_channel(chatter, user->nick); |
513 | return true; |
514 | } |
515 | if (strcmp(msg.cmd, "MODE") == 0) { |
516 | if (strcmp(msg.arg[0], chatter->irc_nick) == 0) |
517 | chatter_printf(chatter, "$B***$0 Mode change ($B%s$0) " |
518 | "for user $B%s$0", msg.msg, msg.arg[0]); |
519 | else { |
520 | user = irc_parse_user(msg.source); |
521 | /* concatenate nicks */ |
522 | msg.msg[0] = '\0'; |
523 | if (msg.arg[2][0] != '\0') |
524 | strlcat(msg.msg, msg.arg[2], sizeof(msg.msg)); |
525 | if (msg.arg[3][0] != '\0') { |
526 | strlcat(msg.msg, " ", sizeof(msg.msg)); |
527 | strlcat(msg.msg, msg.arg[3], sizeof(msg.msg)); |
528 | } |
529 | if (msg.arg[4][0] != '\0') { |
530 | strlcat(msg.msg, " ", sizeof(msg.msg)); |
531 | strlcat(msg.msg, msg.arg[4], sizeof(msg.msg)); |
532 | } |
533 | if (msg.arg[5][0] != '\0') { |
534 | strlcat(msg.msg, " ", sizeof(msg.msg)); |
535 | strlcat(msg.msg, msg.arg[5], sizeof(msg.msg)); |
536 | } |
537 | chatter_printf(chatter, "$B***$0 Mode change ($B%s %s$0) " |
538 | "on $B%s$0 by $B%s%0", msg.arg[1], msg.msg, msg.arg[0], |
539 | user->nick); |
540 | irc_parse_channel_mode_change(chatter, msg.arg[1], |
541 | msg.msg); |
542 | } |
543 | return true; |
544 | } |
545 | if (strcmp(msg.cmd, "NICK") == 0) { |
546 | user = irc_parse_user(msg.source); |
547 | chatter_printf(chatter, "$B*** %s$0 is now known as $B%s$0", |
548 | user->nick, msg.msg); |
549 | irc_change_user_nick(chatter, user, msg.msg); |
550 | return true; |
551 | } |
552 | if (strcmp(msg.cmd, "NOTICE") == 0) { |
553 | if (strncmp(msg.msg, "*** ", 4) == 0) |
554 | chatter_printf(chatter, "$B***$0 $/%s", msg.msg + 4); |
555 | else |
556 | chatter_printf(chatter, "$/%s", msg.msg); |
557 | return true; |
558 | } |
559 | if (strcmp(msg.cmd, "PART") == 0) { |
560 | user = irc_parse_user(msg.source); |
561 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has left " |
562 | "$B%s$0", user->nick, user->username, user->hostname, |
563 | msg.arg[0]); |
564 | if (strcmp(user->nick, chatter->irc_nick) == 0) |
565 | irc_set_active_channel(chatter, NULL); |
566 | else |
567 | irc_remove_nick_from_channel(chatter, user->nick); |
568 | return true; |
569 | } |
570 | if (strcmp(msg.cmd, "PING") == 0) { |
571 | irc_printf(chatter, "PONG :%s\r\n", msg.msg); |
572 | return true; |
573 | } |
574 | if (strcmp(msg.cmd, "QUIT") == 0) { |
575 | user = irc_parse_user(msg.source); |
576 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has quit:$/ %s", |
577 | user->nick, user->username, user->hostname, msg.msg); |
578 | if (strcmp(user->nick, chatter->irc_nick) == 0) |
579 | irc_set_active_channel(chatter, NULL); |
580 | else |
581 | irc_remove_nick_from_channel(chatter, user->nick); |
582 | return true; |
583 | } |
584 | goto unknown; |
585 | } |
586 | |
587 | switch (msg.code) { |
588 | case 0: |
589 | goto unknown; |
590 | case 1: |
591 | case 2: |
592 | /* welcome banners */ |
593 | goto print_msg; |
594 | case 3: |
595 | case 4: |
596 | case 5: |
597 | case 250: |
598 | case 251: |
599 | case 252: |
600 | case 253: |
601 | case 254: |
602 | case 255: |
603 | case 265: |
604 | case 266: |
605 | /* server stats, unhelpful */ |
606 | return true; |
607 | case 307: |
608 | /* WHOIS regnick */ |
609 | chatter_printf(chatter, "$B*** |$0 Authenticated:$/ %s", msg.msg); |
610 | return true; |
611 | case 311: |
612 | /* WHOIS nick: "nick :ident hostname * :name" */ |
613 | chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0", msg.arg[1], |
614 | msg.arg[2], msg.arg[3]); |
615 | chatter_printf(chatter, "$B*** |$0 Name:$/ %s", msg.msg); |
616 | return true; |
617 | case 312: |
618 | /* WHOIS server */ |
619 | chatter_printf(chatter, "$B*** |$0 Server:$/ %s (%s)", msg.arg[2], |
620 | msg.msg); |
621 | return true; |
622 | case 671: |
623 | /* WHOIS server */ |
624 | chatter_printf(chatter, "$B*** |$0 Connection:$/ %s", msg.msg); |
625 | return true; |
626 | case 317: |
627 | /* WHOIS idle */ |
628 | chatter_printf(chatter, "$B*** |$0 Idle:$/ %s %s", msg.arg[2], |
629 | msg.msg); |
630 | return true; |
631 | case 318: |
632 | /* WHOIS end */ |
633 | chatter_printf(chatter, "$B*** `-------$0"); |
634 | return true; |
635 | case 319: |
636 | /* WHOIS channels */ |
637 | chatter_printf(chatter, "$B*** |$0 Channels:$/ %s", msg.msg); |
638 | return true; |
639 | case 330: |
640 | /* WHOIS account */ |
641 | chatter_printf(chatter, "$B*** |$0 Account:$/ %s %s", msg.msg, |
642 | msg.arg[2]); |
643 | return true; |
644 | case 338: |
645 | case 378: |
646 | /* WHOIS host */ |
647 | chatter_printf(chatter, "$B*** |$0 Host:$/ %s %s", msg.msg, |
648 | msg.arg[2]); |
649 | return true; |
650 | case 328: |
651 | /* channel URL, we probably can't do anything with it anyway */ |
652 | return true; |
653 | case 332: |
654 | /* TOPIC */ |
655 | chatter_printf(chatter, "$B***$0 Topic for $B%s$0:$/ %s", |
656 | msg.arg[1], msg.msg); |
657 | if (chatter->irc_channel && |
658 | strcmp(chatter->irc_channel->name, msg.arg[1]) == 0) { |
659 | strlcpy(chatter->irc_channel->topic, msg.msg, |
660 | sizeof(chatter->irc_channel->topic)); |
661 | } |
662 | return true; |
663 | case 333: |
664 | /* TOPIC creator */ |
665 | return true; |
666 | case 352: |
667 | /* WHO output */ |
668 | goto unknown; |
669 | case 315: |
670 | /* end of WHO */ |
671 | goto unknown; |
672 | case 353: |
673 | /* NAMES output */ |
674 | if (chatter->irc_channel && |
675 | strcmp(msg.arg[2], chatter->irc_channel->name) == 0) |
676 | irc_parse_names(chatter, msg.msg); |
677 | return true; |
678 | case 366: |
679 | /* end of NAMES output */ |
680 | if (chatter->irc_channel && |
681 | strcmp(msg.arg[1], chatter->irc_channel->name) == 0) |
682 | chatter_sync_nick_list(chatter, true); |
683 | return true; |
684 | case 372: |
685 | case 375: |
686 | case 376: |
687 | /* MOTD */ |
688 | goto print_msg; |
689 | case 396: |
690 | /* Cloak */ |
691 | chatter_printf(chatter, "$B***$0$/ %s %s", msg.arg[1], msg.msg); |
692 | return true; |
693 | default: |
694 | goto unknown; |
695 | } |
696 | |
697 | print_msg: |
698 | chatter_printf(chatter, "$B***$0$/ %s", msg.msg); |
699 | return true; |
700 | |
701 | unknown: |
702 | chatter_printf(chatter, "$B[?]$0$/ code:%d cmd:%s source:%s " |
703 | "arg0:%s arg1:%s arg2:%s arg3:%s arg4:%s msg:%s", msg.code, msg.cmd, |
704 | msg.source, msg.arg[0], msg.arg[1], msg.arg[2], msg.arg[3], |
705 | msg.arg[4], msg.msg); |
706 | return true; |
707 | } |
708 | |
709 | void |
710 | irc_process_input(struct chatter *chatter, char *str) |
711 | { |
712 | char *arg0, *arg1; |
713 | size_t n; |
714 | |
715 | if (str[0] != '/') { |
716 | if (chatter->irc_channel == NULL) |
717 | goto not_in_channel; |
718 | irc_printf(chatter, "PRIVMSG %s :%s\r\n", |
719 | chatter->irc_channel->name, str); |
720 | chatter_printf(chatter, "<$B%s$0>$/ %s", chatter->irc_nick, str); |
721 | return; |
722 | } |
723 | |
724 | /* skip / */ |
725 | str++; |
726 | |
727 | arg0 = strsep(&str, " "); |
728 | |
729 | if (strcmp(arg0, "join") == 0) { |
730 | if (str == NULL) |
731 | goto not_enough_params; |
732 | irc_printf(chatter, "JOIN %s\r\n", str); |
733 | return; |
734 | } |
735 | if (strcmp(arg0, "me") == 0) { |
736 | if (str == NULL) |
737 | goto not_enough_params; |
738 | if (chatter->irc_channel == NULL) |
739 | goto not_in_channel; |
740 | chatter_printf(chatter, "* %s$/ %s", chatter->irc_nick, str); |
741 | irc_printf(chatter, "PRIVMSG %s :\1ACTION %s\1\r\n", |
742 | chatter->irc_channel->name, str); |
743 | return; |
744 | } |
745 | if (strcmp(arg0, "msg") == 0) { |
746 | arg1 = strsep(&str, " "); |
747 | if (arg1 == NULL || str == NULL) |
748 | goto not_enough_params; |
749 | chatter_printf(chatter, "$B[$0msg$B($0%s$B)]$0$/ %s", arg1, str); |
750 | irc_printf(chatter, "PRIVMSG %s :%s\r\n", arg1, str); |
751 | return; |
752 | } |
753 | if (strcmp(arg0, "nick") == 0) { |
754 | if (str == NULL) |
755 | goto not_enough_params; |
756 | irc_printf(chatter, "NICK %s\r\n", str); |
757 | return; |
758 | } |
759 | if (strcmp(arg0, "part") == 0) { |
760 | if (str == NULL && chatter->irc_channel) |
761 | irc_printf(chatter, "PART %s\r\n", chatter->irc_channel->name); |
762 | else |
763 | irc_printf(chatter, "PART %s\r\n", str); |
764 | return; |
765 | } |
766 | if (strcmp(arg0, "quit") == 0) { |
767 | irc_printf(chatter, "QUIT :%s\r\n", str == NULL ? "&" : str); |
768 | return; |
769 | } |
770 | if (strcmp(arg0, "quote") == 0) { |
771 | if (str == NULL) |
772 | goto not_enough_params; |
773 | irc_printf(chatter, "%s\r\n", str); |
774 | return; |
775 | } |
776 | if (strcmp(arg0, "whois") == 0) { |
777 | if (str == NULL) |
778 | goto not_enough_params; |
779 | irc_printf(chatter, "WHOIS %s\r\n", str); |
780 | return; |
781 | } |
782 | chatter_printf(chatter, "unrecognized command \"$B%s$0\"", arg0); |
783 | return; |
784 | |
785 | not_enough_params: |
786 | chatter_printf(chatter, "$B***$0 Not enough parameters given"); |
787 | return; |
788 | not_in_channel: |
789 | chatter_printf(chatter, "$B*** Cannot send (not in channel)$0"); |
790 | return; |
791 | } |
792 | |
793 | void |
794 | irc_set_active_channel(struct chatter *chatter, char *channame) |
795 | { |
796 | if (chatter->irc_channel) { |
797 | if (chatter->irc_channel->nicks) |
798 | free(chatter->irc_channel->nicks); |
799 | free(chatter->irc_channel); |
800 | chatter->irc_channel = NULL; |
801 | } |
802 | |
803 | if (channame == NULL) |
804 | chatter_sync_nick_list(chatter, false); |
805 | else { |
806 | chatter->irc_channel = xmalloczero(sizeof(struct irc_channel)); |
807 | strlcpy(chatter->irc_channel->name, channame, |
808 | sizeof(chatter->irc_channel->name)); |
809 | } |
810 | |
811 | chatter_update_titlebar(chatter); |
812 | } |
813 | |
814 | void |
815 | irc_parse_names(struct chatter *chatter, char *line) |
816 | { |
817 | size_t n; |
818 | char *nick; |
819 | bool last = false; |
820 | short flags; |
821 | |
822 | if (!chatter->irc_channel) |
823 | return; |
824 | |
825 | LDoDraw(false, chatter->nick_list); |
826 | |
827 | for (;;) { |
828 | nick = strsep(&line, " "); |
829 | |
830 | if (nick[0] == '@') { |
831 | flags = IRC_NICK_FLAG_OP; |
832 | nick++; |
833 | } else if (nick[0] == '+') { |
834 | flags = IRC_NICK_FLAG_VOICE; |
835 | nick++; |
836 | } else if (nick[0] == '\0') { |
837 | /* some servers send a trailing space */ |
838 | break; |
839 | } else |
840 | flags = 0; |
841 | |
842 | irc_add_nick_to_channel(chatter, nick, flags); |
843 | |
844 | if (line == NULL) |
845 | break; |
846 | } |
847 | |
848 | LDoDraw(true, chatter->nick_list); |
849 | InvalRect(&(*(chatter->nick_list))->rView); |
850 | } |
851 | |
852 | void |
853 | irc_add_nick_to_channel(struct chatter *chatter, char *nick, short flags) |
854 | { |
855 | struct irc_channel_nick *anick, *cnick, *pnick; |
856 | short aidx, cidx, ret; |
857 | |
858 | if (chatter->irc_channel->nnicks >= chatter->irc_channel->nicks_size) { |
859 | /* allocate a chunk at a time so we don't do this every iteration */ |
860 | chatter->irc_channel->nicks_size += 5; |
861 | chatter->irc_channel->nicks = |
862 | xreallocarray(chatter->irc_channel->nicks, |
863 | sizeof(struct irc_channel_nick), |
864 | chatter->irc_channel->nicks_size); |
865 | memset(&chatter->irc_channel->nicks[chatter->irc_channel->nnicks], |
866 | 0, |
867 | sizeof(struct irc_channel_nick) * |
868 | (chatter->irc_channel->nicks_size - chatter->irc_channel->nnicks)); |
869 | aidx = chatter->irc_channel->nnicks; |
870 | } else { |
871 | /* find an open slot */ |
872 | for (aidx = 0; aidx < chatter->irc_channel->nicks_size; aidx++) { |
873 | if (chatter->irc_channel->nicks[aidx].nick[0] == '\0') |
874 | break; |
875 | } |
876 | |
877 | if (aidx >= chatter->irc_channel->nicks_size) |
878 | panic("irc_add_nick_to_channel overflow"); |
879 | } |
880 | |
881 | chatter->irc_channel->nnicks++; |
882 | anick = &chatter->irc_channel->nicks[aidx]; |
883 | strlcpy(anick->nick, nick, sizeof(anick->nick)); |
884 | anick->flags = flags; |
885 | |
886 | if (chatter->irc_channel->nnicks == 1) { |
887 | anick->next_nick = -1; |
888 | chatter->irc_channel->first_nick = 0; |
889 | cidx = 0; |
890 | } else { |
891 | /* sort it in the right place by flags descending, then by nick */ |
892 | cnick = &chatter->irc_channel->nicks[ |
893 | chatter->irc_channel->first_nick]; |
894 | pnick = NULL; |
895 | cidx = 0; |
896 | while (cnick) { |
897 | if (cnick->nick[0] == '\0') |
898 | ret = 1; |
899 | if (cnick->flags == anick->flags) |
900 | ret = strnatcasecmp(anick->nick, cnick->nick); |
901 | else if (anick->flags > cnick->flags) |
902 | ret = -1; |
903 | else |
904 | ret = 1; |
905 | |
906 | if (ret <= 0) { |
907 | /* new nick goes before this one */ |
908 | if (pnick) { |
909 | anick->next_nick = pnick->next_nick; |
910 | pnick->next_nick = aidx; |
911 | } else { |
912 | anick->next_nick = chatter->irc_channel->first_nick; |
913 | chatter->irc_channel->first_nick = aidx; |
914 | } |
915 | break; |
916 | } |
917 | |
918 | cidx++; |
919 | |
920 | if (cnick->next_nick == -1) { |
921 | /* end of the line */ |
922 | cnick->next_nick = aidx; |
923 | anick->next_nick = -1; |
924 | break; |
925 | } |
926 | |
927 | pnick = cnick; |
928 | cnick = &chatter->irc_channel->nicks[cnick->next_nick]; |
929 | } |
930 | } |
931 | |
932 | chatter_insert_to_nick_list(chatter, anick, cidx); |
933 | } |
934 | |
935 | void |
936 | irc_remove_nick_from_channel(struct chatter *chatter, char *nick) |
937 | { |
938 | struct irc_channel_nick *cnick, *pnick; |
939 | short cidx; |
940 | |
941 | if (chatter->irc_channel->first_nick == -1) |
942 | return; |
943 | |
944 | pnick = NULL; |
945 | cnick = &chatter->irc_channel->nicks[chatter->irc_channel->first_nick]; |
946 | cidx = 0; |
947 | while (cnick) { |
948 | if (strcmp(cnick->nick, nick) != 0) { |
949 | pnick = cnick; |
950 | cidx++; |
951 | if (cnick->next_nick == -1) |
952 | return; |
953 | cnick = &chatter->irc_channel->nicks[cnick->next_nick]; |
954 | continue; |
955 | } |
956 | |
957 | /* connect the nick that pointed to this one and the one we point to */ |
958 | if (pnick) |
959 | pnick->next_nick = cnick->next_nick; |
960 | else |
961 | chatter->irc_channel->first_nick = cnick->next_nick; |
962 | |
963 | chatter->irc_channel->nnicks--; |
964 | cnick->nick[0] = '\0'; |
965 | LDelRow(1, cidx, chatter->nick_list); |
966 | return; |
967 | } |
968 | } |
969 | |
970 | void |
971 | irc_change_user_nick(struct chatter *chatter, struct irc_user *user, |
972 | char *nick) |
973 | { |
974 | struct irc_channel_nick *cnick; |
975 | short n, flags; |
976 | |
977 | if (chatter->irc_channel && chatter->irc_channel->first_nick > -1) { |
978 | /* preserve flags */ |
979 | cnick = &chatter->irc_channel->nicks[ |
980 | chatter->irc_channel->first_nick]; |
981 | while (cnick) { |
982 | if (strcmp(cnick->nick, user->nick) == 0) { |
983 | flags = cnick->flags; |
984 | break; |
985 | } |
986 | if (cnick->next_nick == -1) |
987 | break; |
988 | cnick = &chatter->irc_channel->nicks[cnick->next_nick]; |
989 | } |
990 | |
991 | irc_remove_nick_from_channel(chatter, user->nick); |
992 | irc_add_nick_to_channel(chatter, nick, flags); |
993 | } |
994 | |
995 | if (strcmp(chatter->irc_nick, user->nick) == 0) { |
996 | free(chatter->irc_nick); |
997 | chatter->irc_nick = xstrdup(nick); |
998 | chatter_update_titlebar(chatter); |
999 | } |
1000 | } |
1001 | |
1002 | void |
1003 | irc_parse_channel_mode_change(struct chatter *chatter, char *mode, |
1004 | char *args) |
1005 | { |
1006 | size_t len; |
1007 | struct irc_channel_nick *cnick; |
1008 | char *user; |
1009 | short n, j, flags; |
1010 | bool add = false; |
1011 | |
1012 | len = strlen(mode); |
1013 | |
1014 | /* mode:"+mvo-vo" args:"nick1 nick2 nick3 nick4" */ |
1015 | for (n = 0; n < len; n++) { |
1016 | switch (mode[n]) { |
1017 | case '+': |
1018 | add = true; |
1019 | break; |
1020 | case '-': |
1021 | add = false; |
1022 | break; |
1023 | case 'v': |
1024 | case 'o': |
1025 | user = strsep(&args, " "); |
1026 | if (user == NULL) |
1027 | user = args; |
1028 | |
1029 | cnick = &chatter->irc_channel->nicks[ |
1030 | chatter->irc_channel->first_nick]; |
1031 | while (cnick) { |
1032 | if (strcmp(cnick->nick, user) != 0) { |
1033 | if (cnick->next_nick == -1) |
1034 | break; |
1035 | cnick = &chatter->irc_channel->nicks[cnick->next_nick]; |
1036 | continue; |
1037 | } |
1038 | |
1039 | flags = cnick->flags; |
1040 | if (mode[n] == 'o') { |
1041 | if (add) |
1042 | flags |= IRC_NICK_FLAG_OP; |
1043 | else |
1044 | flags &= ~IRC_NICK_FLAG_OP; |
1045 | } else if (mode[n] == 'v') { |
1046 | if (add) |
1047 | flags |= IRC_NICK_FLAG_VOICE; |
1048 | else |
1049 | flags &= ~IRC_NICK_FLAG_VOICE; |
1050 | } |
1051 | |
1052 | irc_remove_nick_from_channel(chatter, cnick->nick); |
1053 | /* cnick is probably invalid now */ |
1054 | irc_add_nick_to_channel(chatter, user, flags); |
1055 | break; |
1056 | } |
1057 | |
1058 | break; |
1059 | default: |
1060 | /* some other channel mode */ |
1061 | break; |
1062 | } |
1063 | } |
1064 | } |