/* Copyright (C) 2020 June McEnroe * * This program is free software: you can redistribute it and/or modify * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * 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 #include #include #include #include #include #include #include #include #include "chat.h" 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 caps; } 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] ); } } } 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(EX_PROTOCOL, "%s missing parameter %u", msg->cmd, 1 + i); } } 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(struct Message *msg); 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 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 { 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 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]); } } else { handleErrorGeneric(msg); } } static void handleErrorErroneousNickname(struct Message *msg) { require(msg, false, 3); if (!strcmp(self.nick, "*")) { errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]); } else { handleErrorGeneric(msg); } } 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(EX_CONFIG, "server does not support %s", msg->params[2]); } } #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 (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++] = '='; } dst[i] = '\0'; } 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(EX_USAGE, "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 handleReplyLoggedIn(struct Message *msg) { (void)msg; ircFormat("CAP END\r\n"); handleReplyGeneric(msg); } static void handleErrorSASLFail(struct Message *msg) { require(msg, false, 2); errx(EX_CONFIG, "%s", msg->params[1]); } static void handleReplyWelcome(struct Message *msg) { require(msg, false, 1); set(&self.nick, msg->params[0]); cacheInsert(true, Network, self.nick); 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; } commandCache(); 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(EX_PROTOCOL, "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(EX_PROTOCOL, "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 { uiFormat(Network, Cold, tagTime(msg), "%s", line); } } static void handleErrorNoMOTD(struct Message *msg) { (void)msg; } 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]); cacheInsert(true, None, msg->params[0])->color = idColors[id]; if (replies[ReplyJoin]) { windowShow(windowFor(id)); replies[ReplyJoin]--; } } cacheInsert(true, id, msg->nick)->color = 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 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)) { cacheClear(id); } cacheRemove(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 handleKick(struct Message *msg) { require(msg, true, 2); uint id = idFor(msg->params[0]); bool kicked = !strcmp(msg->params[1], self.nick); cacheInsert(true, id, msg->nick)->color = 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, cacheGet(id, msg->params[1])->color, 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] ?: "") ); cacheRemove(id, msg->params[1]); if (kicked) cacheClear(id); } 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 = cacheNextID(&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] ); } cacheReplace(true, msg->nick, msg->params[0]); } static void handleSetname(struct Message *msg) { require(msg, true, 1); struct Cursor curs = {0}; for (uint id; (id = cacheNextID(&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 handleQuit(struct Message *msg) { require(msg, true, 0); struct Cursor curs = {0}; for (uint id; (id = cacheNextID(&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] ?: "") ); } cacheRemove(None, msg->nick); } 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] ); } } 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); } 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", cacheGet(id, msg->params[1])->color, msg->params[1], hash(msg->params[2]), msg->params[2] ); } static uint prefixBit(char p) { char *s = strchr(network.prefixes, p); if (!s) return 0; return 1 << (s - network.prefixes); } 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); } struct Entry *entry = cacheInsert(false, id, nick); if (user) entry->color = color; entry->prefixBits = 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 ); } static void handleReplyEndOfNames(struct Message *msg) { (void)msg; if (replies[ReplyNamesAuto]) { replies[ReplyNamesAuto]--; } else if (replies[ReplyNames]) { replies[ReplyNames]--; } } static struct { char buf[1024]; char *ptr; char *end; } who = { .ptr = who.buf, .end = &who.buf[sizeof(who.buf)], }; static void handleReplyWho(struct Message *msg) { require(msg, false, 7); if (who.ptr == who.buf) { who.ptr = seprintf( who.ptr, who.end, "The council of \3%02d%s\3 are ", hash(msg->params[1]), msg->params[1] ); } char *prefixes = &msg->params[6][1]; if (prefixes[0] == '*') prefixes++; prefixes[strspn(prefixes, network.prefixes)] = '\0'; if (!prefixes[0] || prefixes[0] == '+') return; who.ptr = seprintf( who.ptr, who.end, "%s\3%02d%s%s\3%s", (who.ptr[-1] == ' ' ? "" : ", "), hash(msg->params[2]), prefixes, msg->params[5], (msg->params[6][0] == 'H' ? "" : " (away)") ); } static void handleReplyEndOfWho(struct Message *msg) { require(msg, false, 2); uiWrite(idFor(msg->params[1]), Warm, tagTime(msg), who.buf); who.ptr = who.buf; } 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 topicCache(uint id, const char *topic) { char buf[512]; struct Cursor curs = {0}; const char *prev = cacheComplete(&curs, id, "/topic "); if (prev) { snprintf(buf, sizeof(buf), "%s", prev); cacheRemove(id, buf); } if (topic) { snprintf(buf, sizeof(buf), "/topic %s", topic); cacheInsert(false, id, buf); } } static void handleReplyTopic(struct Message *msg) { require(msg, false, 3); uint id = idFor(msg->params[1]); topicCache(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 { replies[ReplyTopic]--; } } static void swap(wchar_t *a, wchar_t *b) { wchar_t x = *a; *a = *b; *b = x; } 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 { 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]) { topicCache(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] ); logFormat( id, tagTime(msg), "%s removes the sign in %s", msg->nick, msg->params[0] ); return; } struct Cursor curs = {0}; const char *prev = cacheComplete(&curs, id, "/topic "); if (prev) { prev += 7; } else { 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] ); topicCache(id, msg->params[1]); urlScan(id, msg->nick, msg->params[1]); } 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") ); } 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", }; 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++] ); } else { 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 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; } 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(EX_PROTOCOL, "MODE missing %s parameter", mode); } char *nick = msg->params[i++]; char prefix = network.prefixes[ strchr(network.prefixModes, *ch) - network.prefixModes ]; if (set) { cacheInsert(false, id, nick)->prefixBits |= prefixBit(prefix); } else { cacheInsert(false, id, nick)->prefixBits &= ~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, cacheGet(id, nick)->color, 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(EX_PROTOCOL, "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(EX_PROTOCOL, "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(EX_PROTOCOL, "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 handleErrorChanopPrivsNeeded(struct Message *msg) { require(msg, false, 3); uiFormat( idFor(msg->params[1]), Warm, tagTime(msg), "%s", msg->params[2] ); } 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] ); } 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]) ); } 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, cacheGet(id, msg->params[3])->color, 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 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, cacheGet(id, msg->params[3])->color, 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 handleReplyExceptList(struct Message *msg) { onList("except", msg); } static void handleReplyInviteList(struct Message *msg) { onList("invite", msg); } static void handleReplyList(struct Message *msg) { require(msg, false, 4); 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] ); } static void handleReplyWhoisUser(struct Message *msg) { require(msg, false, 6); cacheInsert(true, Network, msg->params[1])->color = 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] ); } 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)", cacheGet(Network, msg->params[1])->color, msg->params[1], (replies[ReplyWhowas] ? "was" : "is"), msg->params[2], msg->params[3] ); } 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", cacheGet(Network, msg->params[1])->color, msg->params[1], idle, unit, (idle != 1 ? "s" : ""), (msg->params[3] ? ", signed on " : ""), (msg->params[3] ? signon : "") ); } 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", cacheGet(Network, msg->params[1])->color, msg->params[1], buf ); } 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", cacheGet(Network, msg->params[1])->color, msg->params[1], msg->params[2], (msg->params[3] ? " " : ""), (msg->params[3] ?: "") ); } static void handleReplyEndOfWhois(struct Message *msg) { require(msg, false, 2); if (strcmp(msg->params[1], self.nick)) { cacheRemove(Network, msg->params[1]); } } static void handleReplyWhowasUser(struct Message *msg) { require(msg, false, 6); cacheInsert(true, Network, msg->params[1])->color = 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] ); } static void handleReplyEndOfWhowas(struct Message *msg) { require(msg, false, 2); if (strcmp(msg->params[1], self.nick)) { cacheRemove(Network, msg->params[1]); } } 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", cacheGet(id, msg->params[1])->color, msg->params[1], msg->params[2] ); logFormat( id, tagTime(msg), "%s is away: %s", msg->params[1], msg->params[2] ); } 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; } 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; } 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 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; } while (msg < split) { size_t skip = strspn(msg, ",:<> "); ptr = seprintf(ptr, end, "%.*s", (int)skip, msg); msg += skip; size_t len = strcspn(msg, ",:<> "); char *p = seprintf(ptr, end, "%.*s", (int)len, msg); enum Color color = cacheGet(id, ptr)->color; 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 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) { cacheInsert(true, id, msg->nick)->color = 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(EX_UNAVAILABLE, "%s", msg->params[0]); } static const struct Handler { const char *cmd; int reply; Handler *fn; } Handlers[] = { { "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 }, { "315", -ReplyWho, handleReplyEndOfWho }, { "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 }, { "352", +ReplyWho, handleReplyWho }, { "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 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); } }