jcs
/wallops
/amendments
/8
irc: Simplify protocol processing, implement more action responses
jcs made amendment 8 over 3 years ago
--- irc.c	Fri Feb  4 12:58:13 2022
+++ irc.c	Sun Feb  6 19:30:53 2022
@@ -33,6 +33,11 @@ void irc_login(struct chatter *chatter);
 void irc_process_server(struct chatter *chatter);
 void irc_set_active_channel(struct chatter *chatter, char *channame);
 void irc_parse_names(struct chatter *chatter, char *line);
+void irc_add_user_to_channel(struct chatter *chatter,
+  struct irc_user *user);
+void irc_remove_nick_from_channel(struct chatter *chatter, char *nick);
+void irc_parse_channel_mode_change(struct chatter *chatter, char *mode,
+  char *args);
 
 void
 irc_process(struct chatter *chatter)
@@ -126,6 +131,9 @@ irc_init(struct chatter *chatter)
 void
 irc_abort(struct chatter *chatter)
 {
+	chatter->irc_state = IRC_STATE_DISCONNECTED;
+	irc_set_active_channel(chatter, NULL);
+	
 	_TCPClose(&chatter->irc_close_pb, chatter->irc_stream, nil, nil,
 	  false);
 	_TCPAbort(&chatter->irc_close_pb, chatter->irc_stream, nil, nil,
@@ -142,7 +150,6 @@ irc_close(struct chatter *chatter)
 		
 	chatter_printf(chatter, "$B*!*$0 Disconnecting");
 	irc_abort(chatter);
-	chatter->irc_state = IRC_STATE_DISCONNECTED;
 }
 
 short
@@ -251,9 +258,6 @@ irc_printf(struct chatter *chatter, const char *format
 		buf[len - 1] = '\0';
 	}
 	
-#if 0
-	chatter_printf(chatter, ">>>[%ld] %s", len, buf);
-#endif
 	irc_send(chatter, buf, len);
 	
 	return len;
@@ -347,7 +351,7 @@ irc_process_server(struct chatter *chatter)
 {
 	struct irc_msg msg;
 	struct irc_user *user;
-	char *line;
+	char *line, *word;
 	size_t size, n, lastbreak;
 	short state;
 	
@@ -356,124 +360,168 @@ irc_process_server(struct chatter *chatter)
 		return;
 		
 	memset(&msg, 0, sizeof(msg));
+
+	word = strsep(&line, " ");
 	
-	for (n = 0, lastbreak = 0, state = 0; n < size; n++) {
-		if (!(line[n] == ' ' || n == size - 1))
-			continue;
-			
-		line[n] = '\0';
-		
-		switch (state) {
-		case 0:
-			if (line[0] == ':')
-				/* :server.name 001 jcs :Hi -> msg.source=server.name */
-				strlcpy(msg.source, line + 1, sizeof(msg.source));
-			else
-				/* PING :server.name -> msg.cmd=PING */
-				strlcpy(msg.cmd, line, sizeof(msg.cmd));
-			state++;
-			break;
-		case 1:
-			if (isdigit(line[lastbreak]) && isdigit(line[lastbreak + 1]) &&
-			  isdigit(line[lastbreak + 2]))
-				/* :server.name 001 jcs :Hi -> msg.code=1 */
-				msg.code = atoi(line + lastbreak);
-			else if (msg.cmd[0] != '\0') {
-				/* PING :server.name -> msg.msg=server.name */
-				if (line[lastbreak + 1] == ':')
-					lastbreak++;
-				strlcpy(msg.msg, line + lastbreak + 1, sizeof(msg.msg));
-				goto done_parsing; 
-			} else
-				/* :jcs!~jcs@jcs JOIN #wallops -> msg.cmd=JOIN */
-				strlcpy(msg.cmd, line + lastbreak, sizeof(msg.cmd));
-			state++;
-			break;
-		case 2:
-			if (line[lastbreak] == ':') {
-				/* :jcs!... QUIT :Ping timeout -> msg.msg=Ping timeout */
-				strlcpy(msg.msg, line + lastbreak + 1,
-				  sizeof(msg.msg));
-				goto done_parsing;
-			}
-			
-			/* :server.name 251 jcs :Blah -> msg.dest=jcs */
-			strlcpy(msg.dest, line + lastbreak, sizeof(msg.dest));
-			
-			if (line[n + 1] == ':') {
-				strlcpy(msg.msg, line + n + 2, sizeof(msg.msg));
-				goto done_parsing;
-			}
-			state++;
-			break;
-		case 3:
-			if (line[lastbreak] != ':') {
-				/* :server 480 jcs #wallops :Cannot... -> msg.channel=#wallops */
-				strlcpy(msg.channel, line + lastbreak, sizeof(msg.channel));
-				lastbreak = n + 1;
-			}
-			
-			strlcpy(msg.msg, line + lastbreak +
-			  (line[lastbreak] == ':' ? 1 : 0), sizeof(msg.msg));
-			goto done_parsing;
-		}
-		
-		lastbreak = n + 1;
+	/* extract source before command */
+	if (word[0] == ':') {
+		/* ":server.name 001 jcs :Hi" -> msg.source=server.name */
+		strlcpy(msg.source, word + 1, sizeof(msg.source));
+		word = strsep(&line, " ");
 	}
 	
-done_parsing:
-	if (strcmp(msg.cmd, "PING") == 0) {
-		irc_printf(chatter, "PONG :%s\r\n", msg.source);
-		return;
+	/* code or a command name */
+	if (isdigit(word[0]) && isdigit(word[1]) && isdigit(word[2]))
+		/* ":server.name 001 jcs :Hi" -> msg.code=1 */
+		msg.code = atoi(word);
+	else
+		/* "PING :server.name" -> msg.cmd=PING */
+		/* ":jcs!~jcs@jcs JOIN #wallops" -> msg.cmd=JOIN */
+		strlcpy(msg.cmd, word, sizeof(msg.cmd));
+
+	/* parameters */
+
+	if (line[0] == ':') {
+		strlcpy(msg.msg, line + 1, sizeof(msg.msg));
+		goto done_parsing;
 	}
-	if (strcmp(msg.cmd, "NOTICE") == 0) {
-		chatter_printf(chatter, "%s", msg.msg);
-		return;
+	word = strsep(&line, " ");
+	if (word == NULL) {
+		strlcpy(msg.msg, line + 1, sizeof(msg.msg));
+		goto done_parsing;
 	}
-	if (strcmp(msg.cmd, "MODE") == 0) {
-		chatter_printf(chatter, "$B***$0 Mode change ($B%s$0) for $B%s$0",
-		  msg.msg, msg.dest);
-		return;
+	strlcpy(msg.dest, word, sizeof(msg.dest));
+
+	if (line[0] == ':') {
+		strlcpy(msg.msg, line + 1, sizeof(msg.msg));
+		goto done_parsing;
 	}
-	if (strcmp(msg.cmd, "PRIVMSG") == 0) {
-		user = irc_parse_user(msg.source);
-		if (strcmp(msg.dest, chatter->irc_nick) == 0)
-			chatter_printf(chatter, "$B[$0%s$B($0%s@%s$B)]$0$/ %s",
-			  user->nick, user->username, user->hostname, msg.msg);
-		else if (chatter->irc_channel &&
-		  strcmp(msg.dest, chatter->irc_channel->name) == 0) {
-			size = strlen(chatter->irc_nick);
-			if (strncmp(msg.msg, chatter->irc_nick, size) == 0 &&
-			  (msg.msg[size] == ' ' || msg.msg[size] == ':' ||
-			  msg.msg[size] == ',' || msg.msg[size] == '\0'))
-				chatter_printf(chatter, "<$U%s$0>$/ %s", user->nick,
+	word = strsep(&line, " ");
+	if (word == NULL) {
+		strlcpy(msg.msg, line + 1, sizeof(msg.msg));
+		goto done_parsing;
+	}
+	strlcpy(msg.channel, word, sizeof(msg.channel));
+
+	if (line[0] == ':') {
+		strlcpy(msg.msg, line + 1, sizeof(msg.msg));
+		goto done_parsing;
+	}
+	strlcpy(msg.msg, line, sizeof(msg.msg));
+	
+done_parsing:
+	if (msg.cmd[0]) {
+		if (strcmp(msg.cmd, "PING") == 0) {
+			irc_printf(chatter, "PONG :%s\r\n", msg.source);
+			return;
+		}
+		if (strcmp(msg.cmd, "PRIVMSG") == 0) {
+			user = irc_parse_user(msg.source);
+			if (strcmp(msg.dest, chatter->irc_nick) == 0)
+				chatter_printf(chatter, "$B[$0%s$B($0%s@%s$B)]$0$/ %s",
+				  user->nick, user->username, user->hostname, msg.msg);
+			else if (chatter->irc_channel &&
+			  strcmp(msg.dest, chatter->irc_channel->name) == 0) {
+				size = strlen(chatter->irc_nick);
+				if (strncmp(msg.msg, chatter->irc_nick, size) == 0 &&
+				  (msg.msg[size] == ' ' || msg.msg[size] == ':' ||
+				  msg.msg[size] == ',' || msg.msg[size] == '\0')) {
+					/* highlight message */
+					chatter_printf(chatter, "<$U%s$0>$/ %s", user->nick,
+					  msg.msg);
+					if (!chatter->focusable->visible)
+						notify();
+				} else
+					chatter_printf(chatter, "$/<%s> %s", user->nick,
+					  msg.msg);
+			} else
+				chatter_printf(chatter, "$B[%s($0%s@%s$B):%s]$0$/ %s",
+				  user->nick, user->username, user->hostname, msg.dest,
 				  msg.msg);
+			return;
+		}
+		if (strcmp(msg.cmd, "JOIN") == 0) {
+			user = irc_parse_user(msg.source);
+			chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has joined "
+			  "$B%s$0", user->nick, user->username, user->hostname,
+			  msg.dest);
+			if (strcmp(user->nick, chatter->irc_nick) == 0)
+				irc_set_active_channel(chatter, msg.dest);
 			else
-				chatter_printf(chatter, "$/<%s> %s", user->nick,
+				irc_add_user_to_channel(chatter, user);
+			return;
+		}
+		if (strcmp(msg.cmd, "PART") == 0) {
+			user = irc_parse_user(msg.source);
+			chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has left "
+			  "$B%s$0", user->nick, user->username, user->hostname,
+			  msg.dest);
+			if (strcmp(user->nick, chatter->irc_nick) == 0)
+				irc_set_active_channel(chatter, NULL);
+			else
+				irc_remove_nick_from_channel(chatter, user->nick);
+			return;
+		}
+		if (strcmp(msg.cmd, "QUIT") == 0) {
+			user = irc_parse_user(msg.source);
+			chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has quit:$/ %s",
+			  user->nick, user->username, user->hostname, msg.msg);
+			if (strcmp(user->nick, chatter->irc_nick) == 0)
+				irc_set_active_channel(chatter, NULL);
+			else
+				irc_remove_nick_from_channel(chatter, user->nick);
+			return;
+		}
+		if (strcmp(msg.cmd, "NICK") == 0) {
+			user = irc_parse_user(msg.source);
+			chatter_printf(chatter, "$B*** %s$0 is now known as $B%s$0",
+			  user->nick, msg.msg);
+			irc_remove_nick_from_channel(chatter, user->nick);
+			strlcpy(user->nick, msg.msg, sizeof(user->nick));
+			irc_add_user_to_channel(chatter, user);
+			return;
+		}
+		if (strcmp(msg.cmd, "MODE") == 0) {
+			if (strcmp(msg.dest, chatter->irc_nick) == 0)
+				chatter_printf(chatter, "$B***$0 Mode change ($B%s$0) "
+				  "for user $B%s$0", msg.msg, msg.dest);
+			else {
+				/* msg.channel is mode */
+				user = irc_parse_user(msg.source);
+				chatter_printf(chatter, "$B***$0 Mode change ($B%s %s$0) "
+				  "on $B%s$0 by $B%s%0", msg.channel, msg.msg, msg.dest,
+				  user->nick);
+				irc_parse_channel_mode_change(chatter, msg.channel, 
 				  msg.msg);
-		} else
-			chatter_printf(chatter, "$B[%s($0%s@%s$B):%s]$0$/ %s",
-			  user->nick, user->username, user->hostname, msg.dest,
-			  msg.msg);
-			
-		return;
+			}
+			return;
+		}
+		if (strcmp(msg.cmd, "KICK") == 0) {
+			/* dest and channel are swapped */
+			user = irc_parse_user(msg.source);
+			chatter_printf(chatter, "$B*** %s ($0%s$B)$0 was kicked "
+			  "from $B%s$0 by $B%s%0:$/ %s", msg.channel, msg.dest,
+			  user->nick, msg.msg);
+			if (strcmp(msg.channel, chatter->irc_nick) == 0)
+				irc_set_active_channel(chatter, NULL);
+			else
+				irc_remove_nick_from_channel(chatter, msg.channel);
+			return;
+		}
+		if (strcmp(msg.cmd, "NOTICE") == 0) {
+			chatter_printf(chatter, "%s", msg.msg);
+			return;
+		}
+		if (strcmp(msg.cmd, "ERROR") == 0) {
+			chatter_printf(chatter, "$B*!* Disconnected from %s:$0$/ %s",
+			  chatter->irc_hostname, msg.msg);
+			irc_abort(chatter);
+			return;
+		}
+		
+		goto unknown;
 	}
-	if (strcmp(msg.cmd, "JOIN") == 0) {
-		user = irc_parse_user(msg.source);
-		chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has joined $B%s$0",
-		  user->nick, user->username, user->hostname, msg.dest);
-		if (strcmp(user->nick, chatter->irc_nick) == 0)
-			irc_set_active_channel(chatter, msg.dest);
-		return;
-	}
-	if (strcmp(msg.cmd, "PART") == 0) {
-		user = irc_parse_user(msg.source);
-		chatter_printf(chatter, "$B*** %s ($0%s@%s$B)$0 has left $B%s$0",
-		  user->nick, user->username, user->hostname, msg.dest);
-		if (strcmp(user->nick, chatter->irc_nick) == 0)
-			irc_set_active_channel(chatter, NULL);
-		return;
-	}
+	
 	switch (msg.code) {
 	case 0:
 		goto unknown;
@@ -524,14 +572,17 @@ done_parsing:
 		/* end of WHO */
 		goto unknown;
 	case 353:
-		/* NAMES output */
-		if (chatter->irc_channel) /* TODO: match arg to channel name */
+		/* NAMES output, channel is first part of msg.msg, not channel */
+		if (chatter->irc_channel &&
+		  strncmp(msg.msg, chatter->irc_channel->name,
+		  strlen(chatter->irc_channel->name)) == 0)
 			irc_parse_names(chatter, msg.msg);
 		return;
 	case 366:
 		/* end of NAMES output */
-		if (chatter->irc_channel)
-			chatter_sync_nick_list(chatter);
+		if (chatter->irc_channel &&
+		  strcmp(msg.channel, chatter->irc_channel->name) == 0)
+			chatter_sync_nick_list(chatter, true);
 		return;
 	case 372:
 	case 375:
@@ -616,7 +667,7 @@ irc_set_active_channel(struct chatter *chatter, char *
 	}
 	
 	if (channame == NULL)
-		chatter_sync_nick_list(chatter);
+		chatter_sync_nick_list(chatter, false);
 	else {
 		channel = xmalloczero(sizeof(struct irc_channel));
 		strlcpy(channel->name, channame, sizeof(channel->name));
@@ -672,4 +723,98 @@ irc_parse_names(struct chatter *chatter, char *line)
 		if (line == NULL)
 			break;
 	}
+}
+
+void
+irc_add_user_to_channel(struct chatter *chatter, struct irc_user *user)
+{
+	chatter->irc_channel->nusers++;
+	chatter->irc_channel->users =
+	  xreallocarray(chatter->irc_channel->users,
+	  sizeof(struct irc_channel_nick), chatter->irc_channel->nusers);
+	strlcpy(chatter->irc_channel->users[
+	  chatter->irc_channel->nusers - 1].nick, user->nick,
+	  sizeof(chatter->irc_channel->users[0].nick));
+	chatter_sync_nick_list(chatter, false);
+}
+
+void
+irc_remove_nick_from_channel(struct chatter *chatter, char *nick)
+{
+	size_t n;
+	
+	for (n = 0; n < chatter->irc_channel->nusers; n++) {
+		if (strcmp(chatter->irc_channel->users[n].nick, nick) == 0) {
+			LDelRow(1, n, chatter->nick_list);
+			
+			/* move everyone else up */
+			for (; n < chatter->irc_channel->nusers - 1; n++)
+				chatter->irc_channel->users[n] =
+				  chatter->irc_channel->users[n + 1];
+			break;
+		}
+	}
+	
+	chatter->irc_channel->nusers--;
+	chatter->irc_channel->users =
+	  xreallocarray(chatter->irc_channel->users,
+	  sizeof(struct irc_channel_nick), chatter->irc_channel->nusers);
+}
+
+void
+irc_parse_channel_mode_change(struct chatter *chatter, char *mode,
+  char *args)
+{
+	size_t len;
+	struct irc_channel_nick *cnick;
+	char *user;
+	short n, j;
+	bool add = false, need_resync = false;
+	
+	len = strlen(mode);
+	
+	/* mode:"+mvo-vo" args:"nick1 nick2 nick3 nick4" */
+	for (n = 0; n < len; n++) {
+		switch (mode[n]) {
+		case '+':
+			add = true;
+			break;
+		case '-':
+			add = false;
+			break;
+		case 'v':
+		case 'o':
+			user = strsep(&args, " ");
+			if (user == NULL)
+				user = args;
+			for (j = 0; j < chatter->irc_channel->nusers; j++) {
+				cnick = &chatter->irc_channel->users[j];
+				if (strcmp(cnick->nick, user) != 0)
+					continue;
+					
+				if (mode[n] == 'o') {
+					if (add)
+						cnick->flags |= IRC_NICK_FLAG_OP;
+					else
+						cnick->flags &= ~IRC_NICK_FLAG_OP;
+				} else if (mode[n] == 'v') {
+					if (add)
+						cnick->flags |= IRC_NICK_FLAG_VOICE;
+					else
+						cnick->flags &= ~IRC_NICK_FLAG_VOICE;
+				}
+				
+				need_resync = true;
+				break;
+			}
+
+			break;
+		default:
+			/* some other channel mode */
+			break;
+		}
+	}
+	
+	if (need_resync)
+		chatter_sync_nick_list(chatter, false);
 }