Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
5aeb938
feat: adds new endpoints to interact with outbound providers
lucas-a-pelegrino Jun 24, 2025
2cebc59
refactor: moves outbound endpoints to correct place
lucas-a-pelegrino Jun 24, 2025
c89c5db
refactor: adds more minor improvements to listOutboundPRoviders logic
lucas-a-pelegrino Jun 27, 2025
cc5126f
tests: adds unit testing for OutboundMessageProviderService
lucas-a-pelegrino Jul 3, 2025
3d2d04a
chore: adds a minor improvement on OutboundMessageProviderService
lucas-a-pelegrino Jul 8, 2025
0aa24b4
fix
KevLehman Jul 8, 2025
a565e9b
chore: adds minor improvements to typing to ensure reusability across…
lucas-a-pelegrino Jul 8, 2025
056d740
Update apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts
KevLehman Jul 9, 2025
fb5dff0
Update apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts
KevLehman Jul 9, 2025
95a0c4d
app engine outbound 1
KevLehman Jul 9, 2025
fa57364
oops
KevLehman Jul 9, 2025
50d036e
oops
KevLehman Jul 11, 2025
a52cc5c
manager, provider & mod
KevLehman Jul 11, 2025
d388e38
dupe types
KevLehman Jul 11, 2025
8cd5a76
ban types
KevLehman Jul 11, 2025
7ce26fa
lint
KevLehman Jul 14, 2025
79e6ee0
more proxies
KevLehman Jul 14, 2025
188bc88
ts
KevLehman Jul 14, 2025
2c8cb6b
lint
KevLehman Jul 14, 2025
fa458c7
the last?
KevLehman Jul 14, 2025
8133210
test & proxies
KevLehman Jul 14, 2025
ec6e01c
types
KevLehman Jul 14, 2025
75095da
lint
KevLehman Jul 14, 2025
44f0a65
try
KevLehman Jul 14, 2025
63a051b
registration working
KevLehman Jul 14, 2025
17d918a
getprovidermetadata
KevLehman Jul 15, 2025
48b6050
sendmessage
KevLehman Jul 15, 2025
4a32452
fixes
KevLehman Jul 15, 2025
3016524
polish
KevLehman Jul 16, 2025
29d352d
iblock
KevLehman Jul 16, 2025
3389db8
Merge branch 'develop' into feat/app-bridge-outbound
d-gubert Jul 17, 2025
f2d3281
Merge branch 'develop' into feat/app-bridge-outbound
d-gubert Jul 17, 2025
26036bf
Merge branch 'develop' into feat/app-bridge-outbound
d-gubert Jul 17, 2025
6532450
Merge branch 'develop' into feat/app-bridge-outbound
d-gubert Jul 17, 2025
0c13013
async unregister
sampaiodiego Jul 18, 2025
69e9b15
Merge branch 'develop' into feat/app-bridge-outbound
sampaiodiego Jul 18, 2025
f135a13
unregister async
sampaiodiego Jul 18, 2025
37eb1f8
Merge branch 'feat/app-bridge-outbound' of github.com:RocketChat/Rock…
sampaiodiego Jul 18, 2025
d9210ec
Merge branch 'develop' into feat/app-bridge-outbound
kodiakhq[bot] Jul 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/meteor/app/apps/server/bridges/bridges.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -139,6 +141,10 @@ export class RealAppBridges extends AppBridges {
return this._videoConfBridge;
}

getOutboundMessageBridge() {
return this._outboundMessageBridge;
}

getOAuthAppsBridge() {
return this._oAuthBridge;
}
Expand Down
45 changes: 45 additions & 0 deletions apps/meteor/app/apps/server/bridges/outboundCommunication.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -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);
}
Expand All @@ -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;
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const outboundCommsEndpoints = API.v1.get(
providerType: {
type: 'string',
},
documentationUrl: {
type: 'string',
},
},
},
},
Expand Down
21 changes: 13 additions & 8 deletions apps/meteor/server/lib/OutboundMessageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]>;
Expand All @@ -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]);
}
Expand All @@ -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 }),
}));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
15 changes: 15 additions & 0 deletions packages/apps-engine/deno-runtime/lib/accessors/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/apps-engine/deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
};
Expand All @@ -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',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { IOutboundEmailMessageProvider, IOutboundPhoneMessageProvider } from '../outboundComunication';

export interface IOutboundCommunicationProviderExtend {
registerPhoneProvider(provider: IOutboundPhoneMessageProvider): Promise<void>;
registerEmailProvider(provider: IOutboundEmailMessageProvider): Promise<void>;
}
1 change: 1 addition & 0 deletions packages/apps-engine/src/definition/accessors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ export * from './IVideoConferenceExtend';
export * from './IVideoConferenceRead';
export * from './IVideoConfProvidersExtend';
export * from './IModerationModify';
export * from './IOutboundCommunicationProviderExtend';
Original file line number Diff line number Diff line change
@@ -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 }] }
Expand All @@ -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];
10 changes: 10 additions & 0 deletions packages/apps-engine/src/server/AppManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -97,6 +98,8 @@ export class AppManager {

private readonly videoConfProviderManager: AppVideoConfProviderManager;

private readonly outboundCommunicationProviderManager: AppOutboundCommunicationProviderManager;

private readonly signatureManager: AppSignatureManager;

private readonly runtime: AppRuntimeManager;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -198,6 +202,10 @@ export class AppManager {
return this.videoConfProviderManager;
}

public getOutboundCommunicationProviderManager(): AppOutboundCommunicationProviderManager {
return this.outboundCommunicationProviderManager;
}

public getLicenseManager(): AppLicenseManager {
return this.licenseManager;
}
Expand Down Expand Up @@ -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());
}

/**
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading