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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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,36 @@
import { Banner } from '@rocket.chat/core-services';
import type { IUiKitCoreApp, UiKitCoreAppViewClosedPayload } from '@rocket.chat/core-services';
import type * as UiKit from '@rocket.chat/ui-kit';
export class BannerModule implements IUiKitCoreApp {
appId = 'banner-core';
// when banner view is closed we need to dismiss that banner for that user
async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise<UiKit.ServerInteraction> {
const {
payload: { view: { viewId: bannerId } = {} },
user: { _id: userId } = {},
} = payload;
if (!userId) {
throw new Error('invalid user');
}
if (!bannerId) {
throw new Error('invalid banner');
}
if (!payload.triggerId) {
throw new Error('invalid triggerId');
}
await Banner.dismiss(userId, bannerId);
return {
type: 'banner.close',
triggerId: payload.triggerId,
appId: payload.appId,
viewId: bannerId,
};
}
}

View File

@@ -0,0 +1,297 @@
import { Banner } from '@rocket.chat/core-services';
import type {
IUiKitCoreApp,
UiKitCoreAppBlockActionPayload,
UiKitCoreAppViewClosedPayload,
UiKitCoreAppViewSubmitPayload,
} from '@rocket.chat/core-services';
import type { Cloud, IUser } from '@rocket.chat/core-typings';
import { Banners } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { isTruthy } from '@rocket.chat/tools';
import type * as UiKit from '@rocket.chat/ui-kit';
import { CloudWorkspaceConnectionError } from '../../../lib/errors/CloudWorkspaceConnectionError';
import { InvalidCloudAnnouncementInteractionError } from '../../../lib/errors/InvalidCloudAnnouncementInteractionError';
import { InvalidCoreAppInteractionError } from '../../../lib/errors/InvalidCoreAppInteractionError';
import { getWorkspaceAccessToken } from '../../lib/cloud';
import { assertNotOfflineLicense } from '../../lib/cloud/offlineLicense';
import { syncWorkspace } from '../../lib/cloud/syncWorkspace';
import { SystemLogger } from '../../lib/logger/system';
import { settings } from '../../settings';
type CloudAnnouncementInteractant =
| {
user: Pick<IUser, '_id' | 'username' | 'name'>;
}
| {
visitor: Pick<NonNullable<UiKitCoreAppBlockActionPayload['visitor']>, 'id' | 'username' | 'name' | 'department' | 'phone'>;
};
type CloudAnnouncementInteractionRequest = UiKit.UserInteraction & CloudAnnouncementInteractant;
export class CloudAnnouncementsModule implements IUiKitCoreApp {
appId = 'cloud-announcements-core';
protected async getWorkspaceAccessToken() {
return getWorkspaceAccessToken(true);
}
protected getCloudUrl() {
return settings.get('Cloud_Url');
}
blockAction(payload: UiKitCoreAppBlockActionPayload): Promise<UiKit.ServerInteraction | undefined> {
return this.handlePayload(payload);
}
viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise<UiKit.ServerInteraction | undefined> {
return this.handlePayload(payload);
}
async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise<UiKit.ServerInteraction | undefined> {
const {
payload: { view: { viewId, id } = {} },
user: { _id: userId } = {},
} = payload;
if (!userId) {
throw new Error('invalid user');
}
if (!id && !viewId) {
throw new Error('invalid view');
}
if (!payload.triggerId) {
throw new Error('invalid triggerId');
}
// For backwards compatibility: we prefer to use viewId, but some legacy banners
// may only have id. We fetch all matching banners and prioritize viewId match.
const bannerIds = [viewId, id].filter((bannerId) => isTruthy(bannerId));
const banners = await Banners.findByIds(bannerIds).toArray();
const announcement = banners.find((b) => b._id === viewId) || banners.find((b) => b._id === id);
if (!announcement) {
throw new Error('Banner not found');
}
await Banner.dismiss(userId, announcement._id);
const type = announcement.surface === 'banner' ? 'banner.close' : 'modal.close';
// for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response
void this.handlePayload(payload);
return {
type,
triggerId: payload.triggerId,
appId: payload.appId,
viewId: announcement._id,
};
}
protected async handlePayload(
payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload,
): Promise<UiKit.ServerInteraction | undefined> {
const interactant = this.getInteractant(payload);
const interaction = this.getInteraction(payload);
try {
const serverInteraction = await this.pushUserInteraction(interactant, interaction);
if (serverInteraction.appId !== this.appId) {
throw new InvalidCloudAnnouncementInteractionError(`Invalid appId`);
}
if (serverInteraction.triggerId !== interaction.triggerId) {
throw new InvalidCloudAnnouncementInteractionError(`Invalid triggerId`);
}
return serverInteraction;
} catch (err) {
SystemLogger.error({ err });
return undefined;
}
}
protected getInteractant(
payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload,
): CloudAnnouncementInteractant {
if (payload.user) {
return {
user: {
_id: payload.user._id,
username: payload.user.username,
name: payload.user.name,
},
};
}
if ('visitor' in payload && payload.visitor) {
return {
visitor: {
id: payload.visitor.id,
username: payload.visitor.username,
name: payload.visitor.name,
department: payload.visitor.department,
phone: payload.visitor.phone,
},
};
}
throw new CloudWorkspaceConnectionError(`Invalid user data received from Rocket.Chat Cloud`);
}
/**
* Transform the payload received from the Core App back to the format the UI sends from the client
*/
protected getInteraction(
payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload,
): UiKit.UserInteraction {
if (payload.type === 'blockAction' && payload.container?.type === 'message') {
const {
actionId,
payload: { blockId, value },
message,
room,
triggerId,
} = payload;
if (!actionId || !blockId || !triggerId) {
throw new InvalidCoreAppInteractionError();
}
return {
type: 'blockAction',
actionId,
payload: {
blockId,
value,
},
container: {
type: 'message',
id: String(message),
},
mid: String(message),
tmid: undefined,
rid: String(room),
triggerId,
};
}
if (payload.type === 'blockAction' && payload.container?.type === 'view') {
const {
actionId,
payload: { blockId, value },
container: { id },
triggerId,
} = payload;
if (!actionId || !blockId || !triggerId) {
throw new InvalidCoreAppInteractionError();
}
return {
type: 'blockAction',
actionId,
payload: {
blockId,
value,
},
container: {
type: 'view',
id,
},
triggerId,
};
}
if (payload.type === 'viewClosed') {
const {
payload: { view, isCleared },
triggerId,
} = payload;
if (!view?.id || !triggerId) {
throw new InvalidCoreAppInteractionError();
}
return {
type: 'viewClosed',
payload: {
viewId: view.id,
view: view as any,
isCleared: Boolean(isCleared),
},
triggerId,
};
}
if (payload.type === 'viewSubmit') {
const {
payload: { view },
triggerId,
} = payload;
if (!view?.id || !triggerId) {
throw new InvalidCoreAppInteractionError();
}
return {
type: 'viewSubmit',
payload: {
view: view as any,
},
triggerId,
viewId: view.id,
};
}
throw new InvalidCoreAppInteractionError();
}
protected async pushUserInteraction(
interactant: CloudAnnouncementInteractant,
userInteraction: UiKit.UserInteraction,
): Promise<UiKit.ServerInteraction> {
assertNotOfflineLicense();
const token = await this.getWorkspaceAccessToken();
const request: CloudAnnouncementInteractionRequest = {
...interactant,
...userInteraction,
};
const response = await fetch(`${this.getCloudUrl()}/api/v3/comms/workspace/interaction`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(request),
ignoreSsrfValidation: true,
});
if (!response.ok) {
const { error } = await response.json();
throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`);
}
const payload: Cloud.WorkspaceInteractionResponsePayload = await response.json();
const { serverInteraction, serverAction } = payload;
if (serverAction) {
switch (serverAction) {
case 'syncWorkspace': {
await syncWorkspace();
break;
}
}
}
return serverInteraction;
}
}

View File

@@ -0,0 +1,37 @@
import type { UiKitCoreAppViewClosedPayload } from '@rocket.chat/core-services';
import type * as UiKit from '@rocket.chat/ui-kit';
import { CloudAnnouncementsModule } from './cloudAnnouncements.module';
export class CloudSubscriptionCommunication extends CloudAnnouncementsModule {
override appId = 'cloud-communication-core';
override async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise<UiKit.ServerInteraction> {
const {
payload: { view: { viewId } = {} },
user: { _id: userId } = {},
} = payload;
if (!userId) {
throw new Error('invalid user');
}
if (!viewId) {
throw new Error('invalid view');
}
if (!payload.triggerId) {
throw new Error('invalid triggerId');
}
// for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response
void this.handlePayload(payload);
return {
type: 'modal.close',
triggerId: payload.triggerId,
appId: payload.appId,
};
}
}

View File

@@ -0,0 +1,118 @@
import { api } from '@rocket.chat/core-services';
import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload } from '@rocket.chat/core-services';
import type { IMessage, IUser } from '@rocket.chat/core-typings';
import { Subscriptions, Messages } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { i18n } from '../../lib/i18n';
import { processWebhookMessage } from '../../lib/messages/processWebhookMessage';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { addUsersToRoomMethod } from '../../meteor-methods/rooms/addUsersToRoom';
const retrieveMentionsFromPayload = (stringifiedMentions: string): Exclude<IMessage['mentions'], undefined> => {
try {
const mentions = JSON.parse(stringifiedMentions);
if (!Array.isArray(mentions) || !mentions.length || !('username' in mentions[0])) {
throw new Error('Invalid payload');
}
return mentions;
} catch (error) {
throw new Error('Invalid payload');
}
};
export class MentionModule implements IUiKitCoreApp {
appId = 'mention-core';
async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise<undefined> {
const {
actionId,
payload: { value: stringifiedMentions, blockId: referenceMessageId },
} = payload;
const user = payload.user!;
const room = payload.room!;
const mentions = retrieveMentionsFromPayload(stringifiedMentions as string);
const usernames = mentions.map(({ username }) => username);
const message = await Messages.findOneById(referenceMessageId, { projection: { _id: 1, tmid: 1 } });
if (!message) {
throw new Error('Mention bot - Failed to retrieve message information');
}
const joinedUsernames = `@${usernames.join(', @')}`;
if (actionId === 'dismiss') {
void api.broadcast('notify.ephemeralMessage', user._id, room, {
msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', {
mentions: joinedUsernames,
lng: user.language,
}),
_id: payload.message,
tmid: message.tmid,
mentions,
});
return undefined;
}
if (actionId === 'add-users') {
void addUsersToRoomMethod(user._id, { rid: room, users: usernames as string[] }, user);
void api.broadcast('notify.ephemeralMessage', user._id, room, {
msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', {
mentions: joinedUsernames,
lng: user.language,
}),
tmid: message.tmid,
_id: payload.message,
mentions,
});
return undefined;
}
if (actionId === 'share-message') {
const sub = await Subscriptions.findOneByRoomIdAndUserId(room, user._id, { projection: { t: 1, rid: 1, name: 1 } });
// this should exist since the event is fired from withing the room (e.g the user sent a message)
if (!sub) {
throw new Error('Mention bot - Failed to retrieve room information');
}
const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid, name: sub.name });
if (!roomPath) {
throw new Error('Mention bot - Failed to retrieve path to room');
}
const messageText = i18n.t('Youre_not_a_part_of__channel__and_I_mentioned_you_there', {
channel: `#${sub.name}`,
lng: user.language,
});
const link = new URL(Meteor.absoluteUrl(roomPath));
link.searchParams.set('msg', message._id);
const text = `[ ](${link.toString()})\n${messageText}`;
// forwards message to all DMs
await processWebhookMessage(
{
roomId: mentions.map(({ _id }) => _id),
text,
separateResponse: true, // so that messages are sent to other DMs even if one or more fails
},
user as IUser & { username: string },
);
void api.broadcast('notify.ephemeralMessage', user._id, room, {
msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', {
mentions: joinedUsernames,
lng: user.language,
}),
tmid: message.tmid,
_id: payload.message,
mentions,
});
return undefined;
}
}
}

