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 2s
205 lines
6.8 KiB
TypeScript
205 lines
6.8 KiB
TypeScript
import type { IMessage, IUser, RequiredField, MessageAttachment, IRoom } from '@rocket.chat/core-typings';
|
|
import { removeEmpty } from '@rocket.chat/tools';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import _ from 'underscore';
|
|
|
|
import { sendMessage, validateMessage } from './sendMessage';
|
|
import { ensureArray } from '../../../lib/utils/arrayUtils';
|
|
import { trim } from '../../../lib/utils/stringUtils';
|
|
import { settings } from '../../settings';
|
|
import { validateRoomMessagePermissionsAsync } from '../authorization/canSendMessage';
|
|
import { SystemLogger } from '../logger/system';
|
|
import { getRoomByNameOrIdWithOptionToJoin } from '../rooms/getRoomByNameOrIdWithOptionToJoin';
|
|
|
|
type Payload = {
|
|
channel?: string | string[];
|
|
roomId?: string | string[];
|
|
text?: IMessage['msg'];
|
|
msg?: IMessage['msg']; // overridden if text is present
|
|
username?: IMessage['alias'];
|
|
alias?: IMessage['alias']; // overridden if username is present
|
|
icon_emoji?: IMessage['emoji'];
|
|
emoji?: IMessage['emoji']; // overridden if icon_emoji is present
|
|
icon_url?: IMessage['avatar'];
|
|
avatar?: IMessage['avatar']; // overridden if icon_url is present
|
|
attachments?: IMessage['attachments'];
|
|
parseUrls?: boolean;
|
|
bot?: IMessage['bot'];
|
|
groupable?: IMessage['groupable'];
|
|
tmid?: IMessage['tmid'];
|
|
customFields?: IMessage['customFields'];
|
|
};
|
|
|
|
type DefaultValues = {
|
|
channel: string | string[];
|
|
alias: string;
|
|
avatar: string;
|
|
emoji: string;
|
|
};
|
|
|
|
export type WebhookSuccessItem = { channel: string; error?: undefined; message: IMessage };
|
|
export type WebhookFailureItem = { channel: string; error: string; message?: undefined };
|
|
export type WebhookResponseItem = WebhookFailureItem | WebhookSuccessItem;
|
|
|
|
export const validateWebhookMessage = async (message: Partial<IMessage>, room: IRoom | null, user: IUser) => {
|
|
if (message.msg) {
|
|
if (message.msg.length > (settings.get<number>('Message_MaxAllowedSize') ?? 0)) {
|
|
throw Error('error-message-size-exceeded');
|
|
}
|
|
}
|
|
await validateMessage(message, room, user);
|
|
};
|
|
|
|
const getRoomWithOptionToJoin = async (channelType: string, channelValue: string, user: IUser) => {
|
|
switch (channelType) {
|
|
case '#':
|
|
return getRoomByNameOrIdWithOptionToJoin({
|
|
user,
|
|
nameOrId: channelValue,
|
|
joinChannel: true,
|
|
});
|
|
case '@':
|
|
return getRoomByNameOrIdWithOptionToJoin({
|
|
user,
|
|
nameOrId: channelValue,
|
|
type: 'd',
|
|
});
|
|
default:
|
|
const _channelValue = channelType + channelValue;
|
|
|
|
// Try to find the room by id or name if they didn't include the prefix.
|
|
let room = await getRoomByNameOrIdWithOptionToJoin({
|
|
user,
|
|
nameOrId: _channelValue,
|
|
joinChannel: true,
|
|
errorOnEmpty: false,
|
|
});
|
|
if (room) {
|
|
return room;
|
|
}
|
|
|
|
// We didn't get a room, let's try finding direct messages
|
|
room = await getRoomByNameOrIdWithOptionToJoin({
|
|
user,
|
|
nameOrId: _channelValue,
|
|
tryDirectByUserIdOnly: true,
|
|
type: 'd',
|
|
});
|
|
if (room) {
|
|
return room;
|
|
}
|
|
|
|
// No room, so throw an error
|
|
throw new Meteor.Error('invalid-channel');
|
|
}
|
|
};
|
|
|
|
const buildMessage = (messageObj: Payload, defaultValues: DefaultValues) => {
|
|
const message: Partial<IMessage> & { parseUrls?: boolean } = {
|
|
alias: messageObj.username || messageObj.alias || defaultValues.alias,
|
|
msg: trim(messageObj.text || messageObj.msg || ''),
|
|
attachments: messageObj.attachments || [],
|
|
parseUrls: messageObj.parseUrls !== undefined ? messageObj.parseUrls : !messageObj.attachments,
|
|
bot: messageObj.bot,
|
|
groupable: messageObj.groupable !== undefined ? messageObj.groupable : false,
|
|
tmid: messageObj.tmid,
|
|
customFields: messageObj.customFields,
|
|
};
|
|
|
|
if (!_.isEmpty(messageObj.icon_url) || !_.isEmpty(messageObj.avatar)) {
|
|
message.avatar = messageObj.icon_url || messageObj.avatar;
|
|
} else if (!_.isEmpty(messageObj.icon_emoji) || !_.isEmpty(messageObj.emoji)) {
|
|
message.emoji = messageObj.icon_emoji || messageObj.emoji;
|
|
} else if (!_.isEmpty(defaultValues.avatar)) {
|
|
message.avatar = defaultValues.avatar;
|
|
} else if (!_.isEmpty(defaultValues.emoji)) {
|
|
message.emoji = defaultValues.emoji;
|
|
}
|
|
|
|
if (Array.isArray(message.attachments)) {
|
|
for (let i = 0; i < message.attachments.length; i++) {
|
|
const attachment = message.attachments[i] as MessageAttachment & { msg?: string };
|
|
if (attachment.msg) {
|
|
attachment.text = trim(attachment.msg);
|
|
delete attachment.msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
return message;
|
|
};
|
|
|
|
export function processWebhookMessage(
|
|
messageObj: Payload & { separateResponse: true },
|
|
user: RequiredField<IUser, 'username'>,
|
|
defaultValues?: DefaultValues,
|
|
): Promise<WebhookResponseItem[]>;
|
|
|
|
export function processWebhookMessage(
|
|
messageObj: Payload & { separateResponse?: false | undefined },
|
|
user: RequiredField<IUser, 'username'>,
|
|
defaultValues?: DefaultValues,
|
|
): Promise<WebhookSuccessItem[]>;
|
|
|
|
export async function processWebhookMessage(
|
|
messageObj: Payload & {
|
|
/**
|
|
* If true, the response will be sent separately for each channel. Messages will be sent to other channels even if one or more fails. If false or not provided, messages would not be sent to any channel if one or more fails.
|
|
*/
|
|
separateResponse?: boolean;
|
|
},
|
|
user: RequiredField<IUser, 'username'>,
|
|
defaultValues: DefaultValues = { channel: '', alias: '', avatar: '', emoji: '' },
|
|
) {
|
|
const rooms: ({ channel: string } & ({ room: IRoom } | { room: IRoom | null; error?: any }))[] = [];
|
|
const sentData: WebhookResponseItem[] = [];
|
|
|
|
const channels: Array<string> = [...new Set(ensureArray(messageObj.channel || messageObj.roomId || defaultValues.channel))];
|
|
|
|
if (messageObj.attachments && !Array.isArray(messageObj.attachments)) {
|
|
SystemLogger.warn({
|
|
msg: 'Attachments should be Array, ignoring value',
|
|
attachments: messageObj.attachments,
|
|
});
|
|
messageObj.attachments = undefined;
|
|
}
|
|
|
|
const message = buildMessage(messageObj, defaultValues);
|
|
|
|
for (const channel of channels) {
|
|
const channelType = channel[0];
|
|
const channelValue = channel.slice(1);
|
|
let room: IRoom | null = null;
|
|
try {
|
|
room = await getRoomWithOptionToJoin(channelType, channelValue, user);
|
|
if (!room) {
|
|
throw new Error('error-invalid-room');
|
|
}
|
|
await validateRoomMessagePermissionsAsync(room, { uid: user._id, ...user });
|
|
await validateWebhookMessage(message, room, user);
|
|
rooms.push({ room, channel });
|
|
} catch (_error: any) {
|
|
if (messageObj.separateResponse) {
|
|
const { error, message } = _error || {};
|
|
const errorMessage = error || message || 'unknown-error';
|
|
rooms.push({ error: errorMessage, room, channel });
|
|
continue;
|
|
}
|
|
throw _error;
|
|
}
|
|
}
|
|
|
|
for (const roomData of rooms) {
|
|
if ('error' in roomData && roomData.error) {
|
|
if (messageObj.separateResponse) {
|
|
sentData.push({ channel: roomData.channel, error: roomData.error });
|
|
continue;
|
|
}
|
|
}
|
|
const messageReturn = await sendMessage(user, removeEmpty(message), roomData.room);
|
|
sentData.push({ channel: roomData.channel, message: messageReturn });
|
|
}
|
|
|
|
return sentData;
|
|
}
|