Repository: reactiflux/discord-irc (js)

Issue:
Parse channels similar to how names are parsed
`@JohnDoe#1337` is turned into a user mention, channels (`#example`) should be turned into channel links. 

Pull request:
Add channel mentions, similar to user mentions
Fixes #460.

- Convert channel mentions (starting with #, ending with a
  space, #, @, ', !, ?, , or .) to actual codified mentions
- Modify test stubbing to allow for multiple guilds in tests
  (and fix an unused part of the addRole and addEmoji stubs)
- Test that the bot converts channel mentions appropriately:
  * does not convert unmatched channels
  * converts even if a trailing comma is found
  * converts even if special characters (e.g. `é`) are used
  * converts irregular case mentions
  * does not convert mentions to channels from other guilds

Patch:
diff --git a/lib/bot.js b/lib/bot.js
index dc13a70..7f20171 100644
--- a/lib/bot.js
+++ b/lib/bot.js
@@ -471,7 +471,7 @@ class Bot {
       // skips usernames including spaces for ease (they cannot include hashes)
       // checks case insensitively as Discord does
       const user = guild.members.find(x =>
-        Bot.caseComp(x.user.username, username.toUpperCase())
+        Bot.caseComp(x.user.username, username)
         && x.user.discriminator === discriminator);
       if (user) return user;
 
@@ -536,6 +536,16 @@ class Bot {
       const emoji = guild.emojis.find(x => x.name === ident && x.requiresColons);
       if (emoji) return emoji;
 
+      return match;
+    }).replace(/#([^\s#@'!?,.]+)/g, (match, channelName) => {
+      // channel names can't contain spaces, #, @, ', !, ?, , or .
+      // (based on brief testing. they also can't contain some other symbols,
+      // but these seem likely to be common around channel references)
+
+      // discord matches channel names case insensitively
+      const chan = guild.channels.find(x => Bot.caseComp(x.name, channelName));
+      if (chan) return chan;
+
       return match;
     });
 


Test patch:
diff --git a/test/bot.test.js b/test/bot.test.js
index bd0774c..d3bdeeb 100644
--- a/test/bot.test.js
+++ b/test/bot.test.js
@@ -21,12 +21,6 @@ describe('Bot', function () {
     useFakeServer: false
   });
 
