From 2f59cbe2ef183c11de25506307e0e45c10f7e90c Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Fri, 24 Feb 2017 16:43:37 +0100 Subject: [PATCH 01/12] Implemented webhooks with matching avatars --- README.md | 6 ++++- lib/bot.js | 34 ++++++++++++++++++++++++++- package.json | 2 +- test/bot.test.js | 9 +++++++ test/fixtures/single-test-config.json | 3 +++ 5 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ae27930e..4caf705d 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,11 @@ First you need to create a Discord bot user, which you can do by following the i "ircNickColor": false, // Gives usernames a color in IRC for better readability (on by default) // Makes the bot hide the username prefix for messages that start // with one of these characters (commands): - "commandCharacters": ["!", "."] + "commandCharacters": ["!", "."], + // List of webhooks per channel + "webhooks": { + "#discord": "https://discordapp.com/api/webhooks/id/token" + } } ] ``` diff --git a/lib/bot.js b/lib/bot.js index c943d674..eb7aeab0 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -34,12 +34,19 @@ class Bot { this.channels = _.values(options.channelMapping); this.channelMapping = {}; + this.webhooks = {}; // Remove channel passwords from the mapping and lowercase IRC channel names _.forOwn(options.channelMapping, (ircChan, discordChan) => { this.channelMapping[discordChan] = ircChan.split(' ')[0].toLowerCase(); }); + // Extract id and token from Webhook urls + _.forOwn(options.webhooks, (url, chan) => { + const [id, token] = url.split('/').slice(-2); + this.webhooks[chan] = { id, token }; + }); + this.invertedMapping = _.invert(this.channelMapping); this.autoSendCommands = options.autoSendCommands || []; } @@ -47,6 +54,9 @@ class Bot { connect() { logger.debug('Connecting to IRC and Discord'); this.discord.login(this.discordToken); + _.forOwn(this.webhooks, (webhook, channel) => { + this.webhooks[channel].client = new discord.WebhookClient(webhook.id, webhook.token); + }); const ircOptions = { userName: this.nickname, @@ -151,7 +161,9 @@ class Bot { sendToIRC(message) { const author = message.author; // Ignore messages sent by the bot itself: - if (author.id === this.discord.user.id) return; + if (author.id === this.discord.user.id || + Object.keys(this.webhooks).some(chan => this.webhooks[chan].id === author.id) + ) return; const channelName = `#${message.channel.name}`; const ircChannel = this.channelMapping[channelName]; @@ -189,6 +201,16 @@ class Bot { } } + getDiscordAvatar(nick) { + const user = this.discord.guilds.first().members.find( + member => member.user.username.toLowerCase() === nick.toLowerCase() + ); + if (user) { + return user.user.avatarURL; + } + return null; + } + sendToDiscord(author, channel, text) { const discordChannelName = this.invertedMapping[channel.toLowerCase()]; if (discordChannelName) { @@ -202,6 +224,16 @@ class Bot { discordChannelName); return; } + if (this.webhooks[discordChannelName]) { + this.webhooks[discordChannelName].client.sendMessage( + text, + { + username: author, + text, + avatarURL: this.getDiscordAvatar(author) + }).catch(logger.error); + return; + } const withMentions = text.replace(/@[^\s]+\b/g, (match) => { const search = match.substring(1); diff --git a/package.json b/package.json index 62a295b3..d9ef3c73 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "check-env": "1.2.0", "commander": "2.9.0", - "discord.js": "11.0.0", + "discord.js": "git://github.com/hydrabolt/discord.js.git", "irc": "0.5.2", "lodash": "^4.17.4", "strip-json-comments": "2.0.1", diff --git a/test/bot.test.js b/test/bot.test.js index 4162b7c1..190be708 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -413,4 +413,13 @@ describe('Bot', function () { this.bot.sendToDiscord(username, '#irc', text); this.sendMessageStub.should.have.been.calledWith(expected); }); + + it('should create webhooks clients for each webhook url in the config', function () { + this.bot.webhooks.should.have.property('#irc'); + }); + + it('should extract id and token from webhook urls', function () { + this.bot.webhooks['#irc'].id.should.equal('id'); + this.bot.webhooks['#irc'].token.should.equal('token'); + }); }); diff --git a/test/fixtures/single-test-config.json b/test/fixtures/single-test-config.json index fa09687a..691e105b 100644 --- a/test/fixtures/single-test-config.json +++ b/test/fixtures/single-test-config.json @@ -10,5 +10,8 @@ "channelMapping": { "#discord": "#irc channelKey", "#notinchannel": "#otherIRC" + }, + "webhooks": { + "#irc": "https://discordapp.com/api/webhooks/id/token" } } From 3ca43ae0f4e6c62d89341896ef20570082c10979 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Tue, 2 May 2017 12:03:26 +0200 Subject: [PATCH 02/12] use discord.js 11.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9ef3c73..b132cdb4 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "check-env": "1.2.0", "commander": "2.9.0", - "discord.js": "git://github.com/hydrabolt/discord.js.git", + "discord.js": "11.1.0", "irc": "0.5.2", "lodash": "^4.17.4", "strip-json-comments": "2.0.1", From 2c05038c373df3704e5c3748d99ba51459a343e9 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Tue, 2 May 2017 12:56:31 +0200 Subject: [PATCH 03/12] Reworked webhook flow IRC formatting --- lib/bot.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index a4c04d7e..f6a6fd0a 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -322,6 +322,8 @@ class Bot { .find('name', discordChannelName.slice(1)) : this.discord.channels.get(discordChannelName); if (!discordChannel) { + logger.info('Tried to send a message to a channel the bot isn\'t in: ', + discordChannelName); return null; } return discordChannel; @@ -337,13 +339,15 @@ class Bot { return webhook; } } - logger.info('Tried to send a message to a channel the bot isn\'t in or without webhook attached: ', - discordChannelName); return null; } sendToDiscord(author, channel, text) { const discordChannel = this.findDiscordChannel(channel); + if (!discordChannel) { + return; + } + // Convert text formatting (bold, italics, underscore) const withFormat = formatFromIRCToDiscord(text); @@ -369,6 +373,20 @@ class Bot { return match; }); + // Webhooks first + const webhook = this.findWebhook(channel); + if (webhook) { + logger.debug('Sending message to Discord via webhook', withMentions, channel, '->', `#${discordChannel.name}`); + webhook.client.sendMessage( + withMentions, + { + username: author, + text, + avatarURL: this.getDiscordAvatar(author) + }).catch(logger.error); + return; + } + const patternMap = { author, text: withFormat, @@ -381,19 +399,6 @@ class Bot { // Use custom formatting from config / default formatting with bold author const withAuthor = Bot.substitutePattern(this.formatDiscord, patternMap); logger.debug('Sending message to Discord', withAuthor, channel, '->', `#${discordChannel.name}`); - if (!discordChannel) { - const webhook = this.findWebhook(channel); - if (webhook) { - webhook.client.sendMessage( - withAuthor, - { - username: author, - text, - avatarURL: this.getDiscordAvatar(author) - }).catch(logger.error); - } - return; - } discordChannel.sendMessage(withAuthor); } From b4452dd4874e99d4ef60935cd9b9d32e0b5b7998 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Tue, 2 May 2017 13:06:21 +0200 Subject: [PATCH 04/12] remove useless brackets --- lib/bot.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index f6a6fd0a..b0867b78 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -344,10 +344,7 @@ class Bot { sendToDiscord(author, channel, text) { const discordChannel = this.findDiscordChannel(channel); - if (!discordChannel) { - return; - } - + if (!discordChannel) return; // Convert text formatting (bold, italics, underscore) const withFormat = formatFromIRCToDiscord(text); From 2bf761b16318e37c90b87cde10ca516b3a6181c5 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Tue, 30 May 2017 14:37:57 +0200 Subject: [PATCH 05/12] getDiscordAvatar refactoring Added test --- lib/bot.js | 6 ++---- test/bot.test.js | 27 ++++++++++++++++++++++++--- test/fixtures/single-test-config.json | 5 +++-- test/stubs/discord-stub.js | 5 +++++ test/stubs/webhook-stub.js | 14 ++++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 test/stubs/webhook-stub.js diff --git a/lib/bot.js b/lib/bot.js index 8a1e1a53..35a6e743 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -323,11 +323,9 @@ class Bot { } getDiscordAvatar(nick) { - const user = this.discord.guilds.first().members.find( - member => member.user.username.toLowerCase() === nick.toLowerCase() - ); + const user = this.discord.users.find('username', nick); if (user) { - return user.user.avatarURL; + return user.avatarURL; } return null; } diff --git a/test/bot.test.js b/test/bot.test.js index 3a81c0ad..7a62f759 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -8,6 +8,7 @@ import discord from 'discord.js'; import Bot from '../lib/bot'; import createDiscordStub from './stubs/discord-stub'; import ClientStub from './stubs/irc-client-stub'; +import createWebhookStub from './stubs/webhook-stub'; import config from './fixtures/single-test-config.json'; import configMsgFormatDefault from './fixtures/msg-formats-default.json'; @@ -32,6 +33,8 @@ describe('Bot', function () { ClientStub.prototype.say = sandbox.stub(); ClientStub.prototype.send = sandbox.stub(); ClientStub.prototype.join = sandbox.stub(); + this.sendWebhookMessageStub = sandbox.stub(); + discord.WebhookClient = createWebhookStub(this.sendWebhookMessageStub); this.bot = new Bot(config); this.bot.connect(); }); @@ -793,11 +796,29 @@ describe('Bot', function () { }); it('should create webhooks clients for each webhook url in the config', function () { - this.bot.webhooks.should.have.property('#irc'); + this.bot.webhooks.should.have.property('#withwebhook'); }); it('should extract id and token from webhook urls', function () { - this.bot.webhooks['#irc'].id.should.equal('id'); - this.bot.webhooks['#irc'].token.should.equal('token'); + this.bot.webhooks['#withwebhook'].id.should.equal('id'); + this.bot.webhooks['#withwebhook'].token.should.equal('token'); + }); + + it('should find the matching webhook when it exists', function () { + this.bot.findWebhook('#ircwebhook').should.not.equal(null); + }); + + it('should prefer webhooks to send a message when possible', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + bot.sendToDiscord('nick', '#irc', 'text'); + this.sendWebhookMessageStub.should.have.been.called; + }); + + it('should find the user\'s avatar when the irc nick is also a discord username', function () { + const testUser = new discord.User(this.bot.discord, { username: 'avatarUser', id: '123', avatar: '123' }); + this.findUserStub.withArgs('username', testUser.username).returns(testUser); + this.bot.getDiscordAvatar('avatarUser').should.not.equal(null); }); }); diff --git a/test/fixtures/single-test-config.json b/test/fixtures/single-test-config.json index 5495cd00..9889d881 100644 --- a/test/fixtures/single-test-config.json +++ b/test/fixtures/single-test-config.json @@ -10,9 +10,10 @@ "channelMapping": { "#discord": "#irc channelKey", "#notinchannel": "#otherIRC", - "1234": "#channelforid" + "1234": "#channelforid", + "#withwebhook": "#ircwebhook" }, "webhooks": { - "#irc": "https://discordapp.com/api/webhooks/id/token" + "#withwebhook": "https://discordapp.com/api/webhooks/id/token" } } diff --git a/test/stubs/discord-stub.js b/test/stubs/discord-stub.js index f20ec6a8..38161273 100644 --- a/test/stubs/discord-stub.js +++ b/test/stubs/discord-stub.js @@ -19,6 +19,11 @@ export default function createDiscordStub(sendMessageStub, findUserStub, findRol this.users = { find: findUserStub }; + this.options = { + http: { + host: 'host' + } + }; } getChannel(key, value) { diff --git a/test/stubs/webhook-stub.js b/test/stubs/webhook-stub.js new file mode 100644 index 00000000..0be1be94 --- /dev/null +++ b/test/stubs/webhook-stub.js @@ -0,0 +1,14 @@ +/* eslint-disable class-methods-use-this */ +export default function createWebhookStub(sendWebhookMessage) { + return class WebhookStub { + constructor(id, token) { + this.id = id; + this.token = token; + } + + sendMessage() { + sendWebhookMessage(); + return new Promise(() => {}); + } + }; +} From 8c0c11a2b1a5ed532de2a27b50d4ffb340a93160 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Tue, 30 May 2017 16:11:59 +0200 Subject: [PATCH 06/12] Added webhooks section to readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f47ca2c8..91ff3c66 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,15 @@ The `ircOptions` object is passed directly to node-irc ([available options](http To retrieve a discord channel ID, write `\#channel` on the relevant server – it should produce something of the form `<#1234567890>`, which you can then use in the `channelMapping` config. +### Webhooks +Webhooks allow nickname and avatar override, so messages coming from IRC will appear almost as regular Discord messages. + +See [here (part 1 only)](https://support.discordapp.com/hc/en-us/articles/228383668-Intro-to-Webhooks) to create a webhook for a channel. + +Example result: + +![discord-webhook](http://i.imgur.com/lNeJIUI.jpg) + ## Tests Run the tests with: ```bash From 34cdd373afc7b39e002cf2320561ca00bcef3bd4 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Fri, 28 Jul 2017 11:19:25 +0200 Subject: [PATCH 07/12] Avatar matching is now more accurate and case insensitive --- lib/bot.js | 39 ++++++++++++++++++++++++++++---------- test/bot.test.js | 12 +++++++----- test/stubs/discord-stub.js | 3 ++- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index 8edd0fd7..97358a6c 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -270,7 +270,7 @@ class Bot { // Ignore messages sent by the bot itself: if (author.id === this.discord.user.id || Object.keys(this.webhooks).some(chan => this.webhooks[chan].id === author.id) - ) return; + ) return; const channelName = `#${message.channel.name}`; const ircChannel = this.channelMapping[message.channel.id] || @@ -329,14 +329,6 @@ class Bot { } } - getDiscordAvatar(nick) { - const user = this.discord.users.find('username', nick); - if (user) { - return user.avatarURL; - } - return null; - } - findDiscordChannel(ircChannel) { const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()]; if (discordChannelName) { @@ -366,6 +358,33 @@ class Bot { return null; } + getDiscordAvatar(nick, channel) { + const guildMembers = this.findDiscordChannel(channel).guild.members; + const findByNicknameOrUsername = caseSensitive => + (value) => { + if (caseSensitive) { + return value.username === nick || value.nickname === nick; + } + const nickLowerCase = nick.toLowerCase(); + return value.username.toLowerCase() === nickLowerCase + || value.nickname.toLowerCase() === nickLowerCase; + }; + + // Try to find exact matching case + let users = guildMembers.filter(findByNicknameOrUsername(false)); + + // Now let's search case insensitive. + if (!users) { + users = guildMembers.filter(findByNicknameOrUsername(true)); + } + + // No matching user or more than one => no avatar + if (users && users.size === 1) { + return users.first().avatarURL; + } + return null; + } + sendToDiscord(author, channel, text) { const discordChannel = this.findDiscordChannel(channel); if (!discordChannel) return; @@ -430,7 +449,7 @@ class Bot { { username: author, text, - avatarURL: this.getDiscordAvatar(author) + avatarURL: this.getDiscordAvatar(author, channel) }).catch(logger.error); return; } diff --git a/test/bot.test.js b/test/bot.test.js index b9f08fd0..e321a9ed 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -846,7 +846,7 @@ describe('Bot', function () { this.sendMessageStub.getCall(0).args.should.deep.equal([msg]); }); - it('should create webhooks clients for each webhook url in the config', function () { + it('should create webhooks clients for each webhook url in the config', function () { this.bot.webhooks.should.have.property('#withwebhook'); }); @@ -867,9 +867,11 @@ describe('Bot', function () { this.sendWebhookMessageStub.should.have.been.called; }); - it('should find the user\'s avatar when the irc nick is also a discord username', function () { - const testUser = new discord.User(this.bot.discord, { username: 'avatarUser', id: '123', avatar: '123' }); - this.findUserStub.withArgs('username', testUser.username).returns(testUser); - this.bot.getDiscordAvatar('avatarUser').should.not.equal(null); + it('should search for the user\'s avatar when sending via webhook', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + bot.sendToDiscord('nick', '#irc', 'text'); + this.findUserStub.should.have.been.called; }); }); diff --git a/test/stubs/discord-stub.js b/test/stubs/discord-stub.js index 7290b839..bbbfc467 100644 --- a/test/stubs/discord-stub.js +++ b/test/stubs/discord-stub.js @@ -37,7 +37,8 @@ export default function createDiscordStub(sendMessageStub, findUserStub, findRol guild: { members: { find: findUserStub, - get: findUserStub + get: findUserStub, + filter: findUserStub }, roles: { find: findRoleStub, From 4facb56ebb5720b8f28cfab9d0d808bcf32339ff Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Fri, 28 Jul 2017 11:40:20 +0200 Subject: [PATCH 08/12] Updated avatar tests with new guild/members stub --- test/bot.test.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/test/bot.test.js b/test/bot.test.js index 8babdc8f..a7999002 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -832,11 +832,40 @@ describe('Bot', function () { this.sendWebhookMessageStub.should.have.been.called; }); - it('should search for the user\'s avatar when sending via webhook', function () { + it('should find a matching username, case sensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - bot.sendToDiscord('nick', '#irc', 'text'); - this.findUserStub.should.have.been.called; + bot.sendToDiscord('Nick', '#irc', 'text'); + this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); + this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + }); + + it('should find a matching username, case insensitive, when looking for an avatar', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + bot.sendToDiscord('Nick', '#irc', 'text'); + this.guild.members.set(1, { username: 'nick', nickname: 'Different', avatarURL: 'avatarURL' }); + this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + }); + + it('should find a matching nickname, case sensitive, when looking for an avatar', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + bot.sendToDiscord('Different', '#irc', 'text'); + this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); + this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + }); + + it('should not return an avatar with two matching usernames when looking for an avatar', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + bot.sendToDiscord('common', '#irc', 'text'); + this.guild.members.set(1, { username: 'common', nickname: 'Different', avatarURL: 'avatarURL' }); + this.guild.members.set(2, { username: 'Nick', nickname: 'common', avatarURL: 'avatarURL' }); + chai.should().equal(this.bot.getDiscordAvatar('common', '#irc'), null); }); }); From 8a23a9193159b6167185af892e8e25ddfeab38cf Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Fri, 28 Jul 2017 11:54:02 +0200 Subject: [PATCH 09/12] Fix avatar lookup function (and tests) --- lib/bot.js | 6 +++--- test/bot.test.js | 10 +++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index 35c1cc68..d6d87f83 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -371,11 +371,11 @@ class Bot { }; // Try to find exact matching case - let users = guildMembers.filter(findByNicknameOrUsername(false)); + let users = guildMembers.filter(findByNicknameOrUsername(true)); // Now let's search case insensitive. - if (!users) { - users = guildMembers.filter(findByNicknameOrUsername(true)); + if (users.size === 0) { + users = guildMembers.filter(findByNicknameOrUsername(false)); } // No matching user or more than one => no avatar diff --git a/test/bot.test.js b/test/bot.test.js index a7999002..984da78d 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -836,34 +836,30 @@ describe('Bot', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - bot.sendToDiscord('Nick', '#irc', 'text'); this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('avatarURL'); }); it('should find a matching username, case insensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - bot.sendToDiscord('Nick', '#irc', 'text'); this.guild.members.set(1, { username: 'nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('avatarURL'); }); it('should find a matching nickname, case sensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - bot.sendToDiscord('Different', '#irc', 'text'); this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('nick', '#irc').should.equal('avatarURL'); + this.bot.getDiscordAvatar('Different', '#irc').should.equal('avatarURL'); }); it('should not return an avatar with two matching usernames when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - bot.sendToDiscord('common', '#irc', 'text'); this.guild.members.set(1, { username: 'common', nickname: 'Different', avatarURL: 'avatarURL' }); this.guild.members.set(2, { username: 'Nick', nickname: 'common', avatarURL: 'avatarURL' }); chai.should().equal(this.bot.getDiscordAvatar('common', '#irc'), null); From e8c96e2a4477551659b8e9ef23da61726547b9ce Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Mon, 31 Jul 2017 10:59:58 +0200 Subject: [PATCH 10/12] Fixed username/nickname matching --- lib/bot.js | 10 +++++----- test/bot.test.js | 21 +++++++++++++-------- test/stubs/discord-stub.js | 5 +++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index d6d87f83..13886653 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -361,13 +361,13 @@ class Bot { getDiscordAvatar(nick, channel) { const guildMembers = this.findDiscordChannel(channel).guild.members; const findByNicknameOrUsername = caseSensitive => - (value) => { + (member) => { if (caseSensitive) { - return value.username === nick || value.nickname === nick; + return member.user.username === nick || member.nickname === nick; } const nickLowerCase = nick.toLowerCase(); - return value.username.toLowerCase() === nickLowerCase - || value.nickname.toLowerCase() === nickLowerCase; + return member.user.username.toLowerCase() === nickLowerCase + || member.nickname.toLowerCase() === nickLowerCase; }; // Try to find exact matching case @@ -380,7 +380,7 @@ class Bot { // No matching user or more than one => no avatar if (users && users.size === 1) { - return users.first().avatarURL; + return users.first().user.avatarURL; } return null; } diff --git a/test/bot.test.js b/test/bot.test.js index 984da78d..9da4d541 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -836,32 +836,37 @@ describe('Bot', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('Nick', '#irc').should.equal('avatarURL'); + const user = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); + this.guild.members.set(1, { user, nickname: 'Different' }); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); }); it('should find a matching username, case insensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - this.guild.members.set(1, { username: 'nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('Nick', '#irc').should.equal('avatarURL'); + const user = new discord.User(this.bot.discord, { username: 'nick', avatar: 'avatarURL' }); + this.guild.members.set(1, { user, nickname: 'Different' }); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); }); it('should find a matching nickname, case sensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - this.guild.members.set(1, { username: 'Nick', nickname: 'Different', avatarURL: 'avatarURL' }); - this.bot.getDiscordAvatar('Different', '#irc').should.equal('avatarURL'); + const user = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); + this.guild.members.set(1, { user, nickname: 'Different' }); + this.bot.getDiscordAvatar('Different', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); }); it('should not return an avatar with two matching usernames when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - this.guild.members.set(1, { username: 'common', nickname: 'Different', avatarURL: 'avatarURL' }); - this.guild.members.set(2, { username: 'Nick', nickname: 'common', avatarURL: 'avatarURL' }); + const user1 = new discord.User(this.bot.discord, { username: 'common', avatar: 'avatarURL' }); + const user2 = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); + this.guild.members.set(1, { user: user1, nickname: 'Different' }); + this.guild.members.set(2, { user: user2, nickname: 'common' }); chai.should().equal(this.bot.getDiscordAvatar('common', '#irc'), null); }); }); diff --git a/test/stubs/discord-stub.js b/test/stubs/discord-stub.js index 0fd78f10..b2164978 100644 --- a/test/stubs/discord-stub.js +++ b/test/stubs/discord-stub.js @@ -11,6 +11,11 @@ export default function createDiscordStub(sendStub, guild, discordUsers) { id: 'testid' }; this.channels = this.guildChannels(); + this.options = { + http: { + cdn: '' + } + }; this.users = discordUsers; } From ff788083d1cdaf7030aaf5ee9f89b2b442e1019e Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Mon, 31 Jul 2017 15:12:31 +0200 Subject: [PATCH 11/12] Better tests Preventing crash on empty nickname --- lib/bot.js | 2 +- test/bot.test.js | 44 +++++++++++++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index 13886653..233d9a75 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -367,7 +367,7 @@ class Bot { } const nickLowerCase = nick.toLowerCase(); return member.user.username.toLowerCase() === nickLowerCase - || member.nickname.toLowerCase() === nickLowerCase; + || (member.nickname && member.nickname.toLowerCase() === nickLowerCase); }; // Try to find exact matching case diff --git a/test/bot.test.js b/test/bot.test.js index 9da4d541..f7c53515 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -836,37 +836,55 @@ describe('Bot', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - const user = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); - this.guild.members.set(1, { user, nickname: 'Different' }); - this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); + const userObj = { id: 123, username: 'Nick', avatar: 'avatarURL' }; + const memberObj = { nickname: 'Different' }; + this.addUser(userObj, memberObj); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/123/avatarURL.png?size=2048'); }); it('should find a matching username, case insensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - const user = new discord.User(this.bot.discord, { username: 'nick', avatar: 'avatarURL' }); - this.guild.members.set(1, { user, nickname: 'Different' }); - this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); + const userObj = { id: 124, username: 'nick', avatar: 'avatarURL' }; + const memberObj = { nickname: 'Different' }; + this.addUser(userObj, memberObj); + this.bot.getDiscordAvatar('Nick', '#irc').should.equal('/avatars/124/avatarURL.png?size=2048'); }); it('should find a matching nickname, case sensitive, when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - const user = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); - this.guild.members.set(1, { user, nickname: 'Different' }); - this.bot.getDiscordAvatar('Different', '#irc').should.equal('/avatars/<@undefined>/avatarURL.png?size=2048'); + const userObj = { id: 125, username: 'Nick', avatar: 'avatarURL' }; + const memberObj = { nickname: 'Different' }; + this.addUser(userObj, memberObj); + this.bot.getDiscordAvatar('Different', '#irc').should.equal('/avatars/125/avatarURL.png?size=2048'); }); it('should not return an avatar with two matching usernames when looking for an avatar', function () { const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; const bot = new Bot(newConfig); bot.connect(); - const user1 = new discord.User(this.bot.discord, { username: 'common', avatar: 'avatarURL' }); - const user2 = new discord.User(this.bot.discord, { username: 'Nick', avatar: 'avatarURL' }); - this.guild.members.set(1, { user: user1, nickname: 'Different' }); - this.guild.members.set(2, { user: user2, nickname: 'common' }); + const userObj1 = { id: 126, username: 'common', avatar: 'avatarURL' }; + const userObj2 = { id: 127, username: 'Nick', avatar: 'avatarURL' }; + const memberObj1 = { nickname: 'Different' }; + const memberObj2 = { nickname: 'common' }; + this.addUser(userObj1, memberObj1); + this.addUser(userObj2, memberObj2); chai.should().equal(this.bot.getDiscordAvatar('common', '#irc'), null); }); + + it('should not return an avatar when no users match and should handle lack of nickname, when looking for an avatar', function () { + const newConfig = { ...config, webhooks: { '#discord': 'https://discordapp.com/api/webhooks/id/token' } }; + const bot = new Bot(newConfig); + bot.connect(); + const userObj1 = { id: 128, username: 'common', avatar: 'avatarURL' }; + const userObj2 = { id: 129, username: 'Nick', avatar: 'avatarURL' }; + const memberObj1 = {}; + const memberObj2 = { nickname: 'common' }; + this.addUser(userObj1, memberObj1); + this.addUser(userObj2, memberObj2); + chai.should().equal(this.bot.getDiscordAvatar('nonexistent', '#irc'), null); + }); }); From 549b8fb7679fea11f96d23eb82645eb741c0d620 Mon Sep 17 00:00:00 2001 From: Fiaxhs Date: Thu, 3 Aug 2017 13:52:25 +0200 Subject: [PATCH 12/12] One pass for webhook options Variables renaming Improved code readability --- lib/bot.js | 41 ++++++++++++++++++----------------------- test/bot.test.js | 1 - 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/lib/bot.js b/lib/bot.js index 233d9a75..fa763156 100644 --- a/lib/bot.js +++ b/lib/bot.js @@ -36,6 +36,7 @@ class Bot { this.channels = _.values(options.channelMapping); this.ircStatusNotices = options.ircStatusNotices; this.announceSelfJoin = options.announceSelfJoin; + this.webhookOptions = options.webhooks; // "{$keyName}" => "variableValue" // author/nickname: nickname of the user who sent the message @@ -72,12 +73,6 @@ class Bot { this.channelMapping[discordChan] = ircChan.split(' ')[0].toLowerCase(); }); - // Extract id and token from Webhook urls - _.forOwn(options.webhooks, (url, chan) => { - const [id, token] = url.split('/').slice(-2); - this.webhooks[chan] = { id, token }; - }); - this.invertedMapping = _.invert(this.channelMapping); this.autoSendCommands = options.autoSendCommands || []; } @@ -85,8 +80,15 @@ class Bot { connect() { logger.debug('Connecting to IRC and Discord'); this.discord.login(this.discordToken); - _.forOwn(this.webhooks, (webhook, channel) => { - this.webhooks[channel].client = new discord.WebhookClient(webhook.id, webhook.token); + + // Extract id and token from Webhook urls and connect. + _.forOwn(this.webhookOptions, (url, channel) => { + const [id, token] = url.split('/').slice(-2); + const client = new discord.WebhookClient(id, token); + this.webhooks[channel] = { + id, + client + }; }); const ircOptions = { @@ -269,7 +271,7 @@ class Bot { const author = message.author; // Ignore messages sent by the bot itself: if (author.id === this.discord.user.id || - Object.keys(this.webhooks).some(chan => this.webhooks[chan].id === author.id) + Object.keys(this.webhooks).some(channel => this.webhooks[channel].id === author.id) ) return; const channelName = `#${message.channel.name}`; @@ -349,13 +351,7 @@ class Bot { findWebhook(ircChannel) { const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()]; - if (discordChannelName) { - const webhook = this.webhooks[discordChannelName]; - if (webhook) { - return webhook; - } - } - return null; + return discordChannelName && this.webhooks[discordChannelName]; } getDiscordAvatar(nick, channel) { @@ -444,13 +440,12 @@ class Bot { const webhook = this.findWebhook(channel); if (webhook) { logger.debug('Sending message to Discord via webhook', withMentions, channel, '->', `#${discordChannel.name}`); - webhook.client.sendMessage( - withMentions, - { - username: author, - text, - avatarURL: this.getDiscordAvatar(author, channel) - }).catch(logger.error); + const avatarURL = this.getDiscordAvatar(author, channel); + webhook.client.sendMessage(withMentions, { + username: author, + text, + avatarURL + }).catch(logger.error); return; } diff --git a/test/bot.test.js b/test/bot.test.js index f7c53515..956e62ae 100644 --- a/test/bot.test.js +++ b/test/bot.test.js @@ -817,7 +817,6 @@ describe('Bot', function () { it('should extract id and token from webhook urls', function () { this.bot.webhooks['#withwebhook'].id.should.equal('id'); - this.bot.webhooks['#withwebhook'].token.should.equal('token'); }); it('should find the matching webhook when it exists', function () {