diff --git a/src/matrixroomhandler.ts b/src/matrixroomhandler.ts index 13940185d67467539f9e0098c21094ae30dd93e2..c9fe11f3980fb7a71099fe72686c99f6c3d24f2c 100644 --- a/src/matrixroomhandler.ts +++ b/src/matrixroomhandler.ts @@ -67,7 +67,7 @@ export class MatrixRoomHandler { /* We delay the joins to give some implementations a chance to breathe */ let delay = this.config.limits.roomGhostJoinDelay; return this.discord.GetChannelFromRoomId(roomId).then((channel: Discord.Channel) => { - for (const member of (<Discord.TextChannel> channel).guild.members.array()) { + for (const member of (<Discord.TextChannel> channel).members.array()) { if (member.id === this.discord.GetBotId()) { continue; } @@ -78,19 +78,20 @@ export class MatrixRoomHandler { } }).catch((err) => { log.verbose("OnAliasQueried => %s", err); + throw err; }); } - public OnEvent (request, context) { + public OnEvent (request, context): Promise<any> { const event = request.getData(); if (event.unsigned.age > AGE_LIMIT) { log.warn("MatrixRoomHandler", "Skipping event due to age %s > %s", event.unsigned.age, AGE_LIMIT); - return; + return Promise.reject("Event too old"); } if (event.type === "m.room.member" && event.content.membership === "invite") { - this.HandleInvite(event); + return this.HandleInvite(event); } else if (event.type === "m.room.redaction" && context.rooms.remote) { - this.discord.ProcessMatrixRedact(event); + return this.discord.ProcessMatrixRedact(event); } else if (event.type === "m.room.message") { log.verbose("MatrixRoomHandler", "Got m.room.message event"); if (event.content.body && event.content.body.startsWith("!discord")) { @@ -104,6 +105,7 @@ export class MatrixRoomHandler { } else { log.verbose("MatrixRoomHandler", "Got non m.room.message event"); } + return Promise.reject("Event not processed by bridge"); } public HandleInvite(event: any) { @@ -160,7 +162,7 @@ export class MatrixRoomHandler { if (command === "help" && args[0] === "bridge") { const link = Util.GetBotLink(this.config); - this.bridge.getIntent().sendMessage(event.room_id, { + return this.bridge.getIntent().sendMessage(event.room_id, { msgtype: "m.notice", body: "How to bridge a Discord guild:\n" + "1. Invite the bot to your Discord guild using this link: " + link + "\n" + @@ -256,7 +258,7 @@ export class MatrixRoomHandler { } } else if (command === "help") { // Unknown command or no command given to get help on, so we'll just give them the help - this.bridge.getIntent().sendMessage(event.room_id, { + return this.bridge.getIntent().sendMessage(event.room_id, { msgtype: "m.notice", body: "Available commands:\n" + "!discord bridge <guild id> <channel id> - Bridges this room to a Discord channel\n" + diff --git a/test/test_matrixroomhandler.ts b/test/test_matrixroomhandler.ts index a47fb07588818e9ade624b48a8c454381c23973c..142becea3ba0263d100b09cd1e8f665b50b91574 100644 --- a/test/test_matrixroomhandler.ts +++ b/test/test_matrixroomhandler.ts @@ -9,6 +9,7 @@ import {DiscordBot} from "../src/bot"; import {MatrixRoomHandler} from "../src/matrixroomhandler"; import {MockChannel} from "./mocks/channel"; import {MockMember} from "./mocks/member"; +import * as Bluebird from "bluebird"; Chai.use(ChaiAsPromised); const expect = Chai.expect; @@ -19,6 +20,15 @@ const expect = Chai.expect; let USERSJOINED = 0; +function buildRequest(eventData) { + if (eventData.unsigned === undefined) { + eventData.unsigned = {age: 0}; + } + return { + getData: () => eventData, + }; +} + function createRH(opts: any = {}) { USERSJOINED = 0; const bot = { @@ -44,32 +54,219 @@ function createRH(opts: any = {}) { }, GetBotId: () => "bot12345", + ProcessMatrixRedact: () => Promise.resolve("redacted"), + ProcessMatrixMsgEvent: () => Promise.resolve("processed"), }; const config = new DiscordBridgeConfig(); config.limits.roomGhostJoinDelay = 0; + if (opts.disableSS) { + config.bridge.enableSelfServiceBridging = false; + } else { + config.bridge.enableSelfServiceBridging = true; + } + const mxClient = { + getStateEvent: () => { + return Promise.resolve(opts.powerLevels || {}); + }, + }; const provisioner = null; - return new MatrixRoomHandler(bot as any, config, "@botuser:localhost", provisioner); + const handler = new MatrixRoomHandler(bot as any, config, "@botuser:localhost", provisioner); + handler.setBridge({ + getIntent: () => { return { + sendMessage: (roomId, content) => Promise.resolve(content), + getClient: () => mxClient, + }; }, + }); + return handler; } describe("MatrixRoomHandler", () => { describe("OnAliasQueried", () => { it("should join successfully", () => { const handler = createRH(); - return handler.OnAliasQueried("#accept:localhost", "!accept:localhost") - .then(() => { - // test for something - return true; - }); + return expect(handler.OnAliasQueried("#accept:localhost", "!accept:localhost")).to.be.fulfilled; }); it("should join successfully and create ghosts", () => { const EXPECTEDUSERS = 2; + const TESTDELAY = 50; const handler = createRH({createMembers: true}); - return handler.OnAliasQueried("#accept:localhost", "!accept:localhost") - .then(() => { + return handler.OnAliasQueried("#accept:localhost", "!accept:localhost").then(() => { + return Bluebird.delay(TESTDELAY); + }).then(() => { expect(USERSJOINED).to.equal(EXPECTEDUSERS); // test for something return true; - }); + }); + }); + it("should not join successfully", () => { + const handler = createRH(); + return expect(handler.OnAliasQueried("#reject:localhost", "!reject:localhost")).to.be.rejected; + }); + }); + describe("OnEvent", () => { + it("should reject old events", () => { + const AGE = 900001; // 15 * 60 * 1000 + const handler = createRH(); + return expect(handler.OnEvent( + buildRequest({unsigned: {age: AGE}}), null)) + .to.be.rejectedWith("Event too old"); + }); + it("should reject un-processable events", () => { + const AGE = 900000; // 15 * 60 * 1000 + const handler = createRH(); + return expect(handler.OnEvent(buildRequest({ + content: {}, + type: "m.potato", + unsigned: {age: AGE}}), null)).to.be.rejectedWith("Event not processed by bridge"); + }); + it("should handle invites", () => { + const handler = createRH(); + handler.HandleInvite = (ev) => Promise.resolve("invited"); + return expect(handler.OnEvent(buildRequest({ + content: {membership: "invite"}, + type: "m.room.member"}), null)).to.eventually.equal("invited"); + }); + it("should ignore other member types", () => { + const handler = createRH(); + handler.HandleInvite = (ev) => Promise.resolve("invited"); + return expect(handler.OnEvent(buildRequest({ + content: {membership: "join"}, + type: "m.room.member"}), null)).to.be.rejectedWith("Event not processed by bridge"); + }); + it("should handle redactions with existing rooms", () => { + const handler = createRH(); + const context = { + rooms: { + remote: true, + }, + }; + return expect(handler.OnEvent(buildRequest({ + type: "m.room.redaction"}), context)).to.eventually.equal("redacted"); + }); + it("should ignore redactions with no linked room", () => { + const handler = createRH(); + const context = { + rooms: { + remote: null, + }, + }; + return expect(handler.OnEvent(buildRequest({ + type: "m.room.redaction"}), context)).to.be.rejectedWith("Event not processed by bridge"); + }); + it("should process regular messages", () => { + const handler = createRH(); + const context = { + rooms: { + remote: { + roomId: "_discord_123_456", + }, + }, + }; + return expect(handler.OnEvent(buildRequest({ + type: "m.room.message", content: {body: "abc"}}), context)).to.eventually.equal("processed"); + }); + it("should process !discord commands", () => { + const handler = createRH(); + handler.ProcessCommand = (ev) => Promise.resolve("processedcmd"); + return expect(handler.OnEvent(buildRequest({ + type: "m.room.message", content: {body: "!discord cmd"}}), null)) + .to.eventually.equal("processedcmd"); + }); + it("should ignore regular messages with no linked room", () => { + const handler = createRH(); + const context = { + rooms: { + remote: null, + }, + }; + return expect(handler.OnEvent(buildRequest({ + type: "m.room.message", content: {body: "abc"}}), context)) + .to.be.rejectedWith("Event not processed by bridge"); + }); + }); + describe("HandleInvite", () => { + it("should accept invite for bot user", () => { + const handler: any = createRH(); + handler.joinRoom = () => Promise.resolve("joinedroom"); + return expect(handler.HandleInvite({ + state_key: "@botuser:localhost", + })).to.eventually.be.equal("joinedroom"); + }); + it("should deny invite for other users", () => { + const handler: any = createRH(); + handler.joinRoom = () => Promise.resolve("joinedroom"); + return expect(handler.HandleInvite({ + state_key: "@user:localhost", + })).to.be.undefined; + }); + }); + describe("ProcessCommand", () => { + it("should warn if self service is disabled", () => { + const handler: any = createRH({disableSS: true}); + return expect(handler.ProcessCommand({ + room_id: "!123:localhost", + })).to.eventually.be.deep.equal({ + msgtype: "m.notice", + body: "The owner of this bridge does not permit self-service bridging.", + }); + }); + it("should warn if user is not powerful enough with defaults", () => { + const handler: any = createRH(); + return expect(handler.ProcessCommand({ + room_id: "!123:localhost", + })).to.eventually.be.deep.equal({ + msgtype: "m.notice", + body: "You do not have the required power level in this room to create a bridge to a Discord channel.", + }); + }); + it("should warn if user is not powerful enough with custom state default", () => { + const handler: any = createRH({powerLevels: { + state_default: 67, + }}); + return expect(handler.ProcessCommand({ + room_id: "!123:localhost", + })).to.eventually.be.deep.equal({ + msgtype: "m.notice", + body: "You do not have the required power level in this room to create a bridge to a Discord channel.", + }); + }); + it("should allow if user is powerful enough with defaults", () => { + const handler: any = createRH({powerLevels: { + users_default: 60, + }}); + return handler.ProcessCommand({ + room_id: "!123:localhost", + content: {body: "!discord help"}, + }).then((evt) => { + console.log(evt); + return expect(evt.body.startsWith("Available commands")).to.be.true; + }); + }); + it("should allow if user is powerful enough with their own state", () => { + const handler: any = createRH({powerLevels: { + users: { + "@user:localhost": 100, + }, + }}); + return handler.ProcessCommand({ + room_id: "!123:localhost", + sender: "@user:localhost", + content: {body: "!discord help"}, + }).then((evt) => { + return expect(evt.body.startsWith("Available commands")).to.be.true; + }); + }); + it("will not bridge if a link already exists", () => { + const handler: any = createRH({powerLevels: { + users_default: 100, + }}); + const context = {rooms: { remote: true }}; + return handler.ProcessCommand({ + room_id: "!123:localhost", + content: {body: "!discord bridge"}, + }, context).then((evt) => { + return expect(evt.body.startsWith("This room is already bridged to a Discord guild")).to.be.true; + }); }); }); });