Skip to content

Commit

Permalink
feat(messenger): messenger v1 (#329)
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

* bring changes

* fix

* fix

* bring changes

* rewire comment

* fix

* start

* message reception

* fix

* send text

* refact

* readme

* image

* single-choice

* fix

* bring changes

* bump

* fix merge

* use channel in server

* quick_reply

* carousel

* refact

* fix

* audio video location

* file

* readme
  • Loading branch information
samuelmasse committed Jan 29, 2022
1 parent a1acd2f commit 6717e0a
Show file tree
Hide file tree
Showing 27 changed files with 532 additions and 7 deletions.
2 changes: 2 additions & 0 deletions packages/channels/example/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import clc from 'cli-color'
import { Router } from 'express'
import Joi from 'joi'
import { Channel } from '../src/base/channel'
import { MessengerChannel } from '../src/messenger/channel'
import { TelegramChannel } from '../src/telegram/channel'
import { TwilioChannel } from '../src/twilio/channel'
import payloads from './payloads.json'
Expand All @@ -10,6 +11,7 @@ export class App {
constructor(private router: Router, private config: any) {}

async setup() {
await this.setupChannel('messenger', new MessengerChannel())
await this.setupChannel('twilio', new TwilioChannel())
await this.setupChannel('telegram', new TelegramChannel())
}
Expand Down
4 changes: 2 additions & 2 deletions packages/channels/example/payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
},
{
"type": "file",
"title": "here is a zip of the 1.0.0 messaging codebase",
"file": "https://github.com/botpress/messaging/archive/refs/tags/v1.0.0.zip",
"title": "here is a zip of the 0.0.1 messaging codebase",
"file": "https://github.com/botpress/messaging/archive/refs/tags/v0.0.1.zip",
"typing": true
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/channels/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './base/channel'
export * from './base/endpoint'
export * from './messenger/channel'
export * from './twilio/channel'
export * from './telegram/channel'
29 changes: 29 additions & 0 deletions packages/channels/src/messenger/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 ||
93 changes: 93 additions & 0 deletions packages/channels/src/messenger/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import crypto from 'crypto'
import express, { Response, Request, NextFunction } from 'express'
import { IncomingMessage } from 'http'
import { ChannelApi, ChannelApiManager, ChannelApiRequest } from '../base/api'
import { MessengerMessage, MessengerPayload } from './messenger'
import { POSTBACK_PREFIX, SAY_PREFIX } from './renderers/carousel'
import { MessengerService } from './service'

export class MessengerApi extends ChannelApi<MessengerService> {
async setup(router: ChannelApiManager) {
router.use('/messenger', express.json({ verify: this.prepareAuth.bind(this) }))
router.get('/messenger', this.handleWebhookVerification.bind(this))

router.post('/messenger', this.auth.bind(this))
router.post('/messenger', this.handleMessageRequest.bind(this))
}

private prepareAuth(_req: IncomingMessage, res: Response, buffer: Buffer, _encoding: string) {
res.locals.authBuffer = Buffer.from(buffer)
}

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

const mode = req.query['hub.mode']
const token = req.query['hub.verify_token']
const challenge = req.query['hub.challenge']

if (mode === 'subscribe' && token === config.verifyToken) {
res.status(200).send(challenge)
} else {
res.sendStatus(403)
}
}

private async auth(req: Request, res: Response, next: NextFunction) {
const signature = req.headers['x-hub-signature'] as string
const [, hash] = signature.split('=')

const { config } = this.service.get(req.params.scope)
const expectedHash = crypto.createHmac('sha1', config.appSecret).update(res.locals.authBuffer).digest('hex')

if (hash !== expectedHash) {
return res.sendStatus(403)
} else {
next()
}
}

private async handleMessageRequest(req: ChannelApiRequest, res: Response) {
const payload = req.body as MessengerPayload

for (const { messaging } of payload.entry) {
for (const message of messaging) {
await this.receive(req.scope, message)
}
}

res.status(200).send('EVENT_RECEIVED')
}

private async receive(scope: string, message: MessengerMessage) {
if (message.message) {
if (message.message?.quick_reply?.payload) {
await this.service.receive(scope, this.extractEndpoint(message), {
type: 'quick_reply',
text: message.message.text,
payload: message.message.quick_reply.payload
})
} else {
await this.service.receive(scope, this.extractEndpoint(message), { type: 'text', text: message.message.text })
}
} else if (message.postback) {
const payload = message.postback.payload

if (payload.startsWith(SAY_PREFIX)) {
await this.service.receive(scope, this.extractEndpoint(message), {
type: 'say_something',
text: payload.replace(SAY_PREFIX, '')
})
} else if (payload.startsWith(POSTBACK_PREFIX)) {
await this.service.receive(scope, this.extractEndpoint(message), {
type: 'postback',
payload: payload.replace(POSTBACK_PREFIX, '')
})
}
}
}

private extractEndpoint(message: MessengerMessage) {
return { identity: '*', sender: message.sender.id, thread: '*' }
}
}
28 changes: 28 additions & 0 deletions packages/channels/src/messenger/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChannelTemplate } from '../base/channel'
import { MessengerApi } from './api'
import { MessengerConfig, MessengerConfigSchema } from './config'
import { MessengerService } from './service'
import { MessengerStream } from './stream'

