From 85ddc0651b3dc3317c2c95dd804ca26fed2806ad Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 19:31:40 +0100 Subject: [PATCH 1/3] consolidate the two locations commands are defined, as it was a MESS Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.js | 768 +++++++++--------- src/autocomplete/CommandProvider.js | 98 +-- .../views/rooms/MessageComposerInput.js | 4 +- 3 files changed, 413 insertions(+), 457 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index d45e45e84c..3743fbb98c 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -14,28 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import MatrixClientPeg from "./MatrixClientPeg"; -import dis from "./dispatcher"; -import Tinter from "./Tinter"; + +import React from 'react'; +import MatrixClientPeg from './MatrixClientPeg'; +import dis from './dispatcher'; +import Tinter from './Tinter'; import sdk from './index'; -import { _t } from './languageHandler'; +import {_t, _td} from './languageHandler'; import Modal from './Modal'; -import SettingsStore, {SettingLevel} from "./settings/SettingsStore"; +import SettingsStore, {SettingLevel} from './settings/SettingsStore'; class Command { - constructor(name, paramArgs, runFn) { - this.name = name; - this.paramArgs = paramArgs; + constructor({name, args='', description, runFn}) { + this.command = name; + this.args = args; + this.description = description; this.runFn = runFn; } getCommand() { - return "/" + this.name; + return "/" + this.command; } getCommandWithArgs() { - return this.getCommand() + " " + this.paramArgs; + return this.getCommand() + " " + this.args; } run(roomId, args) { @@ -47,16 +50,12 @@ class Command { } } -function reject(msg) { - return { - error: msg, - }; +function reject(error) { + return {error}; } function success(promise) { - return { - promise: promise, - }; + return {promise}; } /* 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 */ -const commands = { - ddg: new Command("ddg", "", function(roomId, args) { - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - // 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(); +export const CommandMap = { + ddg: new Command({ + name: 'ddg', + args: '', + description: _td('Searches DuckDuckGo for results'), + runFn: function(roomId, args) { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + // 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(); + }, }), - // Change your nickname - nick: new Command("nick", "", 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", " []", 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), - ); + nick: new Command({ + name: 'nick', + args: '', + description: _td('Changes your display nickname'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setDisplayName(args)); } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - // Change the room topic - topic: new Command("topic", "", function(roomId, args) { - if (args) { - return success( - MatrixClientPeg.get().setRoomTopic(roomId, args), - ); - } - return reject(this.getUsage()); - }), - - // Invite a user - invite: new Command("invite", "", 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 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; } + tint: new Command({ + name: 'tint', + args: ' []', + description: _td('Changes colour scheme of current room'), + 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})))?$/); + 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; } - if (targetRoomId) { break; } - } - if (!targetRoomId) { - return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); + return success( + SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), + ); } } - } - if (!targetRoomId) targetRoomId = roomId; - return success( - MatrixClientPeg.get().leave(targetRoomId).then( - function() { - dis.dispatch({action: 'view_next_room'}); - }, - ), - ); + return reject(this.getUsage()); + }, }), - // Kick a user from the room with an optional reason - kick: new Command("kick", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - return success( - MatrixClientPeg.get().kick(roomId, matches[1], matches[3]), - ); + topic: new Command({ + name: 'topic', + args: '', + description: _td('Sets the room topic'), + runFn: function(roomId, args) { + if (args) { + return success(MatrixClientPeg.get().setRoomTopic(roomId, args)); } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, + }), + + invite: new Command({ + name: 'invite', + args: '', + 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: '', + 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: '[]', + 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: ' [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: new Command("ban", " []", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+?)( +(.*))?$/); - if (matches) { - return success( - MatrixClientPeg.get().ban(roomId, matches[1], matches[3]), - ); + ban: new Command({ + name: 'ban', + args: ' [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])); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - // Unban a user from the room - unban: new Command("unban", "", 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]), - ); + // Unban a user from ythe room + unban: new Command({ + name: 'unban', + args: '', + description: _td('Unbans user with given id'), + 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])); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - ignore: new Command("ignore", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - const userId = matches[1]; - const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); - ignoredUsers.push(userId); // de-duped internally in the js-sdk - return success( - MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, { - title: _t("Ignored user"), - description: ( -
-

{ _t("You are now ignoring %(userId)s", {userId: userId}) }

-
- ), - hasCancelButton: false, - }); - }), - ); + ignore: new Command({ + name: 'ignore', + args: '', + description: _td('Ignores a user, hiding their messages from you'), + runFn: function(roomId, args) { + if (args) { + const cli = MatrixClientPeg.get(); + + const matches = args.match(/^(\S+)$/); + 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(() => { + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, { + title: _t('Ignored user'), + description:
+

{ _t('You are now ignoring %(userId)s', {userId}) }

+
, + hasCancelButton: false, + }); + }), + ); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - unignore: new Command("unignore", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - const userId = matches[1]; - const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); - const index = ignoredUsers.indexOf(userId); - if (index !== -1) ignoredUsers.splice(index, 1); - return success( - MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, { - title: _t("Unignored user"), - description: ( -
-

{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }

-
- ), - hasCancelButton: false, - }); - }), - ); + unignore: new Command({ + name: 'unignore', + args: '', + description: _td('Stops ignoring a user, showing their messages going forward'), + runFn: function(roomId, args) { + if (args) { + const cli = MatrixClientPeg.get(); + + const matches = args.match(/^(\S+)$/); + 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(() => { + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, { + title: _t('Unignored user'), + description:
+

{ _t('You are no longer ignoring %(userId)s', {userId}) }

+
, + hasCancelButton: false, + }); + }), + ); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), // Define the power level of a user - op: new Command("op", " []", 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]) { - powerLevel = parseInt(matches[3]); - } - if (!isNaN(powerLevel)) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - return reject("Bad room ID: " + roomId); + op: new Command({ + name: 'op', + args: ' []', + 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]) { + powerLevel = parseInt(matches[3]); + } + 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 - deop: new Command("deop", "", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+)$/); - if (matches) { - const room = MatrixClientPeg.get().getRoom(roomId); - if (!room) { - return reject("Bad room ID: " + roomId); - } + deop: new Command({ + name: 'deop', + args: '', + 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); + if (!room) return reject('Bad room ID: ' + roomId); - const powerLevelEvent = room.currentState.getStateEvents( - "m.room.power_levels", "", - ); - return success( - MatrixClientPeg.get().setPowerLevel( - roomId, args, undefined, powerLevelEvent, - ), - ); + const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', ''); + return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent)); + } } - } - return reject(this.getUsage()); + return reject(this.getUsage()); + }, }), - // Open developer tools - devtools: new Command("devtools", "", function(roomId) { - const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog"); - Modal.createDialog(DevtoolsDialog, { roomId }); - return success(); + devtools: new Command({ + name: 'devtools', + description: _td('Opens the Developer Tools dialog'), + runFn: function(roomId) { + const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog'); + Modal.createDialog(DevtoolsDialog, {roomId}); + return success(); + }, }), // Verify a user, device, and pubkey tuple - verify: new Command("verify", " ", function(roomId, args) { - if (args) { - const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); - if (matches) { - const userId = matches[1]; - const deviceId = matches[2]; - const fingerprint = matches[3]; + verify: new Command({ + name: 'verify', + args: ' ', + description: _td('Verifies a user, device, and pubkey tuple'), + runFn: function(roomId, args) { + if (args) { + const matches = args.match(/^(\S+) +(\S+) +(\S+)$/); + if (matches) { + const cli = MatrixClientPeg.get(); - return success( - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => { - if (!device) { - throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`); - } + const userId = matches[1]; + const deviceId = matches[2]; + const fingerprint = matches[3]; - if (device.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new Error(_t(`Device already verified!`)); - } else { - throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`)); + return success( + // Promise.resolve to handle transition from static result to promise; can be removed + // in future + Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { + if (!device) { + throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); } - } - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key' + - ' "%(fingerprint)s". This could mean your communications are being intercepted!', - {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})); - } + if (device.isVerified()) { + if (device.getFingerprint() === fingerprint) { + throw new Error(_t('Device already verified!')); + } else { + throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); + } + } - return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true); - }).then(() => { - // Tell the user we verified everything - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { - title: _t("Verified key"), - description: ( -
-

- { - _t("The signing key you provided matches the signing key you received " + - "from %(userId)s's device %(deviceId)s. Device marked as verified.", - {userId: userId, deviceId: deviceId}) - } -

-
- ), - hasCancelButton: false, - }); - }), - ); + if (device.getFingerprint() !== fingerprint) { + const fprint = device.getFingerprint(); + throw new Error( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + + '"%(fingerprint)s". This could mean your communications are being intercepted!', + { + fprint, + userId, + deviceId, + fingerprint, + })); + } + + return cli.setDeviceVerified(userId, deviceId, true); + }).then(() => { + // Tell the user we verified everything + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { + title: _t('Verified key'), + description:
+

+ { + _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}) + } +

