Skip to content

Commit c9fa8ba

Browse files
authored
Merge pull request #207 from Throne3d/add/joinpart
Bridge IRC join/part/quit messages to Discord
2 parents 1a7ea2d + f996405 commit c9fa8ba

File tree

4 files changed

+172
-41
lines changed

4 files changed

+172
-41
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ First you need to create a Discord bot user, which you can do by following the i
7373
"ircNickColor": false, // Gives usernames a color in IRC for better readability (on by default)
7474
// Makes the bot hide the username prefix for messages that start
7575
// with one of these characters (commands):
76-
"commandCharacters": ["!", "."]
76+
"commandCharacters": ["!", "."],
77+
"ircStatusNotices": true // Enables notifications in Discord when people join/part in the relevant IRC channel
7778
}
7879
]
7980
```

lib/bot.js

+71-35
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class Bot {
3434
this.commandCharacters = options.commandCharacters || [];
3535
this.ircNickColor = options.ircNickColor !== false; // default to true
3636
this.channels = _.values(options.channelMapping);
37+
this.ircStatusNotices = options.ircStatusNotices;
38+
this.announceSelfJoin = options.announceSelfJoin;
3739

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

125+
this.ircClient.on('join', (channel, nick) => {
126+
if (!this.ircStatusNotices) return;
127+
if (nick === this.nickname && !this.announceSelfJoin) return;
128+
this.sendExactToDiscord(channel, `*${nick}* has joined the channel`);
129+
});
130+
131+
this.ircClient.on('part', (channel, nick, reason) => {
132+
if (!this.ircStatusNotices || nick === this.nickname) return;
133+
this.sendExactToDiscord(channel, `*${nick}* has left the channel (${reason})`);
134+
});
135+
136+
this.ircClient.on('quit', (nick, reason, channels) => {
137+
if (!this.ircStatusNotices || nick === this.nickname) return;
138+
channels.forEach((channel) => {
139+
this.sendExactToDiscord(channel, `*${nick}* has quit (${reason})`);
140+
});
141+
});
142+
123143
this.ircClient.on('action', (author, to, text) => {
124144
this.sendToDiscord(author, to, `_${text}_`);
125145
});
@@ -236,8 +256,8 @@ class Bot {
236256
}
237257
}
238258

239-
sendToDiscord(author, channel, text) {
240-
const discordChannelName = this.invertedMapping[channel.toLowerCase()];
259+
findDiscordChannel(ircChannel) {
260+
const discordChannelName = this.invertedMapping[ircChannel.toLowerCase()];
241261
if (discordChannelName) {
242262
// #channel -> channel before retrieving and select only text channels:
243263
const discordChannel = discordChannelName.startsWith('#') ? this.discord.channels
@@ -247,47 +267,63 @@ class Bot {
247267
if (!discordChannel) {
248268
logger.info('Tried to send a message to a channel the bot isn\'t in: ',
249269
discordChannelName);
250-
return;
270+
return null;
251271
}
272+
return discordChannel;
273+
}
274+
return null;
275+
}
252276

253-
// Convert text formatting (bold, italics, underscore)
254-
const withFormat = formatFromIRCToDiscord(text);
277+
sendToDiscord(author, channel, text) {
278+
const discordChannel = this.findDiscordChannel(channel);
279+
if (!discordChannel) return;
280+
281+
// Convert text formatting (bold, italics, underscore)
282+
const withFormat = formatFromIRCToDiscord(text);
283+
284+
const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
285+
const search = match.substring(1);
286+
const guild = discordChannel.guild;
287+
const nickUser = guild.members.find('nickname', search);
288+
if (nickUser) {
289+
return nickUser;
290+
}
255291

256-
const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
257-
const search = match.substring(1);
258-
const guild = discordChannel.guild;
259-
const nickUser = guild.members.find('nickname', search);
260-
if (nickUser) {
261-
return nickUser;
262-
}
292+
const user = this.discord.users.find('username', search);
293+
if (user) {
294+
return user;
295+
}
263296

264-
const user = this.discord.users.find('username', search);
265-
if (user) {
266-
return user;
267-
}
297+
const role = guild.roles.find('name', search);
298+
if (role && role.mentionable) {
299+
return role;
300+
}
268301

269-
const role = guild.roles.find('name', search);
270-
if (role && role.mentionable) {
271-
return role;
272-
}
302+
return match;
303+
});
273304

274-
return match;
275-
});
305+
const patternMap = {
306+
author,
307+
text: withFormat,
308+
withMentions,
309+
discordChannel: `#${discordChannel.name}`,
310+
ircChannel: channel
311+
};
276312

