Skip to content

Commit 2cec8ac

Browse files
feat: Outbound Comms endpoints (#36377)
Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
1 parent 7d15361 commit 2cec8ac

File tree

10 files changed

+129
-28
lines changed

10 files changed

+129
-28
lines changed

.changeset/new-mails-rhyme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/apps-engine": minor
4+
"@rocket.chat/core-typings": minor
5+
---
6+
7+
Adds new endpoints for outbound communications

apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import type { ValidOutboundProvider } from '@rocket.chat/core-typings';
12
import { Box } from '@rocket.chat/fuselage';
23

34
import ContactInfoDetailsEntry from './ContactInfoDetailsEntry';
45
import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber';
56

67
type ContactInfoDetailsGroupProps = {
7-
type: 'phone' | 'email';
8+
type: ValidOutboundProvider;
89
label: string;
910
values: string[];
1011
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { IOutboundProvider, ValidOutboundProvider } from '@rocket.chat/core-typings';
2+
import { ValidOutboundProviderList } from '@rocket.chat/core-typings';
3+
4+
import { OutboundMessageProvider } from '../../../../../../server/lib/OutboundMessageProvider';
5+
6+
export class OutboundMessageProviderService {
7+
private readonly provider: OutboundMessageProvider;
8+
9+
constructor() {
10+
this.provider = new OutboundMessageProvider();
11+
}
12+
13+
private isProviderValid(type: any): type is ValidOutboundProvider {
14+
return ValidOutboundProviderList.includes(type);
15+
}
16+
17+
public listOutboundProviders(type?: string): IOutboundProvider[] {
18+
if (type !== undefined && !this.isProviderValid(type)) {
19+
throw new Error('Invalid type');
20+
}
21+
22+
return this.provider.getOutboundMessageProviders(type);
23+
}
24+
}
25+
26+
export const outboundMessageProvider = new OutboundMessageProviderService();
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { IOutboundProvider } from '@rocket.chat/core-typings';
2+
import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv';
3+
4+
import { API } from '../../../../../app/api/server';
5+
import { isGETOutboundProviderParams } from '../outboundcomms/rest';
6+
import { outboundMessageProvider } from './lib/outbound';
7+
import type { ExtractRoutesFromAPI } from '../../../../../app/api/server/ApiClass';
8+
9+
const outboundCommsEndpoints = API.v1.get(
10+
'omnichannel/outbound/providers',
11+
{
12+
response: {
13+
200: ajv.compile<{ providers: IOutboundProvider[] }>({
14+
providers: {
15+
type: 'array',
16+
items: {
17+
type: 'object',
18+
properties: {
19+
providerId: {
20+
type: 'string',
21+
},
22+
providerName: {
23+
type: 'string',
24+
},
25+
supportsTemplates: {
26+
type: 'boolean',
27+
},
28+
providerType: {
29+
type: 'string',
30+
},
31+
},
32+
},
33+
},
34+
}),
35+
},
36+
query: isGETOutboundProviderParams,
37+
authRequired: true,
38+
},
39+
async function action() {
40+
const { type } = this.queryParams;
41+
42+
const providers = outboundMessageProvider.listOutboundProviders(type);
43+
return API.v1.success({
44+
providers,
45+
});
46+
},
47+
);
48+
49+
export type OutboundCommsEndpoints = ExtractRoutesFromAPI<typeof outboundCommsEndpoints>;

apps/meteor/ee/app/livechat-enterprise/server/outboundcomms/rest.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
1-
import type { IOutboundProvider, IOutboundMessage, IOutboundProviderMetadata } from '@rocket.chat/core-typings';
1+
import type { IOutboundMessage } from '@rocket.chat/core-typings';
22
import Ajv from 'ajv';
33

4+
import type { OutboundCommsEndpoints } from '../api/outbound';
5+
46
const ajv = new Ajv({
57
coerceTypes: true,
68
});
79

810
declare module '@rocket.chat/rest-typings' {
9-
// eslint-disable-next-line @typescript-eslint/naming-convention
10-
interface Endpoints {
11-
'/v1/omnichannel/outbound/providers': {
12-
GET: (params: GETOutboundProviderParams) => IOutboundProvider[];
13-
};
14-
'/v1/omnichannel/outbound/providers/:id/metadata': {
15-
GET: () => IOutboundProviderMetadata;
16-
};
17-
'/v1/omnichannel/outbound/providers/:id/message': {
18-
// Note: we may need to adapt this type when the API is implemented and UI starts to use it
19-
POST: (params: POSTOutboundMessageParams) => void;
20-
};
21-
}
11+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
12+
interface Endpoints extends OutboundCommsEndpoints {}
2213
}
2314

2415
type GETOutboundProviderParams = { type?: string };

apps/meteor/server/lib/OutboundMessageProvider.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import type {
33
IOutboundMessageProviders,
44
IOutboundPhoneMessageProvider,
55
} from '@rocket.chat/apps-engine/definition/outboundComunication';
6+
import type { ValidOutboundProvider, IOutboundProvider } from '@rocket.chat/core-typings';
67

78
interface IOutboundMessageProvider {
89
registerPhoneProvider(provider: IOutboundPhoneMessageProvider): void;
910
registerEmailProvider(provider: IOutboundEmailMessageProvider): void;
10-
getOutboundMessageProviders(type?: 'phone' | 'email'): IOutboundMessageProviders[];
11+
getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[];
1112
unregisterProvider(appId: string, providerType: string): void;
1213
}
1314

1415
export class OutboundMessageProvider implements IOutboundMessageProvider {
15-
private readonly outboundMessageProviders: Map<'phone' | 'email', IOutboundMessageProviders[]>;
16+
private readonly outboundMessageProviders: Map<ValidOutboundProvider, IOutboundMessageProviders[]>;
1617

1718
constructor() {
1819
this.outboundMessageProviders = new Map([
@@ -29,15 +30,27 @@ export class OutboundMessageProvider implements IOutboundMessageProvider {
2930
this.outboundMessageProviders.set('email', [...(this.outboundMessageProviders.get('email') || []), provider]);
3031
}
3132

32-
public getOutboundMessageProviders(type?: 'phone' | 'email'): IOutboundMessageProviders[] {
33+
public getOutboundMessageProviders(type?: ValidOutboundProvider): IOutboundProvider[] {
3334
if (type) {
34-
return Array.from(this.outboundMessageProviders.get(type)?.values() || []);
35+
return Array.from(this.outboundMessageProviders.get(type)?.values() || []).map((provider) => ({
36+
providerId: provider.appId,
37+
providerName: provider.name,
38+
providerType: provider.type,
39+
...(provider.supportsTemplates && { supportsTemplates: provider.supportsTemplates }),
40+
}));
3541
}
3642

37-
return Array.from(this.outboundMessageProviders.values()).flatMap((providers) => providers);
43+
return Array.from(this.outboundMessageProviders.values())
44+
.flatMap((providers) => providers)
45+
.map((provider) => ({
46+
providerId: provider.appId,
47+
providerName: provider.name,
48+
supportsTemplates: provider.supportsTemplates,
49+
providerType: provider.type,
50+
}));
3851
}
3952

40-
public unregisterProvider(appId: string, providerType: 'phone' | 'email'): void {
53+
public unregisterProvider(appId: string, providerType: ValidOutboundProvider): void {
4154
const providers = this.outboundMessageProviders.get(providerType);
4255
if (!providers) {
4356
return;

apps/meteor/tests/unit/app/livechat/server/outbound/outbound.spec.ts

Whitespace-only changes.

apps/meteor/tests/unit/server/lib/OutboundMessageProvider.spec.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
IOutboundPhoneMessageProvider,
44
} from '@rocket.chat/apps-engine/definition/outboundComunication';
55
import { expect } from 'chai';
6+
import { describe, it, beforeEach } from 'mocha';
67
import sinon from 'sinon';
78

89
import { OutboundMessageProvider } from '../../../../server/lib/OutboundMessageProvider';
@@ -28,7 +29,11 @@ describe('OutboundMessageProvider', () => {
2829
const providers = outboundMessageProvider.getOutboundMessageProviders('phone');
2930

3031
expect(providers).to.have.lengthOf(1);
31-
expect(providers[0]).to.deep.equal(phoneProvider);
32+
expect(providers[0]).to.deep.equal({
33+
providerId: '123',
34+
providerName: 'Test Phone Provider',
35+
providerType: 'phone',
36+
});
3237
});
3338

3439
it('should successfully register a email provider', () => {
@@ -44,7 +49,11 @@ describe('OutboundMessageProvider', () => {
4449
const providers = outboundMessageProvider.getOutboundMessageProviders('email');
4550

4651
expect(providers).to.have.lengthOf(1);
47-
expect(providers[0]).to.deep.equal(emailProvider);
52+
expect(providers[0]).to.deep.equal({
53+
providerId: '123',
54+
providerName: 'Test Email Provider',
55+
providerType: 'email',
56+
});
4857
});
4958

5059
it('should list currently registered providers [unfiltered]', () => {
@@ -69,8 +78,8 @@ describe('OutboundMessageProvider', () => {
6978
const providers = outboundMessageProvider.getOutboundMessageProviders();
7079

7180
expect(providers).to.have.lengthOf(2);
72-
expect(providers.some((provider) => provider.type === 'phone')).to.be.true;
73-
expect(providers.some((provider) => provider.type === 'email')).to.be.true;
81+
expect(providers.some((provider) => provider.providerType === 'phone')).to.be.true;
82+
expect(providers.some((provider) => provider.providerType === 'email')).to.be.true;
7483
});
7584

7685
it('should list currently registered providers [filtered by type]', () => {
@@ -95,7 +104,7 @@ describe('OutboundMessageProvider', () => {
95104
const providers = outboundMessageProvider.getOutboundMessageProviders('phone');
96105

97106
expect(providers).to.have.lengthOf(1);
98-
expect(providers[0].type).to.equal('phone');
107+
expect(providers[0].providerType).to.equal('phone');
99108
});
100109

101110
it('should unregister a provider', () => {
@@ -127,6 +136,6 @@ describe('OutboundMessageProvider', () => {
127136
registeredProviders = outboundMessageProvider.getOutboundMessageProviders('phone');
128137

129138
expect(registeredProviders).to.have.lengthOf(1);
130-
expect(registeredProviders.some((provider) => provider.appId !== '123')).to.be.true;
139+
expect(registeredProviders.some((provider) => provider.providerId !== '123')).to.be.true;
131140
});
132141
});

packages/apps-engine/src/definition/outboundComunication/IOutboundCommsProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface IOutboundMessageProviderBase {
1313
appId: string;
1414
name: string;
1515
documentationUrl?: string;
16+
supportsTemplates?: boolean;
1617
sendOutboundMessage(message: IOutboundMessage): Promise<void>;
1718
}
1819

packages/core-typings/src/omnichannel/outbound.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,14 @@ type TemplateParameter =
107107
export type IOutboundProvider = {
108108
providerId: string;
109109
providerName: string;
110-
supportsTemplates: boolean;
110+
supportsTemplates?: boolean;
111111
providerType: 'phone' | 'email';
112112
};
113113

114114
export type IOutboundProviderMetadata = IOutboundProvider & {
115115
templates: Record<string, IOutboundProviderTemplate[]>;
116116
};
117+
118+
export const ValidOutboundProviderList = ['phone', 'email'] as const;
119+
120+
export type ValidOutboundProvider = (typeof ValidOutboundProviderList)[number];

0 commit comments

Comments
 (0)