Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEW] Accept quoted slash command arguments #11744

Merged
merged 19 commits into from
Jun 22, 2022
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c1a1c29
Changed the apps api to recognize quotes on slash command arguments
pierre-lehnen-rc Aug 10, 2018
3771922
Accept slashed quotes as quotes on slash commands
pierre-lehnen-rc Aug 14, 2018
537153f
Merge branch 'develop' into apps.recognize-quoted-arguments
Hudell Aug 20, 2018
e31523a
new eslint rules
pierre-lehnen-rc Aug 21, 2018
e5eaee5
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Aug 27, 2018
ef5c926
Fixed parameter parsing without quotes
pierre-lehnen-rc Aug 27, 2018
fd89d97
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Sep 10, 2018
343c817
Added support for multiple lines on slash commands
pierre-lehnen-rc Sep 12, 2018
ea3cf49
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Sep 12, 2018
a6dd039
Merge branch 'develop' into apps.recognize-quoted-arguments
Hudell Dec 17, 2018
2d486a3
Merge branch 'develop' into apps.recognize-quoted-arguments
d-gubert Dec 18, 2018
22de99a
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Jun 30, 2020
1075abf
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Jul 16, 2020
37a5b1d
Merge branch 'develop' into apps.recognize-quoted-arguments
sampaiodiego Aug 7, 2020
c75c3f9
Merge branch 'develop' into apps.recognize-quoted-arguments
pierre-lehnen-rc Jun 15, 2022
d914e5a
- Added unit tests
pierre-lehnen-rc Jun 15, 2022
c5543d1
Removed a comment
pierre-lehnen-rc Jun 15, 2022
9f4b7f9
Improved handling of line breaks and tabs
pierre-lehnen-rc Jun 16, 2022
1ffe613
Update apps/meteor/lib/utils/parseParameters.ts
pierre-lehnen-rc Jun 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions apps/meteor/app/apps/server/bridges/commands.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import type { IMessage, RequiredField, SlashCommand } from '@rocket.chat/core-ty
import { slashCommands } from '../../../utils/server';
import { Utilities } from '../../lib/misc/Utilities';
import { AppServerOrchestrator } from '../orchestrator';
import { parseParameters } from '../../../../lib/utils/parseParameters';

export class AppCommandsBridge extends CommandBridge {
disabledCommands: Map<string, typeof slashCommands.commands[string]>;
@@ -169,9 +170,15 @@ export class AppCommandsBridge extends CommandBridge {
const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId());
const room = this.orch.getConverters()?.get('rooms').convertById(message.rid);
const threadId = message.tmid;
const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' ');
const params = parseParameters(parameters);

const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId, triggerId);
const context = new SlashCommandContext(
Object.freeze(user),
Object.freeze(room),
Object.freeze(params) as string[],
threadId,
triggerId,
);

Promise.await(this.orch.getManager()?.getCommandManager().executeCommand(command, context));
}
@@ -180,9 +187,9 @@ export class AppCommandsBridge extends CommandBridge {
const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId());
const room = this.orch.getConverters()?.get('rooms').convertById(message.rid);
const threadId = message.tmid;
const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' ');
const params = parseParameters(parameters);

const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId);
const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params) as string[], threadId);
return Promise.await(this.orch.getManager()?.getCommandManager().getPreviews(command, context));
}

@@ -196,9 +203,15 @@ export class AppCommandsBridge extends CommandBridge {
const user = this.orch.getConverters()?.get('users').convertById(Meteor.userId());
const room = this.orch.getConverters()?.get('rooms').convertById(message.rid);
const threadId = message.tmid;
const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' ');

const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params), threadId, triggerId);
const params = parseParameters(parameters);

const context = new SlashCommandContext(
Object.freeze(user),
Object.freeze(room),
Object.freeze(params) as string[],
threadId,
triggerId,
);

