Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a8f632a

Browse files
authored
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
1 parent b6c7fe4 commit a8f632a

File tree

8 files changed

+431
-267
lines changed

8 files changed

+431
-267
lines changed

src/SlashCommands.tsx

Lines changed: 7 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ limitations under the License.
2020
import * as React from "react";
2121
import { User } from "matrix-js-sdk/src/models/user";
2222
import { Direction } from "matrix-js-sdk/src/models/event-timeline";
23-
import { EventType } from "matrix-js-sdk/src/@types/event";
2423
import * as ContentHelpers from "matrix-js-sdk/src/content-helpers";
2524
import { logger } from "matrix-js-sdk/src/logger";
2625
import { IContent } from "matrix-js-sdk/src/models/event";
2726
import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic";
28-
import { SlashCommand as SlashCommandEvent } from "@matrix-org/analytics-events/types/typescript/SlashCommand";
29-
import { MatrixClient } from "matrix-js-sdk/src/matrix";
3027

3128
import dis from "./dispatcher/dispatcher";
3229
import { _t, _td, UserFriendlyError } from "./languageHandler";
@@ -46,192 +43,31 @@ import BugReportDialog from "./components/views/dialogs/BugReportDialog";
4643
import { ensureDMExists } from "./createRoom";
4744
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
4845
import { Action } from "./dispatcher/actions";
49-
import { EffectiveMembership, getEffectiveMembership } from "./utils/membership";
5046
import SdkConfig from "./SdkConfig";
5147
import SettingsStore from "./settings/SettingsStore";
5248
import { UIComponent, UIFeature } from "./settings/UIFeature";
5349
import { CHAT_EFFECTS } from "./effects";
5450
import LegacyCallHandler from "./LegacyCallHandler";
5551
import { guessAndSetDMRoom } from "./Rooms";
5652
import { upgradeRoom } from "./utils/RoomUpgrade";
57-
import UploadConfirmDialog from "./components/views/dialogs/UploadConfirmDialog";
5853
import DevtoolsDialog from "./components/views/dialogs/DevtoolsDialog";
5954
import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog";
6055
import InfoDialog from "./components/views/dialogs/InfoDialog";
6156
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
6257
import { shouldShowComponent } from "./customisations/helpers/UIComponents";
6358
import { TimelineRenderingType } from "./contexts/RoomContext";
64-
import { XOR } from "./@types/common";
65-
import { PosthogAnalytics } from "./PosthogAnalytics";
6659
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
6760
import VoipUserMapper from "./VoipUserMapper";
6861
import { htmlSerializeFromMdIfNeeded } from "./editor/serialize";
6962
import { leaveRoomBehaviour } from "./utils/leave-behaviour";
70-
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
71-
import { SdkContextClass } from "./contexts/SDKContext";
7263
import { MatrixClientPeg } from "./MatrixClientPeg";
7364
import { getDeviceCryptoInfo } from "./utils/crypto/deviceInfo";
65+
import { isCurrentLocalRoom, reject, singleMxcUpload, success, successSync } from "./slash-commands/utils";
66+
import { deop, op } from "./slash-commands/op";
67+
import { CommandCategories } from "./slash-commands/interface";
68+
import { Command } from "./slash-commands/command";
7469

