about summary refs log tree commit diff
path: root/handle.c
diff options
context:
space:
mode:
Diffstat (limited to 'handle.c')
-rw-r--r--handle.c740
1 files changed, 463 insertions, 277 deletions
diff --git a/handle.c b/handle.c
index fcc0c5d..5a2cf7c 100644
--- a/handle.c
+++ b/handle.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2020  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 General Public License as published by
@@ -33,10 +33,11 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sysexits.h>
+#include <wchar.h>
 
 #include "chat.h"
 
-struct Replies replies;
+uint replies[ReplyCap];
 
 static const char *CapNames[] = {
 #define X(name, id) [id##Bit] = name,
@@ -62,21 +63,21 @@ static enum Cap capParse(const char *list) {
 	return caps;
 }
 
-static const char *capList(enum Cap caps) {
-	static char buf[1024];
-	buf[0] = '\0';
-	struct Cat cat = { buf, sizeof(buf), 0 };
+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)) {
-			catf(&cat, "%s%s", (buf[0] ? " " : ""), CapNames[i]);
+			ptr = seprintf(
+				ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i]
+			);
 		}
 	}
-	return buf;
 }
 
 static void require(struct Message *msg, bool origin, uint len) {
 	if (origin) {
-		if (!msg->nick) errx(EX_PROTOCOL, "%s missing origin", msg->cmd);
+		if (!msg->nick) msg->nick = "*.*";
 		if (!msg->user) msg->user = msg->nick;
 		if (!msg->host) msg->host = msg->user;
 	}
@@ -90,13 +91,25 @@ 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;
+	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]) {
@@ -114,10 +127,33 @@ static void handleErrorGeneric(struct Message *msg) {
 	}
 }
 
+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, "*")) {
-		ircFormat("NICK :%s_\r\n", msg->params[1]);
+		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);
 	}
@@ -142,14 +178,18 @@ static void handleCap(struct Message *msg) {
 			caps &= ~CapConsumer;
 		}
 		if (caps) {
-			ircFormat("CAP REQ :%s\r\n", capList(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.plain ? "PLAIN" : "EXTERNAL"));
+			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")) {
@@ -188,33 +228,34 @@ static void base64(char *dst, const byte *src, size_t len) {
 
 static void handleAuthenticate(struct Message *msg) {
 	(void)msg;
-	if (!self.plain) {
+	if (!self.plainUser) {
 		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]);
-	}
+	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));
+	ircSend(b64, BASE64_SIZE(len) - 1);
 	ircFormat("\r\n");
 
 	explicit_bzero(b64, sizeof(b64));
 	explicit_bzero(buf, sizeof(buf));
-	explicit_bzero(self.plain, strlen(self.plain));
+	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) {
@@ -225,30 +266,38 @@ static void handleErrorSASLFail(struct Message *msg) {
 static void handleReplyWelcome(struct Message *msg) {
 	require(msg, false, 1);
 	set(&self.nick, msg->params[0]);
-	completeTouch(Network, self.nick, Default);
+	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);
-		replies.join += count;
-		replies.topic += count;
-		replies.names += count;
+		if (count == 1) replies[ReplyJoin]++;
+		replies[ReplyTopicAuto] += count;
+		replies[ReplyNamesAuto] += count;
 	}
+	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]);
-			uiFormat(
-				Network, Cold, tagTime(msg),
-				"You arrive in %s", 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);
@@ -258,6 +307,9 @@ static void handleReplyISupport(struct Message *msg) {
 		} 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], ")");
@@ -290,7 +342,7 @@ static void handleReplyISupport(struct Message *msg) {
 static void handleReplyMOTD(struct Message *msg) {
 	require(msg, false, 2);
 	char *line = msg->params[1];
-	urlScan(Network, msg->nick, line);
+	urlScan(Network, NULL, line);
 	if (!strncmp(line, "- ", 2)) {
 		uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]);
 	} else {
@@ -302,6 +354,12 @@ 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]);
@@ -314,23 +372,23 @@ static void handleJoin(struct Message *msg) {
 			set(&self.host, msg->host);
 		}
 		idColors[id] = hash(msg->params[0]);
-		completeTouch(None, msg->params[0], idColors[id]);
-		if (replies.join) {
-			uiShowID(id);
-			replies.join--;
+		completePull(None, msg->params[0], idColors[id]);
+		if (replies[ReplyJoin]) {
+			windowShow(windowFor(id));
+			replies[ReplyJoin]--;
 		}
 	}
