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
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);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue