Skip to content

Commit

Permalink
Send interactant (user or visitor) data to Cloud
Browse files Browse the repository at this point in the history
  • Loading branch information
tassoevan committed Nov 28, 2023
1 parent df74ae3 commit 3e45fd5
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class InvalidCloudAnnouncementInteractionError extends Error {
name = InvalidCloudAnnouncementInteractionError.name;
}
3 changes: 3 additions & 0 deletions apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class InvalidCoreAppInteractionError extends Error {
name = InvalidCoreAppInteractionError.name;
}
244 changes: 209 additions & 35 deletions apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts
Original file line number Diff line number Diff line change
@@ -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<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';

Expand All @@ -19,28 +31,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp {
return settings.get('Cloud_Url');
}

private async pushUserInteraction(userInteraction: UiKit.UserInteraction): Promise<Serialized<UiKit.ServerInteraction>> {
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<void> {
protected async handleServerInteraction(serverInteraction: UiKit.ServerInteraction): Promise<void> {
switch (serverInteraction.type) {
case 'modal.open': {
const { view } = serverInteraction;
Expand Down Expand Up @@ -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;
}

Expand All @@ -104,22 +95,25 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp {
}
}

private async handleFallbackInteraction(userInteraction: UiKit.UserInteraction): Promise<void> {
protected async handleFallbackInteraction(userInteraction: UiKit.UserInteraction): Promise<void> {
if (userInteraction.type === 'viewClosed') {
await CloudAnnouncements.deleteMany({ 'view.id': userInteraction.payload.viewId ?? userInteraction.payload.view.id });
}
}

private async forwardInteraction(userInteraction: UiKit.UserInteraction): Promise<UiKit.ServerInteraction> {
protected async forwardInteraction(
interactant: CloudAnnouncementInteractant,
userInteraction: UiKit.UserInteraction,
): Promise<UiKit.ServerInteraction> {
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);
Expand All @@ -132,15 +126,195 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp {
}
}

blockAction(payload: UiKitCoreAppPayload): Promise<unknown> {
return this.forwardInteraction(payload as unknown as UiKit.UserInteraction);
blockAction(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction> {
return this.handlePayload(payload);
}

viewSubmit(payload: UiKitCoreAppPayload): Promise<unknown> {
return this.forwardInteraction(payload as unknown as UiKit.UserInteraction);
viewSubmit(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction> {
return this.handlePayload(payload);
}

async viewClosed(payload: UiKitCoreAppPayload): Promise<unknown> {
return this.forwardInteraction(payload as unknown as UiKit.UserInteraction);
viewClosed(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction> {
return this.handlePayload(payload);
}

protected async handlePayload(payload: UiKitCoreAppPayload): Promise<UiKit.ServerInteraction> {
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;
}
}
1 change: 1 addition & 0 deletions packages/core-services/src/types/IUiKitCoreApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 3e45fd5

Please sign in to comment.