Warn when demoting self via /op and /deop slash commands (#11214)
* Warn when demoting self via /op and /deop slash commands * Iterate and DRY * i18n * Improve coverage * Improve coverage * Improve coverage * Iterate
This commit is contained in:
parent
b6c7fe4235
commit
a8f632ae19
8 changed files with 431 additions and 267 deletions
|
@ -20,13 +20,10 @@ limitations under the License.
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
import * as ContentHelpers from "matrix-js-sdk/src/content-helpers";
|
import * as ContentHelpers from "matrix-js-sdk/src/content-helpers";
|
||||||
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 { 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 { MatrixClient } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
import { _t, _td, UserFriendlyError } from "./languageHandler";
|
import { _t, _td, UserFriendlyError } from "./languageHandler";
|
||||||
|
@ -46,7 +43,6 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
||||||
import { ensureDMExists } from "./createRoom";
|
import { ensureDMExists } from "./createRoom";
|
||||||
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
|
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
import { UIComponent, UIFeature } from "./settings/UIFeature";
|
||||||
|
@ -54,184 +50,24 @@ import { CHAT_EFFECTS } from "./effects";
|
||||||
import LegacyCallHandler from "./LegacyCallHandler";
|
import LegacyCallHandler from "./LegacyCallHandler";
|
||||||
import { guessAndSetDMRoom } from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
import { upgradeRoom } from "./utils/RoomUpgrade";
|
import { upgradeRoom } from "./utils/RoomUpgrade";
|
||||||
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
|
|
||||||
import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog";
|
import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog";
|
||||||
import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog";
|
import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog";
|
||||||
import InfoDialog from "./components/views/dialogs/InfoDialog";
|
import InfoDialog from "./components/views/dialogs/InfoDialog";
|
||||||
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
||||||
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
|
||||||
import { TimelineRenderingType } from "./contexts/RoomContext";
|
import { TimelineRenderingType } from "./contexts/RoomContext";
|
||||||
import { XOR } from "./@types/common";
|
|
||||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
|
||||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||||
import VoipUserMapper from "./VoipUserMapper";
|
import VoipUserMapper from "./VoipUserMapper";
|
||||||
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
|
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
|
||||||
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
|
||||||
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
|
|
||||||
import { SdkContextClass } from "./contexts/SDKContext";
|
|
||||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo";
|
import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo";
|
||||||
|
import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils";
|
||||||
|
import { deop, op } from "./slash-commands/op";
|
||||||
|
import { CommandCategories } from "./slash-commands/interface";
|
||||||
|
import { Command } from "./slash-commands/command";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
export { CommandCategories, Command };
|
||||||
interface HTMLInputEvent extends Event {
|
|
||||||
target: HTMLInputElement & EventTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
const singleMxcUpload = async (cli: MatrixClient): Promise<string | null> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const fileSelector = document.createElement("input");
|
|
||||||
fileSelector.setAttribute("type", "file");
|
|
||||||
fileSelector.onchange = (ev: Event) => {
|
|
||||||
const file = (ev as HTMLInputEvent).target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
Modal.createDialog(UploadConfirmDialog, {
|
|
||||||
file,
|
|
||||||
onFinished: async (shouldContinue): Promise<void> => {
|
|
||||||
if (shouldContinue) {
|
|
||||||
const { content_uri: uri } = await cli.uploadContent(file);
|
|
||||||
resolve(uri);
|
|
||||||
} else {
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
fileSelector.click();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CommandCategories = {
|
|
||||||
messages: _td("Messages"),
|
|
||||||
actions: _td("Actions"),
|
|
||||||
admin: _td("Admin"),
|
|
||||||
advanced: _td("Advanced"),
|
|
||||||
effects: _td("Effects"),
|
|
||||||
other: _td("Other"),
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>;
|
|
||||||
|
|
||||||
type RunFn = (
|
|
||||||
this: Command,
|
|
||||||
matrixClient: MatrixClient,
|
|
||||||
roomId: string,
|
|
||||||
threadId: string | null,
|
|
||||||
args?: string,
|
|
||||||
) => RunResult;
|
|
||||||
|
|
||||||
interface ICommandOpts {
|
|
||||||
command: string;
|
|
||||||
aliases?: string[];
|
|
||||||
args?: string;
|
|
||||||
description: string;
|
|
||||||
analyticsName?: SlashCommandEvent["command"];
|
|
||||||
runFn?: RunFn;
|
|
||||||
category: string;
|
|
||||||
hideCompletionAfterSpace?: boolean;
|
|
||||||
isEnabled?(matrixClient: MatrixClient | null): boolean;
|
|
||||||
renderingTypes?: TimelineRenderingType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Command {
|
|
||||||
public readonly command: string;
|
|
||||||
public readonly aliases: string[];
|
|
||||||
public readonly args?: string;
|
|
||||||
public readonly description: string;
|
|
||||||
public readonly runFn?: RunFn;
|
|
||||||
public readonly category: string;
|
|
||||||
public readonly hideCompletionAfterSpace: boolean;
|
|
||||||
public readonly renderingTypes?: TimelineRenderingType[];
|
|
||||||
public readonly analyticsName?: SlashCommandEvent["command"];
|
|
||||||
private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean;
|
|
||||||
|
|
||||||
public constructor(opts: ICommandOpts) {
|
|
||||||
this.command = opts.command;
|
|
||||||
this.aliases = opts.aliases || [];
|
|
||||||
this.args = opts.args || "";
|
|
||||||
this.description = opts.description;
|
|
||||||
this.runFn = opts.runFn?.bind(this);
|
|
||||||
this.category = opts.category || CommandCategories.other;
|
|
||||||
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
|
|
||||||
this._isEnabled = opts.isEnabled;
|
|
||||||
this.renderingTypes = opts.renderingTypes;
|
|
||||||
this.analyticsName = opts.analyticsName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCommand(): string {
|
|
||||||
return `/${this.command}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCommandWithArgs(): string {
|
|
||||||
return this.getCommand() + " " + this.args;
|
|
||||||
}
|
|
||||||
|
|
||||||
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."));
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room;
|
|
||||||
if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) {
|
|
||||||
return reject(
|
|
||||||
new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", {
|
|
||||||
renderingType,
|
|
||||||
cause: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.analyticsName) {
|
|
||||||
PosthogAnalytics.instance.trackEvent<SlashCommandEvent>({
|
|
||||||
eventName: "SlashCommand",
|
|
||||||
command: this.analyticsName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.runFn(matrixClient, roomId, threadId, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUsage(): string {
|
|
||||||
return _t("Usage") + ": " + this.getCommandWithArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEnabled(cli: MatrixClient | null): boolean {
|
|
||||||
return this._isEnabled?.(cli) ?? true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reject(error?: any): RunResult {
|
|
||||||
return { error };
|
|
||||||
}
|
|
||||||
|
|
||||||
function success(promise: Promise<any> = Promise.resolve()): RunResult {
|
|
||||||
return { promise };
|
|
||||||
}
|
|
||||||
|
|
||||||
function successSync(value: any): RunResult {
|
|
||||||
return success(Promise.resolve(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentLocalRoom = (cli: MatrixClient | null): boolean => {
|
|
||||||
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
|
||||||
if (!roomId) return false;
|
|
||||||
const room = cli?.getRoom(roomId);
|
|
||||||
if (!room) return false;
|
|
||||||
return isLocalRoom(room);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canAffectPowerlevels = (cli: MatrixClient | null): 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const Commands = [
|
export const Commands = [
|
||||||
new Command({
|
new Command({
|
||||||
|
@ -886,78 +722,8 @@ export const Commands = [
|
||||||
},
|
},
|
||||||
category: CommandCategories.actions,
|
category: CommandCategories.actions,
|
||||||
}),
|
}),
|
||||||
new Command({
|
op,
|
||||||
command: "op",
|
deop,
|
||||||
args: "<user-id> [<power-level>]",
|
|
||||||
description: _td("Define the power level of a user"),
|
|
||||||
isEnabled: canAffectPowerlevels,
|
|
||||||
runFn: function (cli, roomId, threadId, args) {
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
|
||||||
let powerLevel = 50; // default power level for op
|
|
||||||
if (matches) {
|
|
||||||
const userId = matches[1];
|
|
||||||
if (matches.length === 4 && undefined !== matches[3]) {
|
|
||||||
powerLevel = parseInt(matches[3], 10);
|
|
||||||
}
|
|
||||||
if (!isNaN(powerLevel)) {
|
|
||||||
const room = cli.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
return reject(
|
|
||||||
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
|
|
||||||
roomId,
|
|
||||||
cause: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const member = room.getMember(userId);
|
|
||||||
if (
|
|
||||||
!member?.membership ||
|
|
||||||
getEffectiveMembership(member.membership) === EffectiveMembership.Leave
|
|
||||||
) {
|
|
||||||
return reject(new UserFriendlyError("Could not find user in room"));
|
|
||||||
}
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
|
||||||
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
},
|
|
||||||
category: CommandCategories.admin,
|
|
||||||
renderingTypes: [TimelineRenderingType.Room],
|
|
||||||
}),
|
|
||||||
new Command({
|
|
||||||
command: "deop",
|
|
||||||
args: "<user-id>",
|
|
||||||
description: _td("Deops user with given id"),
|
|
||||||
isEnabled: canAffectPowerlevels,
|
|
||||||
runFn: function (cli, roomId, threadId, args) {
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
|
||||||
if (matches) {
|
|
||||||
const room = cli.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
return reject(
|
|
||||||
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
|
|
||||||
roomId,
|
|
||||||
cause: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
|
||||||
if (!powerLevelEvent?.getContent().users[args]) {
|
|
||||||
return reject(new UserFriendlyError("Could not find user in room"));
|
|
||||||
}
|
|
||||||
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
},
|
|
||||||
category: CommandCategories.admin,
|
|
||||||
renderingTypes: [TimelineRenderingType.Room],
|
|
||||||
}),
|
|
||||||
new Command({
|
new Command({
|
||||||
command: "devtools",
|
command: "devtools",
|
||||||
description: _td("Opens the Developer Tools dialog"),
|
description: _td("Opens the Developer Tools dialog"),
|
||||||
|
|
|
@ -513,7 +513,7 @@ export const UserOptionsSection: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
|
||||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||||
title: _t("Demote yourself?"),
|
title: _t("Demote yourself?"),
|
||||||
description: (
|
description: (
|
||||||
|
|
|
@ -409,14 +409,6 @@
|
||||||
"Go Back": "Go Back",
|
"Go Back": "Go Back",
|
||||||
"Cancel": "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Setting up keys": "Setting up keys",
|
"Setting up keys": "Setting up keys",
|
||||||
"Messages": "Messages",
|
|
||||||
"Actions": "Actions",
|
|
||||||
"Advanced": "Advanced",
|
|
||||||
"Effects": "Effects",
|
|
||||||
"Other": "Other",
|
|
||||||
"Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.",
|
|
||||||
"Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)",
|
|
||||||
"Usage": "Usage",
|
|
||||||
"Sends the given message as a spoiler": "Sends the given message as a spoiler",
|
"Sends the given message as a spoiler": "Sends the given message as a spoiler",
|
||||||
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
|
||||||
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
|
||||||
|
@ -455,10 +447,6 @@
|
||||||
"Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward",
|
"Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward",
|
||||||
"Unignored user": "Unignored user",
|
"Unignored user": "Unignored user",
|
||||||
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
|
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
|
||||||
"Define the power level of a user": "Define the power level of a user",
|
|
||||||
"Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s",
|
|
||||||
"Could not find user in room": "Could not find user in room",
|
|
||||||
"Deops user with given id": "Deops user with given id",
|
|
||||||
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
|
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
|
||||||
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
|
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
|
||||||
"Please supply a widget URL or embed code": "Please supply a widget URL or embed code",
|
"Please supply a widget URL or embed code": "Please supply a widget URL or embed code",
|
||||||
|
@ -938,6 +926,18 @@
|
||||||
"Unsent": "Unsent",
|
"Unsent": "Unsent",
|
||||||
"unknown": "unknown",
|
"unknown": "unknown",
|
||||||
"Change notification settings": "Change notification settings",
|
"Change notification settings": "Change notification settings",
|
||||||
|
"Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.",
|
||||||
|
"Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)",
|
||||||
|
"Usage": "Usage",
|
||||||
|
"Messages": "Messages",
|
||||||
|
"Actions": "Actions",
|
||||||
|
"Advanced": "Advanced",
|
||||||
|
"Effects": "Effects",
|
||||||
|
"Other": "Other",
|
||||||
|
"Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s",
|
||||||
|
"Could not find user in room": "Could not find user in room",
|
||||||
|
"Define the power level of a user": "Define the power level of a user",
|
||||||
|
"Deops user with given id": "Deops user with given id",
|
||||||
"Messaging": "Messaging",
|
"Messaging": "Messaging",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Spaces": "Spaces",
|
"Spaces": "Spaces",
|
||||||
|
|
116
src/slash-commands/command.ts
Normal file
116
src/slash-commands/command.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand";
|
||||||
|
|
||||||
|
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||||
|
import { reject } from "./utils";
|
||||||
|
import { _t, UserFriendlyError } from "../languageHandler";
|
||||||
|
import { PosthogAnalytics } from "../PosthogAnalytics";
|
||||||
|
import { CommandCategories, RunResult } from "./interface";
|
||||||
|
|
||||||
|
type RunFn = (
|
||||||
|
this: Command,
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
threadId: string | null,
|
||||||
|
args?: string,
|
||||||
|
) => RunResult;
|
||||||
|
|
||||||
|
interface ICommandOpts {
|
||||||
|
command: string;
|
||||||
|
aliases?: string[];
|
||||||
|
args?: string;
|
||||||
|
description: string;
|
||||||
|
analyticsName?: SlashCommandEvent["command"];
|
||||||
|
runFn?: RunFn;
|
||||||
|
category: string;
|
||||||
|
hideCompletionAfterSpace?: boolean;
|
||||||
|
isEnabled?(matrixClient: MatrixClient | null): boolean;
|
||||||
|
renderingTypes?: TimelineRenderingType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Command {
|
||||||
|
public readonly command: string;
|
||||||
|
public readonly aliases: string[];
|
||||||
|
public readonly args?: string;
|
||||||
|
public readonly description: string;
|
||||||
|
public readonly runFn?: RunFn;
|
||||||
|
public readonly category: string;
|
||||||
|
public readonly hideCompletionAfterSpace: boolean;
|
||||||
|
public readonly renderingTypes?: TimelineRenderingType[];
|
||||||
|
public readonly analyticsName?: SlashCommandEvent["command"];
|
||||||
|
private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean;
|
||||||
|
|
||||||
|
public constructor(opts: ICommandOpts) {
|
||||||
|
this.command = opts.command;
|
||||||
|
this.aliases = opts.aliases || [];
|
||||||
|
this.args = opts.args || "";
|
||||||
|
this.description = opts.description;
|
||||||
|
this.runFn = opts.runFn?.bind(this);
|
||||||
|
this.category = opts.category || CommandCategories.other;
|
||||||
|
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
|
||||||
|
this._isEnabled = opts.isEnabled;
|
||||||
|
this.renderingTypes = opts.renderingTypes;
|
||||||
|
this.analyticsName = opts.analyticsName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommand(): string {
|
||||||
|
return `/${this.command}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommandWithArgs(): string {
|
||||||
|
return this.getCommand() + " " + this.args;
|
||||||
|
}
|
||||||
|
|
||||||
|
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."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room;
|
||||||
|
if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) {
|
||||||
|
return reject(
|
||||||
|
new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", {
|
||||||
|
renderingType,
|
||||||
|
cause: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.analyticsName) {
|
||||||
|
PosthogAnalytics.instance.trackEvent<SlashCommandEvent>({
|
||||||
|
eventName: "SlashCommand",
|
||||||
|
command: this.analyticsName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.runFn(matrixClient, roomId, threadId, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsage(): string {
|
||||||
|
return _t("Usage") + ": " + this.getCommandWithArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabled(cli: MatrixClient | null): boolean {
|
||||||
|
return this._isEnabled?.(cli) ?? true;
|
||||||
|
}
|
||||||
|
}
|
34
src/slash-commands/interface.ts
Normal file
34
src/slash-commands/interface.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IContent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { _td } from "../languageHandler";
|
||||||
|
import { XOR } from "../@types/common";
|
||||||
|
|
||||||
|
export const CommandCategories = {
|
||||||
|
messages: _td("Messages"),
|
||||||
|
actions: _td("Actions"),
|
||||||
|
admin: _td("Admin"),
|
||||||
|
advanced: _td("Advanced"),
|
||||||
|
effects: _td("Effects"),
|
||||||
|
other: _td("Other"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>;
|
101
src/slash-commands/op.ts
Normal file
101
src/slash-commands/op.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { _td, UserFriendlyError } from "../languageHandler";
|
||||||
|
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
||||||
|
import { warnSelfDemote } from "../components/views/right_panel/UserInfo";
|
||||||
|
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||||
|
import { canAffectPowerlevels, success, reject } from "./utils";
|
||||||
|
import { CommandCategories, RunResult } from "./interface";
|
||||||
|
import { Command } from "./command";
|
||||||
|
|
||||||
|
const updatePowerLevel = async (room: Room, member: RoomMember, powerLevel: number | undefined): Promise<unknown> => {
|
||||||
|
// Only warn if the target is ourselves and the power level is decreasing or being unset
|
||||||
|
if (member.userId === room.client.getUserId() && (powerLevel === undefined || member.powerLevel > powerLevel)) {
|
||||||
|
const ok = await warnSelfDemote(room.isSpaceRoom());
|
||||||
|
if (!ok) return; // Nothing to do
|
||||||
|
}
|
||||||
|
return room.client.setPowerLevel(room.roomId, member.userId, powerLevel);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePowerLevelHelper = (
|
||||||
|
client: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
userId: string,
|
||||||
|
powerLevel: number | undefined,
|
||||||
|
): RunResult => {
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
return reject(
|
||||||
|
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
|
||||||
|
roomId,
|
||||||
|
cause: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
if (!member?.membership || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) {
|
||||||
|
return reject(new UserFriendlyError("Could not find user in room"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(updatePowerLevel(room, member, powerLevel));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const op = new Command({
|
||||||
|
command: "op",
|
||||||
|
args: "<user-id> [<power-level>]",
|
||||||
|
description: _td("Define the power level of a user"),
|
||||||
|
isEnabled: canAffectPowerlevels,
|
||||||
|
runFn: function (cli, roomId, threadId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||||
|
let powerLevel = 50; // default power level for op
|
||||||
|
if (matches) {
|
||||||
|
const userId = matches[1];
|
||||||
|
if (matches.length === 4 && undefined !== matches[3]) {
|
||||||
|
powerLevel = parseInt(matches[3], 10);
|
||||||
|
}
|
||||||
|
return updatePowerLevelHelper(cli, roomId, userId, powerLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
category: CommandCategories.admin,
|
||||||
|
renderingTypes: [TimelineRenderingType.Room],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deop = new Command({
|
||||||
|
command: "deop",
|
||||||
|
args: "<user-id>",
|
||||||
|
description: _td("Deops user with given id"),
|
||||||
|
isEnabled: canAffectPowerlevels,
|
||||||
|
runFn: function (cli, roomId, threadId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
return updatePowerLevelHelper(cli, roomId, args, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
category: CommandCategories.admin,
|
||||||
|
renderingTypes: [TimelineRenderingType.Room],
|
||||||
|
});
|
83
src/slash-commands/utils.ts
Normal file
83
src/slash-commands/utils.ts
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { SdkContextClass } from "../contexts/SDKContext";
|
||||||
|
import { isLocalRoom } from "../utils/localRoom/isLocalRoom";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import UploadConfirmDialog from "../components/views/dialogs/UploadConfirmDialog";
|
||||||
|
import { RunResult } from "./interface";
|
||||||
|
|
||||||
|
export function reject(error?: any): RunResult {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function success(promise: Promise<any> = Promise.resolve()): RunResult {
|
||||||
|
return { promise };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function successSync(value: any): RunResult {
|
||||||
|
return success(Promise.resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canAffectPowerlevels = (cli: MatrixClient | null): 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
|
interface HTMLInputEvent extends Event {
|
||||||
|
target: HTMLInputElement & EventTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const singleMxcUpload = async (cli: MatrixClient): Promise<string | null> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const fileSelector = document.createElement("input");
|
||||||
|
fileSelector.setAttribute("type", "file");
|
||||||
|
fileSelector.onchange = (ev: Event) => {
|
||||||
|
const file = (ev as HTMLInputEvent).target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
Modal.createDialog(UploadConfirmDialog, {
|
||||||
|
file,
|
||||||
|
onFinished: async (shouldContinue): Promise<void> => {
|
||||||
|
if (shouldContinue) {
|
||||||
|
const { content_uri: uri } = await cli.uploadContent(file);
|
||||||
|
resolve(uri);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSelector.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCurrentLocalRoom = (cli: MatrixClient | null): boolean => {
|
||||||
|
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
||||||
|
if (!roomId) return false;
|
||||||
|
const room = cli?.getRoom(roomId);
|
||||||
|
if (!room) return false;
|
||||||
|
return isLocalRoom(room);
|
||||||
|
};
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { Command, Commands, getCommand } from "../src/SlashCommands";
|
import { Command, Commands, getCommand } from "../src/SlashCommands";
|
||||||
|
@ -26,6 +26,9 @@ import { SdkContextClass } from "../src/contexts/SDKContext";
|
||||||
import Modal from "../src/Modal";
|
import Modal from "../src/Modal";
|
||||||
import WidgetUtils from "../src/utils/WidgetUtils";
|
import WidgetUtils from "../src/utils/WidgetUtils";
|
||||||
import { WidgetType } from "../src/widgets/WidgetType";
|
import { WidgetType } from "../src/widgets/WidgetType";
|
||||||
|
import { warnSelfDemote } from "../src/components/views/right_panel/UserInfo";
|
||||||
|
|
||||||
|
jest.mock("../src/components/views/right_panel/UserInfo");
|
||||||
|
|
||||||
describe("SlashCommands", () => {
|
describe("SlashCommands", () => {
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
@ -47,7 +50,7 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCurrentLocalRoon = (): void => {
|
const setCurrentLocalRoom = (): void => {
|
||||||
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId);
|
mocked(SdkContextClass.instance.roomViewStore.getRoomId).mockReturnValue(localRoomId);
|
||||||
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
mocked(client.getRoom).mockImplementation((rId: string): Room | null => {
|
||||||
if (rId === localRoomId) return localRoom;
|
if (rId === localRoomId) return localRoom;
|
||||||
|
@ -60,8 +63,8 @@ describe("SlashCommands", () => {
|
||||||
|
|
||||||
client = createTestClient();
|
client = createTestClient();
|
||||||
|
|
||||||
room = new Room(roomId, client, client.getUserId()!);
|
room = new Room(roomId, client, client.getSafeUserId());
|
||||||
localRoom = new LocalRoom(localRoomId, client, client.getUserId()!);
|
localRoom = new LocalRoom(localRoomId, client, client.getSafeUserId());
|
||||||
|
|
||||||
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId");
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "getRoomId");
|
||||||
});
|
});
|
||||||
|
@ -116,12 +119,73 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoon();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("/op", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
command = findCommand("op")!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return usage if no args", () => {
|
||||||
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject with usage if given an invalid power level value", () => {
|
||||||
|
expect(command.run(client, roomId, null, "@bob:server Admin").error).toBe(command.getUsage());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject with usage for invalid input", () => {
|
||||||
|
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about self demotion", async () => {
|
||||||
|
setCurrentRoom();
|
||||||
|
const member = new RoomMember(roomId, client.getSafeUserId());
|
||||||
|
member.membership = "join";
|
||||||
|
member.powerLevel = 100;
|
||||||
|
room.getMember = () => member;
|
||||||
|
command.run(client, roomId, null, `${client.getUserId()} 0`);
|
||||||
|
expect(warnSelfDemote).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default to 50 if no powerlevel specified", async () => {
|
||||||
|
setCurrentRoom();
|
||||||
|
const member = new RoomMember(roomId, "@user:server");
|
||||||
|
member.membership = "join";
|
||||||
|
room.getMember = () => member;
|
||||||
|
command.run(client, roomId, null, member.userId);
|
||||||
|
expect(client.setPowerLevel).toHaveBeenCalledWith(roomId, member.userId, 50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("/deop", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
command = findCommand("deop")!;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return usage if no args", () => {
|
||||||
|
expect(command.run(client, roomId, null, undefined).error).toBe(command.getUsage());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn about self demotion", async () => {
|
||||||
|
setCurrentRoom();
|
||||||
|
const member = new RoomMember(roomId, client.getSafeUserId());
|
||||||
|
member.membership = "join";
|
||||||
|
member.powerLevel = 100;
|
||||||
|
room.getMember = () => member;
|
||||||
|
command.run(client, roomId, null, client.getSafeUserId());
|
||||||
|
expect(warnSelfDemote).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject with usage for invalid input", () => {
|
||||||
|
expect(command.run(client, roomId, null, " ").error).toBe(command.getUsage());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("/tovirtual", () => {
|
describe("/tovirtual", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
command = findCommand("tovirtual")!;
|
command = findCommand("tovirtual")!;
|
||||||
|
@ -139,7 +203,7 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoon();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -155,7 +219,7 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoon();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -181,7 +245,7 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoon();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -199,7 +263,7 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for LocalRoom", () => {
|
it("should return false for LocalRoom", () => {
|
||||||
setCurrentLocalRoon();
|
setCurrentLocalRoom();
|
||||||
expect(command.isEnabled(client)).toBe(false);
|
expect(command.isEnabled(client)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -208,9 +272,9 @@ describe("SlashCommands", () => {
|
||||||
|
|
||||||
describe("/part", () => {
|
describe("/part", () => {
|
||||||
it("should part room matching alias if found", async () => {
|
it("should part room matching alias if found", async () => {
|
||||||
const room1 = new Room("room-id", client, client.getUserId()!);
|
const room1 = new Room("room-id", client, client.getSafeUserId());
|
||||||
room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar");
|
room1.getCanonicalAlias = jest.fn().mockReturnValue("#foo:bar");
|
||||||
const room2 = new Room("other-room", client, client.getUserId()!);
|
const room2 = new Room("other-room", client, client.getSafeUserId());
|
||||||
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
room2.getCanonicalAlias = jest.fn().mockReturnValue("#baz:bar");
|
||||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||||
|
|
||||||
|
@ -222,9 +286,9 @@ describe("SlashCommands", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should part room matching alt alias if found", async () => {
|
it("should part room matching alt alias if found", async () => {
|
||||||
const room1 = new Room("room-id", client, client.getUserId()!);
|
const room1 = new Room("room-id", client, client.getSafeUserId());
|
||||||
room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]);
|
room1.getAltAliases = jest.fn().mockReturnValue(["#foo:bar"]);
|
||||||
const room2 = new Room("other-room", client, client.getUserId()!);
|
const room2 = new Room("other-room", client, client.getSafeUserId());
|
||||||
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
room2.getAltAliases = jest.fn().mockReturnValue(["#baz:bar"]);
|
||||||
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
mocked(client.getRooms).mockReturnValue([room1, room2]);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue