diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 21ae3fecf81c..d63593af1486 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -41,9 +41,11 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele // Users without username can't do anything, so there is nothing to remove if (user.username != null) { + let userToReplaceWhenUnlinking: IUser | null = null; + const nameAlias = i18n.t('Removed_User'); await relinquishRoomOwnerships(userId, subscribedRooms); - const messageErasureType = settings.get('Message_ErasureType'); + const messageErasureType = settings.get<'Delete' | 'Unlink' | 'Keep'>('Message_ErasureType'); switch (messageErasureType) { case 'Delete': const store = FileUpload.getStore('Uploads'); @@ -68,12 +70,11 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele break; case 'Unlink': - const rocketCat = await Users.findOneById('rocket.cat'); - const nameAlias = i18n.t('Removed_User'); - if (!rocketCat?._id || !rocketCat?.username) { + userToReplaceWhenUnlinking = await Users.findOneById('rocket.cat'); + if (!userToReplaceWhenUnlinking?._id || !userToReplaceWhenUnlinking?.username) { break; } - await Messages.unlinkUserId(userId, rocketCat?._id, rocketCat?.username, nameAlias); + await Messages.unlinkUserId(userId, userToReplaceWhenUnlinking?._id, userToReplaceWhenUnlinking?.username, nameAlias); break; } @@ -104,8 +105,16 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. // Don't broadcast user.deleted for Erasure Type of 'Keep' so that messages don't disappear from logged in sessions - if (messageErasureType !== 'Keep') { - void api.broadcast('user.deleted', user); + if (messageErasureType === 'Delete') { + void api.broadcast('user.deleted', user, { + messageErasureType, + }); + } + if (messageErasureType === 'Unlink' && userToReplaceWhenUnlinking) { + void api.broadcast('user.deleted', user, { + messageErasureType, + replaceByUser: { _id: userToReplaceWhenUnlinking._id, username: userToReplaceWhenUnlinking?.username, alias: nameAlias }, + }); } } diff --git a/apps/meteor/client/startup/UserDeleted.ts b/apps/meteor/client/startup/UserDeleted.ts index 0e7b75bf4efa..bbaeb6bc0229 100644 --- a/apps/meteor/client/startup/UserDeleted.ts +++ b/apps/meteor/client/startup/UserDeleted.ts @@ -4,7 +4,23 @@ import { ChatMessage } from '../../app/models/client'; import { Notifications } from '../../app/notifications/client'; Meteor.startup(() => { - Notifications.onLogged('Users:Deleted', ({ userId }) => { + Notifications.onLogged('Users:Deleted', ({ userId, messageErasureType, replaceByUser }) => { + if (messageErasureType === 'Unlink' && replaceByUser) { + return ChatMessage.update( + { + 'u._id': userId, + }, + { + $set: { + 'alias': replaceByUser.alias, + 'u._id': replaceByUser._id, + 'u.username': replaceByUser.username, + 'u.name': undefined, + }, + }, + { multi: true }, + ); + } ChatMessage.remove({ 'u._id': userId, }); diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index c580b47d7c6e..df940006d26e 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -99,9 +99,10 @@ export class ListenersModule { }); }); - service.onEvent('user.deleted', ({ _id: userId }) => { + service.onEvent('user.deleted', ({ _id: userId }, data) => { notifications.notifyLoggedInThisInstance('Users:Deleted', { userId, + ...data, }); }); diff --git a/apps/meteor/server/modules/watchers/lib/messages.ts b/apps/meteor/server/modules/watchers/lib/messages.ts new file mode 100644 index 000000000000..ded1c2389e17 --- /dev/null +++ b/apps/meteor/server/modules/watchers/lib/messages.ts @@ -0,0 +1,52 @@ +import type { IMessage, SettingValue, IUser } from '@rocket.chat/core-typings'; +import { Messages, Settings, Users } from '@rocket.chat/models'; +import mem from 'mem'; + +const getSettingCached = mem(async (setting: string): Promise => Settings.getValueById(setting), { maxAge: 10000 }); + +const getUserNameCached = mem( + async (userId: string): Promise => { + const user = await Users.findOne>(userId, { projection: { name: 1 } }); + return user?.name; + }, + { maxAge: 10000 }, +); + +export const broadcastMessageSentEvent = async ({ + id, + data, + broadcastCallback, +}: { + id: IMessage['_id']; + broadcastCallback: (message: IMessage) => Promise; + data?: IMessage; +}): Promise => { + const message = data ?? (await Messages.findOneById(id)); + if (!message) { + return; + } + + if (message._hidden !== true && message.imported == null) { + const UseRealName = (await getSettingCached('UI_Use_Real_Name')) === true; + + if (UseRealName) { + if (message.u?._id) { + const name = await getUserNameCached(message.u._id); + if (name) { + message.u.name = name; + } + } + + if (message.mentions?.length) { + for await (const mention of message.mentions) { + const name = await getUserNameCached(mention._id); + if (name) { + mention.name = name; + } + } + } + } + + void broadcastCallback(message); + } +}; diff --git a/apps/meteor/server/modules/watchers/watchers.module.ts b/apps/meteor/server/modules/watchers/watchers.module.ts index 1d8402786a2f..5ad081348abb 100644 --- a/apps/meteor/server/modules/watchers/watchers.module.ts +++ b/apps/meteor/server/modules/watchers/watchers.module.ts @@ -1,4 +1,5 @@ import type { EventSignatures } from '@rocket.chat/core-services'; +import { dbWatchersDisabled } from '@rocket.chat/core-services'; import type { ISubscription, IUser, @@ -64,17 +65,17 @@ export function isWatcherRunning(): boolean { return watcherStarted; } -export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallback): void { - const getSettingCached = mem(async (setting: string): Promise => Settings.getValueById(setting), { maxAge: 10000 }); +const getSettingCached = mem(async (setting: string): Promise => Settings.getValueById(setting), { maxAge: 10000 }); - const getUserNameCached = mem( - async (userId: string): Promise => { - const user = await Users.findOne>(userId, { projection: { name: 1 } }); - return user?.name; - }, - { maxAge: 10000 }, - ); +const getUserNameCached = mem( + async (userId: string): Promise => { + const user = await Users.findOne>(userId, { projection: { name: 1 } }); + return user?.name; + }, + { maxAge: 10000 }, +); +const messageWatcher = (watcher: DatabaseWatcher, broadcast: BroadcastCallback): void => { watcher.on(Messages.getCollectionName(), async ({ clientAction, id, data }) => { switch (clientAction) { case 'inserted': @@ -110,6 +111,13 @@ export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallb break; } }); +}; + +export function initWatchers(watcher: DatabaseWatcher, broadcast: BroadcastCallback): void { + const dbWatchersEnabled = !dbWatchersDisabled; + if (dbWatchersEnabled) { + messageWatcher(watcher, broadcast); + } watcher.on(Subscriptions.getCollectionName(), async ({ clientAction, id, data, diff }) => { switch (clientAction) { diff --git a/ee/packages/ddp-client/src/types/streams.ts b/ee/packages/ddp-client/src/types/streams.ts index abadd53c1851..f1a54c674b9b 100644 --- a/ee/packages/ddp-client/src/types/streams.ts +++ b/ee/packages/ddp-client/src/types/streams.ts @@ -226,9 +226,16 @@ export interface StreamerEvents { { key: 'Users:Deleted'; args: [ - { - userId: IUser['_id']; - }, + | { + userId: IUser['_id']; + messageErasureType: 'Delete'; + replaceByUser?: never; + } + | { + userId: IUser['_id']; + messageErasureType: 'Unlink'; + replaceByUser?: { _id: IUser['_id']; username: IUser['username']; alias: string }; + }, ]; }, { diff --git a/packages/core-services/src/LocalBroker.ts b/packages/core-services/src/LocalBroker.ts index dc27e62a5acb..cdedd4fa7057 100644 --- a/packages/core-services/src/LocalBroker.ts +++ b/packages/core-services/src/LocalBroker.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'events'; import { InstanceStatus } from '@rocket.chat/models'; import { asyncLocalStorage } from '.'; -import type { EventSignatures } from './Events'; +import type { EventSignatures } from './events/Events'; import type { IBroker, IBrokerNode } from './types/IBroker'; import type { ServiceClass, IServiceClass } from './types/ServiceClass'; diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/events/Events.ts similarity index 96% rename from packages/core-services/src/Events.ts rename to packages/core-services/src/events/Events.ts index c5f36d921655..49e78ea7244a 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -36,7 +36,7 @@ import type { } from '@rocket.chat/core-typings'; import type { LicenseLimitKind } from '@rocket.chat/license'; -import type { AutoUpdateRecord } from './types/IMeteor'; +import type { AutoUpdateRecord } from '../types/IMeteor'; type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; @@ -100,7 +100,17 @@ export type EventSignatures = { 'stream'([streamer, eventName, payload]: [string, string, any[]]): void; 'subscription'(data: { action: string; subscription: Partial }): void; 'user.avatarUpdate'(user: Partial): void; - 'user.deleted'(user: Pick): void; + 'user.deleted'( + user: Pick, + data: + | { + messageErasureType: 'Delete'; + } + | { + messageErasureType: 'Unlink'; + replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string }; + }, + ): void; 'user.deleteCustomStatus'(userStatus: IUserStatus): void; 'user.nameChanged'(user: Pick): void; 'user.realNameChanged'(user: Partial): void; @@ -273,4 +283,5 @@ export type EventSignatures = { 'command.updated'(command: string): void; 'command.removed'(command: string): void; 'actions.changed'(): void; + 'message.sent'(message: IMessage): void; }; diff --git a/packages/core-services/src/events/listeners.ts b/packages/core-services/src/events/listeners.ts new file mode 100644 index 000000000000..ced986cb6f30 --- /dev/null +++ b/packages/core-services/src/events/listeners.ts @@ -0,0 +1,12 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import type { IServiceClass } from '../types/ServiceClass'; + +export const dbWatchersDisabled = ['yes', 'true'].includes(String(process.env.DISABLE_DB_WATCHERS).toLowerCase()); + +export const listenToMessageSentEvent = (service: IServiceClass, action: (message: IMessage) => Promise): void => { + if (dbWatchersDisabled) { + return service.onEvent('message.sent', (message: IMessage) => action(message)); + } + return service.onEvent('watch.messages', ({ message }) => action(message)); +}; diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index e2615e13d57d..46697b7e5c09 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -50,7 +50,8 @@ import type { IVoipService } from './types/IVoipService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; export { api } from './api'; -export { EventSignatures } from './Events'; +export { EventSignatures } from './events/Events'; +export { listenToMessageSentEvent, dbWatchersDisabled } from './events/listeners'; export { LocalBroker } from './LocalBroker'; export { IBroker, IBrokerNode, BaseMetricOptions, IServiceMetrics } from './types/IBroker'; diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index f0b5e67594c2..61a58301a0cc 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -1,4 +1,4 @@ -import type { EventSignatures } from '../Events'; +import type { EventSignatures } from '../events/Events'; import type { IApiService } from '../types/IApiService'; import type { IBroker, IBrokerNode } from '../types/IBroker'; import type { IServiceClass } from '../types/ServiceClass'; diff --git a/packages/core-services/src/types/IApiService.ts b/packages/core-services/src/types/IApiService.ts index 9361eb6fce9d..ef88d57713bc 100644 --- a/packages/core-services/src/types/IApiService.ts +++ b/packages/core-services/src/types/IApiService.ts @@ -1,4 +1,4 @@ -import type { EventSignatures } from '../Events'; +import type { EventSignatures } from '../events/Events'; import type { IBroker, IBrokerNode } from './IBroker'; import type { IServiceClass } from './ServiceClass'; diff --git a/packages/core-services/src/types/IBroker.ts b/packages/core-services/src/types/IBroker.ts index 4bd48afef0ff..cd1e0a3ded19 100644 --- a/packages/core-services/src/types/IBroker.ts +++ b/packages/core-services/src/types/IBroker.ts @@ -1,4 +1,4 @@ -import type { EventSignatures } from '../Events'; +import type { EventSignatures } from '../events/Events'; import type { IServiceClass } from './ServiceClass'; export interface IBrokerNode { diff --git a/packages/core-services/src/types/ServiceClass.ts b/packages/core-services/src/types/ServiceClass.ts index 47f23e757a1b..5e6b202a103d 100644 --- a/packages/core-services/src/types/ServiceClass.ts +++ b/packages/core-services/src/types/ServiceClass.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'events'; -import type { EventSignatures } from '../Events'; +import type { EventSignatures } from '../events/Events'; import { asyncLocalStorage } from '../lib/asyncLocalStorage'; import type { IApiService } from './IApiService'; import type { IBroker, IBrokerNode } from './IBroker';