From f0b7ff542ead704b9ab20c9a3a373182fa5a3da5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 28 Jul 2025 15:39:06 -0600 Subject: [PATCH 1/4] send an outbound message --- .../server/api/lib/outbound.ts | 22 ++++++++- .../server/api/outbound.ts | 17 +++++++ .../server/outboundcomms/rest.ts | 45 +++++++++++++++---- .../IOutboundCommsProvider.ts | 12 ++++- .../IOutboundProviderTemplate.ts | 2 +- .../server/errors/AppOutboundProcessError.ts | 12 +++++ .../AppOutboundCommunicationProvider.ts | 15 +++++-- ...AppOutboundCommunicationProviderManager.ts | 11 ++++- 8 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 packages/apps-engine/src/server/errors/AppOutboundProcessError.ts 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 12f8c06b88857..30e02b3581684 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 @@ -4,6 +4,7 @@ import type { ValidOutboundProvider, IOutboundMessageProviderService, IOutboundProviderMetadata, + IOutboundMessage, } from '@rocket.chat/core-typings'; import { ValidOutboundProviderList } from '@rocket.chat/core-typings'; @@ -55,13 +56,30 @@ export class OutboundMessageProviderService implements IOutboundMessageProviderS return manager; } - public sendMessage(providerId: string, body: any) { + public sendMessage( + providerId: string, + { + to, + type, + templateProviderPhoneNumber, + template, + }: { + to: string; + type: ValidOutboundProvider; + templateProviderPhoneNumber: string; + template: IOutboundMessage; + }, + ) { const provider = this.provider.findOneByProviderId(providerId); if (!provider) { throw new Error('error-invalid-provider'); } - return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, body); + if (provider.type !== type) { + throw new Error('error-invalid-provider-type'); + } + + return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, { to, templateProviderPhoneNumber, template }); } } 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 92ef41a678c36..6dffaeab93833 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts @@ -4,6 +4,9 @@ import { GETOutboundProviderParamsSchema, GETOutboundProviderBadRequestErrorSchema, GETOutboundProviderMetadataSchema, + POSTOutboundMessageParams, + POSTOutboundMessageErrorSchema, + POSTOutboundMessageSuccessSchema, } from '../outboundcomms/rest'; import { outboundMessageProvider } from './lib/outbound'; import type { ExtractRoutesFromAPI } from '../../../../../app/api/server/ApiClass'; @@ -45,6 +48,20 @@ const outboundCommsEndpoints = API.v1 metadata: providerMetadata, }); }, + ) + .post( + 'omnichannel/outbound/providers/:id/message', + { + response: { 200: POSTOutboundMessageSuccessSchema, 400: POSTOutboundMessageErrorSchema }, + authRequired: true, + body: POSTOutboundMessageParams, + }, + async function action() { + const { id } = this.urlParams; + + await outboundMessageProvider.sendMessage(id, this.bodyParams); + return API.v1.success(); + }, ); export type OutboundCommsEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts b/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts index e13d368bea98b..f979ca3078583 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts @@ -1,4 +1,4 @@ -import type { IOutboundMessage, IOutboundProvider, IOutboundProviderMetadata } from '@rocket.chat/core-typings'; +import type { IOutboundMessage, IOutboundProvider, IOutboundProviderMetadata, ValidOutboundProvider } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { OutboundCommsEndpoints } from '../api/outbound'; @@ -12,7 +12,9 @@ declare module '@rocket.chat/rest-typings' { interface Endpoints extends OutboundCommsEndpoints {} } -type GETOutboundProviderParams = { type?: string }; +type GenericErrorResponse = { success: boolean; message: string }; + +type GETOutboundProviderParams = { type?: ValidOutboundProvider }; const GETOutboundProviderSchema = { type: 'object', properties: { @@ -64,19 +66,21 @@ const GETOutboundProviderBadRequestError = { }, }, }; -export const GETOutboundProviderBadRequestErrorSchema = ajv.compile<{ success: boolean; message: string }>( - GETOutboundProviderBadRequestError, -); +export const GETOutboundProviderBadRequestErrorSchema = ajv.compile(GETOutboundProviderBadRequestError); -type POSTOutboundMessageParams = { - message: IOutboundMessage; +type POSTOutboundMessageParamsType = { + to: string; + type: ValidOutboundProvider; + templateProviderPhoneNumber: string; + template: IOutboundMessage; }; + const POSTOutboundMessageSchema = { type: 'object', required: ['to', 'type'], properties: { to: { type: 'string' }, - type: { type: 'string' }, + type: { type: 'string', enum: ['phone', 'email'] }, templateProviderPhoneNumber: { type: 'string' }, template: { type: 'object', @@ -183,7 +187,30 @@ const POSTOutboundMessageSchema = { additionalProperties: false, }; -export const isPOSTOutboundMessageParams = ajv.compile(POSTOutboundMessageSchema); +export const POSTOutboundMessageParams = ajv.compile(POSTOutboundMessageSchema); + +const POSTOutboundMessageError = { + type: 'object', + properties: { + success: { + type: 'boolean', + }, + message: { + type: 'string', + }, + }, + additionalProperties: false, +}; + +export const POSTOutboundMessageErrorSchema = ajv.compile(POSTOutboundMessageError); + +const POSTOutboundMessageSuccess = { + type: 'object', + properties: {}, + additionalProperties: false, +}; + +export const POSTOutboundMessageSuccessSchema = ajv.compile(POSTOutboundMessageSuccess); const OutboundProviderMetadataSchema = { type: 'object', diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts index 696efd788ecb4..b7b953d175d05 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts @@ -15,7 +15,17 @@ interface IOutboundMessageProviderBase { name: string; documentationUrl?: string; supportsTemplates?: boolean; - sendOutboundMessage(message: IOutboundMessage, read: IRead, modify: IModify, http: IHttp, persistence: IPersistence): Promise; + sendOutboundMessage( + message: { + to: string; + templateProviderPhoneNumber: string; + template: IOutboundMessage; + }, + read: IRead, + modify: IModify, + http: IHttp, + persistence: IPersistence, + ): Promise; } export interface IOutboundPhoneMessageProvider extends IOutboundMessageProviderBase { diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundProviderTemplate.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundProviderTemplate.ts index 6011160c42022..aaaabbf5999a3 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundProviderTemplate.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundProviderTemplate.ts @@ -23,7 +23,7 @@ export interface IOutboundProviderTemplate { partnerId: string; externalId: string; updatedExternal: string; // ISO 8601 timestamp - rejectedReason: string | null; + rejectedReason: string | undefined; } type Component = IHeaderComponent | IBodyComponent | IFooterComponent; diff --git a/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts b/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts new file mode 100644 index 0000000000000..a7e905ae90b90 --- /dev/null +++ b/packages/apps-engine/src/server/errors/AppOutboundProcessError.ts @@ -0,0 +1,12 @@ +export class AppOutboundProcessError implements Error { + public name = 'OutboundProviderError'; + + public message: string; + + constructor(message: string, where?: string) { + this.message = message; + if (where) { + this.message += ` (${where})`; + } + } +} diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts index dc7495785d926..e243acc5c4de0 100644 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts @@ -1,6 +1,7 @@ import type { AppAccessorManager } from '.'; import { AppMethod } from '../../definition/metadata'; -import type { IOutboundMessageProviders, ProviderMetadata } from '../../definition/outboundComunication'; +import type { IOutboundMessage, IOutboundMessageProviders, ProviderMetadata } from '../../definition/outboundComunication'; +import { AppOutboundProcessError } from '../errors/AppOutboundProcessError'; import type { ProxiedApp } from '../ProxiedApp'; import type { AppLogStorage } from '../storage'; @@ -18,7 +19,15 @@ export class OutboundMessageProvider { return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); } - public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: any): Promise { + public async runSendOutboundMessage( + logStorage: AppLogStorage, + accessors: AppAccessorManager, + body: { + to: string; + templateProviderPhoneNumber: string; + template: IOutboundMessage; + }, + ): Promise { await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); } @@ -41,7 +50,7 @@ export class OutboundMessageProvider { if (e?.message === 'error-invalid-provider') { throw new Error('error-provider-not-registered'); } - console.error(e); + throw new AppOutboundProcessError(e.message, method); } } } diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts index e5fa8a4077fc5..c9a150c51878c 100644 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts @@ -4,6 +4,7 @@ import type { IOutboundEmailMessageProvider, IOutboundPhoneMessageProvider, ValidOutboundProvider, + IOutboundMessage, } from '../../definition/outboundComunication'; import type { AppManager } from '../AppManager'; import type { OutboundMessageBridge } from '../bridges'; @@ -119,7 +120,15 @@ export class AppOutboundCommunicationProviderManager { return providerInfo.runGetProviderMetadata(this.manager.getLogStorage(), this.accessors); } - public sendOutboundMessage(appId: string, providerType: ValidOutboundProvider, body: unknown) { + public sendOutboundMessage( + appId: string, + providerType: ValidOutboundProvider, + body: { + to: string; + templateProviderPhoneNumber: string; + template: IOutboundMessage; + }, + ) { const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); if (!providerInfo) { throw new Error('provider-not-registered'); From f821f4a1dcca43c0f66dcc7f3b18fc15bf7b2d71 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 28 Jul 2025 15:42:33 -0600 Subject: [PATCH 2/4] Create small-mangos-hang.md --- .changeset/small-mangos-hang.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/small-mangos-hang.md diff --git a/.changeset/small-mangos-hang.md b/.changeset/small-mangos-hang.md new file mode 100644 index 0000000000000..75def9c2b46d4 --- /dev/null +++ b/.changeset/small-mangos-hang.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +--- + +Creates a new endpoint that allows agents to send an outbound message from a registered app provider From be8e72a07f736b3d53f787b38579c3a782f33774 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 29 Jul 2025 09:43:43 -0600 Subject: [PATCH 3/4] fix type --- .../server/api/lib/outbound.ts | 21 ++----------------- .../server/outboundcomms/rest.ts | 11 +++------- .../IOutboundCommsProvider.ts | 12 +---------- .../AppOutboundCommunicationProvider.ts | 12 ++--------- ...AppOutboundCommunicationProviderManager.ts | 10 +-------- .../core-typings/src/omnichannel/outbound.ts | 2 +- 6 files changed, 10 insertions(+), 58 deletions(-) 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 30e02b3581684..f73602f34b43a 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 @@ -56,30 +56,13 @@ export class OutboundMessageProviderService implements IOutboundMessageProviderS return manager; } - public sendMessage( - providerId: string, - { - to, - type, - templateProviderPhoneNumber, - template, - }: { - to: string; - type: ValidOutboundProvider; - templateProviderPhoneNumber: string; - template: IOutboundMessage; - }, - ) { + public sendMessage(providerId: string, message: IOutboundMessage) { const provider = this.provider.findOneByProviderId(providerId); if (!provider) { throw new Error('error-invalid-provider'); } - if (provider.type !== type) { - throw new Error('error-invalid-provider-type'); - } - - return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, { to, templateProviderPhoneNumber, template }); + return this.getProviderManager().sendOutboundMessage(provider.appId, provider.type, message); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts b/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts index f979ca3078583..2f7ad05abc40b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts @@ -68,19 +68,14 @@ const GETOutboundProviderBadRequestError = { }; export const GETOutboundProviderBadRequestErrorSchema = ajv.compile(GETOutboundProviderBadRequestError); -type POSTOutboundMessageParamsType = { - to: string; - type: ValidOutboundProvider; - templateProviderPhoneNumber: string; - template: IOutboundMessage; -}; +type POSTOutboundMessageParamsType = IOutboundMessage; const POSTOutboundMessageSchema = { type: 'object', required: ['to', 'type'], properties: { - to: { type: 'string' }, - type: { type: 'string', enum: ['phone', 'email'] }, + to: { type: 'string', minLength: 1 }, + type: { type: 'string' }, templateProviderPhoneNumber: { type: 'string' }, template: { type: 'object', diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts index b7b953d175d05..696efd788ecb4 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts @@ -15,17 +15,7 @@ interface IOutboundMessageProviderBase { name: string; documentationUrl?: string; supportsTemplates?: boolean; - sendOutboundMessage( - message: { - to: string; - templateProviderPhoneNumber: string; - template: IOutboundMessage; - }, - read: IRead, - modify: IModify, - http: IHttp, - persistence: IPersistence, - ): Promise; + sendOutboundMessage(message: IOutboundMessage, read: IRead, modify: IModify, http: IHttp, persistence: IPersistence): Promise; } export interface IOutboundPhoneMessageProvider extends IOutboundMessageProviderBase { diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts index e243acc5c4de0..cc601ae298fa2 100644 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProvider.ts @@ -1,8 +1,8 @@ import type { AppAccessorManager } from '.'; import { AppMethod } from '../../definition/metadata'; import type { IOutboundMessage, IOutboundMessageProviders, ProviderMetadata } from '../../definition/outboundComunication'; -import { AppOutboundProcessError } from '../errors/AppOutboundProcessError'; import type { ProxiedApp } from '../ProxiedApp'; +import { AppOutboundProcessError } from '../errors/AppOutboundProcessError'; import type { AppLogStorage } from '../storage'; export class OutboundMessageProvider { @@ -19,15 +19,7 @@ export class OutboundMessageProvider { return this.runTheCode(AppMethod._OUTBOUND_GET_PROVIDER_METADATA, logStorage, accessors, []); } - public async runSendOutboundMessage( - logStorage: AppLogStorage, - accessors: AppAccessorManager, - body: { - to: string; - templateProviderPhoneNumber: string; - template: IOutboundMessage; - }, - ): Promise { + public async runSendOutboundMessage(logStorage: AppLogStorage, accessors: AppAccessorManager, body: IOutboundMessage): Promise { await this.runTheCode(AppMethod._OUTBOUND_SEND_MESSAGE, logStorage, accessors, [body]); } diff --git a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts index c9a150c51878c..9abfc9280c4c4 100644 --- a/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts +++ b/packages/apps-engine/src/server/managers/AppOutboundCommunicationProviderManager.ts @@ -120,15 +120,7 @@ export class AppOutboundCommunicationProviderManager { return providerInfo.runGetProviderMetadata(this.manager.getLogStorage(), this.accessors); } - public sendOutboundMessage( - appId: string, - providerType: ValidOutboundProvider, - body: { - to: string; - templateProviderPhoneNumber: string; - template: IOutboundMessage; - }, - ) { + public sendOutboundMessage(appId: string, providerType: ValidOutboundProvider, body: IOutboundMessage) { const providerInfo = this.outboundMessageProviders.get(appId)?.get(providerType); if (!providerInfo) { throw new Error('provider-not-registered'); diff --git a/packages/core-typings/src/omnichannel/outbound.ts b/packages/core-typings/src/omnichannel/outbound.ts index afd85f7884707..c97ba2ecd8c97 100644 --- a/packages/core-typings/src/omnichannel/outbound.ts +++ b/packages/core-typings/src/omnichannel/outbound.ts @@ -27,7 +27,7 @@ export interface IOutboundProviderTemplate { partnerId: string; externalId: string; updatedExternal: string; // ISO 8601 timestamp - rejectedReason: string | null; + rejectedReason: string | undefined; } type Component = IHeaderComponent | IBodyComponent | IFooterComponent; From 3bc8a64d6b9636acdd8a2cf4955c4d4909f84ed6 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 29 Jul 2025 14:37:43 -0600 Subject: [PATCH 4/4] export types --- .../src/definition/outboundComunication/IOutboundMessage.ts | 4 ++-- packages/core-typings/src/omnichannel/outbound.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundMessage.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundMessage.ts index 9c177b4501897..9f8f7e3efeca9 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundMessage.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundMessage.ts @@ -14,12 +14,12 @@ export interface IOutboundMessage { }; } -type TemplateComponent = { +export type TemplateComponent = { type: 'header' | 'body' | 'footer' | 'button'; parameters: TemplateParameter[]; }; -type TemplateParameter = +export type TemplateParameter = | { type: 'text'; text: string; diff --git a/packages/core-typings/src/omnichannel/outbound.ts b/packages/core-typings/src/omnichannel/outbound.ts index c97ba2ecd8c97..78e3115c1e86e 100644 --- a/packages/core-typings/src/omnichannel/outbound.ts +++ b/packages/core-typings/src/omnichannel/outbound.ts @@ -73,12 +73,12 @@ export interface IOutboundMessage { }; } -type TemplateComponent = { +export type TemplateComponent = { type: 'header' | 'body' | 'footer' | 'button'; parameters: TemplateParameter[]; }; -type TemplateParameter = +export type TemplateParameter = | { type: 'text'; text: string;