Skip to content

Commit

Permalink
Merge 8cf07ab into db39ba2
Browse files Browse the repository at this point in the history
  • Loading branch information
Throne3d committed Oct 22, 2017
2 parents db39ba2 + 8cf07ab commit 1e086bc
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 18 deletions.
90 changes: 73 additions & 17 deletions lib/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,18 @@ class Bot {
return null;
}

// compare two strings case-insensitively
// for discord mention matching
static caseComp(str1, str2) {
return str1.toUpperCase() === str2.toUpperCase();
}

// check if the first string starts with the second case-insensitively
// for discord mention matching
static caseStartsWith(str1, str2) {
return str1.toUpperCase().startsWith(str2.toUpperCase());
}

sendToDiscord(author, channel, text) {
const discordChannel = this.findDiscordChannel(channel);
if (!discordChannel) return;
Expand Down Expand Up @@ -400,29 +412,73 @@ class Bot {
}

const { guild } = discordChannel;
const withMentions = withFormat.replace(/@[^\s]+\b/g, (match) => {
const search = match.substring(1);
const nickUser = guild.members.find('nickname', search);
if (nickUser) {
return nickUser;
}
const withMentions = withFormat.replace(/@([^\s#]+)#(\d+)/g, (match, username, discriminator) => {
// @username#1234 => mention
// 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())
&& x.user.discriminator === discriminator);
if (user) return user;

const user = this.discord.users.find('username', search);
if (user) {
return user;
}
return match;
}).replace(/@([^\s]+)/g, (match, reference) => {
// this preliminary stuff is ultimately unnecessary
// but might save time over later more complicated calculations
// @nickname => mention, case insensitively
const nickUser = guild.members.find(x =>
x.nickname !== null && Bot.caseComp(x.nickname, reference));
if (nickUser) return nickUser;

// @username => mention, case insensitively
const user = guild.members.find(x => Bot.caseComp(x.user.username, reference));
if (user) return user;

// @role => mention, case insensitively
const role = guild.roles.find(x => x.mentionable && Bot.caseComp(x.name, reference));
if (role) return role;

// No match found checking the whole word. Check for partial matches now instead.
// @nameextra => [mention]extra, case insensitively, as Discord does
// uses the longest match, and if there are two, whichever is a match by case
let matchLength = 0;
let bestMatch = null;
let caseMatched = false;

// check if a partial match is found in reference and if so update the match values
const checkMatch = function (matchString, matchValue) {
// if the matchString is longer than the current best and is a match
// or if it's the same length but it matches by case unlike the current match
// set the best match to this matchString and matchValue
if ((matchString.length > matchLength && Bot.caseStartsWith(reference, matchString))
|| (matchString.length === matchLength && !caseMatched
&& reference.startsWith(matchString))) {
matchLength = matchString.length;
bestMatch = matchValue;
caseMatched = reference.startsWith(matchString);
}
};

const role = guild.roles.find('name', search);
if (role && role.mentionable) {
return role;
}
// check users by username and nickname
guild.members.forEach((member) => {
checkMatch(member.user.username, member);
if (bestMatch === member || member.nickname === null) return;
checkMatch(member.nickname, member);
});
// check mentionable roles by visible name
guild.roles.forEach((member) => {
if (!member.mentionable) return;
checkMatch(member.name, member);
});

// if a partial match was found, return the match and the unmatched trailing characters
if (bestMatch) return bestMatch.toString() + reference.substring(matchLength);

return match;
}).replace(/:(\w+):/g, (match, ident) => {
// :emoji: => mention, case sensitively
const emoji = guild.emojis.find(x => x.name === ident && x.requiresColons);
if (emoji) {
return `<:${emoji.identifier}>`; // identifier = name + ":" + id
}
if (emoji) return emoji;

return match;
});
Expand Down
48 changes: 47 additions & 1 deletion test/bot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,24 @@ describe('Bot', function () {

const username = 'ircuser';
const text = 'Hello, @testuser!';
const expected = `**<${username}>** Hello, <@${testUser.id}>!`;
const expected = `**<${username}>** Hello, ${testUser}!`;

this.bot.sendToDiscord(username, '#irc', text);
this.sendStub.should.have.been.calledWith(expected);
});

it('should convert username-discriminator mentions from IRC properly', function () {
const user1 = this.addUser({ username: 'user', id: '123', discriminator: '9876' });
const user2 = this.addUser({
username: 'user',
id: '124',
discriminator: '5555',
nickname: 'secondUser'
});

const username = 'ircuser';
const text = 'hello @user#9876 and @user#5555 and @fakeuser#1234';
const expected = `**<${username}>** hello ${user1} and ${user2} and @fakeuser#1234`;

this.bot.sendToDiscord(username, '#irc', text);
this.sendStub.should.have.been.calledWith(expected);
Expand Down Expand Up @@ -637,6 +654,35 @@ describe('Bot', function () {
this.sendStub.should.have.been.calledWith(expected);
});

it('should convert overlapping mentions from IRC properly and case-insensitively', function () {
const user = this.addUser({ username: 'user', id: '111' });
const nickUser = this.addUser({ username: 'user2', id: '112', nickname: 'userTest' });
const nickUserCase = this.addUser({ username: 'user3', id: '113', nickname: 'userTEST' });
const role = this.addRole({ name: 'userTestRole', id: '12345', mentionable: true });

const username = 'ircuser';
const text = 'hello @User, @user, @userTest, @userTEST, @userTestRole and @usertestrole';
const expected = `**<${username}>** hello ${user}, ${user}, ${nickUser}, ${nickUserCase}, ${role} and ${role}`;

this.bot.sendToDiscord(username, '#irc', text);
this.sendStub.should.have.been.calledWith(expected);
});

it('should convert partial matches from IRC properly', function () {
const user = this.addUser({ username: 'user', id: '111' });
const longUser = this.addUser({ username: 'user-punc', id: '112' });
const nickUser = this.addUser({ username: 'user2', id: '113', nickname: 'nick' });
const nickUserCase = this.addUser({ username: 'user3', id: '114', nickname: 'NiCK' });
const role = this.addRole({ name: 'role', id: '12345', mentionable: true });

const username = 'ircuser';
const text = '@user-ific @usermore, @user\'s friend @user-punc, @nicks and @NiCKs @roles';
const expected = `**<${username}>** ${user}-ific ${user}more, ${user}'s friend ${longUser}, ${nickUser}s and ${nickUserCase}s ${role}s`;

this.bot.sendToDiscord(username, '#irc', text);
this.sendStub.should.have.been.calledWith(expected);
});

it('should successfully send messages with default config', function () {
const bot = new Bot(configMsgFormatDefault);
bot.connect();
Expand Down

0 comments on commit 1e086bc

Please sign in to comment.