-  const createGuildStub = () => ({
-    roles: new discord.Collection(),
-    members: new discord.Collection(),
-    emojis: new discord.Collection()
-  });
-
   beforeEach(function () {
     this.infoSpy = sandbox.stub(logger, 'info');
     this.debugSpy = sandbox.stub(logger, 'debug');
@@ -35,8 +29,7 @@ describe('Bot', function () {
 
     this.discordUsers = new discord.Collection();
     irc.Client = ClientStub;
-    this.guild = createGuildStub();
-    discord.Client = createDiscordStub(this.sendStub, this.guild, this.discordUsers);
+    discord.Client = createDiscordStub(this.sendStub, this.discordUsers);
 
     ClientStub.prototype.say = sandbox.stub();
     ClientStub.prototype.send = sandbox.stub();
@@ -44,10 +37,14 @@ describe('Bot', function () {
     this.sendWebhookMessageStub = sandbox.stub();
     discord.WebhookClient = createWebhookStub(this.sendWebhookMessageStub);
     this.bot = new Bot(config);
+    this.guild = this.bot.discord.guild;
     this.bot.connect();
 
+    // modified variants of https://github.com/discordjs/discord.js/blob/stable/src/client/ClientDataManager.js
+    // (for easier stubbing)
     this.addUser = function (user, member = null) {
       const userObj = new discord.User(this.bot.discord, user);
+      // also set guild members
       const guildMember = Object.assign({}, member || user, { user: userObj });
       guildMember.nick = guildMember.nickname; // nick => nickname in Discord API
       const memberObj = new discord.GuildMember(this.guild, guildMember);
@@ -57,13 +54,13 @@ describe('Bot', function () {
     };
 
     this.addRole = function (role) {
-      const roleObj = new discord.Role(this.bot.discord, role);
+      const roleObj = new discord.Role(this.guild, role);
       this.guild.roles.set(roleObj.id, roleObj);
       return roleObj;
     };
 
     this.addEmoji = function (emoji) {
-      const emojiObj = new discord.Emoji(this.bot.discord, emoji);
+      const emojiObj = new discord.Emoji(this.guild, emoji);
       this.guild.emojis.set(emojiObj.id, emojiObj);
       return emojiObj;
     };
@@ -517,6 +514,19 @@ describe('Bot', function () {
     this.sendStub.should.have.been.calledWith(expected);
   });
 
+  it('should convert channel mentions from IRC', function () {
+    this.guild.addTextChannel({ id: '1235', name: 'testchannel' });
+    this.guild.addTextChannel({ id: '1236', name: 'channel-compliqué' });
+    const otherGuild = this.bot.discord.createGuildStub({ id: '2' });
+    otherGuild.addTextChannel({ id: '1237', name: 'foreignchannel' });
+
+    const username = 'ircuser';
+    const text = "Here is a broken #channelname, a working #testchannel, #channel-compliqué, an irregular case #TestChannel and another guild's #foreignchannel";
+    const expected = `**<${username}>** Here is a broken #channelname, a working <#1235>, <#1236>, an irregular case <#1235> and another guild's #foreignchannel`;
+    this.bot.sendToDiscord(username, '#irc', text);
+    this.sendStub.should.have.been.calledWith(expected);
+  });
+
   it('should convert newlines from discord', function () {
     const message = {
       mentions: { users: [] },
diff --git a/test/stubs/discord-stub.js b/test/stubs/discord-stub.js
index b216497..12bd4c9 100644
--- a/test/stubs/discord-stub.js
+++ b/test/stubs/discord-stub.js
@@ -3,14 +3,14 @@ import events from 'events';
 import sinon from 'sinon';
 import discord from 'discord.js';
 
-export default function createDiscordStub(sendStub, guild, discordUsers) {
+export default function createDiscordStub(sendStub, discordUsers) {
   return class DiscordStub extends events.EventEmitter {
     constructor() {
       super();
       this.user = {
         id: 'testid'
       };
-      this.channels = this.guildChannels();
+      this.channels = new discord.Collection();
       this.options = {
         http: {
           cdn: ''
@@ -18,18 +18,46 @@ export default function createDiscordStub(sendStub, guild, discordUsers) {
       };
 
       this.users = discordUsers;
+      this.guilds = new discord.Collection();
+      this.guild = this.createGuildStub();
+      this.guilds.set(this.guild.id, this.guild);
     }
 
-    guildChannels() {
-      const channels = new discord.Collection();
-      channels.set('1234', {
-        name: 'discord',
-        id: '1234',
-        type: 'text',
-        send: sendStub,
-        guild
-      });
-      return channels;
+    addTextChannel(guild, textChannel) {
+      const textChannelData = Object.assign({
+        type: 'text'
+      }, textChannel);
+      const textChannelObj = new discord.TextChannel(guild, textChannelData);
+      textChannelObj.send = sendStub;
+      this.channels.set(textChannelObj.id, textChannelObj);
+      return textChannelObj;
+    }
+
+    createGuildStub(guildData = {}) {
+      const guild = {
+        id: '1',
+        client: this,
+        roles: new discord.Collection(),
+        members: new discord.Collection(),
+        emojis: new discord.Collection(),
+        channels: new discord.Collection(),
+        addTextChannel: (textChannel) => {
+          const textChannelObj = this.addTextChannel(guild, textChannel);
+          textChannelObj.guild.channels.set(textChannelObj.id, textChannelObj);
+          return textChannelObj;
+        }
+      };
+      Object.assign(guild, guildData);
+      this.guilds.set(guild.id, guild);
+
+      if (guild.id === '1') {
+        guild.addTextChannel({
+          name: 'discord',
+          id: '1234',
+        });
+      }
+
+      return guild;
     }
 
     login() {