diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts index dde631e0485b9..1af60216c505d 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts @@ -1,3 +1,4 @@ +import type { UiKit } from '@rocket.chat/core-typings'; import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { v, compile } from 'suretype'; @@ -75,6 +76,43 @@ const fetchCloudAnnouncementsSync = async ({ }; export async function announcementSync() { + await handleAnnouncementsOnWorkspaceSync({ + create: [ + { + _id: 'announcement-1', + _updatedAt: '2021-08-31T13:00:00.000Z', + selector: { + roles: ['admin'], + }, + platform: ['web', 'mobile'], + expireAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(), + startAt: new Date().toISOString(), + createdBy: 'cloud', + createdAt: new Date().toISOString(), + dictionary: {}, + view: { + id: 'announcement-1', + appId: 'cloud-announcements-core', + title: { + type: 'plain_text', + text: 'Announcement 1', + }, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Announcement 1', + }, + }, + ], + } satisfies UiKit.ModalView as UiKit.View, + surface: 'modal', + }, + ], + delete: [], + }); + try { const { workspaceRegistered } = await retrieveRegistrationStatus(); if (!workspaceRegistered) { diff --git a/apps/meteor/lib/errors/InvalidCloudAnnouncementInteractionError.ts b/apps/meteor/lib/errors/InvalidCloudAnnouncementInteractionError.ts new file mode 100644 index 0000000000000..a8e042fdbe634 --- /dev/null +++ b/apps/meteor/lib/errors/InvalidCloudAnnouncementInteractionError.ts @@ -0,0 +1,3 @@ +export class InvalidCloudAnnouncementInteractionError extends Error { + name = InvalidCloudAnnouncementInteractionError.name; +} diff --git a/apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts b/apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts new file mode 100644 index 0000000000000..8db9daa92eaa6 --- /dev/null +++ b/apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts @@ -0,0 +1,3 @@ +export class InvalidCoreAppInteractionError extends Error { + name = InvalidCoreAppInteractionError.name; +} diff --git a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts index 47f3001ca6157..7e1d614343d05 100644 --- a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts +++ b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts @@ -1,13 +1,25 @@ import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; -import type { Serialized, UiKit } from '@rocket.chat/core-typings'; +import type { IUser, UiKit } from '@rocket.chat/core-typings'; import { CloudAnnouncements } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; 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 { exhaustiveCheck } from '../../../lib/utils/exhaustiveCheck'; +type CloudAnnouncementInteractant = + | { + user: Pick; + } + | { + visitor: Pick['visitor'], 'id' | 'username' | 'name' | 'department' | 'phone'>; + }; + +type CloudAnnouncementInteractionRequest = UiKit.UserInteraction & CloudAnnouncementInteractant; + export class CloudAnnouncementsModule implements IUiKitCoreApp { appId = 'cloud-announcements-core'; @@ -19,28 +31,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { return settings.get('Cloud_Url'); } - private async pushUserInteraction(userInteraction: UiKit.UserInteraction): Promise> { - const token = await this.getWorkspaceAccessToken(); - - const response = await fetch(`${this.getCloudUrl()}/api/v3/comms/workspace/interaction`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(userInteraction), - }); - - if (!response.ok) { - const { error } = await response.json(); - throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); - } - - const serverInteraction: UiKit.ServerInteraction = await response.json(); - - return serverInteraction; - } - - private async handleServerInteraction(serverInteraction: UiKit.ServerInteraction): Promise { + protected async handleServerInteraction(serverInteraction: UiKit.ServerInteraction): Promise { switch (serverInteraction.type) { case 'modal.open': { const { view } = serverInteraction; @@ -92,7 +83,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { case 'contextual_bar.close': { const { view } = serverInteraction; - await CloudAnnouncements.updateMany({ 'view.id': view.id }, { $set: { view } }); + await CloudAnnouncements.deleteMany({ 'view.id': view.id }); break; } @@ -104,22 +95,25 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { } } - private async handleFallbackInteraction(userInteraction: UiKit.UserInteraction): Promise { + protected async handleFallbackInteraction(userInteraction: UiKit.UserInteraction): Promise { if (userInteraction.type === 'viewClosed') { await CloudAnnouncements.deleteMany({ 'view.id': userInteraction.payload.viewId ?? userInteraction.payload.view.id }); } } - private async forwardInteraction(userInteraction: UiKit.UserInteraction): Promise { + protected async forwardInteraction( + interactant: CloudAnnouncementInteractant, + userInteraction: UiKit.UserInteraction, + ): Promise { try { - const serverInteraction = await this.pushUserInteraction(userInteraction); + const serverInteraction = await this.pushUserInteraction(interactant, userInteraction); if (serverInteraction.appId !== this.appId) { - throw new CloudWorkspaceConnectionError(`Invalid appId received from Rocket.Chat Cloud`); + throw new InvalidCloudAnnouncementInteractionError(`Invalid appId`); } if (serverInteraction.triggerId !== userInteraction.triggerId) { - throw new CloudWorkspaceConnectionError(`Invalid triggerId received from Rocket.Chat Cloud`); + throw new InvalidCloudAnnouncementInteractionError(`Invalid triggerId`); } await this.handleServerInteraction(serverInteraction); @@ -132,15 +126,195 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { } } - blockAction(payload: UiKitCoreAppPayload): Promise { - return this.forwardInteraction(payload as unknown as UiKit.UserInteraction); + blockAction(payload: UiKitCoreAppPayload): Promise { + return this.handlePayload(payload); } - viewSubmit(payload: UiKitCoreAppPayload): Promise { - return this.forwardInteraction(payload as unknown as UiKit.UserInteraction); + viewSubmit(payload: UiKitCoreAppPayload): Promise { + return this.handlePayload(payload); } - async viewClosed(payload: UiKitCoreAppPayload): Promise { - return this.forwardInteraction(payload as unknown as UiKit.UserInteraction); + viewClosed(payload: UiKitCoreAppPayload): Promise { + return this.handlePayload(payload); + } + + protected async handlePayload(payload: UiKitCoreAppPayload): Promise { + 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`); + } + + await this.handleServerInteraction(serverInteraction); + + return serverInteraction; + } catch (error) { + await this.handleFallbackInteraction(interaction); + + throw 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`); + } + + 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) { + 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 serverInteraction: UiKit.ServerInteraction = await response.json(); + + return serverInteraction; } } diff --git a/packages/core-services/src/types/IUiKitCoreApp.ts b/packages/core-services/src/types/IUiKitCoreApp.ts index 98799918e594f..5ba521b73642c 100644 --- a/packages/core-services/src/types/IUiKitCoreApp.ts +++ b/packages/core-services/src/types/IUiKitCoreApp.ts @@ -2,6 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { IServiceClass } from './ServiceClass'; +// TODO: Fix this type to match `UiKit.UserInteraction` from `@rocket.chat/core-typings` export type UiKitCoreAppPayload = { appId: string; type: 'blockAction' | 'viewClosed' | 'viewSubmit';