diff options
Diffstat (limited to 'handle.c')
-rw-r--r-- | handle.c | 1715 |
1 files changed, 1295 insertions, 420 deletions
diff --git a/handle.c b/handle.c index fe15d9a..0cc7c04 100644 --- a/handle.c +++ b/handle.c @@ -1,568 +1,1443 @@ -/* Copyright (C) 2018, 2019 C. McEnroe <june@causal.agency> +/* Copyright (C) 2020 June McEnroe <june@causal.agency> * * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by + * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. + * GNU General Public License for more details. * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. */ +#include <assert.h> #include <ctype.h> #include <err.h> -#include <stdarg.h> +#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <sysexits.h> -#include <time.h> +#include <wchar.h> #include "chat.h" -static char *paramField(char **params) { - char *rest = *params; - if (rest[0] == ':') { - *params = NULL; - return &rest[1]; +uint replies[ReplyCap]; + +static const char *CapNames[] = { +#define X(name, id) [id##Bit] = name, + ENUM_CAP +#undef X +}; + +static enum Cap capParse(const char *list) { + enum Cap caps = 0; + while (*list) { + enum Cap cap = 0; + size_t len = strcspn(list, " "); + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (len != strlen(CapNames[i])) continue; + if (strncmp(list, CapNames[i], len)) continue; + cap = 1 << i; + break; + } + caps |= cap; + list += len; + if (*list) list++; } - return strsep(params, " "); + return caps; } -static void parse( - char *prefix, char **nick, char **user, char **host, - char *params, size_t req, size_t opt, /* (char **) */ ... -) { - char *field; - if (prefix) { - field = strsep(&prefix, "!"); - if (nick) *nick = field; - field = strsep(&prefix, "@"); - if (user) *user = field; - if (host) *host = prefix; - } - - va_list ap; - va_start(ap, opt); - for (size_t i = 0; i < req; ++i) { - if (!params) errx(EX_PROTOCOL, "%zu params required, found %zu", req, i); - field = paramField(¶ms); - char **param = va_arg(ap, char **); - if (param) *param = field; - } - for (size_t i = 0; i < opt; ++i) { - char **param = va_arg(ap, char **); - if (params) { - *param = paramField(¶ms); - } else { - *param = NULL; +static void capList(char *buf, size_t cap, enum Cap caps) { + *buf = '\0'; + char *ptr = buf, *end = &buf[cap]; + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (caps & (1 << i)) { + ptr = seprintf( + ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i] + ); } } - va_end(ap); } -static bool isPing(const char *mesg) { - size_t len = strlen(self.nick); - const char *match = mesg; - while (NULL != (match = strcasestr(match, self.nick))) { - char b = (match > mesg ? *(match - 1) : ' '); - char a = (match[len] ? match[len] : ' '); - match = &match[len]; - if (!isspace(b) && !ispunct(b)) continue; - if (!isspace(a) && !ispunct(a)) continue; - return true; +static void require(struct Message *msg, bool origin, uint len) { + if (origin) { + if (!msg->nick) msg->nick = "*.*"; + if (!msg->user) msg->user = msg->nick; + if (!msg->host) msg->host = msg->user; + } + for (uint i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(1, "%s missing parameter %u", msg->cmd, 1 + i); } - return false; } -static char *dequote(char *mesg) { - if (mesg[0] == '"') mesg = &mesg[1]; - size_t len = strlen(mesg); - if (mesg[len - 1] == '"') mesg[len - 1] = '\0'; - return mesg; +static const time_t *tagTime(const struct Message *msg) { + static time_t time; + struct tm tm; + if (!msg->tags[TagTime]) return NULL; + if (!strptime(msg->tags[TagTime], "%Y-%m-%dT%T", &tm)) return NULL; + time = timegm(&tm); + return &time; } -typedef void Handler(char *prefix, char *params); +typedef void Handler(struct Message *msg); -static void handlePing(char *prefix, char *params) { - (void)prefix; - ircFmt("PONG %s\r\n", params); +static void handleStandardReply(struct Message *msg) { + require(msg, false, 3); + for (uint i = 2; i < ParamCap - 1; ++i) { + if (msg->params[i + 1]) continue; + uiFormat( + Network, Warm, tagTime(msg), + "%s", msg->params[i] + ); + break; + } } -static void handleError(char *prefix, char *params) { - char *mesg; - parse(prefix, NULL, NULL, NULL, params, 1, 0, &mesg); - if (self.quit) { - uiExit(EX_OK); +static void handleErrorGeneric(struct Message *msg) { + require(msg, false, 2); + if (msg->params[2]) { + size_t len = strlen(msg->params[2]); + if (msg->params[2][len - 1] == '.') msg->params[2][len - 1] = '\0'; + uiFormat( + Network, Warm, tagTime(msg), + "%s: %s", msg->params[2], msg->params[1] + ); } else { - errx(EX_PROTOCOL, "%s", mesg); + uiFormat( + Network, Warm, tagTime(msg), + "%s", msg->params[1] + ); + } +} + +static void handleReplyGeneric(struct Message *msg) { + uint first = 1; + uint id = Network; + if (msg->params[1] && strchr(network.chanTypes, msg->params[1][0])) { + id = idFor(msg->params[1]); + first++; + } + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf(ptr, end, "\3%d(%s)\3\t", Gray, msg->cmd); + for (uint i = first; i < ParamCap && msg->params[i]; ++i) { + ptr = seprintf( + ptr, end, "%s%s", (i > first ? " " : ""), msg->params[i] + ); } + uiWrite(id, Ice, tagTime(msg), buf); } -static void handleCap(char *prefix, char *params) { - char *subc, *list; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &subc, &list); - if (!strcmp(subc, "ACK") && self.auth) { - size_t len = strlen(self.auth); - byte plain[1 + len]; - plain[0] = 0; - for (size_t i = 0; i < len; ++i) { - plain[1 + i] = (self.auth[i] == ':' ? 0 : self.auth[i]); +static void handleErrorNicknameInUse(struct Message *msg) { + require(msg, false, 2); + if (!strcmp(self.nick, "*")) { + static uint i = 1; + if (i < ARRAY_LEN(self.nicks) && self.nicks[i]) { + ircFormat("NICK %s\r\n", self.nicks[i++]); + } else { + ircFormat("NICK %s_\r\n", msg->params[1]); } - char b64[base64Size(sizeof(plain))]; - base64(b64, plain, sizeof(plain)); - ircFmt("AUTHENTICATE PLAIN\r\n"); - ircFmt("AUTHENTICATE %s\r\n", b64); + } else { + handleErrorGeneric(msg); } - ircFmt("CAP END\r\n"); } -static void handleErrorErroneousNickname(char *prefix, char *params) { - char *mesg; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, NULL, &mesg); - uiFmt(TagStatus, UIHot, "You can't use that name here: \"%s\"", mesg); - uiLog(TagStatus, UICold, L"Type /nick <name> to choose a new one"); +static void handleErrorErroneousNickname(struct Message *msg) { + require(msg, false, 3); + if (!strcmp(self.nick, "*")) { + errx(1, "%s: %s", msg->params[1], msg->params[2]); + } else { + handleErrorGeneric(msg); + } } -static void handleReplyWelcome(char *prefix, char *params) { - char *nick; - parse(prefix, NULL, NULL, NULL, params, 1, 0, &nick); +static void handleCap(struct Message *msg) { + require(msg, false, 3); + enum Cap caps = capParse(msg->params[2]); + if (!strcmp(msg->params[1], "LS")) { + caps &= ~CapSASL; + if (caps & CapConsumer && self.pos) { + ircFormat("CAP REQ %s=%zu\r\n", CapNames[CapConsumerBit], self.pos); + caps &= ~CapConsumer; + } + if (caps) { + char buf[512]; + capList(buf, sizeof(buf), caps); + ircFormat("CAP REQ :%s\r\n", buf); + } else { + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } + } else if (!strcmp(msg->params[1], "ACK")) { + self.caps |= caps; + if (caps & CapSASL) { + ircFormat( + "AUTHENTICATE %s\r\n", (self.plainUser ? "PLAIN" : "EXTERNAL") + ); + } + if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n"); + } else if (!strcmp(msg->params[1], "NAK")) { + errx(1, "server does not support %s", msg->params[2]); + } +} - if (strcmp(nick, self.nick)) { - free(self.nick); - self.nick = strdup(nick); - if (!self.nick) err(EX_OSERR, "strdup"); - uiPrompt(true); +#define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4) + +static void base64(char *dst, const byte *src, size_t len) { + static const char Base64[64] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + }; + size_t i = 0; + while (len > 2) { + dst[i++] = Base64[0x3F & (src[0] >> 2)]; + dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; + dst[i++] = Base64[0x3F & (src[1] << 2 | src[2] >> 6)]; + dst[i++] = Base64[0x3F & src[2]]; + src += 3; + len -= 3; } - if (self.join && self.keys) { - ircFmt("JOIN %s %s\r\n", self.join, self.keys); - } else if (self.join) { - ircFmt("JOIN %s\r\n", self.join); + if (len) { + dst[i++] = Base64[0x3F & (src[0] >> 2)]; + if (len > 1) { + dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)]; + dst[i++] = Base64[0x3F & (src[1] << 2)]; + } else { + dst[i++] = Base64[0x3F & (src[0] << 4)]; + dst[i++] = '='; + } + dst[i++] = '='; } - tabTouch(TagStatus, self.nick); + dst[i] = '\0'; +} - uiLog(TagStatus, UICold, L"You have arrived"); +static void handleAuthenticate(struct Message *msg) { + (void)msg; + if (!self.plainUser) { + ircFormat("AUTHENTICATE +\r\n"); + return; + } + + byte buf[299] = {0}; + size_t userLen = strlen(self.plainUser); + size_t passLen = strlen(self.plainPass); + size_t len = 1 + userLen + 1 + passLen; + if (sizeof(buf) < len) errx(1, "SASL PLAIN is too long"); + memcpy(&buf[1], self.plainUser, userLen); + memcpy(&buf[1 + userLen + 1], self.plainPass, passLen); + + char b64[BASE64_SIZE(sizeof(buf))]; + base64(b64, buf, len); + ircFormat("AUTHENTICATE "); + ircSend(b64, BASE64_SIZE(len) - 1); + ircFormat("\r\n"); + + explicit_bzero(b64, sizeof(b64)); + explicit_bzero(buf, sizeof(buf)); + explicit_bzero(self.plainPass, strlen(self.plainPass)); } -static void handleReplyMOTD(char *prefix, char *params) { - char *mesg; - parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &mesg); - if (mesg[0] == '-' && mesg[1] == ' ') mesg = &mesg[2]; +static void handleReplyLoggedIn(struct Message *msg) { + (void)msg; + ircFormat("CAP END\r\n"); + handleReplyGeneric(msg); +} - urlScan(TagStatus, mesg); - uiFmt(TagStatus, UICold, "%s", mesg); +static void handleErrorSASLFail(struct Message *msg) { + require(msg, false, 2); + errx(1, "%s", msg->params[1]); } -static void handleReplyList(char *prefix, char *params) { - char *chan, *count, *topic; - parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &chan, &count, &topic); - if (topic[0] == '[') { - char *skip = strstr(topic, "] "); - if (skip) topic = &skip[2]; +static void handleReplyWelcome(struct Message *msg) { + require(msg, false, 1); + set(&self.nick, msg->params[0]); + completePull(Network, self.nick, Default); + if (self.mode) ircFormat("MODE %s %s\r\n", self.nick, self.mode); + if (self.join) { + uint count = 1; + for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", self.join); + if (count == 1) replies[ReplyJoin]++; + replies[ReplyTopicAuto] += count; + replies[ReplyNamesAuto] += count; } - const char *people = (strcmp(count, "1") ? "people" : "person"); - if (topic[0]) { - uiFmt( - TagStatus, UIWarm, - "You see %s %s in \3%d%s\3 under the banner, \"%s\"", - count, people, colorGen(chan), chan, topic - ); + commandCompletion(); + handleReplyGeneric(msg); +} + +static void handleReplyISupport(struct Message *msg) { + handleReplyGeneric(msg); + for (uint i = 1; i < ParamCap; ++i) { + if (!msg->params[i]) break; + char *key = strsep(&msg->params[i], "="); + if (!strcmp(key, "NETWORK")) { + if (!msg->params[i]) continue; + set(&network.name, msg->params[i]); + static bool arrived; + if (!arrived) { + uiFormat( + Network, Cold, tagTime(msg), + "You arrive in %s", msg->params[i] + ); + arrived = true; + } + } else if (!strcmp(key, "USERLEN")) { + if (!msg->params[i]) continue; + network.userLen = strtoul(msg->params[i], NULL, 10); + } else if (!strcmp(key, "HOSTLEN")) { + if (!msg->params[i]) continue; + network.hostLen = strtoul(msg->params[i], NULL, 10); + } else if (!strcmp(key, "CHANTYPES")) { + if (!msg->params[i]) continue; + set(&network.chanTypes, msg->params[i]); + } else if (!strcmp(key, "STATUSMSG")) { + if (!msg->params[i]) continue; + set(&network.statusmsg, msg->params[i]); + } else if (!strcmp(key, "PREFIX")) { + strsep(&msg->params[i], "("); + char *modes = strsep(&msg->params[i], ")"); + char *prefixes = msg->params[i]; + if (!modes || !prefixes || strlen(modes) != strlen(prefixes)) { + errx(1, "invalid PREFIX value"); + } + set(&network.prefixModes, modes); + set(&network.prefixes, prefixes); + } else if (!strcmp(key, "CHANMODES")) { + char *list = strsep(&msg->params[i], ","); + char *param = strsep(&msg->params[i], ","); + char *setParam = strsep(&msg->params[i], ","); + char *channel = strsep(&msg->params[i], ","); + if (!list || !param || !setParam || !channel) { + errx(1, "invalid CHANMODES value"); + } + set(&network.listModes, list); + set(&network.paramModes, param); + set(&network.setParamModes, setParam); + set(&network.channelModes, channel); + } else if (!strcmp(key, "EXCEPTS")) { + network.excepts = (msg->params[i] ?: "e")[0]; + } else if (!strcmp(key, "INVEX")) { + network.invex = (msg->params[i] ?: "I")[0]; + } + } +} + +static void handleReplyMOTD(struct Message *msg) { + require(msg, false, 2); + char *line = msg->params[1]; + urlScan(Network, NULL, line); + if (!strncmp(line, "- ", 2)) { + uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]); } else { - uiFmt( - TagStatus, UIWarm, - "You see %s %s in \3%d%s\3", - count, people, colorGen(chan), chan - ); + uiFormat(Network, Cold, tagTime(msg), "%s", line); } } -static void handleReplyListEnd(char *prefix, char *params) { - (void)prefix; - (void)params; - uiLog(TagStatus, UICold, L"You don't see anyone else"); +static void handleErrorNoMOTD(struct Message *msg) { + (void)msg; } -static enum IRCColor whoisColor; -static void handleReplyWhoisUser(char *prefix, char *params) { - char *nick, *user, *host, *real; - parse( - prefix, NULL, NULL, NULL, - params, 6, 0, NULL, &nick, &user, &host, NULL, &real - ); - whoisColor = colorGen(user); - uiFmt( - TagStatus, UIWarm, - "\3%d%s\3 is %s@%s, \"%s\"", - whoisColor, nick, user, host, real +static void handleReplyHelp(struct Message *msg) { + require(msg, false, 3); + urlScan(Network, NULL, msg->params[2]); + uiWrite(Network, Warm, tagTime(msg), msg->params[2]); +} + +static void handleJoin(struct Message *msg) { + require(msg, true, 1); + uint id = idFor(msg->params[0]); + if (!strcmp(msg->nick, self.nick)) { + if (!self.user || strcmp(self.user, msg->user)) { + set(&self.user, msg->user); + self.color = hash(msg->user); + } + if (!self.host || strcmp(self.host, msg->host)) { + set(&self.host, msg->host); + } + idColors[id] = hash(msg->params[0]); + completePull(None, msg->params[0], idColors[id]); + if (replies[ReplyJoin]) { + windowShow(windowFor(id)); + replies[ReplyJoin]--; + } + } + completePull(id, msg->nick, hash(msg->user)); + if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { + msg->params[2] = NULL; + } + uiFormat( + id, filterCheck(Cold, id, msg), tagTime(msg), + "\3%02d%s\3\t%s%s%sarrives in \3%02d%s\3", + hash(msg->user), msg->nick, + (msg->params[2] ? "(" : ""), + (msg->params[2] ?: ""), + (msg->params[2] ? "\17) " : ""), + hash(msg->params[0]), msg->params[0] ); + logFormat(id, tagTime(msg), "%s arrives in %s", msg->nick, msg->params[0]); } -static void handleReplyWhoisServer(char *prefix, char *params) { - char *nick, *serv, *info; - parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &nick, &serv, &info); - uiFmt( - TagStatus, UIWarm, - "\3%d%s\3 is connected to %s, \"%s\"", - whoisColor, nick, serv, info +static void handleChghost(struct Message *msg) { + require(msg, true, 2); + if (strcmp(msg->nick, self.nick)) return; + if (!self.user || strcmp(self.user, msg->params[0])) { + set(&self.user, msg->params[0]); + self.color = hash(msg->params[0]); + } + if (!self.host || strcmp(self.host, msg->params[1])) { + set(&self.host, msg->params[1]); + } +} + +static void handlePart(struct Message *msg) { + require(msg, true, 1); + uint id = idFor(msg->params[0]); + if (!strcmp(msg->nick, self.nick)) { + completeRemove(id, NULL); + } + completeRemove(id, msg->nick); + enum Heat heat = filterCheck(Cold, id, msg); + if (heat > Ice) urlScan(id, msg->nick, msg->params[1]); + uiFormat( + id, heat, tagTime(msg), + "\3%02d%s\3\tleaves \3%02d%s\3%s%s", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], + (msg->params[1] ? ": " : ""), (msg->params[1] ?: "") + ); + logFormat( + id, tagTime(msg), "%s leaves %s%s%s", + msg->nick, msg->params[0], + (msg->params[1] ? ": " : ""), (msg->params[1] ?: "") ); } -static void handleReplyWhoisOperator(char *prefix, char *params) { - char *nick, *oper; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &oper); - uiFmt(TagStatus, UIWarm, "\3%d%s\3 %s", whoisColor, nick, oper); -} - -static void handleReplyWhoisIdle(char *prefix, char *params) { - char *nick, *idle, *sign; - parse(prefix, NULL, NULL, NULL, params, 4, 0, NULL, &nick, &idle, &sign); - time_t time = strtoul(sign, NULL, 10); - const char *at = ctime(&time); - unsigned long secs = strtoul(idle, NULL, 10); - unsigned long mins = secs / 60; secs %= 60; - unsigned long hours = mins / 60; mins %= 60; - uiFmt( - TagStatus, UIWarm, - "\3%d%s\3 signed on at %.24s and has been idle for %02lu:%02lu:%02lu", - whoisColor, nick, at, hours, mins, secs +static void handleKick(struct Message *msg) { + require(msg, true, 2); + uint id = idFor(msg->params[0]); + bool kicked = !strcmp(msg->params[1], self.nick); + completePull(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[2]); + uiFormat( + id, (kicked ? Hot : Cold), tagTime(msg), + "%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s", + (kicked ? "\26" : ""), + hash(msg->user), msg->nick, + completeColor(id, msg->params[1]), msg->params[1], + hash(msg->params[0]), msg->params[0], + (msg->params[2] ? ": " : ""), (msg->params[2] ?: "") ); + logFormat( + id, tagTime(msg), "%s kicks %s out of %s%s%s", + msg->nick, msg->params[1], msg->params[0], + (msg->params[2] ? ": " : ""), (msg->params[2] ?: "") + ); + completeRemove(id, msg->params[1]); + if (kicked) completeRemove(id, NULL); } -static void handleReplyWhoisChannels(char *prefix, char *params) { - char *nick, *chans; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &chans); - uiFmt(TagStatus, UIWarm, "\3%d%s\3 is in %s", whoisColor, nick, chans); +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (!strcmp(msg->nick, self.nick)) { + set(&self.nick, msg->params[0]); + inputUpdate(); + } + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { + if (!strcmp(idNames[id], msg->nick)) { + set(&idNames[id], msg->params[0]); + } + uiFormat( + id, filterCheck(Cold, id, msg), tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->user), msg->params[0] + ); + if (id == Network) continue; + logFormat( + id, tagTime(msg), "%s is now known as %s", + msg->nick, msg->params[0] + ); + } + completeReplace(msg->nick, msg->params[0]); } -static void handleErrorNoSuchNick(char *prefix, char *params) { - char *nick, *mesg; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &nick, &mesg); - uiFmt(TagStatus, UIWarm, "%s, \"%s\"", mesg, nick); +static void handleSetname(struct Message *msg) { + require(msg, true, 1); + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { + uiFormat( + id, filterCheck(Cold, id, msg), tagTime(msg), + "\3%02d%s\3\tis now known as \3%02d%s\3 (%s\17)", + hash(msg->user), msg->nick, hash(msg->user), msg->nick, + msg->params[0] + ); + } } -static void handleJoin(char *prefix, char *params) { - char *nick, *user, *chan; - parse(prefix, &nick, &user, NULL, params, 1, 0, &chan); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleQuit(struct Message *msg) { + require(msg, true, 0); + struct Cursor curs = {0}; + for (uint id; (id = completeEachID(&curs, msg->nick));) { + enum Heat heat = filterCheck(Cold, id, msg); + if (heat > Ice) urlScan(id, msg->nick, msg->params[0]); + uiFormat( + id, heat, tagTime(msg), + "\3%02d%s\3\tleaves%s%s", + hash(msg->user), msg->nick, + (msg->params[0] ? ": " : ""), (msg->params[0] ?: "") + ); + if (id == Network) continue; + logFormat( + id, tagTime(msg), "%s leaves%s%s", + msg->nick, + (msg->params[0] ? ": " : ""), (msg->params[0] ?: "") + ); + } + completeRemove(None, msg->nick); +} - if (!strcmp(nick, self.nick)) { - tabTouch(TagNone, chan); - uiShowTag(tag); - logReplay(tag); +static void handleInvite(struct Message *msg) { + require(msg, true, 2); + if (!strcmp(msg->params[0], self.nick)) { + set(&self.invited, msg->params[1]); + uiFormat( + Network, filterCheck(Hot, Network, msg), tagTime(msg), + "\3%02d%s\3\tinvites you to \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->params[1]), msg->params[1] + ); + } else { + uint id = idFor(msg->params[1]); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tinvites %s to \3%02d%s\3", + hash(msg->user), msg->nick, + msg->params[0], + hash(msg->params[1]), msg->params[1] + ); + logFormat( + id, tagTime(msg), "%s invites %s to %s", + msg->nick, msg->params[0], msg->params[1] + ); } - tabTouch(tag, nick); +} + +static void handleReplyInviting(struct Message *msg) { + require(msg, false, 3); + struct Message invite = { + .nick = self.nick, + .user = self.user, + .cmd = "INVITE", + .params[0] = msg->params[1], + .params[1] = msg->params[2], + }; + handleInvite(&invite); +} - uiFmt( - tag, UICold, - "\3%d%s\3 arrives in \3%d%s\3", - colorGen(user), nick, colorGen(chan), chan +static void handleErrorUserOnChannel(struct Message *msg) { + require(msg, false, 3); + uint id = idFor(msg->params[2]); + uiFormat( + id, Warm, tagTime(msg), + "\3%02d%s\3 is already in \3%02d%s\3", + completeColor(id, msg->params[1]), msg->params[1], + hash(msg->params[2]), msg->params[2] ); - logFmt(tag, NULL, "%s arrives in %s", nick, chan); } -static void handlePart(char *prefix, char *params) { - char *nick, *user, *chan, *mesg; - parse(prefix, &nick, &user, NULL, params, 1, 1, &chan, &mesg); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleReplyNames(struct Message *msg) { + require(msg, false, 4); + uint id = idFor(msg->params[2]); + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + while (msg->params[3]) { + char *name = strsep(&msg->params[3], " "); + char *prefixes = strsep(&name, "!"); + char *nick = &prefixes[strspn(prefixes, network.prefixes)]; + char *user = strsep(&name, "@"); + enum Color color = (user ? hash(user) : Default); + uint bits = 0; + for (char *p = prefixes; p < nick; ++p) { + bits |= prefixBit(*p); + } + completePush(id, nick, color); + *completeBits(id, nick) = bits; + if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue; + ptr = seprintf( + ptr, end, "%s\3%02d%s\3", (ptr > buf ? ", " : ""), color, prefixes + ); + } + if (ptr == buf) return; + uiFormat( + id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg), + "In \3%02d%s\3 are %s", + hash(msg->params[2]), msg->params[2], buf + ); +} - if (!strcmp(nick, self.nick)) { - tabClear(tag); - } else { - tabRemove(tag, nick); +static void handleReplyEndOfNames(struct Message *msg) { + (void)msg; + if (replies[ReplyNamesAuto]) { + replies[ReplyNamesAuto]--; + } else if (replies[ReplyNames]) { + replies[ReplyNames]--; } +} - if (mesg) { - urlScan(tag, mesg); - uiFmt( - tag, UICold, - "\3%d%s\3 leaves \3%d%s\3, \"%s\"", - colorGen(user), nick, colorGen(chan), chan, dequote(mesg) - ); - logFmt(tag, NULL, "%s leaves %s, \"%s\"", nick, chan, dequote(mesg)); +static void handleReplyNoTopic(struct Message *msg) { + require(msg, false, 2); + uiFormat( + idFor(msg->params[1]), Warm, tagTime(msg), + "There is no sign in \3%02d%s\3", + hash(msg->params[1]), msg->params[1] + ); +} + +static void topicComplete(uint id, const char *topic) { + char buf[512]; + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); + if (prev) { + snprintf(buf, sizeof(buf), "%s", prev); + completeRemove(id, buf); + } + if (topic) { + snprintf(buf, sizeof(buf), "/topic %s", topic); + completePush(id, buf, Default); + } +} + +static void handleReplyTopic(struct Message *msg) { + require(msg, false, 3); + uint id = idFor(msg->params[1]); + topicComplete(id, msg->params[2]); + if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return; + urlScan(id, NULL, msg->params[2]); + uiFormat( + id, (replies[ReplyTopicAuto] ? Cold : Warm), tagTime(msg), + "The sign in \3%02d%s\3 reads: %s", + hash(msg->params[1]), msg->params[1], msg->params[2] + ); + logFormat( + id, tagTime(msg), "The sign in %s reads: %s", + msg->params[1], msg->params[2] + ); + if (replies[ReplyTopicAuto]) { + replies[ReplyTopicAuto]--; } else { - uiFmt( - tag, UICold, - "\3%d%s\3 leaves \3%d%s\3", - colorGen(user), nick, colorGen(chan), chan - ); - logFmt(tag, NULL, "%s leaves %s", nick, chan); + replies[ReplyTopic]--; } } -static void handleKick(char *prefix, char *params) { - char *nick, *user, *chan, *kick, *mesg; - parse(prefix, &nick, &user, NULL, params, 2, 1, &chan, &kick, &mesg); - struct Tag tag = colorTag(tagFor(chan), chan); - bool kicked = !strcmp(kick, self.nick); +static void swap(wchar_t *a, wchar_t *b) { + wchar_t x = *a; + *a = *b; + *b = x; +} - if (kicked) { - tabClear(tag); +static char *highlightMiddle( + char *ptr, char *end, enum Color color, + wchar_t *str, size_t pre, size_t suf +) { + wchar_t nul = L'\0'; + swap(&str[pre], &nul); + ptr = seprintf(ptr, end, "%ls", str); + swap(&str[pre], &nul); + swap(&str[suf], &nul); + if (hashBound) { + ptr = seprintf( + ptr, end, "\3%02d,%02d%ls\3%02d,%02d", + Default, color, &str[pre], Default, Default + ); } else { - tabRemove(tag, kick); - } - - if (mesg) { - urlScan(tag, mesg); - uiFmt( - tag, (kicked ? UIHot : UICold), - "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3, \"%s\"", - colorGen(user), nick, - colorGen(kick), kick, - colorGen(chan), chan, - dequote(mesg) + ptr = seprintf(ptr, end, "\26%ls\26", &str[pre]); + } + swap(&str[suf], &nul); + ptr = seprintf(ptr, end, "%ls", &str[suf]); + return ptr; +} + +static void handleTopic(struct Message *msg) { + require(msg, true, 2); + uint id = idFor(msg->params[0]); + if (!msg->params[1][0]) { + topicComplete(id, NULL); + uiFormat( + id, Warm, tagTime(msg), + "\3%02d%s\3\tremoves the sign in \3%02d%s\3", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] ); - logFmt( - tag, NULL, - "%s kicks %s out of %s, \"%s\"", nick, kick, chan, dequote(mesg) + logFormat( + id, tagTime(msg), "%s removes the sign in %s", + msg->nick, msg->params[0] ); + return; + } + + struct Cursor curs = {0}; + const char *prev = completePrefix(&curs, id, "/topic "); + if (prev) { + prev += 7; } else { - uiFmt( - tag, (kicked ? UIHot : UICold), - "\3%d%s\3 kicks \3%d%s\3 out of \3%d%s\3", - colorGen(user), nick, - colorGen(kick), kick, - colorGen(chan), chan - ); - logFmt(tag, NULL, "%s kicks %s out of %s", nick, kick, chan); + goto plain; } + + wchar_t old[512]; + wchar_t new[512]; + if (swprintf(old, ARRAY_LEN(old), L"%s", prev) < 0) goto plain; + if (swprintf(new, ARRAY_LEN(new), L"%s", msg->params[1]) < 0) goto plain; + + size_t pre; + for (pre = 0; old[pre] && new[pre] && old[pre] == new[pre]; ++pre); + size_t osuf = wcslen(old); + size_t nsuf = wcslen(new); + while (osuf > pre && nsuf > pre && old[osuf-1] == new[nsuf-1]) { + osuf--; + nsuf--; + } + + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf( + ptr, end, "\3%02d%s\3\ttakes down the sign in \3%02d%s\3: ", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + ptr = highlightMiddle(ptr, end, Brown, old, pre, osuf); + if (osuf != pre) uiWrite(id, Cold, tagTime(msg), buf); + ptr = buf; + ptr = seprintf( + ptr, end, "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: ", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0] + ); + ptr = highlightMiddle(ptr, end, Green, new, pre, nsuf); + uiWrite(id, Warm, tagTime(msg), buf); + goto log; + +plain: + uiFormat( + id, Warm, tagTime(msg), + "\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s", + hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0], + msg->params[1] + ); +log: + logFormat( + id, tagTime(msg), "%s places a new sign in %s: %s", + msg->nick, msg->params[0], msg->params[1] + ); + topicComplete(id, msg->params[1]); + urlScan(id, msg->nick, msg->params[1]); } -static void handleQuit(char *prefix, char *params) { - char *nick, *user, *mesg; - parse(prefix, &nick, &user, NULL, params, 0, 1, &mesg); +static const char *UserModes[256] = { + ['O'] = "local oper", + ['i'] = "invisible", + ['o'] = "oper", + ['r'] = "registered", + ['w'] = "wallops", +}; + +static void handleReplyUserModeIs(struct Message *msg) { + require(msg, false, 2); + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + for (char *ch = msg->params[1]; *ch; ++ch) { + if (*ch == '+') continue; + const char *name = UserModes[(byte)*ch]; + ptr = seprintf( + ptr, end, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: "") + ); + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %s", + self.color, self.nick, (ptr > buf ? &buf[2] : "modeless") + ); +} - struct Tag tag; - while (TagNone.id != (tag = tabTag(nick)).id) { - tabRemove(tag, nick); +static const char *ChanModes[256] = { + ['a'] = "protected", + ['h'] = "halfop", + ['i'] = "invite-only", + ['k'] = "key", + ['l'] = "client limit", + ['m'] = "moderated", + ['n'] = "no external messages", + ['o'] = "operator", + ['q'] = "founder", + ['s'] = "secret", + ['t'] = "protected topic", + ['v'] = "voice", +}; - if (mesg) { - urlScan(tag, mesg); - uiFmt( - tag, UICold, - "\3%d%s\3 leaves, \"%s\"", - colorGen(user), nick, dequote(mesg) +static void handleReplyChannelModeIs(struct Message *msg) { + require(msg, false, 3); + uint param = 3; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + for (char *ch = msg->params[2]; *ch; ++ch) { + if (*ch == '+') continue; + const char *name = ChanModes[(byte)*ch]; + if ( + strchr(network.paramModes, *ch) || + strchr(network.setParamModes, *ch) + ) { + assert(param < ParamCap); + ptr = seprintf( + ptr, end, ", +%c%s%s %s", + *ch, (name ? " " : ""), (name ?: ""), + msg->params[param++] ); - logFmt(tag, NULL, "%s leaves, \"%s\"", nick, dequote(mesg)); } else { - uiFmt(tag, UICold, "\3%d%s\3 leaves", colorGen(user), nick); - logFmt(tag, NULL, "%s leaves", nick); + ptr = seprintf( + ptr, end, ", +%c%s%s", + *ch, (name ? " " : ""), (name ?: "") + ); } } + uiFormat( + idFor(msg->params[1]), Warm, tagTime(msg), + "\3%02d%s\3\tis %s", + hash(msg->params[1]), msg->params[1], + (ptr > buf ? &buf[2] : "modeless") + ); } -static void handleReplyTopic(char *prefix, char *params) { - char *chan, *topic; - parse(prefix, NULL, NULL, NULL, params, 3, 0, NULL, &chan, &topic); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleMode(struct Message *msg) { + require(msg, true, 2); + + if (!strchr(network.chanTypes, msg->params[0][0])) { + bool set = true; + for (char *ch = msg->params[1]; *ch; ++ch) { + if (*ch == '+') { set = true; continue; } + if (*ch == '-') { set = false; continue; } + const char *name = UserModes[(byte)*ch]; + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%ssets \3%02d%s\3 %c%c%s%s", + hash(msg->user), msg->nick, + (set ? "" : "un"), + self.color, msg->params[0], + set["-+"], *ch, (name ? " " : ""), (name ?: "") + ); + } + return; + } - urlScan(tag, topic); - uiFmt( - tag, UICold, - "The sign in \3%d%s\3 reads, \"%s\"", - colorGen(chan), chan, topic - ); - logFmt(tag, NULL, "The sign in %s reads, \"%s\"", chan, topic); + uint id = idFor(msg->params[0]); + bool set = true; + uint i = 2; + for (char *ch = msg->params[1]; *ch; ++ch) { + if (*ch == '+') { set = true; continue; } + if (*ch == '-') { set = false; continue; } + + const char *verb = (set ? "sets" : "unsets"); + const char *name = ChanModes[(byte)*ch]; + if (*ch == network.excepts) name = "except"; + if (*ch == network.invex) name = "invite"; + const char *mode = (const char[]) { + set["-+"], *ch, (name ? ' ' : '\0'), '\0' + }; + if (!name) name = ""; + + if (strchr(network.prefixModes, *ch)) { + if (i >= ParamCap || !msg->params[i]) { + errx(1, "MODE missing %s parameter", mode); + } + char *nick = msg->params[i++]; + char prefix = network.prefixes[ + strchr(network.prefixModes, *ch) - network.prefixModes + ]; + completePush(id, nick, Default); + if (set) { + *completeBits(id, nick) |= prefixBit(prefix); + } else { + *completeBits(id, nick) &= ~prefixBit(prefix); + } + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s \3%02d%c%s\3 %s%s in \3%02d%s\3", + hash(msg->user), msg->nick, verb, + completeColor(id, nick), prefix, nick, + mode, name, hash(msg->params[0]), msg->params[0] + ); + logFormat( + id, tagTime(msg), "%s %s %c%s %s%s in %s", + msg->nick, verb, prefix, nick, mode, name, msg->params[0] + ); + } + + if (strchr(network.listModes, *ch)) { + if (i >= ParamCap || !msg->params[i]) { + errx(1, "MODE missing %s parameter", mode); + } + char *mask = msg->params[i++]; + if (*ch == 'b') { + verb = (set ? "bans" : "unbans"); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s %c%c %s from \3%02d%s\3", + hash(msg->user), msg->nick, verb, set["-+"], *ch, mask, + hash(msg->params[0]), msg->params[0] + ); + logFormat( + id, tagTime(msg), "%s %s %c%c %s from %s", + msg->nick, verb, set["-+"], *ch, mask, msg->params[0] + ); + } else { + verb = (set ? "adds" : "removes"); + const char *to = (set ? "to" : "from"); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s %s %s the \3%02d%s\3 %s%s list", + hash(msg->user), msg->nick, verb, mask, to, + hash(msg->params[0]), msg->params[0], mode, name + ); + logFormat( + id, tagTime(msg), "%s %s %s %s the %s %s%s list", + msg->nick, verb, mask, to, msg->params[0], mode, name + ); + } + } + + if (strchr(network.paramModes, *ch)) { + if (i >= ParamCap || !msg->params[i]) { + errx(1, "MODE missing %s parameter", mode); + } + char *param = msg->params[i++]; + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s \3%02d%s\3 %s%s %s", + hash(msg->user), msg->nick, verb, + hash(msg->params[0]), msg->params[0], mode, name, param + ); + logFormat( + id, tagTime(msg), "%s %s %s %s%s %s", + msg->nick, verb, msg->params[0], mode, name, param + ); + } + + if (strchr(network.setParamModes, *ch) && set) { + if (i >= ParamCap || !msg->params[i]) { + errx(1, "MODE missing %s parameter", mode); + } + char *param = msg->params[i++]; + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s \3%02d%s\3 %s%s %s", + hash(msg->user), msg->nick, verb, + hash(msg->params[0]), msg->params[0], mode, name, param + ); + logFormat( + id, tagTime(msg), "%s %s %s %s%s %s", + msg->nick, verb, msg->params[0], mode, name, param + ); + } else if (strchr(network.setParamModes, *ch)) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s \3%02d%s\3 %s%s", + hash(msg->user), msg->nick, verb, + hash(msg->params[0]), msg->params[0], mode, name + ); + logFormat( + id, tagTime(msg), "%s %s %s %s%s", + msg->nick, verb, msg->params[0], mode, name + ); + } + + if (strchr(network.channelModes, *ch)) { + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\t%s \3%02d%s\3 %s%s", + hash(msg->user), msg->nick, verb, + hash(msg->params[0]), msg->params[0], mode, name + ); + logFormat( + id, tagTime(msg), "%s %s %s %s%s", + msg->nick, verb, msg->params[0], mode, name + ); + } + } } -static void handleTopic(char *prefix, char *params) { - char *nick, *user, *chan, *topic; - parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &topic); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleErrorChanopPrivsNeeded(struct Message *msg) { + require(msg, false, 3); + uiFormat( + idFor(msg->params[1]), Warm, tagTime(msg), + "%s", msg->params[2] + ); +} - if (strcmp(nick, self.nick)) tabTouch(tag, nick); +static void handleErrorUserNotInChannel(struct Message *msg) { + require(msg, false, 4); + uiFormat( + idFor(msg->params[2]), Warm, tagTime(msg), + "%s\tis not in \3%02d%s\3", + msg->params[1], hash(msg->params[2]), msg->params[2] + ); +} - urlScan(tag, topic); - uiFmt( - tag, UICold, - "\3%d%s\3 places a new sign in \3%d%s\3, \"%s\"", - colorGen(user), nick, colorGen(chan), chan, topic +static void handleErrorBanListFull(struct Message *msg) { + require(msg, false, 4); + uiFormat( + idFor(msg->params[1]), Warm, tagTime(msg), + "%s", (msg->params[4] ?: msg->params[3]) ); - logFmt(tag, NULL, "%s places a new sign in %s, \"%s\"", nick, chan, topic); } -static void handleReplyEndOfNames(char *prefix, char *params) { - char *chan; - parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); - ircFmt("WHO %s\r\n", chan); +static void handleReplyBanList(struct Message *msg) { + require(msg, false, 3); + uint id = idFor(msg->params[1]); + if (msg->params[3] && msg->params[4]) { + char since[sizeof("0000-00-00 00:00:00")]; + time_t time = strtol(msg->params[4], NULL, 10); + strftime(since, sizeof(since), "%F %T", localtime(&time)); + uiFormat( + id, Warm, tagTime(msg), + "Banned from \3%02d%s\3 since %s by \3%02d%s\3: %s", + hash(msg->params[1]), msg->params[1], + since, completeColor(id, msg->params[3]), msg->params[3], + msg->params[2] + ); + } else { + uiFormat( + id, Warm, tagTime(msg), + "Banned from \3%02d%s\3: %s", + hash(msg->params[1]), msg->params[1], msg->params[2] + ); + } } -static struct { - char buf[4096]; - size_t len; -} who; +static void onList(const char *list, struct Message *msg) { + require(msg, false, 3); + uint id = idFor(msg->params[1]); + if (msg->params[3] && msg->params[4]) { + char since[sizeof("0000-00-00 00:00:00")]; + time_t time = strtol(msg->params[4], NULL, 10); + strftime(since, sizeof(since), "%F %T", localtime(&time)); + uiFormat( + id, Warm, tagTime(msg), + "On the \3%02d%s\3 %s list since %s by \3%02d%s\3: %s", + hash(msg->params[1]), msg->params[1], list, + since, completeColor(id, msg->params[3]), msg->params[3], + msg->params[2] + ); + } else { + uiFormat( + id, Warm, tagTime(msg), + "On the \3%02d%s\3 %s list: %s", + hash(msg->params[1]), msg->params[1], list, msg->params[2] + ); + } +} -static void handleReplyWho(char *prefix, char *params) { - char *chan, *user, *nick; - parse( - prefix, NULL, NULL, NULL, - params, 6, 0, NULL, &chan, &user, NULL, NULL, &nick - ); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleReplyExceptList(struct Message *msg) { + onList("except", msg); +} - tabAdd(tag, nick); +static void handleReplyInviteList(struct Message *msg) { + onList("invite", msg); +} - size_t cap = sizeof(who.buf) - who.len; - int len = snprintf( - &who.buf[who.len], cap, - "%s\3%d%s\3", - (who.len ? ", " : ""), colorGen(user), nick +static void handleReplyList(struct Message *msg) { + require(msg, false, 3); + uiFormat( + Network, Warm, tagTime(msg), + "In \3%02d%s\3 are %ld under the banner: %s", + hash(msg->params[1]), msg->params[1], + strtol(msg->params[2], NULL, 10), + (msg->params[3] ?: "") ); - if ((size_t)len < cap) who.len += len; } -static void handleReplyEndOfWho(char *prefix, char *params) { - char *chan; - parse(prefix, NULL, NULL, NULL, params, 2, 0, NULL, &chan); - struct Tag tag = colorTag(tagFor(chan), chan); +static void handleReplyWhoisUser(struct Message *msg) { + require(msg, false, 6); + completePull(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %s!%s@%s (%s\17)", + hash(msg->params[2]), msg->params[1], + msg->params[1], msg->params[2], msg->params[3], msg->params[5] + ); +} - uiFmt( - tag, UICold, - "In \3%d%s\3 are %s", - colorGen(chan), chan, who.buf +static void handleReplyWhoisServer(struct Message *msg) { + if (!replies[ReplyWhois] && !replies[ReplyWhowas]) return; + require(msg, false, 4); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%s connected to %s (%s)", + completeColor(Network, msg->params[1]), msg->params[1], + (replies[ReplyWhowas] ? "was" : "is"), msg->params[2], msg->params[3] ); - who.len = 0; } -static void handleNick(char *prefix, char *params) { - char *prev, *user, *next; - parse(prefix, &prev, &user, NULL, params, 1, 0, &next); +static void handleReplyWhoisIdle(struct Message *msg) { + require(msg, false, 3); + unsigned long idle = strtoul(msg->params[2], NULL, 10); + const char *unit = "second"; + if (idle / 60) { + idle /= 60; unit = "minute"; + if (idle / 60) { + idle /= 60; unit = "hour"; + if (idle / 24) { + idle /= 24; unit = "day"; + } + } + } + char signon[sizeof("0000-00-00 00:00:00")]; + time_t time = strtol((msg->params[3] ?: ""), NULL, 10); + strftime(signon, sizeof(signon), "%F %T", localtime(&time)); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis idle for %lu %s%s%s%s", + completeColor(Network, msg->params[1]), msg->params[1], + idle, unit, (idle != 1 ? "s" : ""), + (msg->params[3] ? ", signed on " : ""), (msg->params[3] ? signon : "") + ); +} - if (!strcmp(prev, self.nick)) { - free(self.nick); - self.nick = strdup(next); - if (!self.nick) err(EX_OSERR, "strdup"); - uiPrompt(true); +static void handleReplyWhoisChannels(struct Message *msg) { + require(msg, false, 3); + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + while (msg->params[2]) { + char *channel = strsep(&msg->params[2], " "); + if (!channel[0]) break; + char *name = &channel[strspn(channel, network.prefixes)]; + ptr = seprintf( + ptr, end, "%s\3%02d%s\3", + (ptr > buf ? ", " : ""), hash(name), channel + ); } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis in %s", + completeColor(Network, msg->params[1]), msg->params[1], buf + ); +} - struct Tag tag; - while (TagNone.id != (tag = tabTag(prev)).id) { - tabReplace(tag, prev, next); +static void handleReplyWhoisGeneric(struct Message *msg) { + require(msg, false, 3); + if (msg->params[3]) { + msg->params[0] = msg->params[2]; + msg->params[2] = msg->params[3]; + msg->params[3] = msg->params[0]; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\t%s%s%s", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], (msg->params[3] ? " " : ""), (msg->params[3] ?: "") + ); +} - uiFmt( - tag, UICold, - "\3%d%s\3 is now known as \3%d%s\3", - colorGen(user), prev, colorGen(user), next - ); - logFmt(tag, NULL, "%s is now known as %s", prev, next); +static void handleReplyEndOfWhois(struct Message *msg) { + require(msg, false, 2); + if (strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); } } -static void handleCTCP(struct Tag tag, char *nick, char *user, char *mesg) { - mesg = &mesg[1]; - char *ctcp = strsep(&mesg, " "); - char *params = strsep(&mesg, "\1"); - if (strcmp(ctcp, "ACTION")) return; +static void handleReplyWhowasUser(struct Message *msg) { + require(msg, false, 6); + completePull(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\twas %s!%s@%s (%s)", + hash(msg->params[2]), msg->params[1], + msg->params[1], msg->params[2], msg->params[3], msg->params[5] + ); +} - if (strcmp(nick, self.nick)) tabTouch(tag, nick); +static void handleReplyEndOfWhowas(struct Message *msg) { + require(msg, false, 2); + if (strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } +} - urlScan(tag, params); - bool ping = strcmp(nick, self.nick) && isPing(params); - uiFmt( - tag, (ping ? UIHot : UIWarm), - "%c\3%d* %s\17 %s", - ping["\17\26"], colorGen(user), nick, params +static void handleReplyAway(struct Message *msg) { + require(msg, false, 3); + // Might be part of a WHOIS response. + uint id = (replies[ReplyWhois] ? Network : idFor(msg->params[1])); + uiFormat( + id, (id == Network ? Warm : Cold), tagTime(msg), + "\3%02d%s\3\tis away: %s", + completeColor(id, msg->params[1]), msg->params[1], msg->params[2] + ); + logFormat( + id, tagTime(msg), "%s is away: %s", + msg->params[1], msg->params[2] ); - logFmt(tag, NULL, "* %s %s", nick, params); } -static void handlePrivmsg(char *prefix, char *params) { - char *nick, *user, *chan, *mesg; - parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg); - bool direct = !strcmp(chan, self.nick); - struct Tag tag = tagFor(direct ? nick : chan); - colorTag(tag, direct ? user : chan); - if (mesg[0] == '\1') { - handleCTCP(tag, nick, user, mesg); - return; +static void handleReplyNowAway(struct Message *msg) { + require(msg, false, 2); + uiFormat(Network, Warm, tagTime(msg), "%s", msg->params[1]); +} + +static bool isAction(struct Message *msg) { + if (strncmp(msg->params[1], "\1ACTION", 7)) return false; + if (msg->params[1][7] == ' ') { + msg->params[1] += 8; + } else if (msg->params[1][7] == '\1') { + msg->params[1] += 7; + } else { + return false; + } + size_t len = strlen(msg->params[1]); + if (msg->params[1][len - 1] == '\1') { + msg->params[1][len - 1] = '\0'; } + return true; +} - bool me = !strcmp(nick, self.nick); - if (!me) tabTouch(tag, nick); +static bool matchWord(const char *str, const char *word) { + size_t len = strlen(word); + const char *match = str; + while (NULL != (match = strstr(match, word))) { + char a = (match > str ? match[-1] : ' '); + char b = (match[len] ?: ' '); + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + return true; + } + match = &match[len]; + } + return false; +} - urlScan(tag, mesg); - bool hot = !me && (direct || isPing(mesg)); - bool ping = !me && isPing(mesg); - uiFmt( - tag, (hot ? UIHot : UIWarm), - "%c%c\3%d<%s>%c %s", - (me ? IRCUnderline : IRCColor), (ping ? IRCReverse : IRCColor), - colorGen(user), nick, IRCReset, mesg - ); - logFmt(tag, NULL, "<%s> %s", nick, mesg); +static bool isMention(const struct Message *msg) { + if (matchWord(msg->params[1], self.nick)) return true; + for (uint i = 0; i < ARRAY_LEN(self.nicks) && self.nicks[i]; ++i) { + if (matchWord(msg->params[1], self.nicks[i])) return true; + } + return false; } -static void handleNotice(char *prefix, char *params) { - char *nick, *user, *chan, *mesg; - parse(prefix, &nick, &user, NULL, params, 2, 0, &chan, &mesg); - bool direct = !strcmp(chan, self.nick); - struct Tag tag = TagStatus; - if (user) { - tag = tagFor(direct ? nick : chan); - colorTag(tag, direct ? user : chan); +static char *colorMentions(char *ptr, char *end, uint id, const char *msg) { + // Consider words before a colon, or only the first two. + const char *split = strstr(msg, ": "); + if (!split) { + split = strchr(msg, ' '); + if (split) split = strchr(&split[1], ' '); + } + if (!split) split = &msg[strlen(msg)]; + // Bail if there is existing formatting. + for (const char *ch = msg; ch < split; ++ch) { + if (iscntrl(*ch)) goto rest; } - if (strcmp(nick, self.nick)) tabTouch(tag, nick); + while (msg < split) { + size_t skip = strspn(msg, ",:<> "); + ptr = seprintf(ptr, end, "%.*s", (int)skip, msg); + msg += skip; - urlScan(tag, mesg); - bool ping = strcmp(nick, self.nick) && isPing(mesg); - uiFmt( - tag, (ping ? UIHot : UIWarm), - "%c\3%d-%s-\17 %s", - ping["\17\26"], colorGen(user), nick, mesg - ); - logFmt(tag, NULL, "-%s- %s", nick, mesg); + size_t len = strcspn(msg, ",:<> "); + char *p = seprintf(ptr, end, "%.*s", (int)len, msg); + enum Color color = completeColor(id, ptr); + if (color != Default) { + ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg); + } else { + ptr = p; + } + msg += len; + } + +rest: + return seprintf(ptr, end, "%s", msg); } -static const struct { - const char *command; - Handler *handler; +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + char statusmsg = '\0'; + if (network.statusmsg && strchr(network.statusmsg, msg->params[0][0])) { + statusmsg = msg->params[0][0]; + msg->params[0]++; + } + bool query = !strchr(network.chanTypes, msg->params[0][0]); + bool server = strchr(msg->nick, '.'); + bool mine = !strcmp(msg->nick, self.nick); + uint id; + if (query && server) { + id = Network; + } else if (query && !mine) { + id = idFor(msg->nick); + idColors[id] = hash(msg->user); + } else { + id = idFor(msg->params[0]); + } + + bool notice = (msg->cmd[0] == 'N'); + bool action = !notice && isAction(msg); + bool highlight = !mine && isMention(msg); + enum Heat heat = (!notice && (highlight || query) ? Hot : Warm); + heat = filterCheck(heat, id, msg); + if (heat > Warm && !mine && !query) highlight = true; + if (!notice && !mine && heat > Ice) { + completePull(id, msg->nick, hash(msg->user)); + } + if (heat > Ice) urlScan(id, msg->nick, msg->params[1]); + + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + if (statusmsg) { + ptr = seprintf( + ptr, end, "\3%d[%c]\3 ", hash(msg->params[0]), statusmsg + ); + } + if (notice) { + if (id != Network) { + logFormat(id, tagTime(msg), "-%s- %s", msg->nick, msg->params[1]); + } + ptr = seprintf( + ptr, end, "\3%d-%s-\3%d\t", + hash(msg->user), msg->nick, LightGray + ); + } else if (action) { + logFormat(id, tagTime(msg), "* %s %s", msg->nick, msg->params[1]); + ptr = seprintf( + ptr, end, "%s\35\3%d* %s\17\35\t", + (highlight ? "\26" : ""), hash(msg->user), msg->nick + ); + } else { + logFormat(id, tagTime(msg), "<%s> %s", msg->nick, msg->params[1]); + ptr = seprintf( + ptr, end, "%s\3%d<%s>\17\t", + (highlight ? "\26" : ""), hash(msg->user), msg->nick + ); + } + if (notice) { + ptr = seprintf(ptr, end, "%s", msg->params[1]); + } else { + ptr = colorMentions(ptr, end, id, msg->params[1]); + } + uiWrite(id, heat, tagTime(msg), buf); +} + +static void handlePing(struct Message *msg) { + require(msg, false, 1); + ircFormat("PONG :%s\r\n", msg->params[0]); +} + +static void handleError(struct Message *msg) { + require(msg, false, 1); + errx(69, "%s", msg->params[0]); +} + +static const struct Handler { + const char *cmd; + int reply; + Handler *fn; } Handlers[] = { - { "001", handleReplyWelcome }, - { "311", handleReplyWhoisUser }, - { "312", handleReplyWhoisServer }, - { "313", handleReplyWhoisOperator }, - { "315", handleReplyEndOfWho }, - { "317", handleReplyWhoisIdle }, - { "319", handleReplyWhoisChannels }, - { "322", handleReplyList }, - { "323", handleReplyListEnd }, - { "332", handleReplyTopic }, - { "352", handleReplyWho }, - { "366", handleReplyEndOfNames }, - { "372", handleReplyMOTD }, - { "375", handleReplyMOTD }, - { "401", handleErrorNoSuchNick }, - { "432", handleErrorErroneousNickname }, - { "433", handleErrorErroneousNickname }, - { "CAP", handleCap }, - { "ERROR", handleError }, - { "JOIN", handleJoin }, - { "KICK", handleKick }, - { "NICK", handleNick }, - { "NOTICE", handleNotice }, - { "PART", handlePart }, - { "PING", handlePing }, - { "PRIVMSG", handlePrivmsg }, - { "QUIT", handleQuit }, - { "TOPIC", handleTopic }, + { "001", 0, handleReplyWelcome }, + { "005", 0, handleReplyISupport }, + { "221", -ReplyMode, handleReplyUserModeIs }, + { "276", +ReplyWhois, handleReplyWhoisGeneric }, + { "301", 0, handleReplyAway }, + { "305", -ReplyAway, handleReplyNowAway }, + { "306", -ReplyAway, handleReplyNowAway }, + { "307", +ReplyWhois, handleReplyWhoisGeneric }, + { "311", +ReplyWhois, handleReplyWhoisUser }, + { "312", 0, handleReplyWhoisServer }, + { "313", +ReplyWhois, handleReplyWhoisGeneric }, + { "314", +ReplyWhowas, handleReplyWhowasUser }, + { "317", +ReplyWhois, handleReplyWhoisIdle }, + { "318", -ReplyWhois, handleReplyEndOfWhois }, + { "319", +ReplyWhois, handleReplyWhoisChannels }, + { "320", +ReplyWhois, handleReplyWhoisGeneric }, + { "322", +ReplyList, handleReplyList }, + { "323", -ReplyList, NULL }, + { "324", -ReplyMode, handleReplyChannelModeIs }, + { "330", +ReplyWhois, handleReplyWhoisGeneric }, + { "331", -ReplyTopic, handleReplyNoTopic }, + { "332", 0, handleReplyTopic }, + { "335", +ReplyWhois, handleReplyWhoisGeneric }, + { "338", +ReplyWhois, handleReplyWhoisGeneric }, + { "341", 0, handleReplyInviting }, + { "346", +ReplyInvex, handleReplyInviteList }, + { "347", -ReplyInvex, NULL }, + { "348", +ReplyExcepts, handleReplyExceptList }, + { "349", -ReplyExcepts, NULL }, + { "353", 0, handleReplyNames }, + { "366", 0, handleReplyEndOfNames }, + { "367", +ReplyBan, handleReplyBanList }, + { "368", -ReplyBan, NULL }, + { "369", -ReplyWhowas, handleReplyEndOfWhowas }, + { "372", 0, handleReplyMOTD }, + { "378", +ReplyWhois, handleReplyWhoisGeneric }, + { "379", +ReplyWhois, handleReplyWhoisGeneric }, + { "422", 0, handleErrorNoMOTD }, + { "432", 0, handleErrorErroneousNickname }, + { "433", 0, handleErrorNicknameInUse }, + { "437", 0, handleErrorNicknameInUse }, + { "441", 0, handleErrorUserNotInChannel }, + { "443", 0, handleErrorUserOnChannel }, + { "478", 0, handleErrorBanListFull }, + { "482", 0, handleErrorChanopPrivsNeeded }, + { "671", +ReplyWhois, handleReplyWhoisGeneric }, + { "704", +ReplyHelp, handleReplyHelp }, + { "705", +ReplyHelp, handleReplyHelp }, + { "706", -ReplyHelp, NULL }, + { "900", 0, handleReplyLoggedIn }, + { "904", 0, handleErrorSASLFail }, + { "905", 0, handleErrorSASLFail }, + { "906", 0, handleErrorSASLFail }, + { "AUTHENTICATE", 0, handleAuthenticate }, + { "CAP", 0, handleCap }, + { "CHGHOST", 0, handleChghost }, + { "ERROR", 0, handleError }, + { "FAIL", 0, handleStandardReply }, + { "INVITE", 0, handleInvite }, + { "JOIN", 0, handleJoin }, + { "KICK", 0, handleKick }, + { "MODE", 0, handleMode }, + { "NICK", 0, handleNick }, + { "NOTE", 0, handleStandardReply }, + { "NOTICE", 0, handlePrivmsg }, + { "PART", 0, handlePart }, + { "PING", 0, handlePing }, + { "PRIVMSG", 0, handlePrivmsg }, + { "QUIT", 0, handleQuit }, + { "SETNAME", 0, handleSetname }, + { "TOPIC", 0, handleTopic }, + { "WARN", 0, handleStandardReply }, }; -static const size_t HandlersLen = sizeof(Handlers) / sizeof(Handlers[0]); - -void handle(char *line) { - char *prefix = NULL; - if (line[0] == ':') { - prefix = strsep(&line, " ") + 1; - if (!line) errx(EX_PROTOCOL, "unexpected eol"); - } - char *command = strsep(&line, " "); - for (size_t i = 0; i < HandlersLen; ++i) { - if (strcmp(command, Handlers[i].command)) continue; - Handlers[i].handler(prefix, line); - break; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + +void handle(struct Message *msg) { + if (!msg->cmd) return; + if (msg->tags[TagPos]) { + self.pos = strtoull(msg->tags[TagPos], NULL, 10); + } + const struct Handler *handler = bsearch( + msg->cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar + ); + if (handler) { + if (handler->reply && !replies[abs(handler->reply)]) return; + if (handler->fn) handler->fn(msg); + if (handler->reply < 0) replies[abs(handler->reply)]--; + } else if (strcmp(msg->cmd, "400") >= 0 && strcmp(msg->cmd, "599") <= 0) { + handleErrorGeneric(msg); + } else if (isdigit(msg->cmd[0])) { + handleReplyGeneric(msg); } } |