From c253db3ece80da36fba7b79b3cbe64d705175d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=87=E3=83=AF=E3=83=B3=E3=82=B7=E3=83=A5?= <61188295+Dnouv@users.noreply.github.com> Date: Fri, 17 Oct 2025 19:38:41 +0530 Subject: [PATCH] feat(apps): experimental member room IDs read bridge (#37057) Co-authored-by: Douglas Gubert <1810309+d-gubert@users.noreply.github.com> --- .changeset/chatty-foxes-attend.md | 6 +++ .../meteor/app/apps/server/bridges/bridges.js | 6 +++ .../app/apps/server/bridges/experimental.ts | 17 ++++++++ .../deno-runtime/lib/accessors/mod.ts | 1 + .../definition/accessors/IExperimentalRead.ts | 16 ++++++++ .../src/definition/accessors/IRead.ts | 3 ++ .../src/definition/accessors/index.ts | 1 + .../src/server/accessors/ExperimentalRead.ts | 13 ++++++ .../src/server/accessors/Reader.ts | 7 +++- .../src/server/bridges/AppBridges.ts | 4 ++ .../src/server/bridges/ExperimentalBridge.ts | 40 +++++++++++++++++++ .../apps-engine/src/server/bridges/index.ts | 2 + .../src/server/managers/AppAccessorManager.ts | 21 +++++++++- .../src/server/permissions/AppPermissions.ts | 3 ++ .../tests/server/accessors/Reader.spec.ts | 6 +++ .../tests/test-data/bridges/appBridges.ts | 9 +++++ .../test-data/bridges/experimentalBridge.ts | 7 ++++ 17 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 .changeset/chatty-foxes-attend.md create mode 100644 apps/meteor/app/apps/server/bridges/experimental.ts create mode 100644 packages/apps-engine/src/definition/accessors/IExperimentalRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ExperimentalRead.ts create mode 100644 packages/apps-engine/src/server/bridges/ExperimentalBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts diff --git a/.changeset/chatty-foxes-attend.md b/.changeset/chatty-foxes-attend.md new file mode 100644 index 00000000000..eaf16b85406 --- /dev/null +++ b/.changeset/chatty-foxes-attend.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Adds an experimental API to the apps-engine that retrieves the ids of rooms the user is a member of diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 480ca170f3d..3b49cd91394 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -8,6 +8,7 @@ import { AppContactBridge } from './contact'; import { AppDetailChangesBridge } from './details'; import { AppEmailBridge } from './email'; import { AppEnvironmentalVariableBridge } from './environmental'; +import { AppExperimentalBridge } from './experimental'; import { AppHttpBridge } from './http'; import { AppInternalBridge } from './internal'; import { AppInternalFederationBridge } from './internalFederation'; @@ -59,6 +60,7 @@ export class RealAppBridges extends AppBridges { this._emailBridge = new AppEmailBridge(orch); this._contactBridge = new AppContactBridge(orch); this._outboundMessageBridge = new OutboundCommunicationBridge(orch); + this._experimentalBridge = new AppExperimentalBridge(orch); } getCommandBridge() { @@ -168,4 +170,8 @@ export class RealAppBridges extends AppBridges { getContactBridge() { return this._contactBridge; } + + getExperimentalBridge() { + return this._experimentalBridge; + } } diff --git a/apps/meteor/app/apps/server/bridges/experimental.ts b/apps/meteor/app/apps/server/bridges/experimental.ts new file mode 100644 index 00000000000..6c3f551db0d --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/experimental.ts @@ -0,0 +1,17 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import { ExperimentalBridge } from '@rocket.chat/apps-engine/server/bridges'; +import { Subscriptions } from '@rocket.chat/models'; + +export class AppExperimentalBridge extends ExperimentalBridge { + constructor(private readonly orch: IAppServerOrchestrator) { + super(); + } + + protected async getUserRoomIds(userId: string, appId: string): Promise { + this.orch.debugLog(`The App ${appId} is getting the room ids for the user: "${userId}"`); + + const subscriptions = await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray(); + + return subscriptions.map((subscription) => subscription.rid); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts index afb661a31d0..fc2fb6a3f66 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts @@ -253,6 +253,7 @@ export class AppAccessors { getThreadReader: () => this.proxify('getReader:getThreadReader'), getRoleReader: () => this.proxify('getReader:getRoleReader'), getContactReader: () => this.proxify('getReader:getContactReader'), + getExperimentalReader: () => this.proxify('getReader:getExperimentalReader'), }; } diff --git a/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts new file mode 100644 index 00000000000..5c202911d14 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IExperimentalRead.ts @@ -0,0 +1,16 @@ +/** + * @description + * Experimental bridge for experimental features. + * Methods in this class are not guaranteed to be stable between updates as the + * team evaluates the proper signature, underlying implementation and performance + * impact of candidates for future APIs + */ +export interface IExperimentalRead { + /** + * Fetches the IDs of the rooms that the user is a member of. + * + * @returns an array of room ids or undefined if the app doesn't have the proper permission + * @experimental + */ + getUserRoomIds(userId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IRead.ts b/packages/apps-engine/src/definition/accessors/IRead.ts index e4894e3a14f..9ab0984122c 100644 --- a/packages/apps-engine/src/definition/accessors/IRead.ts +++ b/packages/apps-engine/src/definition/accessors/IRead.ts @@ -1,6 +1,7 @@ import type { ICloudWorkspaceRead } from './ICloudWorkspaceRead'; import type { IContactRead } from './IContactRead'; import type { IEnvironmentRead } from './IEnvironmentRead'; +import type { IExperimentalRead } from './IExperimentalRead'; import type { ILivechatRead } from './ILivechatRead'; import type { IMessageRead } from './IMessageRead'; import type { INotifier } from './INotifier'; @@ -51,4 +52,6 @@ export interface IRead { getRoleReader(): IRoleRead; getContactReader(): IContactRead; + + getExperimentalReader(): IExperimentalRead; } diff --git a/packages/apps-engine/src/definition/accessors/index.ts b/packages/apps-engine/src/definition/accessors/index.ts index 3618dcc9208..16e8054996e 100644 --- a/packages/apps-engine/src/definition/accessors/index.ts +++ b/packages/apps-engine/src/definition/accessors/index.ts @@ -11,6 +11,7 @@ export * from './IEnvironmentalVariableRead'; export * from './IEnvironmentRead'; export * from './IEnvironmentWrite'; export * from './IExternalComponentsExtend'; +export * from './IExperimentalRead'; export * from './IHttp'; export * from './ILivechatCreator'; export * from './ILivechatMessageBuilder'; diff --git a/packages/apps-engine/src/server/accessors/ExperimentalRead.ts b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts new file mode 100644 index 00000000000..e94254d40ed --- /dev/null +++ b/packages/apps-engine/src/server/accessors/ExperimentalRead.ts @@ -0,0 +1,13 @@ +import type { IExperimentalRead } from '../../definition/accessors'; +import type { ExperimentalBridge } from '../bridges'; + +export class ExperimentalRead implements IExperimentalRead { + constructor( + private experimentalBridge: ExperimentalBridge, + private appId: string, + ) {} + + public async getUserRoomIds(userId: string): Promise { + return this.experimentalBridge.doGetUserRoomIds(userId, this.appId); + } +} diff --git a/packages/apps-engine/src/server/accessors/Reader.ts b/packages/apps-engine/src/server/accessors/Reader.ts index 4b83161eb7f..baa05b79ac3 100644 --- a/packages/apps-engine/src/server/accessors/Reader.ts +++ b/packages/apps-engine/src/server/accessors/Reader.ts @@ -1,6 +1,7 @@ import type { ICloudWorkspaceRead, IEnvironmentRead, + IExperimentalRead, ILivechatRead, IMessageRead, INotifier, @@ -29,10 +30,10 @@ export class Reader implements IRead { private cloud: ICloudWorkspaceRead, private videoConf: IVideoConferenceRead, private contactRead: IContactRead, - private oauthApps: IOAuthAppsReader, private thread: IThreadRead, private role: IRoleRead, + private experimental: IExperimentalRead, ) {} public getEnvironmentReader(): IEnvironmentRead { @@ -90,4 +91,8 @@ export class Reader implements IRead { public getContactReader(): IContactRead { return this.contactRead; } + + public getExperimentalReader(): IExperimentalRead { + return this.experimental; + } } diff --git a/packages/apps-engine/src/server/bridges/AppBridges.ts b/packages/apps-engine/src/server/bridges/AppBridges.ts index 66d047e15c1..5e5ef7ca121 100644 --- a/packages/apps-engine/src/server/bridges/AppBridges.ts +++ b/packages/apps-engine/src/server/bridges/AppBridges.ts @@ -6,6 +6,7 @@ import type { CommandBridge } from './CommandBridge'; import type { ContactBridge } from './ContactBridge'; import type { EmailBridge } from './EmailBridge'; import type { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; +import type { ExperimentalBridge } from './ExperimentalBridge'; import type { HttpBridge } from './HttpBridge'; import type { IInternalBridge } from './IInternalBridge'; import type { IInternalFederationBridge } from './IInternalFederationBridge'; @@ -42,6 +43,7 @@ export type Bridge = | IInternalBridge | ServerSettingBridge | EmailBridge + | ExperimentalBridge | UploadBridge | UserBridge | UiInteractionBridge @@ -106,4 +108,6 @@ export abstract class AppBridges { public abstract getRoleBridge(): RoleBridge; public abstract getOutboundMessageBridge(): OutboundMessageBridge; + + public abstract getExperimentalBridge(): ExperimentalBridge; } diff --git a/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts b/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts new file mode 100644 index 00000000000..d808df57904 --- /dev/null +++ b/packages/apps-engine/src/server/bridges/ExperimentalBridge.ts @@ -0,0 +1,40 @@ +import { BaseBridge } from './BaseBridge'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +/** + * @description + * Experimental bridge for experimental features. + * Methods in this class are not guaranteed to be stable between updates as the + * team evaluates the proper signature, underlying implementation and performance + * impact of candidates for future APIs + */ +export abstract class ExperimentalBridge extends BaseBridge { + /** + * + * Candidate bridge: User bridge + */ + public async doGetUserRoomIds(userId: string, appId: string): Promise { + if (this.hasPermission('getUserRoomIds', appId)) { + return this.getUserRoomIds(userId, appId); + } + } + + protected abstract getUserRoomIds(userId: string, appId: string): Promise; + + private hasPermission(feature: keyof typeof AppPermissions.experimental, appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.experimental[feature])) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.experimental[feature]], + }), + ); + + return false; + } +} diff --git a/packages/apps-engine/src/server/bridges/index.ts b/packages/apps-engine/src/server/bridges/index.ts index fc9cb2b7773..c472ad2293a 100644 --- a/packages/apps-engine/src/server/bridges/index.ts +++ b/packages/apps-engine/src/server/bridges/index.ts @@ -7,6 +7,7 @@ import { CommandBridge } from './CommandBridge'; import { ContactBridge } from './ContactBridge'; import { EmailBridge } from './EmailBridge'; import { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; +import { ExperimentalBridge } from './ExperimentalBridge'; import { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; import { IInternalBridge } from './IInternalBridge'; import { IInternalFederationBridge } from './IInternalFederationBridge'; @@ -45,6 +46,7 @@ export { UserBridge, UploadBridge, EmailBridge, + ExperimentalBridge, UiInteractionBridge, SchedulerBridge, AppBridges, diff --git a/packages/apps-engine/src/server/managers/AppAccessorManager.ts b/packages/apps-engine/src/server/managers/AppAccessorManager.ts index 36a9200a27d..9247d83195c 100644 --- a/packages/apps-engine/src/server/managers/AppAccessorManager.ts +++ b/packages/apps-engine/src/server/managers/AppAccessorManager.ts @@ -48,6 +48,7 @@ import { } from '../accessors'; import { CloudWorkspaceRead } from '../accessors/CloudWorkspaceRead'; import { ContactRead } from '../accessors/ContactRead'; +import { ExperimentalRead } from '../accessors/ExperimentalRead'; import { ThreadRead } from '../accessors/ThreadRead'; import { UIExtend } from '../accessors/UIExtend'; import type { AppBridges } from '../bridges/AppBridges'; @@ -188,12 +189,28 @@ export class AppAccessorManager { const oauthApps = new OAuthAppsReader(this.bridges.getOAuthAppsBridge(), appId); const contactReader = new ContactRead(this.bridges, appId); const thread = new ThreadRead(this.bridges.getThreadBridge(), appId); - const role = new RoleRead(this.bridges.getRoleBridge(), appId); + const experimental = new ExperimentalRead(this.bridges.getExperimentalBridge(), appId); this.readers.set( appId, - new Reader(env, msg, persist, room, user, noti, livechat, upload, cloud, videoConf, contactReader, oauthApps, thread, role), + new Reader( + env, + msg, + persist, + room, + user, + noti, + livechat, + upload, + cloud, + videoConf, + contactReader, + oauthApps, + thread, + role, + experimental, + ), ); } diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index 04e4acfa061..92e6c518e3e 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -122,6 +122,9 @@ export const AppPermissions = { 'outboundComms': { provide: { name: 'outbound-communication.provide' }, }, + 'experimental': { + getUserRoomIds: { name: 'experimental.getUserRoomIds' }, + }, }; /** diff --git a/packages/apps-engine/tests/server/accessors/Reader.spec.ts b/packages/apps-engine/tests/server/accessors/Reader.spec.ts index 5b159627ce7..2e5fadec77f 100644 --- a/packages/apps-engine/tests/server/accessors/Reader.spec.ts +++ b/packages/apps-engine/tests/server/accessors/Reader.spec.ts @@ -3,6 +3,7 @@ import { Expect, SetupFixture, Test } from 'alsatian'; import type { ICloudWorkspaceRead, IEnvironmentRead, + IExperimentalRead, ILivechatRead, IMessageRead, INotifier, @@ -47,6 +48,8 @@ export class ReaderAccessorTestFixture { private contact: IContactRead; + private experimental: IExperimentalRead; + @SetupFixture public setupFixture() { this.env = {} as IEnvironmentRead; @@ -63,6 +66,7 @@ export class ReaderAccessorTestFixture { this.thread = {} as IThreadRead; this.role = {} as IRoleRead; this.contact = {} as IContactRead; + this.experimental = {} as IExperimentalRead; } @Test() @@ -84,6 +88,7 @@ export class ReaderAccessorTestFixture { this.oauthApps, this.thread, this.role, + this.experimental, ), ).not.toThrow(); @@ -102,6 +107,7 @@ export class ReaderAccessorTestFixture { this.oauthApps, this.thread, this.role, + this.experimental, ); Expect(rd.getEnvironmentReader()).toBeDefined(); diff --git a/packages/apps-engine/tests/test-data/bridges/appBridges.ts b/packages/apps-engine/tests/test-data/bridges/appBridges.ts index 2087acd4188..b2a35e6aac8 100644 --- a/packages/apps-engine/tests/test-data/bridges/appBridges.ts +++ b/packages/apps-engine/tests/test-data/bridges/appBridges.ts @@ -7,6 +7,7 @@ import { TestsCommandBridge } from './commandBridge'; import { TestContactBridge } from './contactBridge'; import { TestsEmailBridge } from './emailBridge'; import { TestsEnvironmentalVariableBridge } from './environmentalVariableBridge'; +import { TestExperimentalBridge } from './experimentalBridge'; import { TestsHttpBridge } from './httpBridge'; import { TestsInternalBridge } from './internalBridge'; import { TestsInternalFederationBridge } from './internalFederationBridge'; @@ -30,6 +31,7 @@ import type { AppDetailChangesBridge, ContactBridge, EnvironmentalVariableBridge, + ExperimentalBridge, HttpBridge, IInternalBridge, IListenerBridge, @@ -106,6 +108,8 @@ export class TestsAppBridges extends AppBridges { private readonly outboundCommsBridge: TestOutboundCommunicationBridge; + private readonly experimentalBridge: TestExperimentalBridge; + constructor() { super(); this.appDetails = new TestsAppDetailChangesBridge(); @@ -134,6 +138,7 @@ export class TestsAppBridges extends AppBridges { this.emailBridge = new TestsEmailBridge(); this.contactBridge = new TestContactBridge(); this.outboundCommsBridge = new TestOutboundCommunicationBridge(); + this.experimentalBridge = new TestExperimentalBridge(); } public getCommandBridge(): TestsCommandBridge { @@ -243,4 +248,8 @@ export class TestsAppBridges extends AppBridges { public getOutboundMessageBridge(): OutboundMessageBridge { return this.outboundCommsBridge; } + + public getExperimentalBridge(): ExperimentalBridge { + return this.experimentalBridge; + } } diff --git a/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts b/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts new file mode 100644 index 00000000000..350411e2089 --- /dev/null +++ b/packages/apps-engine/tests/test-data/bridges/experimentalBridge.ts @@ -0,0 +1,7 @@ +import { ExperimentalBridge } from '../../../src/server/bridges'; + +export class TestExperimentalBridge extends ExperimentalBridge { + protected getUserRoomIds(userId: string, appId: string): Promise { + throw new Error('Method not implemented.'); + } +}