-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(messenger): messenger v1 (#329)
* 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
1 parent
a1acd2f
commit 6717e0a
Showing
27 changed files
with
532 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ❌ | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: '*' } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
})) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}` }) | ||
} | ||
} |
Oops, something went wrong.