View File

@@ -0,0 +1,70 @@
import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload, UiKitCoreAppViewSubmitPayload } from '@rocket.chat/core-services';
import { Banner, NPS } from '@rocket.chat/core-services';
import type * as UiKit from '@rocket.chat/ui-kit';
import { createModal } from './nps/createModal';
export class Nps implements IUiKitCoreApp {
appId = 'nps-core';
async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise<UiKit.ServerInteraction> {
const {
triggerId,
actionId,
container: { id: viewId } = {},
payload: { value: score, blockId: npsId },
user,
} = payload;
if (!viewId || !triggerId || !user || !npsId) {
throw new Error('Invalid payload');
}
const bannerId = viewId.replace(`${npsId}-`, '');
return createModal({
type: actionId === 'nps-score' ? 'modal.update' : 'modal.open',
id: `${npsId}-${bannerId}`,
appId: this.appId,
npsId,
triggerId,
score: String(score),
user,
});
}
async viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise<undefined> {
if (!payload.payload?.view?.state || !payload.payload?.view?.id) {
throw new Error('Invalid payload');
}
const {
payload: {
view: { state, id: viewId },
},
user: { _id: userId, roles } = {},
} = payload;
const [npsId] = Object.keys(state);
const bannerId = viewId.replace(`${npsId}-`, '');
const {
[npsId]: { 'nps-score': score, comment },
} = state;
await NPS.vote({
npsId,
userId,
comment: String(comment),
roles,
score: Number(score),
});
if (!userId) {
throw new Error('invalid user');
}
await Banner.dismiss(userId, bannerId);
}
}

