Skip to content

Commit

Permalink
feat(twilio): twilio v1 (#324)
Browse files Browse the repository at this point in the history
* delete old

* use legacy

* remove deps

* use channels 0.1.0

* bring changes

* gif

* feat(twilio): twilio v2

* bring changes

* file audio video location

* fix

* fix

* fix

* fix

* bring changes

* rewire comment

* fix

* fix

* fix

* bring changes

* bump

* fix merge

* fix

* add twilio to server
  • Loading branch information
samuelmasse committed Jan 29, 2022
1 parent 626ffb9 commit a1acd2f
Show file tree
Hide file tree
Showing 24 changed files with 341 additions and 8 deletions.
4 changes: 3 additions & 1 deletion packages/channels/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Router } from 'express'
import Joi from 'joi'
import { Channel } from '../src/base/channel'
import { TelegramChannel } from '../src/telegram/channel'
import { TwilioChannel } from '../src/twilio/channel'
import payloads from './payloads.json'

export class App {
constructor(private router: Router, private config: any) {}

async setup() {
await this.setupChannel('twilio', new TwilioChannel())
await this.setupChannel('telegram', new TelegramChannel())
}

Expand Down Expand Up @@ -56,7 +58,7 @@ export class App {
})

channel.makeUrl(async (scope: string) => {
return `${this.config.externalUrl}/webhooks/${scope}/${channel.meta.name}`
return `${this.config.externalUrl}/webhooks/v1/${scope}/${channel.meta.name}`
})

for (const [key, val] of Object.entries<any>(this.config.scopes)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/channels/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const setup = async () => {
await app.setup()

const port = 3100
exp.use('/webhooks', router)
exp.use('/webhooks/v1', router)
exp.listen(port)

console.info(`${clc.cyan('url')} ${config.externalUrl}`)
Expand Down
1 change: 1 addition & 0 deletions packages/channels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"lru-cache": "^6.0.0",
"ms": "^2.1.3",
"telegraf": "^4.6.0",
"twilio": "^3.73.1",
"uuid": "^8.3.2",
"yn": "^4.0.0"
}
Expand Down
1 change: 1 addition & 0 deletions packages/channels/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './base/channel'
export * from './base/endpoint'
export * from './twilio/channel'
export * from './telegram/channel'
29 changes: 29 additions & 0 deletions packages/channels/src/twilio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
### Sending

| Channels | Twilio |
| -------- | :----: |
| Text ||
| Image ||
| Choice ||
| Dropdown ||
| Card ||
| Carousel ||
| File ||
| Audio ||
| Video ||
| Location ||

### Receiving

| Channels | Twilio |
| ------------- | :----: |
| Text ||
| Quick Reply ||
| Postback ||
| Say Something ||
| Voice ||
| Image ||
| File ||
| Audio ||
| Video ||
| Location ||
39 changes: 39 additions & 0 deletions packages/channels/src/twilio/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import express, { Response } from 'express'
import { validateRequest } from 'twilio'
import yn from 'yn'
import { ChannelApi, ChannelApiManager, ChannelApiRequest } from '../base/api'
import { TwilioService } from './service'

export class TwilioApi extends ChannelApi<TwilioService> {
async setup(router: ChannelApiManager) {
router.use('/twilio', express.urlencoded({ extended: true }))
router.post('/twilio', this.handleRequest.bind(this))
}

private async handleRequest(req: ChannelApiRequest, res: Response) {
const { config } = this.service.get(req.scope)

const signature = req.headers['x-twilio-signature'] as string
const webhookUrl = await this.urlCallback!(req.scope)

if (validateRequest(config.authToken, signature, webhookUrl, req.body) || yn(process.env.TWILIO_TESTING)) {
await this.receive(req.scope, req.body)
res.sendStatus(204)
} else {
res.sendStatus(401)
}
}

private async receive(scope: string, body: any) {
const botPhoneNumber = body.To
const userPhoneNumber = body.From

const index = Number(body.Body)
const content = this.service.handleIndexResponse(scope, index, botPhoneNumber, userPhoneNumber) || {
type: 'text',
text: body.Body
}

await this.service.receive(scope, { identity: botPhoneNumber, sender: userPhoneNumber, thread: '*' }, content)
}
}
23 changes: 23 additions & 0 deletions packages/channels/src/twilio/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChannelTemplate } from '../base/channel'
import { TwilioApi } from './api'
import { TwilioConfig, TwilioConfigSchema } from './config'
import { TwilioService } from './service'
import { TwilioStream } from './stream'

