snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
11
apps/meteor/client/startup/slashCommands/archive.ts
Normal file
11
apps/meteor/client/startup/slashCommands/archive.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'archive',
|
||||
options: {
|
||||
description: 'Archive',
|
||||
params: '#channel',
|
||||
permission: 'archive-room',
|
||||
},
|
||||
providesPreview: false,
|
||||
});
|
||||
32
apps/meteor/client/startup/slashCommands/ban.ts
Normal file
32
apps/meteor/client/startup/slashCommands/ban.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { queryClient } from '../../lib/queryClient';
|
||||
import { roomsQueryKeys } from '../../lib/queryKeys';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
const invalidateMembers = (err: unknown, _result: unknown, params: { msg: { rid: string } }) => {
|
||||
if (err) return;
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: roomsQueryKeys.bannedUsers(params.msg.rid) });
|
||||
void queryClient.invalidateQueries({ queryKey: [...roomsQueryKeys.room(params.msg.rid), 'members'] });
|
||||
};
|
||||
|
||||
slashCommands.add({
|
||||
command: 'ban',
|
||||
providesPreview: false,
|
||||
options: {
|
||||
description: 'Ban_user_from_room',
|
||||
params: '@username',
|
||||
permission: 'ban-user',
|
||||
},
|
||||
result: invalidateMembers,
|
||||
});
|
||||
|
||||
slashCommands.add({
|
||||
command: 'unban',
|
||||
providesPreview: false,
|
||||
options: {
|
||||
description: 'Unban_user_from_room',
|
||||
params: '@username',
|
||||
permission: 'ban-user',
|
||||
},
|
||||
result: invalidateMembers,
|
||||
});
|
||||
11
apps/meteor/client/startup/slashCommands/create.ts
Normal file
11
apps/meteor/client/startup/slashCommands/create.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'create',
|
||||
options: {
|
||||
description: 'Create_A_New_Channel',
|
||||
params: '#channel',
|
||||
permission: ['create-c', 'create-p'],
|
||||
},
|
||||
providesPreview: false,
|
||||
});
|
||||
29
apps/meteor/client/startup/slashCommands/federation.ts
Normal file
29
apps/meteor/client/startup/slashCommands/federation.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
const callback = undefined;
|
||||
const result = undefined;
|
||||
const providesPreview = false;
|
||||
const previewer = undefined;
|
||||
const previewCallback = undefined;
|
||||
|
||||
slashCommands.add({
|
||||
command: 'federation',
|
||||
callback,
|
||||
options: {
|
||||
description: 'Federation_slash_commands',
|
||||
params: '#command (dm) #user',
|
||||
},
|
||||
result,
|
||||
providesPreview,
|
||||
previewer,
|
||||
previewCallback,
|
||||
});
|
||||
|
||||
slashCommands.add({
|
||||
command: 'xmpp-join',
|
||||
options: {
|
||||
description: 'Join xmpp rooms',
|
||||
params: '#channel',
|
||||
},
|
||||
providesPreview: false,
|
||||
});
|
||||
22
apps/meteor/client/startup/slashCommands/gimme.ts
Normal file
22
apps/meteor/client/startup/slashCommands/gimme.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
/*
|
||||
* Gimme is a named function that will replace /gimme commands
|
||||
* @param {Object} message - The message object
|
||||
*/
|
||||
async function Gimme({ message, params }: SlashCommandCallbackParams<'gimme'>): Promise<void> {
|
||||
const msg = message;
|
||||
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `༼ つ ◕_◕ ༽つ ${params}` } });
|
||||
}
|
||||
|
||||
slashCommands.add({
|
||||
command: 'gimme',
|
||||
callback: Gimme,
|
||||
options: {
|
||||
description: 'Slash_Gimme_Description',
|
||||
params: 'your_message_optional',
|
||||
clientOnly: true,
|
||||
},
|
||||
});
|
||||
9
apps/meteor/client/startup/slashCommands/hide.ts
Normal file
9
apps/meteor/client/startup/slashCommands/hide.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'hide',
|
||||
options: {
|
||||
description: 'Hide_room',
|
||||
params: '#room',
|
||||
},
|
||||
});
|
||||
19
apps/meteor/client/startup/slashCommands/index.ts
Normal file
19
apps/meteor/client/startup/slashCommands/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import './archive';
|
||||
import './ban';
|
||||
import './create';
|
||||
import './federation';
|
||||
import './gimme';
|
||||
import './hide';
|
||||
import './invite';
|
||||
import './inviteAllTo';
|
||||
import './join';
|
||||
import './kick';
|
||||
import './lenny';
|
||||
import './open';
|
||||
import './shrug';
|
||||
import './slackbridgeImport';
|
||||
import './status';
|
||||
import './tableflip';
|
||||
import './topic';
|
||||
import './unarchive';
|
||||
import './unflip';
|
||||
16
apps/meteor/client/startup/slashCommands/invite.ts
Normal file
16
apps/meteor/client/startup/slashCommands/invite.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { queryClient } from '../../lib/queryClient';
|
||||
import { roomsQueryKeys } from '../../lib/queryKeys';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'invite',
|
||||
options: {
|
||||
description: 'Invite_user_to_join_channel',
|
||||
params: '@username',
|
||||
permission: 'add-user-to-joined-room',
|
||||
},
|
||||
providesPreview: false,
|
||||
result: (err, _result, params) => {
|
||||
if (!err) void queryClient.invalidateQueries({ queryKey: [...roomsQueryKeys.room(params.msg.rid), 'members'] });
|
||||
},
|
||||
});
|
||||
26
apps/meteor/client/startup/slashCommands/inviteAllTo.ts
Normal file
26
apps/meteor/client/startup/slashCommands/inviteAllTo.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { queryClient } from '../../lib/queryClient';
|
||||
import { roomsQueryKeys } from '../../lib/queryKeys';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
const invalidateMembers = (_err: unknown, _result: unknown, params: { msg: { rid: string } }) => {
|
||||
if (!_err) void queryClient.invalidateQueries({ queryKey: [...roomsQueryKeys.room(params.msg.rid), 'members'] });
|
||||
};
|
||||
|
||||
slashCommands.add({
|
||||
command: 'invite-all-to',
|
||||
options: {
|
||||
description: 'Invite_user_to_join_channel_all_to',
|
||||
params: '#room',
|
||||
permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'],
|
||||
},
|
||||
result: invalidateMembers,
|
||||
});
|
||||
slashCommands.add({
|
||||
command: 'invite-all-from',
|
||||
options: {
|
||||
description: 'Invite_user_to_join_channel_all_from',
|
||||
params: '#room',
|
||||
permission: 'add-user-to-joined-room',
|
||||
},
|
||||
result: invalidateMembers,
|
||||
});
|
||||
60
apps/meteor/client/startup/slashCommands/join.spec.ts
Normal file
60
apps/meteor/client/startup/slashCommands/join.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createFakeMessage } from '../../../tests/mocks/data';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
import './join';
|
||||
|
||||
const getResultHandler = () => {
|
||||
const { result } = slashCommands.commands.join;
|
||||
expect(result).toBeDefined();
|
||||
return result as NonNullable<typeof result>;
|
||||
};
|
||||
|
||||
describe('/join slash command', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('redirects to /open and rewrites the command message when the user is already in the room', () => {
|
||||
const result = getResultHandler();
|
||||
const run = jest.spyOn(slashCommands, 'run').mockResolvedValue(undefined);
|
||||
const params = {
|
||||
cmd: 'join',
|
||||
params: 'general',
|
||||
msg: createFakeMessage({ _id: 'message-id', rid: 'room-id', msg: '/join general' }),
|
||||
userId: 'user-id',
|
||||
};
|
||||
|
||||
result({ error: 'error-user-already-in-room' }, undefined, params);
|
||||
|
||||
expect(params.cmd).toBe('open');
|
||||
expect(params.msg.msg).toBe('/open general');
|
||||
expect(run).toHaveBeenCalledWith({
|
||||
command: 'open',
|
||||
params: 'general',
|
||||
message: params.msg,
|
||||
triggerId: '',
|
||||
userId: 'user-id',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['a successful result', undefined],
|
||||
['an unrelated structured error', { error: 'error-not-allowed' }],
|
||||
['a generic error', new Error('Unexpected error')],
|
||||
])('does not redirect to /open for %s', (_case, error) => {
|
||||
const result = getResultHandler();
|
||||
const run = jest.spyOn(slashCommands, 'run').mockResolvedValue(undefined);
|
||||
const params = {
|
||||
cmd: 'join',
|
||||
params: 'general',
|
||||
msg: createFakeMessage({ _id: 'message-id', rid: 'room-id', msg: '/join general' }),
|
||||
userId: 'user-id',
|
||||
};
|
||||
|
||||
result(error, undefined, params);
|
||||
|
||||
expect(params.cmd).toBe('join');
|
||||
expect(params.msg.msg).toBe('/join general');
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
17
apps/meteor/client/startup/slashCommands/join.ts
Normal file
17
apps/meteor/client/startup/slashCommands/join.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'join',
|
||||
options: {
|
||||
description: 'Join_the_given_channel',
|
||||
params: '#channel',
|
||||
permission: 'view-c-room',
|
||||
},
|
||||
result(err, _result: unknown, params: Record<string, any>) {
|
||||
if ((err as { error?: string } | undefined)?.error === 'error-user-already-in-room') {
|
||||
params.cmd = 'open';
|
||||
params.msg.msg = params.msg.msg.replace('join', 'open');
|
||||
return void slashCommands.run({ command: 'open', params: params.params, message: params.msg, triggerId: '', userId: params.userId });
|
||||
}
|
||||
},
|
||||
});
|
||||
73
apps/meteor/client/startup/slashCommands/kick.spec.ts
Normal file
73
apps/meteor/client/startup/slashCommands/kick.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createFakeMessage } from '../../../tests/mocks/data';
|
||||
import { queryClient } from '../../lib/queryClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
import './kick';
|
||||
|
||||
jest.mock('../../lib/queryClient', () => ({
|
||||
queryClient: {
|
||||
invalidateQueries: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const invalidateQueries = jest.mocked(queryClient.invalidateQueries);
|
||||
|
||||
const getResultHandler = () => {
|
||||
const { result } = slashCommands.commands.kick;
|
||||
expect(result).toBeDefined();
|
||||
return result as NonNullable<typeof result>;
|
||||
};
|
||||
|
||||
const getCallback = () => {
|
||||
const { callback } = slashCommands.commands.kick;
|
||||
expect(callback).toBeDefined();
|
||||
return callback as NonNullable<typeof callback>;
|
||||
};
|
||||
|
||||
const runCommand = (params: string) =>
|
||||
getCallback()({
|
||||
command: 'kick',
|
||||
params,
|
||||
message: { _id: 'message-id', rid: 'room-id' },
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
describe('/kick slash command', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['an unadorned username', 'alice'],
|
||||
['surrounding whitespace', ' alice '],
|
||||
['an @ prefix and surrounding whitespace', ' @alice '],
|
||||
])('returns the normalized username when given %s', (_case, input) => {
|
||||
expect(runCommand(input)).toBe('alice');
|
||||
});
|
||||
|
||||
it('returns nothing when the input is empty after trimming', () => {
|
||||
expect(runCommand(' ')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('invalidates the room-members query after success', () => {
|
||||
const result = getResultHandler();
|
||||
result(undefined, undefined, {
|
||||
cmd: 'kick',
|
||||
params: 'alice',
|
||||
msg: createFakeMessage({ rid: 'room-id' }),
|
||||
});
|
||||
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['rooms', 'room-id', 'members'] });
|
||||
});
|
||||
|
||||
it('does not invalidate the room-members query after an error', () => {
|
||||
const result = getResultHandler();
|
||||
result(new Error('Failed to kick user'), undefined, {
|
||||
cmd: 'kick',
|
||||
params: 'alice',
|
||||
msg: createFakeMessage({ rid: 'room-id' }),
|
||||
});
|
||||
|
||||
expect(invalidateQueries).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
24
apps/meteor/client/startup/slashCommands/kick.ts
Normal file
24
apps/meteor/client/startup/slashCommands/kick.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { queryClient } from '../../lib/queryClient';
|
||||
import { roomsQueryKeys } from '../../lib/queryKeys';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'kick',
|
||||
callback({ params }: SlashCommandCallbackParams<'kick'>) {
|
||||
const username = params.trim();
|
||||
if (username === '') {
|
||||
return;
|
||||
}
|
||||
return username.replace('@', '');
|
||||
},
|
||||
options: {
|
||||
description: 'Remove_someone_from_room',
|
||||
params: '@username',
|
||||
permission: 'remove-user',
|
||||
},
|
||||
result: (err, _result, params) => {
|
||||
if (!err) void queryClient.invalidateQueries({ queryKey: [...roomsQueryKeys.room(params.msg.rid), 'members'] });
|
||||
},
|
||||
});
|
||||
23
apps/meteor/client/startup/slashCommands/lenny.ts
Normal file
23
apps/meteor/client/startup/slashCommands/lenny.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
/*
|
||||
* Lenny is a named function that will replace /lenny commands
|
||||
* @param {Object} message - The message object
|
||||
*/
|
||||
|
||||
async function LennyFace({ message, params }: SlashCommandCallbackParams<'lenny'>): Promise<void> {
|
||||
const msg = message;
|
||||
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ( ͡° ͜ʖ ͡°)` } });
|
||||
}
|
||||
|
||||
slashCommands.add({
|
||||
command: 'lennyface',
|
||||
callback: LennyFace,
|
||||
options: {
|
||||
description: 'Slash_LennyFace_Description',
|
||||
params: 'your_message_optional',
|
||||
clientOnly: true,
|
||||
},
|
||||
});
|
||||
160
apps/meteor/client/startup/slashCommands/open.spec.ts
Normal file
160
apps/meteor/client/startup/slashCommands/open.spec.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { createFakeSubscription } from '../../../tests/mocks/data';
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
import { router } from '../../providers/RouterProvider';
|
||||
import { Subscriptions } from '../../stores';
|
||||
|
||||
import './open';
|
||||
|
||||
jest.mock('../../lib/SDKClient', () => ({
|
||||
sdk: {
|
||||
rest: {
|
||||
post: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/rooms/roomCoordinator', () => ({
|
||||
roomCoordinator: {
|
||||
openRouteLink: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../providers/RouterProvider', () => ({
|
||||
router: {
|
||||
getSearchParameters: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../stores', () => ({
|
||||
Subscriptions: {
|
||||
state: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const post = jest.mocked(sdk.rest.post);
|
||||
const openRouteLink = jest.mocked(roomCoordinator.openRouteLink);
|
||||
const getSearchParameters = jest.mocked(router.getSearchParameters);
|
||||
const findSubscription = jest.mocked(Subscriptions.state.find);
|
||||
|
||||
const getCallback = () => {
|
||||
const { callback } = slashCommands.commands.open;
|
||||
expect(callback).toBeDefined();
|
||||
return callback as NonNullable<typeof callback>;
|
||||
};
|
||||
|
||||
const runCommand = (params: string) =>
|
||||
getCallback()({
|
||||
command: 'open',
|
||||
params,
|
||||
message: { _id: 'message-id', rid: 'room-id' },
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
const setSubscriptions = (...subscriptions: ReturnType<typeof createFakeSubscription>[]) => {
|
||||
findSubscription.mockImplementation((predicate) => subscriptions.find(predicate));
|
||||
};
|
||||
|
||||
describe('/open slash command', () => {
|
||||
beforeEach(() => {
|
||||
getSearchParameters.mockReturnValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['channel', 'c', 'general'],
|
||||
['group', 'p', 'secret'],
|
||||
] as const)('opens the matching existing %s subscription', async (_kind, type, name) => {
|
||||
const wrongType = createFakeSubscription({ name, t: 'd' });
|
||||
const subscription = createFakeSubscription({ _id: 'subscription-id', rid: 'target-room-id', name, t: type });
|
||||
setSubscriptions(wrongType, subscription);
|
||||
|
||||
await runCommand(` #${name} `);
|
||||
|
||||
expect(openRouteLink).toHaveBeenCalledWith(type, subscription, {});
|
||||
expect(openRouteLink).toHaveBeenCalledTimes(1);
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens a matching existing subscription when the input has no prefix', async () => {
|
||||
const subscription = createFakeSubscription({ name: 'general', t: 'c' });
|
||||
setSubscriptions(subscription);
|
||||
|
||||
await runCommand('general');
|
||||
|
||||
expect(openRouteLink).toHaveBeenCalledWith('c', subscription, {});
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards the current search parameters when opening a subscription', async () => {
|
||||
const subscription = createFakeSubscription({ name: 'general', t: 'c' });
|
||||
setSubscriptions(subscription);
|
||||
getSearchParameters.mockReturnValue({ layout: 'embedded' });
|
||||
|
||||
await runCommand('#general');
|
||||
|
||||
expect(openRouteLink).toHaveBeenCalledWith('c', subscription, { layout: 'embedded' });
|
||||
});
|
||||
|
||||
it('uses the trimmed username from direct-message input', async () => {
|
||||
post.mockResolvedValue({} as never);
|
||||
|
||||
await runCommand(' @alice ');
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/v1/im.create', { username: 'alice' });
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens an existing direct-message subscription', async () => {
|
||||
const subscription = createFakeSubscription({ name: 'alice', t: 'd' });
|
||||
setSubscriptions(subscription);
|
||||
post.mockResolvedValue({} as never);
|
||||
|
||||
await runCommand('@alice');
|
||||
|
||||
expect(openRouteLink).toHaveBeenCalledWith('d', subscription, {});
|
||||
});
|
||||
|
||||
it('opens the direct-message subscription created by the REST API', async () => {
|
||||
const subscription = createFakeSubscription({ _id: 'subscription-id', rid: 'dm-room-id', name: 'alice', t: 'd' });
|
||||
findSubscription.mockReturnValueOnce(undefined).mockReturnValueOnce(subscription);
|
||||
post.mockResolvedValue({} as never);
|
||||
|
||||
await runCommand('@alice');
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/v1/im.create', { username: 'alice' });
|
||||
expect(openRouteLink).toHaveBeenCalledWith('d', subscription, {});
|
||||
expect(post.mock.invocationCallOrder[0]).toBeLessThan(openRouteLink.mock.invocationCallOrder[0]);
|
||||
});
|
||||
|
||||
it('does nothing when a channel or group subscription is missing', async () => {
|
||||
await expect(runCommand('#missing')).resolves.toBeUndefined();
|
||||
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
expect(openRouteLink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not crash when the direct-message subscription is still missing after creation', async () => {
|
||||
post.mockResolvedValue({} as never);
|
||||
|
||||
await expect(runCommand('@missing')).resolves.toBeUndefined();
|
||||
expect(post).toHaveBeenCalledWith('/v1/im.create', { username: 'missing' });
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect(openRouteLink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not crash when direct-message creation fails', async () => {
|
||||
post.mockRejectedValue(new Error('Failed to create direct message'));
|
||||
|
||||
await expect(runCommand('@alice')).resolves.toBeUndefined();
|
||||
expect(post).toHaveBeenCalledWith('/v1/im.create', { username: 'alice' });
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
expect(openRouteLink).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
50
apps/meteor/client/startup/slashCommands/open.ts
Normal file
50
apps/meteor/client/startup/slashCommands/open.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { RoomType, ISubscription, SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
import { router } from '../../providers/RouterProvider';
|
||||
import { Subscriptions } from '../../stores';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'open',
|
||||
callback: async function Open({ params }: SlashCommandCallbackParams<'open'>): Promise<void> {
|
||||
const dict: Record<string, RoomType[]> = {
|
||||
'#': ['c', 'p'],
|
||||
'@': ['d'],
|
||||
};
|
||||
|
||||
const room = params.trim().replace(/#|@/, '');
|
||||
const type = dict[params.trim()[0]] || [];
|
||||
|
||||
const predicate = ({ name, t }: ISubscription) => {
|
||||
return name === room && (type.length ? type.includes(t) : true);
|
||||
};
|
||||
|
||||
const subscription = Subscriptions.state.find(predicate);
|
||||
|
||||
if (subscription) {
|
||||
roomCoordinator.openRouteLink(subscription.t, subscription, router.getSearchParameters());
|
||||
}
|
||||
|
||||
if (type?.indexOf('d') === -1) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sdk.rest.post('/v1/im.create', { username: room });
|
||||
const subscription = Subscriptions.state.find(predicate);
|
||||
if (!subscription) {
|
||||
return;
|
||||
}
|
||||
roomCoordinator.openRouteLink(subscription.t, subscription, router.getSearchParameters());
|
||||
} catch (err: unknown) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
options: {
|
||||
description: 'Opens_a_channel_group_or_direct_message',
|
||||
params: 'room_name',
|
||||
clientOnly: true,
|
||||
permission: ['view-c-room', 'view-c-room', 'view-d-room', 'view-joined-room', 'create-d'],
|
||||
},
|
||||
});
|
||||
21
apps/meteor/client/startup/slashCommands/shrug.ts
Normal file
21
apps/meteor/client/startup/slashCommands/shrug.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
/*
|
||||
* Shrug is a named function that will replace /shrug commands
|
||||
* @param {Object} message - The message object
|
||||
*/
|
||||
|
||||
slashCommands.add({
|
||||
command: 'shrug',
|
||||
callback: async ({ message, params }: SlashCommandCallbackParams<'shrug'>): Promise<void> => {
|
||||
const msg = message;
|
||||
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ¯\\\\_(ツ)_/¯` } });
|
||||
},
|
||||
options: {
|
||||
description: 'Slash_Shrug_Description',
|
||||
params: 'your_message_optional',
|
||||
clientOnly: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { settings } from '../../lib/settings';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
settings.observe('SlackBridge_Enabled', (_key, value) => {
|
||||
if (value) {
|
||||
slashCommands.add({
|
||||
command: 'slackbridge-import',
|
||||
options: {
|
||||
description: 'Import_old_messages_from_slackbridge',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
delete slashCommands.commands['slackbridge-import'];
|
||||
}
|
||||
});
|
||||
24
apps/meteor/client/startup/slashCommands/status.ts
Normal file
24
apps/meteor/client/startup/slashCommands/status.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
import { dispatchToastMessage } from '../../lib/toast';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'status',
|
||||
callback: async function Status({ params, userId }: SlashCommandCallbackParams<'status'>): Promise<void> {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk.rest.post('/v1/users.setStatus', { message: params });
|
||||
} catch (error) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
}
|
||||
},
|
||||
options: {
|
||||
description: 'Slash_Status_Description',
|
||||
params: 'Slash_Status_Params',
|
||||
},
|
||||
});
|
||||
21
apps/meteor/client/startup/slashCommands/tableflip.ts
Normal file
21
apps/meteor/client/startup/slashCommands/tableflip.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
/*
|
||||
* Tableflip is a named function that will replace /Tableflip commands
|
||||
* @param {Object} message - The message object
|
||||
*/
|
||||
|
||||
slashCommands.add({
|
||||
command: 'tableflip',
|
||||
callback: async ({ message, params }: SlashCommandCallbackParams<'tableflip'>): Promise<void> => {
|
||||
const msg = message;
|
||||
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} (╯°□°)╯︵ ┻━┻` } });
|
||||
},
|
||||
options: {
|
||||
description: 'Slash_Tableflip_Description',
|
||||
params: 'your_message_optional',
|
||||
clientOnly: true,
|
||||
},
|
||||
});
|
||||
113
apps/meteor/client/startup/slashCommands/topic.spec.ts
Normal file
113
apps/meteor/client/startup/slashCommands/topic.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { clientCallbacks } from '@rocket.chat/ui-client';
|
||||
|
||||
import { createFakeRoom } from '../../../tests/mocks/data';
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { hasPermission } from '../../lib/authorization';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
import { dispatchToastMessage } from '../../lib/toast';
|
||||
import { Rooms } from '../../stores';
|
||||
|
||||
import './topic';
|
||||
|
||||
jest.mock('@rocket.chat/ui-client', () => ({
|
||||
clientCallbacks: {
|
||||
run: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/SDKClient', () => ({
|
||||
sdk: {
|
||||
rest: {
|
||||
post: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/authorization', () => ({
|
||||
hasPermission: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../lib/toast', () => ({
|
||||
dispatchToastMessage: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../stores', () => ({
|
||||
Rooms: {
|
||||
state: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const post = jest.mocked(sdk.rest.post);
|
||||
const checkPermission = jest.mocked(hasPermission);
|
||||
const runClientCallback = jest.mocked(clientCallbacks.run);
|
||||
const showToast = jest.mocked(dispatchToastMessage);
|
||||
const getRoom = jest.mocked(Rooms.state.get);
|
||||
|
||||
const room = createFakeRoom({ _id: 'room-id', t: 'c', name: 'general' });
|
||||
|
||||
const getCallback = () => {
|
||||
const { callback } = slashCommands.commands.topic;
|
||||
expect(callback).toBeDefined();
|
||||
return callback as NonNullable<typeof callback>;
|
||||
};
|
||||
|
||||
const runCommand = (params = 'New topic') =>
|
||||
getCallback()({
|
||||
command: 'topic',
|
||||
params,
|
||||
message: { _id: 'message-id', rid: 'room-id' },
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
describe('/topic slash command', () => {
|
||||
beforeEach(() => {
|
||||
checkPermission.mockReturnValue(true);
|
||||
post.mockResolvedValue({} as never);
|
||||
runClientCallback.mockResolvedValue(undefined);
|
||||
getRoom.mockReturnValue(room);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('updates the topic when the user has permission', async () => {
|
||||
await runCommand('Updated topic');
|
||||
|
||||
expect(checkPermission).toHaveBeenCalledWith('edit-room', 'room-id');
|
||||
expect(post).toHaveBeenCalledWith('/v1/rooms.saveRoomSettings', {
|
||||
rid: 'room-id',
|
||||
roomTopic: 'Updated topic',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not call the API when permission is missing', async () => {
|
||||
checkPermission.mockReturnValue(false);
|
||||
|
||||
await runCommand();
|
||||
|
||||
expect(post).not.toHaveBeenCalled();
|
||||
expect(runClientCallback).not.toHaveBeenCalled();
|
||||
expect(getRoom).not.toHaveBeenCalled();
|
||||
expect(showToast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs the roomTopicChanged callback after a successful update', async () => {
|
||||
await runCommand();
|
||||
|
||||
expect(runClientCallback).toHaveBeenCalledWith('roomTopicChanged', room);
|
||||
expect(post.mock.invocationCallOrder[0]).toBeLessThan(runClientCallback.mock.invocationCallOrder[0]);
|
||||
});
|
||||
|
||||
it('displays an error toast and propagates the REST API error when the update fails', async () => {
|
||||
const error = new Error('Failed to update topic');
|
||||
post.mockRejectedValue(error);
|
||||
|
||||
await expect(runCommand()).rejects.toBe(error);
|
||||
expect(showToast).toHaveBeenCalledWith({ type: 'error', message: error });
|
||||
expect(runClientCallback).not.toHaveBeenCalled();
|
||||
expect(getRoom).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
28
apps/meteor/client/startup/slashCommands/topic.ts
Normal file
28
apps/meteor/client/startup/slashCommands/topic.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
import { clientCallbacks } from '@rocket.chat/ui-client';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { hasPermission } from '../../lib/authorization';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
import { dispatchToastMessage } from '../../lib/toast';
|
||||
import { Rooms } from '../../stores';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'topic',
|
||||
callback: async function Topic({ params, message }: SlashCommandCallbackParams<'topic'>): Promise<void> {
|
||||
if (hasPermission('edit-room', message.rid)) {
|
||||
try {
|
||||
await sdk.rest.post('/v1/rooms.saveRoomSettings', { rid: message.rid, roomTopic: params });
|
||||
await clientCallbacks.run('roomTopicChanged', Rooms.state.get(message.rid));
|
||||
} catch (error: unknown) {
|
||||
dispatchToastMessage({ type: 'error', message: error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
options: {
|
||||
description: 'Slash_Topic_Description',
|
||||
params: 'Slash_Topic_Params',
|
||||
permission: 'edit-room',
|
||||
},
|
||||
});
|
||||
11
apps/meteor/client/startup/slashCommands/unarchive.ts
Normal file
11
apps/meteor/client/startup/slashCommands/unarchive.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
|
||||
slashCommands.add({
|
||||
command: 'unarchive',
|
||||
options: {
|
||||
description: 'Unarchive',
|
||||
params: '#channel',
|
||||
permission: 'unarchive-room',
|
||||
},
|
||||
providesPreview: false,
|
||||
});
|
||||
21
apps/meteor/client/startup/slashCommands/unflip.ts
Normal file
21
apps/meteor/client/startup/slashCommands/unflip.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
|
||||
|
||||
import { sdk } from '../../lib/SDKClient';
|
||||
import { slashCommands } from '../../lib/slashCommand';
|
||||
/*
|
||||
* Unflip is a named function that will replace /unflip commands
|
||||
* @param {Object} message - The message object
|
||||
*/
|
||||
|
||||
slashCommands.add({
|
||||
command: 'unflip',
|
||||
callback: async ({ message, params }: SlashCommandCallbackParams<'unflip'>): Promise<void> => {
|
||||
const msg = message;
|
||||
await sdk.rest.post('/v1/chat.sendMessage', { message: { ...msg, msg: `${params} ┬─┬ ノ( ゜-゜ノ)` } });
|
||||
},
|
||||
options: {
|
||||
description: 'Slash_TableUnflip_Description',
|
||||
params: 'your_message_optional',
|
||||
clientOnly: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user