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
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
import {
|
|
type IMessage,
|
|
type ISubscription,
|
|
type IUser,
|
|
type IRoom,
|
|
type NotificationItem,
|
|
isEditedMessage,
|
|
type AtLeast,
|
|
} from '@rocket.chat/core-typings';
|
|
import { Subscriptions, Users } from '@rocket.chat/models';
|
|
import moment from 'moment';
|
|
import type { RootFilterOperators } from 'mongodb';
|
|
|
|
import { getMentions } from './notifyUsersOnMessage';
|
|
import { shortnameToUnicode } from '../../../app/emoji-native/lib/shortnameToUnicode';
|
|
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
|
import { callbacks } from '../../lib/callbacks';
|
|
import { parseMessageTextPerUser, replaceMentionedUsernamesWithFullNames } from '../../lib/notifications/message';
|
|
import { notifyDesktopUser, shouldNotifyDesktop } from '../../lib/notifications/message/desktop';
|
|
import { getEmailData, shouldNotifyEmail } from '../../lib/notifications/message/email';
|
|
import { messageContainsHighlight } from '../../lib/notifications/message/messageContainsHighlight';
|
|
import { getPushData, shouldNotifyMobile } from '../../lib/notifications/message/mobile';
|
|
import { Notification } from '../../lib/notifications/queue/NotificationQueue';
|
|
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
|
import { settings } from '../../settings';
|
|
|
|
type SubscriptionAggregation = {
|
|
receiver: [Pick<IUser, 'active' | 'emails' | 'language' | 'status' | 'statusConnection' | 'username' | 'settings'> | null];
|
|
} & Pick<
|
|
ISubscription,
|
|
| 'desktopNotifications'
|
|
| 'emailNotifications'
|
|
| 'mobilePushNotifications'
|
|
| 'muteGroupMentions'
|
|
| 'name'
|
|
| 'rid'
|
|
| 'userHighlights'
|
|
| 'u'
|
|
| 'audioNotificationValue'
|
|
>;
|
|
|
|
type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
|
|
[Property in Key]-?: Type[Property];
|
|
};
|
|
|
|
export const sendNotification = async ({
|
|
subscription,
|
|
sender,
|
|
hasReplyToThread,
|
|
hasMentionToAll,
|
|
hasMentionToHere,
|
|
message,
|
|
notificationMessage,
|
|
room,
|
|
mentionIds,
|
|
disableAllMessageNotifications,
|
|
}: {
|
|
subscription: SubscriptionAggregation;
|
|
sender: Pick<IUser, '_id' | 'name' | 'username'>;
|
|
|
|
hasReplyToThread: boolean;
|
|
hasMentionToAll: boolean;
|
|
hasMentionToHere: boolean;
|
|
message: AtLeast<IMessage, '_id' | 'u' | 'msg' | 't' | 'attachments'>;
|
|
notificationMessage: string;
|
|
room: IRoom;
|
|
mentionIds: string[];
|
|
disableAllMessageNotifications: boolean;
|
|
}) => {
|
|
if (settings.get<boolean>('Troubleshoot_Disable_Notifications') === true) {
|
|
return;
|
|
}
|
|
|
|
// don't notify the sender
|
|
if (subscription.u._id === sender._id) {
|
|
return;
|
|
}
|
|
|
|
const hasMentionToUser = mentionIds.includes(subscription.u._id);
|
|
|
|
// mute group notifications (@here and @all) if not directly mentioned as well
|
|
if (!hasMentionToUser && !hasReplyToThread && subscription.muteGroupMentions && (hasMentionToAll || hasMentionToHere)) {
|
|
return;
|
|
}
|
|
|
|
if (!subscription.receiver) {
|
|
subscription.receiver = [
|
|
await Users.findOneById(subscription.u._id, {
|
|
projection: {
|
|
'active': 1,
|
|
'emails': 1,
|
|
'language': 1,
|
|
'status': 1,
|
|
'statusConnection': 1,
|
|
'username': 1,
|
|
'settings.preferences.enableMobileRinging': 1,
|
|
},
|
|
}),
|
|
];
|
|
}
|
|
|
|
const [receiver] = subscription.receiver;
|
|
|
|
if (!receiver) {
|
|
throw new Error('receiver not found');
|
|
}
|
|
|
|
const roomType = room.t;
|
|
// If the user doesn't have permission to view direct messages, don't send notification of direct messages.
|
|
if (roomType === 'd' && !(await hasPermissionAsync(subscription.u._id, 'view-d-room'))) {
|
|
return;
|
|
}
|
|
|
|
const isThread = !!message.tmid && !message.tshow;
|
|
const isVideoConf = message.t === 'videoconf';
|
|
|
|
notificationMessage = await parseMessageTextPerUser(notificationMessage, message, receiver);
|
|
|
|
const isHighlighted = messageContainsHighlight(message, subscription.userHighlights);
|
|
|
|
const { desktopNotifications, mobilePushNotifications, emailNotifications } = subscription;
|
|
|
|
// busy users don't receive desktop notification
|
|
if (
|
|
shouldNotifyDesktop({
|
|
disableAllMessageNotifications,
|
|
status: receiver.status ?? 'offline',
|
|
statusConnection: receiver.statusConnection ?? 'offline',
|
|
desktopNotifications,
|
|
hasMentionToAll,
|
|
hasMentionToHere,
|
|
isHighlighted,
|
|
hasMentionToUser,
|
|
hasReplyToThread,
|
|
roomType,
|
|
isThread,
|
|
})
|
|
) {
|
|
await notifyDesktopUser({
|
|
notificationMessage,
|
|
userId: subscription.u._id,
|
|
user: sender,
|
|
message,
|
|
room,
|
|
audioNotificationValue: subscription.audioNotificationValue,
|
|
});
|
|
}
|
|
|
|
const queueItems: NotificationItem[] = [];
|
|
|
|
if (
|
|
shouldNotifyMobile({
|
|
disableAllMessageNotifications,
|
|
mobilePushNotifications,
|
|
hasMentionToAll,
|
|
isHighlighted,
|
|
hasMentionToUser,
|
|
hasReplyToThread,
|
|
roomType,
|
|
isThread,
|
|
isVideoConf,
|
|
userPreferences: receiver.settings?.preferences,
|
|
roomUids: room.uids,
|
|
})
|
|
) {
|
|
queueItems.push({
|
|
type: 'push',
|
|
data: await getPushData({
|
|
notificationMessage,
|
|
room,
|
|
message,
|
|
userId: subscription.u._id,
|
|
senderUsername: sender.username,
|
|
senderName: sender.name,
|
|
receiver,
|
|
}),
|
|
});
|
|
}
|
|
|
|
if (
|
|
receiver.emails &&
|
|
shouldNotifyEmail({
|
|
disableAllMessageNotifications,
|
|
statusConnection: receiver.statusConnection,
|
|
emailNotifications,
|
|
isHighlighted,
|
|
hasMentionToUser,
|
|
hasMentionToAll,
|
|
hasReplyToThread,
|
|
roomType,
|
|
isThread,
|
|
})
|
|
) {
|
|
const messageWithUnicode = message.msg ? shortnameToUnicode(message.msg) : message.msg;
|
|
const firstAttachment = message.attachments?.[0];
|
|
|
|
const convertedFirstAttachment = firstAttachment
|
|
? {
|
|
...firstAttachment,
|
|
description: typeof firstAttachment.description === 'string' ? shortnameToUnicode(firstAttachment.description) : undefined,
|
|
text: typeof firstAttachment.text === 'string' ? shortnameToUnicode(firstAttachment.text) : undefined,
|
|
}
|
|
: undefined;
|
|
|
|
const attachments = convertedFirstAttachment ? [convertedFirstAttachment, ...(message.attachments?.slice(1) ?? [])] : [];
|
|
for (const email of receiver.emails) {
|
|
if (email.verified) {
|
|
queueItems.push({
|
|
type: 'email',
|
|
data: await getEmailData({
|
|
message: {
|
|
...message,
|
|
msg: messageWithUnicode,
|
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
},
|
|
receiver,
|
|
sender,
|
|
subscription,
|
|
room,
|
|
emailAddress: email.address,
|
|
hasMentionToUser,
|
|
}),
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (queueItems.length) {
|
|
void Notification.scheduleItem({
|
|
user: receiver,
|
|
uid: subscription.u._id,
|
|
rid: room._id,
|
|
mid: message._id,
|
|
items: queueItems,
|
|
});
|
|
}
|
|
};
|
|
|
|
const project = {
|
|
$project: {
|
|
'desktopNotifications': 1,
|
|
'emailNotifications': 1,
|
|
'mobilePushNotifications': 1,
|
|
'muteGroupMentions': 1,
|
|
'name': 1,
|
|
'rid': 1,
|
|
'userHighlights': 1,
|
|
'u._id': 1,
|
|
'receiver.active': 1,
|
|
'receiver.emails': 1,
|
|
'receiver.language': 1,
|
|
'receiver.status': 1,
|
|
'receiver.statusConnection': 1,
|
|
'receiver.username': 1,
|
|
'receiver.settings.preferences.enableMobileRinging': 1,
|
|
'audioNotificationValue': 1,
|
|
},
|
|
} as const;
|
|
|
|
const filter = {
|
|
$match: {
|
|
'receiver.active': true,
|
|
},
|
|
} as const;
|
|
|
|
const lookup = {
|
|
$lookup: {
|
|
from: 'users',
|
|
localField: 'u._id',
|
|
foreignField: '_id',
|
|
as: 'receiver',
|
|
},
|
|
} as const;
|
|
|
|
export async function sendMessageNotifications(message: IMessage, room: IRoom, usersInThread: string[] = []) {
|
|
if (settings.get<boolean>('Troubleshoot_Disable_Notifications') === true) {
|
|
return;
|
|
}
|
|
|
|
const sender = await roomCoordinator.getRoomDirectives(room.t).getMsgSender(message);
|
|
if (!sender) {
|
|
return message;
|
|
}
|
|
|
|
const { toAll: hasMentionToAll, toHere: hasMentionToHere, mentionIds } = await getMentions(message);
|
|
|
|
const mentionIdsWithoutGroups = [...mentionIds];
|
|
|
|
// getMentions removes `all` and `here` from mentionIds so we need to add them back for compatibility
|
|
if (hasMentionToAll) {
|
|
mentionIds.push('all');
|
|
}
|
|
if (hasMentionToHere) {
|
|
mentionIds.push('here');
|
|
}
|
|
|
|
// add users in thread to mentions array because they follow the same rules
|
|
mentionIds.push(...usersInThread);
|
|
|
|
let notificationMessage = await callbacks.run('beforeSendMessageNotifications', message.msg);
|
|
if (mentionIds.length > 0 && settings.get('UI_Use_Real_Name')) {
|
|
notificationMessage = replaceMentionedUsernamesWithFullNames(message.msg, message.mentions ?? []);
|
|
}
|
|
|
|
// Don't fetch all users if room exceeds max members
|
|
const maxMembersForNotification = settings.get<number>('Notifications_Max_Room_Members');
|
|
const roomMembersCount = await Users.countRoomMembers(room._id);
|
|
const disableAllMessageNotifications = roomMembersCount > maxMembersForNotification && maxMembersForNotification !== 0;
|
|
|
|
const query: WithRequiredProperty<RootFilterOperators<ISubscription>, '$or'> = {
|
|
rid: room._id,
|
|
ignored: { $ne: sender._id },
|
|
disableNotifications: { $ne: true },
|
|
$or: [{ 'userHighlights.0': { $exists: 1 } }, ...(usersInThread.length > 0 ? [{ 'u._id': { $in: usersInThread } }] : [])],
|
|
} as const;
|
|
|
|
(['desktop', 'mobile', 'email'] as const).forEach((kind) => {
|
|
const notificationField = kind === 'mobile' ? 'mobilePush' : `${kind}Notifications`;
|
|
|
|
query.$or.push({
|
|
[notificationField]: 'all',
|
|
...(disableAllMessageNotifications ? { [`${kind}PrefOrigin`]: { $ne: 'user' } } : {}),
|
|
});
|
|
|
|
if (disableAllMessageNotifications) {
|
|
return;
|
|
}
|
|
|
|
if (room.t === 'd') {
|
|
query.$or.push({
|
|
[notificationField]: 'mentions',
|
|
});
|
|
} else if (mentionIdsWithoutGroups.length > 0) {
|
|
query.$or.push({
|
|
[notificationField]: 'mentions',
|
|
'u._id': { $in: mentionIdsWithoutGroups },
|
|
});
|
|
}
|
|
|
|
const serverField = kind === 'email' ? 'emailNotificationMode' : `${kind}Notifications`;
|
|
const serverPreference = settings.get(`Accounts_Default_User_Preferences_${serverField}`);
|
|
|
|
if (serverPreference === 'all' || hasMentionToAll || hasMentionToHere || room.t === 'd') {
|
|
query.$or.push({
|
|
[notificationField]: { $exists: false },
|
|
});
|
|
} else if (serverPreference === 'mentions' && mentionIdsWithoutGroups.length > 0) {
|
|
query.$or.push({
|
|
[notificationField]: { $exists: false },
|
|
'u._id': { $in: mentionIdsWithoutGroups },
|
|
});
|
|
}
|
|
});
|
|
|
|
// the find below is crucial. All subscription records returned will receive at least one kind of notification.
|
|
// the query is defined by the server's default values and Notifications_Max_Room_Members setting.
|
|
|
|
const subscriptions = await Subscriptions.col.aggregate<SubscriptionAggregation>([{ $match: query }, lookup, filter, project]).toArray();
|
|
|
|
subscriptions.forEach(
|
|
(subscription) =>
|
|
void sendNotification({
|
|
subscription,
|
|
sender,
|
|
hasMentionToAll,
|
|
hasMentionToHere,
|
|
message,
|
|
notificationMessage,
|
|
room,
|
|
mentionIds,
|
|
disableAllMessageNotifications,
|
|
hasReplyToThread: usersInThread?.includes(subscription.u._id),
|
|
}),
|
|
);
|
|
|
|
return {
|
|
sender,
|
|
hasMentionToAll,
|
|
hasMentionToHere,
|
|
notificationMessage,
|
|
mentionIds,
|
|
mentionIdsWithoutGroups,
|
|
};
|
|
}
|
|
|
|
export async function sendAllNotifications(message: IMessage, room: IRoom) {
|
|
if (settings.get<boolean>('Troubleshoot_Disable_Notifications') === true) {
|
|
return message;
|
|
}
|
|
|
|
// threads
|
|
if (message.tmid) {
|
|
return message;
|
|
}
|
|
// skips this callback if the message was edited
|
|
if (isEditedMessage(message)) {
|
|
return message;
|
|
}
|
|
|
|
if (message.ts && Math.abs(moment(message.ts).diff(new Date())) > 60000) {
|
|
return message;
|
|
}
|
|
|
|
if (room?.t == null) {
|
|
return message;
|
|
}
|
|
|
|
await sendMessageNotifications(message, room);
|
|
|
|
return message;
|
|
}
|
|
|
|
settings.watch('Troubleshoot_Disable_Notifications', (value) => {
|
|
if (value) {
|
|
return callbacks.remove('afterSaveMessage', 'sendNotificationsOnMessage');
|
|
}
|
|
|
|
callbacks.add(
|
|
'afterSaveMessage',
|
|
(message, { room, options }) => {
|
|
if (options?.skipNotifications) {
|
|
return message;
|
|
}
|
|
|
|
return sendAllNotifications(message, room);
|
|
},
|
|
callbacks.priority.LOW,
|
|
'sendNotificationsOnMessage',
|
|
);
|
|
});
|