Merge pull request #1988 from matrix-org/t3chguy/refactor_slashcommands

refactor, consolidate and improve SlashCommands
This commit is contained in:
David Baker 2018-06-20 13:56:29 +01:00 committed by GitHub
commit a6d9c25b70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 440 additions and 468 deletions

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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');