/* * Copyright (c) 2021-2022 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include "chatter.h" #include "irc.h" #include "strnatcmp.h" short irc_verify_state(struct irc_connection *conn, short state); short irc_recv(struct irc_connection *conn); size_t irc_send(struct irc_connection *conn, char *line, size_t size); size_t irc_printf(struct irc_connection *conn, const char *format, ...); char * irc_get_line(struct irc_connection *conn, size_t *retsize); struct irc_user * irc_parse_user(char *str); bool irc_can_send(struct irc_connection *conn); struct irc_channel * irc_find_channel(struct irc_connection *conn, char *channame); bool irc_process_server(struct irc_connection *conn); struct irc_channel * irc_create_channel(struct irc_connection *conn, char *channame); void irc_dealloc_channel(struct irc_channel *channel); void irc_set_active_channel(struct irc_connection *conn, char *channame); void irc_parse_names(struct irc_channel *channel, char *line); void irc_add_nick_to_channel(struct irc_channel *channel, char *nick, short flags); void irc_remove_nick_from_channel(struct irc_channel *channel, char *nick); bool irc_nick_is_in_channel(struct irc_channel *channel, char *nick); void irc_change_user_nick(struct irc_channel *channel, struct irc_user *user, char *nick); void irc_parse_channel_mode_change(struct irc_channel *channel, char *mode, char *args); #define IRC_CAN_SEND(conn) ((conn)->send_pb.ioResult <= 0) struct irc_connections_head irc_connections_list = SLIST_HEAD_INITIALIZER(irc_connections_list); struct irc_connection * irc_connect(struct chatter *chatter, const char *server, const unsigned short port, const char *password, const char *nick, const char *ident, const char *realname, const char *channel) { struct irc_connection *conn; char ip_str[] = "255.255.255.255"; char *retname; ip_addr ip = 0, local_ip = 0; ip_port local_port = 0; size_t len; short err; if ((err = _TCPInit()) != 0) panic("TCPInit failed (%d)", err); conn = xmalloczero(sizeof(struct irc_connection), "irc_connection"); SLIST_APPEND(&irc_connections_list, conn, irc_connection, list); SLIST_INIT(&conn->channels_list); conn->chatter = chatter; conn->state = IRC_STATE_DISCONNECTED; conn->hostname = xstrdup(server, "server"); conn->port = port; if (password && password[0]) conn->password = xstrdup(password, "password"); conn->nick = xstrdup(nick, "nick"); conn->ident = xstrdup(ident, "ident"); conn->realname = xstrdup(realname, "realname"); if (channel && channel[0]) conn->channel_autojoin = xstrdup(channel, "chan"); chatter_printf(conn->chatter, conn, NULL, "$B***$0 Connecting to $B%s:%d$0...", conn->hostname, conn->port); if ((err = _TCPCreate(&conn->send_pb, &conn->stream, (Ptr)conn->tcp_buf, sizeof(conn->tcp_buf), nil, nil, nil, false)) != noErr) { chatter_printf(conn->chatter, conn, NULL, "%B*!* TCPCreate failed: %d$0", err); return conn; } if ((err = TCPResolveName(conn->hostname, &ip)) != 0) { chatter_printf(conn->chatter, conn, NULL, "$B*!* Couldn't resolve host %s (%d)$0", conn->hostname, err); conn->state = IRC_STATE_DEAD; return conn; } long2ip(ip, ip_str); if ((err = _TCPActiveOpen(&conn->send_pb, conn->stream, ip, conn->port, &local_ip, &local_port, nil, nil, false)) != noErr) { chatter_printf(conn->chatter, conn, NULL, "$B*!* Failed connecting to %s (%s) port %d: %d$0", conn->hostname, ip_str, conn->port, err); conn->state = IRC_STATE_DEAD; return conn; } chatter_printf(conn->chatter, conn, NULL, "$B***$0 Connected to $B%s$0 (%s) port %d", conn->hostname, ip_str, conn->port); conn->state = IRC_STATE_CONNECTED; if (conn->password && conn->password[0]) { len = snprintf(conn->line, sizeof(conn->line), "PASS %s\r\n", conn->password); irc_send(conn, conn->line, len); } len = snprintf(conn->line, sizeof(conn->line), "NICK %s\r\n", conn->nick); irc_send(conn, conn->line, len); len = snprintf(conn->line, sizeof(conn->line), "USER %s 0 * :%s\r\n", conn->ident, conn->realname); irc_send(conn, conn->line, len); return conn; } void irc_close_connection(struct irc_connection *conn) { if (conn->stream) { _TCPClose(&conn->close_pb, conn->stream, nil, nil, false); _TCPAbort(&conn->close_pb, conn->stream, nil, nil, false); _TCPRelease(&conn->close_pb, conn->stream, nil, nil, false); conn->stream = 0; } conn->state = IRC_STATE_DISCONNECTED; } void irc_dealloc_connection(struct irc_connection *conn) { struct irc_connection *tconn; short n; irc_close_connection(conn); while (!SLIST_EMPTY(&conn->channels_list)) irc_dealloc_channel(SLIST_FIRST(&conn->channels_list)); if (conn->hostname != NULL) xfree(&conn->hostname); if (conn->password != NULL) xfree(&conn->password); if (conn->nick != NULL) xfree(&conn->nick); if (conn->ident != NULL) xfree(&conn->ident); if (conn->realname != NULL) xfree(&conn->realname); if (conn->channel_autojoin != NULL) xfree(&conn->channel_autojoin); SLIST_REMOVE(&irc_connections_list, conn, irc_connection, list); xfree(&conn); } void irc_process(struct irc_connection *conn) { short was_state = conn->state; if (conn->state >= IRC_STATE_CONNECTED) irc_recv(conn); switch (conn->state) { case IRC_STATE_DISCONNECTED: break; case IRC_STATE_DEAD: irc_close_connection(conn); break; case IRC_STATE_CONNECTED: irc_process_server(conn); break; } } short irc_verify_state(struct irc_connection *conn, short state) { if (conn->state != state) { warn("Bad IRC state (in %d, expected %d)", conn->state, state); conn->state = IRC_STATE_DEAD; return 0; } return 1; } short irc_recv(struct irc_connection *conn) { unsigned short rlen; short error, rerror, n; if (conn->state < IRC_STATE_CONNECTING) return 0; error = _TCPStatus(&conn->rcv_pb, conn->stream, &conn->status_pb, nil, nil, false); if (conn->status_pb.amtUnreadData > 0 && conn->ibuflen < sizeof(conn->ibuf)) { rlen = conn->status_pb.amtUnreadData; if (conn->ibuflen + rlen > sizeof(conn->ibuf)) rlen = sizeof(conn->ibuf) - conn->ibuflen; rerror = _TCPRcv(&conn->rcv_pb, conn->stream, (Ptr)(conn->ibuf + conn->ibuflen), &rlen, nil, nil, false); if (rerror) { chatter_printf(conn->chatter, conn, NULL, "$B*!* TCPRecv failed (%d), disconnecting$0", error); conn->state = IRC_STATE_DEAD; return -1; } conn->ibuflen += rlen; } if (error) { /* let already-consumed buffer finish processing */ while (irc_process_server(conn)) SystemTask(); chatter_printf(conn->chatter, conn, NULL, "$B*!* TCPStatus failed: %d$0", error); conn->state = IRC_STATE_DEAD; return -1; } return rlen; } size_t irc_send(struct irc_connection *conn, char *line, size_t size) { short error; if (size > sizeof(conn->obuf)) panic("irc_send: too much data %lu", size); if (!irc_verify_state(conn, IRC_STATE_CONNECTED)) return; while (!IRC_CAN_SEND(conn)) SystemTask(); if (conn->send_pb.ioResult < 0) { chatter_printf(conn->chatter, conn, NULL, "$B*!* TCPSend failed (%d), disconnecting$0", conn->send_pb.ioResult); conn->state = IRC_STATE_DEAD; return 0; } memcpy(&conn->obuf, line, size); /* * _TCPSend only knows how many wds pointers were passed in when it * reads the next one and its pointer is zero (or size is zero?), so * even though we're only sending one wds, memory after wds[0] * has to be zeroed out. */ memset(&conn->wds, 0, sizeof(conn->wds)); conn->wds[0].ptr = (Ptr)&conn->obuf; conn->wds[0].length = size; error = _TCPSend(&conn->send_pb, conn->stream, conn->wds, nil, nil, true); if (error) { chatter_printf(conn->chatter, conn, NULL, "$B*!* TCPSend failed (%d), disconnecting$0", error); conn->state = IRC_STATE_DEAD; return 0; } return size; } size_t irc_printf(struct irc_connection *conn, const char *format, ...) { static char buf[512]; va_list argptr; size_t len = 0; va_start(argptr, format); len = vsnprintf(buf, sizeof(buf), format, argptr); va_end(argptr); if (len > sizeof(buf)) { warn("irc_printf overflow!"); len = sizeof(buf); buf[len - 1] = '\0'; } irc_send(conn, buf, len); return len; } struct irc_user * irc_parse_user(char *str) { static struct irc_user parsed_user; short ret; memset(&parsed_user, 0, sizeof(parsed_user)); ret = sscanf(str, "%[^!]!%[^@]@%s", &parsed_user.nick, &parsed_user.username, &parsed_user.hostname); if (ret != 3) { /* probably just a hostname */ strlcpy(parsed_user.nick, str, sizeof(parsed_user.nick)); strlcpy(parsed_user.username, str, sizeof(parsed_user.username)); strlcpy(parsed_user.hostname, str, sizeof(parsed_user.hostname)); } return &parsed_user; } char * irc_get_line(struct irc_connection *conn, size_t *retsize) { size_t n; if (conn->ibuflen == 0) { if (retsize != NULL) *retsize = 0; return NULL; } for (n = 0; n < conn->ibuflen - 1; n++) { if (conn->ibuf[n] != '\r') continue; if (conn->ibuf[n + 1] != '\n') continue; memcpy(conn->line, conn->ibuf, n + 1); conn->line[n] = '\0'; if (retsize != NULL) *retsize = n + 1; if (n == conn->ibuflen - 2) { conn->ibuflen = 0; } else { conn->ibuflen -= n + 2; if (conn->ibuflen < 0) panic("bogus ibuflen %d", conn->ibuflen); memmove(conn->ibuf, conn->ibuf + n + 2, conn->ibuflen); } return conn->line; } return NULL; } struct irc_channel * irc_find_channel(struct irc_connection *conn, char *channame) { struct irc_channel *channel; short n; if (channame == NULL || channame[0] == '\0') return NULL; SLIST_FOREACH(channel, &conn->channels_list, list) { if (strcasecmp(channel->name, channame) == 0) return channel; } return NULL; } bool irc_process_server(struct irc_connection *conn) { struct irc_msg msg; struct irc_user *user; struct irc_channel *channel; char *line, *word; size_t size, n, lastbreak; short state, curarg; line = irc_get_line(conn, &size); if (size == 0 || line == NULL) return false; memset(&msg, 0, sizeof(msg)); word = strsep(&line, " "); /* extract source before command */ if (word[0] == ':') { /* ":server.name 001 jcs :Hi" -> msg.source=server.name */ strlcpy(msg.source, word + 1, sizeof(msg.source)); word = strsep(&line, " "); } /* code or a command name */ if (isdigit(word[0]) && isdigit(word[1]) && isdigit(word[2])) /* ":server.name 001 jcs :Hi" -> msg.code=1 */ msg.code = atoi(word); else /* "PING :server.name" -> msg.cmd=PING */ /* ":jcs!~jcs@jcs JOIN #wallops" -> msg.cmd=JOIN */ strlcpy(msg.cmd, word, sizeof(msg.cmd)); /* parameters, put into args until we see one starting with : */ curarg = 0; while (line != NULL && line[0] != '\0') { if (line[0] == ':') { strlcpy(msg.msg, line + 1, sizeof(msg.msg)); /* some irc servers put the first arg as the message */ if (curarg == 0) strlcpy(msg.arg[0], line + 1, sizeof(msg.arg[0])); break; } word = strsep(&line, " "); if (word == NULL) { strlcpy(msg.msg, line, sizeof(msg.msg)); break; } strlcpy(msg.arg[curarg], word, sizeof(msg.arg[curarg])); curarg++; if (curarg >= IRC_MSG_MAX_ARGS) { /* just stick the rest in msg */ strlcpy(msg.msg, line, sizeof(msg.msg)); break; } } if (msg.cmd[0]) { /* this one will fire most often, keep at the top */ if (strcmp(msg.cmd, "PRIVMSG") == 0) { user = irc_parse_user(msg.source); size = strlen(msg.msg); channel = irc_find_channel(conn, msg.arg[0]); if (msg.msg[0] == '\1' && msg.msg[size - 1] == '\1') { /* CTCP or a /me action */ msg.msg[size - 1] = '\0'; if (strncmp(msg.msg + 1, "ACTION", 6) == 0) { chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "* %s$/ %s", user->nick, msg.msg + 8); } else if (strncmp(msg.msg + 1, "VERSION", 7) == 0) { chatter_printf(conn->chatter, conn, NULL, "$B*** %s ($0%s@%s$B)$0 requested CTCP $BVERSION$0 " "from $B%s$0", user->nick, user->username, user->hostname, msg.arg[0]); /* only respond if it was sent directly to us */ if (strcmp(msg.arg[0], conn->nick) == 0) { irc_printf(conn, "NOTICE %s :\1VERSION %s %s on a %s\1\r\n", user->nick, PROGRAM_NAME, get_version(false), gestalt_machine_type()); } } else { goto unknown; } } else if (strcmp(msg.arg[0], conn->nick) == 0) { chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "$B[%s($0%s@%s$B)]$0$/ %s", user->nick, user->username, user->hostname, msg.msg); } else if (channel != NULL) { size = strlen(conn->nick); if (strncmp(msg.msg, conn->nick, size) == 0 && (msg.msg[size] == ' ' || msg.msg[size] == ':' || msg.msg[size] == ',' || msg.msg[size] == '\0')) { /* highlight message */ chatter_printf(channel->chatter, conn, channel, "<$U%s$0>$/ %s", user->nick, msg.msg); if (!conn->chatter->focusable->visible) notify(); } else chatter_printf(channel->chatter, conn, channel, "$/<%s> %s", user->nick, msg.msg); } else chatter_printf(conn->chatter, conn, NULL, "$B[%s($0%s@%s$B):%s]$0$/ %s", user->nick, user->username, user->hostname, msg.arg[0], msg.msg); return true; } if (strcmp(msg.cmd, "ERROR") == 0) { chatter_printf(conn->chatter, conn, NULL, "$B*!* Disconnected$0 from $B%s$0:$/ %s", conn->hostname, msg.msg); conn->state = IRC_STATE_DEAD; return true; } if (strcmp(msg.cmd, "JOIN") == 0) { user = irc_parse_user(msg.source); if (strcmp(user->nick, conn->nick) == 0) channel = irc_create_channel(conn, msg.arg[0]); else { channel = irc_find_channel(conn, msg.arg[0]); irc_add_nick_to_channel(channel, user->nick, 0); } chatter_printf(channel->chatter, conn, channel, "$B*** %s ($0%s@%s$B)$0 has joined $B%s$0", user->nick, user->username, user->hostname, msg.arg[0]); return true; } if (strcmp(msg.cmd, "KICK") == 0) { user = irc_parse_user(msg.source); channel = irc_find_channel(conn, msg.arg[0]); if (strcmp(msg.arg[1], conn->nick) == 0) { irc_dealloc_channel(channel); channel = NULL; } else irc_remove_nick_from_channel(channel, msg.arg[1]); chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "$B*** %s$0 was kicked from $B%s$0 by $B%s%0:$/ %s", msg.arg[1], msg.arg[0], user->nick, msg.msg); return true; } if (strcmp(msg.cmd, "KILL") == 0) { user = irc_parse_user(msg.source); if (strcmp(user->nick, conn->nick) == 0) { /* we died :( */ warn("%s (%s@%s) has been killed: %s", user->nick, user->username, user->hostname, msg.msg); conn->state = IRC_STATE_DEAD; return false; } SLIST_FOREACH(channel, &conn->channels_list, list) { if (!irc_nick_is_in_channel(channel, user->nick)) continue; chatter_printf(conn->chatter, conn, channel, "$B*** %s ($0%s@%s$B)$0 has been killed:$/ %s", user->nick, user->username, user->hostname, msg.msg); irc_remove_nick_from_channel(channel, user->nick); } return true; } if (strcmp(msg.cmd, "MODE") == 0) { if (strcmp(msg.arg[0], conn->nick) == 0) chatter_printf(conn->chatter, conn, NULL, "$B***$0 Mode change ($B%s$0) for user $B%s$0", msg.msg, msg.arg[0]); else { user = irc_parse_user(msg.source); channel = irc_find_channel(conn, msg.arg[0]); /* concatenate nicks */ msg.msg[0] = '\0'; if (msg.arg[2][0] != '\0') strlcat(msg.msg, msg.arg[2], sizeof(msg.msg)); if (msg.arg[3][0] != '\0') { strlcat(msg.msg, " ", sizeof(msg.msg)); strlcat(msg.msg, msg.arg[3], sizeof(msg.msg)); } if (msg.arg[4][0] != '\0') { strlcat(msg.msg, " ", sizeof(msg.msg)); strlcat(msg.msg, msg.arg[4], sizeof(msg.msg)); } if (msg.arg[5][0] != '\0') { strlcat(msg.msg, " ", sizeof(msg.msg)); strlcat(msg.msg, msg.arg[5], sizeof(msg.msg)); } chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "$B***$0 Mode change ($B%s %s$0) on $B%s$0 by $B%s%0", msg.arg[1], msg.msg, msg.arg[0], user->nick); if (channel) irc_parse_channel_mode_change(channel, msg.arg[1], msg.msg); } return true; } if (strcmp(msg.cmd, "NICK") == 0) { user = irc_parse_user(msg.source); SLIST_FOREACH(channel, &conn->channels_list, list) { if (!irc_nick_is_in_channel(channel, user->nick)) continue; chatter_printf(channel->chatter, conn, channel, "$B*** %s$0 is now known as $B%s$0", user->nick, msg.msg); irc_change_user_nick(channel, user, msg.msg); } return true; } if (strcmp(msg.cmd, "NOTICE") == 0) { if (strncmp(msg.msg, "*** ", 4) == 0) chatter_printf(conn->chatter, conn, NULL, "$B***$0 $/%s", msg.msg + 4); else chatter_printf(conn->chatter, conn, NULL, "$/%s", msg.msg); return true; } if (strcmp(msg.cmd, "PART") == 0) { user = irc_parse_user(msg.source); channel = irc_find_channel(conn, msg.arg[0]); if (strcmp(user->nick, conn->nick) == 0) { irc_dealloc_channel(channel); /* we don't need to print anything */ } else { irc_remove_nick_from_channel(channel, user->nick); chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "$B*** %s ($0%s@%s$B)$0 has left $B%s$0", user->nick, user->username, user->hostname, msg.arg[0]); } return true; } if (strcmp(msg.cmd, "PING") == 0) { irc_printf(conn, "PONG :%s\r\n", msg.msg); return true; } if (strcmp(msg.cmd, "QUIT") == 0) { user = irc_parse_user(msg.source); SLIST_FOREACH(channel, &conn->channels_list, list) { if (!irc_nick_is_in_channel(channel, user->nick)) continue; irc_remove_nick_from_channel(channel, user->nick); chatter_printf(conn->chatter, conn, channel, "$B*** %s ($0%s@%s$B)$0 has quit:$/ %s", user->nick, user->username, user->hostname, msg.msg); } if (strcmp(user->nick, conn->nick) == 0) { chatter_printf(conn->chatter, conn, NULL, "$B*** %s ($0%s@%s$B)$0 has quit:$/ %s", user->nick, user->username, user->hostname, msg.msg); conn->state = IRC_STATE_DEAD; return false; } return true; } goto unknown; } switch (msg.code) { case 0: goto unknown; case 1: case 2: /* welcome banners */ goto print_msg; case 3: case 4: case 5: case 250: case 251: case 252: case 253: case 254: case 255: case 265: case 266: /* server stats, unhelpful */ return true; case 307: /* WHOIS regnick */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Authenticated:$/ %s", msg.msg); return true; case 311: /* WHOIS nick: "nick :ident hostname * :name" */ chatter_printf(conn->chatter, conn, NULL, "$B*** %s ($0%s@%s$B)$0", msg.arg[1], msg.arg[2], msg.arg[3]); chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Name:$/ %s", msg.msg); return true; case 312: /* WHOIS server */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Server:$/ %s (%s)", msg.arg[2], msg.msg); return true; case 671: /* WHOIS server */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Connection:$/ %s", msg.msg); return true; case 317: /* WHOIS idle */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Idle:$/ %s %s", msg.arg[2], msg.msg); return true; case 318: /* WHOIS end */ chatter_printf(conn->chatter, conn, NULL, "$B*** `-------$0"); return true; case 319: /* WHOIS channels */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Channels:$/ %s", msg.msg); return true; case 330: /* WHOIS account */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Account:$/ %s %s", msg.msg, msg.arg[2]); return true; case 338: case 378: /* WHOIS host */ chatter_printf(conn->chatter, conn, NULL, "$B*** |$0 Host:$/ %s %s", msg.msg, msg.arg[2]); return true; case 328: /* channel URL, we probably can't do anything with it anyway */ return true; case 332: /* TOPIC */ channel = irc_find_channel(conn, msg.arg[1]); chatter_printf(channel ? channel->chatter : conn->chatter, conn, channel, "$B***$0 Topic for $B%s$0:$/ %s", msg.arg[1], msg.msg); if (channel) strlcpy(channel->topic, msg.msg, sizeof(channel->topic)); return true; case 333: /* TOPIC creator */ return true; case 352: /* WHO output */ goto unknown; case 315: /* end of WHO */ goto unknown; case 353: /* NAMES output */ channel = irc_find_channel(conn, msg.arg[2]); if (channel) irc_parse_names(channel, msg.msg); return true; case 366: /* end of NAMES output */ channel = irc_find_channel(conn, msg.arg[1]); if (channel) chatter_sync_nick_list(channel->chatter, channel, true); return true; case 372: case 375: /* MOTD */ goto print_msg; case 376: /* end of MOTD */ if (conn->channel_autojoin && conn->channel_autojoin[0] && !conn->did_autojoin) { irc_printf(conn, "JOIN %s\r\n", conn->channel_autojoin); conn->did_autojoin = true; } goto print_msg; case 396: /* Cloak */ chatter_printf(conn->chatter, conn, NULL, "$B***$0$/ %s %s", msg.arg[1], msg.msg); return true; case 433: { /* Nick in use, try appending a _ */ char *new_nick = xmalloc(strlen(conn->nick) + 2, "new nick"); size_t len; chatter_printf(conn->chatter, conn, NULL, "$B***$0$/ %s: %s", msg.arg[1], msg.msg); sprintf(new_nick, "%s_", conn->nick); xfree(&conn->nick); conn->nick = new_nick; chatter_update_titlebar(conn->chatter); len = snprintf(conn->line, sizeof(conn->line), "NICK %s\r\n", conn->nick); irc_send(conn, conn->line, len); return true; } default: goto unknown; } print_msg: chatter_printf(conn->chatter, conn, NULL, "$B***$0$/ %s", msg.msg); return true; unknown: chatter_printf(conn->chatter, conn, NULL, "$B[?]$0$/ code:%d cmd:%s source:%s arg0:%s arg1:%s arg2:%s arg3:%s " "arg4:%s msg:%s", msg.code, msg.cmd, msg.source, msg.arg[0], msg.arg[1], msg.arg[2], msg.arg[3], msg.arg[4], msg.msg); return true; } void irc_process_input(struct irc_connection *conn, struct irc_channel *channel, char *str) { char *arg0, *arg1; size_t n; if (conn == NULL || conn->state < IRC_STATE_CONNECTED) goto not_connected; if (str[0] != '/') { if (channel == NULL) goto not_in_channel; irc_printf(conn, "PRIVMSG %s :%s\r\n", channel->name, str); chatter_printf(conn->chatter, conn, channel, "<$B%s$0>$/ %s", conn->nick, str); return; } /* skip / */ str++; arg0 = strsep(&str, " "); if (strcasecmp(arg0, "join") == 0) { if (conn == NULL) goto not_connected; if (str == NULL) goto not_enough_params; /* * If we're already in this channel, we won't get any response * from the server to switch channels. */ channel = irc_find_channel(conn, str); if (channel) /* this won't actually create, it'll just switch for us */ irc_create_channel(conn, str); else irc_printf(conn, "JOIN %s\r\n", str); return; } if (strcasecmp(arg0, "me") == 0) { if (conn == NULL) goto not_connected; if (str == NULL) goto not_enough_params; if (channel == NULL) goto not_in_channel; chatter_printf(conn->chatter, conn, channel, "* %s$/ %s", conn->nick, str); irc_printf(conn, "PRIVMSG %s :\1ACTION %s\1\r\n", channel->name, str); return; } if (strcasecmp(arg0, "msg") == 0) { if (conn == NULL) goto not_connected; arg1 = strsep(&str, " "); if (arg1 == NULL || str == NULL) goto not_enough_params; chatter_printf(conn->chatter, conn, NULL, "$B[$0msg$B($0%s$B)]$0$/ %s", arg1, str); irc_printf(conn, "PRIVMSG %s :%s\r\n", arg1, str); return; } if (strcasecmp(arg0, "nick") == 0) { if (conn == NULL) goto not_connected; if (str == NULL) goto not_enough_params; irc_printf(conn, "NICK %s\r\n", str); return; } if (strcasecmp(arg0, "part") == 0) { if (conn == NULL) goto not_connected; if (str == NULL && channel) irc_printf(conn, "PART %s\r\n", channel->name); else if (str) irc_printf(conn, "PART %s\r\n", str); else goto not_in_channel; return; } if (strcasecmp(arg0, "quit") == 0) { if (conn == NULL) goto not_connected; irc_printf(conn, "QUIT :%s\r\n", str == NULL ? "Quitting" : str); return; } if (strcasecmp(arg0, "quote") == 0) { if (conn == NULL) goto not_connected; if (str == NULL) goto not_enough_params; irc_printf(conn, "%s\r\n", str); return; } if (strcasecmp(arg0, "whois") == 0) { if (conn == NULL) goto not_connected; if (str == NULL) goto not_enough_params; irc_printf(conn, "WHOIS %s\r\n", str); return; } chatter_printf(conn->chatter, conn, NULL, "unrecognized command \"$B%s$0\"", arg0); return; not_enough_params: chatter_printf(conn->chatter, conn, NULL, "$B*!*$0 Not enough parameters given"); return; not_connected: chatter_printf(conn->chatter, conn, NULL, "$B*!*$0 Not connected"); return; not_in_channel: chatter_printf(conn->chatter, conn, NULL, "$B*!*$0 Cannot send (not in a channel)"); return; } struct irc_channel * irc_create_channel(struct irc_connection *conn, char *channame) { struct irc_channel *channel = NULL, *tchannel; short n; SLIST_FOREACH(channel, &conn->channels_list, list) { if (strcasecmp(channel->name, channame) == 0) { /* TODO: chatter_switch_to_channel(channel) */ #if 0 channel->chatter->cur_conn = channel->connection; channel->chatter->cur_channel = channel; chatter_sync_nick_list(channel->chatter, channel, false); chatter_update_titlebar(channel->chatter); #endif return channel; } } channel = xmalloczero(sizeof(struct irc_channel), "irc_channel"); SLIST_APPEND(&conn->channels_list, channel, irc_channel, list); channel->connection = conn; strlcpy(channel->name, channame, sizeof(channel->name)); channel->chatter = conn->chatter; chatter_add_tab(channel->chatter, NULL, conn, channel); return channel; } void irc_dealloc_channel(struct irc_channel *channel) { struct irc_connection *conn = channel->connection; struct irc_channel *tchannel; struct chatter *chatter = channel->chatter; chatter_remove_channel(chatter, channel); if (channel->nicks) xfree(&channel->nicks); SLIST_REMOVE(&conn->channels_list, channel, irc_channel, list); chatter_update_titlebar(chatter); } void irc_parse_names(struct irc_channel *channel, char *line) { size_t n; char *nick; bool last = false; short flags; // LDoDraw(false, channel->chatter->nick_list); for (;;) { nick = strsep(&line, " "); if (nick[0] == '@') { flags = IRC_NICK_FLAG_OP; nick++; } else if (nick[0] == '+') { flags = IRC_NICK_FLAG_VOICE; nick++; } else if (nick[0] == '\0') { /* some servers send a trailing space */ break; } else flags = 0; irc_add_nick_to_channel(channel, nick, flags); if (line == NULL) break; } // LDoDraw(true, channel->chatter->nick_list); // InvalRect(&(*(channel->chatter->nick_list))->rView); } void irc_add_nick_to_channel(struct irc_channel *channel, char *nick, short flags) { struct irc_channel_nick *anick, *cnick, *pnick; short aidx, cidx, ret; if (channel->nnicks >= channel->nicks_size) { /* allocate a chunk at a time so we don't do this every iteration */ channel->nicks_size += 5; channel->nicks = xreallocarray(channel->nicks, sizeof(struct irc_channel_nick), channel->nicks_size); memset(&channel->nicks[channel->nnicks], 0, sizeof(struct irc_channel_nick) * (channel->nicks_size - channel->nnicks)); aidx = channel->nnicks; } else { /* find an open slot */ for (aidx = 0; aidx < channel->nicks_size; aidx++) { if (channel->nicks[aidx].nick[0] == '\0') break; } if (aidx >= channel->nicks_size) panic("irc_add_nick_to_channel overflow"); } channel->nnicks++; anick = &channel->nicks[aidx]; strlcpy(anick->nick, nick, sizeof(anick->nick)); anick->flags = flags; if (channel->nnicks == 1) { anick->next_nick = -1; channel->first_nick = 0; cidx = 0; } else { /* sort it in the right place by flags descending, then by nick */ cnick = &channel->nicks[channel->first_nick]; pnick = NULL; cidx = 0; while (cnick) { if (cnick->nick[0] == '\0') ret = 1; if (cnick->flags == anick->flags) ret = strnatcasecmp(anick->nick, cnick->nick); else if (anick->flags > cnick->flags) ret = -1; else ret = 1; if (ret <= 0) { /* new nick goes before this one */ if (pnick) { anick->next_nick = pnick->next_nick; pnick->next_nick = aidx; } else { anick->next_nick = channel->first_nick; channel->first_nick = aidx; } break; } cidx++; if (cnick->next_nick == -1) { /* end of the line */ cnick->next_nick = aidx; anick->next_nick = -1; break; } pnick = cnick; cnick = &channel->nicks[cnick->next_nick]; } } chatter_insert_to_nick_list(channel->chatter, channel, anick, cidx); } void irc_remove_nick_from_channel(struct irc_channel *channel, char *nick) { struct irc_channel_nick *cnick, *pnick; short cidx; if (channel->first_nick == -1) return; pnick = NULL; cnick = &channel->nicks[channel->first_nick]; cidx = 0; while (cnick) { if (strcmp(cnick->nick, nick) != 0) { pnick = cnick; cidx++; if (cnick->next_nick == -1) return; cnick = &channel->nicks[cnick->next_nick]; continue; } /* connect the nick that pointed to this one and the one we point to */ if (pnick) pnick->next_nick = cnick->next_nick; else channel->first_nick = cnick->next_nick; channel->nnicks--; cnick->nick[0] = '\0'; // LDelRow(1, cidx, channel->chatter->nick_list); return; } } bool irc_nick_is_in_channel(struct irc_channel *channel, char *nick) { struct irc_channel_nick *cnick, *pnick; if (channel->first_nick == -1) return; pnick = NULL; cnick = &channel->nicks[channel->first_nick]; while (cnick) { if (strcmp(cnick->nick, nick) == 0) return true; pnick = cnick; if (cnick->next_nick == -1) break; cnick = &channel->nicks[cnick->next_nick]; } return false; } void irc_change_user_nick(struct irc_channel *channel, struct irc_user *user, char *nick) { struct irc_channel_nick *cnick; short n, flags; if (channel && channel->first_nick > -1) { /* preserve flags */ cnick = &channel->nicks[channel->first_nick]; while (cnick) { if (strcmp(cnick->nick, user->nick) == 0) { flags = cnick->flags; break; } if (cnick->next_nick == -1) break; cnick = &channel->nicks[cnick->next_nick]; } irc_remove_nick_from_channel(channel, user->nick); irc_add_nick_to_channel(channel, nick, flags); } if (strcmp(channel->connection->nick, user->nick) == 0) { xfree(&channel->connection->nick); channel->connection->nick = xstrdup(nick, "nick"); chatter_update_titlebar(channel->chatter); } } void irc_parse_channel_mode_change(struct irc_channel *channel, char *mode, char *args) { size_t len; struct irc_channel_nick *cnick; char *user; short n, j, flags; bool add = false; len = strlen(mode); /* mode:"+mvo-vo" args:"nick1 nick2 nick3 nick4" */ for (n = 0; n < len; n++) { switch (mode[n]) { case '+': add = true; break; case '-': add = false; break; case 'v': case 'o': user = strsep(&args, " "); if (user == NULL) user = args; cnick = &channel->nicks[channel->first_nick]; while (cnick) { if (strcmp(cnick->nick, user) != 0) { if (cnick->next_nick == -1) break; cnick = &channel->nicks[cnick->next_nick]; continue; } flags = cnick->flags; if (mode[n] == 'o') { if (add) flags |= IRC_NICK_FLAG_OP; else flags &= ~IRC_NICK_FLAG_OP; } else if (mode[n] == 'v') { if (add) flags |= IRC_NICK_FLAG_VOICE; else flags &= ~IRC_NICK_FLAG_VOICE; } irc_remove_nick_from_channel(channel, cnick->nick); /* cnick is probably invalid now */ irc_add_nick_to_channel(channel, user, flags); break; } break; default: /* some other channel mode */ break; } } }