Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
476a827
feat: adds new endpoints to interact with outbound providers
lucas-a-pelegrino Jun 24, 2025
a12b5d4
refactor: moves outbound endpoints to correct place
lucas-a-pelegrino Jun 24, 2025
0b15f6d
refactor: improve business logic for outbound comms
lucas-a-pelegrino Jun 26, 2025
ef12314
refactor: adds more minor improvements to listOutboundPRoviders logic
lucas-a-pelegrino Jun 27, 2025
e3767f2
tests: adds unit testing for OutboundMessageProviderService
lucas-a-pelegrino Jul 3, 2025
8d16c0b
chore: adds a minor improvement on OutboundMessageProviderService
lucas-a-pelegrino Jul 8, 2025
517c4dd
Merge branch 'develop' into feat/CTZ-182
KevLehman Jul 8, 2025
0facbdc
fix
KevLehman Jul 8, 2025
d9e2196
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into feat…
lucas-a-pelegrino Jul 8, 2025
7e0ba9a
fix: merge conflicts
lucas-a-pelegrino Jul 8, 2025
17586cd
chore: adds minor improvements to typing to ensure reusability across…
lucas-a-pelegrino Jul 8, 2025
5e37cc9
fix 2
KevLehman Jul 8, 2025
03e6f77
Create new-mails-rhyme.md
KevLehman Jul 8, 2025
3c4d938
fix: merge conflicts
lucas-a-pelegrino Jul 8, 2025
a7c4892
Update apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts
KevLehman Jul 9, 2025
f0a5f34
Merge branch 'feat/CTZ-182' of github.com:RocketChat/Rocket.Chat into…
lucas-a-pelegrino Jul 9, 2025
84edd67
Update apps/meteor/ee/app/livechat-enterprise/server/api/lib/outbound.ts
KevLehman Jul 9, 2025
5721a3f
chore: refactor endpoints definition to meet new pattern
lucas-a-pelegrino Jul 9, 2025
d656934
Merge branch 'feat/CTZ-182' of github.com:RocketChat/Rocket.Chat into…
lucas-a-pelegrino Jul 9, 2025
e195b37
fix: imports mismatches, ajv typings
lucas-a-pelegrino Jul 9, 2025
6cbd5fd
Merge branch 'develop' into feat/CTZ-182
kodiakhq[bot] Jul 10, 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
7 changes: 7 additions & 0 deletions .changeset/new-mails-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/apps-engine": minor
"@rocket.chat/core-typings": minor
---

Adds new endpoints for outbound communications
Original file line number Diff line number Diff line change
@@ -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[];
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
49 changes: 49 additions & 0 deletions apps/meteor/ee/app/livechat-enterprise/server/api/outbound.ts
Original file line number Diff line number Diff line change
@@ -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<typeof outboundCommsEndpoints>;
Original file line number Diff line number Diff line change
@@ -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 };
Expand Down
25 changes: 19 additions & 6 deletions apps/meteor/server/lib/OutboundMessageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValidOutboundProvider, IOutboundMessageProviders[]>;

constructor() {
this.outboundMessageProviders = new Map([
Expand All @@ -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;
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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', () => {
Expand All @@ -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]', () => {
Expand All @@ -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]', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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;
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface IOutboundMessageProviderBase {
appId: string;
name: string;
documentationUrl?: string;
supportsTemplates?: boolean;
sendOutboundMessage(message: IOutboundMessage): Promise<void>;
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core-typings/src/omnichannel/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, IOutboundProviderTemplate[]>;
};

export const ValidOutboundProviderList = ['phone', 'email'] as const;

export type ValidOutboundProvider = (typeof ValidOutboundProviderList)[number];
Loading