diff --git a/.changeset/new-mails-rhyme.md b/.changeset/new-mails-rhyme.md new file mode 100644 index 0000000000000..833efddbc95dc --- /dev/null +++ b/.changeset/new-mails-rhyme.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/core-typings": minor +--- + +Adds new endpoints for outbound communications diff --git a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx index f852384e32425..841885ddbad35 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx @@ -1,10 +1,11 @@ +import type { ValidOutboundProvider } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import ContactInfoDetailsEntry from './ContactInfoDetailsEntry'; import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber'; type ContactInfoDetailsGroupProps = { - type: 'phone' | 'email'; + type: ValidOutboundProvider; label: string; values: string[]; }; 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 new file mode 100644 index 0000000000000..7884ff3e61225 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts @@ -0,0 +1,26 @@ +import type { IOutboundProvider, ValidOutboundProvider } from '@rocket.chat/core-typings'; +import { ValidOutboundProviderList } from '@rocket.chat/core-typings'; + +import { OutboundMessageProvider } from '../../../../../../server/lib/OutboundMessageProvider'; + +export class OutboundMessageProviderService { + private readonly provider: OutboundMessageProvider; + + constructor() { + this.provider = new OutboundMessageProvider(); + } + + private isProviderValid(type: any): type is ValidOutboundProvider { + return ValidOutboundProviderList.includes(type); + } + + public listOutboundProviders(type?: string): IOutboundProvider[] { + if (type !== undefined && !this.isProviderValid(type)) { + throw new Error('Invalid type'); + } + + return this.provider.getOutboundMessageProviders(type); + } +} + +export const outboundMessageProvider = new OutboundMessageProviderService(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts new file mode 100644 index 0000000000000..f020cf6719557 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts @@ -0,0 +1,49 @@ +import type { IOutboundProvider } from '@rocket.chat/core-typings'; +import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; + +import { API } from '../../../../../app/api/server'; +import { isGETOutboundProviderParams } from '../outboundcomms/rest'; +import { outboundMessageProvider } from './lib/outbound'; +import type { ExtractRoutesFromAPI } from '../../../../../app/api/server/ApiClass'; + +const outboundCommsEndpoints = API.v1.get( + 'omnichannel/outbound/providers', + { + response: { + 200: ajv.compile<{ providers: IOutboundProvider[] }>({ + providers: { + type: 'array', + items: { + type: 'object', + properties: { + providerId: { + type: 'string', + }, + providerName: { + type: 'string', + }, + supportsTemplates: { + type: 'boolean', + }, + providerType: { + type: 'string', + }, + }, + }, + }, + }), + }, + query: isGETOutboundProviderParams, + authRequired: true, + }, + async function action() { + const { type } = this.queryParams; + + const providers = outboundMessageProvider.listOutboundProviders(type); + return API.v1.success({ + providers, + }); + }, +); + +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 f1699f0847372..5b01cdef085a0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts @@ -1,24 +1,15 @@ -import type { IOutboundProvider, IOutboundMessage, IOutboundProviderMetadata } from '@rocket.chat/core-typings'; +import type { IOutboundMessage } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; +import type { OutboundCommsEndpoints } from '../api/outbound'; + const ajv = new Ajv({ coerceTypes: true, }); declare module '@rocket.chat/rest-typings' { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface Endpoints { - '/v1/omnichannel/outbound/providers': { - GET: (params: GETOutboundProviderParams) => IOutboundProvider[]; - }; - '/v1/omnichannel/outbound/providers/:id/metadata': { - GET: () => IOutboundProviderMetadata; - }; - '/v1/omnichannel/outbound/providers/:id/message': { - // Note: we may need to adapt this type when the API is implemented and UI starts to use it - POST: (params: POSTOutboundMessageParams) => void; - }; - } + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends OutboundCommsEndpoints {} } type GETOutboundProviderParams = { type?: string }; diff --git a/apps/meteor/server/lib/OutboundMessageProvider.ts b/apps/meteor/server/lib/OutboundMessageProvider.ts index 6e3bef870b1bd..8c8a0c380d0ff 100644 --- a/apps/meteor/server/lib/OutboundMessageProvider.ts +++ b/apps/meteor/server/lib/OutboundMessageProvider.ts @@ -3,16 +3,17 @@ 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?: 'phone' | 'email'): IOutboundMessageProviders[]; + getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[]; unregisterProvider(appId: string, providerType: string): void; } export class OutboundMessageProvider implements IOutboundMessageProvider { - private readonly outboundMessageProviders: Map<'phone' | 'email', IOutboundMessageProviders[]>; + private readonly outboundMessageProviders: Map; constructor() { this.outboundMessageProviders = new Map([ @@ -29,15 +30,27 @@ export class OutboundMessageProvider implements IOutboundMessageProvider { this.outboundMessageProviders.set('email', [...(this.outboundMessageProviders.get('email') || []), provider]); } - public getOutboundMessageProviders(type?: 'phone' | 'email'): IOutboundMessageProviders[] { + public getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[] { if (type) { - return Array.from(this.outboundMessageProviders.get(type)?.values() || []); + return Array.from(this.outboundMessageProviders.get(type)?.values() || []).map((provider) => ({ + providerId: provider.appId, + providerName: provider.name, + providerType: provider.type, + ...(provider.supportsTemplates && { supportsTemplates: provider.supportsTemplates }), + })); } - return Array.from(this.outboundMessageProviders.values()).flatMap((providers) => providers); + return Array.from(this.outboundMessageProviders.values()) + .flatMap((providers) => providers) + .map((provider) => ({ + providerId: provider.appId, + providerName: provider.name, + supportsTemplates: provider.supportsTemplates, + providerType: provider.type, + })); } - public unregisterProvider(appId: string, providerType: 'phone' | 'email'): void { + public unregisterProvider(appId: string, providerType: ValidOutboundProvider): void { const providers = this.outboundMessageProviders.get(providerType); if (!providers) { return; diff --git a/apps/meteor/tests/unit/app/livechat/server/outbound/outbound.spec.ts b/apps/meteor/tests/unit/app/livechat/server/outbound/outbound.spec.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/apps/meteor/tests/unit/server/lib/OutboundMessageProvider.spec.ts b/apps/meteor/tests/unit/server/lib/OutboundMessageProvider.spec.ts index 5e6c1250e68b3..aacf062335ab3 100644 --- a/apps/meteor/tests/unit/server/lib/OutboundMessageProvider.spec.ts +++ b/apps/meteor/tests/unit/server/lib/OutboundMessageProvider.spec.ts @@ -3,6 +3,7 @@ import type { IOutboundPhoneMessageProvider, } from '@rocket.chat/apps-engine/definition/outboundComunication'; import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; import sinon from 'sinon'; import { OutboundMessageProvider } from '../../../../server/lib/OutboundMessageProvider'; @@ -28,7 +29,11 @@ describe('OutboundMessageProvider', () => { const providers = outboundMessageProvider.getOutboundMessageProviders('phone'); expect(providers).to.have.lengthOf(1); - expect(providers[0]).to.deep.equal(phoneProvider); + expect(providers[0]).to.deep.equal({ + providerId: '123', + providerName: 'Test Phone Provider', + providerType: 'phone', + }); }); it('should successfully register a email provider', () => { @@ -44,7 +49,11 @@ describe('OutboundMessageProvider', () => { const providers = outboundMessageProvider.getOutboundMessageProviders('email'); expect(providers).to.have.lengthOf(1); - expect(providers[0]).to.deep.equal(emailProvider); + expect(providers[0]).to.deep.equal({ + providerId: '123', + providerName: 'Test Email Provider', + providerType: 'email', + }); }); it('should list currently registered providers [unfiltered]', () => { @@ -69,8 +78,8 @@ describe('OutboundMessageProvider', () => { const providers = outboundMessageProvider.getOutboundMessageProviders(); expect(providers).to.have.lengthOf(2); - expect(providers.some((provider) => provider.type === 'phone')).to.be.true; - expect(providers.some((provider) => provider.type === 'email')).to.be.true; + expect(providers.some((provider) => provider.providerType === 'phone')).to.be.true; + expect(providers.some((provider) => provider.providerType === 'email')).to.be.true; }); it('should list currently registered providers [filtered by type]', () => { @@ -95,7 +104,7 @@ describe('OutboundMessageProvider', () => { const providers = outboundMessageProvider.getOutboundMessageProviders('phone'); expect(providers).to.have.lengthOf(1); - expect(providers[0].type).to.equal('phone'); + expect(providers[0].providerType).to.equal('phone'); }); it('should unregister a provider', () => { @@ -127,6 +136,6 @@ describe('OutboundMessageProvider', () => { registeredProviders = outboundMessageProvider.getOutboundMessageProviders('phone'); expect(registeredProviders).to.have.lengthOf(1); - expect(registeredProviders.some((provider) => provider.appId !== '123')).to.be.true; + expect(registeredProviders.some((provider) => provider.providerId !== '123')).to.be.true; }); }); diff --git a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts index ba83bb03e215c..af40d4b514459 100644 --- a/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts +++ b/packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts @@ -13,6 +13,7 @@ interface IOutboundMessageProviderBase { appId: string; name: string; documentationUrl?: string; + supportsTemplates?: boolean; sendOutboundMessage(message: IOutboundMessage): Promise; } diff --git a/packages/core-typings/src/omnichannel/outbound.ts b/packages/core-typings/src/omnichannel/outbound.ts index c6ffce73ed1ea..38b29d9c5ae9b 100644 --- a/packages/core-typings/src/omnichannel/outbound.ts +++ b/packages/core-typings/src/omnichannel/outbound.ts @@ -107,10 +107,14 @@ type TemplateParameter = export type IOutboundProvider = { providerId: string; providerName: string; - supportsTemplates: boolean; + supportsTemplates?: boolean; providerType: 'phone' | 'email'; }; export type IOutboundProviderMetadata = IOutboundProvider & { templates: Record; }; + +export const ValidOutboundProviderList = ['phone', 'email'] as const; + +export type ValidOutboundProvider = (typeof ValidOutboundProviderList)[number];