From 468427bfdb59c533dc9f0b5d4fff26e2727c4893 Mon Sep 17 00:00:00 2001 From: Alexandre Oliveira Date: Sun, 11 Mar 2018 15:17:57 -0300 Subject: [PATCH] Add support for /ignore, /unignore and /ignorelist commands --- client/css/style.css | 11 ++- client/js/constants.js | 3 + client/js/contextMenuFactory.js | 18 ++++ client/js/socket-events/msg.js | 2 +- client/views/actions/ignore_list.tpl | 16 ++++ client/views/windows/help.tpl | 31 +++++++ src/client.js | 1 + src/helper.js | 42 ++++++++++ src/models/msg.js | 1 + src/models/network.js | 3 + src/plugins/inputs/ignore.js | 120 +++++++++++++++++++++++++++ src/plugins/irc-events/message.js | 10 +++ test/models/network.js | 1 + test/tests/hostmask.js | 62 ++++++++++++++ 14 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 client/views/actions/ignore_list.tpl create mode 100644 src/plugins/inputs/ignore.js create mode 100644 test/tests/hostmask.js diff --git a/client/css/style.css b/client/css/style.css index 88501852..6a14aaac 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1248,15 +1248,18 @@ kbd { } #chat table.channel-list, -#chat table.ban-list { +#chat table.ban-list, +#chat table.ignore-list { margin: 5px 10px; width: calc(100% - 30px); } #chat table.channel-list th, #chat table.ban-list th, +#chat table.ignore-list th, #chat table.channel-list td, -#chat table.ban-list td { +#chat table.ban-list td, +#chat.table.ignore-list td { padding: 5px; vertical-align: top; border-bottom: #eee 1px solid; @@ -1270,7 +1273,9 @@ kbd { #chat table.channel-list .topic, #chat table.ban-list .hostmask, #chat table.ban-list .banned_by, -#chat table.ban-list .banned_at { +#chat table.ban-list .banned_at, +#chat table.ignore-list .hostmask, +#chat table.ignore-list .when { text-align: left; } diff --git a/client/js/constants.js b/client/js/constants.js index 05ff6b3d..e7e1ae84 100644 --- a/client/js/constants.js +++ b/client/js/constants.js @@ -38,6 +38,8 @@ const commands = [ "/expand", "/ho", "/hs", + "/ignore", + "/ignorelist", "/invite", "/join", "/kick", @@ -64,6 +66,7 @@ const commands = [ "/slap", "/topic", "/unban", + "/unignore", "/voice", "/whois", ]; diff --git a/client/js/contextMenuFactory.js b/client/js/contextMenuFactory.js index 526e470a..4ee5fa16 100644 --- a/client/js/contextMenuFactory.js +++ b/client/js/contextMenuFactory.js @@ -293,6 +293,23 @@ function addJoinItem() { }); } +function addIgnoreListItem() { + function ignorelist(itemData) { + socket.emit("input", { + target: parseInt(itemData, 10), + text: "/ignorelist", + }); + } + + addContextMenuItem({ + check: (target) => target.hasClass("lobby"), + className: "list", + displayName: "List ignored users", + data: (target) => target.data("id"), + callback: ignorelist, + }); +} + function addDefaultItems() { addWhoisItem(); addQueryItem(); @@ -306,4 +323,5 @@ function addDefaultItems() { addChannelListItem(); addBanListItem(); addJoinItem(); + addIgnoreListItem(); } diff --git a/client/js/socket-events/msg.js b/client/js/socket-events/msg.js index d24c068d..f96e139d 100644 --- a/client/js/socket-events/msg.js +++ b/client/js/socket-events/msg.js @@ -52,7 +52,7 @@ function processReceivedMessage(data) { const container = channel.find(".messages"); const activeChannelId = chat.find(".chan.active").data("id"); - if (data.msg.type === "channel_list" || data.msg.type === "ban_list") { + if (data.msg.type === "channel_list" || data.msg.type === "ban_list" || data.msg.type === "ignore_list") { $(container).empty(); } diff --git a/client/views/actions/ignore_list.tpl b/client/views/actions/ignore_list.tpl new file mode 100644 index 00000000..88e0e70b --- /dev/null +++ b/client/views/actions/ignore_list.tpl @@ -0,0 +1,16 @@ + + + + + + + + + {{#each ignored}} + + + + + {{/each}} + +
HostmaskIgnored At
{{hostmask}}{{{localetime when}}}
diff --git a/client/views/windows/help.tpl b/client/views/windows/help.tpl index 06ab6a84..20cdbdcb 100644 --- a/client/views/windows/help.tpl +++ b/client/views/windows/help.tpl @@ -360,6 +360,26 @@ +
+
+ /ignore nick +
+
+

