~emersion/public-inbox

gamja: Add support for soju.im/namespace v1 PROPOSED

Hubert Hirtz: 1
 Add support for soju.im/namespace

 3 files changed, 136 insertions(+), 18 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/public-inbox/patches/25601/mbox | git am -3
Learn more about email & git
View this thread in the archives

[RFC PATCH gamja] Add support for soju.im/namespace Export this patch

---

See https://lists.sr.ht/~emersion/soju-dev/patches/25600

 components/app.js | 10 ++++-
 lib/client.js     | 51 +++++++++++++++++---------
 lib/conn.js       | 93 +++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 136 insertions(+), 18 deletions(-)
 create mode 100644 lib/conn.js

diff --git a/components/app.js b/components/app.js
index 1b83173..e873710 100644
--- a/components/app.js
+++ b/components/app.js
@@ -508,7 +508,14 @@ export default class App extends Component {
		});
		this.setState({ connectParams: params });

		let client = new Client(fillConnectParams(params));
		let client = null;
		if (params.useNamespace) {
			client = this.clients.get(params.useNamespace)
				.namespaced(serverID, fillConnectParams(params))
		} else {
			client = new Client(fillConnectParams(params));
		}
		client.reconnect();
		this.clients.set(serverID, client);
		this.setServerState(serverID, { status: client.status });

@@ -790,6 +797,7 @@ export default class App extends Component {
					this.connect({
						...client.params,
						bouncerNetwork: id,
						useNamespace: client.enabledCaps["soju.im/namespace"] && serverID,
					});
				}
			});
diff --git a/lib/client.js b/lib/client.js
index 1e685fb..d916501 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -1,4 +1,5 @@
import * as irc from "./irc.js";
import { NORMAL_CLOSURE, GOING_AWAY, UNSUPPORTED_DATA, IRCConn, NamespacedConn } from "./conn.js";

// Static list of capabilities that are always requested when supported by the
// server
@@ -20,16 +21,11 @@ const permanentCaps = [
	"draft/event-playback",

	"soju.im/bouncer-networks",
	"soju.im/namespace",
];

const RECONNECT_DELAY_SEC = 10;

// WebSocket status codes
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
const NORMAL_CLOSURE = 1000;
const GOING_AWAY = 1001;
const UNSUPPORTED_DATA = 1003;

// See https://github.com/quakenet/snircd/blob/master/doc/readme.who
// Sorted by order of appearance in RPL_WHOSPCRPL
const WHOX_FIELDS = {
@@ -85,8 +81,10 @@ export default class Client extends EventTarget {
		super();

		this.params = { ...this.params, ...params };
	}

		this.reconnect();
	connect() {
		this.ws = new IRCConn(this.params.url);
	}

	reconnect() {
@@ -98,7 +96,7 @@ export default class Client extends EventTarget {
		this.setStatus(Client.Status.CONNECTING);

		try {
			this.ws = new WebSocket(this.params.url);
			this.connect();
		} catch (err) {
			console.error("Failed to create connection:", err);
			setTimeout(() => {
@@ -186,13 +184,7 @@ export default class Client extends EventTarget {
	}

	handleMessage(event) {
		if (typeof event.data !== "string") {
			console.error("Received unsupported data type:", event.data);
			this.ws.close(UNSUPPORTED_DATA);
			return;
		}

		let msg = irc.parseMessage(event.data);
		let msg = event.detail;
		console.debug("Received:", msg);

		// If the prefix is missing, assume it's coming from the server on the
@@ -567,8 +559,7 @@ export default class Client extends EventTarget {
		if (!this.ws) {
			throw new Error("Failed to send IRC message " + msg.command + ": socket is closed");
		}
		this.ws.send(irc.formatMessage(msg));
		console.debug("Sent:", msg);
		this.ws.send(msg);
	}

	setCaseMapping(name) {
@@ -825,4 +816,30 @@ export default class Client extends EventTarget {

		this.send({ command: "MONITOR", params: ["-", target] });
	}

	namespaced(namespace, params) {
		return new Namespaced(this, namespace, params);
	}
}

class Namespaced extends Client {
	parentClient = null;
	namespace = "";

	constructor(parentClient, namespace, params) {
		super(params);

		this.parentClient = parentClient;
		this.namespace = namespace;

		this.parentClient.addEventListener("status", () => {
			if (this.parentClient.status === Client.Status.REGISTERED) {
				this.reconnect()
			}
		});
	}

	connect() {
		this.ws = new NamespacedConn(this.parentClient.ws, this.namespace);
	}
}
diff --git a/lib/conn.js b/lib/conn.js
new file mode 100644
index 0000000..d03981d
--- /dev/null
+++ b/lib/conn.js
@@ -0,0 +1,93 @@
import * as irc from "./irc.js";

// WebSocket status codes
// https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
export const NORMAL_CLOSURE = 1000;
export const GOING_AWAY = 1001;
export const UNSUPPORTED_DATA = 1003;

export class IRCConn extends EventTarget {
	ws = null;

	constructor(url) {
		super();

		this.ws = new WebSocket(url);
		this.ws.addEventListener("open", (event) => {
			this.dispatchEvent(new CustomEvent("open", event));
		});
		this.ws.addEventListener("close", (event) => {
			this.dispatchEvent(new CustomEvent("close", event));
		});
		this.ws.addEventListener("message", (event) => {
			if (typeof event.data !== "string") {
				console.error("Received unsupported data type:", event.data);
				this.ws.close(UNSUPPORTED_DATA);
				return;
			}
			const msg = irc.parseMessage(event.data);
			if (msg.tags["soju.im/namespace"]) {
				return;
			}
			this.dispatchEvent(new CustomEvent("message", { detail: msg }));
		});
	}

	close() {
		this.ws.close();
	}

	send(msg) {
		this.ws.send(irc.formatMessage(msg));
		console.debug("Sent:", msg);
	}
}

export class NamespacedConn extends EventTarget {
	inner = null;
	namespace = "";

	constructor(inner, namespace) {
		super();

		this.inner = inner;
		this.namespace = namespace.toString();

		this.inner.ws.addEventListener("message", (event) => {
			if (typeof event.data !== "string") {
				return;
			}
			const msg = irc.parseMessage(event.data);
			console.debug("namespace handling of ", msg);
			if (msg.command === "NAMESPACE" && msg.params[0] === this.namespace) {
				console.debug("opened namespaced connection");
				this.dispatchEvent(new CustomEvent("open"));
				return;
			}
			if (msg.tags["soju.im/namespace"] !== this.namespace) {
				return;
			}
			delete msg.tags["soju.im/namespace"];
			if (msg.command === "ERROR") {
				this.dispatchEvent(new CloseEvent({ code: NORMAL_CLOSURE, reason: msg.params[0] }));
			} else {
				this.dispatchEvent(new CustomEvent("message", { detail: msg }));
			}
		});

		this.inner.send({ command: "NAMESPACE", params: [this.namespace] });
	}

	close() {
		this.send({ command: "QUIT" });
	}

	send(msg) {
		if (!msg.tags) {
			msg.tags = {};
		}
		msg.tags["soju.im/namespace"] = this.namespace;
		this.inner.send(msg);
		console.debug("Sent:", msg);
	}
}
-- 
2.33.0