75-
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
76-
interface HTMLInputEvent extends Event {
77-
target: HTMLInputElement & EventTarget;
78-
}
79-
80-
const singleMxcUpload = async (cli: MatrixClient): Promise<string | null> => {
81-
return new Promise((resolve) => {
82-
const fileSelector = document.createElement("input");
83-
fileSelector.setAttribute("type", "file");
84-
fileSelector.onchange = (ev: Event) => {
85-
const file = (ev as HTMLInputEvent).target.files?.[0];
86-
if (!file) return;
87-
88-
Modal.createDialog(UploadConfirmDialog, {
89-
file,
90-
onFinished: async (shouldContinue): Promise<void> => {
91-
if (shouldContinue) {
92-
const { content_uri: uri } = await cli.uploadContent(file);
93-
resolve(uri);
94-
} else {
95-
resolve(null);
96-
}
97-
},
98-
});
99-
};
100-
101-
fileSelector.click();
102-
});
103-
};
104-
105-
export const CommandCategories = {
106-
messages: _td("Messages"),
107-
actions: _td("Actions"),
108-
admin: _td("Admin"),
109-
advanced: _td("Advanced"),
110-
effects: _td("Effects"),
111-
other: _td("Other"),
112-
};
113-
114-
export type RunResult = XOR<{ error: Error }, { promise: Promise<IContent | undefined> }>;
115-
116-
type RunFn = (
117-
this: Command,
118-
matrixClient: MatrixClient,
119-
roomId: string,
120-
threadId: string | null,
121-
args?: string,
122-
) => RunResult;
123-
124-
interface ICommandOpts {
125-
command: string;
126-
aliases?: string[];
127-
args?: string;
128-
description: string;
129-
analyticsName?: SlashCommandEvent["command"];
130-
runFn?: RunFn;
131-
category: string;
132-
hideCompletionAfterSpace?: boolean;
133-
isEnabled?(matrixClient: MatrixClient | null): boolean;
134-
renderingTypes?: TimelineRenderingType[];
135-
}
136-
137-
export class Command {
138-
public readonly command: string;
139-
public readonly aliases: string[];
140-
public readonly args?: string;
141-
public readonly description: string;
142-
public readonly runFn?: RunFn;
143-
public readonly category: string;
144-
public readonly hideCompletionAfterSpace: boolean;
145-
public readonly renderingTypes?: TimelineRenderingType[];
146-
public readonly analyticsName?: SlashCommandEvent["command"];
147-
private readonly _isEnabled?: (matrixClient: MatrixClient | null) => boolean;
148-
149-
public constructor(opts: ICommandOpts) {
150-
this.command = opts.command;
151-
this.aliases = opts.aliases || [];
152-
this.args = opts.args || "";
153-
this.description = opts.description;
154-
this.runFn = opts.runFn?.bind(this);
155-
this.category = opts.category || CommandCategories.other;
156-
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
157-
this._isEnabled = opts.isEnabled;
158-
this.renderingTypes = opts.renderingTypes;
159-
this.analyticsName = opts.analyticsName;
160-
}
161-
162-
public getCommand(): string {
163-
return `/${this.command}`;
164-
}
165-
166-
public getCommandWithArgs(): string {
167-
return this.getCommand() + " " + this.args;
168-
}
169-
170-
public run(matrixClient: MatrixClient, roomId: string, threadId: string | null, args?: string): RunResult {
171-
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
172-
if (!this.runFn) {
173-
return reject(new UserFriendlyError("Command error: Unable to handle slash command."));
174-
}
175-
176-
const renderingType = threadId ? TimelineRenderingType.Thread : TimelineRenderingType.Room;
177-
if (this.renderingTypes && !this.renderingTypes?.includes(renderingType)) {
178-
return reject(
179-
new UserFriendlyError("Command error: Unable to find rendering type (%(renderingType)s)", {
180-
renderingType,
181-
cause: undefined,
182-
}),
183-
);
184-
}
185-
186-
if (this.analyticsName) {
187-
PosthogAnalytics.instance.trackEvent<SlashCommandEvent>({
188-
eventName: "SlashCommand",
189-
command: this.analyticsName,
190-
});
191-
}
192-
193-
return this.runFn(matrixClient, roomId, threadId, args);
194-
}
195-
196-
public getUsage(): string {
197-
return _t("Usage") + ": " + this.getCommandWithArgs();
198-
}
199-
200-
public isEnabled(cli: MatrixClient | null): boolean {
201-
return this._isEnabled?.(cli) ?? true;
202-
}
203-
}
204-
205-
function reject(error?: any): RunResult {
206-
return { error };
207-
}
208-
209-
function success(promise: Promise<any> = Promise.resolve()): RunResult {
210-
return { promise };
211-
}
212-
213-
function successSync(value: any): RunResult {
214-
return success(Promise.resolve(value));
215-
}
216-
217-
const isCurrentLocalRoom = (cli: MatrixClient | null): boolean => {
218-
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
219-
if (!roomId) return false;
220-
const room = cli?.getRoom(roomId);
221-
if (!room) return false;
222-
return isLocalRoom(room);
223-
};
224-
225-
const canAffectPowerlevels = (cli: MatrixClient | null): boolean => {
226-
const roomId = SdkContextClass.instance.roomViewStore.getRoomId();
227-
if (!cli || !roomId) return false;
228-
const room = cli?.getRoom(roomId);
229-
return !!room?.currentState.maySendStateEvent(EventType.RoomPowerLevels, cli.getSafeUserId()) && !isLocalRoom(room);
230-
};
231-
232-
/* Disable the "unexpected this" error for these commands - all of the run
233-
* functions are called with `this` bound to the Command instance.
234-
*/
70+
export { CommandCategories, Command };
23571