+
, + 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: '', + description: _td('Displays action'), }), }; /* eslint-enable babel/no-invalid-this */ @@ -421,50 +476,39 @@ const aliases = { j: "join", }; -module.exports = { - /** - * Process the given text for /commands and perform them. - * @param {string} roomId The room in which the command was performed. - * @param {string} input The raw text input by the user. - * @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. - * Returns null if the input didn't match a command. - */ - processInput: function(roomId, input) { - // trim any trailing whitespace, as it can confuse the parser for - // IRC-style commands - input = input.replace(/\s+$/, ""); - 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 - }, +/** + * Process the given text for /commands and perform them. + * @param {string} roomId The room in which the command was performed. + * @param {string} input The raw text input by the user. + * @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. + * Returns null if the input didn't match a command. + */ +export function processCommandInput(roomId, input) { + // trim any trailing whitespace, as it can confuse the parser for + // IRC-style commands + input = input.replace(/\s+$/, ''); + if (input[0] !== '/' || input[1] === '/') return null; // not a command - getCommandList: function() { - // Return all the commands plus /me and /markdown which aren't handled like normal commands - const cmds = Object.keys(commands).sort().map(function(cmdKey) { - return commands[cmdKey]; - }); - cmds.push(new Command("me", "", function() {})); - cmds.push(new Command("markdown", "", function() {})); + const bits = input.match(/^(\S+?)( +((.|\n)*))?$/); + let cmd; + let args; + if (bits) { + cmd = bits[1].substring(1).toLowerCase(); + args = bits[3]; + } 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); + } +} diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index b162f2f92a..01a40be40c 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -18,101 +18,14 @@ limitations under the License. */ import React from 'react'; -import { _t, _td } from '../languageHandler'; +import {_t} from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; +import {CommandMap} from '../SlashCommands'; import type {SelectionRange} from "./Autocompleter"; -// TODO merge this with the factory mechanics of SlashCommands? -// 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: '', - description: _td('Displays action'), - }, - { - command: '/ban', - args: ' [reason]', - description: _td('Bans user with given id'), - }, - { - command: '/unban', - args: '', - description: _td('Unbans user with given id'), - }, - { - command: '/op', - args: ' []', - description: _td('Define the power level of a user'), - }, - { - command: '/deop', - args: '', - description: _td('Deops user with given id'), - }, - { - command: '/invite', - args: '', - description: _td('Invites user with given id to current room'), - }, - { - command: '/join', - args: '', - description: _td('Joins room with given alias'), - }, - { - command: '/part', - args: '[]', - description: _td('Leave room'), - }, - { - command: '/topic', - args: '', - description: _td('Sets the room topic'), - }, - { - command: '/kick', - args: ' [reason]', - description: _td('Kicks user with given id'), - }, - { - command: '/nick', - args: '', - description: _td('Changes your display nickname'), - }, - { - command: '/ddg', - args: '', - description: _td('Searches DuckDuckGo for results'), - }, - { - command: '/tint', - args: ' []', - description: _td('Changes colour scheme of current room'), - }, - { - command: '/verify', - args: ' ', - description: _td('Verifies a user, device, and pubkey tuple'), - }, - { - command: '/ignore', - args: '', - description: _td('Ignores a user, hiding their messages from you'), - }, - { - command: '/unignore', - args: '', - description: _td('Stops ignoring a user, showing their messages going forward'), - }, - { - command: '/devtools', - args: '', - description: _td('Opens the Developer Tools dialog'), - }, -]; +const COMMANDS = Object.values(CommandMap); const COMMAND_RE = /(^\/\w*)(?: .*)?/g; @@ -134,11 +47,10 @@ export default class CommandProvider extends AutocompleteProvider { return { // 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: (), + description={_t(result.description)} />, range, }; }); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 97e8780f0f..1987d4f4be 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -28,7 +28,7 @@ import Promise from 'bluebird'; import MatrixClientPeg from '../../../MatrixClientPeg'; import type {MatrixClient} from 'matrix-js-sdk/lib/matrix'; -import SlashCommands from '../../../SlashCommands'; +import {processCommandInput} from '../../../SlashCommands'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard'; import Modal from '../../../Modal'; import sdk from '../../../index'; @@ -722,7 +722,7 @@ export default class MessageComposerInput extends React.Component { // Some commands (/join) require pills to be replaced with their text content 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.error) { this.historyManager.save(contentState, this.state.isRichtextEnabled ? 'html' : 'markdown'); From 107802fa29a8e3dce0f7058f42264c8957d0b6f0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 19:32:12 +0100 Subject: [PATCH 2/3] simplify arrow func Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 01a40be40c..0e780bddd3 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -43,17 +43,15 @@ export default class CommandProvider extends AutocompleteProvider { // if the query is just `/` (and the user hit TAB or waits), show them all COMMANDS otherwise FuzzyMatch them const matches = query === '/' ? COMMANDS : this.matcher.match(command[1]); - return matches.map((result) => { - return { - // 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: , - range, - }; - }); + 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: , + range, + })); } getName() { From 9de6bf4f5a109e62590d3e848c22f9f9189530da Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 Jun 2018 17:37:55 +0100 Subject: [PATCH 3/3] once command has a space, strict match instead of fuzzy match Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> (cherry picked from commit a852033) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/autocomplete/CommandProvider.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 0e780bddd3..ea271e02ff 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -41,8 +41,26 @@ export default class CommandProvider extends AutocompleteProvider { const {command, range} = this.getCurrentCommand(query, selection); if (!command) return []; - // if the query is just `/` (and the user hit TAB or waits), show them all COMMANDS otherwise FuzzyMatch them - const matches = query === '/' ? COMMANDS : this.matcher.match(command[1]); + let matches; + if (command[0] !== command[1]) { + // The input looks like a command with arguments, perform exact match + const match = COMMANDS.find((o) => o.command === command[1]); + if (match) { + matches = [match]; + } + } + + // If we don't yet have matches + if (!matches) { + 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 + ' '),