Refactor ISUPPORT handling

Add a helper class to parse ISUPPORT tokens. Instead of having
manual ISUPPORT handling all over the place, use pre-processed
values.
This commit is contained in:
Simon Ser 2021-12-07 12:09:10 +01:00
parent 31b293fa03
commit ab3d4dd661
7 changed files with 128 additions and 84 deletions

View file

@ -718,7 +718,7 @@ export default class App extends Component {
target = SERVER_BUFFER; target = SERVER_BUFFER;
} }
let allowedPrefixes = client.isupport.get("STATUSMSG"); let allowedPrefixes = client.isupport.statusMsg();
if (allowedPrefixes) { if (allowedPrefixes) {
let parts = irc.parseTargetPrefix(target, allowedPrefixes); let parts = irc.parseTargetPrefix(target, allowedPrefixes);
if (client.isChannel(parts.name)) { if (client.isChannel(parts.name)) {
@ -890,7 +890,7 @@ export default class App extends Component {
// Auto-join channels given at connect-time // Auto-join channels given at connect-time
let server = this.state.servers.get(serverID); let server = this.state.servers.get(serverID);
let bouncerNetID = server.isupport.get("BOUNCER_NETID"); let bouncerNetID = server.bouncerNetID;
let bouncerNetwork = null; let bouncerNetwork = null;
if (bouncerNetID) { if (bouncerNetID) {
bouncerNetwork = this.state.bouncerNetworks.get(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 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 // This can happen if the user has specified a network to bind
// to via other means, e.g. "<username>/<network>". // to via other means, e.g. "<username>/<network>".
break; break;
@ -1081,7 +1081,7 @@ export default class App extends Component {
} }
for (let [id, server] of this.state.servers) { for (let [id, server] of this.state.servers) {
if (server.isupport.get("BOUNCER_NETID") === bouncerNetID) { if (server.bouncerNetID === bouncerNetID) {
serverID = id; serverID = id;
break; break;
} }
@ -1529,7 +1529,7 @@ export default class App extends Component {
handleManageNetworkClick(serverID) { handleManageNetworkClick(serverID) {
let server = this.state.servers.get(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); let bouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID);
this.openDialog("network", { this.openDialog("network", {
id: bouncerNetID, id: bouncerNetID,
@ -1599,7 +1599,7 @@ export default class App extends Component {
let activeClient = this.clients.get(activeBuffer.server); let activeClient = this.clients.get(activeBuffer.server);
isBouncer = activeClient && activeClient.enabledCaps["soju.im/bouncer-networks"]; isBouncer = activeClient && activeClient.enabledCaps["soju.im/bouncer-networks"];
let bouncerNetID = activeServer.isupport.get("BOUNCER_NETID"); let bouncerNetID = activeServer.bouncerNetID;
if (bouncerNetID) { if (bouncerNetID) {
activeBouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID); activeBouncerNetwork = this.state.bouncerNetworks.get(bouncerNetID);
} }

View file

@ -87,7 +87,7 @@ export default function BufferHeader(props) {
`; `;
if (props.isBouncer) { if (props.isBouncer) {
if (props.server.isupport.get("BOUNCER_NETID")) { if (props.server.bouncerNetID) {
if (fullyConnected) { if (fullyConnected) {
actions.push(joinButton); actions.push(joinButton);
} }
@ -186,7 +186,7 @@ export default function BufferHeader(props) {
item = `authenticated as ${props.user.account}`; item = `authenticated as ${props.user.account}`;
} }
details.push(html`<abbr title=${desc}>${item}</abbr>`); details.push(html`<abbr title=${desc}>${item}</abbr>`);
} 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 // If the server supports MONITOR and WHOX, we can faithfully
// keep user.account up-to-date for user queries // keep user.account up-to-date for user queries
let desc = "This user has not been verified and is not logged in."; let desc = "This user has not been verified and is not logged in.";

View file

@ -44,7 +44,7 @@ export default function BufferList(props) {
let server = props.servers.get(buf.server); let server = props.servers.get(buf.server);
let bouncerNetwork = null; let bouncerNetwork = null;
let bouncerNetID = server.isupport.get("BOUNCER_NETID"); let bouncerNetID = server.bouncerNetID;
if (bouncerNetID) { if (bouncerNetID) {
bouncerNetwork = props.bouncerNetworks.get(bouncerNetID); bouncerNetwork = props.bouncerNetworks.get(bouncerNetID);
} }

View file

@ -125,7 +125,7 @@ class LogLine extends Component {
} }
let status = null; let status = null;
let allowedPrefixes = server.isupport.get("STATUSMSG"); let allowedPrefixes = server.statusMsg;
if (target !== buf.name && allowedPrefixes) { if (target !== buf.name && allowedPrefixes) {
let parts = irc.parseTargetPrefix(target, allowedPrefixes); let parts = irc.parseTargetPrefix(target, allowedPrefixes);
if (parts.name === buf.name) { if (parts.name === buf.name) {
@ -487,8 +487,8 @@ class ProtocolHandlerNagger extends Component {
function AccountNagger({ server, onAuthClick, onRegisterClick }) { function AccountNagger({ server, onAuthClick, onRegisterClick }) {
let accDesc = "an account on this server"; let accDesc = "an account on this server";
if (server.isupport.has("NETWORK")) { if (server.name) {
accDesc = "a " + server.isupport.get("NETWORK") + " account"; accDesc = "a " + server.name + " account";
} }
function handleAuthClick(event) { function handleAuthClick(event) {
@ -564,13 +564,13 @@ export default class Buffer extends Component {
let server = this.props.server; let server = this.props.server;
let bouncerNetwork = this.props.bouncerNetwork; let bouncerNetwork = this.props.bouncerNetwork;
let serverName = server.isupport.get("NETWORK"); let serverName = server.name;
let children = []; let children = [];
if (buf.type == BufferType.SERVER) { if (buf.type == BufferType.SERVER) {
children.push(html`<${NotificationNagger}/>`); 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}/>`); children.push(html`<${ProtocolHandlerNagger} bouncerName=${serverName}/>`);
} }
if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) { if (buf.type == BufferType.SERVER && server.status == ServerStatus.REGISTERED && server.supportsSASLPlain && !server.account) {

View file

@ -78,7 +78,7 @@ export default class Client extends EventTarget {
supportsCap = false; supportsCap = false;
availableCaps = {}; availableCaps = {};
enabledCaps = {}; enabledCaps = {};
isupport = new Map(); isupport = new irc.Isupport();
ws = null; ws = null;
params = { params = {
@ -159,7 +159,7 @@ export default class Client extends EventTarget {
Object.keys(this.pendingCmds).forEach((k) => { Object.keys(this.pendingCmds).forEach((k) => {
this.pendingCmds[k] = Promise.resolve(null); this.pendingCmds[k] = Promise.resolve(null);
}); });
this.isupport = new Map(); this.isupport = new irc.Isupport();
this.monitored = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459); this.monitored = new irc.CaseMapMap(null, irc.CaseMapping.RFC1459);
if (this.autoReconnect) { if (this.autoReconnect) {
@ -282,22 +282,24 @@ export default class Client extends EventTarget {
this.setStatus(Client.Status.REGISTERED); this.setStatus(Client.Status.REGISTERED);
break; break;
case irc.RPL_ISUPPORT: case irc.RPL_ISUPPORT:
let prevMaxMonitorTargets = this.isupport.monitor();
let tokens = msg.params.slice(1, -1); let tokens = msg.params.slice(1, -1);
let changed = irc.parseISUPPORT(tokens, this.isupport); this.isupport.parse(tokens);
if (changed.indexOf("CASEMAPPING") >= 0) { this.updateCaseMapping();
this.setCaseMapping(this.isupport.get("CASEMAPPING"));
} let maxMonitorTargets = this.isupport.monitor();
if (changed.indexOf("MONITOR") >= 0 && this.isupport.has("MONITOR") && this.monitored.size > 0) { if (prevMaxMonitorTargets === 0 && this.monitored.size > 0 && maxMonitorTargets > 0) {
let targets = Array.from(this.monitored.keys()).slice(0, this.maxMonitorTargets()); let targets = Array.from(this.monitored.keys()).slice(0, maxMonitorTargets);
this.send({ command: "MONITOR", params: ["+", targets.join(",")] }); this.send({ command: "MONITOR", params: ["+", targets.join(",")] });
} }
break; break;
case irc.RPL_ENDOFMOTD: case irc.RPL_ENDOFMOTD:
case irc.ERR_NOMOTD: case irc.ERR_NOMOTD:
// These messages are used to indicate the end of the ISUPPORT list // 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 // Server didn't send any CASEMAPPING token, assume RFC 1459
this.setCaseMapping("rfc1459"); this.updateCaseMapping();
} }
break; break;
case "CAP": case "CAP":
@ -451,7 +453,7 @@ export default class Client extends EventTarget {
let params = [mask]; let params = [mask];
let fields = "", token = ""; let fields = "", token = "";
if (options && this.isupport.has("WHOX")) { if (options && this.isupport.whox()) {
let match = ""; // Matches exact channel or nick let match = ""; // Matches exact channel or nick
fields = "t"; // Always include token in reply fields = "t"; // Always include token in reply
@ -685,13 +687,8 @@ export default class Client extends EventTarget {
} }
} }
setCaseMapping(name) { updateCaseMapping() {
this.cm = irc.CaseMapping.byName(name); this.cm = this.isupport.caseMapping();
if (!this.cm) {
console.error("Unsupported case-mapping '" + name + "', falling back to RFC 1459");
this.cm = irc.CaseMapping.RFC1459;
}
this.pendingLists = new irc.CaseMapMap(this.pendingLists, this.cm); this.pendingLists = new irc.CaseMapMap(this.pendingLists, this.cm);
this.monitored = new irc.CaseMapMap(this.monitored, this.cm); this.monitored = new irc.CaseMapMap(this.monitored, this.cm);
} }
@ -705,7 +702,7 @@ export default class Client extends EventTarget {
} }
isChannel(name) { isChannel(name) {
let chanTypes = this.isupport.get("CHANTYPES") || irc.STD_CHANTYPES; let chanTypes = this.isupport.chanTypes();
return chanTypes.indexOf(name[0]) >= 0; return chanTypes.indexOf(name[0]) >= 0;
} }
@ -870,19 +867,9 @@ export default class Client extends EventTarget {
return promise; 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. */ /* Fetch one page of history before the given date. */
fetchHistoryBefore(target, before, limit) { 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]; let params = ["BEFORE", target, "timestamp=" + before, max];
return this.roundtripChatHistory(params).then((messages) => { return this.roundtripChatHistory(params).then((messages) => {
return { more: messages.length >= max }; return { more: messages.length >= max };
@ -891,7 +878,7 @@ export default class Client extends EventTarget {
/* Fetch history in ascending order. */ /* Fetch history in ascending order. */
fetchHistoryBetween(target, after, before, limit) { 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]; let params = ["AFTER", target, "timestamp=" + after.time, max];
return this.roundtripChatHistory(params).then((messages) => { return this.roundtripChatHistory(params).then((messages) => {
limit -= messages.length; 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) { monitor(target) {
if (this.monitored.has(target)) { if (this.monitored.has(target)) {
return; return;
@ -962,7 +938,7 @@ export default class Client extends EventTarget {
this.monitored.set(target, true); this.monitored.set(target, true);
// TODO: add poll-based fallback when MONITOR is not supported // 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; return;
} }
@ -976,7 +952,7 @@ export default class Client extends EventTarget {
this.monitored.delete(target); this.monitored.delete(target);
if (!this.isupport.has("MONITOR")) { if (this.isupport.monitor() <= 0) {
return; return;
} }

View file

@ -382,28 +382,88 @@ function unescapeISUPPORTValue(s) {
}); });
} }
export function parseISUPPORT(tokens, params) { export class Isupport {
let changed = []; raw = new Map();
tokens.forEach((tok) => {
if (tok.startsWith("-")) { parse(tokens) {
let k = tok.slice(1); tokens.forEach((tok) => {
params.delete(k.toUpperCase()); if (tok.startsWith("-")) {
return; 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 cm = CaseMapping.byName(name);
let i = tok.indexOf("="); if (!cm) {
let k = tok, v = ""; console.error("Unsupported case-mapping '" + name + "', falling back to RFC 1459");
if (i >= 0) { return CaseMapping.RFC1459;
k = tok.slice(0, i);
v = unescapeISUPPORTValue(tok.slice(i + 1));
} }
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); whox() {
changed.push(k); return this.raw.has("WHOX");
}); }
return changed;
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 = { export const CaseMapping = {
@ -612,8 +672,8 @@ export function getMessageLabel(msg) {
} }
export function forEachChannelModeUpdate(msg, isupport, callback) { export function forEachChannelModeUpdate(msg, isupport, callback) {
let chanmodes = isupport.get("CHANMODES") || STD_CHANMODES; let chanmodes = isupport.chanModes();
let prefix = isupport.get("PREFIX") || ""; let prefix = isupport.prefix();
let typeByMode = new Map(); let typeByMode = new Map();
let [a, b, c, d] = chanmodes.split(","); let [a, b, c, d] = chanmodes.split(",");

View file

@ -64,7 +64,7 @@ export function getMessageURL(buf, msg) {
} }
export function getServerName(server, bouncerNetwork, isBouncer) { export function getServerName(server, bouncerNetwork, isBouncer) {
let netName = server.isupport.get("NETWORK"); let netName = server.name;
if (bouncerNetwork && bouncerNetwork.name && bouncerNetwork.name !== bouncerNetwork.host) { if (bouncerNetwork && bouncerNetwork.name && bouncerNetwork.name !== bouncerNetwork.host) {
// User has picked a custom name for the network, use that // 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) { 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) => { let prefixPrivs = new Map(irc.parseMembershipModes(prefix).map((membership, i) => {
return [membership.prefix, i]; return [membership.prefix, i];
@ -231,7 +231,7 @@ export const State = {
let cm = irc.CaseMapping.RFC1459; let cm = irc.CaseMapping.RFC1459;
let server = state.servers.get(serverID); let server = state.servers.get(serverID);
if (server) { if (server) {
cm = irc.CaseMapping.byName(server.isupport.get("CASEMAPPING")) || cm; cm = server.cm;
} }
let nameCM = cm(name); let nameCM = cm(name);
@ -252,12 +252,16 @@ export const State = {
let servers = new Map(state.servers); let servers = new Map(state.servers);
servers.set(id, { servers.set(id, {
id, id,
name: null, // from ISUPPORT NETWORK
status: ServerStatus.DISCONNECTED, status: ServerStatus.DISCONNECTED,
isupport: new Map(), cm: irc.CaseMapping.RFC1459,
users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459), users: new irc.CaseMapMap(null, irc.CaseMapping.RFC1459),
account: null, account: null,
supportsSASLPlain: false, supportsSASLPlain: false,
supportsAccountRegistration: false, supportsAccountRegistration: false,
reliableUserAccounts: false,
statusMsg: null, // from ISUPPORT STATUSMSG
bouncerNetID: null,
}); });
return [id, { servers }]; return [id, { servers }];
}, },
@ -343,8 +347,12 @@ export const State = {
buffers, buffers,
...updateServer((server) => { ...updateServer((server) => {
return { return {
isupport: new Map(client.isupport), name: client.isupport.network(),
cm: client.cm,
users: new irc.CaseMapMap(server.users, 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 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) => { let prefixByMode = new Map(irc.parseMembershipModes(prefix).map((membership) => {
return [membership.mode, membership.prefix]; return [membership.mode, membership.prefix];
})); }));