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:
125
apps/meteor/server/lib/notifications/message/desktop.ts
Normal file
125
apps/meteor/server/lib/notifications/message/desktop.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { IMessage, IRoom, IUser, AtLeast } from '@rocket.chat/core-typings';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { metrics } from '../../metrics';
|
||||
import { roomCoordinator } from '../../rooms/roomCoordinator';
|
||||
|
||||
/**
|
||||
* Send notification to user
|
||||
*
|
||||
* @param {string} userId The user to notify
|
||||
* @param {object} user The sender
|
||||
* @param {object} room The room send from
|
||||
* @param {object} message The message object
|
||||
* @param {number} duration Duration of notification
|
||||
* @param {string} notificationMessage The message text to send on notification body
|
||||
*/
|
||||
export async function notifyDesktopUser({
|
||||
userId,
|
||||
user,
|
||||
message,
|
||||
room,
|
||||
duration,
|
||||
notificationMessage,
|
||||
audioNotificationValue,
|
||||
}: {
|
||||
userId: string;
|
||||
user: AtLeast<IUser, '_id' | 'name' | 'username'>;
|
||||
message: IMessage | Pick<IMessage, 'u'>;
|
||||
room: IRoom;
|
||||
duration?: number;
|
||||
notificationMessage: string;
|
||||
audioNotificationValue?: string;
|
||||
}): Promise<void> {
|
||||
const { title, text, name } = await roomCoordinator
|
||||
.getRoomDirectives(room.t)
|
||||
.getNotificationDetails(room, user, notificationMessage, userId);
|
||||
|
||||
const payload = {
|
||||
title: title || '',
|
||||
text,
|
||||
duration,
|
||||
payload: {
|
||||
_id: '',
|
||||
rid: '',
|
||||
tmid: '',
|
||||
...('_id' in message && {
|
||||
// TODO: omnichannel is not sending _id, rid, tmid
|
||||
_id: message._id,
|
||||
rid: message.rid,
|
||||
tmid: message.tmid,
|
||||
}),
|
||||
sender: message.u,
|
||||
type: room.t,
|
||||
message: {
|
||||
msg: 'msg' in message ? message.msg : '',
|
||||
...('t' in message && {
|
||||
t: message.t,
|
||||
}),
|
||||
...('content' in message && {
|
||||
content: message.content,
|
||||
}),
|
||||
},
|
||||
name,
|
||||
audioNotificationValue,
|
||||
},
|
||||
};
|
||||
|
||||
metrics.notificationsSent.inc({ notification_type: 'desktop' });
|
||||
metrics.notificationsSentTotal.inc({ notification_type: 'desktop' });
|
||||
|
||||
void api.broadcast('notify.desktop', userId, payload);
|
||||
}
|
||||
|
||||
export function shouldNotifyDesktop({
|
||||
disableAllMessageNotifications,
|
||||
status,
|
||||
statusConnection,
|
||||
desktopNotifications,
|
||||
hasMentionToAll,
|
||||
hasMentionToHere,
|
||||
isHighlighted,
|
||||
hasMentionToUser,
|
||||
hasReplyToThread,
|
||||
roomType,
|
||||
isThread,
|
||||
}: {
|
||||
disableAllMessageNotifications: boolean;
|
||||
status: string;
|
||||
statusConnection: string;
|
||||
desktopNotifications: string | undefined;
|
||||
hasMentionToAll: boolean;
|
||||
hasMentionToHere: boolean;
|
||||
isHighlighted: boolean;
|
||||
hasMentionToUser: boolean;
|
||||
hasReplyToThread: boolean;
|
||||
roomType: string;
|
||||
isThread: boolean;
|
||||
}): boolean {
|
||||
if (disableAllMessageNotifications && !desktopNotifications && !isHighlighted && !hasMentionToUser && !hasReplyToThread) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (statusConnection === 'offline' || status === 'busy' || desktopNotifications === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!desktopNotifications) {
|
||||
if (settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'all' && (!isThread || hasReplyToThread)) {
|
||||
return true;
|
||||
}
|
||||
if (settings.get('Accounts_Default_User_Preferences_desktopNotifications') === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(roomType === 'd' ||
|
||||
(!disableAllMessageNotifications && (hasMentionToAll || hasMentionToHere)) ||
|
||||
isHighlighted ||
|
||||
desktopNotifications === 'all' ||
|
||||
hasMentionToUser) &&
|
||||
(isHighlighted || !isThread || hasReplyToThread)
|
||||
);
|
||||
}
|
||||
242
apps/meteor/server/lib/notifications/message/email.js
Normal file
242
apps/meteor/server/lib/notifications/message/email.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import { escapeHTML } from '@rocket.chat/tools';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { ltrim } from '../../../../lib/utils/stringUtils';
|
||||
import { settings } from '../../../settings';
|
||||
import { callbacks } from '../../callbacks';
|
||||
import { i18n } from '../../i18n';
|
||||
import { metrics } from '../../metrics';
|
||||
import { roomCoordinator } from '../../rooms/roomCoordinator';
|
||||
import { getURL } from '../../utils/getURL';
|
||||
import * as Mailer from '../email/api';
|
||||
|
||||
let advice = '';
|
||||
let goToMessage = '';
|
||||
Meteor.startup(() => {
|
||||
settings.watch('email_style', () => {
|
||||
goToMessage = Mailer.inlinecss('<p><a class=\'btn\' href="[room_path]">{Offline_Link_Message}</a></p>');
|
||||
});
|
||||
Mailer.getTemplate('Email_Footer_Direct_Reply', (value) => {
|
||||
advice = value;
|
||||
});
|
||||
});
|
||||
|
||||
export async function getEmailContent({ message, user, room }) {
|
||||
const lng = (user && user.language) || settings.get('Language') || 'en';
|
||||
|
||||
const roomName = escapeHTML(`#${await roomCoordinator.getRoomName(room.t, room)}`);
|
||||
const userName = escapeHTML(settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username);
|
||||
|
||||
const roomDirectives = roomCoordinator.getRoomDirectives(room.t);
|
||||
|
||||
const files = (message.files || [message.file]).filter((file) => file && file.typeGroup !== 'thumb');
|
||||
const hasFiles = files.length > 0;
|
||||
const hasText = typeof message.msg === 'string' && message.msg.trim() !== '';
|
||||
const isGroupChat = roomDirectives.isGroupChat(room);
|
||||
|
||||
let header;
|
||||
if (hasFiles && !hasText) {
|
||||
// file-only message
|
||||
const isMultipleFiles = files.length > 1;
|
||||
let headerKey;
|
||||
if (isGroupChat) {
|
||||
headerKey = isMultipleFiles ? 'User_uploaded_files_on_channel' : 'User_uploaded_a_file_on_channel';
|
||||
} else {
|
||||
headerKey = isMultipleFiles ? 'User_uploaded_files_to_you' : 'User_uploaded_a_file_to_you';
|
||||
}
|
||||
header = i18n.t(headerKey, { username: userName, channel: roomName, count: files.length, lng });
|
||||
} else {
|
||||
// text message (with or without files)
|
||||
header = i18n.t(!isGroupChat ? 'User_sent_a_message_to_you' : 'User_sent_a_message_on_channel', {
|
||||
username: userName,
|
||||
channel: roomName,
|
||||
lng,
|
||||
});
|
||||
}
|
||||
|
||||
if (message.t === 'e2e') {
|
||||
return settings.get('Email_notification_show_message') ? i18n.t('Encrypted_message_preview_unavailable', { lng }) : header;
|
||||
}
|
||||
|
||||
if (!settings.get('Email_notification_show_message')) {
|
||||
return header;
|
||||
}
|
||||
|
||||
const contentParts = [];
|
||||
|
||||
if (hasText) {
|
||||
let messageContent = escapeHTML(message.msg);
|
||||
message = await callbacks.run('renderMessage', message);
|
||||
if (message.tokens && message.tokens.length > 0) {
|
||||
message.tokens.forEach((token) => {
|
||||
token.text = token.text.replace(/([^\$])(\$[^\$])/gm, '$1$$$2');
|
||||
messageContent = messageContent.replace(token.token, token.text);
|
||||
});
|
||||
}
|
||||
contentParts.push(messageContent.replace(/\n/gm, '<br/>'));
|
||||
}
|
||||
|
||||
if (hasFiles) {
|
||||
const attachments = message.attachments || [];
|
||||
const fileParts = files.map((file, index) => {
|
||||
let part = escapeHTML(file.name);
|
||||
if (attachments[index]?.description) {
|
||||
part += `<br/><br/>${escapeHTML(attachments[index].description)}`;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
contentParts.push(fileParts.join('<br/><br/>'));
|
||||
}
|
||||
|
||||
if (contentParts.length > 0) {
|
||||
return `${header}:<br/><br/>${contentParts.join('<br/><br/>')}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(message.attachments) && message.attachments.length > 0) {
|
||||
const [attachment] = message.attachments;
|
||||
|
||||
let content = '';
|
||||
|
||||
if (attachment.title) {
|
||||
content += `${escapeHTML(attachment.title)}<br/>`;
|
||||
}
|
||||
if (attachment.text) {
|
||||
content += `${escapeHTML(attachment.text)}<br/>`;
|
||||
}
|
||||
|
||||
return `${header}:<br/><br/>${content}`;
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
const getButtonUrl = (room, subscription, message) => {
|
||||
const basePath = roomCoordinator.getRouteLink(room.t, subscription).replace(Meteor.absoluteUrl(), '');
|
||||
|
||||
const path = `${ltrim(basePath, '/')}?msg=${message._id}`;
|
||||
return getURL(
|
||||
path,
|
||||
{
|
||||
full: true,
|
||||
cloud: settings.get('Offline_Message_Use_DeepLink'),
|
||||
cloud_route: 'room',
|
||||
cloud_params: {
|
||||
rid: room._id,
|
||||
mid: message._id,
|
||||
},
|
||||
},
|
||||
settings.get('DeepLink_Url'),
|
||||
);
|
||||
};
|
||||
|
||||
function generateNameEmail(name, email) {
|
||||
return `${String(name).replace(/@/g, '%40').replace(/[<>,]/g, '')} <${email}>`;
|
||||
}
|
||||
|
||||
export async function getEmailData({ message, receiver, sender, subscription, room, emailAddress, hasMentionToUser }) {
|
||||
const username = settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username;
|
||||
let subjectKey = 'Offline_Mention_All_Email';
|
||||
|
||||
if (!roomCoordinator.getRoomDirectives(room.t).isGroupChat(room)) {
|
||||
subjectKey = 'Offline_DM_Email';
|
||||
} else if (hasMentionToUser) {
|
||||
subjectKey = 'Offline_Mention_Email';
|
||||
}
|
||||
|
||||
const emailSubject = Mailer.replace(settings.get(subjectKey), {
|
||||
user: username,
|
||||
room: await roomCoordinator.getRoomName(room.t, room),
|
||||
});
|
||||
const content = await getEmailContent({
|
||||
message,
|
||||
user: receiver,
|
||||
room,
|
||||
});
|
||||
|
||||
const room_path = getButtonUrl(room, subscription, message);
|
||||
|
||||
const receiverName = settings.get('UI_Use_Real_Name') ? receiver.name || receiver.username : receiver.username;
|
||||
|
||||
const email = {
|
||||
from: generateNameEmail(username, settings.get('From_Email')),
|
||||
to: generateNameEmail(receiverName, emailAddress),
|
||||
subject: emailSubject,
|
||||
html: content + goToMessage + (settings.get('Direct_Reply_Enable') ? advice : ''),
|
||||
data: {
|
||||
room_path,
|
||||
},
|
||||
headers: {},
|
||||
};
|
||||
|
||||
if (sender.emails?.length > 0 && settings.get('Add_Sender_To_ReplyTo')) {
|
||||
const [senderEmail] = sender.emails;
|
||||
email.headers['Reply-To'] = generateNameEmail(username, senderEmail.address);
|
||||
}
|
||||
|
||||
// If direct reply enabled, email content with headers
|
||||
if (settings.get('Direct_Reply_Enable')) {
|
||||
const replyto = settings.get('Direct_Reply_ReplyTo') || settings.get('Direct_Reply_Username');
|
||||
|
||||
// Reply-To header with format "username+messageId@domain"
|
||||
email.headers['Reply-To'] = `${replyto.split('@')[0].split(settings.get('Direct_Reply_Separator'))[0]}${settings.get(
|
||||
'Direct_Reply_Separator',
|
||||
)}${message.tmid || message._id}@${replyto.split('@')[1]}`;
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
export function sendEmailFromData(data) {
|
||||
return Mailer.send(data).then(() => {
|
||||
metrics.notificationsSent.inc({ notification_type: 'email' });
|
||||
metrics.notificationsSentTotal.inc({ notification_type: 'email' });
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldNotifyEmail({
|
||||
disableAllMessageNotifications,
|
||||
statusConnection,
|
||||
emailNotifications,
|
||||
isHighlighted,
|
||||
hasMentionToUser,
|
||||
hasMentionToAll,
|
||||
hasReplyToThread,
|
||||
roomType,
|
||||
isThread,
|
||||
}) {
|
||||
// email notifications are disabled globally
|
||||
if (!settings.get('Accounts_AllowEmailNotifications')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// user/room preference to nothing
|
||||
if (emailNotifications === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// user connected (don't need to send him an email)
|
||||
if (statusConnection === 'online') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no user or room preference
|
||||
if (emailNotifications == null) {
|
||||
if (disableAllMessageNotifications && !isHighlighted && !hasMentionToUser && !hasReplyToThread) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// default server preference is disabled
|
||||
if (settings.get('Accounts_Default_User_Preferences_emailNotificationMode') === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(roomType === 'd' ||
|
||||
isHighlighted ||
|
||||
emailNotifications === 'all' ||
|
||||
hasMentionToUser ||
|
||||
(!disableAllMessageNotifications && hasMentionToAll)) &&
|
||||
(isHighlighted || !isThread || hasReplyToThread)
|
||||
);
|
||||
}
|
||||
49
apps/meteor/server/lib/notifications/message/index.ts
Normal file
49
apps/meteor/server/lib/notifications/message/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import { isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { callbacks } from '../../callbacks';
|
||||
import { i18n } from '../../i18n';
|
||||
|
||||
/**
|
||||
* This function returns a string ready to be shown in the notification
|
||||
*
|
||||
* @param {object} message the message to be parsed
|
||||
*/
|
||||
export async function parseMessageTextPerUser(
|
||||
messageText: string,
|
||||
message: Pick<IMessage, 'u' | 'msg' | 't' | 'attachments'>,
|
||||
receiver: Pick<IUser, 'language'>,
|
||||
): Promise<string> {
|
||||
const lng = receiver.language || settings.get('Language') || 'en';
|
||||
|
||||
const firstAttachment = message.attachments?.[0];
|
||||
if (!message.msg && firstAttachment && isFileAttachment(firstAttachment) && isFileImageAttachment(firstAttachment)) {
|
||||
return firstAttachment.image_type ? i18n.t('User_uploaded_image', { lng }) : i18n.t('User_uploaded_file', { lng });
|
||||
}
|
||||
|
||||
if (message.msg && message.t === 'e2e') {
|
||||
return i18n.t('Encrypted_message', { lng });
|
||||
}
|
||||
|
||||
// perform processing required before sending message as notification such as markdown filtering
|
||||
return callbacks.run('renderNotification', messageText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces @username with full name
|
||||
*
|
||||
* @param {string} message The message to replace
|
||||
* @param {object[]} mentions Array of mentions used to make replacements
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function replaceMentionedUsernamesWithFullNames(message: string, mentions: NonNullable<IMessage['mentions']>): string {
|
||||
mentions.forEach((mention) => {
|
||||
if (mention.name) {
|
||||
message = message.replace(new RegExp(escapeRegExp(`@${mention.username}`), 'g'), mention.name);
|
||||
}
|
||||
});
|
||||
return message;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
|
||||
/**
|
||||
* Checks if a message contains a user highlight
|
||||
*
|
||||
* @param {string} message
|
||||
* @param {array|undefined} highlights
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function messageContainsHighlight(message: Pick<IMessage, 'msg'>, highlights: string[] | undefined): boolean {
|
||||
if (!highlights || highlights.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leftBoundary = '(?<=^|[^\\p{L}\\p{N}_])';
|
||||
const rightBoundary = '(?=$|[^\\p{L}\\p{N}_])';
|
||||
|
||||
return highlights.some((highlight: string) => {
|
||||
// Due to unnecessary escaping in escapeRegExp, we need to remove the escape character for the following characters: - = ! :
|
||||
// This is necessary because it was crashing the client due to Invalid regular expression error.
|
||||
const hl = escapeRegExp(highlight).replace(/\\([-=!:])/g, '$1');
|
||||
const pattern =
|
||||
`(?<!:)${leftBoundary}${hl}${rightBoundary}:` +
|
||||
`|:${leftBoundary}${hl}${rightBoundary}(?!:)` +
|
||||
`|${leftBoundary}(?<!:)${hl}(?!:)${rightBoundary}`;
|
||||
const regexp = new RegExp(pattern, 'iu');
|
||||
return regexp.test(message.msg);
|
||||
});
|
||||
}
|
||||
120
apps/meteor/server/lib/notifications/message/mobile.js
Normal file
120
apps/meteor/server/lib/notifications/message/mobile.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Subscriptions } from '@rocket.chat/models';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { i18n } from '../../i18n';
|
||||
import { isRoomCompatibleWithVideoConfRinging } from '../../isRoomCompatibleWithVideoConfRinging';
|
||||
import { roomCoordinator } from '../../rooms/roomCoordinator';
|
||||
|
||||
const CATEGORY_MESSAGE = 'MESSAGE';
|
||||
const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY';
|
||||
|
||||
function enableNotificationReplyButton(room, username) {
|
||||
// Some users may have permission to send messages even on readonly rooms, but we're ok with false negatives here in exchange of better perfomance
|
||||
if (room.ro === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!room.muted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !room.muted.includes(username);
|
||||
}
|
||||
|
||||
export async function getPushData({
|
||||
room,
|
||||
message,
|
||||
userId,
|
||||
senderUsername,
|
||||
senderName,
|
||||
notificationMessage,
|
||||
receiver,
|
||||
shouldOmitMessage = true,
|
||||
}) {
|
||||
const username = settings.get('Push_show_username_room') ? (settings.get('UI_Use_Real_Name') && senderName) || senderUsername : '';
|
||||
|
||||
const lng = receiver.language || settings.get('Language') || 'en';
|
||||
|
||||
let messageText;
|
||||
if (shouldOmitMessage && settings.get('Push_request_content_from_server')) {
|
||||
messageText = i18n.t('You_have_a_new_message', { lng });
|
||||
} else if (!settings.get('Push_show_message')) {
|
||||
messageText = i18n.t('You_have_a_new_message', { lng });
|
||||
} else {
|
||||
messageText = notificationMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
payload: {
|
||||
sender: message.u,
|
||||
senderName: username,
|
||||
type: room.t,
|
||||
name: settings.get('Push_show_username_room') ? room.name : '',
|
||||
messageType: message.t,
|
||||
tmid: message.tmid,
|
||||
...(message.t === 'e2e' && { msg: message.msg, content: message.content }),
|
||||
},
|
||||
roomName:
|
||||
settings.get('Push_show_username_room') && roomCoordinator.getRoomDirectives(room.t).isGroupChat(room)
|
||||
? `#${await roomCoordinator.getRoomName(room.t, room, userId)}`
|
||||
: '',
|
||||
username,
|
||||
message: messageText,
|
||||
badge: await Subscriptions.getBadgeCount(userId),
|
||||
category: enableNotificationReplyButton(room, receiver.username) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY,
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldNotifyMobile({
|
||||
disableAllMessageNotifications,
|
||||
mobilePushNotifications,
|
||||
hasMentionToAll,
|
||||
isHighlighted,
|
||||
hasMentionToUser,
|
||||
hasReplyToThread,
|
||||
roomType,
|
||||
isThread,
|
||||
isVideoConf,
|
||||
userPreferences,
|
||||
roomUids,
|
||||
}) {
|
||||
if (settings.get('Push_enable') !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disableAllMessageNotifications && mobilePushNotifications == null && !isHighlighted && !hasMentionToUser && !hasReplyToThread) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mobilePushNotifications === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user is going to receive a ringing push notification, do not send another push for the message generated by that videoconference
|
||||
if (
|
||||
isVideoConf &&
|
||||
settings.get('VideoConf_Mobile_Ringing') &&
|
||||
isRoomCompatibleWithVideoConfRinging(roomType, roomUids) &&
|
||||
(userPreferences?.enableMobileRinging ?? settings.get(`Accounts_Default_User_Preferences_enableMobileRinging`))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mobilePushNotifications) {
|
||||
if (settings.get('Accounts_Default_User_Preferences_pushNotifications') === 'all' && (!isThread || hasReplyToThread)) {
|
||||
return true;
|
||||
}
|
||||
if (settings.get('Accounts_Default_User_Preferences_pushNotifications') === 'nothing') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(roomType === 'd' ||
|
||||
(!disableAllMessageNotifications && hasMentionToAll) ||
|
||||
isHighlighted ||
|
||||
mobilePushNotifications === 'all' ||
|
||||
hasMentionToUser) &&
|
||||
(isHighlighted || !isThread || hasReplyToThread)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user