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>
This commit is contained in:
Kevin Aleman 2025-07-18 21:45:39 -06:00 committed by GitHub
parent 0b4f3d3c27
commit fccb53718c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 525 additions and 17 deletions

View File

@ -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;
}

View File

@ -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');
}
}
}

View File

@ -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');
});

View File

@ -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<IOutboundProviderMetadata> {
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;
});

View File

@ -30,6 +30,9 @@ const outboundCommsEndpoints = API.v1.get(
providerType: {
type: 'string',
},
documentationUrl: {
type: 'string',
},
},
},
},

View File

@ -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<ValidOutboundProvider, IOutboundMessageProviders[]>;
@ -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 }),
}));
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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',
};

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
import type { IOutboundEmailMessageProvider, IOutboundPhoneMessageProvider } from '../outboundComunication';
export interface IOutboundCommunicationProviderExtend {
registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise<void>;
registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise<void>;
}

View File

@ -56,3 +56,4 @@ export * from './IVideoConferenceExtend';
export * from './IVideoConferenceRead';
export * from './IVideoConfProvidersExtend';
export * from './IModerationModify';
export * from './IOutboundCommunicationProviderExtend';

View File

@ -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<string, IOutboundProviderTemplate[]>; // 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];

View File

@ -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);
}

View File

@ -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,
) {}
}

View File

@ -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));
}
}

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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>;
}

View File

@ -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,
};

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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,
};

View File

@ -119,6 +119,9 @@ export const AppPermissions = {
read: { name: 'oauth-app.read' },
write: { name: 'oauth-app.write' },
},
'outboundComms': {
provide: { name: 'outbound-communication.provide' },
},
};
/**

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
},

View File

@ -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<string, IOutboundProviderTemplate[]>;
};
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[];
}