diff --git a/lib/bridge/IrcHandler.js b/lib/bridge/IrcHandler.js index 9488e1f92..f7496d900 100644 --- a/lib/bridge/IrcHandler.js +++ b/lib/bridge/IrcHandler.js @@ -486,7 +486,7 @@ IrcHandler.prototype.onMessage = Promise.coroutine(function*(req, server, fromUs } } - mxAction.formatMentions(mapping); + yield mxAction.formatMentions(mapping, this.ircBridge.getAppServiceBridge().getIntent()); if (!mxAction) { req.log.error("Couldn't map IRC action to matrix action"); diff --git a/lib/bridge/MatrixHandler.js b/lib/bridge/MatrixHandler.js index a3f0cb79b..b51ae40a3 100644 --- a/lib/bridge/MatrixHandler.js +++ b/lib/bridge/MatrixHandler.js @@ -1653,7 +1653,9 @@ MatrixHandler.prototype.onUserQuery = function(req, userId) { }; MatrixHandler.prototype.getMetrics = function(serverDomain) { - return this.metrics[serverDomain] || {}; + const metrics = this.metrics[serverDomain] || {}; + this.metrics[serverDomain] = {} + return metrics || {}; } function reqHandler(req, promise) { diff --git a/lib/models/MatrixAction.js b/lib/models/MatrixAction.js index 6eddc6346..f839ca0d4 100644 --- a/lib/models/MatrixAction.js +++ b/lib/models/MatrixAction.js @@ -1,8 +1,10 @@ +/*eslint no-invalid-this: 0*/ // eslint doesn't understand Promise.coroutine wrapping "use strict"; const ircFormatting = require("../irc/formatting"); const log = require("../logging").get("MatrixAction"); const ContentRepo = require("matrix-appservice-bridge").ContentRepo; const escapeStringRegexp = require('escape-string-regexp'); +const Promise = require("bluebird"); const ACTION_TYPES = ["message", "emote", "topic", "notice", "file", "image", "video", "audio"]; const EVENT_TO_TYPE = { @@ -31,15 +33,15 @@ function MatrixAction(type, text, htmlText, timestamp) { this.ts = timestamp || 0; } -MatrixAction.prototype.formatMentions = function(nickUserIdMap) { +MatrixAction.prototype.formatMentions = Promise.coroutine(function*(nickUserIdMap, intent) { const regexString = "(" + Object.keys(nickUserIdMap).map((value) => escapeStringRegexp(value)).join("|") + ")"; - const userRegex = new RegExp(regexString, "igm"); + const usersRegex = MentionRegex(regexString); const matched = new Set(); // lowercased nicknames we have matched already. let match; - for (let i = 0; i < MAX_MATCHES && (match = userRegex.exec(this.text)) !== null; i++) { - let matchName = match[1]; + for (let i = 0; i < MAX_MATCHES && (match = usersRegex.exec(this.text)) !== null; i++) { + let matchName = match[2]; // Deliberately have a minimum length to match on, // so we don't match smaller nicks accidentally. if (matchName.length < PILL_MIN_LENGTH_TO_MATCH || matched.has(matchName.toLowerCase())) { @@ -64,14 +66,32 @@ MatrixAction.prototype.formatMentions = function(nickUserIdMap) { this.htmlText = this.text; } userId = ircFormatting.escapeHtmlChars(userId); - this.htmlText = this.htmlText.replace( - new RegExp(escapeStringRegexp(matchName), "igm"), -`${ircFormatting.escapeHtmlChars(matchName)}` + + /* Due to how Riot and friends do push notifications, + we need the plain text to match something.*/ + let identifier; + try { + identifier = (yield intent.getProfileInfo(userId, 'displayname', true)).displayname; + } + catch (e) { + // This shouldn't happen, but let's not fail to match if so. + } + + if (identifier === undefined) { + // Fallback to userid. + identifier = userId.substr(1, userId.indexOf(":")-1) + } + + const regex = MentionRegex(escapeStringRegexp(matchName)); + this.htmlText = this.htmlText.replace(regex, + `$1`+ + `${ircFormatting.escapeHtmlChars(identifier)}` ); + this.text = this.text.replace(regex, `$1${identifier}`); // Don't match this name twice, we've already replaced all entries. matched.add(matchName.toLowerCase()); } -} +}); MatrixAction.fromEvent = function(client, event, mediaUrl) { event.content = event.content || {}; @@ -128,4 +148,12 @@ MatrixAction.fromIrcAction = function(ircAction) { } }; +function MentionRegex(matcher) { + const WORD_BOUNDARY = "^|\:|\#|```|\\s|$|,"; + return new RegExp( + `(${WORD_BOUNDARY})(@?(${matcher}))(?=${WORD_BOUNDARY})`, + "igmu" + ); +} + module.exports = MatrixAction; diff --git a/spec/unit/MatrixAction.spec.js b/spec/unit/MatrixAction.spec.js index b771b4a6b..51b5fe453 100644 --- a/spec/unit/MatrixAction.spec.js +++ b/spec/unit/MatrixAction.spec.js @@ -1,15 +1,34 @@ "use strict"; const MatrixAction = require("../../lib/models/MatrixAction"); +const FakeIntent = { + getProfileInfo: (userId) => { + return new Promise((resolve, reject) => { + if (userId === "@jc.denton:unatco.gov") { + resolve({displayname: "TheJCDenton"}); + } + else if (userId === "@paul.denton:unatco.gov") { + resolve({displayname: "ThePaulDenton"}); + } + else { + reject("This user was not found"); + } + }); + } +} + describe("MatrixAction", function() { + it("should not highlight mentions to text without mentions", () => { let action = new MatrixAction("message", "Some text"); - action.formatMentions({ + return action.formatMentions({ "Some Person": "@foobar:localhost" + }, FakeIntent).then(() => { + expect(action.text).toEqual("Some text"); + expect(action.htmlText).toBeUndefined(); }); - expect(action.text).toEqual("Some text"); - expect(action.htmlText).toBeUndefined(); }); + it("should highlight a user", () => { let action = new MatrixAction( "message", @@ -17,13 +36,15 @@ describe("MatrixAction", function() { "JCDenton, it's a bomb!", null ); - action.formatMentions({ + return action.formatMentions({ "JCDenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton, it's a bomb!"); + expect(action.htmlText).toEqual( + ""+ + "TheJCDenton, it's a bomb!" + ); }); - expect(action.text).toEqual("JCDenton, it's a bomb!"); - expect(action.htmlText).toEqual( - "JCDenton, it's a bomb!" - ); }); it("should highlight a user, regardless of case", () => { let action = new MatrixAction( @@ -32,33 +53,40 @@ describe("MatrixAction", function() { "JCDenton, it's a bomb!", null ); - action.formatMentions({ + return action.formatMentions({ "jcdenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton, it's a bomb!"); + expect(action.htmlText).toEqual( + ""+ + "TheJCDenton, it's a bomb!" + ); }); - expect(action.text).toEqual("JCDenton, it's a bomb!"); - expect(action.htmlText).toEqual( - "jcdenton, it's a bomb!" - ); + }); it("should highlight a user, with plain text", () => { let action = new MatrixAction("message", "JCDenton, it's a bomb!"); - action.formatMentions({ + return action.formatMentions({ "JCDenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton, it's a bomb!"); + expect(action.htmlText).toEqual( + ""+ + "TheJCDenton, it's a bomb!" + ); }); - expect(action.text).toEqual("JCDenton, it's a bomb!"); - expect(action.htmlText).toEqual( - "JCDenton, it's a bomb!" - ); }); it("should highlight a user, with weird characters", () => { let action = new MatrixAction("message", "`||JCDenton[m], it's a bomb!"); - action.formatMentions({ + return action.formatMentions({ "`||JCDenton[m]": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton, it's a bomb!"); + expect(action.htmlText).toEqual( + ""+ + "TheJCDenton, it's a bomb!" + ); }); - expect(action.text).toEqual("`||JCDenton[m], it's a bomb!"); - expect(action.htmlText).toEqual( - "`||JCDenton[m], it's a bomb!" - ); }); it("should highlight multiple users", () => { let action = new MatrixAction( @@ -67,15 +95,17 @@ describe("MatrixAction", function() { "JCDenton is sent to assassinate PaulDenton", null ); - action.formatMentions({ + return action.formatMentions({ "JCDenton": "@jc.denton:unatco.gov", "PaulDenton": "@paul.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton is sent to assassinate ThePaulDenton"); + expect(action.htmlText).toEqual( + "TheJCDenton is sent" + + " to assassinate " + + "ThePaulDenton" + ); }); - expect(action.text).toEqual("JCDenton is sent to assassinate PaulDenton"); - expect(action.htmlText).toEqual( - "JCDenton is sent" + - " to assassinate PaulDenton" - ); }); it("should highlight multiple mentions of the same user", () => { let action = new MatrixAction( @@ -84,13 +114,63 @@ describe("MatrixAction", function() { "JCDenton, meet JCDenton", null ); - action.formatMentions({ + return action.formatMentions({ "JCDenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("TheJCDenton, meet TheJCDenton"); + expect(action.htmlText).toEqual( + "TheJCDenton," + + " meet TheJCDenton" + ); }); - expect(action.text).toEqual("JCDenton, meet JCDenton"); - expect(action.htmlText).toEqual( - "JCDenton," + - " meet JCDenton" + }); + it("should not highlight mentions in a URL with www.", () => { + let action = new MatrixAction( + "message", + "Go to http://www.JCDenton.com", + "Go to my website", + null ); + return action.formatMentions({ + "JCDenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("Go to http://www.JCDenton.com"); + expect(action.htmlText).toEqual( + "Go to my website" + ); + }); + }); + it("should not highlight mentions in a URL with http://", () => { + let action = new MatrixAction( + "message", + "Go to http://JCDenton.com", + "Go to my website", + null + ); + return action.formatMentions({ + "JCDenton": "@jc.denton:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("Go to http://JCDenton.com"); + expect(action.htmlText).toEqual( + "Go to my website" + ); + }); + }); + it("should fallback to userIds", () => { + let action = new MatrixAction( + "message", + "AnnaNavarre: The machine would not make a mistake!", + "AnnaNavarre: The machine would not make a mistake!", + null + ); + return action.formatMentions({ + "AnnaNavarre": "@anna.navarre:unatco.gov" + }, FakeIntent).then(() => { + expect(action.text).toEqual("anna.navarre: The machine would not make a mistake!"); + expect(action.htmlText).toEqual( + ""+ + "anna.navarre: The machine would not make a mistake!" + ); + }); }); });