2015-09-18 14:54:20 +02:00
|
|
|
|
/*
|
2016-01-07 05:06:39 +01:00
|
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2018-08-29 19:09:37 +02:00
|
|
|
|
Copyright 2018 New Vector Ltd
|
2019-05-09 23:12:21 +02:00
|
|
|
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
2020-03-30 14:59:08 +02:00
|
|
|
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
import * as React from 'react';
|
2021-06-18 17:21:46 +02:00
|
|
|
|
import { User } from "matrix-js-sdk/src/models/user";
|
2020-03-30 14:59:08 +02:00
|
|
|
|
|
2021-04-12 22:34:24 +02:00
|
|
|
|
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
2020-05-14 04:41:41 +02:00
|
|
|
|
import dis from './dispatcher/dispatcher';
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { _t, _td } from './languageHandler';
|
2016-09-13 12:11:52 +02:00
|
|
|
|
import Modal from './Modal';
|
2018-11-29 23:05:53 +01:00
|
|
|
|
import MultiInviter from './utils/MultiInviter';
|
2019-01-31 22:35:58 +01:00
|
|
|
|
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
2019-03-15 00:24:22 +01:00
|
|
|
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
2019-03-24 07:07:00 +01:00
|
|
|
|
import WidgetUtils from "./utils/WidgetUtils";
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { textToHtmlRainbow } from "./utils/colour";
|
2019-08-30 19:29:07 +02:00
|
|
|
|
import { getAddressType } from './UserAddress';
|
|
|
|
|
import { abbreviateUrl } from './utils/UrlUtils';
|
|
|
|
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
2020-04-09 23:25:11 +02:00
|
|
|
|
import { WidgetType } from "./widgets/WidgetType";
|
2020-04-10 00:02:49 +02:00
|
|
|
|
import { Jitsi } from "./widgets/Jitsi";
|
2021-05-07 11:11:56 +02:00
|
|
|
|
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
2020-09-09 22:53:38 +02:00
|
|
|
|
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
2020-05-11 11:54:28 +02:00
|
|
|
|
import { ensureDMExists } from "./createRoom";
|
2020-05-14 05:03:12 +02:00
|
|
|
|
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
|
|
|
|
import { Action } from "./dispatcher/actions";
|
2020-08-20 03:21:40 +02:00
|
|
|
|
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
2020-09-15 16:49:25 +02:00
|
|
|
|
import SdkConfig from "./SdkConfig";
|
2020-08-24 10:43:41 +02:00
|
|
|
|
import SettingsStore from "./settings/SettingsStore";
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { UIFeature } from "./settings/UIFeature";
|
|
|
|
|
import { CHAT_EFFECTS } from "./effects";
|
2020-10-29 18:56:24 +01:00
|
|
|
|
import CallHandler from "./CallHandler";
|
2021-06-29 14:11:58 +02:00
|
|
|
|
import { guessAndSetDMRoom } from "./Rooms";
|
2021-07-02 15:51:55 +02:00
|
|
|
|
import { upgradeRoom } from './utils/RoomUpgrade';
|
2021-07-03 11:24:33 +02:00
|
|
|
|
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
|
|
|
|
|
import ErrorDialog from './components/views/dialogs/ErrorDialog';
|
|
|
|
|
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
|
|
|
|
|
import RoomUpgradeWarningDialog from "./components/views/dialogs/RoomUpgradeWarningDialog";
|
|
|
|
|
import InfoDialog from "./components/views/dialogs/InfoDialog";
|
|
|
|
|
import SlashCommandHelpDialog from "./components/views/dialogs/SlashCommandHelpDialog";
|
2016-01-14 15:39:58 +01:00
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
|
|
|
|
interface HTMLInputEvent extends Event {
|
|
|
|
|
target: HTMLInputElement & EventTarget;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const singleMxcUpload = async (): Promise<any> => {
|
2019-06-27 20:38:12 +02:00
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const fileSelector = document.createElement('input');
|
|
|
|
|
fileSelector.setAttribute('type', 'file');
|
2020-03-30 14:59:08 +02:00
|
|
|
|
fileSelector.onchange = (ev: HTMLInputEvent) => {
|
2019-06-27 20:38:12 +02:00
|
|
|
|
const file = ev.target.files[0];
|
|
|
|
|
|
|
|
|
|
Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, {
|
|
|
|
|
file,
|
|
|
|
|
onFinished: (shouldContinue) => {
|
2019-06-29 08:05:43 +02:00
|
|
|
|
resolve(shouldContinue ? MatrixClientPeg.get().uploadContent(file) : null);
|
2019-06-27 20:38:12 +02:00
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fileSelector.click();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2019-08-06 19:03:38 +02:00
|
|
|
|
export const CommandCategories = {
|
|
|
|
|
"messages": _td("Messages"),
|
|
|
|
|
"actions": _td("Actions"),
|
|
|
|
|
"admin": _td("Admin"),
|
|
|
|
|
"advanced": _td("Advanced"),
|
2020-10-19 21:25:01 +02:00
|
|
|
|
"effects": _td("Effects"),
|
2019-08-06 19:03:38 +02:00
|
|
|
|
"other": _td("Other"),
|
|
|
|
|
};
|
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
type RunFn = ((roomId: string, args: string, cmd: string) => {error: any} | {promise: Promise<any>});
|
|
|
|
|
|
2020-03-31 12:53:26 +02:00
|
|
|
|
interface ICommandOpts {
|
|
|
|
|
command: string;
|
|
|
|
|
aliases?: string[];
|
|
|
|
|
args?: string;
|
|
|
|
|
description: string;
|
|
|
|
|
runFn?: RunFn;
|
|
|
|
|
category: string;
|
|
|
|
|
hideCompletionAfterSpace?: boolean;
|
2020-09-15 16:49:25 +02:00
|
|
|
|
isEnabled?(): boolean;
|
2020-03-31 12:53:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 20:00:54 +02:00
|
|
|
|
export class Command {
|
2020-03-30 14:59:08 +02:00
|
|
|
|
command: string;
|
|
|
|
|
aliases: string[];
|
|
|
|
|
args: undefined | string;
|
|
|
|
|
description: string;
|
|
|
|
|
runFn: undefined | RunFn;
|
|
|
|
|
category: string;
|
|
|
|
|
hideCompletionAfterSpace: boolean;
|
2020-09-15 16:49:25 +02:00
|
|
|
|
_isEnabled?: () => boolean;
|
2020-03-30 14:59:08 +02:00
|
|
|
|
|
2020-03-31 12:53:26 +02:00
|
|
|
|
constructor(opts: ICommandOpts) {
|
|
|
|
|
this.command = opts.command;
|
|
|
|
|
this.aliases = opts.aliases || [];
|
|
|
|
|
this.args = opts.args || "";
|
|
|
|
|
this.description = opts.description;
|
|
|
|
|
this.runFn = opts.runFn;
|
|
|
|
|
this.category = opts.category || CommandCategories.other;
|
|
|
|
|
this.hideCompletionAfterSpace = opts.hideCompletionAfterSpace || false;
|
2020-09-15 16:49:25 +02:00
|
|
|
|
this._isEnabled = opts.isEnabled;
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCommand() {
|
2020-03-30 14:59:08 +02:00
|
|
|
|
return `/${this.command}`;
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getCommandWithArgs() {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return this.getCommand() + " " + this.args;
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-02-25 20:39:20 +01:00
|
|
|
|
run(roomId: string, args: string) {
|
2020-01-21 17:50:04 +01:00
|
|
|
|
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
2020-06-16 01:41:21 +02:00
|
|
|
|
if (!this.runFn) return reject(_t("Command error"));
|
2021-02-25 20:39:20 +01:00
|
|
|
|
return this.runFn.bind(this)(roomId, args);
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getUsage() {
|
2017-05-23 16:16:31 +02:00
|
|
|
|
return _t('Usage') + ': ' + this.getCommandWithArgs();
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
2020-09-15 16:49:25 +02:00
|
|
|
|
|
|
|
|
|
isEnabled() {
|
|
|
|
|
return this._isEnabled ? this._isEnabled() : true;
|
|
|
|
|
}
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-18 20:31:40 +02:00
|
|
|
|
function reject(error) {
|
2021-06-29 14:11:58 +02:00
|
|
|
|
return { error };
|
2017-05-23 10:44:11 +02:00
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
function success(promise?: Promise<any>) {
|
2021-06-29 14:11:58 +02:00
|
|
|
|
return { promise };
|
2017-05-23 10:44:11 +02:00
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
2021-06-17 12:37:06 +02:00
|
|
|
|
function successSync(value: any) {
|
|
|
|
|
return success(Promise.resolve(value));
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-23 10:44:11 +02:00
|
|
|
|
/* Disable the "unexpected this" error for these commands - all of the run
|
|
|
|
|
* functions are called with `this` bound to the Command instance.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
export const Commands = [
|
2021-02-25 22:59:27 +01:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'spoiler',
|
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Sends the given message as a spoiler'),
|
|
|
|
|
runFn: function(roomId, message) {
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeHtmlMessage(
|
2021-03-25 23:16:53 +01:00
|
|
|
|
message,
|
2021-02-25 22:59:27 +01:00
|
|
|
|
`<span data-mx-spoiler>${message}</span>`,
|
|
|
|
|
));
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'shrug',
|
2019-02-19 00:27:22 +01:00
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Prepends ¯\\_(ツ)_/¯ to a plain-text message'),
|
|
|
|
|
runFn: function(roomId, args) {
|
2019-02-20 06:46:17 +01:00
|
|
|
|
let message = '¯\\_(ツ)_/¯';
|
2019-02-19 00:27:22 +01:00
|
|
|
|
if (args) {
|
2019-02-20 06:46:17 +01:00
|
|
|
|
message = message + ' ' + args;
|
2019-02-19 00:27:22 +01:00
|
|
|
|
}
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeTextMessage(message));
|
2019-02-20 06:46:17 +01:00
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.messages,
|
2019-02-19 00:27:22 +01:00
|
|
|
|
}),
|
2020-12-10 12:19:13 +01:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'tableflip',
|
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
let message = '(╯°□°)╯︵ ┻━┻';
|
|
|
|
|
if (args) {
|
|
|
|
|
message = message + ' ' + args;
|
|
|
|
|
}
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeTextMessage(message));
|
2020-12-10 12:19:13 +01:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-12-10 12:19:30 +01:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'unflip',
|
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Prepends ┬──┬ ノ( ゜-゜ノ) to a plain-text message'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
let message = '┬──┬ ノ( ゜-゜ノ)';
|
|
|
|
|
if (args) {
|
|
|
|
|
message = message + ' ' + args;
|
|
|
|
|
}
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeTextMessage(message));
|
2020-12-10 12:19:30 +01:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-08-29 13:29:43 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'lenny',
|
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Prepends ( ͡° ͜ʖ ͡°) to a plain-text message'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
let message = '( ͡° ͜ʖ ͡°)';
|
|
|
|
|
if (args) {
|
|
|
|
|
message = message + ' ' + args;
|
|
|
|
|
}
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeTextMessage(message));
|
2020-08-29 13:29:43 +02:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'plain',
|
2019-09-02 17:44:31 +02:00
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Sends a message as plain text, without interpreting it as markdown'),
|
|
|
|
|
runFn: function(roomId, messages) {
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeTextMessage(messages));
|
2019-09-02 17:44:31 +02:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-03-31 12:49:53 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'html',
|
2020-03-29 21:45:06 +02:00
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Sends a message as html, without interpreting it as markdown'),
|
|
|
|
|
runFn: function(roomId, messages) {
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
|
2020-03-29 21:45:06 +02:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'ddg',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<query>',
|
|
|
|
|
description: _td('Searches DuckDuckGo for results'),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
runFn: function() {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
// TODO Don't explain this away, actually show a search UI here.
|
|
|
|
|
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
|
|
|
|
title: _t('/ddg is not a command'),
|
|
|
|
|
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
|
|
|
|
});
|
|
|
|
|
return success();
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2018-07-18 16:38:21 +02:00
|
|
|
|
hideCompletionAfterSpace: true,
|
2016-09-13 12:11:52 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'upgraderoom',
|
2019-01-17 23:59:05 +01:00
|
|
|
|
args: '<new_version>',
|
|
|
|
|
description: _td('Upgrades a room to a new version'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
2019-08-28 13:00:37 +02:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const room = cli.getRoom(roomId);
|
|
|
|
|
if (!room.currentState.mayClientSendStateEvent("m.room.tombstone", cli)) {
|
|
|
|
|
return reject(_t("You do not have the required permissions to use this command."));
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 14:11:58 +02:00
|
|
|
|
const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
|
|
|
|
|
RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null,
|
2019-12-03 01:26:08 +01:00
|
|
|
|
/*isPriority=*/false, /*isStatic=*/true);
|
2019-11-28 04:29:11 +01:00
|
|
|
|
|
|
|
|
|
return success(finished.then(async ([resp]) => {
|
2021-07-02 15:51:55 +02:00
|
|
|
|
if (!resp?.continue) return;
|
|
|
|
|
await upgradeRoom(room, args, resp.invite);
|
2019-08-28 13:00:37 +02:00
|
|
|
|
}));
|
2019-01-17 23:59:05 +01:00
|
|
|
|
}
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2019-01-17 23:59:05 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'nick',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<display_name>',
|
|
|
|
|
description: _td('Changes your display nickname'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
return success(MatrixClientPeg.get().setDisplayName(args));
|
|
|
|
|
}
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'myroomnick',
|
|
|
|
|
aliases: ['roomnick'],
|
2019-02-24 02:36:47 +01:00
|
|
|
|
args: '<display_name>',
|
|
|
|
|
description: _td('Changes your display nickname in the current room only'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId());
|
|
|
|
|
const content = {
|
2019-02-25 23:15:50 +01:00
|
|
|
|
...ev ? ev.getContent() : { membership: 'join' },
|
2020-03-30 15:13:08 +02:00
|
|
|
|
displayname: args,
|
2019-02-24 02:36:47 +01:00
|
|
|
|
};
|
|
|
|
|
return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId()));
|
|
|
|
|
}
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2019-02-24 02:36:47 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'roomavatar',
|
2019-09-18 17:33:56 +02:00
|
|
|
|
args: '[<mxc_url>]',
|
|
|
|
|
description: _td('Changes the avatar of the current room'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
let promise = Promise.resolve(args);
|
|
|
|
|
if (!args) {
|
|
|
|
|
promise = singleMxcUpload();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return success(promise.then((url) => {
|
|
|
|
|
if (!url) return;
|
2021-06-29 14:11:58 +02:00
|
|
|
|
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
2019-09-18 17:33:56 +02:00
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.actions,
|
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'myroomavatar',
|
2019-05-10 21:28:28 +02:00
|
|
|
|
args: '[<mxc_url>]',
|
2019-05-09 23:12:21 +02:00
|
|
|
|
description: _td('Changes your avatar in this current room only'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const room = cli.getRoom(roomId);
|
|
|
|
|
const userId = cli.getUserId();
|
|
|
|
|
|
|
|
|
|
let promise = Promise.resolve(args);
|
|
|
|
|
if (!args) {
|
2019-06-27 20:41:29 +02:00
|
|
|
|
promise = singleMxcUpload();
|
2019-05-09 23:12:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return success(promise.then((url) => {
|
2019-06-29 08:05:43 +02:00
|
|
|
|
if (!url) return;
|
2019-05-09 23:12:21 +02:00
|
|
|
|
const ev = room.currentState.getStateEvents('m.room.member', userId);
|
|
|
|
|
const content = {
|
|
|
|
|
...ev ? ev.getContent() : { membership: 'join' },
|
|
|
|
|
avatar_url: url,
|
|
|
|
|
};
|
|
|
|
|
return cli.sendStateEvent(roomId, 'm.room.member', content, userId);
|
|
|
|
|
}));
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2019-05-09 23:12:21 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'myavatar',
|
2019-06-27 20:38:12 +02:00
|
|
|
|
args: '[<mxc_url>]',
|
|
|
|
|
description: _td('Changes your avatar in all rooms'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
let promise = Promise.resolve(args);
|
|
|
|
|
if (!args) {
|
2019-06-27 20:41:29 +02:00
|
|
|
|
promise = singleMxcUpload();
|
2019-06-27 20:38:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return success(promise.then((url) => {
|
2019-06-29 08:05:43 +02:00
|
|
|
|
if (!url) return;
|
2019-06-27 20:38:12 +02:00
|
|
|
|
return MatrixClientPeg.get().setAvatarUrl(url);
|
|
|
|
|
}));
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2019-06-27 20:38:12 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'topic',
|
2019-01-30 11:22:05 +01:00
|
|
|
|
args: '[<topic>]',
|
|
|
|
|
description: _td('Gets or sets the room topic'),
|
2018-06-18 20:31:40 +02:00
|
|
|
|
runFn: function(roomId, args) {
|
2019-01-30 11:22:05 +01:00
|
|
|
|
const cli = MatrixClientPeg.get();
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (args) {
|
2019-01-30 11:22:05 +01:00
|
|
|
|
return success(cli.setRoomTopic(roomId, args));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
2019-01-30 11:22:05 +01:00
|
|
|
|
const room = cli.getRoom(roomId);
|
2020-04-11 17:41:07 +02:00
|
|
|
|
if (!room) return reject(_t("Failed to set topic"));
|
2019-01-30 11:22:05 +01:00
|
|
|
|
|
|
|
|
|
const topicEvents = room.currentState.getStateEvents('m.room.topic', '');
|
2019-02-06 20:10:44 +01:00
|
|
|
|
const topic = topicEvents && topicEvents.getContent().topic;
|
2019-01-31 22:35:58 +01:00
|
|
|
|
const topicHtml = topic ? linkifyAndSanitizeHtml(topic) : _t('This room has no topic.');
|
2019-01-30 11:22:05 +01:00
|
|
|
|
|
2019-01-31 18:00:37 +01:00
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'Topic', InfoDialog, {
|
2019-01-30 11:22:05 +01:00
|
|
|
|
title: room.name,
|
|
|
|
|
description: <div dangerouslySetInnerHTML={{ __html: topicHtml }} />,
|
2020-05-12 11:51:27 +02:00
|
|
|
|
hasCloseButton: true,
|
2019-01-30 11:22:05 +01:00
|
|
|
|
});
|
|
|
|
|
return success();
|
2018-06-18 20:31:40 +02:00
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'roomname',
|
2019-01-04 00:42:17 +01:00
|
|
|
|
args: '<name>',
|
|
|
|
|
description: _td('Sets the room name'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
return success(MatrixClientPeg.get().setRoomName(roomId, args));
|
|
|
|
|
}
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2019-01-04 00:42:17 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'invite',
|
2021-02-26 23:10:20 +01:00
|
|
|
|
args: '<user-id> [<reason>]',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
description: _td('Invites user with given id to current room'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
2021-02-26 23:10:20 +01:00
|
|
|
|
const [address, reason] = args.split(/\s+(.+)/);
|
|
|
|
|
if (address) {
|
2018-11-29 23:05:53 +01:00
|
|
|
|
// We use a MultiInviter to re-use the invite logic, even though
|
|
|
|
|
// we're only inviting one user.
|
2019-08-30 19:29:07 +02:00
|
|
|
|
// If we need an identity server but don't have one, things
|
|
|
|
|
// get a bit more complex here, but we try to show something
|
|
|
|
|
// meaningful.
|
2020-07-13 01:19:15 +02:00
|
|
|
|
let prom = Promise.resolve();
|
2019-08-30 19:29:07 +02:00
|
|
|
|
if (
|
|
|
|
|
getAddressType(address) === 'email' &&
|
|
|
|
|
!MatrixClientPeg.get().getIdentityServerUrl()
|
|
|
|
|
) {
|
|
|
|
|
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
|
|
|
|
if (defaultIdentityServerUrl) {
|
2020-07-13 01:19:15 +02:00
|
|
|
|
const { finished } = Modal.createTrackedDialog<[boolean]>(
|
|
|
|
|
'Slash Commands',
|
|
|
|
|
'Identity server',
|
2019-08-30 19:29:07 +02:00
|
|
|
|
QuestionDialog, {
|
|
|
|
|
title: _t("Use an identity server"),
|
2021-07-19 23:43:11 +02:00
|
|
|
|
description: <p>{ _t(
|
2019-08-30 19:29:07 +02:00
|
|
|
|
"Use an identity server to invite by email. " +
|
|
|
|
|
"Click continue to use the default identity server " +
|
|
|
|
|
"(%(defaultIdentityServerName)s) or manage in Settings.",
|
|
|
|
|
{
|
|
|
|
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
|
|
|
|
},
|
2021-07-19 23:43:11 +02:00
|
|
|
|
) }</p>,
|
2019-08-30 19:29:07 +02:00
|
|
|
|
button: _t("Continue"),
|
|
|
|
|
},
|
2020-07-13 01:19:15 +02:00
|
|
|
|
);
|
2020-04-01 17:53:25 +02:00
|
|
|
|
|
2020-07-13 01:19:15 +02:00
|
|
|
|
prom = finished.then(([useDefault]) => {
|
2020-04-01 17:53:25 +02:00
|
|
|
|
if (useDefault) {
|
|
|
|
|
useDefaultIdentityServer();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
throw new Error(_t("Use an identity server to invite by email. Manage in Settings."));
|
|
|
|
|
});
|
2019-08-30 19:29:07 +02:00
|
|
|
|
} else {
|
|
|
|
|
return reject(_t("Use an identity server to invite by email. Manage in Settings."));
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-29 23:05:53 +01:00
|
|
|
|
const inviter = new MultiInviter(roomId);
|
2020-07-13 01:19:15 +02:00
|
|
|
|
return success(prom.then(() => {
|
2021-02-26 23:10:20 +01:00
|
|
|
|
return inviter.invite([address], reason);
|
2019-08-30 19:29:07 +02:00
|
|
|
|
}).then(() => {
|
|
|
|
|
if (inviter.getCompletionState(address) !== "invited") {
|
|
|
|
|
throw new Error(inviter.getErrorText(address));
|
2018-11-29 23:16:45 +01:00
|
|
|
|
}
|
2018-11-29 23:05:53 +01:00
|
|
|
|
}));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'join',
|
|
|
|
|
aliases: ['j', 'goto'],
|
2020-04-14 11:06:57 +02:00
|
|
|
|
args: '<room-address>',
|
|
|
|
|
description: _td('Joins room with given address'),
|
2021-07-19 23:43:11 +02:00
|
|
|
|
runFn: function(roomId, args) {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (args) {
|
2018-11-08 00:53:29 +01:00
|
|
|
|
// Note: we support 2 versions of this command. The first is
|
|
|
|
|
// the public-facing one for most users and the other is a
|
|
|
|
|
// power-user edition where someone may join via permalink or
|
|
|
|
|
// room ID with optional servers. Practically, this results
|
|
|
|
|
// in the following variations:
|
|
|
|
|
// /join #example:example.org
|
|
|
|
|
// /join !example:example.org
|
|
|
|
|
// /join !example:example.org altserver.com elsewhere.ca
|
|
|
|
|
// /join https://matrix.to/#/!example:example.org?via=altserver.com
|
|
|
|
|
// The command also supports event permalinks transparently:
|
|
|
|
|
// /join https://matrix.to/#/!example:example.org/$something:example.org
|
|
|
|
|
// /join https://matrix.to/#/!example:example.org/$something:example.org?via=altserver.com
|
|
|
|
|
const params = args.split(' ');
|
|
|
|
|
if (params.length < 1) return reject(this.getUsage());
|
|
|
|
|
|
2019-10-01 00:06:00 +02:00
|
|
|
|
let isPermalink = false;
|
|
|
|
|
if (params[0].startsWith("http:") || params[0].startsWith("https:")) {
|
|
|
|
|
// It's at least a URL - try and pull out a hostname to check against the
|
|
|
|
|
// permalink handler
|
|
|
|
|
const parsedUrl = new URL(params[0]);
|
|
|
|
|
const hostname = parsedUrl.host || parsedUrl.hostname; // takes first non-falsey value
|
|
|
|
|
|
2020-08-03 17:02:26 +02:00
|
|
|
|
// if we're using a Element permalink handler, this will catch it before we get much further.
|
2019-10-01 00:06:00 +02:00
|
|
|
|
// see below where we make assumptions about parsing the URL.
|
|
|
|
|
if (isPermalinkHost(hostname)) {
|
|
|
|
|
isPermalink = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-11-08 00:53:29 +01:00
|
|
|
|
if (params[0][0] === '#') {
|
|
|
|
|
let roomAlias = params[0];
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (!roomAlias.includes(':')) {
|
|
|
|
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
2018-06-18 20:31:40 +02:00
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
room_alias: roomAlias,
|
|
|
|
|
auto_join: true,
|
2020-10-29 16:53:14 +01:00
|
|
|
|
_type: "slash_command", // instrumentation
|
2018-06-18 20:31:40 +02:00
|
|
|
|
});
|
2018-11-08 00:53:29 +01:00
|
|
|
|
return success();
|
2018-11-08 01:01:47 +01:00
|
|
|
|
} else if (params[0][0] === '!') {
|
2020-06-29 17:27:59 +02:00
|
|
|
|
const [roomId, ...viaServers] = params;
|
2018-11-08 00:53:29 +01:00
|
|
|
|
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
room_id: roomId,
|
|
|
|
|
opts: {
|
|
|
|
|
// These are passed down to the js-sdk's /join call
|
2019-06-29 00:26:28 +02:00
|
|
|
|
viaServers: viaServers,
|
2018-11-08 00:53:29 +01:00
|
|
|
|
},
|
2019-06-29 00:26:28 +02:00
|
|
|
|
via_servers: viaServers, // for the rejoin button
|
2018-11-08 00:53:29 +01:00
|
|
|
|
auto_join: true,
|
2020-10-29 16:53:14 +01:00
|
|
|
|
_type: "slash_command", // instrumentation
|
2018-11-08 00:53:29 +01:00
|
|
|
|
});
|
|
|
|
|
return success();
|
2019-10-01 00:06:00 +02:00
|
|
|
|
} else if (isPermalink) {
|
|
|
|
|
const permalinkParts = parsePermalink(params[0]);
|
2018-11-08 00:53:29 +01:00
|
|
|
|
|
2019-10-01 00:06:00 +02:00
|
|
|
|
// This check technically isn't needed because we already did our
|
|
|
|
|
// safety checks up above. However, for good measure, let's be sure.
|
|
|
|
|
if (!permalinkParts) {
|
|
|
|
|
return reject(this.getUsage());
|
2018-11-08 00:53:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-01 00:06:00 +02:00
|
|
|
|
// If for some reason someone wanted to join a group or user, we should
|
|
|
|
|
// stop them now.
|
|
|
|
|
if (!permalinkParts.roomIdOrAlias) {
|
|
|
|
|
return reject(this.getUsage());
|
2018-11-08 00:53:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-01 00:06:00 +02:00
|
|
|
|
const entity = permalinkParts.roomIdOrAlias;
|
|
|
|
|
const viaServers = permalinkParts.viaServers;
|
|
|
|
|
const eventId = permalinkParts.eventId;
|
|
|
|
|
|
2018-11-08 00:53:29 +01:00
|
|
|
|
const dispatch = {
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
auto_join: true,
|
2020-10-29 16:53:14 +01:00
|
|
|
|
_type: "slash_command", // instrumentation
|
2018-11-08 00:53:29 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (entity[0] === '!') dispatch["room_id"] = entity;
|
|
|
|
|
else dispatch["room_alias"] = entity;
|
|
|
|
|
|
|
|
|
|
if (eventId) {
|
|
|
|
|
dispatch["event_id"] = eventId;
|
|
|
|
|
dispatch["highlighted"] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (viaServers) {
|
2019-06-29 00:26:28 +02:00
|
|
|
|
// For the join
|
2018-11-08 00:53:29 +01:00
|
|
|
|
dispatch["opts"] = {
|
|
|
|
|
// These are passed down to the js-sdk's /join call
|
2019-06-29 00:26:28 +02:00
|
|
|
|
viaServers: viaServers,
|
2018-11-08 01:01:47 +01:00
|
|
|
|
};
|
2019-06-29 00:26:28 +02:00
|
|
|
|
|
|
|
|
|
// For if the join fails (rejoin button)
|
|
|
|
|
dispatch['via_servers'] = viaServers;
|
2018-11-08 00:53:29 +01:00
|
|
|
|
}
|
2015-12-28 03:58:40 +01:00
|
|
|
|
|
2018-11-08 00:53:29 +01:00
|
|
|
|
dis.dispatch(dispatch);
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return success();
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'part',
|
2020-04-14 11:06:57 +02:00
|
|
|
|
args: '[<room-address>]',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
description: _td('Leave room'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
|
|
|
|
|
let targetRoomId;
|
|
|
|
|
if (args) {
|
|
|
|
|
const matches = args.match(/^(\S+)$/);
|
|
|
|
|
if (matches) {
|
|
|
|
|
let roomAlias = matches[1];
|
|
|
|
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
|
|
|
|
|
|
|
|
|
if (!roomAlias.includes(':')) {
|
|
|
|
|
roomAlias += ':' + cli.getDomain();
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
2018-06-18 20:31:40 +02:00
|
|
|
|
// Try to find a room with this alias
|
|
|
|
|
const rooms = cli.getRooms();
|
|
|
|
|
for (let i = 0; i < rooms.length; i++) {
|
|
|
|
|
const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases');
|
|
|
|
|
for (let j = 0; j < aliasEvents.length; j++) {
|
|
|
|
|
const aliases = aliasEvents[j].getContent().aliases || [];
|
|
|
|
|
for (let k = 0; k < aliases.length; k++) {
|
|
|
|
|
if (aliases[k] === roomAlias) {
|
|
|
|
|
targetRoomId = rooms[i].roomId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (targetRoomId) break;
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (targetRoomId) break;
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2020-04-14 11:06:57 +02:00
|
|
|
|
if (!targetRoomId) return reject(_t('Unrecognised room address:') + ' ' + roomAlias);
|
2017-05-23 10:44:11 +02:00
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
|
|
|
|
|
if (!targetRoomId) targetRoomId = roomId;
|
2020-08-20 03:21:40 +02:00
|
|
|
|
return success(leaveRoomBehaviour(targetRoomId));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'kick',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id> [reason]',
|
|
|
|
|
description: _td('Kicks user with given id'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
|
|
|
|
if (matches) {
|
|
|
|
|
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3]));
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'ban',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id> [reason]',
|
|
|
|
|
description: _td('Bans user with given id'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
|
|
|
|
if (matches) {
|
|
|
|
|
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3]));
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'unban',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id>',
|
2019-05-16 22:35:43 +02:00
|
|
|
|
description: _td('Unbans user with given ID'),
|
2018-06-18 20:31:40 +02:00
|
|
|
|
runFn: function(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]));
|
|
|
|
|
}
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'ignore',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id>',
|
|
|
|
|
description: _td('Ignores a user, hiding their messages from you'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
|
2020-07-03 23:22:33 +02:00
|
|
|
|
const matches = args.match(/^(@[^:]+:\S+)$/);
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (matches) {
|
|
|
|
|
const userId = matches[1];
|
|
|
|
|
const ignoredUsers = cli.getIgnoredUsers();
|
|
|
|
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
|
|
|
|
return success(
|
|
|
|
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
2019-02-06 19:43:55 +01:00
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
title: _t('Ignored user'),
|
|
|
|
|
description: <div>
|
2021-06-29 14:11:58 +02:00
|
|
|
|
<p>{ _t('You are now ignoring %(userId)s', { userId }) }</p>
|
2018-06-18 20:31:40 +02:00
|
|
|
|
</div>,
|
|
|
|
|
});
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-09-15 00:08:51 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2017-09-15 00:08:51 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'unignore',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id>',
|
|
|
|
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
|
2020-07-03 23:22:33 +02:00
|
|
|
|
const matches = args.match(/(^@[^:]+:\S+$)/);
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (matches) {
|
|
|
|
|
const userId = matches[1];
|
|
|
|
|
const ignoredUsers = cli.getIgnoredUsers();
|
|
|
|
|
const index = ignoredUsers.indexOf(userId);
|
|
|
|
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
|
|
|
|
return success(
|
|
|
|
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
2019-02-06 19:43:55 +01:00
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
title: _t('Unignored user'),
|
|
|
|
|
description: <div>
|
2021-06-29 14:11:58 +02:00
|
|
|
|
<p>{ _t('You are no longer ignoring %(userId)s', { userId }) }</p>
|
2018-06-18 20:31:40 +02:00
|
|
|
|
</div>,
|
|
|
|
|
});
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
2017-09-15 00:08:51 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.actions,
|
2017-09-15 00:08:51 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'op',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id> [<power-level>]',
|
|
|
|
|
description: _td('Define the power level of a user'),
|
|
|
|
|
runFn: function(roomId, 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]) {
|
2020-03-30 15:09:10 +02:00
|
|
|
|
powerLevel = parseInt(matches[3], 10);
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
|
|
|
|
if (!isNaN(powerLevel)) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const room = cli.getRoom(roomId);
|
2020-04-11 17:41:07 +02:00
|
|
|
|
if (!room) return reject(_t("Command failed"));
|
2020-08-13 17:29:25 +02:00
|
|
|
|
const member = room.getMember(userId);
|
2020-07-14 11:59:06 +02:00
|
|
|
|
if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) {
|
|
|
|
|
return reject(_t("Could not find user in room"));
|
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
|
|
|
|
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2016-01-14 15:39:58 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'deop',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id>',
|
|
|
|
|
description: _td('Deops user with given id'),
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (args) {
|
|
|
|
|
const matches = args.match(/^(\S+)$/);
|
|
|
|
|
if (matches) {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const room = cli.getRoom(roomId);
|
2020-04-11 17:41:07 +02:00
|
|
|
|
if (!room) return reject(_t("Command failed"));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
|
|
|
|
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
2020-04-11 17:41:07 +02:00
|
|
|
|
if (!powerLevelEvent.getContent().users[args]) return reject(_t("Could not find user in room"));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
2015-09-18 14:54:20 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2017-05-23 10:24:18 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'devtools',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
description: _td('Opens the Developer Tools dialog'),
|
|
|
|
|
runFn: function(roomId) {
|
2021-06-29 14:11:58 +02:00
|
|
|
|
Modal.createDialog(DevtoolsDialog, { roomId });
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return success();
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.advanced,
|
2017-07-31 13:08:28 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'addwidget',
|
2020-04-20 17:35:35 +02:00
|
|
|
|
args: '<url | embed code | Jitsi url>',
|
2019-03-24 07:07:00 +01:00
|
|
|
|
description: _td('Adds a custom widget by URL to the room'),
|
2020-09-16 12:38:50 +02:00
|
|
|
|
isEnabled: () => SettingsStore.getValue(UIFeature.Widgets),
|
2020-04-10 00:02:49 +02:00
|
|
|
|
runFn: function(roomId, widgetUrl) {
|
|
|
|
|
if (!widgetUrl) {
|
|
|
|
|
return reject(_t("Please supply a widget URL or embed code"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try and parse out a widget URL from iframes
|
|
|
|
|
if (widgetUrl.toLowerCase().startsWith("<iframe ")) {
|
|
|
|
|
// We use parse5, which doesn't render/create a DOM node. It instead runs
|
|
|
|
|
// some superfast regex over the text so we don't have to.
|
|
|
|
|
const embed = parseHtml(widgetUrl);
|
|
|
|
|
if (embed && embed.childNodes && embed.childNodes.length === 1) {
|
2021-05-07 11:11:56 +02:00
|
|
|
|
const iframe = embed.childNodes[0] as ChildElement;
|
2020-04-10 00:02:49 +02:00
|
|
|
|
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
|
|
|
|
|
const srcAttr = iframe.attrs.find(a => a.name === 'src');
|
|
|
|
|
console.log("Pulling URL out of iframe (embed code)");
|
|
|
|
|
widgetUrl = srcAttr.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!widgetUrl.startsWith("https://") && !widgetUrl.startsWith("http://")) {
|
2019-03-24 07:07:00 +01:00
|
|
|
|
return reject(_t("Please supply a https:// or http:// widget URL"));
|
|
|
|
|
}
|
|
|
|
|
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
|
|
|
|
const userId = MatrixClientPeg.get().getUserId();
|
|
|
|
|
const nowMs = (new Date()).getTime();
|
|
|
|
|
const widgetId = encodeURIComponent(`${roomId}_${userId}_${nowMs}`);
|
2020-04-10 00:02:49 +02:00
|
|
|
|
let type = WidgetType.CUSTOM;
|
|
|
|
|
let name = "Custom Widget";
|
|
|
|
|
let data = {};
|
|
|
|
|
|
|
|
|
|
// Make the widget a Jitsi widget if it looks like a Jitsi widget
|
|
|
|
|
const jitsiData = Jitsi.getInstance().parsePreferredConferenceUrl(widgetUrl);
|
|
|
|
|
if (jitsiData) {
|
|
|
|
|
console.log("Making /addwidget widget a Jitsi conference");
|
|
|
|
|
type = WidgetType.JITSI;
|
|
|
|
|
name = "Jitsi Conference";
|
|
|
|
|
data = jitsiData;
|
|
|
|
|
widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return success(WidgetUtils.setRoomWidget(roomId, widgetId, type, widgetUrl, name, data));
|
2019-03-24 07:07:00 +01:00
|
|
|
|
} else {
|
|
|
|
|
return reject(_t("You cannot modify widgets in this room."));
|
|
|
|
|
}
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.admin,
|
2019-03-24 07:07:00 +01:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'verify',
|
2018-06-18 20:31:40 +02:00
|
|
|
|
args: '<user-id> <device-id> <device-signing-key>',
|
2020-01-29 16:48:25 +01:00
|
|
|
|
description: _td('Verifies a user, session, and pubkey tuple'),
|
2018-06-18 20:31:40 +02:00
|
|
|
|
runFn: function(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];
|
2017-05-23 10:24:18 +02:00
|
|
|
|
|
2020-01-03 13:08:35 +01:00
|
|
|
|
return success((async () => {
|
2020-04-28 19:35:16 +02:00
|
|
|
|
const device = cli.getStoredDevice(userId, deviceId);
|
2020-01-03 13:08:35 +01:00
|
|
|
|
if (!device) {
|
2020-01-29 16:48:25 +01:00
|
|
|
|
throw new Error(_t('Unknown (user, session) pair:') + ` (${userId}, ${deviceId})`);
|
2020-01-03 13:08:35 +01:00
|
|
|
|
}
|
|
|
|
|
const deviceTrust = await cli.checkDeviceTrust(userId, deviceId);
|
2017-05-23 10:24:18 +02:00
|
|
|
|
|
2020-01-03 13:08:35 +01:00
|
|
|
|
if (deviceTrust.isVerified()) {
|
|
|
|
|
if (device.getFingerprint() === fingerprint) {
|
2020-01-29 16:48:25 +01:00
|
|
|
|
throw new Error(_t('Session already verified!'));
|
2020-01-03 13:08:35 +01:00
|
|
|
|
} else {
|
2020-01-29 16:48:25 +01:00
|
|
|
|
throw new Error(_t('WARNING: Session already verified, but keys do NOT MATCH!'));
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
2020-01-03 13:08:35 +01:00
|
|
|
|
}
|
2017-05-23 10:24:18 +02:00
|
|
|
|
|
2020-01-03 13:08:35 +01:00
|
|
|
|
if (device.getFingerprint() !== fingerprint) {
|
|
|
|
|
const fprint = device.getFingerprint();
|
|
|
|
|
throw new Error(
|
2020-01-29 16:48:25 +01:00
|
|
|
|
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' +
|
2020-01-03 13:08:35 +01:00
|
|
|
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
|
|
|
|
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
2020-08-29 02:11:08 +02:00
|
|
|
|
{
|
|
|
|
|
fprint,
|
|
|
|
|
userId,
|
|
|
|
|
deviceId,
|
|
|
|
|
fingerprint,
|
|
|
|
|
}));
|
2020-01-03 13:08:35 +01:00
|
|
|
|
}
|
2017-05-23 10:24:18 +02:00
|
|
|
|
|
2020-01-03 13:08:35 +01:00
|
|
|
|
await cli.setDeviceVerified(userId, deviceId, true);
|
|
|
|
|
|
|
|
|
|
// Tell the user we verified everything
|
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, {
|
|
|
|
|
title: _t('Verified key'),
|
|
|
|
|
description: <div>
|
|
|
|
|
<p>
|
|
|
|
|
{
|
|
|
|
|
_t('The signing key you provided matches the signing key you received ' +
|
2020-01-29 16:48:25 +01:00
|
|
|
|
'from %(userId)s\'s session %(deviceId)s. Session marked as verified.',
|
2021-06-29 14:11:58 +02:00
|
|
|
|
{ userId, deviceId })
|
2020-01-03 13:08:35 +01:00
|
|
|
|
}
|
|
|
|
|
</p>
|
|
|
|
|
</div>,
|
|
|
|
|
});
|
|
|
|
|
})());
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
2017-05-23 10:24:18 +02:00
|
|
|
|
}
|
2018-06-18 20:31:40 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.advanced,
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: 'discardsession',
|
2018-08-29 19:09:37 +02:00
|
|
|
|
description: _td('Forces the current outbound group session in an encrypted room to be discarded'),
|
|
|
|
|
runFn: function(roomId) {
|
|
|
|
|
try {
|
|
|
|
|
MatrixClientPeg.get().forceDiscardSession(roomId);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return reject(e.message);
|
|
|
|
|
}
|
|
|
|
|
return success();
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.advanced,
|
2018-08-29 19:09:37 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "rainbow",
|
2019-05-12 17:40:27 +02:00
|
|
|
|
description: _td("Sends the given message coloured as a rainbow"),
|
2019-05-12 17:36:43 +02:00
|
|
|
|
args: '<message>',
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (!args) return reject(this.getUserId());
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
2019-05-12 17:36:43 +02:00
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.messages,
|
2019-05-12 17:36:43 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "rainbowme",
|
2019-05-12 17:40:27 +02:00
|
|
|
|
description: _td("Sends the given emote coloured as a rainbow"),
|
2019-05-12 17:36:43 +02:00
|
|
|
|
args: '<message>',
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
if (!args) return reject(this.getUserId());
|
2021-06-17 12:37:06 +02:00
|
|
|
|
return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
2019-05-12 17:36:43 +02:00
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.messages,
|
2019-05-12 18:14:30 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "help",
|
2019-07-29 18:58:34 +02:00
|
|
|
|
description: _td("Displays list of commands with usages and descriptions"),
|
|
|
|
|
runFn: function() {
|
2019-08-06 19:03:38 +02:00
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'Help', SlashCommandHelpDialog);
|
2019-07-29 18:58:34 +02:00
|
|
|
|
return success();
|
|
|
|
|
},
|
2019-08-06 19:03:38 +02:00
|
|
|
|
category: CommandCategories.advanced,
|
2019-07-29 18:58:34 +02:00
|
|
|
|
}),
|
2020-03-30 15:13:08 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "whois",
|
|
|
|
|
description: _td("Displays information about a user"),
|
|
|
|
|
args: "<user-id>",
|
2020-03-31 12:53:26 +02:00
|
|
|
|
runFn: function(roomId, userId) {
|
2020-03-30 15:13:08 +02:00
|
|
|
|
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
2020-05-14 05:03:12 +02:00
|
|
|
|
dis.dispatch<ViewUserPayload>({
|
|
|
|
|
action: Action.ViewUser,
|
2021-06-18 17:21:46 +02:00
|
|
|
|
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
|
|
|
|
member: member || { userId } as User,
|
2020-03-30 15:13:08 +02:00
|
|
|
|
});
|
|
|
|
|
return success();
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.advanced,
|
|
|
|
|
}),
|
2020-04-19 13:09:07 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "rageshake",
|
|
|
|
|
aliases: ["bugreport"],
|
|
|
|
|
description: _td("Send a bug report with logs"),
|
2020-09-15 16:49:25 +02:00
|
|
|
|
isEnabled: () => !!SdkConfig.get().bug_report_endpoint_url,
|
2020-04-19 13:09:07 +02:00
|
|
|
|
args: "<description>",
|
|
|
|
|
runFn: function(roomId, args) {
|
2020-04-19 13:19:31 +02:00
|
|
|
|
return success(
|
2020-09-09 22:55:27 +02:00
|
|
|
|
Modal.createTrackedDialog('Slash Commands', 'Bug Report Dialog', BugReportDialog, {
|
2020-09-09 22:53:38 +02:00
|
|
|
|
initialText: args,
|
|
|
|
|
}).finished,
|
2020-04-19 13:19:31 +02:00
|
|
|
|
);
|
2020-04-19 13:09:07 +02:00
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.advanced,
|
|
|
|
|
}),
|
2020-05-11 11:54:28 +02:00
|
|
|
|
new Command({
|
|
|
|
|
command: "query",
|
|
|
|
|
description: _td("Opens chat with the given user"),
|
|
|
|
|
args: "<user-id>",
|
|
|
|
|
runFn: function(roomId, userId) {
|
2020-11-04 16:32:21 +01:00
|
|
|
|
// easter-egg for now: look up phone numbers through the thirdparty API
|
|
|
|
|
// (very dumb phone number detection...)
|
|
|
|
|
const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId);
|
|
|
|
|
if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) {
|
2020-05-11 11:54:28 +02:00
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return success((async () => {
|
2020-11-04 16:32:21 +01:00
|
|
|
|
if (isPhoneNumber) {
|
2021-02-12 21:55:54 +01:00
|
|
|
|
const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
|
2020-11-04 16:32:21 +01:00
|
|
|
|
if (!results || results.length === 0 || !results[0].userid) {
|
|
|
|
|
throw new Error("Unable to find Matrix ID for phone number");
|
|
|
|
|
}
|
|
|
|
|
userId = results[0].userid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
|
|
|
|
|
2020-05-11 11:54:28 +02:00
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
2020-11-04 16:32:21 +01:00
|
|
|
|
room_id: roomId,
|
2020-05-11 11:54:28 +02:00
|
|
|
|
});
|
|
|
|
|
})());
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.actions,
|
|
|
|
|
}),
|
|
|
|
|
new Command({
|
|
|
|
|
command: "msg",
|
|
|
|
|
description: _td("Sends a message to the given user"),
|
|
|
|
|
args: "<user-id> <message>",
|
2021-07-19 23:43:11 +02:00
|
|
|
|
runFn: function(roomId, args) {
|
2020-05-11 11:54:28 +02:00
|
|
|
|
if (args) {
|
2020-05-12 11:23:53 +02:00
|
|
|
|
// matches the first whitespace delimited group and then the rest of the string
|
2020-05-11 11:54:28 +02:00
|
|
|
|
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);
|
|
|
|
|
if (matches) {
|
|
|
|
|
const [userId, msg] = matches.slice(1);
|
|
|
|
|
if (msg && userId && userId.startsWith("@") && userId.includes(":")) {
|
|
|
|
|
return success((async () => {
|
|
|
|
|
const cli = MatrixClientPeg.get();
|
|
|
|
|
const roomId = await ensureDMExists(cli, userId);
|
|
|
|
|
dis.dispatch({
|
|
|
|
|
action: 'view_room',
|
|
|
|
|
room_id: roomId,
|
|
|
|
|
});
|
|
|
|
|
cli.sendTextMessage(roomId, msg);
|
|
|
|
|
})());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return reject(this.getUsage());
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.actions,
|
|
|
|
|
}),
|
2020-10-29 18:56:24 +01:00
|
|
|
|
new Command({
|
|
|
|
|
command: "holdcall",
|
2020-11-05 10:49:14 +01:00
|
|
|
|
description: _td("Places the call in the current room on hold"),
|
2020-10-29 18:56:24 +01:00
|
|
|
|
category: CommandCategories.other,
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
|
|
|
|
if (!call) {
|
|
|
|
|
return reject("No active call in this room");
|
|
|
|
|
}
|
|
|
|
|
call.setRemoteOnHold(true);
|
|
|
|
|
return success();
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
new Command({
|
|
|
|
|
command: "unholdcall",
|
2020-11-05 10:49:14 +01:00
|
|
|
|
description: _td("Takes the call in the current room off hold"),
|
2020-10-29 18:56:24 +01:00
|
|
|
|
category: CommandCategories.other,
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
|
|
|
|
if (!call) {
|
|
|
|
|
return reject("No active call in this room");
|
|
|
|
|
}
|
|
|
|
|
call.setRemoteOnHold(false);
|
|
|
|
|
return success();
|
|
|
|
|
},
|
|
|
|
|
}),
|
2021-01-13 15:30:09 +01:00
|
|
|
|
new Command({
|
|
|
|
|
command: "converttodm",
|
|
|
|
|
description: _td("Converts the room to a DM"),
|
|
|
|
|
category: CommandCategories.other,
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
|
|
|
return success(guessAndSetDMRoom(room, true));
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
new Command({
|
|
|
|
|
command: "converttoroom",
|
|
|
|
|
description: _td("Converts the DM to a room"),
|
|
|
|
|
category: CommandCategories.other,
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
|
|
|
return success(guessAndSetDMRoom(room, false));
|
|
|
|
|
},
|
|
|
|
|
}),
|
2020-02-29 02:29:52 +01:00
|
|
|
|
|
2020-03-30 14:59:08 +02:00
|
|
|
|
// Command definitions for autocompletion ONLY:
|
|
|
|
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
|
|
|
|
new Command({
|
2020-04-10 14:59:01 +02:00
|
|
|
|
command: "me",
|
2020-03-30 14:59:08 +02:00
|
|
|
|
args: '<message>',
|
|
|
|
|
description: _td('Displays action'),
|
|
|
|
|
category: CommandCategories.messages,
|
|
|
|
|
hideCompletionAfterSpace: true,
|
2020-02-29 02:29:52 +01:00
|
|
|
|
}),
|
2020-11-26 18:27:35 +01:00
|
|
|
|
|
2020-11-27 14:54:21 +01:00
|
|
|
|
...CHAT_EFFECTS.map((effect) => {
|
2020-10-21 13:37:36 +02:00
|
|
|
|
return new Command({
|
|
|
|
|
command: effect.command,
|
|
|
|
|
description: effect.description(),
|
|
|
|
|
args: '<message>',
|
|
|
|
|
runFn: function(roomId, args) {
|
|
|
|
|
return success((async () => {
|
|
|
|
|
if (!args) {
|
|
|
|
|
args = effect.fallbackMessage();
|
|
|
|
|
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
|
|
|
|
|
} else {
|
|
|
|
|
const content = {
|
|
|
|
|
msgtype: effect.msgType,
|
|
|
|
|
body: args,
|
|
|
|
|
};
|
|
|
|
|
MatrixClientPeg.get().sendMessage(roomId, content);
|
|
|
|
|
}
|
2021-06-29 14:11:58 +02:00
|
|
|
|
dis.dispatch({ action: `effects.${effect.command}` });
|
2020-10-21 13:37:36 +02:00
|
|
|
|
})());
|
|
|
|
|
},
|
|
|
|
|
category: CommandCategories.effects,
|
2021-06-29 14:11:58 +02:00
|
|
|
|
});
|
2020-08-18 17:57:51 +02:00
|
|
|
|
}),
|
2020-03-30 14:59:08 +02:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// build a map from names and aliases to the Command objects.
|
2021-06-30 14:01:26 +02:00
|
|
|
|
export const CommandMap = new Map<string, Command>();
|
2020-03-30 14:59:08 +02:00
|
|
|
|
Commands.forEach(cmd => {
|
|
|
|
|
CommandMap.set(cmd.command, cmd);
|
|
|
|
|
cmd.aliases.forEach(alias => {
|
|
|
|
|
CommandMap.set(alias, cmd);
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-09-18 14:54:20 +02:00
|
|
|
|
|
2021-06-30 14:01:26 +02:00
|
|
|
|
export function parseCommandString(input: string): { cmd?: string, args?: string } {
|
2018-06-18 20:31:40 +02:00
|
|
|
|
// trim any trailing whitespace, as it can confuse the parser for
|
|
|
|
|
// IRC-style commands
|
|
|
|
|
input = input.replace(/\s+$/, '');
|
2020-07-18 12:40:45 +02:00
|
|
|
|
if (input[0] !== '/') return {}; // not a command
|
2018-06-18 20:31:40 +02:00
|
|
|
|
|
2021-02-15 13:16:32 +01:00
|
|
|
|
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
|
2021-06-30 14:01:26 +02:00
|
|
|
|
let cmd: string;
|
|
|
|
|
let args: string;
|
2018-06-18 20:31:40 +02:00
|
|
|
|
if (bits) {
|
|
|
|
|
cmd = bits[1].substring(1).toLowerCase();
|
2020-01-21 17:50:04 +01:00
|
|
|
|
args = bits[2];
|
2018-06-18 20:31:40 +02:00
|
|
|
|
} else {
|
|
|
|
|
cmd = input;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-29 14:11:58 +02:00
|
|
|
|
return { cmd, args };
|
2020-04-10 14:59:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
2021-06-30 14:01:26 +02:00
|
|
|
|
interface ICmd {
|
|
|
|
|
cmd?: Command;
|
|
|
|
|
args?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-10 14:59:01 +02:00
|
|
|
|
/**
|
|
|
|
|
* Process the given text for /commands and return a bound method to perform them.
|
|
|
|
|
* @param {string} roomId The room in which the command was performed.
|
|
|
|
|
* @param {string} input The raw text input by the user.
|
|
|
|
|
* @return {null|function(): Object} Function returning an object with the property 'error' if there was an error
|
|
|
|
|
* processing the command, or 'promise' if a request was sent out.
|
|
|
|
|
* Returns null if the input didn't match a command.
|
|
|
|
|
*/
|
2021-06-30 14:01:26 +02:00
|
|
|
|
export function getCommand(input: string): ICmd {
|
2021-06-29 14:11:58 +02:00
|
|
|
|
const { cmd, args } = parseCommandString(input);
|
2020-04-10 14:59:01 +02:00
|
|
|
|
|
2020-09-15 16:49:25 +02:00
|
|
|
|
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
|
2021-02-25 20:39:20 +01:00
|
|
|
|
return {
|
|
|
|
|
cmd: CommandMap.get(cmd),
|
|
|
|
|
args,
|
|
|
|
|
};
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|
2021-04-13 11:33:32 +02:00
|
|
|
|
return {};
|
2018-06-18 20:31:40 +02:00
|
|
|
|
}
|