mirror of
https://github.com/element-hq/element-web
synced 2024-11-24 18:25:49 +03:00
Merge pull request #1988 from matrix-org/t3chguy/refactor_slashcommands
refactor, consolidate and improve SlashCommands
This commit is contained in:
commit
a6d9c25b70
3 changed files with 440 additions and 468 deletions
|
@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MatrixClientPeg from "./MatrixClientPeg";
|
|
||||||
import dis from "./dispatcher";
|
import React from 'react';
|
||||||
import Tinter from "./Tinter";
|
import MatrixClientPeg from './MatrixClientPeg';
|
||||||
|
import dis from './dispatcher';
|
||||||
|
import Tinter from './Tinter';
|
||||||
import sdk from './index';
|
import sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import {_t, _td} from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
|
||||||
|
|
||||||
|
|
||||||
class Command {
|
class Command {
|
||||||
constructor(name, paramArgs, runFn) {
|
constructor({name, args='', description, runFn}) {
|
||||||
this.name = name;
|
this.command = name;
|
||||||
this.paramArgs = paramArgs;
|
this.args = args;
|
||||||
|
this.description = description;
|
||||||
this.runFn = runFn;
|
this.runFn = runFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommand() {
|
getCommand() {
|
||||||
return "/" + this.name;
|
return "/" + this.command;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommandWithArgs() {
|
getCommandWithArgs() {
|
||||||
return this.getCommand() + " " + this.paramArgs;
|
return this.getCommand() + " " + this.args;
|
||||||
}
|
}
|
||||||
|
|
||||||
run(roomId, args) {
|
run(roomId, args) {
|
||||||
|
@ -47,16 +50,12 @@ class Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(msg) {
|
function reject(error) {
|
||||||
return {
|
return {error};
|
||||||
error: msg,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function success(promise) {
|
function success(promise) {
|
||||||
return {
|
return {promise};
|
||||||
promise: promise,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable the "unexpected this" error for these commands - all of the run
|
/* Disable the "unexpected this" error for these commands - all of the run
|
||||||
|
@ -65,352 +64,408 @@ function success(promise) {
|
||||||
|
|
||||||
/* eslint-disable babel/no-invalid-this */
|
/* eslint-disable babel/no-invalid-this */
|
||||||
|
|
||||||
const commands = {
|
export const CommandMap = {
|
||||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
ddg: new Command({
|
||||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
name: 'ddg',
|
||||||
// TODO Don't explain this away, actually show a search UI here.
|
args: '<query>',
|
||||||
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
description: _td('Searches DuckDuckGo for results'),
|
||||||
title: _t('/ddg is not a command'),
|
runFn: function(roomId, args) {
|
||||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||||
});
|
// TODO Don't explain this away, actually show a search UI here.
|
||||||
return success();
|
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();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change your nickname
|
nick: new Command({
|
||||||
nick: new Command("nick", "<display_name>", function(roomId, args) {
|
name: 'nick',
|
||||||
if (args) {
|
args: '<display_name>',
|
||||||
return success(
|
description: _td('Changes your display nickname'),
|
||||||
MatrixClientPeg.get().setDisplayName(args),
|
runFn: function(roomId, args) {
|
||||||
);
|
if (args) {
|
||||||
}
|
return success(MatrixClientPeg.get().setDisplayName(args));
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Changes the colorscheme of your current room
|
|
||||||
tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
|
||||||
if (matches) {
|
|
||||||
Tinter.tint(matches[1], matches[4]);
|
|
||||||
const colorScheme = {};
|
|
||||||
colorScheme.primary_color = matches[1];
|
|
||||||
if (matches[4]) {
|
|
||||||
colorScheme.secondary_color = matches[4];
|
|
||||||
} else {
|
|
||||||
colorScheme.secondary_color = colorScheme.primary_color;
|
|
||||||
}
|
|
||||||
return success(
|
|
||||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Change the room topic
|
tint: new Command({
|
||||||
topic: new Command("topic", "<topic>", function(roomId, args) {
|
name: 'tint',
|
||||||
if (args) {
|
args: '<color1> [<color2>]',
|
||||||
return success(
|
description: _td('Changes colour scheme of current room'),
|
||||||
MatrixClientPeg.get().setRoomTopic(roomId, args),
|
runFn: function(roomId, args) {
|
||||||
);
|
if (args) {
|
||||||
}
|
const matches = args.match(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/);
|
||||||
return reject(this.getUsage());
|
if (matches) {
|
||||||
}),
|
Tinter.tint(matches[1], matches[4]);
|
||||||
|
const colorScheme = {};
|
||||||
// Invite a user
|
colorScheme.primary_color = matches[1];
|
||||||
invite: new Command("invite", "<userId>", function(roomId, args) {
|
if (matches[4]) {
|
||||||
if (args) {
|
colorScheme.secondary_color = matches[4];
|
||||||
const matches = args.match(/^(\S+)$/);
|
} else {
|
||||||
if (matches) {
|
colorScheme.secondary_color = colorScheme.primary_color;
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().invite(roomId, matches[1]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Join a room
|
|
||||||
join: new Command("join", "#alias:domain", function(roomId, args) {
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
|
||||||
if (matches) {
|
|
||||||
let roomAlias = matches[1];
|
|
||||||
if (roomAlias[0] !== '#') {
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
|
||||||
if (!roomAlias.match(/:/)) {
|
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_alias: roomAlias,
|
|
||||||
auto_join: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}),
|
|
||||||
|
|
||||||
part: new Command("part", "[#alias:domain]", function(roomId, args) {
|
|
||||||
let targetRoomId;
|
|
||||||
if (args) {
|
|
||||||
const matches = args.match(/^(\S+)$/);
|
|
||||||
if (matches) {
|
|
||||||
let roomAlias = matches[1];
|
|
||||||
if (roomAlias[0] !== '#') {
|
|
||||||
return reject(this.getUsage());
|
|
||||||
}
|
|
||||||
if (!roomAlias.match(/:/)) {
|
|
||||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find a room with this alias
|
|
||||||
const rooms = MatrixClientPeg.get().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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targetRoomId) { break; }
|
|
||||||
}
|
}
|
||||||
if (targetRoomId) { break; }
|
return success(
|
||||||
}
|
SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||||
if (!targetRoomId) {
|
);
|
||||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
if (!targetRoomId) targetRoomId = roomId;
|
},
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().leave(targetRoomId).then(
|
|
||||||
function() {
|
|
||||||
dis.dispatch({action: 'view_next_room'});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Kick a user from the room with an optional reason
|
topic: new Command({
|
||||||
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
|
name: 'topic',
|
||||||
if (args) {
|
args: '<topic>',
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
description: _td('Sets the room topic'),
|
||||||
if (matches) {
|
runFn: function(roomId, args) {
|
||||||
return success(
|
if (args) {
|
||||||
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
|
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
invite: new Command({
|
||||||
|
name: 'invite',
|
||||||
|
args: '<user-id>',
|
||||||
|
description: _td('Invites user with given id to current room'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
join: new Command({
|
||||||
|
name: 'join',
|
||||||
|
args: '<room-alias>',
|
||||||
|
description: _td('Joins room with given alias'),
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
if (args) {
|
||||||
|
const matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
let roomAlias = matches[1];
|
||||||
|
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||||
|
|
||||||
|
if (!roomAlias.includes(':')) {
|
||||||
|
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_alias: roomAlias,
|
||||||
|
auto_join: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
part: new Command({
|
||||||
|
name: 'part',
|
||||||
|
args: '[<room-alias>]',
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (targetRoomId) break;
|
||||||
|
}
|
||||||
|
if (targetRoomId) break;
|
||||||
|
}
|
||||||
|
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetRoomId) targetRoomId = roomId;
|
||||||
|
return success(
|
||||||
|
cli.leave(targetRoomId).then(function() {
|
||||||
|
dis.dispatch({action: 'view_next_room'});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
kick: new Command({
|
||||||
|
name: 'kick',
|
||||||
|
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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Ban a user from the room with an optional reason
|
// Ban a user from the room with an optional reason
|
||||||
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
|
ban: new Command({
|
||||||
if (args) {
|
name: 'ban',
|
||||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
args: '<user-id> [reason]',
|
||||||
if (matches) {
|
description: _td('Bans user with given id'),
|
||||||
return success(
|
runFn: function(roomId, args) {
|
||||||
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
|
if (args) {
|
||||||
);
|
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
if (matches) {
|
||||||
|
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Unban a user from the room
|
// Unban a user from ythe room
|
||||||
unban: new Command("unban", "<userId>", function(roomId, args) {
|
unban: new Command({
|
||||||
if (args) {
|
name: 'unban',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Unbans user with given id'),
|
||||||
// Reset the user membership to "leave" to unban him
|
runFn: function(roomId, args) {
|
||||||
return success(
|
if (args) {
|
||||||
MatrixClientPeg.get().unban(roomId, matches[1]),
|
const matches = args.match(/^(\S+)$/);
|
||||||
);
|
if (matches) {
|
||||||
|
// Reset the user membership to "leave" to unban him
|
||||||
|
return success(MatrixClientPeg.get().unban(roomId, matches[1]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
ignore: new Command({
|
||||||
if (args) {
|
name: 'ignore',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Ignores a user, hiding their messages from you'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
if (args) {
|
||||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
const cli = MatrixClientPeg.get();
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
const matches = args.match(/^(\S+)$/);
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
if (matches) {
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
const userId = matches[1];
|
||||||
title: _t("Ignored user"),
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
description: (
|
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||||
<div>
|
return success(
|
||||||
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
</div>
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
),
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||||
hasCancelButton: false,
|
title: _t('Ignored user'),
|
||||||
});
|
description: <div>
|
||||||
}),
|
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
||||||
);
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
unignore: new Command({
|
||||||
if (args) {
|
name: 'unignore',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
if (args) {
|
||||||
const index = ignoredUsers.indexOf(userId);
|
const cli = MatrixClientPeg.get();
|
||||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
|
||||||
return success(
|
const matches = args.match(/^(\S+)$/);
|
||||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
if (matches) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const userId = matches[1];
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
title: _t("Unignored user"),
|
const index = ignoredUsers.indexOf(userId);
|
||||||
description: (
|
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||||
<div>
|
return success(
|
||||||
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||||
</div>
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
),
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||||
hasCancelButton: false,
|
title: _t('Unignored user'),
|
||||||
});
|
description: <div>
|
||||||
}),
|
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
||||||
);
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
op: new Command({
|
||||||
if (args) {
|
name: 'op',
|
||||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
args: '<user-id> [<power-level>]',
|
||||||
let powerLevel = 50; // default power level for op
|
description: _td('Define the power level of a user'),
|
||||||
if (matches) {
|
runFn: function(roomId, args) {
|
||||||
const userId = matches[1];
|
if (args) {
|
||||||
if (matches.length === 4 && undefined !== matches[3]) {
|
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||||
powerLevel = parseInt(matches[3]);
|
let powerLevel = 50; // default power level for op
|
||||||
}
|
if (matches) {
|
||||||
if (!isNaN(powerLevel)) {
|
const userId = matches[1];
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
if (matches.length === 4 && undefined !== matches[3]) {
|
||||||
if (!room) {
|
powerLevel = parseInt(matches[3]);
|
||||||
return reject("Bad room ID: " + roomId);
|
}
|
||||||
|
if (!isNaN(powerLevel)) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (!room) return reject('Bad room ID: ' + roomId);
|
||||||
|
|
||||||
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
|
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
||||||
}
|
}
|
||||||
const powerLevelEvent = room.currentState.getStateEvents(
|
|
||||||
"m.room.power_levels", "",
|
|
||||||
);
|
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
|
||||||
roomId, userId, powerLevel, powerLevelEvent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Reset the power level of a user
|
// Reset the power level of a user
|
||||||
deop: new Command("deop", "<userId>", function(roomId, args) {
|
deop: new Command({
|
||||||
if (args) {
|
name: 'deop',
|
||||||
const matches = args.match(/^(\S+)$/);
|
args: '<user-id>',
|
||||||
if (matches) {
|
description: _td('Deops user with given id'),
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
runFn: function(roomId, args) {
|
||||||
if (!room) {
|
if (args) {
|
||||||
return reject("Bad room ID: " + roomId);
|
const matches = args.match(/^(\S+)$/);
|
||||||
}
|
if (matches) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const room = cli.getRoom(roomId);
|
||||||
|
if (!room) return reject('Bad room ID: ' + roomId);
|
||||||
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents(
|
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
"m.room.power_levels", "",
|
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
||||||
);
|
}
|
||||||
return success(
|
|
||||||
MatrixClientPeg.get().setPowerLevel(
|
|
||||||
roomId, args, undefined, powerLevelEvent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Open developer tools
|
devtools: new Command({
|
||||||
devtools: new Command("devtools", "", function(roomId) {
|
name: 'devtools',
|
||||||
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
description: _td('Opens the Developer Tools dialog'),
|
||||||
Modal.createDialog(DevtoolsDialog, { roomId });
|
runFn: function(roomId) {
|
||||||
return success();
|
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||||
|
Modal.createDialog(DevtoolsDialog, {roomId});
|
||||||
|
return success();
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Verify a user, device, and pubkey tuple
|
// Verify a user, device, and pubkey tuple
|
||||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
verify: new Command({
|
||||||
if (args) {
|
name: 'verify',
|
||||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
args: '<user-id> <device-id> <device-signing-key>',
|
||||||
if (matches) {
|
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||||
const userId = matches[1];
|
runFn: function(roomId, args) {
|
||||||
const deviceId = matches[2];
|
if (args) {
|
||||||
const fingerprint = matches[3];
|
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
return success(
|
const userId = matches[1];
|
||||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
const deviceId = matches[2];
|
||||||
// in future
|
const fingerprint = matches[3];
|
||||||
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
|
|
||||||
if (!device) {
|
|
||||||
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.isVerified()) {
|
return success(
|
||||||
if (device.getFingerprint() === fingerprint) {
|
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||||
throw new Error(_t(`Device already verified!`));
|
// in future
|
||||||
} else {
|
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
||||||
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
if (!device) {
|
||||||
|
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (device.getFingerprint() !== fingerprint) {
|
if (device.isVerified()) {
|
||||||
const fprint = device.getFingerprint();
|
if (device.getFingerprint() === fingerprint) {
|
||||||
throw new Error(
|
throw new Error(_t('Device already verified!'));
|
||||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
} else {
|
||||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
}
|
||||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
if (device.getFingerprint() !== fingerprint) {
|
||||||
}).then(() => {
|
const fprint = device.getFingerprint();
|
||||||
// Tell the user we verified everything
|
throw new Error(
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||||
title: _t("Verified key"),
|
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||||
description: (
|
{
|
||||||
<div>
|
fprint,
|
||||||
<p>
|
userId,
|
||||||
{
|
deviceId,
|
||||||
_t("The signing key you provided matches the signing key you received " +
|
fingerprint,
|
||||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
}));
|
||||||
{userId: userId, deviceId: deviceId})
|
}
|
||||||
}
|
|
||||||
</p>
|
return cli.setDeviceVerified(userId, deviceId, true);
|
||||||
</div>
|
}).then(() => {
|
||||||
),
|
// Tell the user we verified everything
|
||||||
hasCancelButton: false,
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
});
|
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||||
}),
|
title: _t('Verified key'),
|
||||||
);
|
description: <div>
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
_t('The signing key you provided matches the signing key you received ' +
|
||||||
|
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||||
|
{userId, deviceId})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>,
|
||||||
|
hasCancelButton: false,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return reject(this.getUsage());
|
||||||
return reject(this.getUsage());
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Command definitions for autocompletion ONLY:
|
||||||
|
|
||||||
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||||
|
me: new Command({
|
||||||
|
name: 'me',
|
||||||
|
args: '<message>',
|
||||||
|
description: _td('Displays action'),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
/* eslint-enable babel/no-invalid-this */
|
/* eslint-enable babel/no-invalid-this */
|
||||||
|
@ -421,50 +476,39 @@ const aliases = {
|
||||||
j: "join",
|
j: "join",
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
/**
|
||||||
/**
|
* Process the given text for /commands and perform them.
|
||||||
* Process the given text for /commands and perform them.
|
* @param {string} roomId The room in which the command was performed.
|
||||||
* @param {string} roomId The room in which the command was performed.
|
* @param {string} input The raw text input by the user.
|
||||||
* @param {string} input The raw text input by the user.
|
* @return {Object|null} An object with the property 'error' if there was an error
|
||||||
* @return {Object|null} An object with the property 'error' if there was an error
|
* processing the command, or 'promise' if a request was sent out.
|
||||||
* processing the command, or 'promise' if a request was sent out.
|
* Returns null if the input didn't match a command.
|
||||||
* Returns null if the input didn't match a command.
|
*/
|
||||||
*/
|
export function processCommandInput(roomId, input) {
|
||||||
processInput: function(roomId, input) {
|
// trim any trailing whitespace, as it can confuse the parser for
|
||||||
// trim any trailing whitespace, as it can confuse the parser for
|
// IRC-style commands
|
||||||
// IRC-style commands
|
input = input.replace(/\s+$/, '');
|
||||||
input = input.replace(/\s+$/, "");
|
if (input[0] !== '/' || input[1] === '/') return null; // not a command
|
||||||
if (input[0] === "/" && input[1] !== "/") {
|
|
||||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
|
||||||
let cmd;
|
|
||||||
let args;
|
|
||||||
if (bits) {
|
|
||||||
cmd = bits[1].substring(1).toLowerCase();
|
|
||||||
args = bits[3];
|
|
||||||
} else {
|
|
||||||
cmd = input;
|
|
||||||
}
|
|
||||||
if (cmd === "me") return null;
|
|
||||||
if (aliases[cmd]) {
|
|
||||||
cmd = aliases[cmd];
|
|
||||||
}
|
|
||||||
if (commands[cmd]) {
|
|
||||||
return commands[cmd].run(roomId, args);
|
|
||||||
} else {
|
|
||||||
return reject(_t("Unrecognised command:") + ' ' + input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null; // not a command
|
|
||||||
},
|
|
||||||
|
|
||||||
getCommandList: function() {
|
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
let cmd;
|
||||||
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
let args;
|
||||||
return commands[cmdKey];
|
if (bits) {
|
||||||
});
|
cmd = bits[1].substring(1).toLowerCase();
|
||||||
cmds.push(new Command("me", "<action>", function() {}));
|
args = bits[3];
|
||||||
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
} else {
|
||||||
|
cmd = input;
|
||||||
|
}
|
||||||
|
|
||||||
return cmds;
|
if (aliases[cmd]) {
|
||||||
},
|
cmd = aliases[cmd];
|
||||||
};
|
}
|
||||||
|
if (CommandMap[cmd]) {
|
||||||
|
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||||
|
if (!CommandMap[cmd].runFn) return null;
|
||||||
|
|
||||||
|
return CommandMap[cmd].run(roomId, args);
|
||||||
|
} else {
|
||||||
|
return reject(_t('Unrecognised command:') + ' ' + input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,101 +18,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t, _td } from '../languageHandler';
|
import {_t} from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import FuzzyMatcher from './FuzzyMatcher';
|
import FuzzyMatcher from './FuzzyMatcher';
|
||||||
import {TextualCompletion} from './Components';
|
import {TextualCompletion} from './Components';
|
||||||
|
import {CommandMap} from '../SlashCommands';
|
||||||
import type {SelectionRange} from "./Autocompleter";
|
import type {SelectionRange} from "./Autocompleter";
|
||||||
|
|
||||||
// TODO merge this with the factory mechanics of SlashCommands?
|
const COMMANDS = Object.values(CommandMap);
|
||||||
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
|
|
||||||
const COMMANDS = [
|
|
||||||
{
|
|
||||||
command: '/me',
|
|
||||||
args: '<message>',
|
|
||||||
description: _td('Displays action'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ban',
|
|
||||||
args: '<user-id> [reason]',
|
|
||||||
description: _td('Bans user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/unban',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Unbans user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/op',
|
|
||||||
args: '<user-id> [<power-level>]',
|
|
||||||
description: _td('Define the power level of a user'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/deop',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Deops user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/invite',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Invites user with given id to current room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/join',
|
|
||||||
args: '<room-alias>',
|
|
||||||
description: _td('Joins room with given alias'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/part',
|
|
||||||
args: '[<room-alias>]',
|
|
||||||
description: _td('Leave room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/topic',
|
|
||||||
args: '<topic>',
|
|
||||||
description: _td('Sets the room topic'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/kick',
|
|
||||||
args: '<user-id> [reason]',
|
|
||||||
description: _td('Kicks user with given id'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/nick',
|
|
||||||
args: '<display-name>',
|
|
||||||
description: _td('Changes your display nickname'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ddg',
|
|
||||||
args: '<query>',
|
|
||||||
description: _td('Searches DuckDuckGo for results'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/tint',
|
|
||||||
args: '<color1> [<color2>]',
|
|
||||||
description: _td('Changes colour scheme of current room'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/verify',
|
|
||||||
args: '<user-id> <device-id> <device-signing-key>',
|
|
||||||
description: _td('Verifies a user, device, and pubkey tuple'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/ignore',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Ignores a user, hiding their messages from you'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/unignore',
|
|
||||||
args: '<user-id>',
|
|
||||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
command: '/devtools',
|
|
||||||
args: '',
|
|
||||||
description: _td('Opens the Developer Tools dialog'),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
||||||
|
|
||||||
|
@ -128,20 +41,35 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (!command) return [];
|
if (!command) return [];
|
||||||
|
|
||||||
// if the query is just `/` (and the user hit TAB or waits), show them all COMMANDS otherwise FuzzyMatch them
|
let matches;
|
||||||
const matches = query === '/' ? COMMANDS : this.matcher.match(command[1]);
|
if (command[0] !== command[1]) {
|
||||||
return matches.map((result) => {
|
// The input looks like a command with arguments, perform exact match
|
||||||
return {
|
const match = COMMANDS.find((o) => o.command === command[1]);
|
||||||
// If the command is the same as the one they entered, we don't want to discard their arguments
|
if (match) {
|
||||||
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
matches = [match];
|
||||||
component: (<TextualCompletion
|
}
|
||||||
title={result.command}
|
}
|
||||||
subtitle={result.args}
|
|
||||||
description={_t(result.description)}
|
// If we don't yet have matches
|
||||||
/>),
|
if (!matches) {
|
||||||
range,
|
if (query === '/') {
|
||||||
};
|
// If they have just entered `/` show everything
|
||||||
});
|
matches = COMMANDS;
|
||||||
|
} else {
|
||||||
|
// otherwise fuzzy match against all of the fields
|
||||||
|
matches = this.matcher.match(command[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.map((result) => ({
|
||||||
|
// If the command is the same as the one they entered, we don't want to discard their arguments
|
||||||
|
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
||||||
|
component: <TextualCompletion
|
||||||
|
title={result.command}
|
||||||
|
subtitle={result.args}
|
||||||
|
description={_t(result.description)} />,
|
||||||
|
range,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -28,7 +28,7 @@ import Promise from 'bluebird';
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
import SlashCommands from '../../../SlashCommands';
|
import {processCommandInput} from '../../../SlashCommands';
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
@ -721,7 +721,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// Some commands (/join) require pills to be replaced with their text content
|
// Some commands (/join) require pills to be replaced with their text content
|
||||||
const commandText = this.removeMDLinks(contentState, ['#']);
|
const commandText = this.removeMDLinks(contentState, ['#']);
|
||||||
const cmd = SlashCommands.processInput(this.props.room.roomId, commandText);
|
const cmd = processCommandInput(this.props.room.roomId, commandText);
|
||||||
if (cmd) {
|
if (cmd) {
|
||||||
if (!cmd.error) {
|
if (!cmd.error) {
|
||||||
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown');
|
||||||
|
|
Loading…
Reference in a new issue