23672
export const Commands = [
23773
new Command({
@@ -886,78 +722,8 @@ export const Commands = [
886722
},
887723
category: CommandCategories.actions,
888724
}),
889-
new Command({
890-
command: "op",
891-
args: "<user-id> [<power-level>]",
892-
description: _td("Define the power level of a user"),
893-
isEnabled: canAffectPowerlevels,
894-
runFn: function (cli, roomId, threadId, args) {
895-
if (args) {
896-
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
897-
let powerLevel = 50; // default power level for op
898-
if (matches) {
899-
const userId = matches[1];
900-
if (matches.length === 4 && undefined !== matches[3]) {
901-
powerLevel = parseInt(matches[3], 10);
902-
}
903-
if (!isNaN(powerLevel)) {
904-
const room = cli.getRoom(roomId);
905-
if (!room) {
906-
return reject(
907-
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
908-
roomId,
909-
cause: undefined,
910-
}),
911-
);
912-
}
913-
const member = room.getMember(userId);
914-
if (
915-
!member?.membership ||
916-
getEffectiveMembership(member.membership) === EffectiveMembership.Leave
917-
) {
918-
return reject(new UserFriendlyError("Could not find user in room"));
919-
}
920-
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
921-
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
922-
}
923-
}
924-
}
925-
return reject(this.getUsage());
926-
},
927-
category: CommandCategories.admin,
928-
renderingTypes: [TimelineRenderingType.Room],
929-
}),
930-
new Command({
931-
command: "deop",
932-
args: "<user-id>",
933-
description: _td("Deops user with given id"),
934-
isEnabled: canAffectPowerlevels,
935-
runFn: function (cli, roomId, threadId, args) {
936-
if (args) {
937-
const matches = args.match(/^(\S+)$/);
938-
if (matches) {
939-
const room = cli.getRoom(roomId);
940-
if (!room) {
941-
return reject(
942-
new UserFriendlyError("Command failed: Unable to find room (%(roomId)s", {
943-
roomId,
944-
cause: undefined,
945-
}),
946-
);
947-
}
948-
949-
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
950-
if (!powerLevelEvent?.getContent().users[args]) {
951-
return reject(new UserFriendlyError("Could not find user in room"));
952-
}
953-
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
954-
}
955-
}
956-
return reject(this.getUsage());
957-
},
958-
category: CommandCategories.admin,
959-
renderingTypes: [TimelineRenderingType.Room],
960-
}),
725+
op,
726+
deop,
961727
new Command({
962728
command: "devtools",
963729
description: _td("Opens the Developer Tools dialog"),

src/components/views/right_panel/UserInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ export const UserOptionsSection: React.FC<{
513513
);
514514
};
515515

516-
const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
516+
export const warnSelfDemote = async (isSpace: boolean): Promise<boolean> => {
517517
const { finished } = Modal.createDialog(QuestionDialog, {
518518
title: _t("Demote yourself?"),
519519
description: (

src/i18n/strings/en_EN.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -409,14 +409,6 @@
409409
"Go Back": "Go Back",
410410
"Cancel": "Cancel",
411411
"Setting up keys": "Setting up keys",
412-
"Messages": "Messages",
413-
"Actions": "Actions",
414-
"Advanced": "Advanced",
415-
"Effects": "Effects",
416-
"Other": "Other",
417-
"Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.",
418-
"Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)",
419-
"Usage": "Usage",
420412
"Sends the given message as a spoiler": "Sends the given message as a spoiler",
421413
"Prepends ¯\\_(ツ)_/¯ to a plain-text message": "Prepends ¯\\_(ツ)_/¯ to a plain-text message",
422414
"Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message": "Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message",
@@ -455,10 +447,6 @@
455447
"Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward",
456448
"Unignored user": "Unignored user",
457449
"You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s",
458-
"Define the power level of a user": "Define the power level of a user",
459-
"Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s",
460-
"Could not find user in room": "Could not find user in room",
461-
"Deops user with given id": "Deops user with given id",
462450
"Opens the Developer Tools dialog": "Opens the Developer Tools dialog",
463451
"Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room",
464452
"Please supply a widget URL or embed code": "Please supply a widget URL or embed code",
@@ -938,6 +926,18 @@
938926
"Unsent": "Unsent",
939927
"unknown": "unknown",
940928
"Change notification settings": "Change notification settings",
929+
"Command error: Unable to handle slash command.": "Command error: Unable to handle slash command.",
930+
"Command error: Unable to find rendering type (%(renderingType)s)": "Command error: Unable to find rendering type (%(renderingType)s)",
931+
"Usage": "Usage",
932+
"Messages": "Messages",
933+
"Actions": "Actions",
934+
"Advanced": "Advanced",
935+
"Effects": "Effects",
936+
"Other": "Other",
937+
"Command failed: Unable to find room (%(roomId)s": "Command failed: Unable to find room (%(roomId)s",
938+
"Could not find user in room": "Could not find user in room",
939+
"Define the power level of a user": "Define the power level of a user",
940+
"Deops user with given id": "Deops user with given id",
941941
"Messaging": "Messaging",
942942
"Profile": "Profile",
943943
"Spaces": "Spaces",

0 commit comments

Comments
 (0)