Skip to content

Commit db9e1f6

Browse files
ggazzod-gubert
andauthoredJun 14, 2023
feat: Add Apps-engine Thread Bridge (RocketChat#29500)
Co-authored-by: Douglas <douglas.gubert@gmail.com>
1 parent c70cfe8 commit db9e1f6

File tree

7 files changed

+232
-7
lines changed

7 files changed

+232
-7
lines changed
 

‎.changeset/cyan-panthers-allow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/meteor': minor
3+
---
4+
5+
feat: Add Apps engine Thread Bridge

‎apps/meteor/app/apps/server/bridges/bridges.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { AppVideoConferenceBridge } from './videoConferences';
2222
import { AppOAuthAppsBridge } from './oauthApps';
2323
import { AppInternalFederationBridge } from './internalFederation';
2424
import { AppModerationBridge } from './moderation';
25+
import { AppThreadBridge } from './thread';
2526

2627
export class RealAppBridges extends AppBridges {
2728
constructor(orch) {
@@ -47,8 +48,9 @@ export class RealAppBridges extends AppBridges {
4748
this._cloudWorkspaceBridge = new AppCloudBridge(orch);
4849
this._videoConfBridge = new AppVideoConferenceBridge(orch);
4950
this._oAuthBridge = new AppOAuthAppsBridge(orch);
50-
this._internalFedBridge = new AppInternalFederationBridge(orch);
51+
this._internalFedBridge = new AppInternalFederationBridge();
5152
this._moderationBridge = new AppModerationBridge(orch);
53+
this._threadBridge = new AppThreadBridge(orch);
5254
}
5355

5456
getCommandBridge() {
@@ -75,6 +77,10 @@ export class RealAppBridges extends AppBridges {
7577
return this._msgBridge;
7678
}
7779

80+
getThreadBridge() {
81+
return this._threadBridge;
82+
}
83+
7884
getPersistenceBridge() {
7985
return this._persistBridge;
8086
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ThreadBridge } from '@rocket.chat/apps-engine/server/bridges/ThreadBridge';
2+
import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
3+
4+
import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator';
5+
6+
export class AppThreadBridge extends ThreadBridge {
7+
// eslint-disable-next-line no-empty-function
8+
constructor(private readonly orch: AppServerOrchestrator) {
9+
super();
10+
}
11+
12+
protected async getById(threadID: string, appId: string): Promise<IMessage[]> {
13+
this.orch.debugLog(`The App ${appId} is getting the thread: "${threadID}"`);
14+
15+
return this.orch.getConverters()?.get('threads').convertById(threadID);
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { isEditedMessage, type IMessage } from '@rocket.chat/core-typings';
2+
import { Messages } from '@rocket.chat/models';
3+
import type { IMessage as AppsEngineMessage } from '@rocket.chat/apps-engine/definition/messages';
4+
import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms';
5+
6+
import { transformMappedData } from '../../../../ee/lib/misc/transformMappedData';
7+
8+
// eslint-disable-next-line @typescript-eslint/naming-convention
9+
interface Orchestrator {
10+
rooms: () => {
11+
convertById(id: string): Promise<unknown>;
12+
};
13+
users: () => {
14+
convertById(id: string): Promise<unknown>;
15+
convertToApp(user: unknown): Promise<unknown>;
16+
};
17+
}
18+
19+
const cachedFunction = <F extends (...args: any[]) => any>(fn: F) => {
20+
const cache = new Map<string, unknown>();
21+
22+
return ((...args) => {
23+
const cacheKey = JSON.stringify(args);
24+
25+
if (cache.has(cacheKey)) {
26+
return cache.get(cacheKey) as ReturnType<F>;
27+
}
28+
29+
const result = fn(...args);
30+
31+
cache.set(cacheKey, result);
32+
33+
return result;
34+
}) as F;
35+
};
36+
37+
export class AppThreadsConverter {
38+
constructor(
39+
private readonly orch: {
40+
getConverters: () => {
41+
get: <O extends keyof Orchestrator>(key: O) => ReturnType<Orchestrator[O]>;
42+
};
43+
},
44+
) {
45+
this.orch = orch;
46+
}
47+
48+
async convertById(threadId: string) {
49+
const query = {
50+
$or: [
51+
{
52+
_id: threadId,
53+
},
54+
{
55+
tmid: threadId,
56+
},
57+
],
58+
};
59+
60+
const mainMessage = await Messages.findOneById(threadId);
61+
62+
if (!mainMessage) {
63+
return [];
64+
}
65+
66+
const replies = await Messages.find(query).toArray();
67+
68+
const room = (await this.orch.getConverters().get('rooms').convertById(mainMessage.rid)) as IRoom | undefined;
69+
70+
if (!room) {
71+
return [];
72+
}
73+
74+
const convertToApp = cachedFunction(this.orch.getConverters().get('users').convertToApp.bind(this.orch.getConverters().get('users')));
75+
76+
const convertUserById = cachedFunction(this.orch.getConverters().get('users').convertById.bind(this.orch.getConverters().get('users')));
77+
78+
return Promise.all([mainMessage, ...replies].map((msg) => this.convertMessage(msg, room, convertUserById, convertToApp)));
79+
}
80+
81+
async convertMessage(
82+
msgObj: IMessage,
83+
room: IRoom,
84+
convertUserById: ReturnType<Orchestrator['users']>['convertById'],
85+
convertToApp: ReturnType<Orchestrator['users']>['convertToApp'],
86+
): Promise<AppsEngineMessage> {
87+
const map = {
88+
id: '_id',
89+
threadId: 'tmid',
90+
reactions: 'reactions',
91+
parseUrls: 'parseUrls',
92+
text: 'msg',
93+
createdAt: 'ts',
94+
updatedAt: '_updatedAt',
95+
editedAt: 'editedAt',
96+
emoji: 'emoji',
97+
avatarUrl: 'avatar',
98+
alias: 'alias',
99+
file: 'file',
100+
customFields: 'customFields',
101+
groupable: 'groupable',
102+
token: 'token',
103+
blocks: 'blocks',
104+
room,
105+
editor: async (message: IMessage) => {
106+
if (!isEditedMessage(message)) {
107+
return undefined;
108+
}
109+
110+
const { editedBy } = message;
111+
112+
return convertUserById(editedBy._id);
113+
},
114+
attachments: async (message: IMessage) => {
115+
if (!message.attachments) {
116+
return undefined;
117+
}
118+
const result = await this._convertAttachmentsToApp(message.attachments);
119+
delete message.attachments;
120+
return result;
121+
},
122+
sender: async (message: IMessage) => {
123+
if (!message.u?._id) {
124+
return undefined;
125+
}
126+
127+
let user = await convertUserById(message.u._id);
128+
129+
// When the sender of the message is a Guest (livechat) and not a user
130+
if (!user) {
131+
user = await convertToApp(message.u);
132+
}
133+
134+
return user;
135+
},
136+
};
137+
138+
return (await transformMappedData(msgObj, map)) as unknown as AppsEngineMessage;
139+
}
140+
141+
async _convertAttachmentsToApp(attachments: NonNullable<IMessage['attachments']>) {
142+
const map = {
143+
collapsed: 'collapsed',
144+
color: 'color',
145+
text: 'text',
146+
timestampLink: 'message_link',
147+
thumbnailUrl: 'thumb_url',
148+
imageDimensions: 'image_dimensions',
149+
imagePreview: 'image_preview',
150+
imageUrl: 'image_url',
151+
imageType: 'image_type',
152+
imageSize: 'image_size',
153+
audioUrl: 'audio_url',
154+
audioType: 'audio_type',
155+
audioSize: 'audio_size',
156+
videoUrl: 'video_url',
157+
videoType: 'video_type',
158+
videoSize: 'video_size',
159+
fields: 'fields',
160+
actionButtonsAlignment: 'button_alignment',
161+
actions: 'actions',
162+
type: 'type',
163+
description: 'description',
164+
author: (attachment: NonNullable<IMessage['attachments']>[number]) => {
165+
if (!('author_name' in attachment)) {
166+
return;
167+
}
168+
169+
const { author_name: name, author_link: link, author_icon: icon } = attachment;
170+
171+
delete attachment.author_name;
172+
delete attachment.author_link;
173+
delete attachment.author_icon;
174+
175+
return { name, link, icon };
176+
},
177+
title: (attachment: NonNullable<IMessage['attachments']>[number]) => {
178+
const { title: value, title_link: link, title_link_download: displayDownloadLink } = attachment;
179+
180+
delete attachment.title;
181+
delete attachment.title_link;
182+
delete attachment.title_link_download;
183+
184+
return { value, link, displayDownloadLink };
185+
},
186+
timestamp: (attachment: NonNullable<IMessage['attachments']>[number]) => {
187+
const result = attachment.ts ? new Date(attachment.ts) : undefined;
188+
delete attachment.ts;
189+
return result;
190+
},
191+
};
192+
193+
return Promise.all(attachments.map(async (attachment) => transformMappedData(attachment, map)));
194+
}
195+
}

‎apps/meteor/ee/server/apps/orchestrator.js

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../../../app/apps/server/converters';
2121
import { AppRealLogsStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage';
2222
import { canEnableApp } from '../../app/license/server/license';
23+
import { AppThreadsConverter } from '../../../app/apps/server/converters/threads';
2324

2425
function isTesting() {
2526
return process.env.TEST_MODE === 'true';
@@ -62,6 +63,7 @@ export class AppServerOrchestrator {
6263
this._converters.set('departments', new AppDepartmentsConverter(this));
6364
this._converters.set('uploads', new AppUploadsConverter(this));
6465
this._converters.set('videoConferences', new AppVideoConferencesConverter());
66+
this._converters.set('threads', new AppThreadsConverter(this));
6567

6668
this._bridges = new RealAppBridges(this);
6769

‎apps/meteor/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@
219219
"@rocket.chat/account-utils": "workspace:^",
220220
"@rocket.chat/agenda": "workspace:^",
221221
"@rocket.chat/api-client": "workspace:^",
222-
"@rocket.chat/apps-engine": "1.39.1-alpha.248",
222+
"@rocket.chat/apps-engine": "1.40.0-alpha.266",
223223
"@rocket.chat/base64": "workspace:^",
224224
"@rocket.chat/cas-validate": "workspace:^",
225225
"@rocket.chat/core-services": "workspace:^",

‎yarn.lock

+5-5
Original file line numberDiff line numberDiff line change
@@ -7819,9 +7819,9 @@ __metadata:
78197819
languageName: node
78207820
linkType: hard
78217821

7822-
"@rocket.chat/apps-engine@npm:1.39.1-alpha.248":
7823-
version: 1.39.1-alpha.248
7824-
resolution: "@rocket.chat/apps-engine@npm:1.39.1-alpha.248"
7822+
"@rocket.chat/apps-engine@npm:1.40.0-alpha.266":
7823+
version: 1.40.0-alpha.266
7824+
resolution: "@rocket.chat/apps-engine@npm:1.40.0-alpha.266"
78257825
dependencies:
78267826
adm-zip: ^0.5.9
78277827
cryptiles: ^4.1.3
@@ -7833,7 +7833,7 @@ __metadata:
78337833
vm2: ^3.9.19
78347834
peerDependencies:
78357835
"@rocket.chat/ui-kit": "*"
7836-
checksum: d7be27748f482bb41ee234344bfbe6040f161b33c5cfe20d3e054e07d5b7c6ff87a40672c49d9250da354d1a0de0a6b4d2c2cb1ce65e889726932404898b1e6d
7836+
checksum: 5176cd8f365b0249cf9d6882e26eea0e0503d37d86d27018541ee9d34dd95feb10456192d64f2c7827c4051b651d76039c8b1a9e46e6e541f62f9991c950c5be
78377837
languageName: node
78387838
linkType: hard
78397839

@@ -8570,7 +8570,7 @@ __metadata:
85708570
"@rocket.chat/account-utils": "workspace:^"
85718571
"@rocket.chat/agenda": "workspace:^"
85728572
"@rocket.chat/api-client": "workspace:^"
8573-
"@rocket.chat/apps-engine": 1.39.1-alpha.248
8573+
"@rocket.chat/apps-engine": 1.40.0-alpha.266
85748574
"@rocket.chat/base64": "workspace:^"
85758575
"@rocket.chat/cas-validate": "workspace:^"
85768576
"@rocket.chat/core-services": "workspace:^"

0 commit comments

Comments
 (0)
Please sign in to comment.