diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js index 2e9def73758..480ca170f3d 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.js @@ -16,6 +16,7 @@ import { AppLivechatBridge } from './livechat'; import { AppMessageBridge } from './messages'; import { AppModerationBridge } from './moderation'; import { AppOAuthAppsBridge } from './oauthApps'; +import { OutboundCommunicationBridge } from './outboundCommunication'; import { AppPersistenceBridge } from './persistence'; import { AppRoleBridge } from './roles'; import { AppRoomBridge } from './rooms'; @@ -57,6 +58,7 @@ export class RealAppBridges extends AppBridges { this._roleBridge = new AppRoleBridge(orch); this._emailBridge = new AppEmailBridge(orch); this._contactBridge = new AppContactBridge(orch); + this._outboundMessageBridge = new OutboundCommunicationBridge(orch); } getCommandBridge() { @@ -139,6 +141,10 @@ export class RealAppBridges extends AppBridges { return this._videoConfBridge; } + getOutboundMessageBridge() { + return this._outboundMessageBridge; + } + getOAuthAppsBridge() { return this._oAuthBridge; } diff --git a/apps/meteor/app/apps/server/bridges/outboundCommunication.ts b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts new file mode 100644 index 00000000000..ba7e3aeb9eb --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/outboundCommunication.ts @@ -0,0 +1,45 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { + IOutboundEmailMessageProvider, + IOutboundMessageProviders, + IOutboundPhoneMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundComunication'; +import { OutboundMessageBridge } from '@rocket.chat/apps-engine/server/bridges'; + +import { getOutboundService } from '../../../livechat/server/lib/outboundcommunication'; + +export class OutboundCommunicationBridge extends OutboundMessageBridge { + constructor(private readonly orch: IAppServerOrchestrator) { + super(); + } + + protected async registerPhoneProvider(provider: IOutboundPhoneMessageProvider, appId: string): Promise { + try { + this.orch.debugLog(`App ${appId} is registering a phone outbound provider.`); + getOutboundService().outboundMessageProvider.registerPhoneProvider(provider); + } catch (err) { + this.orch.getRocketChatLogger().error({ appId, err, msg: 'Failed to register phone provider' }); + throw new Error('error-registering-provider'); + } + } + + protected async registerEmailProvider(provider: IOutboundEmailMessageProvider, appId: string): Promise { + try { + this.orch.debugLog(`App ${appId} is registering an email outbound provider.`); + getOutboundService().outboundMessageProvider.registerEmailProvider(provider); + } catch (err) { + this.orch.getRocketChatLogger().error({ appId, err, msg: 'Failed to register email provider' }); + throw new Error('error-registering-provider'); + } + } + + protected async unRegisterProvider(provider: IOutboundMessageProviders, appId: string): Promise { + try { + this.orch.debugLog(`App ${appId} is unregistering an outbound provider.`); + getOutboundService().outboundMessageProvider.unregisterProvider(appId, provider.type); + } catch (err) { + this.orch.getRocketChatLogger().error({ appId, err, msg: 'Failed to unregister provider' }); + throw new Error('error-unregistering-provider'); + } + } +} diff --git a/apps/meteor/app/livechat/server/lib/outboundcommunication.ts b/apps/meteor/app/livechat/server/lib/outboundcommunication.ts new file mode 100644 index 00000000000..238bdfd459d --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/outboundcommunication.ts @@ -0,0 +1,6 @@ +import type { IOutboundMessageProviderService } from '@rocket.chat/core-typings'; +import { makeFunction } from '@rocket.chat/patch-injection'; + +export const getOutboundService = makeFunction((): IOutboundMessageProviderService => { + throw new Error('error-no-license'); +}); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts index 7884ff3e612..12f8c06b888 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts @@ -1,15 +1,26 @@ -import type { IOutboundProvider, ValidOutboundProvider } from '@rocket.chat/core-typings'; +import { Apps } from '@rocket.chat/apps'; +import type { + IOutboundProvider, + ValidOutboundProvider, + IOutboundMessageProviderService, + IOutboundProviderMetadata, +} from '@rocket.chat/core-typings'; import { ValidOutboundProviderList } from '@rocket.chat/core-typings'; +import { getOutboundService } from '../../../../../../app/livechat/server/lib/outboundcommunication'; import { OutboundMessageProvider } from '../../../../../../server/lib/OutboundMessageProvider'; -export class OutboundMessageProviderService { +export class OutboundMessageProviderService implements IOutboundMessageProviderService { private readonly provider: OutboundMessageProvider; constructor() { this.provider = new OutboundMessageProvider(); } + get outboundMessageProvider() { + return this.provider; + } + private isProviderValid(type: any): type is ValidOutboundProvider { return ValidOutboundProviderList.includes(type); } @@ -21,6 +32,41 @@ export class OutboundMessageProviderService { return this.provider.getOutboundMessageProviders(type); } + + public getProviderMetadata(providerId: string): Promise { + const provider = this.provider.findOneByProviderId(providerId); + if (!provider) { + throw new Error('error-invalid-provider'); + } + + return this.getProviderManager().getProviderMetadata(provider.appId, provider.type); + } + + private getProviderManager() { + if (!Apps.self?.isLoaded()) { + throw new Error('apps-engine-not-loaded'); + } + + const manager = Apps.self?.getManager()?.getOutboundCommunicationProviderManager(); + if (!manager) { + throw new Error('apps-engine-not-configured-correctly'); + } + + return manager; + } + + public sendMessage(providerId: string, body: any) { + const provider = this.provider.findOneByProviderId(providerId); + if (!provider) { + throw new Error('error-invalid-provider'); + } + + return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, body); + } } export const outboundMessageProvider = new OutboundMessageProviderService(); + +getOutboundService.patch(() => { + return outboundMessageProvider; +}); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts index 5416f8a2f99..12ce248e4b0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts @@ -30,6 +30,9 @@ const outboundCommsEndpoints = API.v1.get( providerType: { type: 'string', }, + documentationUrl: { + type: 'string', + }, }, }, }, diff --git a/apps/meteor/server/lib/OutboundMessageProvider.ts b/apps/meteor/server/lib/OutboundMessageProvider.ts index 8c8a0c380d0..22773b157b0 100644 --- a/apps/meteor/server/lib/OutboundMessageProvider.ts +++ b/apps/meteor/server/lib/OutboundMessageProvider.ts @@ -3,14 +3,7 @@ import type { IOutboundMessageProviders, IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundComunication'; -import type { ValidOutboundProvider, IOutboundProvider } from '@rocket.chat/core-typings'; - -interface IOutboundMessageProvider { - registerPhoneProvider(provider: IOutboundPhoneMessageProvider): void; - registerEmailProvider(provider: IOutboundEmailMessageProvider): void; - getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[]; - unregisterProvider(appId: string, providerType: string): void; -} +import type { ValidOutboundProvider, IOutboundProvider, IOutboundMessageProvider } from '@rocket.chat/core-typings'; export class OutboundMessageProvider implements IOutboundMessageProvider { private readonly outboundMessageProviders: Map; @@ -22,6 +15,17 @@ export class OutboundMessageProvider implements IOutboundMessageProvider { ]); } + public findOneByProviderId(providerId: string) { + for (const providers of this.outboundMessageProviders.values()) { + for (const provider of providers) { + if (provider.appId === providerId) { + return provider; + } + } + } + return undefined; + } + public registerPhoneProvider(provider: IOutboundPhoneMessageProvider): void { this.outboundMessageProviders.set('phone', [...(this.outboundMessageProviders.get('phone') || []), provider]); } @@ -36,6 +40,7 @@ export class OutboundMessageProvider implements IOutboundMessageProvider { providerId: provider.appId, providerName: provider.name, providerType: provider.type, + ...(provider.documentationUrl && { documentationUrl: provider.documentationUrl }), ...(provider.supportsTemplates && { supportsTemplates: provider.supportsTemplates }), })); } diff --git a/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts b/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts new file mode 100644 index 00000000000..b701eb25ca6 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/outboundcomms-handler.ts @@ -0,0 +1,32 @@ +import { JsonRpcError, Defined } from 'jsonrpc-lite'; +import type { IOutboundMessageProviders } from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { Logger } from '../lib/logger.ts'; + +export default async function outboundMessageHandler(call: string, params: unknown): Promise { + const [, providerName, methodName] = call.split(':'); + const provider = AppObjectRegistry.get(`outboundCommunication:${providerName}`); + if (!provider) { + return new JsonRpcError('error-invalid-provider', -32000); + } + const method = provider[methodName as keyof IOutboundMessageProviders]; + const logger = AppObjectRegistry.get('logger'); + const args = (params as Array) ?? []; + + try { + logger?.debug(`Executing ${methodName} on outbound communication provider...`); + + // deno-lint-ignore ban-types + return await (method as Function).apply(provider, [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + } catch (e) { + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts index 01a42b31e67..afb661a31d0 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts @@ -13,6 +13,10 @@ import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcom import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; +import type { + IOutboundPhoneMessageProvider, + IOutboundEmailMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundCommunication/IOutboundCommsProvider.ts'; import { Http } from './http.ts'; import { HttpExtend } from './extenders/HttpExtender.ts'; @@ -188,6 +192,17 @@ export class AppAccessors { return this._proxy.provideVideoConfProvider(provider); }, }, + outboundCommunication: { + _proxy: this.proxify('getConfigurationExtend:outboundCommunication'), + registerEmailProvider(provider: IOutboundEmailMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerEmailProvider(provider); + }, + registerPhoneProvider(provider: IOutboundPhoneMessageProvider) { + AppObjectRegistry.set(`outboundCommunication:${provider.name}-${provider.type}`, provider); + return this._proxy.registerPhoneProvider(provider); + }, + }, slashCommands: { _proxy: this.proxify('getConfigurationExtend:slashCommands'), provideSlashCommand(slashcommand: ISlashCommand) { diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts index 2debc6f8005..25a8228066c 100644 --- a/packages/apps-engine/deno-runtime/main.ts +++ b/packages/apps-engine/deno-runtime/main.ts @@ -23,12 +23,14 @@ import handleApp from './handlers/app/handler.ts'; import handleScheduler from './handlers/scheduler-handler.ts'; import registerErrorListeners from './error-handlers.ts'; import { sendMetrics } from './lib/metricsCollector.ts'; +import outboundMessageHandler from './handlers/outboundcomms-handler.ts'; type Handlers = { app: typeof handleApp; api: typeof apiHandler; slashcommand: typeof slashcommandHandler; videoconference: typeof videoConferenceHandler; + outboundCommunication: typeof outboundMessageHandler; scheduler: typeof handleScheduler; ping: (method: string, params: unknown) => 'pong'; }; @@ -41,6 +43,7 @@ async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promi api: apiHandler, slashcommand: slashcommandHandler, videoconference: videoConferenceHandler, + outboundCommunication: outboundMessageHandler, scheduler: handleScheduler, ping: (_method, _params) => 'pong', }; diff --git a/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts index c6390bdee20..88244c0fba8 100644 --- a/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts +++ b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts @@ -1,6 +1,7 @@ import type { IApiExtend } from './IApiExtend'; import type { IExternalComponentsExtend } from './IExternalComponentsExtend'; import type { IHttpExtend } from './IHttp'; +import type { IOutboundCommunicationProviderExtend } from './IOutboundCommunicationProviderExtend'; import type { ISchedulerExtend } from './ISchedulerExtend'; import type { ISettingsExtend } from './ISettingsExtend'; import type { ISlashCommandsExtend } from './ISlashCommandsExtend'; @@ -33,4 +34,7 @@ export interface IConfigurationExtend { /** Accessor for declaring the videoconf providers which your App provides. */ readonly videoConfProviders: IVideoConfProvidersExtend; + + /** Accessor for declaring outbound communication providers */ + readonly outboundCommunication: IOutboundCommunicationProviderExtend; } diff --git a/packages/apps-engine/src/definition/accessors/IOutboundCommunicationProviderExtend.ts b/packages/apps-engine/src/definition/accessors/IOutboundCommunicationProviderExtend.ts new file mode 100644 index 00000000000..1ce6b8ded70 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOutboundCommunicationProviderExtend.ts @@ -0,0 +1,6 @@ +import type { IOutboundEmailMessageProvider, IOutboundPhoneMessageProvider } from '../outboundComunication'; + +export interface IOutboundCommunicationProviderExtend { + registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise; + registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/index.ts b/packages/apps-engine/src/definition/accessors/index.ts index e98a4208fe1..3618dcc9208 100644 --- a/packages/apps-engine/src/definition/accessors/index.ts +++ b/packages/apps-engine/src/definition/accessors/index.ts @@ -56,3 +56,4 @@ export * from './IVideoConferenceExtend'; export * from './IVideoConferenceRead'; export * from './IVideoConfProvidersExtend'; export * from './IModerationModify'; +export * from './IOutboundCommunicationProviderExtend'; diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts index af40d4b5144..9b4e288e5ce 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts @@ -1,9 +1,9 @@ import type { IOutboundMessage } from './IOutboundMessage'; import type { IOutboundProviderTemplate } from './IOutboundProviderTemplate'; -type ProviderMetadata = { - appId: string; - appName: string; +export type ProviderMetadata = { + providerId: string; + providerName: string; providerType: 'phone' | 'email'; supportsTemplates: boolean; // Indicates if the provider uses templates or not templates: Record; // Format: { '+1121221212': [{ template }] } @@ -30,3 +30,7 @@ export interface IOutboundEmailMessageProvider extends IOutboundMessageProviderB } export type IOutboundMessageProviders = IOutboundPhoneMessageProvider | IOutboundEmailMessageProvider; + +export const ValidOutboundProviderList = ['phone', 'email'] as const; + +export type ValidOutboundProvider = (typeof ValidOutboundProviderList)[number]; diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index eb8a9ee1880..591594788a5 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -26,6 +26,7 @@ import { AppSlashCommandManager, AppVideoConfProviderManager, } from './managers'; +import { AppOutboundCommunicationProviderManager } from './managers/AppOutboundCommunicationProviderManager'; import { AppRuntimeManager } from './managers/AppRuntimeManager'; import { AppSignatureManager } from './managers/AppSignatureManager'; import { UIActionButtonManager } from './managers/UIActionButtonManager'; @@ -97,6 +98,8 @@ export class AppManager { private readonly videoConfProviderManager: AppVideoConfProviderManager; + private readonly outboundCommunicationProviderManager: AppOutboundCommunicationProviderManager; + private readonly signatureManager: AppSignatureManager; private readonly runtime: AppRuntimeManager; @@ -147,6 +150,7 @@ export class AppManager { this.schedulerManager = new AppSchedulerManager(this); this.uiActionButtonManager = new UIActionButtonManager(this); this.videoConfProviderManager = new AppVideoConfProviderManager(this); + this.outboundCommunicationProviderManager = new AppOutboundCommunicationProviderManager(this); this.signatureManager = new AppSignatureManager(this); this.runtime = new AppRuntimeManager(this); @@ -198,6 +202,10 @@ export class AppManager { return this.videoConfProviderManager; } + public getOutboundCommunicationProviderManager(): AppOutboundCommunicationProviderManager { + return this.outboundCommunicationProviderManager; + } + public getLicenseManager(): AppLicenseManager { return this.licenseManager; } @@ -1075,6 +1083,7 @@ export class AppManager { this.accessorManager.purifyApp(app.getID()); this.uiActionButtonManager.clearAppActionButtons(app.getID()); this.videoConfProviderManager.unregisterProviders(app.getID()); + await this.outboundCommunicationProviderManager.unregisterProviders(app.getID()); } /** @@ -1148,6 +1157,7 @@ export class AppManager { this.listenerManager.registerListeners(app); this.listenerManager.releaseEssentialEvents(app); this.videoConfProviderManager.registerProviders(app.getID()); + await this.outboundCommunicationProviderManager.registerProviders(app.getID()); } else { await this.purgeAppConfig(app); } diff --git a/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts b/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts index fb32b28917b..92bbb2a69ac 100644 --- a/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts +++ b/packages/apps-engine/src/server/accessors/ConfigurationExtend.ts @@ -8,6 +8,7 @@ import type { ISlashCommandsExtend, IUIExtend, IVideoConfProvidersExtend, + IOutboundCommunicationProviderExtend, } from '../../definition/accessors'; export class ConfigurationExtend implements IConfigurationExtend { @@ -20,5 +21,6 @@ export class ConfigurationExtend implements IConfigurationExtend { public readonly scheduler: ISchedulerExtend, public readonly ui: IUIExtend, public readonly videoConfProviders: IVideoConfProvidersExtend, + public readonly outboundCommunication: IOutboundCommunicationProviderExtend, ) {} } diff --git a/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts b/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts new file mode 100644 index 00000000000..d15e9262441 --- /dev/null +++ b/packages/apps-engine/src/server/accessors/OutboundCommunicationProviderExtend.ts @@ -0,0 +1,18 @@ +import type { IOutboundCommunicationProviderExtend } from '../../definition/accessors/IOutboundCommunicationProviderExtend'; +import type { IOutboundPhoneMessageProvider, IOutboundEmailMessageProvider } from '../../definition/outboundComunication'; +import type { AppOutboundCommunicationProviderManager } from '../managers/AppOutboundCommunicationProviderManager'; + +export class OutboundMessageProviderExtend implements IOutboundCommunicationProviderExtend { + constructor( + private readonly manager: AppOutboundCommunicationProviderManager, + private readonly appId: string, + ) {} + + public registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } + + public registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise { + return Promise.resolve(this.manager.addProvider(this.appId, provider)); + } +} diff --git a/packages/apps-engine/src/server/accessors/index.ts b/packages/apps-engine/src/server/accessors/index.ts index 150c0701c07..eb5cbdc0218 100644 --- a/packages/apps-engine/src/server/accessors/index.ts +++ b/packages/apps-engine/src/server/accessors/index.ts @@ -20,6 +20,7 @@ import { ModifyUpdater } from './ModifyUpdater'; import { Notifier } from './Notifier'; import { OAuthAppsModify } from './OAuthAppsModify'; import { OAuthAppsReader } from './OAuthAppsReader'; +import { OutboundMessageProviderExtend } from './OutboundCommunicationProviderExtend'; import { Persistence } from './Persistence'; import { PersistenceRead } from './PersistenceRead'; import { Reader } from './Reader'; @@ -92,4 +93,5 @@ export { VideoConfProviderExtend, OAuthAppsModify, OAuthAppsReader, + OutboundMessageProviderExtend, }; diff --git a/packages/apps-engine/src/server/bridges/AppBridges.ts b/packages/apps-engine/src/server/bridges/AppBridges.ts index c26b58d1d77..66d047e15c1 100644 --- a/packages/apps-engine/src/server/bridges/AppBridges.ts +++ b/packages/apps-engine/src/server/bridges/AppBridges.ts @@ -14,6 +14,7 @@ import type { LivechatBridge } from './LivechatBridge'; import type { MessageBridge } from './MessageBridge'; import type { ModerationBridge } from './ModerationBridge'; import type { OAuthAppsBridge } from './OAuthAppsBridge'; +import type { OutboundMessageBridge } from './OutboundMessagesBridge'; import type { PersistenceBridge } from './PersistenceBridge'; import type { RoleBridge } from './RoleBridge'; import type { RoomBridge } from './RoomBridge'; @@ -48,7 +49,8 @@ export type Bridge = | VideoConferenceBridge | OAuthAppsBridge | ModerationBridge - | RoleBridge; + | RoleBridge + | OutboundMessageBridge; export abstract class AppBridges { public abstract getCommandBridge(): CommandBridge; @@ -102,4 +104,6 @@ export abstract class AppBridges { public abstract getThreadBridge(): ThreadBridge; public abstract getRoleBridge(): RoleBridge; + + public abstract getOutboundMessageBridge(): OutboundMessageBridge; } diff --git a/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts b/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts new file mode 100644 index 00000000000..a0772522aa6 --- /dev/null +++ b/packages/apps-engine/src/server/bridges/OutboundMessagesBridge.ts @@ -0,0 +1,50 @@ +import { BaseBridge } from './BaseBridge'; +import type { + IOutboundEmailMessageProvider, + IOutboundMessageProviders, + IOutboundPhoneMessageProvider, +} from '../../definition/outboundComunication'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissionManager } from '../managers/AppPermissionManager'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export abstract class OutboundMessageBridge extends BaseBridge { + public async doRegisterPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.registerPhoneProvider(info, appId); + } + } + + public async doRegisterEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.registerEmailProvider(info, appId); + } + } + + public async doUnRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise { + if (this.hasProviderPermission(appId)) { + return this.unRegisterProvider(info, appId); + } + } + + private hasProviderPermission(appId: string): boolean { + if (AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { + return true; + } + + AppPermissionManager.notifyAboutError( + new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.outboundComms.provide], + }), + ); + + return false; + } + + protected abstract registerPhoneProvider(info: IOutboundPhoneMessageProvider, appId: string): Promise; + + protected abstract registerEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise; + + protected abstract unRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise; +} diff --git a/packages/apps-engine/src/server/bridges/index.ts b/packages/apps-engine/src/server/bridges/index.ts index 409489a0476..fc9cb2b7773 100644 --- a/packages/apps-engine/src/server/bridges/index.ts +++ b/packages/apps-engine/src/server/bridges/index.ts @@ -14,6 +14,7 @@ import { IListenerBridge } from './IListenerBridge'; import { LivechatBridge } from './LivechatBridge'; import { MessageBridge } from './MessageBridge'; import { ModerationBridge } from './ModerationBridge'; +import { OutboundMessageBridge } from './OutboundMessagesBridge'; import { PersistenceBridge } from './PersistenceBridge'; import { RoleBridge } from './RoleBridge'; import { RoomBridge } from './RoomBridge'; @@ -51,4 +52,5 @@ export { IInternalFederationBridge, ModerationBridge, RoleBridge, + OutboundMessageBridge, }; diff --git a/packages/apps-engine/src/server/managers/AppAccessorManager.ts b/packages/apps-engine/src/server/managers/AppAccessorManager.ts index 17927e9cb86..36a9200a27d 100644 --- a/packages/apps-engine/src/server/managers/AppAccessorManager.ts +++ b/packages/apps-engine/src/server/managers/AppAccessorManager.ts @@ -25,6 +25,7 @@ import { Modify, Notifier, OAuthAppsReader, + OutboundMessageProviderExtend, Persistence, PersistenceRead, Reader, @@ -114,8 +115,9 @@ export class AppAccessorManager { const excs = new ExternalComponentsExtend(this.manager.getExternalComponentManager(), appId); const scheduler = new SchedulerExtend(this.manager.getSchedulerManager(), appId); const ui = new UIExtend(this.manager.getUIActionButtonManager(), appId); + const outboundComms = new OutboundMessageProviderExtend(this.manager.getOutboundCommunicationProviderManager(), appId); - this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf)); + this.configExtenders.set(appId, new ConfigurationExtend(htt, sets, cmds, apis, excs, scheduler, ui, videoConf, outboundComms)); } return this.configExtenders.get(appId); diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts new file mode 100644 index 00000000000..dc7495785d9 --- /dev/null +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts @@ -0,0 +1,47 @@ +import type { AppAccessorManager } from '.'; +import { AppMethod } from '../../definition/metadata'; +import type { IOutboundMessageProviders, ProviderMetadata } from '../../definition/outboundComunication'; +import type { ProxiedApp } from '../ProxiedApp'; +import type { AppLogStorage } from '../storage'; + +export class OutboundMessageProvider { + public isRegistered: boolean; + + constructor( + public app: ProxiedApp, + public provider: IOutboundMessageProviders, + ) { + this.isRegistered = false; + } + + public async runGetProviderMetadata(logStorage: AppLogStorage, accessors: AppAccessorManager): Promise { + return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); + } + + public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: any): Promise { + await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); + } + + private async runTheCode( + method: AppMethod._OUTBOUND_GET_PROVIDER_METADATA | AppMethod._OUTBOUND_SEND_MESSAGE, + logStorage: AppLogStorage, + accessors: AppAccessorManager, + runContextArgs: Array, + ): Promise { + const provider = `${this.provider.name}-${this.provider.type}`; + + try { + const result = await this.app.getDenoRuntime().sendRequest({ + method: `outboundCommunication:${provider}:${method}`, + params: runContextArgs, + }); + + return result as T; + } catch (e) { + if (e?.message === 'error-invalid-provider') { + throw new Error('error-provider-not-registered'); + } + console.error(e); + } + } +} diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts new file mode 100644 index 00000000000..e5fa8a4077f --- /dev/null +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts @@ -0,0 +1,130 @@ +import type { AppAccessorManager } from '.'; +import type { + IOutboundMessageProviders, + IOutboundEmailMessageProvider, + IOutboundPhoneMessageProvider, + ValidOutboundProvider, +} from '../../definition/outboundComunication'; +import type { AppManager } from '../AppManager'; +import type { OutboundMessageBridge } from '../bridges'; +import { OutboundMessageProvider } from './AppOutboundCommunicationProvider'; +import { AppPermissionManager } from './AppPermissionManager'; +import { PermissionDeniedError } from '../errors/PermissionDeniedError'; +import { AppPermissions } from '../permissions/AppPermissions'; + +export class AppOutboundCommunicationProviderManager { + private readonly accessors: AppAccessorManager; + + private readonly bridge: OutboundMessageBridge; + + private outboundMessageProviders: Map>; + + constructor(private readonly manager: AppManager) { + this.bridge = this.manager.getBridges().getOutboundMessageBridge(); + this.accessors = this.manager.getAccessorManager(); + + this.outboundMessageProviders = new Map>(); + } + + public isAlreadyDefined(providerId: string, providerType: ValidOutboundProvider): boolean { + const providersByApp = this.outboundMessageProviders.get(providerId); + if (!providersByApp) { + return false; + } + if (!providersByApp.get(providerType)) { + return false; + } + return true; + } + + public addProvider(appId: string, provider: IOutboundMessageProviders): void { + const app = this.manager.getOneById(appId); + if (!app) { + throw new Error('App must exist in order for an outbound provider to be added.'); + } + + if (!AppPermissionManager.hasPermission(appId, AppPermissions.outboundComms.provide)) { + throw new PermissionDeniedError({ + appId, + missingPermissions: [AppPermissions.outboundComms.provide], + }); + } + + if (!this.outboundMessageProviders.has(appId)) { + this.outboundMessageProviders.set(appId, new Map()); + } + + this.outboundMessageProviders.get(appId).set(provider.type, new OutboundMessageProvider(app, provider)); + } + + public async registerProviders(appId: string): Promise { + if (!this.outboundMessageProviders.has(appId)) { + return; + } + + const appProviders = this.outboundMessageProviders.get(appId); + if (!appProviders) { + return; + } + + for await (const [, providerInfo] of appProviders) { + if (providerInfo.provider.type === 'phone') { + await this.registerPhoneProvider(appId, providerInfo.provider); + } else if (providerInfo.provider.type === 'email') { + await this.registerEmailProvider(appId, providerInfo.provider); + } + } + } + + public async unregisterProviders(appId: string): Promise { + if (!this.outboundMessageProviders.has(appId)) { + return; + } + + const appProviders = this.outboundMessageProviders.get(appId); + for await (const [, providerInfo] of appProviders) { + await this.unregisterProvider(appId, providerInfo); + } + + this.outboundMessageProviders.delete(appId); + } + + private registerPhoneProvider(appId: string, provider: IOutboundPhoneMessageProvider): Promise { + return this.bridge.doRegisterPhoneProvider(provider, appId); + } + + private registerEmailProvider(appId: string, provider: IOutboundEmailMessageProvider): Promise { + return this.bridge.doRegisterEmailProvider(provider, appId); + } + + private async unregisterProvider(appId: string, info: OutboundMessageProvider): Promise { + const key = info.provider.type; + + await this.bridge.doUnRegisterProvider(info.provider, appId); + + info.isRegistered = false; + + const map = this.outboundMessageProviders.get(appId); + if (map) { + map.delete(key); + } + } + + public getProviderMetadata(appId: string, providerType: ValidOutboundProvider) { + const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); + if (!providerInfo) { + throw new Error('provider-not-registered'); + } + + return providerInfo.runGetProviderMetadata(this.manager.getLogStorage(), this.accessors); + } + + public sendOutboundMessage(appId: string, providerType: ValidOutboundProvider, body: unknown) { + const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); + if (!providerInfo) { + throw new Error('provider-not-registered'); + } + + return providerInfo.runSendOutboundMessage(this.manager.getLogStorage(), this.accessors, body); + } +} diff --git a/packages/apps-engine/src/server/managers/index.ts b/packages/apps-engine/src/server/managers/index.ts index e47a542f9c0..9d7b22c79bc 100644 --- a/packages/apps-engine/src/server/managers/index.ts +++ b/packages/apps-engine/src/server/managers/index.ts @@ -3,6 +3,7 @@ import { AppApiManager } from './AppApiManager'; import { AppExternalComponentManager } from './AppExternalComponentManager'; import { AppLicenseManager } from './AppLicenseManager'; import { AppListenerManager } from './AppListenerManager'; +import { AppOutboundCommunicationProviderManager } from './AppOutboundCommunicationProviderManager'; import { AppSchedulerManager } from './AppSchedulerManager'; import { AppSettingsManager } from './AppSettingsManager'; import { AppSlashCommandManager } from './AppSlashCommandManager'; @@ -18,4 +19,5 @@ export { AppApiManager, AppSchedulerManager, AppVideoConfProviderManager, + AppOutboundCommunicationProviderManager, }; diff --git a/packages/apps-engine/src/server/permissions/AppPermissions.ts b/packages/apps-engine/src/server/permissions/AppPermissions.ts index 62c4001dd09..04e4acfa061 100644 --- a/packages/apps-engine/src/server/permissions/AppPermissions.ts +++ b/packages/apps-engine/src/server/permissions/AppPermissions.ts @@ -119,6 +119,9 @@ export const AppPermissions = { read: { name: 'oauth-app.read' }, write: { name: 'oauth-app.write' }, }, + 'outboundComms': { + provide: { name: 'outbound-communication.provide' }, + }, }; /** diff --git a/packages/apps-engine/tests/server/AppManager.spec.ts b/packages/apps-engine/tests/server/AppManager.spec.ts index a87555e1bda..1f6ef3e4a18 100644 --- a/packages/apps-engine/tests/server/AppManager.spec.ts +++ b/packages/apps-engine/tests/server/AppManager.spec.ts @@ -11,6 +11,7 @@ import { AppSettingsManager, AppSlashCommandManager, AppVideoConfProviderManager, + AppOutboundCommunicationProviderManager, } from '../../src/server/managers'; import type { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage'; import { SimpleClass, TestInfastructureSetup } from '../test-data/utilities'; @@ -118,5 +119,6 @@ export class AppManagerTestFixture { Expect(manager.getApiManager() instanceof AppApiManager).toBe(true); Expect(manager.getSettingsManager() instanceof AppSettingsManager).toBe(true); Expect(manager.getVideoConfProviderManager() instanceof AppVideoConfProviderManager).toBe(true); + Expect(manager.getOutboundCommunicationProviderManager() instanceof AppOutboundCommunicationProviderManager).toBe(true); } } diff --git a/packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts b/packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts index a5ceeb411e8..a861df1d878 100644 --- a/packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts +++ b/packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts @@ -1,5 +1,6 @@ import { Expect, Setup, SetupFixture, Test } from 'alsatian'; +import type { AppOutboundCommunicationProviderManager } from '../../../server/managers/AppOutboundCommunicationProviderManager'; import { AppStatus } from '../../../src/definition/AppStatus'; import type { AppMethod } from '../../../src/definition/metadata'; import type { AppManager } from '../../../src/server/AppManager'; @@ -84,7 +85,10 @@ export class AppAccessorsTestFixture { getSettingsManager() { return {} as AppSettingsManager; }, - } as AppManager; + getOutboundCommunicationProviderManager() { + return {} as AppOutboundCommunicationProviderManager; + }, + } as unknown as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); const ac = this.mockAccessors; diff --git a/packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts b/packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts index a10481ad7f7..93b2d13f728 100644 --- a/packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts +++ b/packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts @@ -1,6 +1,7 @@ import type { RestorableFunctionSpy } from 'alsatian'; import { Expect, Setup, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; +import type { AppOutboundCommunicationProviderManager } from '../../../server/managers/AppOutboundCommunicationProviderManager'; import type { AppManager } from '../../../src/server/AppManager'; import type { ProxiedApp } from '../../../src/server/ProxiedApp'; import type { AppBridges } from '../../../src/server/bridges'; @@ -52,7 +53,10 @@ export class AppAccessorManagerTestFixture { getVideoConfProviderManager() { return {} as AppVideoConfProviderManager; }, - } as AppManager; + getOutboundCommunicationProviderManager() { + return {} as AppOutboundCommunicationProviderManager; + }, + } as unknown as AppManager; } @Setup diff --git a/packages/apps-engine/tests/test-data/bridges/appBridges.ts b/packages/apps-engine/tests/test-data/bridges/appBridges.ts index f786fd4d851..2087acd4188 100644 --- a/packages/apps-engine/tests/test-data/bridges/appBridges.ts +++ b/packages/apps-engine/tests/test-data/bridges/appBridges.ts @@ -13,6 +13,7 @@ import { TestsInternalFederationBridge } from './internalFederationBridge'; import { TestLivechatBridge } from './livechatBridge'; import { TestsMessageBridge } from './messageBridge'; import { TestsModerationBridge } from './moderationBridge'; +import { TestOutboundCommunicationBridge } from './outboundComms'; import { TestsPersisBridge } from './persisBridge'; import { TestsRoleBridge } from './roleBridge'; import { TestsRoomBridge } from './roomBridge'; @@ -35,6 +36,7 @@ import type { LivechatBridge, MessageBridge, ModerationBridge, + OutboundMessageBridge, PersistenceBridge, RoleBridge, RoomBridge, @@ -102,6 +104,8 @@ export class TestsAppBridges extends AppBridges { private readonly threadBridge: ThreadBridge; + private readonly outboundCommsBridge: TestOutboundCommunicationBridge; + constructor() { super(); this.appDetails = new TestsAppDetailChangesBridge(); @@ -129,6 +133,7 @@ export class TestsAppBridges extends AppBridges { this.threadBridge = new TestsThreadBridge(); this.emailBridge = new TestsEmailBridge(); this.contactBridge = new TestContactBridge(); + this.outboundCommsBridge = new TestOutboundCommunicationBridge(); } public getCommandBridge(): TestsCommandBridge { @@ -234,4 +239,8 @@ export class TestsAppBridges extends AppBridges { public getContactBridge(): ContactBridge { return this.contactBridge; } + + public getOutboundMessageBridge(): OutboundMessageBridge { + return this.outboundCommsBridge; + } } diff --git a/packages/apps-engine/tests/test-data/bridges/outboundComms.ts b/packages/apps-engine/tests/test-data/bridges/outboundComms.ts new file mode 100644 index 00000000000..4f8b22ebe74 --- /dev/null +++ b/packages/apps-engine/tests/test-data/bridges/outboundComms.ts @@ -0,0 +1,20 @@ +import type { + IOutboundEmailMessageProvider, + IOutboundMessageProviders, + IOutboundPhoneMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundComunication'; +import { OutboundMessageBridge } from '@rocket.chat/apps-engine/server/bridges'; + +export class TestOutboundCommunicationBridge extends OutboundMessageBridge { + protected async registerPhoneProvider(provider: IOutboundPhoneMessageProvider, appId: string): Promise { + return Promise.resolve(); + } + + protected async registerEmailProvider(provider: IOutboundEmailMessageProvider, appId: string): Promise { + return Promise.resolve(); + } + + protected async unRegisterProvider(provider: IOutboundMessageProviders, appId: string): Promise { + return Promise.resolve(); + } +} diff --git a/packages/apps-engine/tests/test-data/utilities.ts b/packages/apps-engine/tests/test-data/utilities.ts index f2e22b57e3b..e69cedca7d5 100644 --- a/packages/apps-engine/tests/test-data/utilities.ts +++ b/packages/apps-engine/tests/test-data/utilities.ts @@ -4,6 +4,7 @@ import { TestsAppBridges } from './bridges/appBridges'; import { TestSourceStorage } from './storage/TestSourceStorage'; import { TestsAppLogStorage } from './storage/logStorage'; import { TestsAppStorage } from './storage/storage'; +import type { AppOutboundCommunicationProviderManager } from '../../server/managers/AppOutboundCommunicationProviderManager'; import { AppStatus } from '../../src/definition/AppStatus'; import type { IHttp, IModify, IPersistence, IRead } from '../../src/definition/accessors'; import { HttpStatusCode } from '../../src/definition/accessors'; @@ -109,6 +110,9 @@ export class TestInfastructureSetup { getVideoConfProviderManager() { return {} as AppVideoConfProviderManager; }, + getOutboundCommunicationProviderManager() { + return {} as AppOutboundCommunicationProviderManager; + }, getSettingsManager() { return {} as AppSettingsManager; }, diff --git a/packages/core-typings/src/omnichannel/outbound.ts b/packages/core-typings/src/omnichannel/outbound.ts index 38b29d9c5ae..afd85f78847 100644 --- a/packages/core-typings/src/omnichannel/outbound.ts +++ b/packages/core-typings/src/omnichannel/outbound.ts @@ -1,3 +1,8 @@ +import type { + IOutboundEmailMessageProvider, + IOutboundPhoneMessageProvider, +} from '@rocket.chat/apps-engine/definition/outboundComunication'; + export interface IOutboundProviderTemplate { id: string; name: string; @@ -115,6 +120,18 @@ export type IOutboundProviderMetadata = IOutboundProvider & { templates: Record; }; +export interface IOutboundMessageProvider { + registerPhoneProvider(provider: IOutboundPhoneMessageProvider): void; + registerEmailProvider(provider: IOutboundEmailMessageProvider): void; + getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[]; + unregisterProvider(appId: string, providerType: string): void; +} + export const ValidOutboundProviderList = ['phone', 'email'] as const; export type ValidOutboundProvider = (typeof ValidOutboundProviderList)[number]; + +export interface IOutboundMessageProviderService { + outboundMessageProvider: IOutboundMessageProvider; + listOutboundProviders(type?: string): IOutboundProvider[]; +}