export class TwilioChannel extends ChannelTemplate<TwilioConfig, TwilioService, TwilioApi, TwilioStream> {
get meta() {
return {
id: 'a711e325-7e71-4955-a76c-b46e62cdebd7',
name: 'twilio',
version: '1.0.0',
schema: TwilioConfigSchema,
initiable: false,
lazy: true
}
}

constructor() {
const service = new TwilioService()
super(service, new TwilioApi(service), new TwilioStream(service))
}
}
12 changes: 12 additions & 0 deletions packages/channels/src/twilio/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Joi from 'joi'
import { ChannelConfig } from '../base/config'

export interface TwilioConfig extends ChannelConfig {
accountSID: string
authToken: string
}

export const TwilioConfigSchema = {
accountSID: Joi.string().required(),
authToken: Joi.string().required()
}
8 changes: 8 additions & 0 deletions packages/channels/src/twilio/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { MessageListInstanceCreateOptions } from 'twilio/lib/rest/api/v2010/account/message'
import { ChannelContext, IndexChoiceOption } from '../base/context'
import { TwilioState } from './service'

export type TwilioContext = ChannelContext<TwilioState> & {
messages: Partial<MessageListInstanceCreateOptions>[]
prepareIndexResponse(scope: string, identity: string, sender: string, options: IndexChoiceOption[]): void
}
9 changes: 9 additions & 0 deletions packages/channels/src/twilio/renderers/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AudioRenderer } from '../../base/renderers/audio'
import { AudioContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioAudioRenderer extends AudioRenderer {
renderAudio(context: TwilioContext, payload: AudioContent) {
context.messages.push({ body: payload.title, mediaUrl: payload.audio })
}
}
57 changes: 57 additions & 0 deletions packages/channels/src/twilio/renderers/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IndexChoiceOption, IndexChoiceType } from '../../base/context'
import { CarouselRenderer, CarouselContext } from '../../base/renderers/carousel'
import { ActionOpenURL, ActionPostback, ActionSaySomething, CardContent, CarouselContent } from '../../content/types'
import { TwilioContext } from '../context'

type Context = CarouselContext<TwilioContext> & {
options: IndexChoiceOption[]
allOptions: IndexChoiceOption[]
index: number
}

