Properly track user modes for context menu (#4267)

* properly track user modes for context menu

The RPL_ISUPPORT response contains a PREFIX element, which not only tracks the
prefix chars ("@", "+" etc) but also their corresponding mode chars (+O, +v)
This commit changes the context menu to not rely on a hardcoded list but rather
user the one given in the prefix response by the server.

Co-authored-by: Max Leiter <maxwell.leiter@gmail.com>
This commit is contained in:
Reto 2021-07-21 09:30:07 +02:00 committed by GitHub
parent 03d38812e3
commit 8fcd079204
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 46 deletions

View File

@ -252,22 +252,30 @@ export function generateUserContextMenu($root, channel, network, user) {
return items;
}
// Names of the modes we are able to change
const modes = {
"~": ["owner", "q"],
"&": ["admin", "a"],
"@": ["operator", "o"],
"%": ["half-op", "h"],
"+": ["voice", "v"],
// Names of the standard modes we are able to change
const modeCharToName = {
"~": "owner",
"&": "admin",
"@": "operator",
"%": "half-op",
"+": "voice",
};
// Labels for the mode changes. For example .rev(['admin', 'a']) => 'Revoke admin (-a)'
// Labels for the mode changes. For example .rev({mode: "a", symbol: "&"}) => 'Revoke admin (-a)'
const modeTextTemplate = {
revoke: (m) => `Revoke ${m[0]} (-${m[1]})`,
give: (m) => `Give ${m[0]} (+${m[1]})`,
revoke(m) {
const name = modeCharToName[m.symbol];
const res = name ? `Revoke ${name} (-${m.mode})` : `Mode -${m.mode}`;
return res;
},
give(m) {
const name = modeCharToName[m.symbol];
const res = name ? `Give ${name} (+${m.mode})` : `Mode +${m.mode}`;
return res;
},
};
const networkModes = network.serverOptions.PREFIX;
const networkModeSymbols = network.serverOptions.PREFIX.symbols;
/**
* Determine whether the prefix of mode p1 has access to perform actions on p2.
@ -284,38 +292,38 @@ export function generateUserContextMenu($root, channel, network, user) {
function compare(p1, p2) {
// The modes ~ and @ can perform actions on their own mode. The others on modes below.
return "~@".indexOf(p1) > -1
? networkModes.indexOf(p1) <= networkModes.indexOf(p2)
: networkModes.indexOf(p1) < networkModes.indexOf(p2);
? networkModeSymbols.indexOf(p1) <= networkModeSymbols.indexOf(p2)
: networkModeSymbols.indexOf(p1) < networkModeSymbols.indexOf(p2);
}
networkModes.forEach((prefix) => {
if (!compare(currentChannelUser.modes[0], prefix)) {
network.serverOptions.PREFIX.prefix.forEach((mode) => {
if (!compare(currentChannelUser.modes[0], mode.symbol)) {
// Our highest mode is below the current mode. Bail.
return;
}
if (!user.modes.includes(prefix)) {
if (!user.modes.includes(mode.symbol)) {
// The target doesn't already have this mode, therefore we can set it.
items.push({
label: modeTextTemplate.give(modes[prefix]),
label: modeTextTemplate.give(mode),
type: "item",
class: "action-set-mode",
action() {
socket.emit("input", {
target: channel.id,
text: "/mode +" + modes[prefix][1] + " " + user.nick,
text: "/mode +" + mode.mode + " " + user.nick,
});
},
});
} else {
items.push({
label: modeTextTemplate.revoke(modes[prefix]),
label: modeTextTemplate.revoke(mode),
type: "item",
class: "action-revoke-mode",
action() {
socket.emit("input", {
target: channel.id,
text: "/mode -" + modes[prefix][1] + " " + user.nick,
text: "/mode -" + mode.mode + " " + user.nick,
});
},
});
@ -323,9 +331,9 @@ export function generateUserContextMenu($root, channel, network, user) {
});
// Determine if we are half-op or op depending on the network modes so we can kick.
if (!compare(networkModes.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) {
if (!compare(networkModeSymbols.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) {
// Check if the target user has no mode or a mode lower than ours.
if (user.modes.length === 0 || compare(currentChannelUser.modes[0], user.modes[0])) {
// Check if the target user has no mode or a mode lower than ours.
items.push({
label: "Kick",
type: "item",

View File

@ -79,7 +79,7 @@ function parse(createElement, text, message = undefined, network = undefined) {
// arrays of objects containing start and end markers, as well as metadata
// depending on what was found (channel or link).
const channelPrefixes = network ? network.serverOptions.CHANTYPES : ["#", "&"];
const userModes = network ? network.serverOptions.PREFIX : ["!", "@", "%", "+"];
const userModes = network?.serverOptions?.PREFIX.symbols || ["!", "@", "%", "+"];
const channelParts = findChannels(cleanText, channelPrefixes, userModes);
const linkParts = findLinks(cleanText);
const emojiParts = findEmoji(cleanText);

View File

@ -5,6 +5,7 @@ const {v4: uuidv4} = require("uuid");
const IrcFramework = require("irc-framework");
const Chan = require("./chan");
const Msg = require("./msg");
const Prefix = require("./prefix");
const Helper = require("../helper");
const STSPolicies = require("../plugins/sts");
const ClientCertificate = require("../plugins/clientCertificate");
@ -43,7 +44,12 @@ function Network(attr) {
irc: null,
serverOptions: {
CHANTYPES: ["#", "&"],
PREFIX: ["!", "@", "%", "+"],
PREFIX: new Prefix([
{symbol: "!", mode: "Y"},
{symbol: "@", mode: "o"},
{symbol: "%", mode: "h"},
{symbol: "+", mode: "v"},
]),
NETWORK: "",
},

33
src/models/prefix.js Normal file
View File

@ -0,0 +1,33 @@
"use strict";
class Prefix {
constructor(prefix) {
this.prefix = prefix || []; // [{symbol: "@", mode: "o"}, ... ]
this.modeToSymbol = {};
this.symbols = [];
this._update_internals();
}
_update_internals() {
// clean out the old cruft
this.modeToSymbol = {};
this.symbols = [];
const that = this;
this.prefix.forEach(function (p) {
that.modeToSymbol[p.mode] = p.symbol;
that.symbols.push(p.symbol);
});
}
update(prefix) {
this.prefix = prefix || [];
this._update_internals();
}
forEach(f) {
return this.prefix.forEach(f);
}
}
module.exports = Prefix;

View File

@ -4,7 +4,7 @@ const _ = require("lodash");
module.exports = User;
function User(attr, prefixLookup) {
function User(attr, prefix) {
_.defaults(this, attr, {
modes: [],
away: "",
@ -18,12 +18,12 @@ function User(attr, prefixLookup) {
},
});
this.setModes(this.modes, prefixLookup);
this.setModes(this.modes, prefix);
}
User.prototype.setModes = function (modes, prefixLookup) {
User.prototype.setModes = function (modes, prefix) {
// irc-framework sets character mode, but The Lounge works with symbols
this.modes = modes.map((mode) => prefixLookup[mode]);
this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
};
User.prototype.toJSON = function () {

View File

@ -63,10 +63,9 @@ module.exports = function (irc, network) {
});
irc.on("socket connected", function () {
network.prefixLookup = {};
irc.network.options.PREFIX.forEach(function (mode) {
network.prefixLookup[mode.mode] = mode.symbol;
});
if (irc.network.options.PREFIX) {
network.serverOptions.PREFIX.update(irc.network.options.PREFIX);
}
network.channels[0].pushMessage(
client,
@ -197,20 +196,12 @@ module.exports = function (irc, network) {
});
irc.on("server options", function (data) {
network.prefixLookup = {};
data.options.PREFIX.forEach((mode) => {
network.prefixLookup[mode.mode] = mode.symbol;
});
network.serverOptions.PREFIX.update(data.options.PREFIX);
if (data.options.CHANTYPES) {
network.serverOptions.CHANTYPES = data.options.CHANTYPES;
}
if (network.serverOptions.PREFIX) {
network.serverOptions.PREFIX = data.options.PREFIX.map((p) => p.symbol);
}
network.serverOptions.NETWORK = data.options.NETWORK;
client.emit("network:options", {

View File

@ -107,7 +107,7 @@ module.exports = function (irc, network) {
return;
}
const changedMode = network.prefixLookup[char];
const changedMode = network.serverOptions.PREFIX.modeToSymbol[char];
if (!add) {
_.pull(user.modes, changedMode);

View File

@ -14,7 +14,7 @@ module.exports = function (irc, network) {
data.users.forEach((user) => {
const newUser = chan.getUser(user.nick);
newUser.setModes(user.modes, network.prefixLookup);
newUser.setModes(user.modes, network.serverOptions.PREFIX);
newUsers.set(user.nick.toLowerCase(), newUser);
});

View File

@ -20,10 +20,10 @@ describe("Chan", function () {
},
};
const prefixLookup = {};
const prefixLookup = {modeToSymbol: {}};
network.network.options.PREFIX.forEach((mode) => {
prefixLookup[mode.mode] = mode.symbol;
prefixLookup.modeToSymbol[mode.mode] = mode.symbol;
});
describe("#findMessage(id)", function () {

View File

@ -8,7 +8,7 @@ const User = require("../../src/models/user");
describe("Msg", function () {
["from", "target"].forEach((prop) => {
it(`should keep a copy of the original user in the \`${prop}\` property`, function () {
const prefixLookup = {a: "&", o: "@"};
const prefixLookup = {modeToSymbol: {a: "&", o: "@"}};
const user = new User(
{
modes: ["o"],