From 4cabae89fffd262358b4a9e9ccc0b68599ec3bea Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Fri, 10 Dec 2021 15:34:51 +0100 Subject: [PATCH] lib/irc: add CapRegistry --- commands.js | 3 ++- components/app.js | 16 ++++++------ lib/client.js | 62 +++++++++++---------------------------------- lib/irc.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++ state.js | 4 +-- 5 files changed, 90 insertions(+), 59 deletions(-) diff --git a/commands.js b/commands.js index 5f2d5d9..c0eea21 100644 --- a/commands.js +++ b/commands.js @@ -322,10 +322,11 @@ export default { execute: (app, args) => { let newRealname = args.join(" "); let client = getActiveClient(app); - if (!client.enabledCaps["setname"]) { + if (!client.caps.enabled.has("setname")) { throw new Error("Server doesn't support changing the realname"); } client.send({ command: "SETNAME", params: [newRealname] }); + // TODO: save to local storage }, }, "stats": { diff --git a/components/app.js b/components/app.js index 00b1a27..2b98575 100644 --- a/components/app.js +++ b/components/app.js @@ -364,7 +364,7 @@ export default class App extends Component { let client = this.clients.get(serverID); let stored = this.bufferStore.get({ name, server: client.params }); - if (client.enabledCaps["draft/chathistory"] && stored) { + if (client.caps.enabled.has("draft/chathistory") && stored) { this.setBufferState({ server: serverID, name }, { unread: stored.unread }); } if (!stored) { @@ -855,7 +855,7 @@ export default class App extends Component { switch (msg.command) { case irc.RPL_WELCOME: let lastReceipt = this.latestReceipt(ReceiptType.DELIVERED); - if (lastReceipt && lastReceipt.time && client.enabledCaps["draft/chathistory"] && (!client.enabledCaps["soju.im/bouncer-networks"] || client.params.bouncerNetwork)) { + if (lastReceipt && lastReceipt.time && client.caps.enabled.has("draft/chathistory") && (!client.caps.enabled.has("soju.im/bouncer-networks") || client.params.bouncerNetwork)) { let now = irc.formatDate(new Date()); client.fetchHistoryTargets(now, lastReceipt.time).then((targets) => { targets.forEach((target) => { @@ -878,7 +878,7 @@ export default class App extends Component { } if (client.isChannel(buf.name)) { - if (client.enabledCaps["soju.im/bouncer-networks"]) { + if (client.caps.enabled.has("soju.im/bouncer-networks")) { continue; } join.push(buf.name); @@ -1068,7 +1068,7 @@ export default class App extends Component { if (!bouncerNetID) { // Open dialog to create network if bouncer let client = this.clients.values().next().value; - if (!client || !client.enabledCaps["soju.im/bouncer-networks"]) { + if (!client || !client.caps.enabled.has("soju.im/bouncer-networks")) { return false; } @@ -1171,7 +1171,7 @@ export default class App extends Component { return { buffers, activeBuffer }; }); - let disconnectAll = client && !client.params.bouncerNetwork && client.enabledCaps["soju.im/bouncer-networks"]; + let disconnectAll = client && !client.params.bouncerNetwork && client.caps.enabled.has("soju.im/bouncer-networks"); this.disconnect(buf.server); @@ -1255,7 +1255,7 @@ export default class App extends Component { let msg = { command: "PRIVMSG", params: [target, text] }; client.send(msg); - if (!client.enabledCaps["echo-message"]) { + if (!client.caps.enabled.has("echo-message")) { msg.prefix = { name: client.nick }; this.addMessage(serverID, target, msg); } @@ -1395,7 +1395,7 @@ export default class App extends Component { let client = this.clients.get(buf.server); - if (!client || !client.enabledCaps["draft/chathistory"] || !client.enabledCaps["server-time"]) { + if (!client || !client.caps.enabled.has("draft/chathistory") || !client.caps.enabled.has("server-time")) { return; } if (this.endOfHistory.get(buf.id)) { @@ -1413,7 +1413,7 @@ export default class App extends Component { this.endOfHistory.set(buf.id, true); let limit = 100; - if (client.enabledCaps["draft/event-playback"]) { + if (client.caps.enabled.has("draft/event-playback")) { limit = 200; } diff --git a/lib/client.js b/lib/client.js index b401880..792cac7 100644 --- a/lib/client.js +++ b/lib/client.js @@ -109,8 +109,7 @@ export default class Client extends EventTarget { serverPrefix = { name: "*" }; nick = null; supportsCap = false; - availableCaps = {}; - enabledCaps = {}; + caps = new irc.CapRegistry(); isupport = new irc.Isupport(); ws = null; @@ -187,8 +186,7 @@ export default class Client extends EventTarget { this.setStatus(Client.Status.DISCONNECTED); this.nick = null; this.serverPrefix = null; - this.availableCaps = {}; - this.enabledCaps = {}; + this.caps = new irc.CapRegistry(); this.batches = new Map(); Object.keys(this.pendingCmds).forEach((k) => { this.pendingCmds[k] = Promise.resolve(null); @@ -602,21 +600,8 @@ export default class Client extends EventTarget { }); } - addAvailableCaps(s) { - let l = s.split(" "); - l.forEach((s) => { - let i = s.indexOf("="); - let k = s, v = ""; - if (i >= 0) { - k = s.slice(0, i); - v = s.slice(i + 1); - } - this.availableCaps[k.toLowerCase()] = v; - }); - } - supportsSASL(mech) { - let saslCap = this.availableCaps["sasl"]; + let saslCap = this.caps.available.get("sasl"); if (saslCap === undefined) { return false; } @@ -624,7 +609,7 @@ export default class Client extends EventTarget { } checkAccountRegistrationCap(k) { - let v = this.availableCaps["draft/account-registration"]; + let v = this.caps.available.get("draft/account-registration"); if (v === undefined) { return false; } @@ -637,35 +622,30 @@ export default class Client extends EventTarget { wantCaps.push("soju.im/bouncer-networks-notify"); } - let reqCaps = []; - wantCaps.forEach((cap) => { - if (this.availableCaps[cap] !== undefined && !this.enabledCaps[cap]) { - reqCaps.push(cap); - } - }); - - if (reqCaps.length > 0) { - this.send({ command: "CAP", params: ["REQ", reqCaps.join(" ")] }); + let msg = this.caps.requestAvailable(wantCaps); + if (msg) { + this.send(msg); } } handleCap(msg) { + this.caps.parse(msg); + let subCmd = msg.params[1]; let args = msg.params.slice(2); switch (subCmd) { case "LS": this.supportsCap = true; - this.addAvailableCaps(args[args.length - 1]); if (args[0] == "*") { break; } - console.log("Available server caps:", this.availableCaps); + console.log("Available server caps:", this.caps.available); this.requestCaps(); if (this.status !== Client.Status.REGISTERED) { - if (this.availableCaps["sasl"] !== undefined) { + if (this.caps.available.has("sasl")) { let promise; if (this.params.saslPlain) { promise = this.authenticate("PLAIN", this.params.saslPlain); @@ -678,7 +658,7 @@ export default class Client extends EventTarget { }); } - if (this.availableCaps["soju.im/bouncer-networks"] !== undefined && this.params.bouncerNetwork) { + if (this.caps.available.has("soju.im/bouncer-networks") && this.params.bouncerNetwork) { this.send({ command: "BOUNCER", params: ["BIND", this.params.bouncerNetwork] }); } @@ -686,28 +666,18 @@ export default class Client extends EventTarget { } break; case "NEW": - this.addAvailableCaps(args[0]); console.log("Server added available caps:", args[0]); this.requestCaps(); break; case "DEL": - args[0].split(" ").forEach((cap) => { - cap = cap.toLowerCase(); - delete this.availableCaps[cap]; - delete this.enabledCaps[cap]; - }); console.log("Server removed available caps:", args[0]); break; case "ACK": console.log("Server ack'ed caps:", args[0]); - args[0].split(" ").forEach((cap) => { - cap = cap.toLowerCase(); - this.enabledCaps[cap] = true; - }); break; case "NAK": console.log("Server nak'ed caps:", args[0]); - if (this.status != Client.Status.REGISTERED) { + if (this.status !== Client.Status.REGISTERED) { this.send({ command: "CAP", params: ["END"] }); } break; @@ -764,7 +734,7 @@ export default class Client extends EventTarget { let cmd = msg.command; let label; - if (this.enabledCaps["labeled-response"]) { + if (this.caps.enabled.has("labeled-response")) { lastLabel++; label = String(lastLabel); msg.tags = { ...msg.tags, label }; @@ -950,10 +920,6 @@ export default class Client extends EventTarget { } listBouncerNetworks() { - if (!this.enabledCaps["soju.im/bouncer-networks"]) { - return Promise.reject(new Error("Server doesn't support the BOUNCER extension")); - } - let req = { command: "BOUNCER", params: ["LISTNETWORKS"] }; return this.fetchBatch(req, "soju.im/bouncer-networks").then((batch) => { let networks = new Map(); diff --git a/lib/irc.js b/lib/irc.js index e139702..716f7d6 100644 --- a/lib/irc.js +++ b/lib/irc.js @@ -794,3 +794,67 @@ export function parseURL(str) { return { host, enttype, entity }; } + +export class CapRegistry { + available = new Map(); + enabled = new Set(); + + addAvailable(s) { + let l = s.split(" "); + l.forEach((s) => { + let i = s.indexOf("="); + let k = s, v = ""; + if (i >= 0) { + k = s.slice(0, i); + v = s.slice(i + 1); + } + this.available.set(k.toLowerCase(), v); + }); + } + + parse(msg) { + if (msg.command !== "CAP") { + return; + } + + let subCmd = msg.params[1]; + let args = msg.params.slice(2); + switch (subCmd) { + case "LS": + this.addAvailable(args[args.length - 1]); + break; + case "NEW": + this.addAvailable(args[0]); + break; + case "DEL": + args[0].split(" ").forEach((cap) => { + cap = cap.toLowerCase(); + this.available.delete(cap); + this.enabled.delete(cap); + }); + break; + case "ACK": + // TODO: handle `ACK -cap` to + args[0].split(" ").forEach((cap) => { + cap = cap.toLowerCase(); + if (cap.startsWith("-")) { + this.enabled.delete(cap.slice(1)); + } else { + this.enabled.add(cap); + } + }); + break; + } + } + + requestAvailable(l) { + l = l.filter((cap) => { + return this.available.has(cap) && !this.enabled.has(cap); + }); + + if (l.length === 0) { + return null; + } + return { command: "CAP", params: ["REQ", l.join(" ")] }; + } +} diff --git a/state.js b/state.js index 3ed9501..32f6f38 100644 --- a/state.js +++ b/state.js @@ -360,8 +360,8 @@ export const State = { case "CAP": return updateServer({ supportsSASLPlain: client.supportsSASL("PLAIN"), - supportsAccountRegistration: !!client.enabledCaps["draft/account-registration"], - isBouncer: !!client.enabledCaps["soju.im/bouncer-networks"], + supportsAccountRegistration: client.caps.enabled.has("draft/account-registration"), + isBouncer: client.caps.enabled.has("soju.im/bouncer-networks"), }); case irc.RPL_LOGGEDIN: return updateServer({ account: msg.params[2] });