Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(messenger): messenger v1 #329

Merged
merged 41 commits into from
Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7e1be72
delete old
samuelmasse Jan 25, 2022
7e24136
use legacy
samuelmasse Jan 25, 2022
dee4dfa
remove deps
samuelmasse Jan 25, 2022
6442a12
Merge branch 'master' into sm-channels-v2
samuelmasse Jan 26, 2022
98f1bce
use channels 0.1.0
samuelmasse Jan 26, 2022
2e350e9
bring changes
samuelmasse Jan 26, 2022
8825394
gif
samuelmasse Jan 26, 2022
cf8aad6
bring changes
samuelmasse Jan 26, 2022
3b87577
fix
samuelmasse Jan 26, 2022
49e8ae3
fix
samuelmasse Jan 26, 2022
160c2ef
bring changes
samuelmasse Jan 26, 2022
6a79deb
rewire comment
samuelmasse Jan 28, 2022
ed8d371
fix
samuelmasse Jan 28, 2022
fbbc10f
Merge branch 'master' into sm-channels-v2
samuelmasse Jan 28, 2022
538cc6a
start
samuelmasse Jan 28, 2022
ba953cd
message reception
samuelmasse Jan 28, 2022
baaa306
fix
samuelmasse Jan 28, 2022
5accf74
send text
samuelmasse Jan 28, 2022
ff78006
refact
samuelmasse Jan 28, 2022
18dd3b8
readme
samuelmasse Jan 28, 2022
e439100
image
samuelmasse Jan 28, 2022
e372e33
single-choice
samuelmasse Jan 28, 2022
181aa5b
Merge branch 'master' into sm-channels-v2
samuelmasse Jan 28, 2022
dd4155a
Merge branch 'sm-channels-v2' into sm-messenger-v2
samuelmasse Jan 28, 2022
0286604
fix
samuelmasse Jan 28, 2022
32e7d9e
bring changes
samuelmasse Jan 28, 2022
bb85c82
Merge branch 'sm-channels-v2' into sm-messenger-v2
samuelmasse Jan 28, 2022
744ecd7
bump
samuelmasse Jan 28, 2022
2b87c8a
Merge branch 'sm-channels-v2' into sm-messenger-v2
samuelmasse Jan 28, 2022
4caa3cc
Merge branch 'master' into sm-messenger-v2
samuelmasse Jan 28, 2022
b6fd2f6
fix merge
samuelmasse Jan 28, 2022
14e9c6c
Merge branch 'master' into sm-messenger-v2
samuelmasse Jan 29, 2022
0f02324
Merge branch 'master' into sm-messenger-v2
samuelmasse Jan 29, 2022
864413e
use channel in server
samuelmasse Jan 29, 2022
ec64535
quick_reply
samuelmasse Jan 29, 2022
25e6a3f
carousel
samuelmasse Jan 29, 2022
556514f
refact
samuelmasse Jan 29, 2022
76ea3f3
fix
samuelmasse Jan 29, 2022
e5c9ec3
audio video location
samuelmasse Jan 29, 2022
7f2e592
file
samuelmasse Jan 29, 2022
e0e2389
readme
samuelmasse Jan 29, 2022
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
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}` })
}
}
Loading