Skip to content

Commit

Permalink
feat(provider): send notifications/messages to whatsapp or as text SM…
Browse files Browse the repository at this point in the history
…S using Twilio (#91)

* feat(provider): send notifications/messages to whatsapp or as text SMS using TWILIO(#88)

GH-88

* feat(provider): send notifications/messages to whatsapp or as text SMS using TWILIO #88

GH-88

* send notifications/messages to whatsapp or as text SMS using Twilio

* send notifications/messages to whatsapp or as text SMS using Twilio

* send notifications/messages to whatsapp or as text SMS using Twilio

* send notifications/messages to whatsapp or as text SMS using Twilio

* send notifications/messages to whatsapp or as text SMS using Twilio

* send notifications/messages to whatsapp or as text SMS using Twilio
  • Loading branch information
sadarunnisa-sf committed Mar 10, 2023
1 parent 6f035a6 commit f9d9799
Show file tree
Hide file tree
Showing 10 changed files with 395 additions and 4 deletions.
70 changes: 68 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ It provides a generic provider-based framework to add your own implementation or
5. [FCM](https://firebase.google.com/docs/cloud-messaging) - It's one of the PushProvider for sending realtime push notifications to mobile applications as well as web applications.
6. [Nodemailer](https://nodemailer.com/about/) - It's one of the EmailProvider for sending email messages.
7. [Apple Push Notification service](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1) - It's one of the push notification providers that integrates notification service created by Apple Inc. that enables third party application developers to send notification data to applications installed on Apple devices.
8. [Twilio SMS Service](https://www.twilio.com/docs/sms, https://www.twilio.com/docs/whatsapp) - Twilio is a modern communication API Used by developers for establishing communications. Twilio can be used for sending SMS or Whatapp notifications.
You can use one of these services or add your own implementation or integration using the same interfaces and attach it as a provider for that specific type.

You can use one of these services or add your own implementation or integration using the same interfaces and attach it as a provider for that specific type.

Expand Down Expand Up @@ -281,7 +283,7 @@ If you wish to use any other service provider of your choice, you can create a p
this.bind(NotificationBindings.EmailProvider).toProvider(MyOwnProvider);
```

### SMS Notifications
### SMS Notifications using AWS SNS

This extension provides in-built support of AWS Simple Notification Service integration for sending SMS from the application. In order to use it, run `npm install aws-sdk`, and then bind the SnsProvider as below in `application.ts`.

Expand Down Expand Up @@ -341,7 +343,71 @@ If you wish to use any other service provider of your choice, you can create a p
this.bind(NotificationBindings.SMSProvider).toProvider(MyOwnProvider);
```

### Push Notifications With Pubnub

### SMS / Whatsapp Notifications using Twilio

This extension provides in-built support of Twilio integration for sending SMS / whatsapp notifications from the application. In order to use it, run `npm install twilio`, and then bind the TwilioProvider as below in application.ts.

```ts
import {
NotificationsComponent,
NotificationBindings,
} from 'loopback4-notifications';
import {
TwilioProvider
} from 'loopback4-notification/twilio';
....

export class NotificationServiceApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
....

this.component(NotificationsComponent);
this.bind(NotificationBindings.SMSProvider).toProvider(TwilioProvider);
....
}
}
```

There are some additional configurations needed in order to connect to Twilio. You need to add them as below. Make sure these are added before the provider binding.

```ts
import {
NotificationsComponent,
NotificationBindings,
} from 'loopback4-notifications';
import {
TwilioBindings,
TwilioProvider
} from 'loopback4-notification/twilio';
....

export class NotificationServiceApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
....

this.component(NotificationsComponent);
this.bind(TwilioBindings.Config).to({
accountSid: process.env.TWILIO_ACCOUNT_SID,
authToken: process.env.TWILIO_AUTH_TOKEN,
waFrom: process.env.TWILIO_WA_FROM,
smsFrom: process.env.TWILIO_SMS_FROM,
waStatusCallback:process.env.TWILIO_WA_STATUS_CALLBACK,
smsStatusCallback:process.env.TWILIO_SMS_STATUS_CALLBACK,
});
this.bind(NotificationBindings.SMSProvider).toProvider(TwilioProvider);
....
}
}
```

All the configurations as specified by Twilio docs and console are supported in above TwilioBindings Config key. smsFrom could be messaging service id, twilio number or short code. waFrom could be whats app number or number associated to channel.

### Push Notifications with Pubnub

This extension provides in-built support of Pubnub integration for sending realtime push notifications from the application. In order to use it, run `npm install pubnub`, and then bind the PushProvider as below in `application.ts`.

Expand Down
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
"./sns": {
"type": "./dist/providers/sms/sns/index.d.ts",
"default": "./dist/providers/sms/sns/index.js"
},
"./twilio": {
"type": "./dist/providers/sms/twilio/index.d.ts",
"default": "./dist/providers/sms/twilio/index.js"
}
},
"typesVersions": {
Expand All @@ -61,6 +65,9 @@
],
"sns": [
"./dist/providers/sms/sns/index.d.ts"
],
"twilio": [
"./dist/providers/sms/twilio/index.d.ts"
]
}
},
Expand Down Expand Up @@ -140,7 +147,8 @@
"simple-git": "^3.15.1",
"socket.io-client": "^4.5.1",
"source-map-support": "^0.5.21",
"typescript": "~4.9.4"
"typescript": "~4.9.4",
"twilio": "^3.82.0"
},
"overrides": {
"@parse/node-apn": {
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/mock-sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import AWS from 'aws-sdk';
import Pubnub from 'pubnub';
import twilio, {Twilio} from 'twilio';
import {TwilioAuthConfig, TwilioMessage} from '../providers';
import Mail = require('nodemailer/lib/mailer');
import SMTPTransport = require('nodemailer/lib/smtp-transport');

Expand Down Expand Up @@ -38,3 +40,13 @@ export class MockPubnub {
grant(grantConfig: Pubnub.GrantParameters) {}
async publish(publishConfig: Pubnub.PublishParameters) {}
}

export class MockTwilio {
twilioService: Twilio;
constructor(config: TwilioAuthConfig) {
this.twilioService = twilio(config.accountSid, config.authToken);
}
// sonarignore:start
// this is intensional
async publish(message: TwilioMessage) {}
// sonarignore:end
157 changes: 157 additions & 0 deletions src/__tests__/unit/twilio.provider.unit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {Constructor} from '@loopback/core';
import {expect, sinon} from '@loopback/testlab';
import proxyquire from 'proxyquire';
import {TwilioMessage, TwilioProvider} from '../../providers';

describe('Twilio Service', () => {
const message: TwilioMessage = {
receiver: {
to: [],
},
body: 'test',
sentDate: new Date(),
type: 0,
subject: undefined,
};
const messageText: TwilioMessage = {
receiver: {
to: [
{
id: 'XXXXXXXXXXX',
type: 1,
},
],
},
body: 'Test SMS Text Notification',
sentDate: new Date(),
type: 2,
subject: undefined,
};
const messageTextMedia: TwilioMessage = {
receiver: {
to: [
{
id: 'XXXXXXXXXXX',
type: 1,
},
],
},
body: 'Test SMS Notification with media',
mediaUrl: ['https://demo.twilio.com/owl.png'],
sentDate: new Date(),
type: 2,
subject: undefined,
};
const messageWhatsApp: TwilioMessage = {
receiver: {
to: [
{
id: 'XXXXXXXXXXX',
type: 0,
},
],
},
body: 'Test Whatsapp Notification',
sentDate: new Date(),
type: 2,
subject: undefined,
};
const messageWAMedia: TwilioMessage = {
receiver: {
to: [
{
id: 'XXXXXXXXXXX',
type: 0,
},
],
},
body: 'Test Whatsapp message with media',
mediaUrl: ['https://demo.twilio.com/owl.png'],
sentDate: new Date(),
type: 2,
subject: undefined,
};
const configration = {
accountSid: 'ACTSIDDUMMY',
authToken: 'AUTHDUMMY',
waFrom: '', //Ex. whatsapp:+XXXXXXXXXXX
smsFrom: '',
opts: {dummy: true}, //Change dummy value to false when using unit test
};

let TwilioProviderMock: Constructor<TwilioProvider>;
beforeEach(setupMockTwilio);
describe('twilio configration addition', () => {
it('returns error message on passing reciever length as zero', async () => {
const twilioProvider = new TwilioProviderMock(configration).value();
const result = await twilioProvider
.publish(message)
.catch(err => err.message);
expect(result).which.eql('Message receiver not found in request');
});

it('returns error message when no twilio config', async () => {
try {

// NOSONAR
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const twilioProvider = new TwilioProvider();

} catch (err) {
const result = err.message;
expect(result).which.eql('Twilio Config missing !');
}
});

it('returns the message (SMS text)', async () => {
const twilioProvider = new TwilioProviderMock(configration).value();
const result = twilioProvider.publish(messageText);
if (configration.opts?.dummy) {
expect(result).to.have.Promise();
} else {
await expect(result).to.be.fulfilled();
}
});

it('returns the message (SMS with media)', async () => {
const twilioProvider = new TwilioProviderMock(configration).value();
const result = twilioProvider.publish(messageTextMedia);
if (configration.opts?.dummy) {
expect(result).to.have.Promise();
} else {
await expect(result).to.be.fulfilled();
}
});

it('returns the message (Whatsapp)', async () => {
const twilioProvider = new TwilioProviderMock(configration).value();
const result = twilioProvider.publish(messageWhatsApp);
if (configration.opts?.dummy) {
expect(result).to.have.Promise();
} else {
await expect(result).to.be.fulfilled();
}
});

it('returns the message (Whatsapp with Media)', async () => {
const twilioProvider = new TwilioProviderMock(configration).value();
const result = twilioProvider.publish(messageWAMedia);
if (configration.opts?.dummy) {
expect(result).to.have.Promise();
} else {
await expect(result).to.be.fulfilled();
}
});
});

function setupMockTwilio() {
const mockTwilio = sinon.stub();
mockTwilio.prototype.publish = sinon.stub().returns(Promise.resolve());
TwilioProviderMock = proxyquire(
'../../providers/sms/twilio/twilio.provider',
{
'twilio.twilio': mockTwilio,
},
).TwilioProvider;
}
});
1 change: 1 addition & 0 deletions src/providers/sms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './sns';
export * from './twilio';
export * from './types';
3 changes: 3 additions & 0 deletions src/providers/sms/twilio/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './keys';
export * from './twilio.provider';
export * from './types';
8 changes: 8 additions & 0 deletions src/providers/sms/twilio/keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {BindingKey} from '@loopback/core';
import {TwilioAuthConfig} from '../twilio/types';

export namespace TwilioBindings {
export const Config = BindingKey.create<TwilioAuthConfig | null>(
'sf.notification.config.twilio',
);
}
77 changes: 77 additions & 0 deletions src/providers/sms/twilio/twilio.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {inject, Provider} from '@loopback/core';
import {HttpErrors} from '@loopback/rest';
import twilio, {Twilio} from 'twilio';
import {TwilioBindings} from './keys';

import {
TwilioAuthConfig,
TwilioCreateMessageParams,
TwilioMessage,
TwilioNotification,
TwilioSubscriberType,
} from '../twilio/types';

export class TwilioProvider implements Provider<TwilioNotification> {
twilioService: Twilio;
constructor(
@inject(TwilioBindings.Config, {
optional: true,
})
private readonly twilioConfig?: TwilioAuthConfig,
) {
if (this.twilioConfig) {
this.twilioService = twilio(
this.twilioConfig.accountSid,
this.twilioConfig.authToken,
);
} else {
throw new HttpErrors.PreconditionFailed('Twilio Config missing !');
}
}

value() {
return {
publish: async (message: TwilioMessage) => {
if (message.receiver.to.length === 0) {
throw new HttpErrors.BadRequest(
'Message receiver not found in request',
);
}
const publishes = message.receiver.to.map(async receiver => {
const msg: string = message.body;
const twilioMsgObj: TwilioCreateMessageParams = {
body: msg,
from:
receiver.type &&
receiver.type === TwilioSubscriberType.TextSMSUser
? String(this.twilioConfig?.smsFrom)
: String(this.twilioConfig?.waFrom),
to:
receiver.type &&
receiver.type === TwilioSubscriberType.TextSMSUser
? `+${receiver.id}`
: `whatsapp:+${receiver.id}`,
};

// eslint-disable-next-line no-unused-expressions
message.mediaUrl && (twilioMsgObj.mediaUrl = message.mediaUrl);

// eslint-disable-next-line no-unused-expressions
receiver.type &&
receiver.type === TwilioSubscriberType.TextSMSUser &&
this.twilioConfig?.smsStatusCallback &&
(twilioMsgObj.statusCallback =
this.twilioConfig?.smsStatusCallback);

// eslint-disable-next-line no-unused-expressions
!receiver.type &&
this.twilioConfig?.waStatusCallback &&
(twilioMsgObj.statusCallback = this.twilioConfig?.waStatusCallback);

return this.twilioService.messages.create(twilioMsgObj);
});
await Promise.all(publishes);
},
};
}
}
Loading

0 comments on commit f9d9799

Please sign in to comment.