View File

@@ -0,0 +1,89 @@
import type { IUser } from '@rocket.chat/core-typings';
import { i18n } from '../../../lib/i18n';
import { settings } from '../../../settings';
type ModalParams = {
id: string;
type: string;
appId: string;
npsId: string;
triggerId: string;
score: string;
user: IUser;
};
export const createModal = ({ type = 'modal.open', id, appId, npsId, triggerId, score, user }: ModalParams): any => {
const language = user.language || settings.get('Language') || 'en';
return {
type,
triggerId,
appId,
view: {
appId,
type: 'modal',
id,
title: {
type: 'plain_text',
text: i18n.t('We_appreciate_your_feedback', { lng: language }),
emoji: false,
},
submit: {
type: 'button',
text: {
type: 'plain_text',
text: i18n.t('Send', { lng: language }),
emoji: false,
},
actionId: 'send-vote',
},
close: {
type: 'button',
text: {
type: 'plain_text',
text: i18n.t('Cancel', { lng: language }),
emoji: false,
},
actionId: 'cancel',
},
blocks: [
{
blockId: npsId,
type: 'actions',
elements: [
{
type: 'linear_scale',
initialValue: score,
actionId: 'nps-score',
preLabel: { type: 'plain_text', text: i18n.t('Not_likely', { lng: language }) },
postLabel: {
type: 'plain_text',
text: i18n.t('Extremely_likely', { lng: language }),
},
},
],
label: {
type: 'plain_text',
text: i18n.t('Score', { lng: language }),
emoji: false,
},
},
{
blockId: npsId,
type: 'input',
element: {
type: 'plain_text_input',
multiline: true,
actionId: 'comment',
},
label: {
type: 'plain_text',
text: i18n.t('Why_did_you_chose__score__', { score, lng: language }),
emoji: false,
},
},
],
},
};
};

View File

@@ -0,0 +1,57 @@
import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload } from '@rocket.chat/core-services';
import { VideoConf } from '@rocket.chat/core-services';
import type * as UiKit from '@rocket.chat/ui-kit';
import { i18n } from '../../lib/i18n';
export class VideoConfModule implements IUiKitCoreApp {
appId = 'videoconf-core';
async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise<UiKit.ServerInteraction | undefined> {
const {
triggerId,
actionId,
payload: { blockId: callId },
user: { _id: userId } = {},
} = payload;
if (!callId) {
throw new Error('invalid call');
}
if (actionId === 'join') {
await VideoConf.join(userId, callId, {});
}
if (actionId === 'info') {
const blocks = await VideoConf.getInfo(callId, userId);
return {
type: 'modal.open',
triggerId,
appId: this.appId,
view: {
appId: this.appId,
id: `${callId}-info`,
title: {
type: 'plain_text',
text: i18n.t('Video_Conference_Info'),
emoji: false,
},
close: {
type: 'button',
appId: this.appId,
blockId: callId,
text: {
type: 'plain_text',
text: i18n.t('Close'),
emoji: false,
},
actionId: 'cancel',
},
blocks,
},
};
}
}
}