about summary refs log tree commit diff
path: root/client.c
diff options
context:
space:
mode:
Diffstat (limited to 'client.c')
-rw-r--r--client.c433
1 files changed, 297 insertions, 136 deletions
diff --git a/client.c b/client.c
index 36f8008..23cde36 100644
--- a/client.c
+++ b/client.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+/* Copyright (C) 2019  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
@@ -27,6 +27,7 @@
 
 #include <assert.h>
 #include <err.h>
+#include <fcntl.h>
 #include <regex.h>
 #include <stdarg.h>
 #include <stdbool.h>
@@ -41,69 +42,87 @@
 
 #include "bounce.h"
 
-enum Cap clientCaps = CapServerTime | CapConsumer | CapPassive | CapSTS;
+enum Cap clientCaps = 0
+	| CapConsumer
+	| CapPassive
+	| CapReadMarker
+	| CapSTS
+	| CapSelfMessage
+	| CapServerTime;
+
+char *clientOrigin;
 char *clientPass;
 char *clientAway;
+char *clientQuit;
 
 static size_t active;
 
-enum Need {
-	BIT(NeedNick),
-	BIT(NeedUser),
-	BIT(NeedPass),
-	BIT(NeedCapEnd),
-};
+static void activeIncr(const struct Client *client) {
+	if (client->need) return;
+	if (client->caps & CapPassive) return;
+	if (!active++) {
+		serverEnqueue("AWAY\r\n");
+	}
+}
 
-struct Client {
-	struct tls *tls;
-	enum Need need;
-	size_t consumer;
-	size_t setPos;
-	enum Cap caps;
-	char buf[MessageCap];
-	size_t len;
-	bool error;
-};
+static void activeDecr(const struct Client *client) {
+	if (client->need) return;
+	if (client->caps & CapPassive) return;
+	if (!--active && !stateAway) {
+		serverEnqueue("AWAY :%s\r\n", clientAway);
+	}
+}
 
-struct Client *clientAlloc(struct tls *tls) {
+struct Client *clientAlloc(int sock, struct tls *tls) {
 	struct Client *client = calloc(1, sizeof(*client));
 	if (!client) err(EX_OSERR, "calloc");
+	fcntl(sock, F_SETFL, O_NONBLOCK);
+	client->sock = sock;
 	client->tls = tls;
-	client->need = NeedNick | NeedUser | (clientPass ? NeedPass : 0);
-	if ((clientCaps & CapSASL) && tls_peer_cert_provided(tls)) {
+	client->time = time(NULL);
+	client->idle = client->time;
+	client->need = NeedHandshake | NeedNick | NeedUser;
+	if (clientPass) client->need |= NeedPass;
+	return client;
+}
+
+static void clientHandshake(struct Client *client) {
+	int error = tls_handshake(client->tls);
+	if (error == TLS_WANT_POLLIN || error == TLS_WANT_POLLOUT) return;
+	if (error) {
+		warnx("client tls_handshake: %s", tls_error(client->tls));
+		client->remove = true;
+		return;
+	}
+	client->need &= ~NeedHandshake;
+	if ((clientCaps & CapSASL) && tls_peer_cert_provided(client->tls)) {
 		client->need &= ~NeedPass;
 	}
-	return client;
 }
 
 void clientFree(struct Client *client) {
-	if (!client->need) {
-		if (!(client->caps & CapPassive) && !--active) {
-			serverEnqueue("AWAY :%s\r\n", clientAway);
-		}
-	}
+	activeDecr(client);
 	tls_close(client->tls);
 	tls_free(client->tls);
 	free(client);
 }
 
-bool clientError(const struct Client *client) {
-	return client->error;
-}
-
 void clientSend(struct Client *client, const char *ptr, size_t len) {
-	if (verbose) fprintf(stderr, "\x1B[34m%.*s\x1B[m", (int)len, ptr);
+	verboseLog("<-", ptr, len);
+	fcntl(client->sock, F_SETFL, 0);
 	while (len) {
 		ssize_t ret = tls_write(client->tls, ptr, len);
 		if (ret == TLS_WANT_POLLIN || ret == TLS_WANT_POLLOUT) continue;
 		if (ret < 0) {
 			warnx("client tls_write: %s", tls_error(client->tls));
-			client->error = true;
-			return;
+			client->remove = true;
+			break;
 		}
 		ptr += ret;
 		len -= ret;
 	}
+	fcntl(client->sock, F_SETFL, O_NONBLOCK);
+	client->idle = time(NULL);
 }
 
 void clientFormat(struct Client *client, const char *format, ...) {
@@ -121,9 +140,9 @@ static void passRequired(struct Client *client) {
 		client,
 		":%s 464 * :Password incorrect\r\n"
 		"ERROR :Password incorrect\r\n",
-		ORIGIN
+		clientOrigin
 	);
-	client->error = true;
+	client->remove = true;
 }
 
 static void maybeSync(struct Client *client) {
@@ -131,9 +150,7 @@ static void maybeSync(struct Client *client) {
 	if (!client->need) {
 		stateSync(client);
 		if (client->setPos) ringSet(client->consumer, client->setPos);
-		if (!(client->caps & CapPassive) && !active++) {
-			serverEnqueue("AWAY\r\n");
-		}
+		activeIncr(client);
 	}
 }
 
@@ -147,7 +164,7 @@ static void handleNick(struct Client *client, struct Message *msg) {
 
 static void handleUser(struct Client *client, struct Message *msg) {
 	if (!msg->params[0]) {
-		client->error = true;
+		client->remove = true;
 		return;
 	}
 	if (client->need & NeedPass) {
@@ -163,7 +180,7 @@ static void handleUser(struct Client *client, struct Message *msg) {
 static void handlePass(struct Client *client, struct Message *msg) {
 	if (!clientPass) return;
 	if (!msg->params[0]) {
-		client->error = true;
+		client->remove = true;
 		return;
 	}
 #ifdef __OpenBSD__
@@ -171,13 +188,13 @@ static void handlePass(struct Client *client, struct Message *msg) {
 #else
 	int error = strcmp(crypt(msg->params[0], clientPass), clientPass);
 #endif
+	explicit_bzero(msg->params[0], strlen(msg->params[0]));
 	if (error) {
 		passRequired(client);
 	} else {
 		client->need &= ~NeedPass;
 		maybeSync(client);
 	}
-	explicit_bzero(msg->params[0], strlen(msg->params[0]));
 }
 
 static void handleCap(struct Client *client, struct Message *msg) {
@@ -202,12 +219,12 @@ static void handleCap(struct Client *client, struct Message *msg) {
 			if (avail & CapCapNotify) client->caps |= CapCapNotify;
 			clientFormat(
 				client, ":%s CAP * LS :%s\r\n",
-				ORIGIN, capList(avail, values)
+				clientOrigin, capList(avail, values)
 			);
 		} else {
 			clientFormat(
 				client, ":%s CAP * LS :%s\r\n",
-				ORIGIN, capList(avail, NULL)
+				clientOrigin, capList(avail, NULL)
 			);
 		}
 
@@ -215,102 +232,152 @@ static void handleCap(struct Client *client, struct Message *msg) {
 		if (client->need) client->need |= NeedCapEnd;
 		enum Cap caps = capParse(msg->params[1], values);
 		if (caps == (avail & caps)) {
-			client->caps |= caps;
 			if (caps & CapConsumer && values[CapConsumerBit]) {
 				client->setPos = strtoull(values[CapConsumerBit], NULL, 10);
 			}
-			clientFormat(client, ":%s CAP * ACK :%s\r\n", ORIGIN, msg->params[1]);
+			if (caps & CapPassive && !(client->caps & CapPassive)) {
+				activeDecr(client);
+			}
+			client->caps |= caps;
+			clientFormat(
+				client, ":%s CAP * ACK :%s\r\n",
+				clientOrigin, msg->params[1]
+			);
 		} else {
-			clientFormat(client, ":%s CAP * NAK :%s\r\n", ORIGIN, msg->params[1]);
+			clientFormat(
+				client, ":%s CAP * NAK :%s\r\n",
+				clientOrigin, msg->params[1]
+			);
 		}
 
 	} else if (!strcmp(msg->params[0], "LIST")) {
 		clientFormat(
 			client, ":%s CAP * LIST :%s\r\n",
-			ORIGIN, capList(client->caps, NULL)
+			clientOrigin, capList(client->caps, NULL)
 		);
 
 	} else {
-		clientFormat(client, ":%s 410 * :Invalid CAP subcommand\r\n", ORIGIN);
+		clientFormat(
+			client, ":%s 410 * :Invalid CAP subcommand\r\n", clientOrigin
+		);
 	}
 }
 
 static void handleAuthenticate(struct Client *client, struct Message *msg) {
 	if (!msg->params[0]) msg->params[0] = "";
-	if (!strcmp(msg->params[0], "EXTERNAL")) {
+	bool cert = (clientCaps & CapSASL) && tls_peer_cert_provided(client->tls);
+	if (cert && !strcmp(msg->params[0], "EXTERNAL")) {
 		clientFormat(client, "AUTHENTICATE +\r\n");
-	} else if (!strcmp(msg->params[0], "+")) {
+	} else if (cert && !strcmp(msg->params[0], "+")) {
+		const char *account = (stateAccount ? stateAccount : "*");
 		clientFormat(
-			client, ":%s 900 * %s * :You are now logged in as *\r\n",
-			ORIGIN, stateEcho()
+			client, ":%s 900 * %s %s :You are now logged in as %s\r\n",
+			clientOrigin, stateEcho(), account, account
 		);
 		clientFormat(
 			client, ":%s 903 * :SASL authentication successful\r\n",
-			ORIGIN
+			clientOrigin
 		);
 	} else {
 		clientFormat(
 			client, ":%s 904 * :SASL authentication failed\r\n",
-			ORIGIN
+			clientOrigin
 		);
 	}
 }
 
-static void handleQuit(struct Client *client, struct Message *msg) {
+static void handleJoin(struct Client *client, struct Message *msg) {
+	(void)client;
 	(void)msg;
-	clientFormat(client, "ERROR :Detaching\r\n");
-	client->error = true;
+	// irssi intentionally sends an invalid JOIN command, at
+	// an invalid time (during client registration), on every
+	// connection. Utterly mind-boggling. Ignore it so the
+	// connection doesn't just get dropped like it deserves to be.
+}
+
+static void handleQuit(struct Client *client, struct Message *msg) {
+	const char *mesg = msg->params[0];
+	if (mesg && !strncmp(mesg, "$pounce", 7) && (!mesg[7] || mesg[7] == ' ')) {
+		mesg += 7;
+		mesg += strspn(mesg, " ");
+		clientQuit = strdup(mesg);
+		if (!clientQuit) err(EX_OSERR, "strdup");
+	} else {
+		clientFormat(client, "ERROR :Detaching\r\n");
+		client->remove = true;
+	}
+}
+
+static bool hasTag(const char *tags, const char *tag) {
+	if (!tags) return false;
+	size_t len = strlen(tag);
+	bool val = strchr(tag, '=');
+	while (*tags && *tags != ' ') {
+		if (
+			!strncmp(tags, tag, len) &&
+			(!tags[len] || strchr((val ? "; " : "=; "), tags[len]))
+		) return true;
+		tags += strcspn(tags, "; ");
+		tags += (*tags == ';');
+	}
+	return false;
+}
+
+static const char *synthLabel(struct Client *client) {
+	enum { LabelCap = 64 };
+	static char buf[sizeof("label=") + LabelCap];
+	snprintf(buf, sizeof(buf), "label=pounce~%zu", client->consumer);
+	return buf;
 }
 
 static void reserialize(
 	char *buf, size_t cap, const char *origin, const struct Message *msg
 ) {
-	size_t len = 0;
+	char *ptr = buf, *end = &buf[cap];
 	if (msg->tags) {
-		len += snprintf(&buf[len], cap - len, "@%s ", msg->tags);
-		if (len >= cap) return;
+		ptr = seprintf(ptr, end, "@%s ", msg->tags);
 	}
-	if (!origin) origin = msg->origin;
-	if (origin) {
-		len += snprintf(&buf[len], cap - len, ":%s ", origin);
-		if (len >= cap) return;
+	if (origin || msg->origin) {
+		ptr = seprintf(ptr, end, ":%s ", (origin ? origin : msg->origin));
 	}
-	len += snprintf(&buf[len], cap - len, "%s", msg->cmd);
-	if (len >= cap) return;
+	ptr = seprintf(ptr, end, "%s", msg->cmd);
 	for (size_t i = 0; i < ParamCap && msg->params[i]; ++i) {
 		if (i + 1 == ParamCap || !msg->params[i + 1]) {
-			len += snprintf(&buf[len], cap - len, " :%s", msg->params[i]);
+			ptr = seprintf(ptr, end, " :%s", msg->params[i]);
 		} else {
-			len += snprintf(&buf[len], cap - len, " %s", msg->params[i]);
+			ptr = seprintf(ptr, end, " %s", msg->params[i]);
 		}
-		if (len >= cap) return;
 	}
 }
 
 static void clientProduce(struct Client *client, const char *line) {
 	size_t diff = ringDiff(client->consumer);
 	ringProduce(line);
-	if (!diff) ringConsume(NULL, client->consumer);
+	if (!diff && !(client->caps & CapEchoMessage)) {
+		ringConsume(NULL, client->consumer);
+	}
 }
 
 static void handlePrivmsg(struct Client *client, struct Message *msg) {
 	if (!msg->params[0]) return;
 	char buf[MessageCap];
-	reserialize(buf, sizeof(buf), stateEcho(), msg);
-	clientProduce(client, buf);
-	if (!strcmp(msg->params[0], stateNick())) return;
-	reserialize(buf, sizeof(buf), NULL, msg);
-	serverFormat("%s\r\n", buf);
-}
-
-static void handleTagmsg(struct Client *client, struct Message *msg) {
-	if (!msg->params[0]) return;
-	char buf[MessageCap];
-	reserialize(buf, sizeof(buf), stateEcho(), msg);
-	clientProduce(client, buf);
-	if (!strcmp(msg->params[0], stateNick())) return;
+	bool self = !strcmp(msg->params[0], stateNick());
+	if (!(stateCaps & CapEchoMessage) || self) {
+		reserialize(buf, sizeof(buf), stateEcho(), msg);
+		clientProduce(client, buf);
+	}
+	if (self) return;
 	reserialize(buf, sizeof(buf), NULL, msg);
-	serverFormat("%s\r\n", buf);
+	if (stateCaps & CapEchoMessage && !hasTag(msg->tags, "label")) {
+		serverFormat(
+			"@%s%c%s\r\n",
+			synthLabel(client),
+			(buf[0] == '@' ? ';' : ' '),
+			(buf[0] == '@' ? &buf[1] : buf)
+		);
+	} else {
+		serverFormat("%s\r\n", buf);
+	}
 }
 
 static void handlePalaver(struct Client *client, struct Message *msg) {
@@ -320,6 +387,103 @@ static void handlePalaver(struct Client *client, struct Message *msg) {
 	clientProduce(client, buf);
 }
 
+struct Marker {
+	char *target;
+	char *timestamp;
+};
+
+static struct {
+	struct Marker *ptr;
+	size_t cap, len;
+} markers;
+
+void clientGetMarker(struct Client *client, const char *target) {
+	for (size_t i = 0; i < markers.len; ++i) {
+		struct Marker marker = markers.ptr[i];
+		if (strcasecmp(marker.target, target)) continue;
+		clientFormat(
+			client, ":%s MARKREAD %s timestamp=%s\r\n",
+			clientOrigin, target, marker.timestamp
+		);
+		return;
+	}
+	clientFormat(client, ":%s MARKREAD %s *\r\n", clientOrigin, target);
+}
+
+static void clientSetMarker(
+	struct Client *client, const char *target, const char *timestamp
+) {
+	struct Marker *marker = NULL;
+	for (size_t i = 0; i < markers.len; ++i) {
+		marker = &markers.ptr[i];
+		if (strcasecmp(marker->target, target)) continue;
+		if (strcmp(timestamp, marker->timestamp) < 0) {
+			clientFormat(
+				client, ":%s MARKREAD %s timestamp=%s\r\n",
+				clientOrigin, target, marker->timestamp
+			);
+			return;
+		}
+		set(&marker->timestamp, timestamp);
+		goto notify;
+	}
+	if (markers.len == markers.cap) {
+		markers.cap = (markers.cap ? markers.cap * 2 : 8);
+		markers.ptr = realloc(markers.ptr, sizeof(*markers.ptr) * markers.cap);
+		if (!markers.ptr) err(EX_OSERR, "realloc");
+	}
+	marker = &markers.ptr[markers.len++];
+	*marker = (struct Marker) {0};
+	set(&marker->target, target);
+	set(&marker->timestamp, timestamp);
+notify:;
+	char buf[512];
+	snprintf(
+		buf, sizeof(buf), ":%s MARKREAD %s timestamp=%s",
+		clientOrigin, marker->target, marker->timestamp
+	);
+	ringProduce(buf);
+}
+
+static regex_t *TimestampRegex(void) {
+	static const char *Pattern = {
+#define R2D "[0-9]{2}"
+		"^timestamp=[0-9]{4,}-" R2D "-" R2D
+		"T" R2D ":" R2D ":" R2D "[.][0-9]{3}Z$"
+#undef R2D
+	};
+	static bool compiled;
+	static regex_t regex;
+	if (!compiled) {
+		int error = regcomp(&regex, Pattern, REG_EXTENDED | REG_NOSUB);
+		assert(!error);
+	}
+	compiled = true;
+	return &regex;
+}
+
+static void handleMarkRead(struct Client *client, struct Message *msg) {
+	if (!msg->params[0]) {
+		clientFormat(
+			client, "FAIL MARKREAD NEED_MORE_PARAMS :Missing parameters\r\n"
+		);
+	} else if (!msg->params[1]) {
+		clientGetMarker(client, msg->params[0]);
+	} else if (regexec(TimestampRegex(), msg->params[1], 0, NULL, 0)) {
+		clientFormat(
+			client, "FAIL MARKREAD INVALID_PARAMS %s :Invalid parameters\r\n",
+			msg->params[1]
+		);
+	} else {
+		clientSetMarker(client, msg->params[0], &msg->params[1][10]);
+	}
+}
+
+static void handlePong(struct Client *client, struct Message *msg) {
+	(void)client;
+	(void)msg;
+}
+
 static const struct {
 	bool intercept;
 	bool need;
@@ -327,15 +491,18 @@ static const struct {
 	Handler *fn;
 } Handlers[] = {
 	{ false, false, "AUTHENTICATE", handleAuthenticate },
+	{ false, false, "JOIN", handleJoin },
 	{ false, false, "NICK", handleNick },
 	{ false, false, "PASS", handlePass },
 	{ false, false, "USER", handleUser },
 	{ true, false, "CAP", handleCap },
 	{ true, false, "PALAVER", handlePalaver },
+	{ true, false, "PONG", handlePong },
+	{ true, true, "MARKREAD", handleMarkRead },
 	{ true, true, "NOTICE", handlePrivmsg },
 	{ true, true, "PRIVMSG", handlePrivmsg },
 	{ true, true, "QUIT", handleQuit },
-	{ true, true, "TAGMSG", handleTagmsg },
+	{ true, true, "TAGMSG", handlePrivmsg },
 };
 
 static void clientParse(struct Client *client, char *line) {
@@ -347,7 +514,7 @@ static void clientParse(struct Client *client, char *line) {
 		Handlers[i].fn(client, &msg);
 		return;
 	}
-	client->error = true;
+	client->remove = true;
 }
 
 static bool intercept(const char *line, size_t len) {
@@ -369,6 +536,11 @@ static bool intercept(const char *line, size_t len) {
 }
 
 void clientRecv(struct Client *client) {
+	if (client->need & NeedHandshake) {
+		clientHandshake(client);
+		return;
+	}
+
 	ssize_t read = tls_read(
 		client->tls,
 		&client->buf[client->len], sizeof(client->buf) - client->len
@@ -376,7 +548,7 @@ void clientRecv(struct Client *client) {
 	if (read == TLS_WANT_POLLIN || read == TLS_WANT_POLLOUT) return;
 	if (read <= 0) {
 		if (read < 0) warnx("client tls_read: %s", tls_error(client->tls));
-		client->error = true;
+		client->remove = true;
 		return;
 	}
 	client->len += read;
@@ -386,9 +558,7 @@ void clientRecv(struct Client *client) {
 	for (;;) {
 		lf = memchr(line, '\n', &client->buf[client->len] - line);
 		if (!lf) break;
-		if (verbose) {
-			fprintf(stderr, "\x1B[33m%.*s\x1B[m\n", (int)(lf - line), line);
-		}
+		verboseLog("->", line, lf - line);
 		if (client->need || intercept(line, lf - line)) {
 			lf[0] = '\0';
 			if (lf - line && lf[-1] == '\r') lf[-1] = '\0';
@@ -400,11 +570,7 @@ void clientRecv(struct Client *client) {
 	}
 	client->len -= line - client->buf;
 	memmove(client->buf, line, client->len);
-}
-
-size_t clientDiff(const struct Client *client) {
-	if (client->need) return 0;
-	return ringDiff(client->consumer);
+	client->idle = time(NULL);
 }
 
 static int wordcmp(const char *line, size_t i, const char *word) {
@@ -426,34 +592,22 @@ static int wordcmp(const char *line, size_t i, const char *word) {
 		: (int)len - (int)strlen(word);
 }
 
-static size_t strlcpyn(char *dst, const char *src, size_t cap, size_t len) {
-	if (len < cap) {
-		memcpy(dst, src, len);
-		dst[len] = '\0';
-	} else {
-		memcpy(dst, src, cap - 1);
-		dst[cap - 1] = '\0';
-	}
-	return len;
-}
-
 // s/..(..)../\1/g
-static char *snip(char *dst, size_t cap, const char *src, const regex_t *regex) {
-	size_t len = 0;
+static char *
+snip(char *dst, size_t cap, const char *src, const regex_t *regex) {
+	char *ptr = dst, *end = &dst[cap];
 	regmatch_t match[2];
 	assert(regex->re_nsub);
 	for (; *src; src += match[0].rm_eo) {
 		if (regexec(regex, src, 2, match, 0)) break;
-		len += strlcpyn(&dst[len], src, cap - len, match[0].rm_so);
-		if (len >= cap) return NULL;
-		len += strlcpyn(
-			&dst[len], &src[match[1].rm_so],
-			cap - len, match[1].rm_eo - match[1].rm_so
+		ptr = seprintf(
+			ptr, end, "%.*s%.*s",
+			(int)match[0].rm_so, src,
+			(int)(match[1].rm_eo - match[1].rm_so), &src[match[1].rm_so]
 		);
-		if (len >= cap) return NULL;
 	}
-	len += snprintf(&dst[len], cap - len, "%s", src);
-	return (len < cap ? dst : NULL);
+	ptr = seprintf(ptr, end, "%s", src);
+	return (ptr == end ? NULL : dst);
 }
 
 static regex_t *compile(regex_t *regex, const char *pattern) {
@@ -531,6 +685,10 @@ static const char *filterMultiPrefix(const char *line) {
 	}
 }
 
+static const char *filterReadMarker(const char *line) {
+	return (wordcmp(line, 0, "MARKREAD") ? line : NULL);
+}
+
 static const char *filterPalaverApp(const char *line) {
 	return (wordcmp(line, 0, "PALAVER") ? line : NULL);
 }
@@ -549,13 +707,7 @@ static const char *filterUserhostInNames(const char *line) {
 	);
 }
 
-static const char *filterTags(const char *line) {
-	if (line[0] != '@') return line;
-	const char *sp = strchr(line, ' ');
-	return (sp ? sp + 1 : NULL);
-}
-
-static Filter *Filters[] = {
+static Filter *Filters[CapBits] = {
 	[CapAccountNotifyBit] = filterAccountNotify,
 	[CapAwayNotifyBit] = filterAwayNotify,
 	[CapBatchBit] = filterBatch,
@@ -567,18 +719,21 @@ static Filter *Filters[] = {
 	[CapMessageTagsBit] = filterMessageTags,
 	[CapMultiPrefixBit] = filterMultiPrefix,
 	[CapPalaverAppBit] = filterPalaverApp,
+	[CapReadMarkerBit] = filterReadMarker,
 	[CapSetnameBit] = filterSetname,
 	[CapUserhostInNamesBit] = filterUserhostInNames,
 };
 
-static bool hasTime(const char *line) {
-	if (!strncmp(line, "@time=", 6)) return true;
-	while (*line && *line != ' ') {
-		line += strcspn(line, "; ");
-		if (!strncmp(line, ";time=", 6)) return true;
-		if (*line == ';') line++;
-	}
-	return false;
+static const char *filterEchoMessage(struct Client *client, const char *line) {
+	if (line[0] != '@') return line;
+	if (!hasTag(&line[1], synthLabel(client))) return line;
+	return NULL;
+}
+
+static const char *filterTags(const char *line) {
+	if (line[0] != '@') return line;
+	const char *sp = strchr(line, ' ');
+	return (sp ? sp + 1 : NULL);
 }
 
 void clientConsume(struct Client *client) {
@@ -586,10 +741,13 @@ void clientConsume(struct Client *client) {
 	const char *line = ringPeek(&time, client->consumer);
 	if (!line) return;
 
-	if (stateCaps & TagCaps && !(client->caps & TagCaps)) {
+	enum Cap diff = client->caps ^ (clientCaps | stateCaps);
+	if (diff & CapEchoMessage) {
+		line = filterEchoMessage(client, line);
+	}
+	if (line && stateCaps & TagCaps && !(client->caps & TagCaps)) {
 		line = filterTags(line);
 	}
-	enum Cap diff = client->caps ^ (clientCaps | stateCaps);
 	for (size_t i = 0; line && i < ARRAY_LEN(Filters); ++i) {
 		if (!Filters[i]) continue;
 		if (diff & (1 << i)) line = Filters[i](line);
@@ -599,7 +757,10 @@ void clientConsume(struct Client *client) {
 		return;
 	}
 
-	if (client->caps & CapServerTime && !hasTime(line)) {
+	if (
+		client->caps & CapServerTime &&
+		(line[0] != '@' || !hasTag(&line[1], "time"))
+	) {
 		char ts[sizeof("YYYY-MM-DDThh:mm:ss")];
 		struct tm *tm = gmtime(&time.tv_sec);
 		strftime(ts, sizeof(ts), "%FT%T", tm);
@@ -620,5 +781,5 @@ void clientConsume(struct Client *client) {
 	} else {
 		clientFormat(client, "%s\r\n", line);
 	}
-	if (!client->error) ringConsume(NULL, client->consumer);
+	if (!client->remove) ringConsume(NULL, client->consumer);
 }