Skip to content

Commit

Permalink
Merge pull request #230 from Fiaxhs/master
Browse files Browse the repository at this point in the history
Use webhooks to reflect irc usernames + avatars
  • Loading branch information
ekmartin authored Mar 22, 2018
2 parents f660afd + 90c901a commit c7263b7
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 2 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ First you need to create a Discord bot user, which you can do by following the i
"ignoreUsers": {
"irc": ["irc_nick1", "irc_nick2"], // Ignore specified IRC nicks and do not send their messages to Discord.
"discord": ["discord_nick1", "discord_nick2"] // Ignore specified Discord nicks and do not send their messages to IRC.
},
// List of webhooks per channel
"webhooks": {
"#discord": "https://discordapp.com/api/webhooks/id/token"
}
}
]
Expand All @@ -102,6 +106,15 @@ The `ircOptions` object is passed directly to irc-upd ([available options](https

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)

### Encodings

If you encounter trouble with some characters being corrupted from some clients (particularly umlauted characters, such as `ä` or `ö`), try installing the optional dependencies `iconv` and `node-icu-charset-detector`.
Expand Down
61 changes: 60 additions & 1 deletion lib/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Bot {
this.channels = _.values(options.channelMapping);
this.ircStatusNotices = options.ircStatusNotices;
this.announceSelfJoin = options.announceSelfJoin;
this.webhookOptions = options.webhooks;

// Nicks to ignore
this.ignoreUsers = options.ignoreUsers || {};
Expand Down Expand Up @@ -70,6 +71,7 @@ class Bot {
this.channelUsers = {};

this.channelMapping = {};
this.webhooks = {};

// Remove channel passwords from the mapping and lowercase IRC channel names
_.forOwn(options.channelMapping, (ircChan, discordChan) => {
Expand All @@ -84,6 +86,16 @@ class Bot {
logger.debug('Connecting to IRC and Discord');
this.discord.login(this.discordToken);

// 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 = {
userName: this.nickname,
realName: this.nickname,
Expand Down Expand Up @@ -285,7 +297,9 @@ class Bot {
sendToIRC(message) {
const { author } = message;
// 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(channel => this.webhooks[channel].id === author.id)
) return;

// Do not send to IRC if this user is on the ignore list.
if (this.ignoredDiscordUser(author.username)) {
Expand Down Expand Up @@ -369,6 +383,38 @@ class Bot {
return null;
}

findWebhook(ircChannel) {
const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()];
return discordChannelName && this.webhooks[discordChannelName];
}

getDiscordAvatar(nick, channel) {
const guildMembers = this.findDiscordChannel(channel).guild.members;
const findByNicknameOrUsername = caseSensitive =>
(member) => {
if (caseSensitive) {
return member.user.username === nick || member.nickname === nick;
}
const nickLowerCase = nick.toLowerCase();
return member.user.username.toLowerCase() === nickLowerCase
|| (member.nickname && member.nickname.toLowerCase() === nickLowerCase);
};

// Try to find exact matching case
let users = guildMembers.filter(findByNicknameOrUsername(true));

// Now let's search case insensitive.
if (users.size === 0) {
users = guildMembers.filter(findByNicknameOrUsername(false));
}

// No matching user or more than one => no avatar
if (users && users.size === 1) {
return users.first().user.avatarURL;
}
return null;
}

// compare two strings case-insensitively
// for discord mention matching
static caseComp(str1, str2) {
Expand Down Expand Up @@ -485,6 +531,19 @@ 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}`);
const avatarURL = this.getDiscordAvatar(author, channel);
webhook.client.sendMessage(withMentions, {
username: author,
text,
avatarURL
}).catch(logger.error);
return;
}

patternMap.withMentions = withMentions;

// Add bold formatting:
Expand Down
79 changes: 79 additions & 0 deletions test/bot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -40,6 +41,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();

Expand Down Expand Up @@ -892,6 +895,82 @@ describe('Bot', function () {
this.sendStub.getCall(0).args.should.deep.equal([msg]);
});

it('should create webhooks clients for each webhook url in the config', function () {
this.bot.webhooks.should.have.property('#withwebhook');
});

it('should extract id and token from webhook urls', function () {
this.bot.webhooks['#withwebhook'].id.should.equal('id');
});

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 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();
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 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 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 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);
});

it(
'should not send messages to Discord if IRC user is ignored',
function () {
Expand Down
6 changes: 5 additions & 1 deletion test/fixtures/single-test-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
"channelMapping": {
"#discord": "#irc channelKey",
"#notinchannel": "#otherIRC",
"1234": "#channelforid"
"1234": "#channelforid",
"#withwebhook": "#ircwebhook"
},
"webhooks": {
"#withwebhook": "https://discordapp.com/api/webhooks/id/token"
},
"ignoreUsers": {
"irc": ["irc_ignored_user"],
Expand Down
5 changes: 5 additions & 0 deletions test/stubs/discord-stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export default function createDiscordStub(sendStub, guild, discordUsers) {
id: 'testid'
};
this.channels = this.guildChannels();
this.options = {
http: {
cdn: ''
}
};

this.users = discordUsers;
}
Expand Down
14 changes: 14 additions & 0 deletions test/stubs/webhook-stub.js
Original file line number Diff line number Diff line change
@@ -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(() => {});
}
};
}

0 comments on commit c7263b7

Please sign in to comment.