diff --git a/lib/bootstrap/index.ts b/lib/bootstrap/index.ts new file mode 100644 index 0000000..9ed0bf6 --- /dev/null +++ b/lib/bootstrap/index.ts @@ -0,0 +1,2 @@ +export * from './twilio.channel'; +export * from './twilio-channel.message'; diff --git a/lib/bootstrap/twilio-channel.message.ts b/lib/bootstrap/twilio-channel.message.ts new file mode 100644 index 0000000..b7f18cb --- /dev/null +++ b/lib/bootstrap/twilio-channel.message.ts @@ -0,0 +1,137 @@ +import { TwilioChannelModuleOptions } from '../interfaces'; +import { TwilioChannelException } from '../exceptions'; + +/** + * Service name message + * @class TwilioChannelMessage + */ +export class TwilioChannelMessage { + /** + * @property + * @private + */ + private accountSid: string; + + /** + * @property + * @private + */ + private authToken: string; + + /** + * @property + * @private + */ + private sender: string; + + /** + * @property + * @private + */ + private message: string; + + /** + * @property + * @private + */ + private toPhoneNumber: string; + + /** + * @constructor + * @param {TwilioChannelModuleOptions} options + */ + constructor(options?: TwilioChannelModuleOptions) { + this.accountSid = options?.twilioAccountSid; + this.authToken = options?.twilioAuthToken; + this.sender = options?.twilioSender; + } + + /** + * @method + */ + get getAccountSid() { + return this.accountSid; + } + + /** + * @method + * @param sid + */ + setAccountSid(sid: string) { + this.accountSid = sid; + + return this; + } + + /** + * @method + */ + get getAccountToken() { + return this.authToken; + } + + /** + * @method + * @param token + */ + setAccountToken(token: string) { + this.authToken = token; + + return this; + } + + /** + * @method + */ + get getSender() { + return this.sender; + } + + /** + * @method + * @param sender + */ + setSender(sender: string) { + this.sender = sender; + + return this; + } + + /** + * @method + */ + get getToPhoneNumber() { + return this.toPhoneNumber; + } + + /** + * @method + * @param toPhoneNumber + */ + setToPhoneNumber(toPhoneNumber: string) { + if (!toPhoneNumber.startsWith('+')) { + throw new TwilioChannelException('Your phone number must start with +.'); + } + + this.toPhoneNumber = toPhoneNumber; + + return this; + } + + /** + * @method + */ + get getMessage() { + return this.message; + } + + /** + * @method + * @param message + */ + setMessage(message: string) { + this.message = message; + + return this; + } +} diff --git a/lib/bootstrap/twilio.channel.ts b/lib/bootstrap/twilio.channel.ts new file mode 100644 index 0000000..be57ebe --- /dev/null +++ b/lib/bootstrap/twilio.channel.ts @@ -0,0 +1,147 @@ +import { Inject, Injectable, Optional } from '@nestjs/common'; +import { INestjsNotificationChannel } from '@sinuos/nestjs-notification'; +import { Twilio } from 'twilio'; +import { TWILIO_CHANNEL_OPTIONS } from '../constants'; +import { ITwilioChannel, TwilioChannelModuleOptions } from '../interfaces'; +import { TwilioChannelException } from '../exceptions'; + +@Injectable() +export class TwilioChannel implements INestjsNotificationChannel { + /** + * @property + * @private + */ + private readonly accountSid = process.env.TWILIO_ACCOUNT_SID; + + /** + * @property + * @private + */ + private readonly authToken = process.env.TWILIO_AUTH_TOKEN; + + /** + * @property + * @private + */ + private readonly sender = process.env.TWILIO_SENDER; + + /** + * Twilio channel constructor. + * @constructor + * @param {TwilioChannelModuleOptions} options + */ + constructor( + @Optional() + @Inject(TWILIO_CHANNEL_OPTIONS) + options: TwilioChannelModuleOptions, + ) { + if (options?.twilioAccountSid) { + this.accountSid = options.twilioAccountSid; + } + + if (options?.twilioAuthToken) { + this.authToken = options.twilioAuthToken; + } + + if (options?.twilioSender) { + this.sender = options.twilioSender; + } + } + + /** + * Send notify action + * @public + * @param {ITwilioChannel} notification + * @return Promise> + */ + public async send(notification: ITwilioChannel): Promise { + // Validator notification requirements. + this.validator(notification); + + // get message content. + const message = TwilioChannel.getData(notification); + + // twilio username. + const username = this.accountSid ?? message.getAccountSid; + + // twilio password + const password = this.authToken ?? message.getAccountToken; + + // sender + const sender = this.sender || message.getSender; + + /** @const twilioClient - Init twilio client */ + const twilioClient = new Twilio(username, password); + + // return twilio message instance + try { + return await twilioClient.messages.create({ + body: message.getMessage, + to: message.getToPhoneNumber, + from: sender, + }); + } catch (e) { + throw new TwilioChannelException(e.message); + } + } + + /** + * Get data. + * @method + * @param {ITwilioChannel} notification + * @private + */ + private static getData(notification: ITwilioChannel) { + return TwilioChannel.getChannelData(notification); + } + + /** + * Validator. + * @method + * @param {ITwilioChannel} notification + * @private + */ + private validator(notification: ITwilioChannel) { + const message = TwilioChannel.getData(notification); + + /** Account sid is empty */ + if (!this.accountSid && !message.getAccountSid) { + throw new TwilioChannelException('Account sid is required'); + } + + /** Auth token is empty */ + if (!this.authToken && !message.getAccountToken) { + throw new TwilioChannelException('Account token is required'); + } + + /** Sender is empty */ + if (!this.sender && !message.getSender) { + throw new TwilioChannelException('Sender is required'); + } + + /** Phone number is empty */ + if (!message.getToPhoneNumber) { + throw new TwilioChannelException('To phone number is required'); + } + + /** Message is empty */ + if (!message.getMessage) { + throw new TwilioChannelException('Message is required'); + } + } + + /** + * Get the data for the notification. + * @method + * @param notification + */ + private static getChannelData(notification: ITwilioChannel) { + if (typeof notification.toTwilio === 'function') { + return notification.toTwilio(); + } + + throw new TwilioChannelException( + 'Notification is missing toTwilio method.', + ); + } +} diff --git a/lib/constants/index.ts b/lib/constants/index.ts new file mode 100644 index 0000000..6744262 --- /dev/null +++ b/lib/constants/index.ts @@ -0,0 +1 @@ +export * from './twilio.constant'; diff --git a/lib/constants/twilio.constant.ts b/lib/constants/twilio.constant.ts new file mode 100644 index 0000000..a0a027c --- /dev/null +++ b/lib/constants/twilio.constant.ts @@ -0,0 +1 @@ +export const TWILIO_CHANNEL_OPTIONS = 'TWILIO_CHANNEL_OPTIONS'; diff --git a/lib/exceptions/index.ts b/lib/exceptions/index.ts new file mode 100644 index 0000000..c7a1dba --- /dev/null +++ b/lib/exceptions/index.ts @@ -0,0 +1 @@ +export * from './twilio-channel.exception'; diff --git a/lib/exceptions/twilio-channel.exception.ts b/lib/exceptions/twilio-channel.exception.ts new file mode 100644 index 0000000..66307a2 --- /dev/null +++ b/lib/exceptions/twilio-channel.exception.ts @@ -0,0 +1,15 @@ +/** + * @class TwilioChannelException + * @extends Error + */ +export class TwilioChannelException extends Error { + /** + * @constructor + * @param message + */ + constructor(message: string) { + super(message); + + this.name = 'TwilioChannelException'; + } +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..bc5929c --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,3 @@ +export * from './bootstrap'; +export * from './interfaces'; +export * from './twilio-channel.module'; diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts new file mode 100644 index 0000000..0e501d9 --- /dev/null +++ b/lib/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './twilio-channel-module.interface'; +export * from './twilio-channel.interface'; diff --git a/lib/interfaces/twilio-channel-module.interface.ts b/lib/interfaces/twilio-channel-module.interface.ts new file mode 100644 index 0000000..e215a80 --- /dev/null +++ b/lib/interfaces/twilio-channel-module.interface.ts @@ -0,0 +1,42 @@ +import { ModuleMetadata, Provider, Type } from '@nestjs/common'; + +/** + * @interface TwilioChannelModuleOptionsFactory + * @property createTwilioChannelOptions() + */ +export interface TwilioChannelModuleOptionsFactory { + createTwilioChannelOptions(): + | Promise + | TwilioChannelModuleOptions; +} + +/** + * @interface TwilioChannelModuleAsyncOptions + * @extends {Pick} + * @property useExisting + * @property useClass + * @property useFactory + * @property inject + * @property extraProviders + */ +export interface TwilioChannelModuleAsyncOptions + extends Pick { + useExisting?: Type; + useClass?: Type; + useFactory?: ( + ...args: any[] + ) => Promise | TwilioChannelModuleOptions; + inject?: any[]; + extraProviders?: Provider[]; +} + +/** + * @interface TwilioChannelModuleOptions + * @property {string} twilioAccountSid + * @property {string} twilioAuthToken + */ +export interface TwilioChannelModuleOptions { + twilioAccountSid?: string; + twilioAuthToken?: string; + twilioSender?: string; +} diff --git a/lib/interfaces/twilio-channel.interface.ts b/lib/interfaces/twilio-channel.interface.ts new file mode 100644 index 0000000..7531b20 --- /dev/null +++ b/lib/interfaces/twilio-channel.interface.ts @@ -0,0 +1,15 @@ +import { NestJsNotification } from '@sinuos/nestjs-notification'; + +/** + * Twilio channel model + * @interface ITwilioChannel + * @extends NestJsNotification + */ +export interface ITwilioChannel extends NestJsNotification { + /** + * Get representation of the notification. + * @property + * @returns {any} + */ + toTwilio?(): any; +} diff --git a/lib/twilio-channel.module.ts b/lib/twilio-channel.module.ts new file mode 100644 index 0000000..2bfe3f7 --- /dev/null +++ b/lib/twilio-channel.module.ts @@ -0,0 +1,129 @@ +import { + DynamicModule, + Global, + Module, + Provider, + ValueProvider, +} from '@nestjs/common'; +import { TwilioChannel } from './bootstrap'; +import { + TwilioChannelModuleAsyncOptions, + TwilioChannelModuleOptions, + TwilioChannelModuleOptionsFactory, +} from './interfaces'; +import { TWILIO_CHANNEL_OPTIONS } from './constants'; + +@Global() +@Module({}) +export class TwilioChannelModule { + /** + * Register module + * @static + * @param {TwilioChannelModuleOptions} options + * @return DynamicModule + */ + static register(options: TwilioChannelModuleOptions): DynamicModule { + const channelProvider: ValueProvider = { + provide: TWILIO_CHANNEL_OPTIONS, + useValue: options, + }; + + return { + module: TwilioChannelModule, + providers: [TwilioChannel, channelProvider], + exports: [TwilioChannel, channelProvider], + }; + } + + /** + * Register async + * @static + * @param {TwilioChannelModuleAsyncOptions} options + * @return DynamicModule + */ + static registerAsync( + options: TwilioChannelModuleAsyncOptions, + ): DynamicModule { + const channelProvider: ValueProvider = { + provide: TWILIO_CHANNEL_OPTIONS, + useValue: options, + }; + + return { + module: TwilioChannelModule, + imports: options.imports || [], + providers: [ + TwilioChannel, + channelProvider, + ...this.createAsyncProviders(options), + ], + exports: [TwilioChannel, channelProvider], + }; + } + + /** + * Create async providers + * @private + * @param {TwilioChannelModuleAsyncOptions} options + * @return Provider[] + */ + private static createAsyncProviders( + options: TwilioChannelModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncConfigProvider(options)]; + } else if (!options.useClass) { + return [ + { + provide: TWILIO_CHANNEL_OPTIONS, + useValue: {}, + inject: options.inject || [], + }, + ]; + } + + return [ + this.createAsyncConfigProvider(options), + { + provide: options.useClass, + useClass: options.useClass, + }, + ]; + } + + /** + * Create async config provider + * @private + * @param {TwilioChannelModuleAsyncOptions} options + * @return Provider + */ + private static createAsyncConfigProvider( + options: TwilioChannelModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: TWILIO_CHANNEL_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + + const inject = options.useClass || options.useExisting; + + if (!inject) { + throw new Error( + 'Invalid configuration. Must provide useFactory, useClass or useExisting', + ); + } + + return { + provide: TWILIO_CHANNEL_OPTIONS, + async useFactory( + optionsFactory: TwilioChannelModuleOptionsFactory, + ): Promise { + return optionsFactory.createTwilioChannelOptions(); + }, + inject: [inject], + }; + } +}