RocketChat/Rocket.Chat

View on GitHub
apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts

Summary

Maintainability
C
1 day
Test Coverage
import { Banner } from '@rocket.chat/core-services';
import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services';
import type { Cloud, IBanner, IUser } from '@rocket.chat/core-typings';
import { Banners } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import type * as UiKit from '@rocket.chat/ui-kit';

import { getWorkspaceAccessToken } from '../../../app/cloud/server';
import { syncWorkspace } from '../../../app/cloud/server/functions/syncWorkspace';
import { settings } from '../../../app/settings/server';
import { CloudWorkspaceConnectionError } from '../../../lib/errors/CloudWorkspaceConnectionError';
import { InvalidCloudAnnouncementInteractionError } from '../../../lib/errors/InvalidCloudAnnouncementInteractionError';
import { InvalidCoreAppInteractionError } from '../../../lib/errors/InvalidCoreAppInteractionError';
import { SystemLogger } from '../../lib/logger/system';

type CloudAnnouncementInteractant =
    | {
            user: Pick<IUser, '_id' | 'username' | 'name'>;
      }
    | {
            visitor: Pick<Required<UiKitCoreAppPayload>['visitor'], 'id' | 'username' | 'name' | 'department' | 'phone'>;
      };

type CloudAnnouncementInteractionRequest = UiKit.UserInteraction & CloudAnnouncementInteractant;

export class CloudAnnouncementsModule implements IUiKitCoreApp {
    appId = 'cloud-announcements-core';

    protected async getWorkspaceAccessToken() {
        return getWorkspaceAccessToken(true);
    }

    protected getCloudUrl() {
        return settings.get('Cloud_Url');
    }

    blockAction(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction | void> {
        return this.handlePayload(payload);
    }

    viewSubmit(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction | void> {
        return this.handlePayload(payload);
    }

    async viewClosed(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction> {
        const {
            payload: { view: { viewId } = {} },
            user: { _id: userId } = {},
        } = payload;

        if (!userId) {
            throw new Error('invalid user');
        }

        if (!viewId) {
            throw new Error('invalid view');
        }

        if (!payload.triggerId) {
            throw new Error('invalid triggerId');
        }

        await Banner.dismiss(userId, viewId);

        const announcement = await Banners.findOneById<Pick<IBanner, 'surface'>>(viewId, { projection: { surface: 1 } });

        const type = announcement?.surface === 'banner' ? 'banner.close' : 'modal.close';

        // for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response
        setImmediate(async () => {
            await this.handlePayload(payload);
        });

        return {
            type,
            triggerId: payload.triggerId,
            appId: payload.appId,
            viewId,
        };
    }

    protected async handlePayload(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction | void> {
        const interactant = this.getInteractant(payload);
        const interaction = this.getInteraction(payload);

        try {
            const serverInteraction = await this.pushUserInteraction(interactant, interaction);

            if (serverInteraction.appId !== this.appId) {
                throw new InvalidCloudAnnouncementInteractionError(`Invalid appId`);
            }

            if (serverInteraction.triggerId !== interaction.triggerId) {
                throw new InvalidCloudAnnouncementInteractionError(`Invalid triggerId`);
            }

            return serverInteraction;
        } catch (error) {
            SystemLogger.error(error);
        }
    }

    protected getInteractant(payload: UiKitCoreAppPayload): CloudAnnouncementInteractant {
        if (payload.user) {
            return {
                user: {
                    _id: payload.user._id,
                    username: payload.user.username,
                    name: payload.user.name,
                },
            };
        }

        if (payload.visitor) {
            return {
                visitor: {
                    id: payload.visitor.id,
                    username: payload.visitor.username,
                    name: payload.visitor.name,
                    department: payload.visitor.department,
                    phone: payload.visitor.phone,
                },
            };
        }

        throw new CloudWorkspaceConnectionError(`Invalid user data received from Rocket.Chat Cloud`);
    }

    /**
     * Transform the payload received from the Core App back to the format the UI sends from the client
     */
    protected getInteraction(payload: UiKitCoreAppPayload): UiKit.UserInteraction {
        if (payload.type === 'blockAction' && payload.container?.type === 'message') {
            const {
                actionId,
                payload: { blockId, value },
                message,
                room,
                triggerId,
            } = payload;

            if (!actionId || !blockId || !triggerId) {
                throw new InvalidCoreAppInteractionError();
            }

            return {
                type: 'blockAction',
                actionId,
                payload: {
                    blockId,
                    value,
                },
                container: {
                    type: 'message',
                    id: String(message),
                },
                mid: String(message),
                tmid: undefined,
                rid: String(room),
                triggerId,
            };
        }

        if (payload.type === 'blockAction' && payload.container?.type === 'view') {
            const {
                actionId,
                payload: { blockId, value },
                container: { id },
                triggerId,
            } = payload;

            if (!actionId || !blockId || !triggerId) {
                throw new InvalidCoreAppInteractionError();
            }

            return {
                type: 'blockAction',
                actionId,
                payload: {
                    blockId,
                    value,
                },
                container: {
                    type: 'view',
                    id,
                },
                triggerId,
            };
        }

        if (payload.type === 'viewClosed') {
            const {
                payload: { view, isCleared },
                triggerId,
            } = payload;

            if (!view?.id || !triggerId) {
                throw new InvalidCoreAppInteractionError();
            }

            return {
                type: 'viewClosed',
                payload: {
                    viewId: view.id,
                    view: view as any,
                    isCleared: Boolean(isCleared),
                },
                triggerId,
            };
        }

        if (payload.type === 'viewSubmit') {
            const {
                payload: { view },
                triggerId,
            } = payload;

            if (!view?.id || !triggerId) {
                throw new InvalidCoreAppInteractionError();
            }

            return {
                type: 'viewSubmit',
                payload: {
                    view: view as any,
                },
                triggerId,
                viewId: view.id,
            };
        }

        throw new InvalidCoreAppInteractionError();
    }

    protected async pushUserInteraction(
        interactant: CloudAnnouncementInteractant,
        userInteraction: UiKit.UserInteraction,
    ): Promise<UiKit.ServerInteraction> {
        const token = await this.getWorkspaceAccessToken();

        const request: CloudAnnouncementInteractionRequest = {
            ...interactant,
            ...userInteraction,
        };

        const response = await fetch(`${this.getCloudUrl()}/api/v3/comms/workspace/interaction`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${token}`,
            },
            body: JSON.stringify(request),
        });

        if (!response.ok) {
            const { error } = await response.json();
            throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`);
        }

        const payload: Cloud.WorkspaceInteractionResponsePayload = await response.json();

        const { serverInteraction, serverAction } = payload;

        if (serverAction) {
            switch (serverAction) {
                case 'syncWorkspace': {
                    await syncWorkspace();
                    break;
                }
            }
        }

        return serverInteraction;
    }
}