diff --git a/src/matrixcommandhandler.ts b/src/matrixcommandhandler.ts index 66ddabb9fbf6853663b5ec354e1688fcc91e2274..9402a9ea3098b60d047eceefb111cc66aedd09b0 100644 --- a/src/matrixcommandhandler.ts +++ b/src/matrixcommandhandler.ts @@ -42,8 +42,6 @@ export class MatrixCommandHandler { return; } - const {command, args} = Util.MsgToArgs(event.content!.body as string, "!discord"); - const actions: ICommandActions = { bridge: { description: "Bridges this room to a Discord channel", @@ -58,7 +56,7 @@ export class MatrixCommandHandler { " The URL is formatted as https://discordapp.com/channels/GUILD_ID/CHANNEL_ID\n" + "5. Enjoy your new bridge!", // tslint:enable prefer-template - params: ["guildid", "channelId"], + params: ["guildId", "channelId"], permission: { cat: "events", level: PROVISIONING_DEFAULT_POWER_LEVEL, @@ -69,7 +67,7 @@ export class MatrixCommandHandler { // TODO: parse guildId/channelId if (context.rooms.remote) { - return "This room is already bridged to a Discord guild"; + return "This room is already bridged to a Discord guild."; } if (!guildId || !channelId) { return "Invalid syntax. For more information try `!discord help bridge`"; @@ -129,18 +127,38 @@ export class MatrixCommandHandler { }, }; + /* + we hack togeather that "guildId/channelId" is the same as "guildId channelId" + we do this by assuming that guildId is parsed first, and split the / off and then pass + that on to channelId, if applicable + */ + let guildIdRemainder: string | undefined = undefined; const parameters: ICommandParameters = { guildId: { description: "The ID of a guild/server on discord", + get: async (s) => { + if (!s) { + return s; + } + const parts = s.split("/"); + guildIdRemainder = parts[1]; + return parts[0]; + }, }, channelId: { description: "The ID of a channel on discord", + get: async (s) => { + if (!s && guildIdRemainder) { + return guildIdRemainder; + } + return s; + }, }, }; const permissionCheck: ICommandPermissonCheck = async (permission) => { if (permission.selfService && !this.config.bridge.enableSelfServiceBridging) { - return false; + return "The owner of this bridge does not permit self-service bridging."; } return await Util.CheckMatrixPermission( this.bridge.getIntent().getClient(), @@ -152,16 +170,6 @@ export class MatrixCommandHandler { ); }; - - - if (!this.config.bridge.enableSelfServiceBridging) { - // We can do this here because the only commands we support are self-service bridging - return this.bridge.getIntent().sendMessage(event.room_id, { - body: "The owner of this bridge does not permit self-service bridging.", - msgtype: "m.notice", - }); - } - const reply = await Util.ParseCommand("!discord", event.content!.body!, actions, parameters, permissionCheck); await this.bridge.getIntent().sendMessage(event.room_id, { diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts index 3d54c2d5641aa0ab9f22262ed30a3ef3e4014db1..f71d7debdce3c15efe449f2208264a2efa44a004 100644 --- a/src/matrixtypes.ts +++ b/src/matrixtypes.ts @@ -42,6 +42,7 @@ export interface IMatrixEvent { unsigned?: any; // tslint:disable-line no-any origin_server_ts?: number; users?: any; // tslint:disable-line no-any + users_default?: any // tslint:disable-line no-any notifications?: any; // tslint:disable-line no-any } diff --git a/src/util.ts b/src/util.ts index c5ed597bc17ded67814efee8dda3e0cfbc006fc1..8df0d180186d03a927dad2ba02208dd65a8422d5 100644 --- a/src/util.ts +++ b/src/util.ts @@ -53,7 +53,7 @@ export interface ICommandParameters { } export interface ICommandPermissonCheck { - (permission: PERMISSIONTYPES): Promise<boolean>; + (permission: PERMISSIONTYPES): Promise<boolean | string>; } export interface IPatternMap { @@ -251,8 +251,14 @@ export class Util { if (!actions[actionKey]) { return `**ERROR:** unknown command! Try \`${prefix} help\` to see all commands`; } - if (action.permission !== undefined && permissionCheck && !(await permissionCheck(action.permission))) { - return `**ERROR:** permission denied! Try \`${prefix} help\` to see all available commands`; + if (action.permission !== undefined && permissionCheck) { + const permCheck = await permissionCheck(action.permission); + if (typeof permCheck === "string") { + return `**ERROR:** ${permCheck}`; + } + if (!permCheck) { + return `**ERROR:** permission denied! Try \`${prefix} help\` to see all available commands`; + } } reply += `\`${prefix} ${actionKey}`; for (const param of action.params) { @@ -267,8 +273,11 @@ export class Util { reply += "Available Commands:\n"; for (const actionKey of Object.keys(actions)) { const action = actions[actionKey]; - if (action.permission !== undefined && permissionCheck && !(await permissionCheck(action.permission))) { - continue; + if (action.permission !== undefined && permissionCheck) { + const permCheck = await permissionCheck(action.permission); + if (typeof permCheck === "string" || !permCheck) { + continue; + } } reply += ` - \`${prefix} ${actionKey}`; for (const param of action.params) { @@ -301,8 +310,14 @@ export class Util { return `**ERROR:** unknown command. Try \`${prefix} help\` to see all commands`; } const action = actions[command]; - if (action.permission !== undefined && permissionCheck && !permissionCheck(action.permission)) { - return `**ERROR:** insufficiant permissions to use this command`; + if (action.permission !== undefined && permissionCheck) { + const permCheck = await permissionCheck(action.permission); + if (typeof permCheck === "string") { + return `**ERROR:** ${permCheck}`; + } + if (!permCheck) { + return `**ERROR:** insufficiant permissions to use this command! Try \`${prefix} help\` to see all available commands`; + } } if (action.params.length === 1) { args[0] = args.join(" "); @@ -392,6 +407,9 @@ export class Util { } let haveLevel = 0; + if (res && res.users_default) { + haveLevel = res.users_default; + } if (res && res.users && res.users[userId] !== undefined) { haveLevel = res.users[userId]; } diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index c50c76f502d4310e552499025979f66c3287777a..6cf8e29f9642534d52a1121cddad377f4dc148e8 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -175,8 +175,8 @@ describe("DiscordBot", () => { discord.channelSync = { GetRoomIdsFromChannel: async (chan) => ["!asdf:localhost"], }; - discord.roomHandler = { - HandleDiscordCommand: async (msg) => { HANDLE_COMMAND = true; }, + discord.discordCommandHandler = { + Process: async (msg) => { HANDLE_COMMAND = true; }, }; discord.store = { Insert: async (_) => { }, diff --git a/test/test_matrixcommandhandler.ts b/test/test_matrixcommandhandler.ts index 78f4b13fab38a350fa2e93ab1a71cd3780a513d9..3468a4b3a69df899609f97b80d319db940a500ae 100644 --- a/test/test_matrixcommandhandler.ts +++ b/test/test_matrixcommandhandler.ts @@ -1,7 +1,8 @@ import * as Chai from "chai"; -import { MatrixCommandHandler } from "../src/matrixcommandhandler"; +import { Util } from "../src/util"; import { DiscordBridgeConfig } from "../src/config"; import { MockChannel } from "./mocks/channel"; +import * as Proxyquire from "proxyquire"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -52,9 +53,6 @@ function createCH(opts: any = {}) { config.bridge.enableSelfServiceBridging = true; } const mxClient = { - getStateEvent: async () => { - return opts.powerLevels || {}; - }, getUserId: () => "@user:localhost", joinRoom: async () => { USERSJOINED++; @@ -92,210 +90,119 @@ function createCH(opts: any = {}) { }, Provisioner: provisioner, }; - return new MatrixCommandHandler(bot as any, bridge, config); + + const MatrixCommandHndl = (Proxyquire("../src/matrixcommandhandler", { + "./util": { + Util: { + CheckMatrixPermission: async () => { + return opts.power !== undefined ? opts.power : true; + }, + GetBotLink: Util.GetBotLink, + ParseCommand: Util.ParseCommand, + } + } + })).MatrixCommandHandler; + return new MatrixCommandHndl(bot as any, bridge, config); +} + +function createEvent(msg: string, room?: string, user_id?: string) { + return { + content: { + body: msg, + }, + room_id: room ? room : "!123:localhost", + sender: user_id, + }; +} + +function createContext(remoteData?: any) { + return { + rooms: { + remote: remoteData, + }, + }; } describe("MatrixCommandHandler", () => { describe("Process", () => { it("should not process command if not in room", async () => { const handler: any = createCH({disableSS: true}); - const ret = await handler.Process({ - room_id: "!666:localhost", - }); - expect(ret).to.be.undefined; + await handler.Process(createEvent("", "!666:localhost"), createContext()); + expect(MESSAGESENT.body).to.equal(undefined); }); it("should warn if self service is disabled", async () => { const handler: any = createCH({disableSS: true}); - await handler.Process({ - room_id: "!123:localhost", - }); - expect(MESSAGESENT.body).equals("The owner of this bridge does not permit self-service bridging."); - }); - it("should warn if user is not powerful enough with defaults", async () => { - const handler: any = createCH(); - await handler.Process({ - room_id: "!123:localhost", - }); - expect(MESSAGESENT.body).equals("You do not have the required power level in this room to " + - "create a bridge to a Discord channel."); + await handler.Process(createEvent("!discord bridge"), createContext()); + expect(MESSAGESENT.body).to.equal("**ERROR:** The owner of this bridge does not permit self-service bridging."); }); - it("should warn if user is not powerful enough with custom state default", async () => { - const handler: any = createCH({powerLevels: { - state_default: 67, - }}); - await handler.Process({ - room_id: "!123:localhost", - }); - expect(MESSAGESENT.body).equals("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", async () => { - const handler: any = createCH({powerLevels: { - users_default: 60, - }}); - const evt = await handler.Process({ - content: {body: "!discord help"}, - room_id: "!123:localhost", - }); - expect(evt.body.startsWith("Available commands")).to.be.true; - }); - it("should allow if user is powerful enough with their own state", async () => { - const handler: any = createCH({powerLevels: { - users: { - "@user:localhost": 100, - }, - }}); - const evt = await handler.Process({ - content: {body: "!discord help"}, - room_id: "!123:localhost", - sender: "@user:localhost", + it("should warn if user is not powerful enough", async () => { + const handler: any = createCH({ + power: false, }); - expect(evt.body.startsWith("Available commands")).to.be.true; + await handler.Process(createEvent("!discord bridge"), createContext()); + expect(MESSAGESENT.body).to.equal("**ERROR:** insufficiant permissions to use this command! Try `!discord help` to see all available commands"); }); describe("!discord bridge", () => { it("will bridge a new room, and ask for permissions", async () => { - const handler: any = createCH({powerLevels: { - users_default: 100, - }}); - const context = {rooms: {}}; - const evt = await handler.Process({ - content: {body: "!discord bridge 123 456"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("I have bridged this room to your channel"); + const handler: any = createCH(); + await handler.Process(createEvent("!discord bridge 123 456"), createContext()); + expect(MESSAGESENT.body).to.equal("I have bridged this room to your channel"); }); it("will fail to bridge if permissions were denied", async () => { const handler: any = createCH({ denyBridgePermission: true, - powerLevels: { - users_default: 100, - }, }); - const context = {rooms: {}}; - const evt = await handler.Process({ - content: {body: "!discord bridge 123 456"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("The bridge has been declined by the Discord guild"); + await handler.Process(createEvent("!discord bridge 123 456"), createContext()); + expect(MESSAGESENT.body).to.equal("The bridge has been declined by the Discord guild"); }); - it("will fail to bridge if permissions were denied", async () => { + it("will fail to bridge if permissions were failed", async () => { const handler: any = createCH({ failBridgeMatrix: true, - powerLevels: { - users_default: 100, - }, }); - const context = {rooms: {}}; - const evt = await handler.Process({ - content: {body: "!discord bridge 123 456"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("There was a problem bridging that channel - has " + + const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext()); + expect(MESSAGESENT.body).to.equal("There was a problem bridging that channel - has " + "the guild owner approved the bridge?"); }); it("will not bridge if a link already exists", async () => { - const handler: any = createCH({ - powerLevels: { - users_default: 100, - }, - }); - const context = {rooms: { remote: true }}; - const evt = await handler.Process({ - content: {body: "!discord bridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("This room is already bridged to a Discord guild."); + const handler: any = createCH(); + const evt = await handler.Process(createEvent("!discord bridge 123 456"), createContext(true)); + expect(MESSAGESENT.body).to.equal("This room is already bridged to a Discord guild."); }); it("will not bridge without required args", async () => { - const handler: any = createCH({ - powerLevels: { - users_default: 100, - }, - }); - const context = {rooms: {}}; - const evt = await handler.Process({ - content: {body: "!discord bridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).to.contain("Invalid syntax"); + const handler: any = createCH(); + const evt = await handler.Process(createEvent("!discord bridge"), createContext()); + expect(MESSAGESENT.body).to.contain("Invalid syntax"); }); it("will bridge with x/y syntax", async () => { const handler: any = createCH({powerLevels: { users_default: 100, }}); - const context = {rooms: {}}; - const evt = await handler.Process({ - content: {body: "!discord bridge 123/456"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("I have bridged this room to your channel"); + const evt = await handler.Process(createEvent("!discord bridge 123/456"), createContext()); + expect(MESSAGESENT.body).equals("I have bridged this room to your channel"); }); }); describe("!discord unbridge", () => { it("will unbridge", async () => { - const handler: any = createCH({ - powerLevels: { - users_default: 100, - }, - }); - const context = {rooms: { remote: { - data: { - plumbed: true, - }, - } }}; - const evt = await handler.Process({ - content: {body: "!discord unbridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("This room has been unbridged"); + const handler: any = createCH(); + await handler.Process(createEvent("!discord unbridge"), createContext({data:{plumbed:true}})); + expect(MESSAGESENT.body).equals("This room has been unbridged"); }); it("will not unbridge if a link does not exist", async () => { - const handler: any = createCH({ - powerLevels: { - users_default: 100, - }, - }); - const context = {rooms: { remote: undefined }}; - const evt = await handler.Process({ - content: {body: "!discord unbridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("This room is not bridged."); + const handler: any = createCH(); + const evt = await handler.Process(createEvent("!discord unbridge"), createContext()); + expect(MESSAGESENT.body).equals("This room is not bridged."); }); it("will not unbridge non-plumbed rooms", async () => { - const handler: any = createCH({ - powerLevels: { - users_default: 100, - }, - }); - const context = {rooms: { remote: { - data: { - plumbed: false, - }, - }}}; - const evt = await handler.Process({ - content: {body: "!discord unbridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).equals("This room cannot be unbridged."); + const handler: any = createCH(); + await handler.Process(createEvent("!discord unbridge"), createContext({data:{plumbed:false}})); + expect(MESSAGESENT.body).equals("This room cannot be unbridged."); }); it("will show error if unbridge fails", async () => { const handler: any = createCH({ failUnbridge: true, - powerLevels: { - users_default: 100, - }, }); - const context = {rooms: { remote: { - data: { - plumbed: true, - }, - }}}; - const evt = await handler.Process({ - content: {body: "!discord unbridge"}, - room_id: "!123:localhost", - }, context); - expect(evt.body).to.contain("There was an error unbridging this room."); + await handler.Process(createEvent("!discord unbridge"), createContext({data:{plumbed:true}})); + expect(MESSAGESENT.body).to.contain("There was an error unbridging this room."); }); }); }); diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index 71ec84bf415758f4abb0e4b1e8a7f234c261802f..eefa16106ec3cb60dc45886d4726299950a33e08 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -89,12 +89,13 @@ let STATE_EVENT_MSG = ""; let USERSYNC_HANDLED = false; let MESSAGE_PROCCESS = ""; let KICKBAN_HANDLED = false; +let COMMAND_PROCESSED = false; function createMatrixEventProcessor(): MatrixEventProcessor { USERSYNC_HANDLED = false; STATE_EVENT_MSG = ""; MESSAGE_PROCCESS = ""; - KICKBAN_HANDLED = true; + KICKBAN_HANDLED = false; const bridge = { getBot: () => { return { @@ -230,7 +231,7 @@ function createMatrixEventProcessor(): MatrixEventProcessor { HandleInvite: async (evt) => { MESSAGE_PROCCESS = "invited"; }, - ProcessCommand: async (evt) => { + Process: async (evt) => { MESSAGE_PROCCESS = "command_processed"; }, }); diff --git a/test/test_util.ts b/test/test_util.ts index 17664269950452f36138dacec1f8ad3e0ce13a97..70c8d7b57a7c8572d45155ce9c114f4e18973c4a 100644 --- a/test/test_util.ts +++ b/test/test_util.ts @@ -16,7 +16,7 @@ limitations under the License. import * as Chai from "chai"; -import { Util, ICommandAction, ICommandParameters } from "../src/util"; +import { Util, ICommandActions, ICommandParameters } from "../src/util"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -62,10 +62,12 @@ describe("Util", () => { }); describe("ParseCommand", () => { it("parses commands", async () => { - const action: ICommandAction = { - params: ["param1", "param2"], - run: async ({param1, param2}) => { - return `param1: ${param1}\nparam2: ${param2}`; + const actions: ICommandActions = { + action: { + params: ["param1", "param2"], + run: async ({param1, param2}) => { + return `param1: ${param1}\nparam2: ${param2}`; + }, }, }; const parameters: ICommandParameters = { @@ -80,7 +82,12 @@ describe("Util", () => { }, }, }; - const retStr = await Util.ParseCommand(action, parameters, ["hello", "world"]); + const retStr = await Util.ParseCommand( + "!fox", + "!fox action hello world", + actions, + parameters, + ); expect(retStr).equal("param1: param1_hello\nparam2: param2_world"); }); });