export class TwilioCarouselRenderer extends CarouselRenderer {
startRender(context: Context, carousel: CarouselContent) {
context.allOptions = []
}

startRenderCard(context: Context, card: CardContent) {
context.options = []
}

renderButtonUrl(context: Context, button: ActionOpenURL) {
context.options.push({
type: IndexChoiceType.OpenUrl,
title: `${button.title} : ${button.url}`
})
}

renderButtonPostback(context: Context, button: ActionPostback) {
context.options.push({ type: IndexChoiceType.PostBack, title: button.title, value: button.payload })
}

renderButtonSay(context: Context, button: ActionSaySomething) {
context.options.push({
type: IndexChoiceType.SaySomething,
title: button.title,
value: button.text
})
}

endRenderCard(context: Context, card: CardContent) {
const body = `${card.title}\n\n${card.subtitle || ''}\n\n${context.options
.map(({ title }, idx) => `${idx + context.allOptions.length + 1}. ${title}`)
.join('\n')}`

context.channel.messages.push(<any>{ body, mediaUrl: card.image })
context.allOptions.push(...context.options)
}

endRender(context: Context, carousel: CarouselContent) {
context.channel.prepareIndexResponse(
context.channel.scope,
context.channel.identity,
context.channel.sender,
context.allOptions
)
}
}
25 changes: 25 additions & 0 deletions packages/channels/src/twilio/renderers/choices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IndexChoiceType } from '../../base/context'
import { ChoicesRenderer } from '../../base/renderers/choices'
import { ChoiceContent, ChoiceOption } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioChoicesRenderer extends ChoicesRenderer {
renderChoice(context: TwilioContext, payload: ChoiceContent) {
if (!context.messages.length) {
context.messages.push({})
}

const message = context.messages[0]

message.body = `${message.body || ''}\n\n${payload.choices
.map(({ title }, idx) => `${idx + 1}. ${title}`)
.join('\n')}`

context.prepareIndexResponse(
context.scope,
context.identity,
context.sender,
payload.choices.map((x: ChoiceOption) => ({ ...x, type: IndexChoiceType.QuickReply }))
)
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/twilio/renderers/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FileRenderer } from '../../base/renderers/file'
import { FileContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioFileRenderer extends FileRenderer {
renderFile(context: TwilioContext, payload: FileContent) {
context.messages.push({ body: `${payload.title}\n\n${payload.file}` })
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/twilio/renderers/image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ImageRenderer } from '../../base/renderers/image'
import { ImageContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioImageRenderer extends ImageRenderer {
renderImage(context: TwilioContext, payload: ImageContent) {
context.messages.push({ body: payload.title, mediaUrl: payload.image })
}
}
19 changes: 19 additions & 0 deletions packages/channels/src/twilio/renderers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { TwilioAudioRenderer } from './audio'
import { TwilioCarouselRenderer } from './carousel'
import { TwilioChoicesRenderer } from './choices'
import { TwilioFileRenderer } from './file'
import { TwilioImageRenderer } from './image'
import { TwilioLocationRenderer } from './location'
import { TwilioTextRenderer } from './text'
import { TwilioVideoRenderer } from './video'

export const TwilioRenderers = [
new TwilioTextRenderer(),
new TwilioImageRenderer(),
new TwilioCarouselRenderer(),
new TwilioChoicesRenderer(),
new TwilioFileRenderer(),
new TwilioAudioRenderer(),
new TwilioVideoRenderer(),
new TwilioLocationRenderer()
]
11 changes: 11 additions & 0 deletions packages/channels/src/twilio/renderers/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LocationRenderer } from '../../base/renderers/location'
import { LocationContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioLocationRenderer extends LocationRenderer {
renderLocation(context: TwilioContext, payload: LocationContent) {
const googleMapsLink = `https://www.google.com/maps/search/?api=1&query=${payload.latitude},${payload.longitude}`

context.messages.push({ body: `${payload.title}\n\n${googleMapsLink}` })
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/twilio/renderers/text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { TextRenderer } from '../../base/renderers/text'
import { TextContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioTextRenderer extends TextRenderer {
renderText(context: TwilioContext, payload: TextContent) {
context.messages.push({ body: payload.text })
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/twilio/renderers/video.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { VideoRenderer } from '../../base/renderers/video'
import { VideoContent } from '../../content/types'
import { TwilioContext } from '../context'

export class TwilioVideoRenderer extends VideoRenderer {
renderVideo(context: TwilioContext, payload: VideoContent) {
context.messages.push({ body: `${payload.title}\n\n${payload.video}` })
}
}
14 changes: 14 additions & 0 deletions packages/channels/src/twilio/senders/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CommonSender } from '../../base/senders/common'
import { TwilioContext } from '../context'

export class TwilioCommonSender extends CommonSender {
async send(context: TwilioContext) {
for (const message of context.messages) {
await context.state.twilio.messages.create({
...message,
from: context.identity,
to: context.sender
})
}
}
}
3 changes: 3 additions & 0 deletions packages/channels/src/twilio/senders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { TwilioCommonSender } from './common'

export const TwilioSenders = [new TwilioCommonSender()]
16 changes: 16 additions & 0 deletions packages/channels/src/twilio/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Twilio } from 'twilio'
import { ChannelService, ChannelState } from '../base/service'
import { TwilioConfig } from './config'

export interface TwilioState extends ChannelState<TwilioConfig> {
twilio: Twilio
}

export class TwilioService extends ChannelService<TwilioConfig, TwilioState> {
async create(scope: string, config: TwilioConfig) {
return {
config,
twilio: new Twilio(config.accountSID, config.authToken)
}
}
}
27 changes: 27 additions & 0 deletions packages/channels/src/twilio/stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ChannelContext } from '../base/context'
import { CardToCarouselRenderer } from '../base/renderers/card'
import { DropdownToChoicesRenderer } from '../base/renderers/dropdown'
import { TypingSender } from '../base/senders/typing'
import { ChannelStream } from '../base/stream'
import { TwilioContext } from './context'
import { TwilioRenderers } from './renderers'
import { TwilioSenders } from './senders'
import { TwilioService } from './service'

export class TwilioStream extends ChannelStream<TwilioService, TwilioContext> {
get renderers() {
return [new CardToCarouselRenderer(), new DropdownToChoicesRenderer(), ...TwilioRenderers]
}

get senders() {
return [new TypingSender(), ...TwilioSenders]
}

protected async getContext(base: ChannelContext<any>): Promise<TwilioContext> {
return {
...base,
messages: [],
prepareIndexResponse: this.service.prepareIndexResponse.bind(this.service)
}
}
}

0 comments on commit a1acd2f

Please sign in to comment.