277-
const patternMap = {
278-
author,
279-
text: withFormat,
280-
withMentions,
281-
discordChannel: `#${discordChannel.name}`,
282-
ircChannel: channel
283-
};
313+
// Add bold formatting:
314+
// Use custom formatting from config / default formatting with bold author
315+
const withAuthor = Bot.substitutePattern(this.formatDiscord, patternMap);
316+
logger.debug('Sending message to Discord', withAuthor, channel, '->', `#${discordChannel.name}`);
317+
discordChannel.sendMessage(withAuthor);
318+
}
284319

285-
// Add bold formatting:
286-
// Use custom formatting from config / default formatting with bold author
287-
const withAuthor = Bot.substitutePattern(this.formatDiscord, patternMap);
288-
logger.debug('Sending message to Discord', withAuthor, channel, '->', discordChannelName);
289-
discordChannel.sendMessage(withAuthor);
290-
}
320+
/* Sends a message to Discord exactly as it appears */
321+
sendExactToDiscord(channel, text) {
322+
const discordChannel = this.findDiscordChannel(channel);
323+
if (!discordChannel) return;
324+
325+
logger.debug('Sending special message to Discord', text, channel, '->', `#${discordChannel.name}`);
326+
discordChannel.sendMessage(text);
291327
}
292328
}
293329

test/bot-events.test.js

+71-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ describe('Bot Events', function () {
1919
useFakeServer: false
2020
});
2121

