RocketChat/Rocket.Chat

View on GitHub
apps/meteor/server/modules/listeners/listeners.module.ts

Summary

Maintainability
F
4 days
Test Coverage
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings';
import type { IServiceClass } from '@rocket.chat/core-services';
import { EnterpriseSettings } from '@rocket.chat/core-services';
import { isSettingColor, isSettingEnterprise, UserStatus } from '@rocket.chat/core-typings';
import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger } from '@rocket.chat/logger';
import { parse } from '@rocket.chat/message-parser';

import { settings } from '../../../app/settings/server/cached';
import type { NotificationsModule } from '../notifications/notifications.module';

const isMessageParserDisabled = process.env.DISABLE_MESSAGE_PARSER === 'true';

const STATUS_MAP: Record<UserStatus, 0 | 1 | 2 | 3> = {
    [UserStatus.OFFLINE]: 0,
    [UserStatus.ONLINE]: 1,
    [UserStatus.AWAY]: 2,
    [UserStatus.BUSY]: 3,
    [UserStatus.DISABLED]: 0,
} as const;

const minimongoChangeMap: Record<string, string> = {
    inserted: 'added',
    updated: 'changed',
    removed: 'removed',
} as const;

export class ListenersModule {
    constructor(service: IServiceClass, notifications: NotificationsModule) {
        const logger = new Logger('ListenersModule');

        service.onEvent('license.sync', () => notifications.notifyAllInThisInstance('license'));
        service.onEvent('license.actions', () => notifications.notifyAllInThisInstance('license'));

        service.onEvent('emoji.deleteCustom', (emoji) => {
            notifications.notifyLoggedInThisInstance('deleteEmojiCustom', {
                emojiData: emoji,
            });
        });

        service.onEvent('emoji.updateCustom', (emoji) => {
            notifications.notifyLoggedInThisInstance('updateEmojiCustom', {
                emojiData: emoji,
            });
        });

        service.onEvent('notify.ephemeralMessage', (uid, rid, message) => {
            if (!isMessageParserDisabled && message.msg) {
                const customDomains = settings.get<string>('Message_CustomDomain_AutoLink')
                    ? settings
                            .get<string>('Message_CustomDomain_AutoLink')
                            .split(',')
                            .map((domain) => domain.trim())
                    : [];

                message.md = parse(message.msg, {
                    colors: settings.get('HexColorPreview_Enabled'),
                    emoticons: true,
                    customDomains,
                    ...(settings.get('Katex_Enabled') && {
                        katex: {
                            dollarSyntax: settings.get('Katex_Dollar_Syntax'),
                            parenthesisSyntax: settings.get('Katex_Parenthesis_Syntax'),
                        },
                    }),
                });
            }

            notifications.notifyUserInThisInstance(uid, 'message', {
                groupable: false,
                u: {
                    _id: 'rocket.cat',
                    username: 'rocket.cat',
                },
                private: true,
                _id: message._id || String(Date.now()),
                rid,
                ts: new Date(),
                _updatedAt: new Date(),
                ...message,
            });
        });

        service.onEvent('permission.changed', ({ clientAction, data }) => {
            notifications.notifyLoggedInThisInstance('permissions-changed', clientAction, data);
        });

        service.onEvent('room.avatarUpdate', ({ _id: rid, avatarETag: etag }) => {
            notifications.notifyLoggedInThisInstance('updateAvatar', {
                rid,
                etag,
            });
        });

        service.onEvent('user.avatarUpdate', ({ username, avatarETag: etag }) => {
            notifications.notifyLoggedInThisInstance('updateAvatar', {
                username,
                etag,
            });
        });

        service.onEvent('user.deleted', ({ _id: userId }, data) => {
            notifications.notifyLoggedInThisInstance('Users:Deleted', {
                userId,
                ...data,
            });
        });

        service.onEvent('user.deleteCustomStatus', (userStatus) => {
            notifications.notifyLoggedInThisInstance('deleteCustomUserStatus', {
                userStatusData: userStatus,
            });
        });

        service.onEvent('user.nameChanged', (user) => {
            notifications.notifyLoggedInThisInstance('Users:NameChanged', user);
        });

        service.onEvent('user.roleUpdate', (update) => {
            notifications.notifyLoggedInThisInstance('roles-change', update);
        });

        service.onEvent(
            'user.video-conference',
            ({
                userId,
                action,
                params,
            }: {
                userId: string;
                action: string;
                params: {
                    callId: VideoConference['_id'];
                    uid: IUser['_id'];
                    rid: IRoom['_id'];
                };
            }) => {
                notifications.notifyUserInThisInstance(userId, 'video-conference', { action, params });
            },
        );

        service.onEvent('room.video-conference', ({ rid, callId }) => {
            /* deprecated */
            (notifications.notifyRoom as any)(rid, callId);

            notifications.notifyRoom(rid, 'videoconf', callId);
        });

        service.onEvent('presence.status', ({ user }) => {
            const { _id, username, name, status, statusText, roles } = user;
            if (!status || !username) {
                return;
            }

            notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]);

            if (_id) {
                notifications.sendPresence(_id, username, STATUS_MAP[status], statusText);
            }
        });

        service.onEvent('user.updateCustomStatus', (userStatus) => {
            notifications.notifyLoggedInThisInstance('updateCustomUserStatus', {
                userStatusData: userStatus,
            });
        });

        service.onEvent('watch.messages', async ({ message }) => {
            if (!message.rid) {
                return;
            }

            notifications.streamRoomMessage._emit('__my_messages__', [message], undefined, false, (streamer, _sub, eventName, args, allowed) =>
                streamer.changedPayload(streamer.subscriptionName, 'id', {
                    eventName,
                    args: [...args, allowed],
                }),
            );

            notifications.streamRoomMessage.emitWithoutBroadcast(message.rid, message);
        });

        service.onEvent('notify.messagesRead', ({ rid, until, tmid }): void => {
            notifications.notifyRoomInThisInstance(rid, 'messagesRead', { tmid, until });
        });

        service.onEvent('watch.subscriptions', ({ clientAction, subscription }) => {
            if (!subscription.u?._id) {
                return;
            }

            // emit a removed event on msg stream to remove the user's stream-room-messages subscription when the user is removed from room
            if (clientAction === 'removed') {
                notifications.streamRoomMessage.__emit(subscription.u._id, clientAction, subscription);
            }

            notifications.streamUser.__emit(subscription.u._id, clientAction, subscription);

            notifications.notifyUserInThisInstance(subscription.u._id, 'subscriptions-changed', clientAction, subscription as any);
        });

        service.onEvent('watch.roles', ({ clientAction, role }): void => {
            const payload = {
                type: clientAction,
                ...role,
            };
            notifications.streamRoles.emitWithoutBroadcast('roles', payload as any);
        });

        service.onEvent('watch.inquiries', async ({ clientAction, inquiry, diff }): Promise<void> => {
            const type = minimongoChangeMap[clientAction] as 'added' | 'changed' | 'removed';
            if (clientAction === 'removed') {
                notifications.streamLivechatQueueData.emitWithoutBroadcast(inquiry._id, {
                    _id: inquiry._id,
                    clientAction,
                });

                if (inquiry.department) {
                    return notifications.streamLivechatQueueData.emitWithoutBroadcast(`department/${inquiry.department}`, { type, ...inquiry });
                }

                return notifications.streamLivechatQueueData.emitWithoutBroadcast('public', {
                    type,
                    ...inquiry,
                });
            }

            // Don't do notifications for updating inquiries when the only thing changing is the queue metadata
            if (
                clientAction === 'updated' &&
                diff?.hasOwnProperty('lockedAt') &&
                diff?.hasOwnProperty('locked') &&
                diff?.hasOwnProperty('_updatedAt') &&
                Object.keys(diff).length === 3
            ) {
                return;
            }

            notifications.streamLivechatQueueData.emitWithoutBroadcast(inquiry._id, {
                ...inquiry,
                clientAction,
            });

            if (!inquiry.department) {
                return notifications.streamLivechatQueueData.emitWithoutBroadcast('public', {
                    type,
                    ...inquiry,
                });
            }

            notifications.streamLivechatQueueData.emitWithoutBroadcast(`department/${inquiry.department}`, { type, ...inquiry });

            if (clientAction === 'updated' && !diff?.department) {
                notifications.streamLivechatQueueData.emitWithoutBroadcast('public', { type, ...inquiry });
            }
        });

        service.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => {
            // if a EE setting changed make sure we broadcast the correct value according to license
            if (clientAction !== 'removed' && isSettingEnterprise(setting)) {
                try {
                    const result = await EnterpriseSettings.changeSettingValue(setting);
                    if (result !== undefined && !(result instanceof Error)) {
                        setting.value = result;
                    }
                } catch (err: unknown) {
                    logger.error({ msg: 'Error getting proper enterprise setting value. Returning `invalidValue` instead.', err });
                    setting.value = setting.invalidValue;
                }
            }

            if (setting.hidden) {
                return;
            }

            const value = {
                _id: setting._id,
                value: setting.value,
                ...(isSettingColor(setting) && { editor: setting.editor }),
                properties: setting.properties,
                enterprise: setting.enterprise,
                requiredOnWizard: setting.requiredOnWizard,
            } as ISetting;

            if (setting.public === true) {
                notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value);
                notifications.notifyAllInThisInstance('public-info', ['public-settings-changed', [clientAction, value]]);
            }

            notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, value);
        });

        service.onEvent('watch.rooms', ({ clientAction, room }): void => {
            // this emit will cause the user to receive a 'rooms-changed' event
            notifications.streamUser.__emit(room._id, clientAction, room);

            notifications.streamRoomData.emitWithoutBroadcast(room._id, room as IOmnichannelRoom);
        });

        service.onEvent('watch.users', (event): void => {
            switch (event.clientAction) {
                case 'updated':
                    notifications.notifyUserInThisInstance(event.id, 'userData', {
                        id: event.id,
                        diff: event.diff,
                        unset: event.unset,
                        type: 'updated',
                    });
                    break;
                case 'inserted':
                    notifications.notifyUserInThisInstance(event.id, 'userData', { id: event.id, data: event.data, type: 'inserted' });
                    break;
                case 'removed':
                    notifications.notifyUserInThisInstance(event.id, 'userData', { id: event.id, type: 'removed' });
                    break;
            }
        });

        service.onEvent('watch.integrationHistory', ({ clientAction, data, diff, id }): void => {
            if (!data?.integration?._id) {
                return;
            }
            switch (clientAction) {
                case 'updated': {
                    notifications.streamIntegrationHistory.emitWithoutBroadcast(data.integration._id, {
                        id,
                        diff,
                        type: clientAction,
                    });
                    break;
                }
                case 'inserted': {
                    notifications.streamIntegrationHistory.emitWithoutBroadcast(data.integration._id, {
                        data,
                        type: clientAction,
                    });
                    break;
                }
            }
        });

        service.onEvent('watch.livechatDepartmentAgents', ({ clientAction, data }): void => {
            const { agentId } = data;
            if (!agentId) {
                return;
            }

            notifications.notifyUserInThisInstance(agentId, 'departmentAgentData', {
                action: clientAction,
                ...data,
            });
        });

        service.onEvent('banner.user', (userId, banner): void => {
            notifications.notifyUserInThisInstance(userId, 'banners', banner);
        });

        service.onEvent('banner.new', (bannerId): void => {
            notifications.notifyLoggedInThisInstance('new-banner', { bannerId }); // deprecated
            notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
        });

        service.onEvent('banner.disabled', (bannerId): void => {
            notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
        });

        service.onEvent('banner.enabled', (bannerId): void => {
            notifications.notifyLoggedInThisInstance('banner-changed', { bannerId });
        });

        service.onEvent('voip.events', (userId, data): void => {
            notifications.notifyUserInThisInstance(userId, 'voip.events', data);
        });

        service.onEvent('call.callerhangup', (userId, data): void => {
            notifications.notifyUserInThisInstance(userId, 'call.hangup', data);
        });

        service.onEvent('notify.desktop', (uid, notification): void => {
            notifications.notifyUserInThisInstance(uid, 'notification', notification);
        });

        service.onEvent('notify.uiInteraction', (uid, interaction): void => {
            notifications.notifyUserInThisInstance(uid, 'uiInteraction', interaction);
        });

        service.onEvent('notify.updateInvites', (uid, data): void => {
            notifications.notifyUserInThisInstance(uid, 'updateInvites', data);
        });

        service.onEvent('notify.webdav', (uid, data): void => {
            notifications.notifyUserInThisInstance(uid, 'webdav', data);
        });

        service.onEvent('notify.e2e.keyRequest', (rid, data): void => {
            notifications.notifyRoomInThisInstance(rid, 'e2e.keyRequest', data);
        });

        service.onEvent('notify.deleteMessage', (rid, data): void => {
            notifications.notifyRoomInThisInstance(rid, 'deleteMessage', data);
        });

        service.onEvent('notify.deleteMessageBulk', (rid, data): void => {
            notifications.notifyRoomInThisInstance(rid, 'deleteMessageBulk', data);
        });

        service.onEvent('notify.deleteCustomSound', (data): void => {
            notifications.notifyAllInThisInstance('deleteCustomSound', data);
            notifications.notifyAllInThisInstance('public-info', ['deleteCustomSound', [data]]);
        });

        service.onEvent('notify.updateCustomSound', (data): void => {
            notifications.notifyAllInThisInstance('updateCustomSound', data);
            notifications.notifyAllInThisInstance('public-info', ['updateCustomSound', [data]]);
        });

        service.onEvent('notify.calendar', (uid, data): void => {
            notifications.notifyUserInThisInstance(uid, 'calendar', data);
        });

        service.onEvent('notify.importedMessages', ({ roomIds }): void => {
            roomIds.forEach((rid) => {
                // couldnt get TS happy by providing no data, so had to provide null
                notifications.notifyRoomInThisInstance(rid, 'messagesImported', null);
            });
        });

        service.onEvent('connector.statuschanged', (enabled): void => {
            notifications.notifyLoggedInThisInstance('voip.statuschanged', enabled);
        });
        service.onEvent('omnichannel.room', (roomId, data): void => {
            notifications.streamLivechatRoom.emitWithoutBroadcast(roomId, data);
        });
        service.onEvent('watch.priorities', async ({ clientAction, diff, id }): Promise<void> => {
            notifications.notifyLoggedInThisInstance('omnichannel.priority-changed', { id, clientAction, name: diff?.name });
        });

        service.onEvent('apps.added', (appId: string) => {
            notifications.streamApps.emitWithoutBroadcast('app/added', appId);
            notifications.streamApps.emitWithoutBroadcast('apps', ['app/added', [appId]]);
        });

        service.onEvent('apps.removed', (appId: string) => {
            notifications.streamApps.emitWithoutBroadcast('app/removed', appId);
            notifications.streamApps.emitWithoutBroadcast('apps', ['app/removed', [appId]]);
        });

        service.onEvent('apps.updated', (appId: string) => {
            notifications.streamApps.emitWithoutBroadcast('app/updated', appId);
            notifications.streamApps.emitWithoutBroadcast('apps', ['app/updated', [appId]]);
        });

        service.onEvent('apps.statusUpdate', (appId: string, status: AppStatus) => {
            notifications.streamApps.emitWithoutBroadcast('app/statusUpdate', { appId, status });
            notifications.streamApps.emitWithoutBroadcast('apps', ['app/statusUpdate', [{ appId, status }]]);
        });

        service.onEvent('apps.settingUpdated', (appId: string, setting: AppsSetting) => {
            notifications.streamApps.emitWithoutBroadcast('app/settingUpdated', { appId, setting });
            notifications.streamApps.emitWithoutBroadcast('apps', ['app/settingUpdated', [{ appId, setting }]]);
        });

        service.onEvent('command.added', (command: string) => {
            notifications.streamApps.emitWithoutBroadcast('command/added', command);
            notifications.streamApps.emitWithoutBroadcast('apps', ['command/added', [command]]);
        });

        service.onEvent('command.disabled', (command: string) => {
            notifications.streamApps.emitWithoutBroadcast('command/disabled', command);
            notifications.streamApps.emitWithoutBroadcast('apps', ['command/disabled', [command]]);
        });

        service.onEvent('command.updated', (command: string) => {
            notifications.streamApps.emitWithoutBroadcast('command/updated', command);
            notifications.streamApps.emitWithoutBroadcast('apps', ['command/updated', [command]]);
        });

        service.onEvent('command.removed', (command: string) => {
            notifications.streamApps.emitWithoutBroadcast('command/removed', command);
            notifications.streamApps.emitWithoutBroadcast('apps', ['command/removed', [command]]);
        });

        service.onEvent('actions.changed', () => {
            notifications.streamApps.emitWithoutBroadcast('actions/changed');
            notifications.streamApps.emitWithoutBroadcast('apps', ['actions/changed', []]);
        });
    }
}