1 /* Copyright (C) 2020  C. McEnroe <june@causal.agency>
2  *
3  * This program is free software: you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation, either version 3 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
15  *
16  * Additional permission under GNU GPL version 3 section 7:
17  *
18  * If you modify this Program, or any covered work, by linking or
19  * combining it with OpenSSL (or a modified version of that library),
20  * containing parts covered by the terms of the OpenSSL License and the
21  * original SSLeay license, the licensors of this Program grant you
22  * additional permission to convey the resulting work. Corresponding
23  * Source for a non-source form of such a combination shall include the
24  * source code for the parts of OpenSSL used as well as that of the
25  * covered work.
26  */
27 
28 #include <assert.h>
29 #include <ctype.h>
30 #include <err.h>
31 #include <stdbool.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <sysexits.h>
36 #include <wchar.h>
37 
38 #include "chat.h"
39 
40 uint replies[ReplyCap];
41 
42 static const char *CapNames[] = {
43 #define X(name, id) [id##Bit] = name,
44 	ENUM_CAP
45 #undef X
46 };
47 
capParse(const char * list)48 static enum Cap capParse(const char *list) {
49 	enum Cap caps = 0;
50 	while (*list) {
51 		enum Cap cap = 0;
52 		size_t len = strcspn(list, " ");
53 		for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) {
54 			if (len != strlen(CapNames[i])) continue;
55 			if (strncmp(list, CapNames[i], len)) continue;
56 			cap = 1 << i;
57 			break;
58 		}
59 		caps |= cap;
60 		list += len;
61 		if (*list) list++;
62 	}
63 	return caps;
64 }
65 
capList(char * buf,size_t cap,enum Cap caps)66 static void capList(char *buf, size_t cap, enum Cap caps) {
67 	*buf = '\0';
68 	char *ptr = buf, *end = &buf[cap];
69 	for (size_t i = 0; i < ARRAY_LEN(CapNames); ++i) {
70 		if (caps & (1 << i)) {
71 			ptr = seprintf(
72 				ptr, end, "%s%s", (ptr > buf ? " " : ""), CapNames[i]
73 			);
74 		}
75 	}
76 }
77 
require(struct Message * msg,bool origin,uint len)78 static void require(struct Message *msg, bool origin, uint len) {
79 	if (origin) {
80 		if (!msg->nick) msg->nick = "*.*";
81 		if (!msg->user) msg->user = msg->nick;
82 		if (!msg->host) msg->host = msg->user;
83 	}
84 	for (uint i = 0; i < len; ++i) {
85 		if (msg->params[i]) continue;
86 		errx(EX_PROTOCOL, "%s missing parameter %u", msg->cmd, 1 + i);
87 	}
88 }
89 
tagTime(const struct Message * msg)90 static const time_t *tagTime(const struct Message *msg) {
91 	static time_t time;
92 	struct tm tm;
93 	if (!msg->tags[TagTime]) return NULL;
94 	if (!strptime(msg->tags[TagTime], "%Y-%m-%dT%T", &tm)) return NULL;
95 	time = timegm(&tm);
96 	return &time;
97 }
98 
99 typedef void Handler(struct Message *msg);
100 
handleStandardReply(struct Message * msg)101 static void handleStandardReply(struct Message *msg) {
102 	require(msg, false, 3);
103 	for (uint i = 2; i < ParamCap - 1; ++i) {
104 		if (msg->params[i + 1]) continue;
105 		uiFormat(
106 			Network, Warm, tagTime(msg),
107 			"%s", msg->params[i]
108 		);
109 		break;
110 	}
111 }
112 
handleErrorGeneric(struct Message * msg)113 static void handleErrorGeneric(struct Message *msg) {
114 	require(msg, false, 2);
115 	if (msg->params[2]) {
116 		size_t len = strlen(msg->params[2]);
117 		if (msg->params[2][len - 1] == '.') msg->params[2][len - 1] = '\0';
118 		uiFormat(
119 			Network, Warm, tagTime(msg),
120 			"%s: %s", msg->params[2], msg->params[1]
121 		);
122 	} else {
123 		uiFormat(
124 			Network, Warm, tagTime(msg),
125 			"%s", msg->params[1]
126 		);
127 	}
128 }
129 
handleErrorNicknameInUse(struct Message * msg)130 static void handleErrorNicknameInUse(struct Message *msg) {
131 	require(msg, false, 2);
132 	if (!strcmp(self.nick, "*")) {
133 		ircFormat("NICK :%s_\r\n", msg->params[1]);
134 	} else {
135 		handleErrorGeneric(msg);
136 	}
137 }
138 
handleErrorErroneousNickname(struct Message * msg)139 static void handleErrorErroneousNickname(struct Message *msg) {
140 	require(msg, false, 3);
141 	if (!strcmp(self.nick, "*")) {
142 		errx(EX_CONFIG, "%s: %s", msg->params[1], msg->params[2]);
143 	} else {
144 		handleErrorGeneric(msg);
145 	}
146 }
147 
handleCap(struct Message * msg)148 static void handleCap(struct Message *msg) {
149 	require(msg, false, 3);
150 	enum Cap caps = capParse(msg->params[2]);
151 	if (!strcmp(msg->params[1], "LS")) {
152 		caps &= ~CapSASL;
153 		if (caps & CapConsumer && self.pos) {
154 			ircFormat("CAP REQ %s=%zu\r\n", CapNames[CapConsumerBit], self.pos);
155 			caps &= ~CapConsumer;
156 		}
157 		if (caps) {
158 			char buf[512];
159 			capList(buf, sizeof(buf), caps);
160 			ircFormat("CAP REQ :%s\r\n", buf);
161 		} else {
162 			if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n");
163 		}
164 	} else if (!strcmp(msg->params[1], "ACK")) {
165 		self.caps |= caps;
166 		if (caps & CapSASL) {
167 			ircFormat("AUTHENTICATE %s\r\n", (self.plain ? "PLAIN" : "EXTERNAL"));
168 		}
169 		if (!(self.caps & CapSASL)) ircFormat("CAP END\r\n");
170 	} else if (!strcmp(msg->params[1], "NAK")) {
171 		errx(EX_CONFIG, "server does not support %s", msg->params[2]);
172 	}
173 }
174 
175 #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4)
176 
base64(char * dst,const byte * src,size_t len)177 static void base64(char *dst, const byte *src, size_t len) {
178 	static const char Base64[64] = {
179 		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
180 	};
181 	size_t i = 0;
182 	while (len > 2) {
183 		dst[i++] = Base64[0x3F & (src[0] >> 2)];
184 		dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)];
185 		dst[i++] = Base64[0x3F & (src[1] << 2 | src[2] >> 6)];
186 		dst[i++] = Base64[0x3F & src[2]];
187 		src += 3;
188 		len -= 3;
189 	}
190 	if (len) {
191 		dst[i++] = Base64[0x3F & (src[0] >> 2)];
192 		if (len > 1) {
193 			dst[i++] = Base64[0x3F & (src[0] << 4 | src[1] >> 4)];
194 			dst[i++] = Base64[0x3F & (src[1] << 2)];
195 		} else {
196 			dst[i++] = Base64[0x3F & (src[0] << 4)];
197 			dst[i++] = '=';
198 		}
199 		dst[i++] = '=';
200 	}
201 	dst[i] = '\0';
202 }
203 
handleAuthenticate(struct Message * msg)204 static void handleAuthenticate(struct Message *msg) {
205 	(void)msg;
206 	if (!self.plain) {
207 		ircFormat("AUTHENTICATE +\r\n");
208 		return;
209 	}
210 
211 	byte buf[299] = {0};
212 	size_t len = 1 + strlen(self.plain);
213 	if (sizeof(buf) < len) errx(EX_USAGE, "SASL PLAIN is too long");
214 	memcpy(&buf[1], self.plain, len - 1);
215 	byte *sep = memchr(buf, ':', len);
216 	if (!sep) errx(EX_USAGE, "SASL PLAIN missing colon");
217 	*sep = 0;
218 
219 	char b64[BASE64_SIZE(sizeof(buf))];
220 	base64(b64, buf, len);
221 	ircFormat("AUTHENTICATE ");
222 	ircSend(b64, BASE64_SIZE(len));
223 	ircFormat("\r\n");
224 
225 	explicit_bzero(b64, sizeof(b64));
226 	explicit_bzero(buf, sizeof(buf));
227 	explicit_bzero(self.plain, strlen(self.plain));
228 }
229 
handleReplyLoggedIn(struct Message * msg)230 static void handleReplyLoggedIn(struct Message *msg) {
231 	(void)msg;
232 	ircFormat("CAP END\r\n");
233 }
234 
handleErrorSASLFail(struct Message * msg)235 static void handleErrorSASLFail(struct Message *msg) {
236 	require(msg, false, 2);
237 	errx(EX_CONFIG, "%s", msg->params[1]);
238 }
239 
handleReplyWelcome(struct Message * msg)240 static void handleReplyWelcome(struct Message *msg) {
241 	require(msg, false, 1);
242 	set(&self.nick, msg->params[0]);
243 	completeTouch(Network, self.nick, Default);
244 	if (self.mode) ircFormat("MODE %s %s\r\n", self.nick, self.mode);
245 	if (self.join) {
246 		uint count = 1;
247 		for (const char *ch = self.join; *ch && *ch != ' '; ++ch) {
248 			if (*ch == ',') count++;
249 		}
250 		ircFormat("JOIN %s\r\n", self.join);
251 		if (count == 1) replies[ReplyJoin]++;
252 		replies[ReplyTopicAuto] += count;
253 		replies[ReplyNamesAuto] += count;
254 	}
255 }
256 
handleReplyISupport(struct Message * msg)257 static void handleReplyISupport(struct Message *msg) {
258 	for (uint i = 1; i < ParamCap; ++i) {
259 		if (!msg->params[i]) break;
260 		char *key = strsep(&msg->params[i], "=");
261 		if (!strcmp(key, "NETWORK")) {
262 			if (!msg->params[i]) continue;
263 			set(&network.name, msg->params[i]);
264 			uiFormat(
265 				Network, Cold, tagTime(msg),
266 				"You arrive in %s", msg->params[i]
267 			);
268 		} else if (!strcmp(key, "USERLEN")) {
269 			if (!msg->params[i]) continue;
270 			network.userLen = strtoul(msg->params[i], NULL, 10);
271 		} else if (!strcmp(key, "HOSTLEN")) {
272 			if (!msg->params[i]) continue;
273 			network.hostLen = strtoul(msg->params[i], NULL, 10);
274 		} else if (!strcmp(key, "CHANTYPES")) {
275 			if (!msg->params[i]) continue;
276 			set(&network.chanTypes, msg->params[i]);
277 		} else if (!strcmp(key, "STATUSMSG")) {
278 			if (!msg->params[i]) continue;
279 			set(&network.statusmsg, msg->params[i]);
280 		} else if (!strcmp(key, "PREFIX")) {
281 			strsep(&msg->params[i], "(");
282 			char *modes = strsep(&msg->params[i], ")");
283 			char *prefixes = msg->params[i];
284 			if (!modes || !prefixes || strlen(modes) != strlen(prefixes)) {
285 				errx(EX_PROTOCOL, "invalid PREFIX value");
286 			}
287 			set(&network.prefixModes, modes);
288 			set(&network.prefixes, prefixes);
289 		} else if (!strcmp(key, "CHANMODES")) {
290 			char *list = strsep(&msg->params[i], ",");
291 			char *param = strsep(&msg->params[i], ",");
292 			char *setParam = strsep(&msg->params[i], ",");
293 			char *channel = strsep(&msg->params[i], ",");
294 			if (!list || !param || !setParam || !channel) {
295 				errx(EX_PROTOCOL, "invalid CHANMODES value");
296 			}
297 			set(&network.listModes, list);
298 			set(&network.paramModes, param);
299 			set(&network.setParamModes, setParam);
300 			set(&network.channelModes, channel);
301 		} else if (!strcmp(key, "EXCEPTS")) {
302 			network.excepts = (msg->params[i] ?: "e")[0];
303 		} else if (!strcmp(key, "INVEX")) {
304 			network.invex = (msg->params[i] ?: "I")[0];
305 		}
306 	}
307 }
308 
handleReplyMOTD(struct Message * msg)309 static void handleReplyMOTD(struct Message *msg) {
310 	require(msg, false, 2);
311 	char *line = msg->params[1];
312 	urlScan(Network, NULL, line);
313 	if (!strncmp(line, "- ", 2)) {
314 		uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]);
315 	} else {
316 		uiFormat(Network, Cold, tagTime(msg), "%s", line);
317 	}
318 }
319 
handleErrorNoMOTD(struct Message * msg)320 static void handleErrorNoMOTD(struct Message *msg) {
321 	(void)msg;
322 }
323 
handleReplyHelp(struct Message * msg)324 static void handleReplyHelp(struct Message *msg) {
325 	require(msg, false, 3);
326 	urlScan(Network, NULL, msg->params[2]);
327 	uiWrite(Network, Warm, tagTime(msg), msg->params[2]);
328 }
329 
handleJoin(struct Message * msg)330 static void handleJoin(struct Message *msg) {
331 	require(msg, true, 1);
332 	uint id = idFor(msg->params[0]);
333 	if (!strcmp(msg->nick, self.nick)) {
334 		if (!self.user || strcmp(self.user, msg->user)) {
335 			set(&self.user, msg->user);
336 			self.color = hash(msg->user);
337 		}
338 		if (!self.host || strcmp(self.host, msg->host)) {
339 			set(&self.host, msg->host);
340 		}
341 		idColors[id] = hash(msg->params[0]);
342 		completeTouch(None, msg->params[0], idColors[id]);
343 		if (replies[ReplyJoin]) {
344 			uiShowID(id);
345 			replies[ReplyJoin]--;
346 		}
347 	}
348 	completeTouch(id, msg->nick, hash(msg->user));
349 	if (msg->params[2] && !strcasecmp(msg->params[2], msg->nick)) {
350 		msg->params[2] = NULL;
351 	}
352 	uiFormat(
353 		id, filterCheck(Cold, id, msg), tagTime(msg),
354 		"\3%02d%s\3\t%s%s%sarrives in \3%02d%s\3",
355 		hash(msg->user), msg->nick,
356 		(msg->params[2] ? "(" : ""),
357 		(msg->params[2] ?: ""),
358 		(msg->params[2] ? "\17) " : ""),
359 		hash(msg->params[0]), msg->params[0]
360 	);
361 	logFormat(id, tagTime(msg), "%s arrives in %s", msg->nick, msg->params[0]);
362 }
363 
handleChghost(struct Message * msg)364 static void handleChghost(struct Message *msg) {
365 	require(msg, true, 2);
366 	if (strcmp(msg->nick, self.nick)) return;
367 	if (!self.user || strcmp(self.user, msg->params[0])) {
368 		set(&self.user, msg->params[0]);
369 		self.color = hash(msg->params[0]);
370 	}
371 	if (!self.host || strcmp(self.host, msg->params[1])) {
372 		set(&self.host, msg->params[1]);
373 	}
374 }
375 
handlePart(struct Message * msg)376 static void handlePart(struct Message *msg) {
377 	require(msg, true, 1);
378 	uint id = idFor(msg->params[0]);
379 	if (!strcmp(msg->nick, self.nick)) {
380 		completeClear(id);
381 	}
382 	completeRemove(id, msg->nick);
383 	enum Heat heat = filterCheck(Cold, id, msg);
384 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
385 	uiFormat(
386 		id, heat, tagTime(msg),
387 		"\3%02d%s\3\tleaves \3%02d%s\3%s%s",
388 		hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0],
389 		(msg->params[1] ? ": " : ""), (msg->params[1] ?: "")
390 	);
391 	logFormat(
392 		id, tagTime(msg), "%s leaves %s%s%s",
393 		msg->nick, msg->params[0],
394 		(msg->params[1] ? ": " : ""), (msg->params[1] ?: "")
395 	);
396 }
397 
handleKick(struct Message * msg)398 static void handleKick(struct Message *msg) {
399 	require(msg, true, 2);
400 	uint id = idFor(msg->params[0]);
401 	bool kicked = !strcmp(msg->params[1], self.nick);
402 	completeTouch(id, msg->nick, hash(msg->user));
403 	urlScan(id, msg->nick, msg->params[2]);
404 	uiFormat(
405 		id, (kicked ? Hot : Cold), tagTime(msg),
406 		"%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s",
407 		(kicked ? "\26" : ""),
408 		hash(msg->user), msg->nick,
409 		completeColor(id, msg->params[1]), msg->params[1],
410 		hash(msg->params[0]), msg->params[0],
411 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
412 	);
413 	logFormat(
414 		id, tagTime(msg), "%s kicks %s out of %s%s%s",
415 		msg->nick, msg->params[1], msg->params[0],
416 		(msg->params[2] ? ": " : ""), (msg->params[2] ?: "")
417 	);
418 	completeRemove(id, msg->params[1]);
419 	if (kicked) completeClear(id);
420 }
421 
handleNick(struct Message * msg)422 static void handleNick(struct Message *msg) {
423 	require(msg, true, 1);
424 	if (!strcmp(msg->nick, self.nick)) {
425 		set(&self.nick, msg->params[0]);
426 		uiRead(); // Update prompt.
427 	}
428 	for (uint id; (id = completeID(msg->nick));) {
429 		if (!strcmp(idNames[id], msg->nick)) {
430 			set(&idNames[id], msg->params[0]);
431 		}
432 		uiFormat(
433 			id, filterCheck(Cold, id, msg), tagTime(msg),
434 			"\3%02d%s\3\tis now known as \3%02d%s\3",
435 			hash(msg->user), msg->nick, hash(msg->user), msg->params[0]
436 		);
437 		if (id == Network) continue;
438 		logFormat(
439 			id, tagTime(msg), "%s is now known as %s",
440 			msg->nick, msg->params[0]
441 		);
442 	}
443 	completeReplace(None, msg->nick, msg->params[0]);
444 }
445 
handleSetname(struct Message * msg)446 static void handleSetname(struct Message *msg) {
447 	require(msg, true, 1);
448 	for (uint id; (id = completeID(msg->nick));) {
449 		uiFormat(
450 			id, filterCheck(Cold, id, msg), tagTime(msg),
451 			"\3%02d%s\3\tis now known as \3%02d%s\3 (%s\17)",
452 			hash(msg->user), msg->nick, hash(msg->user), msg->nick,
453 			msg->params[0]
454 		);
455 	}
456 }
457 
handleQuit(struct Message * msg)458 static void handleQuit(struct Message *msg) {
459 	require(msg, true, 0);
460 	for (uint id; (id = completeID(msg->nick));) {
461 		enum Heat heat = filterCheck(Cold, id, msg);
462 		if (heat > Ice) urlScan(id, msg->nick, msg->params[0]);
463 		uiFormat(
464 			id, heat, tagTime(msg),
465 			"\3%02d%s\3\tleaves%s%s",
466 			hash(msg->user), msg->nick,
467 			(msg->params[0] ? ": " : ""), (msg->params[0] ?: "")
468 		);
469 		if (id == Network) continue;
470 		logFormat(
471 			id, tagTime(msg), "%s leaves%s%s",
472 			msg->nick,
473 			(msg->params[0] ? ": " : ""), (msg->params[0] ?: "")
474 		);
475 	}
476 	completeRemove(None, msg->nick);
477 }
478 
handleInvite(struct Message * msg)479 static void handleInvite(struct Message *msg) {
480 	require(msg, true, 2);
481 	if (!strcmp(msg->params[0], self.nick)) {
482 		set(&self.invited, msg->params[1]);
483 		uiFormat(
484 			Network, filterCheck(Hot, Network, msg), tagTime(msg),
485 			"\3%02d%s\3\tinvites you to \3%02d%s\3",
486 			hash(msg->user), msg->nick, hash(msg->params[1]), msg->params[1]
487 		);
488 	} else {
489 		uint id = idFor(msg->params[1]);
490 		uiFormat(
491 			id, Cold, tagTime(msg),
492 			"\3%02d%s\3\tinvites %s to \3%02d%s\3",
493 			hash(msg->user), msg->nick,
494 			msg->params[0],
495 			hash(msg->params[1]), msg->params[1]
496 		);
497 		logFormat(
498 			id, tagTime(msg), "%s invites %s to %s",
499 			msg->nick, msg->params[0], msg->params[1]
500 		);
501 	}
502 }
503 
handleReplyInviting(struct Message * msg)504 static void handleReplyInviting(struct Message *msg) {
505 	require(msg, false, 3);
506 	struct Message invite = {
507 		.nick = self.nick,
508 		.user = self.user,
509 		.cmd = "INVITE",
510 		.params[0] = msg->params[1],
511 		.params[1] = msg->params[2],
512 	};
513 	handleInvite(&invite);
514 }
515 
handleErrorUserOnChannel(struct Message * msg)516 static void handleErrorUserOnChannel(struct Message *msg) {
517 	require(msg, false, 3);
518 	uint id = idFor(msg->params[2]);
519 	uiFormat(
520 		id, Warm, tagTime(msg),
521 		"\3%02d%s\3 is already in \3%02d%s\3",
522 		completeColor(id, msg->params[1]), msg->params[1],
523 		hash(msg->params[2]), msg->params[2]
524 	);
525 }
526 
handleReplyNames(struct Message * msg)527 static void handleReplyNames(struct Message *msg) {
528 	require(msg, false, 4);
529 	uint id = idFor(msg->params[2]);
530 	char buf[1024];
531 	char *ptr = buf, *end = &buf[sizeof(buf)];
532 	while (msg->params[3]) {
533 		char *name = strsep(&msg->params[3], " ");
534 		char *prefixes = strsep(&name, "!");
535 		char *nick = &prefixes[strspn(prefixes, network.prefixes)];
536 		char *user = strsep(&name, "@");
537 		enum Color color = (user ? hash(user) : Default);
538 		completeAdd(id, nick, color);
539 		if (!replies[ReplyNames] && !replies[ReplyNamesAuto]) continue;
540 		ptr = seprintf(
541 			ptr, end, "%s\3%02d%s\3", (ptr > buf ? ", " : ""), color, prefixes
542 		);
543 	}
544 	if (ptr == buf) return;
545 	uiFormat(
546 		id, (replies[ReplyNamesAuto] ? Cold : Warm), tagTime(msg),
547 		"In \3%02d%s\3 are %s",
548 		hash(msg->params[2]), msg->params[2], buf
549 	);
550 }
551 
handleReplyEndOfNames(struct Message * msg)552 static void handleReplyEndOfNames(struct Message *msg) {
553 	(void)msg;
554 	if (replies[ReplyNamesAuto]) {
555 		replies[ReplyNamesAuto]--;
556 	} else if (replies[ReplyNames]) {
557 		replies[ReplyNames]--;
558 	}
559 }
560 
561 static struct {
562 	char buf[1024];
563 	char *ptr;
564 	char *end;
565 } who = {
566 	.ptr = who.buf,
567 	.end = &who.buf[sizeof(who.buf)],
568 };
569 
handleReplyWho(struct Message * msg)570 static void handleReplyWho(struct Message *msg) {
571 	require(msg, false, 7);
572 	if (who.ptr == who.buf) {
573 		who.ptr = seprintf(
574 			who.ptr, who.end, "The council of \3%02d%s\3 are ",
575 			hash(msg->params[1]), msg->params[1]
576 		);
577 	}
578 	char *prefixes = &msg->params[6][1];
579 	if (prefixes[0] == '*') prefixes++;
580 	prefixes[strspn(prefixes, network.prefixes)] = '\0';
581 	if (!prefixes[0] || prefixes[0] == '+') return;
582 	who.ptr = seprintf(
583 		who.ptr, who.end, "%s\3%02d%s%s\3%s",
584 		(who.ptr[-1] == ' ' ? "" : ", "),
585 		hash(msg->params[2]), prefixes, msg->params[5],
586 		(msg->params[6][0] == 'H' ? "" : " (away)")
587 	);
588 }
589 
handleReplyEndOfWho(struct Message * msg)590 static void handleReplyEndOfWho(struct Message *msg) {
591 	require(msg, false, 2);
592 	uiWrite(idFor(msg->params[1]), Warm, tagTime(msg), who.buf);
593 	who.ptr = who.buf;
594 }
595 
handleReplyNoTopic(struct Message * msg)596 static void handleReplyNoTopic(struct Message *msg) {
597 	require(msg, false, 2);
598 	uiFormat(
599 		idFor(msg->params[1]), Warm, tagTime(msg),
600 		"There is no sign in \3%02d%s\3",
601 		hash(msg->params[1]), msg->params[1]
602 	);
603 }
604 
topicComplete(uint id,const char * topic)605 static void topicComplete(uint id, const char *topic) {
606 	char buf[512];
607 	const char *prev = complete(id, "/topic ");
608 	if (prev) {
609 		snprintf(buf, sizeof(buf), "%s", prev);
610 		completeRemove(id, buf);
611 	}
612 	if (topic) {
613 		snprintf(buf, sizeof(buf), "/topic %s", topic);
614 		completeAdd(id, buf, Default);
615 	}
616 }
617 
handleReplyTopic(struct Message * msg)618 static void handleReplyTopic(struct Message *msg) {
619 	require(msg, false, 3);
620 	uint id = idFor(msg->params[1]);
621 	topicComplete(id, msg->params[2]);
622 	if (!replies[ReplyTopic] && !replies[ReplyTopicAuto]) return;
623 	urlScan(id, NULL, msg->params[2]);
624 	uiFormat(
625 		id, (replies[ReplyTopicAuto] ? Cold : Warm), tagTime(msg),
626 		"The sign in \3%02d%s\3 reads: %s",
627 		hash(msg->params[1]), msg->params[1], msg->params[2]
628 	);
629 	logFormat(
630 		id, tagTime(msg), "The sign in %s reads: %s",
631 		msg->params[1], msg->params[2]
632 	);
633 	if (replies[ReplyTopicAuto]) {
634 		replies[ReplyTopicAuto]--;
635 	} else {
636 		replies[ReplyTopic]--;
637 	}
638 }
639 
swap(wchar_t * a,wchar_t * b)640 static void swap(wchar_t *a, wchar_t *b) {
641 	wchar_t x = *a;
642 	*a = *b;
643 	*b = x;
644 }
645 
handleTopic(struct Message * msg)646 static void handleTopic(struct Message *msg) {
647 	require(msg, true, 2);
648 	uint id = idFor(msg->params[0]);
649 	if (!msg->params[1][0]) {
650 		topicComplete(id, NULL);
651 		uiFormat(
652 			id, Warm, tagTime(msg),
653 			"\3%02d%s\3\tremoves the sign in \3%02d%s\3",
654 			hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0]
655 		);
656 		logFormat(
657 			id, tagTime(msg), "%s removes the sign in %s",
658 			msg->nick, msg->params[0]
659 		);
660 		return;
661 	}
662 
663 	char buf[1024];
664 	char *ptr = buf, *end = &buf[sizeof(buf)];
665 	const char *prev = complete(id, "/topic ");
666 	completeReject();
667 	if (prev) {
668 		prev += 7;
669 	} else {
670 		goto plain;
671 	}
672 
673 	wchar_t old[512];
674 	wchar_t new[512];
675 	if (swprintf(old, ARRAY_LEN(old), L"%s", prev) < 0) goto plain;
676 	if (swprintf(new, ARRAY_LEN(new), L"%s", msg->params[1]) < 0) goto plain;
677 
678 	if (!hashBound) {
679 		ptr = seprintf(ptr, end, "%c%ls%c -> %c%ls%c", R, old, O, R, new, O);
680 		goto plain;
681 	}
682 
683 	size_t pre;
684 	for (pre = 0; old[pre] && new[pre] && old[pre] == new[pre]; ++pre);
685 	wchar_t *osuf = &old[wcslen(old)];
686 	wchar_t *nsuf = &new[wcslen(new)];
687 	while (osuf > &old[pre] && nsuf > &new[pre] && osuf[-1] == nsuf[-1]) {
688 		osuf--;
689 		nsuf--;
690 	}
691 
692 	wchar_t nul = L'\0';
693 	swap(&new[pre], &nul);
694 	ptr = seprintf(ptr, end, "%ls", new);
695 	swap(&new[pre], &nul);
696 	swap(osuf, &nul);
697 	ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Brown, &old[pre]);
698 	swap(osuf, &nul);
699 	swap(nsuf, &nul);
700 	ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Green, &new[pre]);
701 	swap(nsuf, &nul);
702 	ptr = seprintf(ptr, end, "\3%02d,%02d%ls", Default, Default, nsuf);
703 
704 plain:
705 	topicComplete(id, msg->params[1]);
706 	urlScan(id, msg->nick, msg->params[1]);
707 	uiFormat(
708 		id, Warm, tagTime(msg),
709 		"\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s",
710 		hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0],
711 		(ptr > buf ? buf : msg->params[1])
712 	);
713 	logFormat(
714 		id, tagTime(msg), "%s places a new sign in %s: %s",
715 		msg->nick, msg->params[0], msg->params[1]
716 	);
717 }
718 
719 static const char *UserModes[256] = {
720 	['O'] = "local oper",
721 	['i'] = "invisible",
722 	['o'] = "oper",
723 	['r'] = "registered",
724 	['w'] = "wallops",
725 };
726 
handleReplyUserModeIs(struct Message * msg)727 static void handleReplyUserModeIs(struct Message *msg) {
728 	require(msg, false, 2);
729 	char buf[1024];
730 	char *ptr = buf, *end = &buf[sizeof(buf)];
731 	for (char *ch = msg->params[1]; *ch; ++ch) {
732 		if (*ch == '+') continue;
733 		const char *name = UserModes[(byte)*ch];
734 		ptr = seprintf(
735 			ptr, end, ", +%c%s%s", *ch, (name ? " " : ""), (name ?: "")
736 		);
737 	}
738 	uiFormat(
739 		Network, Warm, tagTime(msg),
740 		"\3%02d%s\3\tis %s",
741 		self.color, self.nick, (ptr > buf ? &buf[2] : "modeless")
742 	);
743 }
744 
745 static const char *ChanModes[256] = {
746 	['a'] = "protected",
747 	['h'] = "halfop",
748 	['i'] = "invite-only",
749 	['k'] = "key",
750 	['l'] = "client limit",
751 	['m'] = "moderated",
752 	['n'] = "no external messages",
753 	['o'] = "operator",
754 	['q'] = "founder",
755 	['s'] = "secret",
756 	['t'] = "protected topic",
757 	['v'] = "voice",
758 };
759 
handleReplyChannelModeIs(struct Message * msg)760 static void handleReplyChannelModeIs(struct Message *msg) {
761 	require(msg, false, 3);
762 	uint param = 3;
763 	char buf[1024];
764 	char *ptr = buf, *end = &buf[sizeof(buf)];
765 	for (char *ch = msg->params[2]; *ch; ++ch) {
766 		if (*ch == '+') continue;
767 		const char *name = ChanModes[(byte)*ch];
768 		if (
769 			strchr(network.paramModes, *ch) ||
770 			strchr(network.setParamModes, *ch)
771 		) {
772 			assert(param < ParamCap);
773 			ptr = seprintf(
774 				ptr, end, ", +%c%s%s %s",
775 				*ch, (name ? " " : ""), (name ?: ""),
776 				msg->params[param++]
777 			);
778 		} else {
779 			ptr = seprintf(
780 				ptr, end, ", +%c%s%s",
781 				*ch, (name ? " " : ""), (name ?: "")
782 			);
783 		}
784 	}
785 	uiFormat(
786 		idFor(msg->params[1]), Warm, tagTime(msg),
787 		"\3%02d%s\3\tis %s",
788 		hash(msg->params[1]), msg->params[1],
789 		(ptr > buf ? &buf[2] : "modeless")
790 	);
791 }
792 
handleMode(struct Message * msg)793 static void handleMode(struct Message *msg) {
794 	require(msg, true, 2);
795 
796 	if (!strchr(network.chanTypes, msg->params[0][0])) {
797 		bool set = true;
798 		for (char *ch = msg->params[1]; *ch; ++ch) {
799 			if (*ch == '+') { set = true; continue; }
800 			if (*ch == '-') { set = false; continue; }
801 			const char *name = UserModes[(byte)*ch];
802 			uiFormat(
803 				Network, Warm, tagTime(msg),
804 				"\3%02d%s\3\t%ssets \3%02d%s\3 %c%c%s%s",
805 				hash(msg->user), msg->nick,
806 				(set ? "" : "un"),
807 				self.color, msg->params[0],
808 				set["-+"], *ch, (name ? " " : ""), (name ?: "")
809 			);
810 		}
811 		return;
812 	}
813 
814 	uint id = idFor(msg->params[0]);
815 	bool set = true;
816 	uint i = 2;
817 	for (char *ch = msg->params[1]; *ch; ++ch) {
818 		if (*ch == '+') { set = true; continue; }
819 		if (*ch == '-') { set = false; continue; }
820 
821 		const char *verb = (set ? "sets" : "unsets");
822 		const char *name = ChanModes[(byte)*ch];
823 		if (*ch == network.excepts) name = "except";
824 		if (*ch == network.invex) name = "invite";
825 		const char *mode = (const char[]) {
826 			set["-+"], *ch, (name ? ' ' : '\0'), '\0'
827 		};
828 		if (!name) name = "";
829 
830 		if (strchr(network.prefixModes, *ch)) {
831 			if (i >= ParamCap || !msg->params[i]) {
832 				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
833 			}
834 			char *nick = msg->params[i++];
835 			char prefix = network.prefixes[
836 				strchr(network.prefixModes, *ch) - network.prefixModes
837 			];
838 			uiFormat(
839 				id, Cold, tagTime(msg),
840 				"\3%02d%s\3\t%s \3%02d%c%s\3 %s%s in \3%02d%s\3",
841 				hash(msg->user), msg->nick, verb,
842 				completeColor(id, nick), prefix, nick,
843 				mode, name, hash(msg->params[0]), msg->params[0]
844 			);
845 			logFormat(
846 				id, tagTime(msg), "%s %s %c%s %s%s in %s",
847 				msg->nick, verb, prefix, nick, mode, name, msg->params[0]
848 			);
849 		}
850 
851 		if (strchr(network.listModes, *ch)) {
852 			if (i >= ParamCap || !msg->params[i]) {
853 				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
854 			}
855 			char *mask = msg->params[i++];
856 			if (*ch == 'b') {
857 				verb = (set ? "bans" : "unbans");
858 				uiFormat(
859 					id, Cold, tagTime(msg),
860 					"\3%02d%s\3\t%s %c%c %s from \3%02d%s\3",
861 					hash(msg->user), msg->nick, verb, set["-+"], *ch, mask,
862 					hash(msg->params[0]), msg->params[0]
863 				);
864 				logFormat(
865 					id, tagTime(msg), "%s %s %c%c %s from %s",
866 					msg->nick, verb, set["-+"], *ch, mask, msg->params[0]
867 				);
868 			} else {
869 				verb = (set ? "adds" : "removes");
870 				const char *to = (set ? "to" : "from");
871 				uiFormat(
872 					id, Cold, tagTime(msg),
873 					"\3%02d%s\3\t%s %s %s the \3%02d%s\3 %s%s list",
874 					hash(msg->user), msg->nick, verb, mask, to,
875 					hash(msg->params[0]), msg->params[0], mode, name
876 				);
877 				logFormat(
878 					id, tagTime(msg), "%s %s %s %s the %s %s%s list",
879 					msg->nick, verb, mask, to, msg->params[0], mode, name
880 				);
881 			}
882 		}
883 
884 		if (strchr(network.paramModes, *ch)) {
885 			if (i >= ParamCap || !msg->params[i]) {
886 				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
887 			}
888 			char *param = msg->params[i++];
889 			uiFormat(
890 				id, Cold, tagTime(msg),
891 				"\3%02d%s\3\t%s \3%02d%s\3 %s%s %s",
892 				hash(msg->user), msg->nick, verb,
893 				hash(msg->params[0]), msg->params[0], mode, name, param
894 			);
895 			logFormat(
896 				id, tagTime(msg), "%s %s %s %s%s %s",
897 				msg->nick, verb, msg->params[0], mode, name, param
898 			);
899 		}
900 
901 		if (strchr(network.setParamModes, *ch) && set) {
902 			if (i >= ParamCap || !msg->params[i]) {
903 				errx(EX_PROTOCOL, "MODE missing %s parameter", mode);
904 			}
905 			char *param = msg->params[i++];
906 			uiFormat(
907 				id, Cold, tagTime(msg),
908 				"\3%02d%s\3\t%s \3%02d%s\3 %s%s %s",
909 				hash(msg->user), msg->nick, verb,
910 				hash(msg->params[0]), msg->params[0], mode, name, param
911 			);
912 			logFormat(
913 				id, tagTime(msg), "%s %s %s %s%s %s",
914 				msg->nick, verb, msg->params[0], mode, name, param
915 			);
916 		} else if (strchr(network.setParamModes, *ch)) {
917 			uiFormat(
918 				id, Cold, tagTime(msg),
919 				"\3%02d%s\3\t%s \3%02d%s\3 %s%s",
920 				hash(msg->user), msg->nick, verb,
921 				hash(msg->params[0]), msg->params[0], mode, name
922 			);
923 			logFormat(
924 				id, tagTime(msg), "%s %s %s %s%s",
925 				msg->nick, verb, msg->params[0], mode, name
926 			);
927 		}
928 
929 		if (strchr(network.channelModes, *ch)) {
930 			uiFormat(
931 				id, Cold, tagTime(msg),
932 				"\3%02d%s\3\t%s \3%02d%s\3 %s%s",
933 				hash(msg->user), msg->nick, verb,
934 				hash(msg->params[0]), msg->params[0], mode, name
935 			);
936 			logFormat(
937 				id, tagTime(msg), "%s %s %s %s%s",
938 				msg->nick, verb, msg->params[0], mode, name
939 			);
940 		}
941 	}
942 }
943 
handleErrorChanopPrivsNeeded(struct Message * msg)944 static void handleErrorChanopPrivsNeeded(struct Message *msg) {
945 	require(msg, false, 3);
946 	uiFormat(
947 		idFor(msg->params[1]), Warm, tagTime(msg),
948 		"%s", msg->params[2]
949 	);
950 }
951 
handleErrorUserNotInChannel(struct Message * msg)952 static void handleErrorUserNotInChannel(struct Message *msg) {
953 	require(msg, false, 4);
954 	uiFormat(
955 		idFor(msg->params[2]), Warm, tagTime(msg),
956 		"%s\tis not in \3%02d%s\3",
957 		msg->params[1], hash(msg->params[2]), msg->params[2]
958 	);
959 }
960 
handleErrorBanListFull(struct Message * msg)961 static void handleErrorBanListFull(struct Message *msg) {
962 	require(msg, false, 4);
963 	uiFormat(
964 		idFor(msg->params[1]), Warm, tagTime(msg),
965 		"%s", (msg->params[4] ?: msg->params[3])
966 	);
967 }
968 
handleReplyBanList(struct Message * msg)969 static void handleReplyBanList(struct Message *msg) {
970 	require(msg, false, 3);
971 	uint id = idFor(msg->params[1]);
972 	if (msg->params[3] && msg->params[4]) {
973 		char since[sizeof("0000-00-00 00:00:00")];
974 		time_t time = strtol(msg->params[4], NULL, 10);
975 		strftime(since, sizeof(since), "%F %T", localtime(&time));
976 		uiFormat(
977 			id, Warm, tagTime(msg),
978 			"Banned from \3%02d%s\3 since %s by \3%02d%s\3: %s",
979 			hash(msg->params[1]), msg->params[1],
980 			since, completeColor(id, msg->params[3]), msg->params[3],
981 			msg->params[2]
982 		);
983 	} else {
984 		uiFormat(
985 			id, Warm, tagTime(msg),
986 			"Banned from \3%02d%s\3: %s",
987 			hash(msg->params[1]), msg->params[1], msg->params[2]
988 		);
989 	}
990 }
991 
onList(const char * list,struct Message * msg)992 static void onList(const char *list, struct Message *msg) {
993 	require(msg, false, 3);
994 	uint id = idFor(msg->params[1]);
995 	if (msg->params[3] && msg->params[4]) {
996 		char since[sizeof("0000-00-00 00:00:00")];
997 		time_t time = strtol(msg->params[4], NULL, 10);
998 		strftime(since, sizeof(since), "%F %T", localtime(&time));
999 		uiFormat(
1000 			id, Warm, tagTime(msg),
1001 			"On the \3%02d%s\3 %s list since %s by \3%02d%s\3: %s",
1002 			hash(msg->params[1]), msg->params[1], list,
1003 			since, completeColor(id, msg->params[3]), msg->params[3],
1004 			msg->params[2]
1005 		);
1006 	} else {
1007 		uiFormat(
1008 			id, Warm, tagTime(msg),
1009 			"On the \3%02d%s\3 %s list: %s",
1010 			hash(msg->params[1]), msg->params[1], list, msg->params[2]
1011 		);
1012 	}
1013 }
1014 
handleReplyExceptList(struct Message * msg)1015 static void handleReplyExceptList(struct Message *msg) {
1016 	onList("except", msg);
1017 }
1018 
handleReplyInviteList(struct Message * msg)1019 static void handleReplyInviteList(struct Message *msg) {
1020 	onList("invite", msg);
1021 }
1022 
handleReplyList(struct Message * msg)1023 static void handleReplyList(struct Message *msg) {
1024 	require(msg, false, 4);
1025 	uiFormat(
1026 		Network, Warm, tagTime(msg),
1027 		"In \3%02d%s\3 are %ld under the banner: %s",
1028 		hash(msg->params[1]), msg->params[1],
1029 		strtol(msg->params[2], NULL, 10),
1030 		msg->params[3]
1031 	);
1032 }
1033 
handleReplyWhoisUser(struct Message * msg)1034 static void handleReplyWhoisUser(struct Message *msg) {
1035 	require(msg, false, 6);
1036 	completeTouch(Network, msg->params[1], hash(msg->params[2]));
1037 	uiFormat(
1038 		Network, Warm, tagTime(msg),
1039 		"\3%02d%s\3\tis %s!%s@%s (%s\17)",
1040 		hash(msg->params[2]), msg->params[1],
1041 		msg->params[1], msg->params[2], msg->params[3], msg->params[5]
1042 	);
1043 }
1044 
handleReplyWhoisServer(struct Message * msg)1045 static void handleReplyWhoisServer(struct Message *msg) {
1046 	if (!replies[ReplyWhois] && !replies[ReplyWhowas]) return;
1047 	require(msg, false, 4);
1048 	uiFormat(
1049 		Network, Warm, tagTime(msg),
1050 		"\3%02d%s\3\t%s connected to %s (%s)",
1051 		completeColor(Network, msg->params[1]), msg->params[1],
1052 		(replies[ReplyWhowas] ? "was" : "is"), msg->params[2], msg->params[3]
1053 	);
1054 }
1055 
handleReplyWhoisIdle(struct Message * msg)1056 static void handleReplyWhoisIdle(struct Message *msg) {
1057 	require(msg, false, 3);
1058 	unsigned long idle = strtoul(msg->params[2], NULL, 10);
1059 	const char *unit = "second";
1060 	if (idle / 60) {
1061 		idle /= 60; unit = "minute";
1062 		if (idle / 60) {
1063 			idle /= 60; unit = "hour";
1064 			if (idle / 24) {
1065 				idle /= 24; unit = "day";
1066 			}
1067 		}
1068 	}
1069 	char signon[sizeof("0000-00-00 00:00:00")];
1070 	time_t time = strtol((msg->params[3] ?: ""), NULL, 10);
1071 	strftime(signon, sizeof(signon), "%F %T", localtime(&time));
1072 	uiFormat(
1073 		Network, Warm, tagTime(msg),
1074 		"\3%02d%s\3\tis idle for %lu %s%s%s%s",
1075 		completeColor(Network, msg->params[1]), msg->params[1],
1076 		idle, unit, (idle != 1 ? "s" : ""),
1077 		(msg->params[3] ? ", signed on " : ""), (msg->params[3] ? signon : "")
1078 	);
1079 }
1080 
handleReplyWhoisChannels(struct Message * msg)1081 static void handleReplyWhoisChannels(struct Message *msg) {
1082 	require(msg, false, 3);
1083 	char buf[1024];
1084 	char *ptr = buf, *end = &buf[sizeof(buf)];
1085 	while (msg->params[2]) {
1086 		char *channel = strsep(&msg->params[2], " ");
1087 		if (!channel[0]) break;
1088 		char *name = &channel[strspn(channel, network.prefixes)];
1089 		ptr = seprintf(
1090 			ptr, end, "%s\3%02d%s\3",
1091 			(ptr > buf ? ", " : ""), hash(name), channel
1092 		);
1093 	}
1094 	uiFormat(
1095 		Network, Warm, tagTime(msg),
1096 		"\3%02d%s\3\tis in %s",
1097 		completeColor(Network, msg->params[1]), msg->params[1], buf
1098 	);
1099 }
1100 
handleReplyWhoisGeneric(struct Message * msg)1101 static void handleReplyWhoisGeneric(struct Message *msg) {
1102 	require(msg, false, 3);
1103 	if (msg->params[3]) {
1104 		msg->params[0] = msg->params[2];
1105 		msg->params[2] = msg->params[3];
1106 		msg->params[3] = msg->params[0];
1107 	}
1108 	uiFormat(
1109 		Network, Warm, tagTime(msg),
1110 		"\3%02d%s\3\t%s%s%s",
1111 		completeColor(Network, msg->params[1]), msg->params[1],
1112 		msg->params[2], (msg->params[3] ? " " : ""), (msg->params[3] ?: "")
1113 	);
1114 }
1115 
handleReplyEndOfWhois(struct Message * msg)1116 static void handleReplyEndOfWhois(struct Message *msg) {
1117 	require(msg, false, 2);
1118 	if (strcmp(msg->params[1], self.nick)) {
1119 		completeRemove(Network, msg->params[1]);
1120 	}
1121 }
1122 
handleReplyWhowasUser(struct Message * msg)1123 static void handleReplyWhowasUser(struct Message *msg) {
1124 	require(msg, false, 6);
1125 	completeTouch(Network, msg->params[1], hash(msg->params[2]));
1126 	uiFormat(
1127 		Network, Warm, tagTime(msg),
1128 		"\3%02d%s\3\twas %s!%s@%s (%s)",
1129 		hash(msg->params[2]), msg->params[1],
1130 		msg->params[1], msg->params[2], msg->params[3], msg->params[5]
1131 	);
1132 }
1133 
handleReplyEndOfWhowas(struct Message * msg)1134 static void handleReplyEndOfWhowas(struct Message *msg) {
1135 	require(msg, false, 2);
1136 	if (strcmp(msg->params[1], self.nick)) {
1137 		completeRemove(Network, msg->params[1]);
1138 	}
1139 }
1140 
handleReplyAway(struct Message * msg)1141 static void handleReplyAway(struct Message *msg) {
1142 	require(msg, false, 3);
1143 	// Might be part of a WHOIS response.
1144 	uint id;
1145 	if (completeColor(Network, msg->params[1]) != Default) {
1146 		id = Network;
1147 	} else {
1148 		id = idFor(msg->params[1]);
1149 	}
1150 	uiFormat(
1151 		id, Warm, tagTime(msg),
1152 		"\3%02d%s\3\tis away: %s",
1153 		completeColor(id, msg->params[1]), msg->params[1], msg->params[2]
1154 	);
1155 	logFormat(
1156 		id, tagTime(msg), "%s is away: %s",
1157 		msg->params[1], msg->params[2]
1158 	);
1159 }
1160 
handleReplyNowAway(struct Message * msg)1161 static void handleReplyNowAway(struct Message *msg) {
1162 	require(msg, false, 2);
1163 	uiFormat(Network, Warm, tagTime(msg), "%s", msg->params[1]);
1164 }
1165 
isAction(struct Message * msg)1166 static bool isAction(struct Message *msg) {
1167 	if (strncmp(msg->params[1], "\1ACTION", 7)) return false;
1168 	if (msg->params[1][7] == ' ') {
1169 		msg->params[1] += 8;
1170 	} else if (msg->params[1][7] == '\1') {
1171 		msg->params[1] += 7;
1172 	} else {
1173 		return false;
1174 	}
1175 	size_t len = strlen(msg->params[1]);
1176 	if (msg->params[1][len - 1] == '\1') {
1177 		msg->params[1][len - 1] = '\0';
1178 	}
1179 	return true;
1180 }
1181 
isMention(const struct Message * msg)1182 static bool isMention(const struct Message *msg) {
1183 	size_t len = strlen(self.nick);
1184 	const char *match = msg->params[1];
1185 	while (NULL != (match = strstr(match, self.nick))) {
1186 		char a = (match > msg->params[1] ? match[-1] : ' ');
1187 		char b = (match[len] ?: ' ');
1188 		if ((isspace(a) || ispunct(a)) && (isspace(b) || ispunct(b))) {
1189 			return true;
1190 		}
1191 		match = &match[len];
1192 	}
1193 	return false;
1194 }
1195 
colorMentions(char * buf,size_t cap,uint id,struct Message * msg)1196 static void colorMentions(char *buf, size_t cap, uint id, struct Message *msg) {
1197 	*buf = '\0';
1198 
1199 	char *split = strstr(msg->params[1], ": ");
1200 	if (!split) {
1201 		split = strchr(msg->params[1], ' ');
1202 		if (split) split = strchr(&split[1], ' ');
1203 	}
1204 	if (!split) split = &msg->params[1][strlen(msg->params[1])];
1205 	for (char *ch = msg->params[1]; ch < split; ++ch) {
1206 		if (iscntrl(*ch)) return;
1207 	}
1208 	char delimit = *split;
1209 	char *mention = msg->params[1];
1210 	msg->params[1] = (delimit ? &split[1] : split);
1211 	*split = '\0';
1212 
1213 	char *ptr = buf, *end = &buf[cap];
1214 	while (*mention) {
1215 		size_t skip = strspn(mention, ",<> ");
1216 		ptr = seprintf(ptr, end, "%.*s", (int)skip, mention);
1217 		mention += skip;
1218 
1219 		size_t len = strcspn(mention, ",<> ");
1220 		char punct = mention[len];
1221 		mention[len] = '\0';
1222 		enum Color color = completeColor(id, mention);
1223 		if (color != Default) {
1224 			ptr = seprintf(ptr, end, "\3%02d%s\3", color, mention);
1225 		} else {
1226 			ptr = seprintf(ptr, end, "%s", mention);
1227 		}
1228 		mention[len] = punct;
1229 		mention += len;
1230 	}
1231 	seprintf(ptr, end, "%c", delimit);
1232 }
1233 
handlePrivmsg(struct Message * msg)1234 static void handlePrivmsg(struct Message *msg) {
1235 	require(msg, true, 2);
1236 	if (network.statusmsg) {
1237 		msg->params[0] += strspn(msg->params[0], network.statusmsg);
1238 	}
1239 	bool query = !strchr(network.chanTypes, msg->params[0][0]);
1240 	bool server = strchr(msg->nick, '.');
1241 	bool mine = !strcmp(msg->nick, self.nick);
1242 	uint id;
1243 	if (query && server) {
1244 		id = Network;
1245 	} else if (query && !mine) {
1246 		id = idFor(msg->nick);
1247 		idColors[id] = hash(msg->user);
1248 	} else {
1249 		id = idFor(msg->params[0]);
1250 	}
1251 
1252 	bool notice = (msg->cmd[0] == 'N');
1253 	bool action = !notice && isAction(msg);
1254 	bool highlight = !mine && isMention(msg);
1255 	enum Heat heat = filterCheck((highlight || query ? Hot : Warm), id, msg);
1256 	if (heat > Warm && !mine && !query) highlight = true;
1257 	if (!notice && !mine && heat > Ice) {
1258 		completeTouch(id, msg->nick, hash(msg->user));
1259 	}
1260 	if (heat > Ice) urlScan(id, msg->nick, msg->params[1]);
1261 
1262 	char buf[1024];
1263 	if (notice) {
1264 		if (id != Network) {
1265 			logFormat(id, tagTime(msg), "-%s- %s", msg->nick, msg->params[1]);
1266 		}
1267 		uiFormat(
1268 			id, filterCheck(Warm, id, msg), tagTime(msg),
1269 			"\3%d-%s-\3%d\t%s",
1270 			hash(msg->user), msg->nick, LightGray, msg->params[1]
1271 		);
1272 	} else if (action) {
1273 		logFormat(id, tagTime(msg), "* %s %s", msg->nick, msg->params[1]);
1274 		colorMentions(buf, sizeof(buf), id, msg);
1275 		uiFormat(
1276 			id, heat, tagTime(msg),
1277 			"%s\35\3%d* %s\17\35\t%s%s",
1278 			(highlight ? "\26" : ""), hash(msg->user), msg->nick,
1279 			buf, msg->params[1]
1280 		);
1281 	} else {
1282 		logFormat(id, tagTime(msg), "<%s> %s", msg->nick, msg->params[1]);
1283 		colorMentions(buf, sizeof(buf), id, msg);
1284 		uiFormat(
1285 			id, heat, tagTime(msg),
1286 			"%s\3%d<%s>\17\t%s%s",
1287 			(highlight ? "\26" : ""), hash(msg->user), msg->nick,
1288 			buf, msg->params[1]
1289 		);
1290 	}
1291 }
1292 
handlePing(struct Message * msg)1293 static void handlePing(struct Message *msg) {
1294 	require(msg, false, 1);
1295 	ircFormat("PONG :%s\r\n", msg->params[0]);
1296 }
1297 
handleError(struct Message * msg)1298 static void handleError(struct Message *msg) {
1299 	require(msg, false, 1);
1300 	errx(EX_UNAVAILABLE, "%s", msg->params[0]);
1301 }
1302 
1303 static const struct Handler {
1304 	const char *cmd;
1305 	int reply;
1306 	Handler *fn;
1307 } Handlers[] = {
1308 	{ "001", 0, handleReplyWelcome },
1309 	{ "005", 0, handleReplyISupport },
1310 	{ "221", -ReplyMode, handleReplyUserModeIs },
1311 	{ "276", +ReplyWhois, handleReplyWhoisGeneric },
1312 	{ "301", 0, handleReplyAway },
1313 	{ "305", -ReplyAway, handleReplyNowAway },
1314 	{ "306", -ReplyAway, handleReplyNowAway },
1315 	{ "307", +ReplyWhois, handleReplyWhoisGeneric },
1316 	{ "311", +ReplyWhois, handleReplyWhoisUser },
1317 	{ "312", 0, handleReplyWhoisServer },
1318 	{ "313", +ReplyWhois, handleReplyWhoisGeneric },
1319 	{ "314", +ReplyWhowas, handleReplyWhowasUser },
1320 	{ "315", -ReplyWho, handleReplyEndOfWho },
1321 	{ "317", +ReplyWhois, handleReplyWhoisIdle },
1322 	{ "318", -ReplyWhois, handleReplyEndOfWhois },
1323 	{ "319", +ReplyWhois, handleReplyWhoisChannels },
1324 	{ "320", +ReplyWhois, handleReplyWhoisGeneric },
1325 	{ "322", +ReplyList, handleReplyList },
1326 	{ "323", -ReplyList, NULL },
1327 	{ "324", -ReplyMode, handleReplyChannelModeIs },
1328 	{ "330", +ReplyWhois, handleReplyWhoisGeneric },
1329 	{ "331", -ReplyTopic, handleReplyNoTopic },
1330 	{ "332", 0, handleReplyTopic },
1331 	{ "335", +ReplyWhois, handleReplyWhoisGeneric },
1332 	{ "338", +ReplyWhois, handleReplyWhoisGeneric },
1333 	{ "341", 0, handleReplyInviting },
1334 	{ "346", +ReplyInvex, handleReplyInviteList },
1335 	{ "347", -ReplyInvex, NULL },
1336 	{ "348", +ReplyExcepts, handleReplyExceptList },
1337 	{ "349", -ReplyExcepts, NULL },
1338 	{ "352", +ReplyWho, handleReplyWho },
1339 	{ "353", 0, handleReplyNames },
1340 	{ "366", 0, handleReplyEndOfNames },
1341 	{ "367", +ReplyBan, handleReplyBanList },
1342 	{ "368", -ReplyBan, NULL },
1343 	{ "369", -ReplyWhowas, handleReplyEndOfWhowas },
1344 	{ "372", 0, handleReplyMOTD },
1345 	{ "378", +ReplyWhois, handleReplyWhoisGeneric },
1346 	{ "379", +ReplyWhois, handleReplyWhoisGeneric },
1347 	{ "422", 0, handleErrorNoMOTD },
1348 	{ "432", 0, handleErrorErroneousNickname },
1349 	{ "433", 0, handleErrorNicknameInUse },
1350 	{ "437", 0, handleErrorNicknameInUse },
1351 	{ "441", 0, handleErrorUserNotInChannel },
1352 	{ "443", 0, handleErrorUserOnChannel },
1353 	{ "478", 0, handleErrorBanListFull },
1354 	{ "482", 0, handleErrorChanopPrivsNeeded },
1355 	{ "671", +ReplyWhois, handleReplyWhoisGeneric },
1356 	{ "704", +ReplyHelp, handleReplyHelp },
1357 	{ "705", +ReplyHelp, handleReplyHelp },
1358 	{ "706", -ReplyHelp, NULL },
1359 	{ "900", 0, handleReplyLoggedIn },
1360 	{ "904", 0, handleErrorSASLFail },
1361 	{ "905", 0, handleErrorSASLFail },
1362 	{ "906", 0, handleErrorSASLFail },
1363 	{ "AUTHENTICATE", 0, handleAuthenticate },
1364 	{ "CAP", 0, handleCap },
1365 	{ "CHGHOST", 0, handleChghost },
1366 	{ "ERROR", 0, handleError },
1367 	{ "FAIL", 0, handleStandardReply },
1368 	{ "INVITE", 0, handleInvite },
1369 	{ "JOIN", 0, handleJoin },
1370 	{ "KICK", 0, handleKick },
1371 	{ "MODE", 0, handleMode },
1372 	{ "NICK", 0, handleNick },
1373 	{ "NOTE", 0, handleStandardReply },
1374 	{ "NOTICE", 0, handlePrivmsg },
1375 	{ "PART", 0, handlePart },
1376 	{ "PING", 0, handlePing },
1377 	{ "PRIVMSG", 0, handlePrivmsg },
1378 	{ "QUIT", 0, handleQuit },
1379 	{ "SETNAME", 0, handleSetname },
1380 	{ "TOPIC", 0, handleTopic },
1381 	{ "WARN", 0, handleStandardReply },
1382 };
1383 
compar(const void * cmd,const void * _handler)1384 static int compar(const void *cmd, const void *_handler) {
1385 	const struct Handler *handler = _handler;
1386 	return strcmp(cmd, handler->cmd);
1387 }
1388 
handle(struct Message * msg)1389 void handle(struct Message *msg) {
1390 	if (!msg->cmd) return;
1391 	if (msg->tags[TagPos]) {
1392 		self.pos = strtoull(msg->tags[TagPos], NULL, 10);
1393 	}
1394 	const struct Handler *handler = bsearch(
1395 		msg->cmd, Handlers, ARRAY_LEN(Handlers), sizeof(*handler), compar
1396 	);
1397 	if (handler) {
1398 		if (handler->reply && !replies[abs(handler->reply)]) return;
1399 		if (handler->fn) handler->fn(msg);
1400 		if (handler->reply < 0) replies[abs(handler->reply)]--;
1401 	} else if (strcmp(msg->cmd, "400") >= 0 && strcmp(msg->cmd, "599") <= 0) {
1402 		handleErrorGeneric(msg);
1403 	}
1404 }
1405