From 2c1bb4ce6ab1fa508967397400cc6db3e84a77be Mon Sep 17 00:00:00 2001
From: Simon Ser <contact@emersion.fr>
Date: Fri, 4 Jun 2021 18:27:21 +0200
Subject: [PATCH] Introduce State.handleMessage

Takes an IRC message, updates the state.

Doesn't yet handle all messages, this will be a step-by-step process.
---
 components/app.js | 123 +++++-----------------------------------------
 lib/client.js     |   5 ++
 state.js          | 101 +++++++++++++++++++++++++++++++++++++
 3 files changed, 118 insertions(+), 111 deletions(-)

diff --git a/components/app.js b/components/app.js
index d1797d7..c51d51e 100644
--- a/components/app.js
+++ b/components/app.js
@@ -561,6 +561,9 @@ export default class App extends Component {
 
 	handleMessage(serverID, msg) {
 		var client = this.clients.get(serverID);
+
+		this.setState((state) => State.handleMessage(state, msg, serverID, client));
+
 		switch (msg.command) {
 		case irc.RPL_WELCOME:
 			if (this.state.connectParams.autojoin.length > 0) {
@@ -585,91 +588,6 @@ export default class App extends Component {
 				});
 			}
 			break;
-		case irc.RPL_MYINFO:
-			// TODO: parse available modes
-			var serverInfo = {
-				name: msg.params[1],
-				version: msg.params[2],
-			};
-			this.setBufferState({ server: serverID, name: SERVER_BUFFER }, { serverInfo });
-			break;
-		case irc.RPL_ISUPPORT:
-			this.setServerState(serverID, (server) => {
-				return { isupport: new Map(client.isupport) };
-			});
-			this.setState((state) => {
-				var buffers = new Map(state.buffers);
-				state.buffers.forEach((buf) => {
-					if (buf.server != serverID) {
-						return;
-					}
-					var members = new irc.CaseMapMap(buf.members, client.cm);
-					buffers.set(buf.id, { ...buf, members });
-				});
-				return { buffers };
-			});
-			break;
-		case irc.RPL_NOTOPIC:
-			var channel = msg.params[1];
-
-			this.setBufferState({ server: serverID, name: channel }, { topic: null });
-			break;
-		case irc.RPL_TOPIC:
-			var channel = msg.params[1];
-			var topic = msg.params[2];
-
-			this.setBufferState({ server: serverID, name: channel }, { topic });
-			break;
-		case irc.RPL_TOPICWHOTIME:
-			// Ignore
-			break;
-		case irc.RPL_NAMREPLY:
-			var channel = msg.params[2];
-			var membersList = msg.params[3].split(" ");
-
-			this.setBufferState({ server: serverID, name: channel }, (buf) => {
-				var members = new irc.CaseMapMap(buf.members);
-				membersList.forEach((s) => {
-					var member = irc.parseTargetPrefix(s);
-					members.set(member.name, member.prefix);
-				});
-
-				return { members };
-			});
-			break;
-		case irc.RPL_ENDOFNAMES:
-			break;
-		case irc.RPL_WHOREPLY:
-			var last = msg.params[msg.params.length - 1];
-			var who = {
-				username: msg.params[2],
-				hostname: msg.params[3],
-				server: msg.params[4],
-				nick: msg.params[5],
-				away: msg.params[6] == 'G', // H for here, G for gone
-				realname: last.slice(last.indexOf(" ") + 1),
-			};
-
-			this.setBufferState({ server: serverID, name: who.nick }, { who, offline: false });
-
-			this.addMessage(serverID, SERVER_BUFFER, msg);
-			break;
-		case irc.RPL_ENDOFWHO:
-			var target = msg.params[1];
-			if (!this.isChannel(target) && target.indexOf("*") < 0) {
-				// Not a channel nor a mask, likely a nick
-				this.setBufferState({ server: serverID, name: target }, (buf) => {
-					// TODO: mark user offline if we have old WHO info but this
-					// WHO reply is empty
-					if (buf.who) {
-						return;
-					}
-					return { offline: true };
-				});
-			}
-
-			this.addMessage(serverID, SERVER_BUFFER, msg);
-			break;
 		case "MODE":
 			var target = msg.params[0];
 			if (this.isChannel(target)) {
@@ -732,19 +650,7 @@ export default class App extends Component {
 			break;
 		case "KICK":
 			var channel = msg.params[0];
-			var user = msg.params[1];
-
-			this.setBufferState({ server: serverID, name: channel }, (buf) => {
-				var members = new irc.CaseMapMap(buf.members);
-				members.delete(user);
-				return { members };
-			});
 			this.addMessage(serverID, channel, msg);
-
-			if (client.isMyNick(msg.prefix.name)) {
-				this.receipts.delete(channel);
-				this.saveReceipts();
-			}
 			break;
 		case "QUIT":
 			var affectedBuffers = [];
@@ -790,12 +696,6 @@ export default class App extends Component {
 			});
 			affectedBuffers.forEach((name) => this.addMessage(serverID, name, msg));
 			break;
-		case "SETNAME":
-			this.setBufferState({ server: serverID, name: msg.prefix.name }, (buf) => {
-				var who = { ...buf.who, realname: msg.params[0] };
-				return { who }
-			});
-			break;
 		case "TOPIC":
 			var channel = msg.params[0];
 			var topic = msg.params[1];
@@ -814,14 +714,6 @@ export default class App extends Component {
 
 			this.addMessage(serverID, bufName, msg);
 			break;
-		case "AWAY":
-			var awayMessage = msg.params[0];
-
-			this.setBufferState({ server: serverID, name: msg.prefix.name }, (buf) => {
-				var who = { ...buf.who, away: !!awayMessage };
-				return { who };
-			});
-			break;
 		case "BOUNCER":
 			if (msg.params[0] !== "NETWORK") {
 				break; // We're only interested in network updates
@@ -870,6 +762,15 @@ export default class App extends Component {
 			var channel = msg.params[1];
 			this.addMessage(serverID, channel, msg);
 			break;
+		case irc.RPL_MYINFO:
+		case irc.RPL_ISUPPORT:
+		case irc.RPL_NOTOPIC:
+		case irc.RPL_TOPIC:
+		case irc.RPL_TOPICWHOTIME:
+		case irc.RPL_NAMREPLY:
+		case irc.RPL_ENDOFNAMES:
+		case "AWAY":
+		case "SETNAME":
 		case "CAP":
 		case "AUTHENTICATE":
 		case "PING":
diff --git a/lib/client.js b/lib/client.js
index d24749a..3f6f93e 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -466,6 +466,11 @@ export default class Client extends EventTarget {
 		return this.cm(nick) == this.cm(this.nick);
 	}
 
+	isChannel(name) {
+		// TODO: use the ISUPPORT token if available
+		return irc.STD_CHANNEL_TYPES.indexOf(name[0]) >= 0;
+	}
+
 	setPingInterval(sec) {
 		clearInterval(this.pingIntervalID);
 		this.pingIntervalID = null;
diff --git a/state.js b/state.js
index ae4b386..29f6376 100644
--- a/state.js
+++ b/state.js
@@ -161,4 +161,105 @@ export const State = {
 			throw new Error("Invalid buffer ID type: " + (typeof id));
 		}
 	},
+	handleMessage(state, msg, serverID, client) {
+		function updateServer(updater) {
+			return State.updateServer(state, serverID, updater);
+		}
+		function updateBuffer(name, updater) {
+			return State.updateBuffer(state, { server: serverID, name }, updater);
+		}
+
+		switch (msg.command) {
+		case irc.RPL_MYINFO:
+			// TODO: parse available modes
+			var serverInfo = {
+				name: msg.params[1],
+				version: msg.params[2],
+			};
+			return updateBuffer(SERVER_BUFFER, { serverInfo });
+		case irc.RPL_ISUPPORT:
+			var buffers = new Map(state.buffers);
+			state.buffers.forEach((buf) => {
+				if (buf.server != serverID) {
+					return;
+				}
+				var members = new irc.CaseMapMap(buf.members, client.cm);
+				buffers.set(buf.id, { ...buf, members });
+			});
+			return {
+				buffers,
+				...updateServer({ isupport: new Map(client.isupport) }),
+			};
+		case irc.RPL_NOTOPIC:
+			var channel = msg.params[1];
+			return updateBuffer(channel, { topic: null });
+		case irc.RPL_TOPIC:
+			var channel = msg.params[1];
+			var topic = msg.params[2];
+			return updateBuffer(channel, { topic });
+		case irc.RPL_TOPICWHOTIME:
+			// Ignore
+			break;
+		case irc.RPL_NAMREPLY:
+			var channel = msg.params[2];
+			var membersList = msg.params[3].split(" ");
+
+			return updateBuffer(channel, (buf) => {
+				var members = new irc.CaseMapMap(buf.members);
+				membersList.forEach((s) => {
+					var member = irc.parseTargetPrefix(s);
+					members.set(member.name, member.prefix);
+				});
+				return { members };
+			});
+		case irc.RPL_ENDOFNAMES:
+			break;
+		case irc.RPL_WHOREPLY:
+			var last = msg.params[msg.params.length - 1];
+			var who = {
+				username: msg.params[2],
+				hostname: msg.params[3],
+				server: msg.params[4],
+				nick: msg.params[5],
+				away: msg.params[6] == 'G', // H for here, G for gone
+				realname: last.slice(last.indexOf(" ") + 1),
+			};
+			return updateBuffer(who.nick, { who, offline: false });
+		case irc.RPL_ENDOFWHO:
+			var target = msg.params[1];
+			if (!client.isChannel(target) && target.indexOf("*") < 0) {
+				// Not a channel nor a mask, likely a nick
+				return updateBuffer(target, (buf) => {
+					// TODO: mark user offline if we have old WHO info but this
+					// WHO reply is empty
+					if (buf.who) {
+						return;
+					}
+					return { offline: true };
+				});
+			}
+			break;
+		case "KICK":
+			var channel = msg.params[0];
+			var nick = msg.params[1];
+
+			return updateBuffer(channel, (buf) => {
+				var members = new irc.CaseMapMap(buf.members);
+				members.delete(nick);
+				return { members };
+			});
+		case "SETNAME":
+			return updateBuffer(msg.prefix.name, (buf) => {
+				var who = { ...buf.who, realname: msg.params[0] };
+				return { who };
+			});
+		case "AWAY":
+			var awayMessage = msg.params[0];
+
+			return updateBuffer(msg.prefix.name, (buf) => {
+				var who = { ...buf.who, away: !!awayMessage };
+				return { who };
+			});
+		}
+	},
 };