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

View file

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

View file

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

View file

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

View file

@ -19,11 +19,13 @@ import { mocked } from "jest-mock";
import { Command, Commands, getCommand } from "../src/SlashCommands";
import { createTestClient } from "./test-utils";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../src/models/LocalRoom";
import SettingsStore from "../src/settings/SettingsStore";
import LegacyCallHandler from "../src/LegacyCallHandler";
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", () => {
let client: MatrixClient;
@ -57,7 +59,6 @@ describe("SlashCommands", () => {
jest.clearAllMocks();
client = createTestClient();
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
room = new Room(roomId, client, client.getUserId()!);
localRoom = new LocalRoom(localRoomId, client, client.getUserId()!);
@ -70,9 +71,16 @@ describe("SlashCommands", () => {
const command = getCommand("/topic pizza");
expect(command.cmd).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);
});
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([
@ -104,12 +112,12 @@ describe("SlashCommands", () => {
describe("isEnabled", () => {
it("should return true for Room", () => {
setCurrentRoom();
expect(command.isEnabled()).toBe(true);
expect(command.isEnabled(client)).toBe(true);
});
it("should return false for LocalRoom", () => {
setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
});
});
@ -127,12 +135,12 @@ describe("SlashCommands", () => {
it("should return true for Room", () => {
setCurrentRoom();
expect(command.isEnabled()).toBe(true);
expect(command.isEnabled(client)).toBe(true);
});
it("should return false for LocalRoom", () => {
setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
});
@ -143,12 +151,12 @@ describe("SlashCommands", () => {
it("should return false for Room", () => {
setCurrentRoom();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
it("should return false for LocalRoom", () => {
setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
});
});
@ -169,12 +177,12 @@ describe("SlashCommands", () => {
it("should return true for Room", () => {
setCurrentRoom();
expect(command.isEnabled()).toBe(true);
expect(command.isEnabled(client)).toBe(true);
});
it("should return false for LocalRoom", () => {
setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
});
@ -187,12 +195,12 @@ describe("SlashCommands", () => {
it("should return false for Room", () => {
setCurrentRoom();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
it("should return false for LocalRoom", () => {
setCurrentLocalRoon();
expect(command.isEnabled()).toBe(false);
expect(command.isEnabled(client)).toBe(false);
});
});
});
@ -209,7 +217,7 @@ describe("SlashCommands", () => {
const command = getCommand("/part #foo:bar");
expect(command.cmd).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());
});
@ -223,7 +231,7 @@ describe("SlashCommands", () => {
const command = getCommand("/part #foo:bar");
expect(command.cmd).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());
});
});
@ -232,11 +240,45 @@ describe("SlashCommands", () => {
const command = findCommand(commandName)!;
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", () => {
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
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`] = `
{
"body": "this is a test message",
@ -17,3 +31,45 @@ exports[`SlashCommands /rainbowme should make things rainbowy 1`] = `
"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",
}
`;