await this.orch.getManager()?.getCommandManager().executePreview(command, preview, context);
}
14 changes: 7 additions & 7 deletions apps/meteor/app/ui/client/lib/chatMessages.js
Original file line number Diff line number Diff line change
@@ -416,13 +416,13 @@ export class ChatMessages {

async processSlashCommand(msgObject) {
if (msgObject.msg[0] === '/') {
const match = msgObject.msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
const match = msgObject.msg.match(/^\/([^\s]+)/m);
if (match) {
let command;
if (slashCommands.commands[match[1]]) {
const commandOptions = slashCommands.commands[match[1]];
command = match[1];
const param = match[2] || '';
const command = match[1];

if (slashCommands.commands[command]) {
const commandOptions = slashCommands.commands[command];
const param = msgObject.msg.replace(/^\/([^\s]+)/m, '');

if (!commandOptions.permission || hasAtLeastOnePermission(commandOptions.permission, Session.get('openedRoom'))) {
if (commandOptions.clientOnly) {
@@ -449,7 +449,7 @@ export class ChatMessages {
_id: Random.id(),
rid: msgObject.rid,
ts: new Date(),
msg: TAPi18n.__('No_such_command', { command: escapeHTML(match[1]) }),
msg: TAPi18n.__('No_such_command', { command: escapeHTML(command) }),
u: {
username: settings.get('InternalHubot_Username') || 'rocket.cat',
},
64 changes: 64 additions & 0 deletions apps/meteor/lib/utils/parseParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
function split(parameters: string, ignoreExtraUnquotedSpaces = true): string[] {
// Replace \n\r with a single \n
const line = parameters.replace(/\n\r/gm, '\n');

const items = line.split(/[ \n\r\t]/);

if (ignoreExtraUnquotedSpaces) {
return items.filter(Boolean);
}

return items;
}

export function parseParameters(parameters: string, ignoreExtraUnquotedSpaces = true): string[] {
if (!parameters.trim()) {
return [];
}

const match = parameters.match(/((["'])(?:(?=(\\?))\3.)*?\2)/gs);
let line = parameters;

if (!match) {
return split(line, ignoreExtraUnquotedSpaces);
}

match.forEach((item) => {
const newItem = item
// Replace start quote with SSA character
.replace(/(^['"])/g, '\u0086')
// Replace end quote with ESA character
.replace(/['"]$/g, '\u0087')
// Replace spaces inside the quotes with a Punctuation Space character
.replace(/ /g, '\u2008')
// Replace new lines inside the quotes with a PLD character
.replace(/\n/gm, '\u008B')
// Replace carriage returns inside the quotes with a PLU character
.replace(/\r/gm, '\u008C')
// Replace tabs inside the quotes with a VTS character
.replace(/\t/g, '\u008A');

line = line.replace(item, newItem);
});

// If two quoted parameters are not separated by a space, add one automatically
line = line.replace(/\u0087\u0086/g, '\u0087 \u0086');

const items = split(line, ignoreExtraUnquotedSpaces);

return items.map((item) =>
item
// Convert back the spaces from inside quotes
.replace(/\u2008/g, ' ')
// Convert back the new lines from inside quotes
.replace(/\u008B/g, '\n')
// Convert back the carriage returns from inside quotes
.replace(/\u008C/g, '\r')
// Convert back the tabs from inside quotes
.replace(/\u008A/g, '\t')
// Remove SSA and ESA characters
.replace(/[\u0086\u0087]/g, '')
// Unescape quotes
.replace(/\\\"/g, '"'),
);
}
185 changes: 185 additions & 0 deletions apps/meteor/tests/unit/lib/utils/parseParameters.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { expect } from 'chai';

import { parseParameters } from '../../../../lib/utils/parseParameters';

describe('Parse Parameters', () => {
it('should return an empty array for an empty string', () => {
const result = parseParameters('');

expect(result).to.be.an('Array').with.lengthOf(0);
});

it('should return an array with one item', () => {
const value = 'value';
const result = parseParameters(value);

expect(result).to.be.an('Array').with.lengthOf(1).and.eql([value]);
});

it('should return an array with three items', () => {
const value1 = 'value1';
const value2 = 'value2';
const value3 = 'value3';

const parameters = `${value1} ${value2} ${value3}`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3).and.eql([value1, value2, value3]);
});

it('should ignore extra spaces by default', () => {
const value1 = 'value1';
const value2 = 'value2';

const parameters = ` ${value1} ${value2} `;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(2).and.eql([value1, value2]);
});

it('should NOT ignore extra spaces when requested', () => {
const value1 = '';
const value2 = '';
const value3 = 'value3';
const value4 = '';
const value5 = '';
const value6 = 'value6';
const value7 = '';
const value8 = '';

const parameters = `${value1} ${value2} ${value3} ${value4} ${value5} ${value6} ${value7} ${value8}`;
const result = parseParameters(parameters, false);

expect(result).to.be.an('Array').with.lengthOf(8).and.eql([value1, value2, value3, value4, value5, value6, value7, value8]);
});

it('should return an array with one item without quotes', () => {
const value = 'value';
const result = parseParameters(`"${value}"`);

expect(result).to.be.an('Array').with.lengthOf(1).and.eql([value]);
});
it('should return an array with three items without quotes', () => {
const value1 = 'value1';
const value2 = 'value2';
const value3 = 'value3';
const result = parseParameters(`"${value1}" "${value2}" "${value3}"`);

expect(result).to.be.an('Array').with.lengthOf(3).and.eql([value1, value2, value3]);
});

it('should not trim spaces inside quotes', () => {
const value1 = 'value1 ';
const value2 = ' value2';
const value3 = ' value3 ';

const parameters = `"${value1}" "${value2}" "${value3}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3).and.eql([value1, value2, value3]);
});

it('should split parameters even without spaces', () => {
const value1 = 'value1';
const value2 = 'value2';
const value3 = 'value3';
const value4 = 'value4';

const parameters = `"${value1}""${value2}""${value3}" "${value4}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(4).and.eql([value1, value2, value3, value4]);
});

it('should split parameters by both spaces and quotes in the same line', () => {
const value1 = 'value1';
const value2 = 'value2';

const parameters = `"${value1}" ${value2}`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(2).and.eql([value1, value2]);
});

it('should unescape quotes inside values', () => {
const value1 = `this is \\"value1\\" here`;
const value2 = `\\"value2\\"`;

const parameters = `"${value1}" "${value2}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(2);
expect(result[0]).to.be.equal(value1.replace(/\\\"/g, '"'));
expect(result[1]).to.be.equal(value2.replace(/\\\"/g, '"'));
});

it('should not ignore empty quoted parameters', () => {
const value1 = 'value1';
const value2 = '';
const value3 = 'value3';

const parameters = `"${value1}" "${value2}" "${value3}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3).and.eql([value1, value2, value3]);
});

it('should accept line breaks inside quotes', () => {
const value1 = `value1
is multiline`;
const value2 = 'value2\nhas a multine too';
const value3 = 'value3\rhas a carriage return';

const parameters = `"${value1}" "${value2}" "${value3}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3);
expect(result[0]).to.be.equal(value1);
expect(result[1]).to.be.equal(value2);
expect(result[2]).to.be.equal(value3);
});

it('should split on line breaks without quotes', () => {
const value1 = `value1`;
const value2 = 'value2';
const value3 = 'value3';

const parameters = `${value1}
${value2}
${value3}`;

const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3);
expect(result[0]).to.be.equal(value1);
expect(result[1]).to.be.equal(value2);
expect(result[2]).to.be.equal(value3);
});

it('should accept tabs inside quotes', () => {
const value1 = `value1 is tabbed`;
const value2 = 'value2';

const parameters = `"${value1}" "${value2}"`;
const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(2);
expect(result[0]).to.be.equal(value1);
expect(result[1]).to.be.equal(value2);
});

it('should split on tabs without quotes', () => {
const value1 = `value1`;
const value2 = 'value2';
const value3 = 'value3';

const parameters = `${value1} ${value2} ${value3}`;

const result = parseParameters(parameters);

expect(result).to.be.an('Array').with.lengthOf(3);
expect(result[0]).to.be.equal(value1);
expect(result[1]).to.be.equal(value2);
expect(result[2]).to.be.equal(value3);
});
});