feat(outbound): Apps engine bridge (#36390)
Co-authored-by: Lucas Pelegrino <16467257+lucas-a-pelegrino@users.noreply.github.com> Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com>vite7^2
parent
0b4f3d3c27
commit
fccb53718c
@ -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<void> { |
||||
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<void> { |
||||
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<void> { |
||||
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'); |
||||
} |
||||
} |
||||
} |
||||
@ -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'); |
||||
}); |
||||
@ -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<JsonRpcError | Defined> { |
||||
const [, providerName, methodName] = call.split(':'); |
||||
const provider = AppObjectRegistry.get<IOutboundMessageProviders>(`outboundCommunication:${providerName}`); |
||||
if (!provider) { |
||||
return new JsonRpcError('error-invalid-provider', -32000); |
||||
} |
||||
const method = provider[methodName as keyof IOutboundMessageProviders]; |
||||
const logger = AppObjectRegistry.get<Logger>('logger'); |
||||
const args = (params as Array<unknown>) ?? []; |
||||
|
||||
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); |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@ |
||||
import type { IOutboundEmailMessageProvider, IOutboundPhoneMessageProvider } from '../outboundComunication'; |
||||
|
||||
export interface IOutboundCommunicationProviderExtend { |
||||
registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise<void>; |
||||
registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise<void>; |
||||
} |
||||
@ -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<void> { |
||||
return Promise.resolve(this.manager.addProvider(this.appId, provider)); |
||||
} |
||||
|
||||
public registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise<void> { |
||||
return Promise.resolve(this.manager.addProvider(this.appId, provider)); |
||||
} |
||||
} |
||||
@ -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<void> { |
||||
if (this.hasProviderPermission(appId)) { |
||||
return this.registerPhoneProvider(info, appId); |
||||
} |
||||
} |
||||
|
||||
public async doRegisterEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise<void> { |
||||
if (this.hasProviderPermission(appId)) { |
||||
return this.registerEmailProvider(info, appId); |
||||
} |
||||
} |
||||
|
||||
public async doUnRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise<void> { |
||||
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<void>; |
||||
|
||||
protected abstract registerEmailProvider(info: IOutboundEmailMessageProvider, appId: string): Promise<void>; |
||||
|
||||
protected abstract unRegisterProvider(info: IOutboundMessageProviders, appId: string): Promise<void>; |
||||
} |
||||
@ -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<ProviderMetadata> { |
||||
return this.runTheCode<ProviderMetadata>(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); |
||||
} |
||||
|
||||
public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: any): Promise<void> { |
||||
await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); |
||||
} |
||||
|
||||
private async runTheCode<T = unknown>( |
||||
method: AppMethod._OUTBOUND_GET_PROVIDER_METADATA | AppMethod._OUTBOUND_SEND_MESSAGE, |
||||
logStorage: AppLogStorage, |
||||
accessors: AppAccessorManager, |
||||
runContextArgs: Array<any>, |
||||
): Promise<T> { |
||||
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); |
||||
} |
||||
} |
||||
} |
||||
@ -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<string, Map<ValidOutboundProvider, OutboundMessageProvider>>; |
||||
|
||||
constructor(private readonly manager: AppManager) { |
||||
this.bridge = this.manager.getBridges().getOutboundMessageBridge(); |
||||
this.accessors = this.manager.getAccessorManager(); |
||||
|
||||
this.outboundMessageProviders = new Map<string, Map<ValidOutboundProvider, OutboundMessageProvider>>(); |
||||
} |
||||
|
||||
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<ValidOutboundProvider, OutboundMessageProvider>()); |
||||
} |
||||
|
||||
this.outboundMessageProviders.get(appId).set(provider.type, new OutboundMessageProvider(app, provider)); |
||||
} |
||||
|
||||
public async registerProviders(appId: string): Promise<void> { |
||||
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<void> { |
||||
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<void> { |
||||
return this.bridge.doRegisterPhoneProvider(provider, appId); |
||||
} |
||||
|
||||
private registerEmailProvider(appId: string, provider: IOutboundEmailMessageProvider): Promise<void> { |
||||
return this.bridge.doRegisterEmailProvider(provider, appId); |
||||
} |
||||
|
||||
private async unregisterProvider(appId: string, info: OutboundMessageProvider): Promise<void> { |
||||
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); |
||||
} |
||||
} |
||||
@ -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<void> { |
||||
return Promise.resolve(); |
||||
} |
||||
|
||||
protected async registerEmailProvider(provider: IOutboundEmailMessageProvider, appId: string): Promise<void> { |
||||
return Promise.resolve(); |
||||
} |
||||
|
||||
protected async unRegisterProvider(provider: IOutboundMessageProviders, appId: string): Promise<void> { |
||||
return Promise.resolve(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue