diff --git a/components/app.js b/components/app.js index 6cbfbdc..749fd9f 100644 --- a/components/app.js +++ b/components/app.js @@ -718,7 +718,7 @@ export default class App extends Component { target = SERVER_BUFFER; } - let allowedPrefixes = client.isupport.get("STATUSMSG"); + let allowedPrefixes = client.isupport.statusMsg(); if (allowedPrefixes) { let parts = irc.parseTargetPrefix(target, allowedPrefixes); if (client.isChannel(parts.name)) { @@ -890,7 +890,7 @@ export default class App extends Component { // Auto-join channels given at connect-time let server = this.state.servers.get(serverID); - let bouncerNetID = server.isupport.get("BOUNCER_NETID"); + let bouncerNetID = server.bouncerNetID; let bouncerNetwork = null; if (bouncerNetID) { bouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID); @@ -936,7 +936,7 @@ export default class App extends Component { break; // We're only interested in network updates } - if (client.isupport.has("BOUNCER_NETID")) { + if (client.isupport.bouncerNetID()) { // This can happen if the user has specified a network to bind // to via other means, e.g. "/". break; @@ -1081,7 +1081,7 @@ export default class App extends Component { } for (let [id, server] of this.state.servers) { - if (server.isupport.get("BOUNCER_NETID") === bouncerNetID) { + if (server.bouncerNetID === bouncerNetID) { serverID = id; break; } @@ -1529,7 +1529,7 @@ export default class App extends Component { handleManageNetworkClick(serverID) { let server = this.state.servers.get(serverID); - let bouncerNetID = server.isupport.get("BOUNCER_NETID"); + let bouncerNetID = server.bouncerNetID; let bouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID); this.openDialog("network", { id: bouncerNetID, @@ -1599,7 +1599,7 @@ export default class App extends Component { let activeClient = this.clients.get(activeBuffer.server); isBouncer = activeClient && activeClient.enabledCaps["soju.im/bouncer-networks"]; - let bouncerNetID = activeServer.isupport.get("BOUNCER_NETID"); + let bouncerNetID = activeServer.bouncerNetID; if (bouncerNetID) { activeBouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID); } diff --git a/components/buffer-header.js b/components/buffer-header.js index d75cb45..ef45f35 100644 --- a/components/buffer-header.js +++ b/components/buffer-header.js @@ -87,7 +87,7 @@ export default function BufferHeader(props) { `; if (props.isBouncer) { - if (props.server.isupport.get("BOUNCER_NETID")) { + if (props.server.bouncerNetID) { if (fullyConnected) { actions.push(joinButton); } @@ -186,7 +186,7 @@ export default function BufferHeader(props) { item = `authenticated as ${props.user.account}`; } details.push(html`${item}`); - } else if (props.server.isupport.has("MONITOR") && props.server.isupport.has("WHOX")) { + } else if (props.server.reliableUserAccounts) { // If the server supports MONITOR and WHOX, we can faithfully // keep user.account up-to-date for user queries let desc = "This user has not been verified and is not logged in."; diff --git a/components/buffer-list.js b/components/buffer-list.js index c2930ec..96a219e 100644 --- a/components/buffer-list.js +++ b/components/buffer-list.js @@ -44,7 +44,7 @@ export default function BufferList(props) { let server = props.servers.get(buf.server); let bouncerNetwork = null; - let bouncerNetID = server.isupport.get("BOUNCER_NETID"); + let bouncerNetID = server.bouncerNetID; if (bouncerNetID) { bouncerNetwork = props.bouncerNetworks.get(bouncerNetID); } diff --git a/components/buffer.js b/components/buffer.js index f976923..6e08faf 100644 --- a/components/buffer.js +++ b/components/buffer.js @@ -125,7 +125,7 @@ class LogLine extends Component { } let status = null; - let allowedPrefixes = server.isupport.get("STATUSMSG"); + let allowedPrefixes = server.statusMsg; if (target !== buf.name && allowedPrefixes) { let parts = irc.parseTargetPrefix(target, allowedPrefixes); if (parts.name === buf.name) { @@ -487,8 +487,8 @@ class ProtocolHandlerNagger extends Component { function AccountNagger({ server, onAuthClick, onRegisterClick }) { let accDesc = "an account on this server"; - if (server.isupport.has("NETWORK")) { - accDesc = "a " + server.isupport.get("NETWORK") + " account"; + if (server.name) { + accDesc = "a " + server.name + " account"; } function handleAuthClick(event) { @@ -564,13 +564,13 @@ export default class Buffer extends Component { let server = this.props.server; let bouncerNetwork = this.props.bouncerNetwork; - let serverName = server.isupport.get("NETWORK"); + let serverName = server.name; let children = []; if (buf.type == BufferType.SERVER) { children.push(html`<${NotificationNagger}/>`); } - if (buf.type == BufferType.SERVER && this.props.isBouncer && !server.isupport.has("BOUNCER_NETID")) { + if (buf.type == BufferType.SERVER && this.props.isBouncer && !server.bouncerNetID) { children.push(html`<${ProtocolHandlerNagger} bouncerName=${serverName}/>`); } if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) { diff --git a/lib/client.js b/lib/client.js index 9c39ce7..31252a2 100644 --- a/lib/client.js +++ b/lib/client.js @@ -78,7 +78,7 @@ export default class Client extends EventTarget { supportsCap = false; availableCaps = {}; enabledCaps = {}; - isupport = new Map(); + isupport = new irc.Isupport(); ws = null; params = { @@ -159,7 +159,7 @@ export default class Client extends EventTarget { Object.keys(this.pendingCmds).forEach((k) => { this.pendingCmds[k] = Promise.resolve(null); }); - this.isupport = new Map(); + this.isupport = new irc.Isupport(); this.monitored = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459); if (this.autoReconnect) { @@ -282,22 +282,24 @@ export default class Client extends EventTarget { this.setStatus(Client.Status.REGISTERED); break; case irc.RPL_ISUPPORT: + let prevMaxMonitorTargets = this.isupport.monitor(); + let tokens = msg.params.slice(1, -1); - let changed = irc.parseISUPPORT(tokens, this.isupport); - if (changed.indexOf("CASEMAPPING") >= 0) { - this.setCaseMapping(this.isupport.get("CASEMAPPING")); - } - if (changed.indexOf("MONITOR") >= 0 && this.isupport.has("MONITOR") && this.monitored.size > 0) { - let targets = Array.from(this.monitored.keys()).slice(0, this.maxMonitorTargets()); + this.isupport.parse(tokens); + this.updateCaseMapping(); + + let maxMonitorTargets = this.isupport.monitor(); + if (prevMaxMonitorTargets === 0 && this.monitored.size > 0 && maxMonitorTargets > 0) { + let targets = Array.from(this.monitored.keys()).slice(0, maxMonitorTargets); this.send({ command: "MONITOR", params: ["+", targets.join(",")] }); } break; case irc.RPL_ENDOFMOTD: case irc.ERR_NOMOTD: // These messages are used to indicate the end of the ISUPPORT list - if (!this.isupport.has("CASEMAPPING")) { + if (!this.isupport.raw.has("CASEMAPPING")) { // Server didn't send any CASEMAPPING token, assume RFC 1459 - this.setCaseMapping("rfc1459"); + this.updateCaseMapping(); } break; case "CAP": @@ -451,7 +453,7 @@ export default class Client extends EventTarget { let params = [mask]; let fields = "", token = ""; - if (options && this.isupport.has("WHOX")) { + if (options && this.isupport.whox()) { let match = ""; // Matches exact channel or nick fields = "t"; // Always include token in reply @@ -685,13 +687,8 @@ export default class Client extends EventTarget { } } - setCaseMapping(name) { - this.cm = irc.CaseMapping.byName(name); - if (!this.cm) { - console.error("Unsupported case-mapping '" + name + "', falling back to RFC 1459"); - this.cm = irc.CaseMapping.RFC1459; - } - + updateCaseMapping() { + this.cm = this.isupport.caseMapping(); this.pendingLists = new irc.CaseMapMap(this.pendingLists, this.cm); this.monitored = new irc.CaseMapMap(this.monitored, this.cm); } @@ -705,7 +702,7 @@ export default class Client extends EventTarget { } isChannel(name) { - let chanTypes = this.isupport.get("CHANTYPES") || irc.STD_CHANTYPES; + let chanTypes = this.isupport.chanTypes(); return chanTypes.indexOf(name[0]) >= 0; } @@ -870,19 +867,9 @@ export default class Client extends EventTarget { return promise; } - chatHistoryPageSize() { - if (this.isupport.has("CHATHISTORY")) { - let pageSize = parseInt(this.isupport.get("CHATHISTORY"), 10); - if (pageSize > 0) { - return pageSize; - } - } - return 100; - } - /* Fetch one page of history before the given date. */ fetchHistoryBefore(target, before, limit) { - let max = Math.min(limit, this.chatHistoryPageSize()); + let max = Math.min(limit, this.isupport.chatHistory()); let params = ["BEFORE", target, "timestamp=" + before, max]; return this.roundtripChatHistory(params).then((messages) => { return { more: messages.length >= max }; @@ -891,7 +878,7 @@ export default class Client extends EventTarget { /* Fetch history in ascending order. */ fetchHistoryBetween(target, after, before, limit) { - let max = Math.min(limit, this.chatHistoryPageSize()); + let max = Math.min(limit, this.isupport.chatHistory()); let params = ["AFTER", target, "timestamp=" + after.time, max]; return this.roundtripChatHistory(params).then((messages) => { limit -= messages.length; @@ -943,17 +930,6 @@ export default class Client extends EventTarget { }); } - maxMonitorTargets() { - if (!this.isupport.has("MONITOR")) { - return 0; - } - let v = this.isupport.get("MONITOR"); - if (v === "") { - return Infinity; - } - return parseInt(v, 10); - } - monitor(target) { if (this.monitored.has(target)) { return; @@ -962,7 +938,7 @@ export default class Client extends EventTarget { this.monitored.set(target, true); // TODO: add poll-based fallback when MONITOR is not supported - if (this.monitored.size + 1 > this.maxMonitorTargets()) { + if (this.monitored.size + 1 > this.isupport.monitor()) { return; } @@ -976,7 +952,7 @@ export default class Client extends EventTarget { this.monitored.delete(target); - if (!this.isupport.has("MONITOR")) { + if (this.isupport.monitor() <= 0) { return; } diff --git a/lib/irc.js b/lib/irc.js index 39f204f..e139702 100644 --- a/lib/irc.js +++ b/lib/irc.js @@ -382,28 +382,88 @@ function unescapeISUPPORTValue(s) { }); } -export function parseISUPPORT(tokens, params) { - let changed = []; - tokens.forEach((tok) => { - if (tok.startsWith("-")) { - let k = tok.slice(1); - params.delete(k.toUpperCase()); - return; +export class Isupport { + raw = new Map(); + + parse(tokens) { + tokens.forEach((tok) => { + if (tok.startsWith("-")) { + let k = tok.slice(1); + this.raw.delete(k.toUpperCase()); + return; + } + + let i = tok.indexOf("="); + let k = tok, v = ""; + if (i >= 0) { + k = tok.slice(0, i); + v = unescapeISUPPORTValue(tok.slice(i + 1)); + } + + k = k.toUpperCase(); + + this.raw.set(k, v); + }); + } + + caseMapping() { + let name = this.raw.get("CASEMAPPING"); + if (!name) { + return CaseMapping.RFC1459; } - - let i = tok.indexOf("="); - let k = tok, v = ""; - if (i >= 0) { - k = tok.slice(0, i); - v = unescapeISUPPORTValue(tok.slice(i + 1)); + let cm = CaseMapping.byName(name); + if (!cm) { + console.error("Unsupported case-mapping '" + name + "', falling back to RFC 1459"); + return CaseMapping.RFC1459; } + return cm; + } - k = k.toUpperCase(); + monitor() { + if (!this.raw.has("MONITOR")) { + return 0; + } + let v = this.raw.get("MONITOR"); + if (v === "") { + return Infinity; + } + return parseInt(v, 10); + } - params.set(k, v); - changed.push(k); - }); - return changed; + whox() { + return this.raw.has("WHOX"); + } + + prefix() { + return this.raw.get("PREFIX") || ""; + } + + chanTypes() { + return this.raw.get("CHANTYPES") || STD_CHANTYPES; + } + + statusMsg() { + return this.raw.get("STATUSMSG"); + } + + network() { + return this.raw.get("NETWORK"); + } + + chatHistory() { + if (!this.raw.has("CHATHISTORY")) { + return 0; + } + let n = parseInt(this.raw.get("CHATHISTORY"), 10); + if (n <= 0) { + return Infinity; + } + return n; + } + + bouncerNetID() { + return this.raw.get("BOUNCER_NETID"); + } } export const CaseMapping = { @@ -612,8 +672,8 @@ export function getMessageLabel(msg) { } export function forEachChannelModeUpdate(msg, isupport, callback) { - let chanmodes = isupport.get("CHANMODES") || STD_CHANMODES; - let prefix = isupport.get("PREFIX") || ""; + let chanmodes = isupport.chanModes(); + let prefix = isupport.prefix(); let typeByMode = new Map(); let [a, b, c, d] = chanmodes.split(","); diff --git a/state.js b/state.js index c38425d..37a5c9a 100644 --- a/state.js +++ b/state.js @@ -64,7 +64,7 @@ export function getMessageURL(buf, msg) { } export function getServerName(server, bouncerNetwork, isBouncer) { - let netName = server.isupport.get("NETWORK"); + let netName = server.name; if (bouncerNetwork && bouncerNetwork.name && bouncerNetwork.name !== bouncerNetwork.host) { // User has picked a custom name for the network, use that @@ -118,7 +118,7 @@ function compareBuffers(a, b) { } function updateMembership(membership, letter, add, client) { - let prefix = client.isupport.get("PREFIX") || ""; + let prefix = client.isupport.prefix(); let prefixPrivs = new Map(irc.parseMembershipModes(prefix).map((membership, i) => { return [membership.prefix, i]; @@ -231,7 +231,7 @@ export const State = { let cm = irc.CaseMapping.RFC1459; let server = state.servers.get(serverID); if (server) { - cm = irc.CaseMapping.byName(server.isupport.get("CASEMAPPING")) || cm; + cm = server.cm; } let nameCM = cm(name); @@ -252,12 +252,16 @@ export const State = { let servers = new Map(state.servers); servers.set(id, { id, + name: null, // from ISUPPORT NETWORK status: ServerStatus.DISCONNECTED, - isupport: new Map(), + cm: irc.CaseMapping.RFC1459, users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459), account: null, supportsSASLPlain: false, supportsAccountRegistration: false, + reliableUserAccounts: false, + statusMsg: null, // from ISUPPORT STATUSMSG + bouncerNetID: null, }); return [id, { servers }]; }, @@ -343,8 +347,12 @@ export const State = { buffers, ...updateServer((server) => { return { - isupport: new Map(client.isupport), + name: client.isupport.network(), + cm: client.cm, users: new irc.CaseMapMap(server.users, client.cm), + reliableUserAccounts: client.isupport.monitor() > 0 && client.isupport.whox(), + statusMsg: client.isupport.statusMsg(), + bouncerNetID: client.isupport.bouncerNetID, }; }), }; @@ -550,7 +558,7 @@ export const State = { return; // TODO: handle user mode changes too } - let prefix = client.isupport.get("PREFIX") || ""; + let prefix = client.prefix(); let prefixByMode = new Map(irc.parseMembershipModes(prefix).map((membership) => { return [membership.mode, membership.prefix]; }));