diff options
Diffstat (limited to 'handle.c')
-rw-r--r-- | handle.c | 637 |
1 files changed, 637 insertions, 0 deletions
diff --git a/handle.c b/handle.c new file mode 100644 index 0000000..ce56a51 --- /dev/null +++ b/handle.c @@ -0,0 +1,637 @@ +/* Copyright (C) 2020 C. McEnroe <june@causal.agency> + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <ctype.h> +#include <err.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> + +#include "chat.h" + +struct Replies replies; + +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 const char *capList(enum Cap caps) { + static char buf[1024]; + buf[0] = '\0'; + for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) { + if (caps & (1 << i)) { + if (buf[0]) strlcat(buf, " ", sizeof(buf)); + strlcat(buf, CapNames[i], sizeof(buf)); + } + } + return buf; +} + +static void require(struct Message *msg, bool origin, size_t len) { + if (origin) { + if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd); + if (!msg->user) msg->user = msg->nick; + if (!msg->host) msg->host = msg->user; + } + for (size_t i = 0; i < len; ++i) { + if (msg->params[i]) continue; + errx(EX_PROTOCOL, "%s missing parameter %zu", 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], "%FT%T", &tm)) return NULL; + time = timegm(&tm); + return &time; +} + +typedef void Handler(struct Message *msg); + +static void handleErrorNicknameInUse(struct Message *msg) { + if (self.nick) return; + require(msg, false, 2); + ircFormat("NICK :%s_\r\n", msg->params[1]); +} + +static void handleErrorErroneousNickname(struct Message *msg) { + require(msg, false, 3); + errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]); +} + +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) { + ircFormat("CAP REQ :%s\r\n", capList(caps)); + } 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.plain ? "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]); + } +} + +static void handleAuthenticate(struct Message *msg) { + (void)msg; + if (!self.plain) { + ircFormat("AUTHENTICATE +\r\n"); + return; + } + + byte buf[299]; + size_t len = 1 + strlen(self.plain); + if (sizeof(buf) < len) errx(EX_CONFIG, "SASL PLAIN is too long"); + buf[0] = 0; + for (size_t i = 0; self.plain[i]; ++i) { + buf[1 + i] = (self.plain[i] == ':' ? 0 : self.plain[i]); + } + + char b64[BASE64_SIZE(sizeof(buf))]; + base64(b64, buf, len); + ircFormat("AUTHENTICATE "); + ircSend(b64, BASE64_SIZE(len)); + ircFormat("\r\n"); + + explicit_bzero(b64, sizeof(b64)); + explicit_bzero(buf, sizeof(buf)); + explicit_bzero(self.plain, strlen(self.plain)); +} + +static void handleReplyLoggedIn(struct Message *msg) { + (void)msg; + ircFormat("CAP END\r\n"); +} + +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]); + completeTouch(Network, self.nick, Default); + if (self.join) { + size_t count = 1; + for (const char *ch = self.join; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", self.join); + replies.join += count; + replies.topic += count; + replies.names += count; + } +} + +static void handleReplyISupport(struct Message *msg) { + for (size_t i = 1; i < ParamCap; ++i) { + if (!msg->params[i]) break; + char *key = strsep(&msg->params[i], "="); + if (!msg->params[i]) continue; + if (!strcmp(key, "NETWORK")) { + set(&self.network, msg->params[i]); + uiFormat( + Network, Cold, tagTime(msg), + "You arrive in %s", msg->params[i] + ); + } else if (!strcmp(key, "CHANTYPES")) { + set(&self.chanTypes, msg->params[i]); + } else if (!strcmp(key, "PREFIX")) { + strsep(&msg->params[i], ")"); + if (!msg->params[i]) continue; + set(&self.prefixes, msg->params[i]); + } + } +} + +static void handleReplyMOTD(struct Message *msg) { + require(msg, false, 2); + char *line = msg->params[1]; + urlScan(Network, msg->nick, 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 handleJoin(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + if (!self.user) { + set(&self.user, msg->user); + self.color = hash(msg->user); + } + idColors[id] = hash(msg->params[0]); + completeTouch(None, msg->params[0], idColors[id]); + if (replies.join) { + uiShowID(id); + replies.join--; + } + } + completeTouch(id, msg->nick, hash(msg->user)); + if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) { + msg->params[2] = NULL; + } + uiFormat( + id, Cold, 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] : ""), + (msg->params[2] ? ") " : ""), + hash(msg->params[0]), msg->params[0] + ); +} + +static void handlePart(struct Message *msg) { + require(msg, true, 1); + size_t id = idFor(msg->params[0]); + if (self.nick && !strcmp(msg->nick, self.nick)) { + completeClear(id); + } + completeRemove(id, msg->nick); + urlScan(id, msg->nick, msg->params[1]); + uiFormat( + id, Cold, 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] ? msg->params[1] : "") + ); +} + +static void handleKick(struct Message *msg) { + require(msg, true, 2); + size_t id = idFor(msg->params[0]); + bool kicked = self.nick && !strcmp(msg->params[1], self.nick); + completeTouch(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] ? msg->params[2] : "") + ); + completeRemove(id, msg->params[1]); + if (kicked) completeClear(id); +} + +static void handleNick(struct Message *msg) { + require(msg, true, 1); + if (self.nick && !strcmp(msg->nick, self.nick)) { + set(&self.nick, msg->params[0]); + uiRead(); // Update prompt. + } + size_t id; + while (None != (id = completeID(msg->nick))) { + uiFormat( + id, Cold, 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] + ); + } + completeReplace(None, msg->nick, msg->params[0]); +} + +static void handleQuit(struct Message *msg) { + require(msg, true, 0); + size_t id; + while (None != (id = completeID(msg->nick))) { + urlScan(id, msg->nick, msg->params[0]); + uiFormat( + id, Cold, tagTime(msg), + "\3%02d%s\3\tleaves%s%s", + hash(msg->user), msg->nick, + (msg->params[0] ? ": " : ""), + (msg->params[0] ? msg->params[0] : "") + ); + } + completeRemove(None, msg->nick); +} + +static void handleReplyNames(struct Message *msg) { + require(msg, false, 4); + size_t id = idFor(msg->params[2]); + char buf[1024]; + size_t len = 0; + while (msg->params[3]) { + char *name = strsep(&msg->params[3], " "); + name += strspn(name, self.prefixes); + char *nick = strsep(&name, "!"); + char *user = strsep(&name, "@"); + enum Color color = (user ? hash(user) : Default); + completeAdd(id, nick, color); + if (!replies.names) continue; + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), color, nick + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + if (!replies.names) return; + uiFormat( + id, Cold, 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.names) replies.names--; +} + +static void handleReplyNoTopic(struct Message *msg) { + require(msg, false, 2); + if (!replies.topic) return; + replies.topic--; + uiFormat( + idFor(msg->params[1]), Cold, tagTime(msg), + "There is no sign in \3%02d%s\3", + hash(msg->params[1]), msg->params[1] + ); +} + +static void handleReplyTopic(struct Message *msg) { + require(msg, false, 3); + if (!replies.topic) return; + replies.topic--; + size_t id = idFor(msg->params[1]); + urlScan(id, NULL, msg->params[2]); + uiFormat( + id, Cold, tagTime(msg), + "The sign in \3%02d%s\3 reads: %s", + hash(msg->params[1]), msg->params[1], msg->params[2] + ); +} + +static void handleTopic(struct Message *msg) { + require(msg, true, 2); + size_t id = idFor(msg->params[0]); + if (msg->params[1][0]) { + urlScan(id, msg->nick, msg->params[1]); + 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] + ); + } else { + 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] + ); + } +} + +static void handleReplyWhoisUser(struct Message *msg) { + require(msg, false, 6); + if (!replies.whois) return; + completeTouch(Network, msg->params[1], hash(msg->params[2])); + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis %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 handleReplyWhoisServer(struct Message *msg) { + require(msg, false, 4); + if (!replies.whois) return; + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis connected to %s (%s)", + completeColor(Network, msg->params[1]), msg->params[1], + msg->params[2], msg->params[3] + ); +} + +static void handleReplyWhoisIdle(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + 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"; } + time_t signon = (msg->params[3] ? strtoul(msg->params[3], NULL, 10) : 0); + 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" : ""), + (signon ? ", signed on " : ""), + 24, (signon ? ctime(&signon) : "") + ); +} + +static void handleReplyWhoisChannels(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + char buf[1024]; + size_t len = 0; + while (msg->params[2]) { + char *channel = strsep(&msg->params[2], " "); + channel += strspn(channel, self.prefixes); + int n = snprintf( + &buf[len], sizeof(buf) - len, + "%s\3%02d%s\3", (len ? ", " : ""), hash(channel), channel + ); + assert(n > 0 && len + n < sizeof(buf)); + len += n; + } + uiFormat( + Network, Warm, tagTime(msg), + "\3%02d%s\3\tis in %s", + completeColor(Network, msg->params[1]), msg->params[1], buf + ); +} + +static void handleReplyWhoisGeneric(struct Message *msg) { + require(msg, false, 3); + if (!replies.whois) return; + 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] ? msg->params[3] : "") + ); +} + +static void handleReplyEndOfWhois(struct Message *msg) { + require(msg, false, 2); + if (!replies.whois) return; + if (!self.nick || strcmp(msg->params[1], self.nick)) { + completeRemove(Network, msg->params[1]); + } + replies.whois--; +} + +static bool isAction(struct Message *msg) { + if (strncmp(msg->params[1], "\1ACTION ", 8)) return false; + msg->params[1] += 8; + size_t len = strlen(msg->params[1]); + if (msg->params[1][len - 1] == '\1') msg->params[1][len - 1] = '\0'; + return true; +} + +static bool isMention(const struct Message *msg) { + if (!self.nick) return false; + size_t len = strlen(self.nick); + const char *match = msg->params[1]; + while (NULL != (match = strcasestr(match, self.nick))) { + char a = (match > msg->params[1] ? match[-1] : ' '); + char b = (match[len] ? match[len] : ' '); + if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) { + return true; + } + match = &match[len]; + } + return false; +} + +static const char *colorMentions(size_t id, struct Message *msg) { + char *split = strchr(msg->params[1], ':'); + if (!split) split = strchr(msg->params[1], ' '); + if (!split) split = &msg->params[1][strlen(msg->params[1])]; + for (char *ch = msg->params[1]; ch < split; ++ch) { + if (iscntrl(*ch)) return ""; + } + char delimit = *split; + char *mention = msg->params[1]; + msg->params[1] = (delimit ? &split[1] : split); + *split = '\0'; + + static char buf[1024]; + FILE *str = fmemopen(buf, sizeof(buf), "w"); + if (!str) err(EX_OSERR, "fmemopen"); + + while (*mention) { + size_t skip = strspn(mention, ",<> "); + fwrite(mention, skip, 1, str); + mention += skip; + + size_t len = strcspn(mention, ",<> "); + char punct = mention[len]; + mention[len] = '\0'; + fprintf(str, "\3%02d%s\3", completeColor(id, mention), mention); + mention[len] = punct; + mention += len; + } + fputc(delimit, str); + + fclose(str); + buf[sizeof(buf) - 1] = '\0'; + return buf; +} + +static void handlePrivmsg(struct Message *msg) { + require(msg, true, 2); + bool query = !strchr(self.chanTypes, msg->params[0][0]); + bool network = strchr(msg->nick, '.'); + bool mine = self.nick && !strcmp(msg->nick, self.nick); + size_t id; + if (query && network) { + 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 = isAction(msg); + bool mention = !mine && isMention(msg); + if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user)); + urlScan(id, msg->nick, msg->params[1]); + if (notice) { + uiFormat( + id, Warm, tagTime(msg), + "%s\3%d-%s-\17\3%d\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + LightGray, msg->params[1] + ); + } else if (action) { + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\35\3%d* %s\17\35\t%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, msg->params[1] + ); + } else { + const char *mentions = colorMentions(id, msg); + uiFormat( + id, (mention || query ? Hot : Warm), tagTime(msg), + "%s\3%d<%s>\17\t%s%s", + (mention ? "\26" : ""), hash(msg->user), msg->nick, + mentions, msg->params[1] + ); + } +} + +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; + Handler *fn; +} Handlers[] = { + { "001", handleReplyWelcome }, + { "005", handleReplyISupport }, + { "276", handleReplyWhoisGeneric }, + { "307", handleReplyWhoisGeneric }, + { "311", handleReplyWhoisUser }, + { "312", handleReplyWhoisServer }, + { "313", handleReplyWhoisGeneric }, + { "317", handleReplyWhoisIdle }, + { "318", handleReplyEndOfWhois }, + { "319", handleReplyWhoisChannels }, + { "330", handleReplyWhoisGeneric }, + { "331", handleReplyNoTopic }, + { "332", handleReplyTopic }, + { "353", handleReplyNames }, + { "366", handleReplyEndOfNames }, + { "372", handleReplyMOTD }, + { "432", handleErrorErroneousNickname }, + { "433", handleErrorNicknameInUse }, + { "671", handleReplyWhoisGeneric }, + { "900", handleReplyLoggedIn }, + { "904", handleErrorSASLFail }, + { "905", handleErrorSASLFail }, + { "906", handleErrorSASLFail }, + { "AUTHENTICATE", handleAuthenticate }, + { "CAP", handleCap }, + { "ERROR", handleError }, + { "JOIN", handleJoin }, + { "KICK", handleKick }, + { "NICK", handleNick }, + { "NOTICE", handlePrivmsg }, + { "PART", handlePart }, + { "PING", handlePing }, + { "PRIVMSG", handlePrivmsg }, + { "QUIT", handleQuit }, + { "TOPIC", handleTopic }, +}; + +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; + const struct Handler *handler = bsearch( + msg.cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar + ); + if (handler) handler->fn(&msg); +} |