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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import { slashCommands } from '../../lib/slashCommand';
slashCommands.add({
command: 'archive',
options: {
description: 'Archive',
params: '#channel',
permission: 'archive-room',
},
providesPreview: false,
});

View 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,
});

View 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,
});

View 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,
});

View 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,
},
});

View File

@@ -0,0 +1,9 @@
import { slashCommands } from '../../lib/slashCommand';
slashCommands.add({
command: 'hide',
options: {
description: 'Hide_room',
params: '#room',
},
});

View 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';

View 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'] });
},
});

View 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,
});

View 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();
});
});

View 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 });
}
},
});

View 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();
});
});

View 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'] });
},
});

View 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,
},
});

View 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();
});
});

View 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'],
},
});

View 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,
},
});

View File

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

View 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',
},
});

View 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,
},
});

View 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();
});
});

View 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',
},
});

View File

@@ -0,0 +1,11 @@
import { slashCommands } from '../../lib/slashCommand';
slashCommands.add({
command: 'unarchive',
options: {
description: 'Unarchive',
params: '#channel',
permission: 'unarchive-room',
},
providesPreview: false,
});

View 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,
},
});