-	completeTouch(id, msg->nick, hash(msg->user));
+	completePull(id, msg->nick, hash(msg->user));
 	if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) {
 		msg->params[2] = NULL;
 	}
 	uiFormat(
-		id, ignoreCheck(Cold, id, msg), tagTime(msg),
+		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] ? ") " : ""),
+		(msg->params[2] ? "\17) " : ""),
 		hash(msg->params[0]), msg->params[0]
 	);
 	logFormat(id, tagTime(msg), "%s arrives in %s", msg->nick, msg->params[0]);
@@ -352,10 +410,10 @@ static void handlePart(struct Message *msg) {
 	require(msg, true, 1);
 	uint id = idFor(msg->params[0]);
 	if (!strcmp(msg->nick, self.nick)) {
-		completeClear(id);
+		completeRemove(id, NULL);
 	}
 	completeRemove(id, msg->nick);
-	enum Heat heat = ignoreCheck(Cold, id, msg);
+	enum Heat heat = filterCheck(Cold, id, msg);
 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
 	uiFormat(
 		id, heat, tagTime(msg),
@@ -374,7 +432,7 @@ static void handleKick(struct Message *msg) {
 	require(msg, true, 2);
 	uint id = idFor(msg->params[0]);
 	bool kicked = !strcmp(msg->params[1], self.nick);
-	completeTouch(id, msg->nick, hash(msg->user));
+	completePull(id, msg->nick, hash(msg->user));
 	urlScan(id, msg->nick, msg->params[2]);
 	uiFormat(
 		id, (kicked ? Hot : Cold), tagTime(msg),
@@ -391,21 +449,22 @@ static void handleKick(struct Message *msg) {
 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
 	);
 	completeRemove(id, msg->params[1]);
-	if (kicked) completeClear(id);
+	if (kicked) completeRemove(id, NULL);
 }
 
 static void handleNick(struct Message *msg) {
 	require(msg, true, 1);
 	if (!strcmp(msg->nick, self.nick)) {
 		set(&self.nick, msg->params[0]);
-		uiRead(); // Update prompt.
+		inputUpdate();
 	}
-	for (uint id; (id = completeID(msg->nick));) {
+	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, ignoreCheck(Cold, id, msg), tagTime(msg),
+			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]
 		);
@@ -415,13 +474,27 @@ static void handleNick(struct Message *msg) {
 			msg->nick, msg->params[0]
 		);
 	}
-	completeReplace(None, msg->nick, msg->params[0]);
+	completeReplace(msg->nick, msg->params[0]);
+}
+
+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 handleQuit(struct Message *msg) {
 	require(msg, true, 0);
-	for (uint id; (id = completeID(msg->nick));) {
-		enum Heat heat = ignoreCheck(Cold, id, msg);
+	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),
@@ -442,8 +515,9 @@ static void handleQuit(struct Message *msg) {
 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, ignoreCheck(Hot, Network, msg), tagTime(msg),
+			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]
 		);
@@ -465,7 +539,6 @@ static void handleInvite(struct Message *msg) {
 
 static void handleReplyInviting(struct Message *msg) {
 	require(msg, false, 3);
-	if (self.caps & CapInviteNotify) return;
 	struct Message invite = {
 		.nick = self.nick,
 		.user = self.user,
@@ -477,10 +550,10 @@ static void handleReplyInviting(struct Message *msg) {
 }
 
 static void handleErrorUserOnChannel(struct Message *msg) {
-	require(msg, false, 4);
+	require(msg, false, 3);
 	uint id = idFor(msg->params[2]);
 	uiFormat(
-		id, Cold, tagTime(msg),
+		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]
@@ -490,21 +563,28 @@ static void handleErrorUserOnChannel(struct Message *msg) {
 static void handleReplyNames(struct Message *msg) {
 	require(msg, false, 4);
 	uint id = idFor(msg->params[2]);
-	char buf[1024] = "";
-	struct Cat cat = { buf, sizeof(buf), 0 };
+	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);
-		completeAdd(id, nick, color);
-		if (!replies.names) continue;
-		catf(&cat, "%s\3%02d%s\3", (buf[0] ? ", " : ""), color, prefixes);
+		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 (!replies.names) return;
+	if (ptr == buf) return;
 	uiFormat(
-		id, Cold, tagTime(msg),
+		id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg),
 		"In \3%02d%s\3 are %s",
 		hash(msg->params[2]), msg->params[2], buf
 	);
@@ -512,15 +592,17 @@ static void handleReplyNames(struct Message *msg) {
 
 static void handleReplyEndOfNames(struct Message *msg) {
 	(void)msg;
-	if (replies.names) replies.names--;
+	if (replies[ReplyNamesAuto]) {
+		replies[ReplyNamesAuto]--;
+	} else if (replies[ReplyNames]) {
+		replies[ReplyNames]--;
+	}
 }
 
 static void handleReplyNoTopic(struct Message *msg) {
 	require(msg, false, 2);
-	if (!replies.topic) return;
-	replies.topic--;
 	uiFormat(
-		idFor(msg->params[1]), Cold, tagTime(msg),
+		idFor(msg->params[1]), Warm, tagTime(msg),
 		"There is no sign in \3%02d%s\3",
 		hash(msg->params[1]), msg->params[1]
 	);
@@ -528,14 +610,15 @@ static void handleReplyNoTopic(struct Message *msg) {
 
 static void topicComplete(uint id, const char *topic) {
 	char buf[512];
-	const char *prev = complete(id, "/topic ");
+	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);
-		completeAdd(id, buf, Default);
+		completePush(id, buf, Default);
 	}
 }
 
@@ -543,11 +626,10 @@ static void handleReplyTopic(struct Message *msg) {
 	require(msg, false, 3);
 	uint id = idFor(msg->params[1]);
 	topicComplete(id, msg->params[2]);
-	if (!replies.topic) return;
-	replies.topic--;
+	if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return;
 	urlScan(id, NULL, msg->params[2]);
 	uiFormat(
-		id, Cold, tagTime(msg),
+		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]
 	);
@@ -555,25 +637,45 @@ static void handleReplyTopic(struct Message *msg) {
 		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]) {
-		topicComplete(id, msg->params[1]);
-		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]
-		);
-		logFormat(
-			id, tagTime(msg), "%s places a new sign in %s: %s",
-			msg->nick, msg->params[0], msg->params[1]
-		);
-	} else {
+	if (!msg->params[1][0]) {
 		topicComplete(id, NULL);
 		uiFormat(
 			id, Warm, tagTime(msg),
@@ -584,7 +686,62 @@ static void handleTopic(struct Message *msg) {
 			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 {
+		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 const char *UserModes[256] = {
@@ -597,20 +754,19 @@ static const char *UserModes[256] = {
 
 static void handleReplyUserModeIs(struct Message *msg) {
 	require(msg, false, 2);
-	if (!replies.mode) return;
-	replies.mode--;
-
-	char buf[1024] = "";
-	struct Cat cat = { buf, sizeof(buf), 0 };
+	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];
-		catf(&cat, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: ""));
+		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, (buf[0] ? &buf[2] : "modeless")
+		self.color, self.nick, (ptr > buf ? &buf[2] : "modeless")
 	);
 }
 
@@ -631,12 +787,9 @@ static const char *ChanModes[256] = {
 
 static void handleReplyChannelModeIs(struct Message *msg) {
 	require(msg, false, 3);
-	if (!replies.mode) return;
-	replies.mode--;
-
 	uint param = 3;
-	char buf[1024] = "";
-	struct Cat cat = { buf, sizeof(buf), 0 };
+	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];
@@ -645,23 +798,23 @@ static void handleReplyChannelModeIs(struct Message *msg) {
 			strchr(network.setParamModes, *ch)
 		) {
 			assert(param < ParamCap);
-			catf(
-				&cat, ", +%c%s%s %s",
+			ptr = seprintf(
+				ptr, end, ", +%c%s%s %s",
 				*ch, (name ? " " : ""), (name ?: ""),
 				msg->params[param++]
 			);
 		} else {
-			catf(
-				&cat, ", +%c%s%s",
+			ptr = seprintf(
+				ptr, end, ", +%c%s%s",
 				*ch, (name ? " " : ""), (name ?: "")
 			);
 		}
 	}
 	uiFormat(
-		idFor(msg->params[1]), Cold, tagTime(msg),
+		idFor(msg->params[1]), Warm, tagTime(msg),
 		"\3%02d%s\3\tis %s",
 		hash(msg->params[1]), msg->params[1],
-		(buf[0] ? &buf[2] : "modeless")
+		(ptr > buf ? &buf[2] : "modeless")
 	);
 }
 
@@ -710,6 +863,12 @@ static void handleMode(struct Message *msg) {
 			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",
@@ -819,7 +978,7 @@ static void handleMode(struct Message *msg) {
 static void handleErrorChanopPrivsNeeded(struct Message *msg) {
 	require(msg, false, 3);
 	uiFormat(
-		idFor(msg->params[1]), Cold, tagTime(msg),
+		idFor(msg->params[1]), Warm, tagTime(msg),
 		"%s", msg->params[2]
 	);
 }
@@ -827,7 +986,7 @@ static void handleErrorChanopPrivsNeeded(struct Message *msg) {
 static void handleErrorUserNotInChannel(struct Message *msg) {
 	require(msg, false, 4);
 	uiFormat(
-		idFor(msg->params[2]), Cold, tagTime(msg),
+		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]
 	);
@@ -836,21 +995,20 @@ static void handleErrorUserNotInChannel(struct Message *msg) {
 static void handleErrorBanListFull(struct Message *msg) {
 	require(msg, false, 4);
 	uiFormat(
-		idFor(msg->params[1]), Cold, tagTime(msg),
+		idFor(msg->params[1]), Warm, tagTime(msg),
 		"%s", (msg->params[4] ?: msg->params[3])
 	);
 }
 
 static void handleReplyBanList(struct Message *msg) {
 	require(msg, false, 3);
-	if (!replies.ban) return;
 	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, Cold, tagTime(msg),
+			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],
@@ -858,26 +1016,22 @@ static void handleReplyBanList(struct Message *msg) {
 		);
 	} else {
 		uiFormat(
-			id, Cold, tagTime(msg),
+			id, Warm, tagTime(msg),
 			"Banned from \3%02d%s\3: %s",
 			hash(msg->params[1]), msg->params[1], msg->params[2]
 		);
 	}
 }
 
-static void handleReplyEndOfBanList(struct Message *msg) {
-	(void)msg;
-	if (replies.ban) replies.ban--;
-}
-
 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, Cold, tagTime(msg),
+			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],
@@ -885,7 +1039,7 @@ static void onList(const char *list, struct Message *msg) {
 		);
 	} else {
 		uiFormat(
-			id, Cold, tagTime(msg),
+			id, Warm, tagTime(msg),
 			"On the \3%02d%s\3 %s list: %s",
 			hash(msg->params[1]), msg->params[1], list, msg->params[2]
 		);
@@ -893,71 +1047,48 @@ static void onList(const char *list, struct Message *msg) {
 }
 
 static void handleReplyExceptList(struct Message *msg) {
-	require(msg, false, 3);
-	if (!replies.excepts) return;
 	onList("except", msg);
 }
 
-static void handleReplyEndOfExceptList(struct Message *msg) {
-	(void)msg;
-	if (replies.excepts) replies.excepts--;
-}
-
 static void handleReplyInviteList(struct Message *msg) {
-	require(msg, false, 3);
-	if (!replies.invex) return;
 	onList("invite", msg);
 }
 
-static void handleReplyEndOfInviteList(struct Message *msg) {
-	(void)msg;
-	if (replies.invex) replies.invex--;
-}
-
 static void handleReplyList(struct Message *msg) {
-	require(msg, false, 4);
-	if (!replies.list) return;
+	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]
+		(msg->params[3] ?: "")
 	);
 }
 
-static void handleReplyListEnd(struct Message *msg) {
-	(void)msg;
-	if (!replies.list) return;
-	replies.list--;
-}
-
 static void handleReplyWhoisUser(struct Message *msg) {
 	require(msg, false, 6);
-	if (!replies.whois) return;
-	completeTouch(Network, msg->params[1], hash(msg->params[2]));
+	completePull(Network, msg->params[1], hash(msg->params[2]));
 	uiFormat(
 		Network, Warm, tagTime(msg),
-		"\3%02d%s\3\tis %s!%s@%s (%s)",
+		"\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);
-	if (!replies.whois) return;
 	uiFormat(
 		Network, Warm, tagTime(msg),
-		"\3%02d%s\3\tis connected to %s (%s)",
+		"\3%02d%s\3\t%s connected to %s (%s)",
 		completeColor(Network, msg->params[1]), msg->params[1],
-		msg->params[2], msg->params[3]
+		(replies[ReplyWhowas] ? "was" : "is"), 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) {
@@ -983,13 +1114,16 @@ static void handleReplyWhoisIdle(struct Message *msg) {
 
 static void handleReplyWhoisChannels(struct Message *msg) {
 	require(msg, false, 3);
-	if (!replies.whois) return;
-	char buf[1024] = "";
-	struct Cat cat = { buf, sizeof(buf), 0 };
+	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)];
-		catf(&cat, "%s\3%02d%s\3", (buf[0] ? ", " : ""), hash(name), channel);
+		ptr = seprintf(
+			ptr, end, "%s\3%02d%s\3",
+			(ptr > buf ? ", " : ""), hash(name), channel
+		);
 	}
 	uiFormat(
 		Network, Warm, tagTime(msg),
@@ -1000,7 +1134,6 @@ static void handleReplyWhoisChannels(struct Message *msg) {
 
 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];
@@ -1016,24 +1149,35 @@ static void handleReplyWhoisGeneric(struct Message *msg) {
 
 static void handleReplyEndOfWhois(struct Message *msg) {
 	require(msg, false, 2);
-	if (!replies.whois) return;
 	if (strcmp(msg->params[1], self.nick)) {
 		completeRemove(Network, msg->params[1]);
 	}
-	replies.whois--;
+}
+
+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]
+	);
+}
+
+static void handleReplyEndOfWhowas(struct Message *msg) {
+	require(msg, false, 2);
+	if (strcmp(msg->params[1], self.nick)) {
+		completeRemove(Network, msg->params[1]);
+	}
 }
 
 static void handleReplyAway(struct Message *msg) {
 	require(msg, false, 3);
 	// Might be part of a WHOIS response.
-	uint id;
-	if (completeColor(Network, msg->params[1]) != Default) {
-		id = Network;
-	} else {
-		id = idFor(msg->params[1]);
-	}
+	uint id = (replies[ReplyWhois] ? Network : idFor(msg->params[1]));
 	uiFormat(
-		id, Warm, tagTime(msg),
+		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]
 	);
@@ -1045,24 +1189,30 @@ static void handleReplyAway(struct Message *msg) {
 
 static void handleReplyNowAway(struct Message *msg) {
 	require(msg, false, 2);
-	if (!replies.away) return;
 	uiFormat(Network, Warm, tagTime(msg), "%s", msg->params[1]);
-	replies.away--;
 }
 
 static bool isAction(struct Message *msg) {
-	if (strncmp(msg->params[1], "\1ACTION ", 8)) return false;
-	msg->params[1] += 8;
+	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';
+	if (msg->params[1][len - 1] == '\1') {
+		msg->params[1][len - 1] = '\0';
+	}
 	return true;
 }
 
-static bool isMention(const struct Message *msg) {
-	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] : ' ');
+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;
@@ -1072,47 +1222,54 @@ static bool isMention(const struct Message *msg) {
 	return false;
 }
 
-static const char *colorMentions(uint id, struct Message *msg) {
-	char *split = strstr(msg->params[1], ": ");
+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->params[1], ' ');
+		split = strchr(msg, ' ');
 		if (split) split = strchr(&split[1], ' ');
 	}
-	if (!split) split = &msg->params[1][strlen(msg->params[1])];
-	for (char *ch = msg->params[1]; ch < split; ++ch) {
-		if (iscntrl(*ch)) return "";
+	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;
 	}
-	char delimit = *split;
-	char *mention = msg->params[1];
-	msg->params[1] = (delimit ? &split[1] : split);
-	*split = '\0';
-
-	static char buf[1024];
-	buf[0] = '\0';
-	struct Cat cat = { buf, sizeof(buf), 0 };
-	while (*mention) {
-		size_t skip = strspn(mention, ",<> ");
-		catf(&cat, "%.*s", (int)skip, mention);
-		mention += skip;
-
-		size_t len = strcspn(mention, ",<> ");
-		char punct = mention[len];
-		mention[len] = '\0';
-		enum Color color = completeColor(id, mention);
+
+	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 = completeColor(id, ptr);
 		if (color != Default) {
-			catf(&cat, "\3%02d%s\3", color, mention);
+			ptr = seprintf(ptr, end, "\3%02d%.*s\3", color, (int)len, msg);
 		} else {
-			catf(&cat, "%s", mention);
+			ptr = p;
 		}
-		mention[len] = punct;
-		mention += len;
+		msg += len;
 	}
-	catf(&cat, "%c", delimit);
-	return buf;
+
+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);
@@ -1127,39 +1284,50 @@ static void handlePrivmsg(struct Message *msg) {
 	}
 
 	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));
-	enum Heat heat = ignoreCheck((mention || query ? Hot : Warm), id, msg);
+	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]);
 		}
-		uiFormat(
-			id, ignoreCheck(Warm, id, msg), tagTime(msg),
-			"\3%d-%s-\3%d\t%s",
-			hash(msg->user), msg->nick, LightGray, 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]);
-		const char *mentions = colorMentions(id, msg);
-		uiFormat(
-			id, heat, tagTime(msg),
-			"%s\35\3%d* %s\17\35\t%s%s",
-			(mention ? "\26" : ""), hash(msg->user), msg->nick,
-			mentions, 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]);
-		const char *mentions = colorMentions(id, msg);
-		uiFormat(
-			id, heat, tagTime(msg),
-			"%s\3%d<%s>\17\t%s%s",
-			(mention ? "\26" : ""), hash(msg->user), msg->nick,
-			mentions, 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) {
@@ -1174,67 +1342,81 @@ static void handleError(struct Message *msg) {
 
 static const struct Handler {
 	const char *cmd;
+	int reply;
 	Handler *fn;
 } Handlers[] = {
-	{ "001", handleReplyWelcome },
-	{ "005", handleReplyISupport },
-	{ "221", handleReplyUserModeIs },
-	{ "276", handleReplyWhoisGeneric },
-	{ "301", handleReplyAway },
-	{ "305", handleReplyNowAway },
-	{ "306", handleReplyNowAway },
-	{ "307", handleReplyWhoisGeneric },
-	{ "311", handleReplyWhoisUser },
-	{ "312", handleReplyWhoisServer },
-	{ "313", handleReplyWhoisGeneric },
-	{ "317", handleReplyWhoisIdle },
-	{ "318", handleReplyEndOfWhois },
-	{ "319", handleReplyWhoisChannels },
-	{ "322", handleReplyList },
-	{ "323", handleReplyListEnd },
-	{ "324", handleReplyChannelModeIs },
-	{ "330", handleReplyWhoisGeneric },
-	{ "331", handleReplyNoTopic },
-	{ "332", handleReplyTopic },
-	{ "341", handleReplyInviting },
-	{ "346", handleReplyInviteList },
-	{ "347", handleReplyEndOfInviteList },
-	{ "348", handleReplyExceptList },
-	{ "349", handleReplyEndOfExceptList },
-	{ "353", handleReplyNames },
-	{ "366", handleReplyEndOfNames },
-	{ "367", handleReplyBanList },
-	{ "368", handleReplyEndOfBanList },
-	{ "372", handleReplyMOTD },
-	{ "378", handleReplyWhoisGeneric },
-	{ "379", handleReplyWhoisGeneric },
-	{ "422", handleErrorNoMOTD },
-	{ "432", handleErrorErroneousNickname },
-	{ "433", handleErrorNicknameInUse },
-	{ "441", handleErrorUserNotInChannel },
-	{ "443", handleErrorUserOnChannel },
-	{ "478", handleErrorBanListFull },
-	{ "482", handleErrorChanopPrivsNeeded },
-	{ "671", handleReplyWhoisGeneric },
-	{ "900", handleReplyLoggedIn },
-	{ "904", handleErrorSASLFail },
-	{ "905", handleErrorSASLFail },
-	{ "906", handleErrorSASLFail },
-	{ "AUTHENTICATE", handleAuthenticate },
-	{ "CAP", handleCap },
-	{ "CHGHOST", handleChghost },
-	{ "ERROR", handleError },
-	{ "INVITE", handleInvite },
-	{ "JOIN", handleJoin },
-	{ "KICK", handleKick },
-	{ "MODE", handleMode },
-	{ "NICK", handleNick },
-	{ "NOTICE", handlePrivmsg },
-	{ "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 int compar(const void *cmd, const void *_handler) {
@@ -1251,8 +1433,12 @@ void handle(struct Message *msg) {
 		msg->cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar
 	);
 	if (handler) {
-		handler->fn(msg);
+		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);
 	}
 }