22-
const createBot = () => {
23-
const bot = new Bot(config);
22+
const createBot = (optConfig = null) => {
23+
const useConfig = optConfig || config;
24+
const bot = new Bot(useConfig);
2425
bot.sendToIRC = sandbox.stub();
2526
bot.sendToDiscord = sandbox.stub();
27+
bot.sendExactToDiscord = sandbox.stub();
2628
return bot;
2729
};
2830

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

126+
it('should send join messages to discord when config enabled', function () {
127+
const bot = createBot({ ...config, ircStatusNotices: true });
128+
bot.connect();
129+
const channel = '#channel';
130+
const nick = 'user';
131+
const text = `*${nick}* has joined the channel`;
132+
bot.ircClient.emit('join', channel, nick);
133+
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
134+
});
135+
136+
it('should not announce itself joining by default', function () {
137+
const bot = createBot({ ...config, ircStatusNotices: true });
138+
bot.connect();
139+
const channel = '#channel';
140+
const nick = bot.nickname;
141+
bot.ircClient.emit('join', channel, nick);
142+
bot.sendExactToDiscord.should.not.have.been.called;
143+
});
144+
145+
it('should be possible to get the bot to announce itself joining', function () {
146+
const bot = createBot({ ...config, ircStatusNotices: true, announceSelfJoin: true });
147+
bot.connect();
148+
const channel = '#channel';
149+
const nick = this.bot.nickname;
150+
const text = `*${nick}* has joined the channel`;
151+
bot.ircClient.emit('join', channel, nick);
152+
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
153+
});
154+
155+
it('should send part messages to discord when config enabled', function () {
156+
const bot = createBot({ ...config, ircStatusNotices: true });
157+
bot.connect();
158+
const channel = '#channel';
159+
const nick = 'user';
160+
const reason = 'Leaving';
161+
const text = `*${nick}* has left the channel (${reason})`;
162+
bot.ircClient.emit('part', channel, nick, reason);
163+
bot.sendExactToDiscord.should.have.been.calledWithExactly(channel, text);
164+
});
165+
166+
it('should send quit messages to discord when config enabled', function () {
167+
const bot = createBot({ ...config, ircStatusNotices: true });
168+
bot.connect();
169+
const channel1 = '#channel1';
170+
const channel2 = '#channel2';
171+
const nick = 'user';
172+
const reason = 'Quit: Leaving';
173+
const text = `*${nick}* has quit (${reason})`;
174+
bot.ircClient.emit('quit', nick, reason, [channel1, channel2]);
175+
bot.sendExactToDiscord.getCall(0).args.should.deep.equal([channel1, text]);
176+
bot.sendExactToDiscord.getCall(1).args.should.deep.equal([channel2, text]);
177+
});
178+
179+
it('should be possible to disable join/part/quit messages', function () {
180+
const bot = createBot({ ...config, ircStatusNotices: false });
181+
bot.connect();
182+
const channel = '#channel';
183+
const nick = 'user';
184+
const reason = 'Leaving';
185+
186+
bot.ircClient.emit('join', channel, nick);
187+
bot.ircClient.emit('part', channel, nick, reason);
188+
bot.ircClient.emit('join', channel, nick);
189+
bot.ircClient.emit('quit', nick, reason, [channel]);
190+
bot.sendExactToDiscord.should.not.have.been.called;
191+
});
192+
124193
it('should not listen to discord debug messages in production', function () {
125194
logger.level = 'info';
126195
const bot = createBot();

test/bot.test.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ describe('Bot', function () {
2121
});
2222

2323
beforeEach(function () {
24-
sandbox.stub(logger, 'info');
25-
sandbox.stub(logger, 'debug');
26-
sandbox.stub(logger, 'error');
24+
this.infoSpy = sandbox.stub(logger, 'info');
25+
this.debugSpy = sandbox.stub(logger, 'debug');
26+
this.errorSpy = sandbox.stub(logger, 'error');
2727
this.sendMessageStub = sandbox.stub();
2828
this.findUserStub = sandbox.stub();
2929
this.findRoleStub = sandbox.stub();
@@ -78,6 +78,12 @@ describe('Bot', function () {
7878
});
7979

8080
it('should not send messages to discord if the channel isn\'t in the channel mapping',
81+
function () {
82+
this.bot.sendToDiscord('user', '#no-irc', 'message');
83+
this.sendMessageStub.should.not.have.been.called;
84+
});
85+
86+
it('should not send messages to discord if it isn\'t in the channel',
8187
function () {
8288
this.bot.sendToDiscord('user', '#otherirc', 'message');
8389
this.sendMessageStub.should.not.have.been.called;
@@ -91,6 +97,25 @@ describe('Bot', function () {
9197
this.sendMessageStub.should.have.been.calledWith(formatted);
9298
});
9399

100+
it('should not send special messages to discord if the channel isn\'t in the channel mapping',
101+
function () {
102+
this.bot.sendExactToDiscord('#no-irc', 'message');
103+
this.sendMessageStub.should.not.have.been.called;
104+
});
105+
106+
it('should not send special messages to discord if it isn\'t in the channel',
107+
function () {
108+
this.bot.sendExactToDiscord('#otherirc', 'message');
109+
this.sendMessageStub.should.not.have.been.called;
110+
});
111+
112+
it('should send special messages to discord',
113+
function () {
114+
this.bot.sendExactToDiscord('#irc', 'message');
115+
this.sendMessageStub.should.have.been.calledWith('message');
116+
this.debugSpy.should.have.been.calledWith('Sending special message to Discord', 'message', '#irc', '->', '#discord');
117+
});
118+
94119
it('should not color irc messages if the option is disabled', function () {
95120
const text = 'testmessage';
96121
const newConfig = { ...config, ircNickColor: false };

0 commit comments

Comments
 (0)