Skip to content

Bridge IRC join/part/quit messages to Discord #207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ 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": ["!", "."],
"ircStatusNotices": true // Enables notifications in Discord when people join/part in the relevant IRC channel
}
]
```
Expand Down
106 changes: 71 additions & 35 deletions lib/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Bot {
this.commandCharacters = options.commandCharacters || [];
this.ircNickColor = options.ircNickColor !== false; // default to true
this.channels = _.values(options.channelMapping);
this.ircStatusNotices = options.ircStatusNotices;
this.announceSelfJoin = options.announceSelfJoin;

this.format = options.format || {};
// "{$keyName}" => "variableValue"
Expand Down Expand Up @@ -120,6 +122,24 @@ class Bot {
this.sendToDiscord(author, to, `*${text}*`);
});

this.ircClient.on('join', (channel, nick) => {
if (!this.ircStatusNotices) return;
if (nick === this.nickname && !this.announceSelfJoin) return;
this.sendExactToDiscord(channel, `*${nick}* has joined the channel`);
});

this.ircClient.on('part', (channel, nick, reason) => {
if (!this.ircStatusNotices || nick === this.nickname) return;
this.sendExactToDiscord(channel, `*${nick}* has left the channel (${reason})`);
});

this.ircClient.on('quit', (nick, reason, channels) => {
if (!this.ircStatusNotices || nick === this.nickname) return;
channels.forEach((channel) => {
this.sendExactToDiscord(channel, `*${nick}* has quit (${reason})`);
});
});

this.ircClient.on('action', (author, to, text) => {
this.sendToDiscord(author, to, `_${text}_`);
});
Expand Down Expand Up @@ -236,8 +256,8 @@ class Bot {
}
}

sendToDiscord(author, channel, text) {
const discordChannelName = this.invertedMapping[channel.toLowerCase()];
findDiscordChannel(ircChannel) {
const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()];
if (discordChannelName) {
// #channel -> channel before retrieving and select only text channels:
const discordChannel = discordChannelName.startsWith('#') ? this.discord.channels
Expand All @@ -247,47 +267,63 @@ class Bot {
if (!discordChannel) {
logger.info('Tried to send a message to a channel the bot isn\'t in: ',
discordChannelName);
return;
return null;
}
return discordChannel;
}
return null;
}

// Convert text formatting (bold, italics, underscore)
const withFormat = formatFromIRCToDiscord(text);
sendToDiscord(author, channel, text) {
const discordChannel = this.findDiscordChannel(channel);
if (!discordChannel) return;

// Convert text formatting (bold, italics, underscore)
const withFormat = formatFromIRCToDiscord(text);

const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
const search = match.substring(1);
const guild = discordChannel.guild;
const nickUser = guild.members.find('nickname', search);
if (nickUser) {
return nickUser;
}

const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
const search = match.substring(1);
const guild = discordChannel.guild;
const nickUser = guild.members.find('nickname', search);
if (nickUser) {
return nickUser;
}
const user = this.discord.users.find('username', search);
if (user) {
return user;
}

const user = this.discord.users.find('username', search);
if (user) {
return user;
}
const role = guild.roles.find('name', search);
if (role && role.mentionable) {
return role;
}

const role = guild.roles.find('name', search);
if (role && role.mentionable) {
return role;
}
return match;
});

return match;
});
const patternMap = {
author,
text: withFormat,
withMentions,
discordChannel: `#${discordChannel.name}`,
ircChannel: channel
};

const patternMap = {
author,
text: withFormat,
withMentions,
discordChannel: `#${discordChannel.name}`,
ircChannel: channel
};
// Add bold formatting:
// 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}`);
discordChannel.sendMessage(withAuthor);
}

// Add bold formatting:
// 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, '->', discordChannelName);
discordChannel.sendMessage(withAuthor);
}
/* Sends a message to Discord exactly as it appears */
sendExactToDiscord(channel, text) {
const discordChannel = this.findDiscordChannel(channel);
if (!discordChannel) return;

logger.debug('Sending special message to Discord', text, channel, '->', `#${discordChannel.name}`);
discordChannel.sendMessage(text);
}
}

Expand Down
73 changes: 71 additions & 2 deletions test/bot-events.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ describe('Bot Events', function () {
useFakeServer: false
});