export class MessengerChannel extends ChannelTemplate<
MessengerConfig,
MessengerService,
MessengerApi,
MessengerStream
> {
get meta() {
return {
id: 'aa88f73d-a9fb-456f-b0d0-5c0031e4aa34',
name: 'messenger',
version: '1.0.0',
schema: MessengerConfigSchema,
initiable: true,
lazy: true
}
}

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

export interface MessengerConfig extends ChannelConfig {
appId: string
appSecret: string
verifyToken: string
pageId: string
accessToken: string
}

export const MessengerConfigSchema = {
appId: Joi.string().required(),
appSecret: Joi.string().required(),
verifyToken: Joi.string().required(),
pageId: Joi.string().required(),
accessToken: Joi.string().required()
}
8 changes: 8 additions & 0 deletions packages/channels/src/messenger/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ChannelContext } from '../base/context'
import { MessengerState } from './service'
import { MessengerStream } from './stream'

export type MessengerContext = ChannelContext<MessengerState> & {
messages: any[]
stream: MessengerStream
}
40 changes: 40 additions & 0 deletions packages/channels/src/messenger/messenger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export interface MessengerPayload {
object: string
entry: MessengerEntry[]
}

export interface MessengerEntry {
id: string
time: number
messaging: MessengerMessage[]
}

export interface MessengerMessage {
sender: { id: string }
recipient: { id: string }
timestamp: number
message?: {
mid: string
text: string
quick_reply?: { payload: string }
}
postback?: {
mid: string
payload: string
title: string
}
}

export interface MessengerCard {
title: string
image_url?: string
subtitle?: string
buttons: MessengerButton[]
}

export interface MessengerButton {
type: 'web_url' | 'postback'
title?: string
payload?: string
url?: string
}
21 changes: 21 additions & 0 deletions packages/channels/src/messenger/renderers/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AudioRenderer } from '../../base/renderers/audio'
import { AudioContent } from '../../content/types'
import { MessengerContext } from '../context'

export class MessengerAudioRenderer extends AudioRenderer {
renderAudio(context: MessengerContext, payload: AudioContent) {
context.messages.push({
attachment: {
type: 'audio',
payload: {
is_reusable: true,
url: payload.audio
}
}
})

if (payload.title?.length) {
context.messages.push({ text: payload.title })
}
}
}
75 changes: 75 additions & 0 deletions packages/channels/src/messenger/renderers/carousel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { CarouselContext, CarouselRenderer } from '../../base/renderers/carousel'
import { ActionOpenURL, ActionPostback, ActionSaySomething, CardContent, CarouselContent } from '../../content/types'
import { MessengerContext } from '../context'
import { MessengerButton, MessengerCard } from '../messenger'

export const POSTBACK_PREFIX = 'postback::'
export const SAY_PREFIX = 'say::'

type Context = CarouselContext<MessengerContext> & {
cards: MessengerCard[]
buttons: MessengerButton[]
}

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

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

renderButtonUrl(context: Context, button: ActionOpenURL) {
context.buttons.push({
type: 'web_url',
url: button.url,
title: button.title
})
}

renderButtonPostback(context: Context, button: ActionPostback) {
context.buttons.push({
type: 'postback',
title: button.title,
payload: `${POSTBACK_PREFIX}${button.payload}`
})
}

renderButtonSay(context: Context, button: ActionSaySomething) {
context.buttons.push({
type: 'postback',
title: button.title,
payload: `${SAY_PREFIX}${button.text}`
})
}

endRenderCard(context: Context, card: CardContent) {
if (context.buttons.length === 0) {
context.buttons.push({
type: 'postback',
title: card.title,
payload: card.title
})
}

context.cards.push({
title: card.title,
image_url: card.image ? card.image : undefined,
subtitle: card.subtitle,
buttons: context.buttons
})
}

endRender(context: Context, carousel: CarouselContent) {
context.channel.messages.push({
attachment: {
type: 'template',
payload: {
template_type: 'generic',
elements: context.cards
}
}
})
}
}
15 changes: 15 additions & 0 deletions packages/channels/src/messenger/renderers/choices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ChoicesRenderer } from '../../base/renderers/choices'
import { ChoiceContent } from '../../content/types'
import { MessengerContext } from '../context'

export class MessengerChoicesRenderer extends ChoicesRenderer {
renderChoice(context: MessengerContext, payload: ChoiceContent): void {
const message = context.messages[0]

message.quick_replies = payload.choices.map((c) => ({
content_type: 'text',
title: c.title,
payload: c.value
}))
}
}
9 changes: 9 additions & 0 deletions packages/channels/src/messenger/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 { MessengerContext } from '../context'

export class MessengerFileRenderer extends FileRenderer {
renderFile(context: MessengerContext, payload: FileContent) {
context.messages.push({ text: `${payload.title ? `${payload.title}\n` : payload.title}${payload.file}` })
}
}

0 comments on commit 6717e0a

Please sign in to comment.