+ Block any messages from the specified user on the current network. + This can be a nickname or a hostmask.

+
+
+ +
+
+ /ignorelist +
+
+

Load the list of ignored users for the current network.

+
+
+
/join channel @@ -537,6 +557,17 @@
+
+
+ /unignore nick +
+
+

+ Unblock messages from the specified user on the current network. + This can be a nickname or a hostmask.

+
+
+
/voice nick [...nick] diff --git a/src/client.js b/src/client.js index 0aa1355b..e1b5c456 100644 --- a/src/client.js +++ b/src/client.js @@ -48,6 +48,7 @@ const inputs = [ "away", "connect", "disconnect", + "ignore", "invite", "kick", "mode", diff --git a/src/helper.js b/src/helper.js index 2fe864aa..a06267a4 100644 --- a/src/helper.js +++ b/src/helper.js @@ -33,6 +33,8 @@ const Helper = { ip2hex, mergeConfig, getDefaultNick, + parseHostmask, + compareHostmask, password: { hash: passwordHash, @@ -226,3 +228,43 @@ function mergeConfig(oldConfig, newConfig) { } }); } + +function parseHostmask(hostmask) { + let nick = ""; + let ident = "*"; + let hostname = "*"; + let parts = []; + + // Parse hostname first, then parse the rest + parts = hostmask.split("@"); + + if (parts.length >= 2) { + hostname = parts[1] || "*"; + hostmask = parts[0]; + } + + hostname = hostname.toLowerCase(); + + parts = hostmask.split("!"); + + if (parts.length >= 2) { + ident = parts[1] || "*"; + hostmask = parts[0]; + } + + ident = ident.toLowerCase(); + + nick = hostmask.toLowerCase() || "*"; + + const result = { + nick: nick, + ident: ident, + hostname: hostname, + }; + + return result; +} + +function compareHostmask(a, b) { + return (a.nick.toLowerCase() === b.nick.toLowerCase() || a.nick === "*") && (a.ident.toLowerCase() === b.ident.toLowerCase() || a.ident === "*") && (a.hostname.toLowerCase() === b.hostname.toLowerCase() || a.hostname === "*"); +} diff --git a/src/models/msg.js b/src/models/msg.js index 2d292add..ed87240b 100644 --- a/src/models/msg.js +++ b/src/models/msg.js @@ -67,6 +67,7 @@ Msg.Type = { TOPIC_SET_BY: "topic_set_by", WHOIS: "whois", BANLIST: "ban_list", + IGNORELIST: "ignore_list", }; module.exports = Msg; diff --git a/src/models/network.js b/src/models/network.js index 22574885..0698c113 100644 --- a/src/models/network.js +++ b/src/models/network.js @@ -18,6 +18,7 @@ const filteredFromClient = { highlightRegex: true, irc: true, password: true, + ignoreList: true, }; function Network(attr) { @@ -41,6 +42,7 @@ function Network(attr) { NETWORK: "", }, chanCache: [], + ignoreList: [], }); if (!this.uuid) { @@ -325,6 +327,7 @@ Network.prototype.export = function() { "commands", "ip", "hostname", + "ignoreList", ]); network.channels = this.channels diff --git a/src/plugins/inputs/ignore.js b/src/plugins/inputs/ignore.js new file mode 100644 index 00000000..4b999570 --- /dev/null +++ b/src/plugins/inputs/ignore.js @@ -0,0 +1,120 @@ +"use strict"; + +const Chan = require("../../models/chan"); +const Msg = require("../../models/msg"); +const Helper = require("../../helper"); + +exports.commands = [ + "ignore", + "unignore", + "ignorelist", +]; + +exports.input = function(network, chan, cmd, args) { + const client = this; + let target; + let hostmask; + + if (cmd !== "ignorelist" && (args.length === 0 || args[0].trim().length === 0)) { + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: `Usage: /${cmd} [!ident][@host]`, + })); + + return; + } + + if (cmd !== "ignorelist") { + // Trim to remove any spaces from the hostmask + target = args[0].trim(); + hostmask = Helper.parseHostmask(target); + } + + switch (cmd) { + case "ignore": { + // IRC nicks are case insensitive + if (hostmask.nick.toLowerCase() === network.nick.toLowerCase()) { + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: "You can't ignore yourself", + })); + } else if (!network.ignoreList.some(function(entry) { + return Helper.compareHostmask(entry, hostmask); + })) { + hostmask.when = Date.now(); + network.ignoreList.push(hostmask); + + client.save(); + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: `\u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f added to ignorelist`, + })); + } else { + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: "The specified user/hostmask is already ignored", + })); + } + + break; + } + + case "unignore": { + const idx = network.ignoreList.findIndex(function(entry) { + return Helper.compareHostmask(entry, hostmask); + }); + + // Check if the entry exists before removing it, otherwise + // let the user know. + if (idx !== -1) { + network.ignoreList.splice(idx, 1); + client.save(); + + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: `Successfully removed \u0002${hostmask.nick}!${hostmask.ident}@${hostmask.hostname}\u000f from ignorelist`, + })); + } else { + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: "The specified user/hostmask is not ignored", + })); + } + + break; + } + + case "ignorelist": + if (network.ignoreList.length === 0) { + chan.pushMessage(client, new Msg({ + type: Msg.Type.ERROR, + text: "Ignorelist is empty", + })); + } else { + const chanName = "Ignored users"; + let newChan = network.getChannel(chanName); + + if (typeof newChan === "undefined") { + newChan = client.createChannel({ + type: Chan.Type.SPECIAL, + name: chanName, + }); + client.emit("join", { + network: network.uuid, + chan: newChan.getFilteredClone(true), + index: network.addChannel(newChan), + }); + } + + newChan.pushMessage(client, new Msg({ + type: Msg.Type.IGNORELIST, + ignored: network.ignoreList.map((data) => ({ + hostmask: `${data.nick}!${data.ident}@${data.hostname}`, + when: data.when, + })), + }), true); + } + + break; + } +}; diff --git a/src/plugins/irc-events/message.js b/src/plugins/irc-events/message.js index a6e523ff..c6714258 100644 --- a/src/plugins/irc-events/message.js +++ b/src/plugins/irc-events/message.js @@ -4,6 +4,7 @@ const Chan = require("../../models/chan"); const Msg = require("../../models/msg"); const LinkPrefetch = require("./link"); const cleanIrcMessage = require("../../../client/js/libs/handlebars/ircmessageparser/cleanIrcMessage"); +const Helper = require("../../helper"); const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g; module.exports = function(irc, network) { @@ -43,11 +44,20 @@ module.exports = function(irc, network) { let showInActive = false; const self = data.nick === irc.user.nick; + // Check if the sender is in our ignore list + const shouldIgnore = network.ignoreList.some(function(entry) { + return Helper.compareHostmask(entry, data); + }); + // Server messages go to server window, no questions asked if (data.from_server) { chan = network.channels[0]; from = chan.getUser(data.nick); } else { + if (shouldIgnore) { + return; + } + let target = data.target; // If the message is targeted at us, use sender as target instead diff --git a/test/models/network.js b/test/models/network.js index 7fe7f374..b0e3da8d 100644 --- a/test/models/network.js +++ b/test/models/network.js @@ -47,6 +47,7 @@ describe("Network", function() { {name: "&secure", key: "bar"}, {name: "PrivateChat", type: "query"}, ], + ignoreList: [], }); }); diff --git a/test/tests/hostmask.js b/test/tests/hostmask.js new file mode 100644 index 00000000..2c215643 --- /dev/null +++ b/test/tests/hostmask.js @@ -0,0 +1,62 @@ +"use strict"; + +const expect = require("chai").expect; +const Helper = require("../../src/helper"); + +describe("Hostmask", function() { + it(".parseHostmask", function() { + expect(Helper.parseHostmask("nick").nick).to.equal("nick"); + expect(Helper.parseHostmask("nick").ident).to.equal("*"); + expect(Helper.parseHostmask("nick").hostname).to.equal("*"); + + expect(Helper.parseHostmask("!user").nick).to.equal("*"); + expect(Helper.parseHostmask("!user").ident).to.equal("user"); + expect(Helper.parseHostmask("!user").hostname).to.equal("*"); + + expect(Helper.parseHostmask("@host").nick).to.equal("*"); + expect(Helper.parseHostmask("@host").ident).to.equal("*"); + expect(Helper.parseHostmask("@host").hostname).to.equal("host"); + + expect(Helper.parseHostmask("!").nick).to.equal("*"); + expect(Helper.parseHostmask("!").ident).to.equal("*"); + expect(Helper.parseHostmask("!").hostname).to.equal("*"); + + expect(Helper.parseHostmask("@").nick).to.equal("*"); + expect(Helper.parseHostmask("@").ident).to.equal("*"); + expect(Helper.parseHostmask("@").hostname).to.equal("*"); + + expect(Helper.parseHostmask("!@").nick).to.equal("*"); + expect(Helper.parseHostmask("!@").ident).to.equal("*"); + expect(Helper.parseHostmask("!@").hostname).to.equal("*"); + + expect(Helper.parseHostmask("nick!user@host").nick).to.equal("nick"); + expect(Helper.parseHostmask("nick!user@host").ident).to.equal("user"); + expect(Helper.parseHostmask("nick!user@host").hostname).to.equal("host"); + + expect(Helper.parseHostmask("nick!!!!@thing@@host").nick).to.equal("nick"); + expect(Helper.parseHostmask("nick!!!!@thing@@host").ident).to.equal("*"); + expect(Helper.parseHostmask("nick!!!!@thing@@host").hostname).to.equal("thing"); + + expect(Helper.parseHostmask("!!!!@thing@@host").nick).to.equal("*"); + expect(Helper.parseHostmask("!!!!@thing@@host").ident).to.equal("*"); + expect(Helper.parseHostmask("!!!!@thing@@host").hostname).to.equal("thing"); + + expect(Helper.parseHostmask("NiCK!uSEr@HOST").nick).to.equal("nick"); + expect(Helper.parseHostmask("NiCK!uSEr@HOST").ident).to.equal("user"); + expect(Helper.parseHostmask("NiCK!uSEr@HOST").hostname).to.equal("host"); + }); + + it(".compareHostmask (wildcard)", function() { + const a = Helper.parseHostmask("nick!user@host"); + const b = Helper.parseHostmask("nick!*@*"); + expect(Helper.compareHostmask(b, a)).to.be.true; + expect(Helper.compareHostmask(a, b)).to.be.false; + }); + + it(".compareHostmask", function() { + const a = Helper.parseHostmask("nick!user@host"); + const b = Helper.parseHostmask("NiCK!useR@HOST"); + expect(Helper.compareHostmask(b, a)).to.be.true; + expect(Helper.compareHostmask(a, b)).to.be.true; + }); +});