const createBot = () => {
const bot = new Bot(config);
const createBot = (optConfig = null) => {
const useConfig = optConfig || config;
const bot = new Bot(useConfig);
bot.sendToIRC = sandbox.stub();
bot.sendToDiscord = sandbox.stub();
bot.sendExactToDiscord = sandbox.stub();
return bot;
};

Expand Down Expand Up @@ -121,6 +123,73 @@ describe('Bot Events', function () {
this.bot.sendToDiscord.should.have.been.calledWithExactly(author, channel, formattedText);
});

it('should send join messages to discord when config enabled', function () {
const bot = createBot({ ...config, ircStatusNotices: true });
bot.connect();
const channel = '#channel';
const nick = 'user';
const text = `*${nick}* has joined the channel`;
bot.ircClient.emit('join', channel, nick);
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
});

it('should not announce itself joining by default', function () {
const bot = createBot({ ...config, ircStatusNotices: true });
bot.connect();
const channel = '#channel';
const nick = bot.nickname;
bot.ircClient.emit('join', channel, nick);
bot.sendExactToDiscord.should.not.have.been.called;
});

it('should be possible to get the bot to announce itself joining', function () {
const bot = createBot({ ...config, ircStatusNotices: true, announceSelfJoin: true });
bot.connect();
const channel = '#channel';
const nick = this.bot.nickname;
const text = `*${nick}* has joined the channel`;
bot.ircClient.emit('join', channel, nick);
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
});

it('should send part messages to discord when config enabled', function () {
const bot = createBot({ ...config, ircStatusNotices: true });
bot.connect();
const channel = '#channel';
const nick = 'user';
const reason = 'Leaving';
const text = `*${nick}* has left the channel (${reason})`;
bot.ircClient.emit('part', channel, nick, reason);
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
});

it('should send quit messages to discord when config enabled', function () {
const bot = createBot({ ...config, ircStatusNotices: true });
bot.connect();
const channel1 = '#channel1';
const channel2 = '#channel2';
const nick = 'user';
const reason = 'Quit: Leaving';
const text = `*${nick}* has quit (${reason})`;
bot.ircClient.emit('quit', nick, reason, [channel1, channel2]);
bot.sendExactToDiscord.getCall(0).args.should.deep.equal([channel1, text]);
bot.sendExactToDiscord.getCall(1).args.should.deep.equal([channel2, text]);
});

it('should be possible to disable join/part/quit messages', function () {
const bot = createBot({ ...config, ircStatusNotices: false });
bot.connect();
const channel = '#channel';
const nick = 'user';
const reason = 'Leaving';

bot.ircClient.emit('join', channel, nick);
bot.ircClient.emit('part', channel, nick, reason);
bot.ircClient.emit('join', channel, nick);
bot.ircClient.emit('quit', nick, reason, [channel]);
bot.sendExactToDiscord.should.not.have.been.called;
});

it('should not listen to discord debug messages in production', function () {
logger.level = 'info';
const bot = createBot();
Expand Down
31 changes: 28 additions & 3 deletions test/bot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ describe('Bot', function () {
});

beforeEach(function () {
sandbox.stub(logger, 'info');
sandbox.stub(logger, 'debug');
sandbox.stub(logger, 'error');
this.infoSpy = sandbox.stub(logger, 'info');
this.debugSpy = sandbox.stub(logger, 'debug');
this.errorSpy = sandbox.stub(logger, 'error');
this.sendMessageStub = sandbox.stub();
this.findUserStub = sandbox.stub();
this.findRoleStub = sandbox.stub();
Expand Down Expand Up @@ -78,6 +78,12 @@ describe('Bot', function () {
});

it('should not send messages to discord if the channel isn\'t in the channel mapping',
function () {
this.bot.sendToDiscord('user', '#no-irc', 'message');
this.sendMessageStub.should.not.have.been.called;
});

it('should not send messages to discord if it isn\'t in the channel',
function () {
this.bot.sendToDiscord('user', '#otherirc', 'message');
this.sendMessageStub.should.not.have.been.called;
Expand All @@ -91,6 +97,25 @@ describe('Bot', function () {
this.sendMessageStub.should.have.been.calledWith(formatted);
});

it('should not send special messages to discord if the channel isn\'t in the channel mapping',
function () {
this.bot.sendExactToDiscord('#no-irc', 'message');
this.sendMessageStub.should.not.have.been.called;
});

it('should not send special messages to discord if it isn\'t in the channel',
function () {
this.bot.sendExactToDiscord('#otherirc', 'message');
this.sendMessageStub.should.not.have.been.called;
});

it('should send special messages to discord',
function () {
this.bot.sendExactToDiscord('#irc', 'message');
this.sendMessageStub.should.have.been.calledWith('message');
this.debugSpy.should.have.been.calledWith('Sending special message to Discord', 'message', '#irc', '->', '#discord');
});

it('should not color irc messages if the option is disabled', function () {
const text = 'testmessage';
const newConfig = { ...config, ircNickColor: false };
Expand Down