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
188 lines
6.0 KiB
TypeScript
188 lines
6.0 KiB
TypeScript
import { api } from '@rocket.chat/core-services';
|
|
import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings';
|
|
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
|
import type { RocketchatI18nKeys } from '@rocket.chat/i18n';
|
|
import { MessageTypes } from '@rocket.chat/message-types';
|
|
import { Messages, Users } from '@rocket.chat/models';
|
|
import type { TOptions } from 'i18next';
|
|
import { check, Match } from 'meteor/check';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import moment from 'moment';
|
|
|
|
import { RateLimiterClass as RateLimiter } from '../../lib/RateLimiter';
|
|
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
|
|
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
|
import { applyAirGappedRestrictionsValidation } from '../../lib/cloud/license/airGappedRestrictionsWrapper';
|
|
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
|
import { i18n } from '../../lib/i18n';
|
|
import { SystemLogger } from '../../lib/logger/system';
|
|
import { sendMessage } from '../../lib/messages/sendMessage';
|
|
import { metrics } from '../../lib/metrics';
|
|
import { settings } from '../../settings';
|
|
/**
|
|
*
|
|
* @param uid
|
|
* @param message
|
|
* @param extraInfo
|
|
* - ts: The timestamp of the message. the message object already has a ts, but this value is validated and only a window of 10 seconds is allowed to be used. this value overrides the message.ts value without validation.
|
|
*
|
|
*
|
|
* @returns
|
|
*/
|
|
export async function executeSendMessage(
|
|
uid: IUser['_id'] | IUser,
|
|
message: AtLeast<IMessage, 'rid'>,
|
|
extraInfo?: { ts?: Date; previewUrls?: string[] },
|
|
) {
|
|
if (message.tshow && !message.tmid) {
|
|
throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
|
|
if (message.tmid && !settings.get('Threads_enabled')) {
|
|
throw new Meteor.Error('error-not-allowed', 'not-allowed', {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
|
|
const isTimestampFromClient = Boolean(!extraInfo?.ts && message.ts);
|
|
const now = new Date();
|
|
message.ts = extraInfo?.ts ?? message.ts ?? now;
|
|
if (isTimestampFromClient) {
|
|
const tsDiff = Math.abs(moment(message.ts).diff(Date.now()));
|
|
if (tsDiff > 60000) {
|
|
throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', {
|
|
method: 'sendMessage',
|
|
message_ts: message.ts,
|
|
server_ts: new Date().getTime(),
|
|
});
|
|
}
|
|
if (tsDiff > 10000) {
|
|
message.ts = now;
|
|
}
|
|
}
|
|
|
|
if (message.msg) {
|
|
if (message.msg.length > (settings.get<number>('Message_MaxAllowedSize') ?? 0)) {
|
|
throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
}
|
|
|
|
const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid;
|
|
if (!user?.username) {
|
|
throw new Meteor.Error('error-invalid-user', 'Invalid user');
|
|
}
|
|
|
|
let { rid } = message;
|
|
|
|
// do not allow nested threads
|
|
if (message.tmid) {
|
|
const parentMessage = await Messages.findOneById(message.tmid, { projection: { rid: 1, tmid: 1 } });
|
|
message.tmid = parentMessage?.tmid || message.tmid;
|
|
|
|
if (parentMessage?.rid) {
|
|
rid = parentMessage?.rid;
|
|
}
|
|
}
|
|
|
|
if (!rid) {
|
|
throw new Error("The 'rid' property on the message object is missing.");
|
|
}
|
|
|
|
check(rid, String);
|
|
|
|
try {
|
|
const room = await canSendMessageAsync(rid, user);
|
|
|
|
if (room.encrypted && settings.get<boolean>('E2E_Enable') && !settings.get<boolean>('E2E_Allow_Unencrypted_Messages')) {
|
|
if (message.t !== 'e2e') {
|
|
throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
}
|
|
|
|
const result = await sendMessage(user, message, room, { previewUrls: extraInfo?.previewUrls });
|
|
|
|
metrics.messagesSent.inc();
|
|
metrics.messagesSentTotal.inc();
|
|
|
|
return result;
|
|
} catch (err: any) {
|
|
SystemLogger.error({ msg: 'Error sending message:', err });
|
|
|
|
const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message;
|
|
const errorContext: TOptions = err.details ?? {};
|
|
void api.broadcast('notify.ephemeralMessage', user._id, message.rid, {
|
|
msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }),
|
|
});
|
|
|
|
if (typeof err === 'string') {
|
|
throw new Error(err);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
declare module '@rocket.chat/ddp-client' {
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
interface ServerMethods {
|
|
sendMessage(message: AtLeast<IMessage, '_id' | 'rid' | 'msg'>, previewUrls?: string[]): any;
|
|
}
|
|
}
|
|
|
|
Meteor.methods<ServerMethods>({
|
|
async sendMessage(message, previewUrls) {
|
|
methodDeprecationLogger.method('sendMessage', '9.0.0', '/v1/chat.sendMessage');
|
|
|
|
check(message, {
|
|
_id: Match.Maybe(String),
|
|
rid: Match.Maybe(String),
|
|
msg: Match.Maybe(String),
|
|
tmid: Match.Maybe(String),
|
|
tshow: Match.Maybe(Boolean),
|
|
ts: Match.Maybe(Date),
|
|
t: Match.Maybe(String),
|
|
bot: Match.Maybe(Object),
|
|
content: Match.Maybe(Object),
|
|
e2e: Match.Maybe(String),
|
|
e2eMentions: Match.Maybe(Object),
|
|
customFields: Match.Maybe(Object),
|
|
federation: Match.Maybe(Object),
|
|
groupable: Match.Maybe(Boolean),
|
|
sentByEmail: Match.Maybe(Boolean),
|
|
});
|
|
|
|
const user = (await Meteor.userAsync()) as IUser;
|
|
if (!user) {
|
|
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
|
|
if (MessageTypes.isSystemMessage(message)) {
|
|
throw new Error("Cannot send system messages using 'sendMessage'");
|
|
}
|
|
|
|
try {
|
|
return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls }));
|
|
} catch (error: any) {
|
|
if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) {
|
|
throw new Meteor.Error(error.error || error.message, error.reason, {
|
|
method: 'sendMessage',
|
|
});
|
|
}
|
|
}
|
|
},
|
|
});
|
|
// Limit a user, who does not have the "bot" role, to sending 5 msgs/second
|
|
RateLimiter.limitMethod('sendMessage', 5, 1000, {
|
|
async userId(userId: IUser['_id']) {
|
|
return !(await hasPermissionAsync(userId, 'send-many-messages'));
|
|
},
|
|
});
|