Refactor SlashCommands to not use MatrixClientPeg (#10905)

This commit is contained in:
Michael Telatynski 2023-05-25 16:29:48 +01:00 committed by GitHub
parent 192e6f6c3d
commit 796ed35e75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 244 additions and 170 deletions

View file

@ -26,8 +26,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IContent } from "matrix-js-sdk/src/models/event"; import { IContent } from "matrix-js-sdk/src/models/event";
import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic"; import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic";
import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand"; import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "./MatrixClientPeg";
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import { _t, _td, UserFriendlyError } from "./languageHandler"; import { _t, _td, UserFriendlyError } from "./languageHandler";
import Modal from "./Modal"; import Modal from "./Modal";
@ -75,7 +75,7 @@ interface HTMLInputEvent extends Event {
target: HTMLInputElement & EventTarget; target: HTMLInputElement & EventTarget;
} }
const singleMxcUpload = async (): Promise<string | null> => { const singleMxcUpload = async (cli: MatrixClient): Promise<string | null> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const fileSelector = document.createElement("input"); const fileSelector = document.createElement("input");
fileSelector.setAttribute("type", "file"); fileSelector.setAttribute("type", "file");
@ -87,7 +87,7 @@ const singleMxcUpload = async (): Promise<string | null> => {
file, file,
onFinished: async (shouldContinue): Promise<void> => { onFinished: async (shouldContinue): Promise<void> => {
if (shouldContinue) { if (shouldContinue) {
const { content_uri: uri } = await MatrixClientPeg.get().uploadContent(file); const { content_uri: uri } = await cli.uploadContent(file);
resolve(uri); resolve(uri);
} else { } else {
resolve(null); resolve(null);
@ -111,7 +111,7 @@ export const CommandCategories = {
export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>; export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>;
type RunFn = (this: Command, roomId: string, args?: string) => RunResult; type RunFn = (this: Command, matrixClient: MatrixClient, roomId: string, args?: string) => RunResult;
interface ICommandOpts { interface ICommandOpts {
command: string; command: string;
@ -122,7 +122,7 @@ interface ICommandOpts {
runFn?: RunFn; runFn?: RunFn;
category: string; category: string;
hideCompletionAfterSpace?: boolean; hideCompletionAfterSpace?: boolean;
isEnabled?(): boolean; isEnabled?(matrixClient?: MatrixClient): boolean;
renderingTypes?: TimelineRenderingType[]; renderingTypes?: TimelineRenderingType[];
} }
@ -136,7 +136,7 @@ export class Command {
public readonly hideCompletionAfterSpace: boolean; public readonly hideCompletionAfterSpace: boolean;
public readonly renderingTypes?: TimelineRenderingType[]; public readonly renderingTypes?: TimelineRenderingType[];
public readonly analyticsName?: SlashCommandEvent["command"]; public readonly analyticsName?: SlashCommandEvent["command"];
private readonly _isEnabled?: () => boolean; private readonly _isEnabled?: (matrixClient?: MatrixClient) => boolean;
public constructor(opts: ICommandOpts) { public constructor(opts: ICommandOpts) {
this.command = opts.command; this.command = opts.command;
@ -159,7 +159,7 @@ export class Command {
return this.getCommand() + " " + this.args; return this.getCommand() + " " + this.args;
} }
public run(roomId: string, threadId: string | null, args?: string): RunResult { public run(matrixClient: MatrixClient, roomId: string, threadId: string | null, args?: string): RunResult {
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me` // if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
if (!this.runFn) { if (!this.runFn) {
return reject(new UserFriendlyError("Command error: Unable to handle slash command.")); return reject(new UserFriendlyError("Command error: Unable to handle slash command."));
@ -182,15 +182,15 @@ export class Command {
}); });
} }
return this.runFn(roomId, args); return this.runFn(matrixClient, roomId, args);
} }
public getUsage(): string { public getUsage(): string {
return _t("Usage") + ": " + this.getCommandWithArgs(); return _t("Usage") + ": " + this.getCommandWithArgs();
} }
public isEnabled(): boolean { public isEnabled(cli?: MatrixClient): boolean {
return this._isEnabled ? this._isEnabled() : true; return this._isEnabled?.(cli) ?? true;
} }
} }
@ -206,15 +206,21 @@ function successSync(value: any): RunResult {
return success(Promise.resolve(value)); return success(Promise.resolve(value));
} }
const isCurrentLocalRoom = (): boolean => { const isCurrentLocalRoom = (cli?: MatrixClient): boolean => {
const cli = MatrixClientPeg.get();
const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
if (!roomId) return false; if (!roomId) return false;
const room = cli.getRoom(roomId); const room = cli?.getRoom(roomId);
if (!room) return false; if (!room) return false;
return isLocalRoom(room); return isLocalRoom(room);
}; };
const canAffectPowerlevels = (cli?: MatrixClient): boolean => {
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
if (!cli || !roomId) return false;
const room = cli?.getRoom(roomId);
return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room);
};
/* Disable the "unexpected this" error for these commands - all of the run /* Disable the "unexpected this" error for these commands - all of the run
* functions are called with `this` bound to the Command instance. * functions are called with `this` bound to the Command instance.
*/ */
@ -224,7 +230,7 @@ export const Commands = [
command: "spoiler", command: "spoiler",
args: "<message>", args: "<message>",
description: _td("Sends the given message as a spoiler"), description: _td("Sends the given message as a spoiler"),
runFn: function (roomId, message = "") { runFn: function (cli, roomId, message = "") {
return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`)); return successSync(ContentHelpers.makeHtmlMessage(message, `<span data-mx-spoiler>${message}</span>`));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -233,7 +239,7 @@ export const Commands = [
command: "shrug", command: "shrug",
args: "<message>", args: "<message>",
description: _td("Prepends ¯\\_(ツ)_/¯ to a plain-text message"), description: _td("Prepends ¯\\_(ツ)_/¯ to a plain-text message"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let message = "¯\\_(ツ)_/¯"; let message = "¯\\_(ツ)_/¯";
if (args) { if (args) {
message = message + " " + args; message = message + " " + args;
@ -246,7 +252,7 @@ export const Commands = [
command: "tableflip", command: "tableflip",
args: "<message>", args: "<message>",
description: _td("Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"), description: _td("Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let message = "(╯°□°)╯︵ ┻━┻"; let message = "(╯°□°)╯︵ ┻━┻";
if (args) { if (args) {
message = message + " " + args; message = message + " " + args;
@ -259,7 +265,7 @@ export const Commands = [
command: "unflip", command: "unflip",
args: "<message>", args: "<message>",
description: _td("Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message"), description: _td("Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let message = "┬──┬ ( ゜-゜ノ)"; let message = "┬──┬ ( ゜-゜ノ)";
if (args) { if (args) {
message = message + " " + args; message = message + " " + args;
@ -272,7 +278,7 @@ export const Commands = [
command: "lenny", command: "lenny",
args: "<message>", args: "<message>",
description: _td("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"), description: _td("Prepends ( ͡° ͜ʖ ͡°) to a plain-text message"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let message = "( ͡° ͜ʖ ͡°)"; let message = "( ͡° ͜ʖ ͡°)";
if (args) { if (args) {
message = message + " " + args; message = message + " " + args;
@ -285,7 +291,7 @@ export const Commands = [
command: "plain", command: "plain",
args: "<message>", args: "<message>",
description: _td("Sends a message as plain text, without interpreting it as markdown"), description: _td("Sends a message as plain text, without interpreting it as markdown"),
runFn: function (roomId, messages = "") { runFn: function (cli, roomId, messages = "") {
return successSync(ContentHelpers.makeTextMessage(messages)); return successSync(ContentHelpers.makeTextMessage(messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -294,7 +300,7 @@ export const Commands = [
command: "html", command: "html",
args: "<message>", args: "<message>",
description: _td("Sends a message as html, without interpreting it as markdown"), description: _td("Sends a message as html, without interpreting it as markdown"),
runFn: function (roomId, messages = "") { runFn: function (cli, roomId, messages = "") {
return successSync(ContentHelpers.makeHtmlMessage(messages, messages)); return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
@ -303,10 +309,9 @@ export const Commands = [
command: "upgraderoom", command: "upgraderoom",
args: "<new_version>", args: "<new_version>",
description: _td("Upgrades a room to a new version"), description: _td("Upgrades a room to a new version"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) { if (!room?.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) {
return reject( return reject(
@ -339,7 +344,7 @@ export const Commands = [
args: "<YYYY-MM-DD>", args: "<YYYY-MM-DD>",
description: _td("Jump to the given date in the timeline"), description: _td("Jump to the given date in the timeline"),
isEnabled: () => SettingsStore.getValue("feature_jump_to_date"), isEnabled: () => SettingsStore.getValue("feature_jump_to_date"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
return success( return success(
(async (): Promise<void> => { (async (): Promise<void> => {
@ -352,7 +357,6 @@ export const Commands = [
); );
} }
const cli = MatrixClientPeg.get();
const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent( const { event_id: eventId, origin_server_ts: originServerTs } = await cli.timestampToEvent(
roomId, roomId,
unixTimestamp, unixTimestamp,
@ -381,9 +385,9 @@ export const Commands = [
command: "nick", command: "nick",
args: "<display_name>", args: "<display_name>",
description: _td("Changes your display nickname"), description: _td("Changes your display nickname"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
return success(MatrixClientPeg.get().setDisplayName(args)); return success(cli.setDisplayName(args));
} }
return reject(this.getUsage()); return reject(this.getUsage());
}, },
@ -395,16 +399,15 @@ export const Commands = [
aliases: ["roomnick"], aliases: ["roomnick"],
args: "<display_name>", args: "<display_name>",
description: _td("Changes your display nickname in the current room only"), description: _td("Changes your display nickname in the current room only"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const cli = MatrixClientPeg.get(); const ev = cli.getRoom(roomId)?.currentState.getStateEvents("m.room.member", cli.getSafeUserId());
const ev = cli.getRoom(roomId)?.currentState.getStateEvents("m.room.member", cli.getUserId()!);
const content = { const content = {
...(ev ? ev.getContent() : { membership: "join" }), ...(ev ? ev.getContent() : { membership: "join" }),
displayname: args, displayname: args,
}; };
return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getUserId()!)); return success(cli.sendStateEvent(roomId, "m.room.member", content, cli.getSafeUserId()));
} }
return reject(this.getUsage()); return reject(this.getUsage());
}, },
@ -415,17 +418,17 @@ export const Commands = [
command: "roomavatar", command: "roomavatar",
args: "[<mxc_url>]", args: "[<mxc_url>]",
description: _td("Changes the avatar of the current room"), description: _td("Changes the avatar of the current room"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let promise = Promise.resolve(args ?? null); let promise = Promise.resolve(args ?? null);
if (!args) { if (!args) {
promise = singleMxcUpload(); promise = singleMxcUpload(cli);
} }
return success( return success(
promise.then((url) => { promise.then((url) => {
if (!url) return; if (!url) return;
return MatrixClientPeg.get().sendStateEvent(roomId, "m.room.avatar", { url }, ""); return cli.sendStateEvent(roomId, "m.room.avatar", { url }, "");
}), }),
); );
}, },
@ -436,15 +439,14 @@ export const Commands = [
command: "myroomavatar", command: "myroomavatar",
args: "[<mxc_url>]", args: "[<mxc_url>]",
description: _td("Changes your avatar in this current room only"), description: _td("Changes your avatar in this current room only"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
const userId = cli.getUserId()!; const userId = cli.getSafeUserId();
let promise = Promise.resolve(args ?? null); let promise = Promise.resolve(args ?? null);
if (!args) { if (!args) {
promise = singleMxcUpload(); promise = singleMxcUpload(cli);
} }
return success( return success(
@ -466,16 +468,16 @@ export const Commands = [
command: "myavatar", command: "myavatar",
args: "[<mxc_url>]", args: "[<mxc_url>]",
description: _td("Changes your avatar in all rooms"), description: _td("Changes your avatar in all rooms"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let promise = Promise.resolve(args ?? null); let promise = Promise.resolve(args ?? null);
if (!args) { if (!args) {
promise = singleMxcUpload(); promise = singleMxcUpload(cli);
} }
return success( return success(
promise.then((url) => { promise.then((url) => {
if (!url) return; if (!url) return;
return MatrixClientPeg.get().setAvatarUrl(url); return cli.setAvatarUrl(url);
}), }),
); );
}, },
@ -486,9 +488,8 @@ export const Commands = [
command: "topic", command: "topic",
args: "[<topic>]", args: "[<topic>]",
description: _td("Gets or sets the room topic"), description: _td("Gets or sets the room topic"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const cli = MatrixClientPeg.get();
if (args) { if (args) {
const html = htmlSerializeFromMdIfNeeded(args, { forceHTML: false }); const html = htmlSerializeFromMdIfNeeded(args, { forceHTML: false });
return success(cli.setRoomTopic(roomId, args, html)); return success(cli.setRoomTopic(roomId, args, html));
@ -525,10 +526,10 @@ export const Commands = [
command: "roomname", command: "roomname",
args: "<name>", args: "<name>",
description: _td("Sets the room name"), description: _td("Sets the room name"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
return success(MatrixClientPeg.get().setRoomName(roomId, args)); return success(cli.setRoomName(roomId, args));
} }
return reject(this.getUsage()); return reject(this.getUsage());
}, },
@ -540,8 +541,8 @@ export const Commands = [
args: "<user-id> [<reason>]", args: "<user-id> [<reason>]",
description: _td("Invites user with given id to current room"), description: _td("Invites user with given id to current room"),
analyticsName: "Invite", analyticsName: "Invite",
isEnabled: () => !isCurrentLocalRoom() && shouldShowComponent(UIComponent.InviteUsers), isEnabled: (cli) => !isCurrentLocalRoom(cli) && shouldShowComponent(UIComponent.InviteUsers),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const [address, reason] = args.split(/\s+(.+)/); const [address, reason] = args.split(/\s+(.+)/);
if (address) { if (address) {
@ -551,10 +552,7 @@ export const Commands = [
// get a bit more complex here, but we try to show something // get a bit more complex here, but we try to show something
// meaningful. // meaningful.
let prom = Promise.resolve(); let prom = Promise.resolve();
if ( if (getAddressType(address) === AddressType.Email && !cli.getIdentityServerUrl()) {
getAddressType(address) === AddressType.Email &&
!MatrixClientPeg.get().getIdentityServerUrl()
) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) { if (defaultIdentityServerUrl) {
const { finished } = Modal.createDialog(QuestionDialog, { const { finished } = Modal.createDialog(QuestionDialog, {
@ -576,7 +574,7 @@ export const Commands = [
prom = finished.then(([useDefault]) => { prom = finished.then(([useDefault]) => {
if (useDefault) { if (useDefault) {
setToDefaultIdentityServer(MatrixClientPeg.get()); setToDefaultIdentityServer(cli);
return; return;
} }
throw new UserFriendlyError( throw new UserFriendlyError(
@ -589,7 +587,7 @@ export const Commands = [
); );
} }
} }
const inviter = new MultiInviter(MatrixClientPeg.get(), roomId); const inviter = new MultiInviter(cli, roomId);
return success( return success(
prom prom
.then(() => { .then(() => {
@ -621,7 +619,7 @@ export const Commands = [
aliases: ["j", "goto"], aliases: ["j", "goto"],
args: "<room-address>", args: "<room-address>",
description: _td("Joins room with given address"), description: _td("Joins room with given address"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
// Note: we support 2 versions of this command. The first is // Note: we support 2 versions of this command. The first is
// the public-facing one for most users and the other is a // the public-facing one for most users and the other is a
@ -654,7 +652,7 @@ export const Commands = [
if (params[0][0] === "#") { if (params[0][0] === "#") {
let roomAlias = params[0]; let roomAlias = params[0];
if (!roomAlias.includes(":")) { if (!roomAlias.includes(":")) {
roomAlias += ":" + MatrixClientPeg.get().getDomain(); roomAlias += ":" + cli.getDomain();
} }
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
@ -733,10 +731,8 @@ export const Commands = [
args: "[<room-address>]", args: "[<room-address>]",
description: _td("Leave room"), description: _td("Leave room"),
analyticsName: "Part", analyticsName: "Part",
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const cli = MatrixClientPeg.get();
let targetRoomId: string | undefined; let targetRoomId: string | undefined;
if (args) { if (args) {
const matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
@ -775,12 +771,12 @@ export const Commands = [
aliases: ["kick"], aliases: ["kick"],
args: "<user-id> [reason]", args: "<user-id> [reason]",
description: _td("Removes user with given id from this room"), description: _td("Removes user with given id from this room"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3])); return success(cli.kick(roomId, matches[1], matches[3]));
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());
@ -792,12 +788,12 @@ export const Commands = [
command: "ban", command: "ban",
args: "<user-id> [reason]", args: "<user-id> [reason]",
description: _td("Bans user with given id"), description: _td("Bans user with given id"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+?)( +(.*))?$/); const matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) { if (matches) {
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3])); return success(cli.ban(roomId, matches[1], matches[3]));
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());
@ -809,13 +805,13 @@ export const Commands = [
command: "unban", command: "unban",
args: "<user-id>", args: "<user-id>",
description: _td("Unbans user with given ID"), description: _td("Unbans user with given ID"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
// Reset the user membership to "leave" to unban him // Reset the user membership to "leave" to unban him
return success(MatrixClientPeg.get().unban(roomId, matches[1])); return success(cli.unban(roomId, matches[1]));
} }
} }
return reject(this.getUsage()); return reject(this.getUsage());
@ -827,10 +823,8 @@ export const Commands = [
command: "ignore", command: "ignore",
args: "<user-id>", args: "<user-id>",
description: _td("Ignores a user, hiding their messages from you"), description: _td("Ignores a user, hiding their messages from you"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const cli = MatrixClientPeg.get();
const matches = args.match(/^(@[^:]+:\S+)$/); const matches = args.match(/^(@[^:]+:\S+)$/);
if (matches) { if (matches) {
const userId = matches[1]; const userId = matches[1];
@ -858,10 +852,8 @@ export const Commands = [
command: "unignore", command: "unignore",
args: "<user-id>", args: "<user-id>",
description: _td("Stops ignoring a user, showing their messages going forward"), description: _td("Stops ignoring a user, showing their messages going forward"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const cli = MatrixClientPeg.get();
const matches = args.match(/(^@[^:]+:\S+$)/); const matches = args.match(/(^@[^:]+:\S+$)/);
if (matches) { if (matches) {
const userId = matches[1]; const userId = matches[1];
@ -890,17 +882,8 @@ export const Commands = [
command: "op", command: "op",
args: "<user-id> [<power-level>]", args: "<user-id> [<power-level>]",
description: _td("Define the power level of a user"), description: _td("Define the power level of a user"),
isEnabled(): boolean { isEnabled: canAffectPowerlevels,
const cli = MatrixClientPeg.get(); runFn: function (cli, roomId, args) {
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
if (!roomId) return false;
const room = cli.getRoom(roomId);
return (
!!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()!) &&
!isLocalRoom(room)
);
},
runFn: function (roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+?)( +(-?\d+))?$/); const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
let powerLevel = 50; // default power level for op let powerLevel = 50; // default power level for op
@ -910,7 +893,6 @@ export const Commands = [
powerLevel = parseInt(matches[3], 10); powerLevel = parseInt(matches[3], 10);
} }
if (!isNaN(powerLevel)) { if (!isNaN(powerLevel)) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) { if (!room) {
return reject( return reject(
@ -941,21 +923,11 @@ export const Commands = [
command: "deop", command: "deop",
args: "<user-id>", args: "<user-id>",
description: _td("Deops user with given id"), description: _td("Deops user with given id"),
isEnabled(): boolean { isEnabled: canAffectPowerlevels,
const cli = MatrixClientPeg.get(); runFn: function (cli, roomId, args) {
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
if (!roomId) return false;
const room = cli.getRoom(roomId);
return (
!!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getUserId()!) &&
!isLocalRoom(room)
);
},
runFn: function (roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+)$/); const matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) { if (!room) {
return reject( return reject(
@ -981,7 +953,7 @@ export const Commands = [
new Command({ new Command({
command: "devtools", command: "devtools",
description: _td("Opens the Developer Tools dialog"), description: _td("Opens the Developer Tools dialog"),
runFn: function (roomId) { runFn: function (cli, roomId) {
Modal.createDialog(DevtoolsDialog, { roomId }, "mx_DevtoolsDialog_wrapper"); Modal.createDialog(DevtoolsDialog, { roomId }, "mx_DevtoolsDialog_wrapper");
return success(); return success();
}, },
@ -991,11 +963,11 @@ export const Commands = [
command: "addwidget", command: "addwidget",
args: "<url | embed code | Jitsi url>", args: "<url | embed code | Jitsi url>",
description: _td("Adds a custom widget by URL to the room"), description: _td("Adds a custom widget by URL to the room"),
isEnabled: () => isEnabled: (cli) =>
SettingsStore.getValue(UIFeature.Widgets) && SettingsStore.getValue(UIFeature.Widgets) &&
shouldShowComponent(UIComponent.AddIntegrations) && shouldShowComponent(UIComponent.AddIntegrations) &&
!isCurrentLocalRoom(), !isCurrentLocalRoom(cli),
runFn: function (roomId, widgetUrl) { runFn: function (cli, roomId, widgetUrl) {
if (!widgetUrl) { if (!widgetUrl) {
return reject(new UserFriendlyError("Please supply a widget URL or embed code")); return reject(new UserFriendlyError("Please supply a widget URL or embed code"));
} }
@ -1005,12 +977,12 @@ export const Commands = [
const embed = new DOMParser().parseFromString(widgetUrl, "text/html").body; const embed = new DOMParser().parseFromString(widgetUrl, "text/html").body;
if (embed?.childNodes?.length === 1) { if (embed?.childNodes?.length === 1) {
const iframe = embed.firstElementChild; const iframe = embed.firstElementChild;
if (iframe.tagName.toLowerCase() === "iframe") { if (iframe?.tagName.toLowerCase() === "iframe") {
logger.log("Pulling URL out of iframe (embed code)"); logger.log("Pulling URL out of iframe (embed code)");
if (!iframe.hasAttribute("src")) { if (!iframe.hasAttribute("src")) {
return reject(new UserFriendlyError("iframe has no src attribute")); return reject(new UserFriendlyError("iframe has no src attribute"));
} }
widgetUrl = iframe.getAttribute("src"); widgetUrl = iframe.getAttribute("src")!;
} }
} }
} }
@ -1018,8 +990,8 @@ export const Commands = [
if (!widgetUrl.startsWith("https://") && !widgetUrl.startsWith("http://")) { if (!widgetUrl.startsWith("https://") && !widgetUrl.startsWith("http://")) {
return reject(new UserFriendlyError("Please supply a https:// or http:// widget URL")); return reject(new UserFriendlyError("Please supply a https:// or http:// widget URL"));
} }
if (WidgetUtils.canUserModifyWidgets(MatrixClientPeg.get(), roomId)) { if (WidgetUtils.canUserModifyWidgets(cli, roomId)) {
const userId = MatrixClientPeg.get().getUserId(); const userId = cli.getUserId();
const nowMs = new Date().getTime(); const nowMs = new Date().getTime();
const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`); const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`);
let type = WidgetType.CUSTOM; let type = WidgetType.CUSTOM;
@ -1036,9 +1008,7 @@ export const Commands = [
widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();
} }
return success( return success(WidgetUtils.setRoomWidget(cli, roomId, widgetId, type, widgetUrl, name, data));
WidgetUtils.setRoomWidget(MatrixClientPeg.get(), roomId, widgetId, type, widgetUrl, name, data),
);
} else { } else {
return reject(new UserFriendlyError("You cannot modify widgets in this room.")); return reject(new UserFriendlyError("You cannot modify widgets in this room."));
} }
@ -1050,12 +1020,10 @@ export const Commands = [
command: "verify", command: "verify",
args: "<user-id> <device-id> <device-signing-key>", args: "<user-id> <device-id> <device-signing-key>",
description: _td("Verifies a user, session, and pubkey tuple"), description: _td("Verifies a user, session, and pubkey tuple"),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
if (matches) { if (matches) {
const cli = MatrixClientPeg.get();
const userId = matches[1]; const userId = matches[1];
const deviceId = matches[2]; const deviceId = matches[2];
const fingerprint = matches[3]; const fingerprint = matches[3];
@ -1130,10 +1098,10 @@ export const Commands = [
new Command({ new Command({
command: "discardsession", command: "discardsession",
description: _td("Forces the current outbound group session in an encrypted room to be discarded"), description: _td("Forces the current outbound group session in an encrypted room to be discarded"),
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId) { runFn: function (cli, roomId) {
try { try {
MatrixClientPeg.get().forceDiscardSession(roomId); cli.forceDiscardSession(roomId);
} catch (e) { } catch (e) {
return reject(e.message); return reject(e.message);
} }
@ -1145,19 +1113,19 @@ export const Commands = [
new Command({ new Command({
command: "remakeolm", command: "remakeolm",
description: _td("Developer command: Discards the current outbound group session and sets up new Olm sessions"), description: _td("Developer command: Discards the current outbound group session and sets up new Olm sessions"),
isEnabled: () => { isEnabled: (cli) => {
return SettingsStore.getValue("developerMode") && !isCurrentLocalRoom(); return SettingsStore.getValue("developerMode") && !isCurrentLocalRoom(cli);
}, },
runFn: (roomId) => { runFn: (cli, roomId) => {
try { try {
const room = MatrixClientPeg.get().getRoom(roomId); const room = cli.getRoom(roomId);
MatrixClientPeg.get().forceDiscardSession(roomId); cli.forceDiscardSession(roomId);
return success( return success(
room?.getEncryptionTargetMembers().then((members) => { room?.getEncryptionTargetMembers().then((members) => {
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall
MatrixClientPeg.get().crypto?.ensureOlmSessionsForUsers( cli.crypto?.ensureOlmSessionsForUsers(
members.map((m) => m.userId), members.map((m) => m.userId),
true, true,
); );
@ -1174,7 +1142,7 @@ export const Commands = [
command: "rainbow", command: "rainbow",
description: _td("Sends the given message coloured as a rainbow"), description: _td("Sends the given message coloured as a rainbow"),
args: "<message>", args: "<message>",
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (!args) return reject(this.getUsage()); if (!args) return reject(this.getUsage());
return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
}, },
@ -1184,7 +1152,7 @@ export const Commands = [
command: "rainbowme", command: "rainbowme",
description: _td("Sends the given emote coloured as a rainbow"), description: _td("Sends the given emote coloured as a rainbow"),
args: "<message>", args: "<message>",
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (!args) return reject(this.getUsage()); if (!args) return reject(this.getUsage());
return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
}, },
@ -1203,13 +1171,13 @@ export const Commands = [
command: "whois", command: "whois",
description: _td("Displays information about a user"), description: _td("Displays information about a user"),
args: "<user-id>", args: "<user-id>",
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, userId) { runFn: function (cli, roomId, userId) {
if (!userId || !userId.startsWith("@") || !userId.includes(":")) { if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
return reject(this.getUsage()); return reject(this.getUsage());
} }
const member = MatrixClientPeg.get().getRoom(roomId)?.getMember(userId); const member = cli.getRoom(roomId)?.getMember(userId);
dis.dispatch<ViewUserPayload>({ dis.dispatch<ViewUserPayload>({
action: Action.ViewUser, action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants. // XXX: We should be using a real member object and not assuming what the receiver wants.
@ -1225,7 +1193,7 @@ export const Commands = [
description: _td("Send a bug report with logs"), description: _td("Send a bug report with logs"),
isEnabled: () => !!SdkConfig.get().bug_report_endpoint_url, isEnabled: () => !!SdkConfig.get().bug_report_endpoint_url,
args: "<description>", args: "<description>",
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
return success( return success(
Modal.createDialog(BugReportDialog, { Modal.createDialog(BugReportDialog, {
initialText: args, initialText: args,
@ -1238,10 +1206,10 @@ export const Commands = [
command: "tovirtual", command: "tovirtual",
description: _td("Switches to this room's virtual room, if it has one"), description: _td("Switches to this room's virtual room, if it has one"),
category: CommandCategories.advanced, category: CommandCategories.advanced,
isEnabled(): boolean { isEnabled(cli): boolean {
return !!LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom(); return !!LegacyCallHandler.instance.getSupportsVirtualRooms() && !isCurrentLocalRoom(cli);
}, },
runFn: (roomId) => { runFn: (cli, roomId) => {
return success( return success(
(async (): Promise<void> => { (async (): Promise<void> => {
const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId); const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId);
@ -1260,7 +1228,7 @@ export const Commands = [
command: "query", command: "query",
description: _td("Opens chat with the given user"), description: _td("Opens chat with the given user"),
args: "<user-id>", args: "<user-id>",
runFn: function (roomId, userId) { runFn: function (cli, roomId, userId) {
// easter-egg for now: look up phone numbers through the thirdparty API // easter-egg for now: look up phone numbers through the thirdparty API
// (very dumb phone number detection...) // (very dumb phone number detection...)
const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId); const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId);
@ -1278,7 +1246,7 @@ export const Commands = [
userId = results[0].userid; userId = results[0].userid;
} }
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); const roomId = await ensureDMExists(cli, userId);
if (!roomId) throw new Error("Failed to ensure DM exists"); if (!roomId) throw new Error("Failed to ensure DM exists");
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
@ -1296,7 +1264,7 @@ export const Commands = [
command: "msg", command: "msg",
description: _td("Sends a message to the given user"), description: _td("Sends a message to the given user"),
args: "<user-id> [<message>]", args: "<user-id> [<message>]",
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
if (args) { if (args) {
// matches the first whitespace delimited group and then the rest of the string // matches the first whitespace delimited group and then the rest of the string
const matches = args.match(/^(\S+?)(?: +(.*))?$/s); const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
@ -1305,7 +1273,6 @@ export const Commands = [
if (userId && userId.startsWith("@") && userId.includes(":")) { if (userId && userId.startsWith("@") && userId.includes(":")) {
return success( return success(
(async (): Promise<void> => { (async (): Promise<void> => {
const cli = MatrixClientPeg.get();
const roomId = await ensureDMExists(cli, userId); const roomId = await ensureDMExists(cli, userId);
if (!roomId) throw new Error("Failed to ensure DM exists"); if (!roomId) throw new Error("Failed to ensure DM exists");
@ -1332,8 +1299,8 @@ export const Commands = [
command: "holdcall", command: "holdcall",
description: _td("Places the call in the current room on hold"), description: _td("Places the call in the current room on hold"),
category: CommandCategories.other, category: CommandCategories.other,
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const call = LegacyCallHandler.instance.getCallForRoom(roomId); const call = LegacyCallHandler.instance.getCallForRoom(roomId);
if (!call) { if (!call) {
return reject(new UserFriendlyError("No active call in this room")); return reject(new UserFriendlyError("No active call in this room"));
@ -1347,8 +1314,8 @@ export const Commands = [
command: "unholdcall", command: "unholdcall",
description: _td("Takes the call in the current room off hold"), description: _td("Takes the call in the current room off hold"),
category: CommandCategories.other, category: CommandCategories.other,
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const call = LegacyCallHandler.instance.getCallForRoom(roomId); const call = LegacyCallHandler.instance.getCallForRoom(roomId);
if (!call) { if (!call) {
return reject(new UserFriendlyError("No active call in this room")); return reject(new UserFriendlyError("No active call in this room"));
@ -1362,9 +1329,9 @@ export const Commands = [
command: "converttodm", command: "converttodm",
description: _td("Converts the room to a DM"), description: _td("Converts the room to a DM"),
category: CommandCategories.other, category: CommandCategories.other,
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) return reject(new UserFriendlyError("Could not find room")); if (!room) return reject(new UserFriendlyError("Could not find room"));
return success(guessAndSetDMRoom(room, true)); return success(guessAndSetDMRoom(room, true));
}, },
@ -1374,9 +1341,9 @@ export const Commands = [
command: "converttoroom", command: "converttoroom",
description: _td("Converts the DM to a room"), description: _td("Converts the DM to a room"),
category: CommandCategories.other, category: CommandCategories.other,
isEnabled: () => !isCurrentLocalRoom(), isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) return reject(new UserFriendlyError("Could not find room")); if (!room) return reject(new UserFriendlyError("Could not find room"));
return success(guessAndSetDMRoom(room, false)); return success(guessAndSetDMRoom(room, false));
}, },
@ -1398,7 +1365,7 @@ export const Commands = [
command: effect.command, command: effect.command,
description: effect.description(), description: effect.description(),
args: "<message>", args: "<message>",
runFn: function (roomId, args) { runFn: function (cli, roomId, args) {
let content: IContent; let content: IContent;
if (!args) { if (!args) {
content = ContentHelpers.makeEmoteMessage(effect.fallbackMessage()); content = ContentHelpers.makeEmoteMessage(effect.fallbackMessage());

View file

@ -341,7 +341,13 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
const [cmd, args, commandText] = getSlashCommand(this.model); const [cmd, args, commandText] = getSlashCommand(this.model);
if (cmd) { if (cmd) {
const threadId = editedEvent?.getThread()?.id || null; const threadId = editedEvent?.getThread()?.id || null;
const [content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId); const [content, commandSuccessful] = await runSlashCommand(
MatrixClientPeg.get(),
cmd,
args,
roomId,
threadId,
);
if (!commandSuccessful) { if (!commandSuccessful) {
return; // errored return; // errored
} }

View file

@ -474,6 +474,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
let commandSuccessful: boolean; let commandSuccessful: boolean;
[content, commandSuccessful] = await runSlashCommand( [content, commandSuccessful] = await runSlashCommand(
MatrixClientPeg.get(),
cmd, cmd,
args, args,
this.props.room.roomId, this.props.room.roomId,

View file

@ -85,7 +85,7 @@ export async function sendMessage(
if (cmd) { if (cmd) {
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null; const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation?.event_id : null;
let commandSuccessful: boolean; let commandSuccessful: boolean;
[content, commandSuccessful] = await runSlashCommand(cmd, args, roomId, threadId ?? null); [content, commandSuccessful] = await runSlashCommand(mxClient, cmd, args, roomId, threadId ?? null);
if (!commandSuccessful) { if (!commandSuccessful) {
return; // errored return; // errored

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { IContent } from "matrix-js-sdk/src/models/event"; import { IContent } from "matrix-js-sdk/src/models/event";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import EditorModel from "./model"; import EditorModel from "./model";
import { Type } from "./parts"; import { Type } from "./parts";
@ -58,12 +59,13 @@ export function getSlashCommand(model: EditorModel): [Command | undefined, strin
} }
export async function runSlashCommand( export async function runSlashCommand(
matrixClient: MatrixClient,
cmd: Command, cmd: Command,
args: string | undefined, args: string | undefined,
roomId: string, roomId: string,
threadId: string | null, threadId: string | null,
): Promise<[content: IContent | null, success: boolean]> { ): Promise<[content: IContent | null, success: boolean]> {
const result = cmd.run(roomId, threadId, args); const result = cmd.run(matrixClient, roomId, threadId, args);
let messageContent: IContent | null = null; let messageContent: IContent | null = null;
let error: any = result.error; let error: any = result.error;
if (result.promise) { if (result.promise) {

View file

@ -19,11 +19,13 @@ import { mocked } from "jest-mock";
import { Command, Commands, getCommand } from "../src/SlashCommands"; import { Command, Commands, getCommand } from "../src/SlashCommands";
import { createTestClient } from "./test-utils"; import { createTestClient } from "./test-utils";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../src/models/LocalRoom"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../src/models/LocalRoom";
import SettingsStore from "../src/settings/SettingsStore"; import SettingsStore from "../src/settings/SettingsStore";
import LegacyCallHandler from "../src/LegacyCallHandler"; import LegacyCallHandler from "../src/LegacyCallHandler";
import { SdkContextClass } from "../src/contexts/SDKContext"; import { SdkContextClass } from "../src/contexts/SDKContext";
import Modal from "../src/Modal";
import WidgetUtils from "../src/utils/WidgetUtils";
import { WidgetType } from "../src/widgets/WidgetType";
describe("SlashCommands", () => { describe("SlashCommands", () => {
let client: MatrixClient; let client: MatrixClient;
@ -57,7 +59,6 @@ describe("SlashCommands", () => {
jest.clearAllMocks(); jest.clearAllMocks();
client = createTestClient(); client = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
room = new Room(roomId, client, client.getUserId()!); room = new Room(roomId, client, client.getUserId()!);
localRoom = new LocalRoom(localRoomId, client, client.getUserId()!); localRoom = new LocalRoom(localRoomId, client, client.getUserId()!);
@ -70,9 +71,16 @@ describe("SlashCommands", () => {
const command = getCommand("/topic pizza"); const command = getCommand("/topic pizza");
expect(command.cmd).toBeDefined(); expect(command.cmd).toBeDefined();
expect(command.args).toBeDefined(); expect(command.args).toBeDefined();
await command.cmd!.run("room-id", null, command.args); await command.cmd!.run(client, "room-id", null, command.args);
expect(client.setRoomTopic).toHaveBeenCalledWith("room-id", "pizza", undefined); expect(client.setRoomTopic).toHaveBeenCalledWith("room-id", "pizza", undefined);
}); });
it("should show topic modal if no args passed", async () => {
const spy = jest.spyOn(Modal, "createDialog");
const command = getCommand("/topic")!;
await command.cmd!.run(client, roomId, null);
expect(spy).toHaveBeenCalled();
});
}); });
describe.each([ describe.each([
@ -104,12 +112,12 @@ describe("SlashCommands", () => {
describe("isEnabled", () => { describe("isEnabled", () => {
it("should return true for Room", () => { it("should return true for Room", () => {
setCurrentRoom(); setCurrentRoom();
expect(command.isEnabled()).toBe(true); expect(command.isEnabled(client)).toBe(true);
}); });
it("should return false for LocalRoom", () => { it("should return false for LocalRoom", () => {
setCurrentLocalRoon(); setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
}); });
}); });
@ -127,12 +135,12 @@ describe("SlashCommands", () => {
it("should return true for Room", () => { it("should return true for Room", () => {
setCurrentRoom(); setCurrentRoom();
expect(command.isEnabled()).toBe(true); expect(command.isEnabled(client)).toBe(true);
}); });
it("should return false for LocalRoom", () => { it("should return false for LocalRoom", () => {
setCurrentLocalRoon(); setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
}); });
@ -143,12 +151,12 @@ describe("SlashCommands", () => {
it("should return false for Room", () => { it("should return false for Room", () => {
setCurrentRoom(); setCurrentRoom();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
it("should return false for LocalRoom", () => { it("should return false for LocalRoom", () => {
setCurrentLocalRoon(); setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
}); });
}); });
@ -169,12 +177,12 @@ describe("SlashCommands", () => {
it("should return true for Room", () => { it("should return true for Room", () => {
setCurrentRoom(); setCurrentRoom();
expect(command.isEnabled()).toBe(true); expect(command.isEnabled(client)).toBe(true);
}); });
it("should return false for LocalRoom", () => { it("should return false for LocalRoom", () => {
setCurrentLocalRoon(); setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
}); });
@ -187,12 +195,12 @@ describe("SlashCommands", () => {
it("should return false for Room", () => { it("should return false for Room", () => {
setCurrentRoom(); setCurrentRoom();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
it("should return false for LocalRoom", () => { it("should return false for LocalRoom", () => {
setCurrentLocalRoon(); setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false); expect(command.isEnabled(client)).toBe(false);
}); });
}); });
}); });
@ -209,7 +217,7 @@ describe("SlashCommands", () => {
const command = getCommand("/part #foo:bar"); const command = getCommand("/part #foo:bar");
expect(command.cmd).toBeDefined(); expect(command.cmd).toBeDefined();
expect(command.args).toBeDefined(); expect(command.args).toBeDefined();
await command.cmd!.run("room-id", null, command.args); await command.cmd!.run(client, "room-id", null, command.args);
expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything()); expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything());
}); });
@ -223,7 +231,7 @@ describe("SlashCommands", () => {
const command = getCommand("/part #foo:bar"); const command = getCommand("/part #foo:bar");
expect(command.cmd).toBeDefined(); expect(command.cmd).toBeDefined();
expect(command.args).toBeDefined(); expect(command.args).toBeDefined();
await command.cmd!.run("room-id", null, command.args!); await command.cmd!.run(client, "room-id", null, command.args!);
expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything()); expect(client.leaveRoomChain).toHaveBeenCalledWith("room-id", expect.anything());
}); });
}); });
@ -232,11 +240,45 @@ describe("SlashCommands", () => {
const command = findCommand(commandName)!; const command = findCommand(commandName)!;
it("should return usage if no args", () => { it("should return usage if no args", () => {
expect(command.run(roomId, null, undefined).error).toBe(command.getUsage()); expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
}); });
it("should make things rainbowy", () => { it("should make things rainbowy", () => {
return expect(command.run(roomId, null, "this is a test message").promise).resolves.toMatchSnapshot(); return expect(
command.run(client, roomId, null, "this is a test message").promise,
).resolves.toMatchSnapshot();
});
});
describe.each(["shrug", "tableflip", "unflip", "lenny"])("/%s", (commandName: string) => {
const command = findCommand(commandName)!;
it("should match snapshot with no args", () => {
return expect(command.run(client, roomId, null).promise).resolves.toMatchSnapshot();
});
it("should match snapshot with args", () => {
return expect(
command.run(client, roomId, null, "this is a test message").promise,
).resolves.toMatchSnapshot();
});
});
describe("/addwidget", () => {
it("should parse html iframe snippets", async () => {
jest.spyOn(WidgetUtils, "canUserModifyWidgets").mockReturnValue(true);
const spy = jest.spyOn(WidgetUtils, "setRoomWidget");
const command = findCommand("addwidget")!;
await command.run(client, roomId, null, '<iframe src="https://element.io"></iframe>');
expect(spy).toHaveBeenCalledWith(
client,
roomId,
expect.any(String),
WidgetType.CUSTOM,
"https://element.io",
"Custom",
{},
);
}); });
}); });
}); });

View file

@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SlashCommands /lenny should match snapshot with args 1`] = `
{
"body": "( ͡° ͜ʖ ͡°) this is a test message",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /lenny should match snapshot with no args 1`] = `
{
"body": "( ͡° ͜ʖ ͡°)",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /rainbow should make things rainbowy 1`] = ` exports[`SlashCommands /rainbow should make things rainbowy 1`] = `
{ {
"body": "this is a test message", "body": "this is a test message",
@ -17,3 +31,45 @@ exports[`SlashCommands /rainbowme should make things rainbowy 1`] = `
"msgtype": "m.emote", "msgtype": "m.emote",
} }
`; `;
exports[`SlashCommands /shrug should match snapshot with args 1`] = `
{
"body": "¯\\_(ツ)_/¯ this is a test message",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /shrug should match snapshot with no args 1`] = `
{
"body": "¯\\_(ツ)_/¯",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /tableflip should match snapshot with args 1`] = `
{
"body": "(╯°□°)╯︵ ┻━┻ this is a test message",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /tableflip should match snapshot with no args 1`] = `
{
"body": "(╯°□°)╯︵ ┻━┻",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /unflip should match snapshot with args 1`] = `
{
"body": "┬──┬ ( ゜-゜ノ) this is a test message",
"msgtype": "m.text",
}
`;
exports[`SlashCommands /unflip should match snapshot with no args 1`] = `
{
"body": "┬──┬ ( ゜-゜ノ)",
"msgtype": "m.text",
}
`;