Skip to content

Commit c7d8349

Browse files
committed
fix(messenger): fixes webhooks
1 parent 97daffd commit c7d8349

File tree

13 files changed

+216
-72
lines changed

13 files changed

+216
-72
lines changed

modules/channel-messenger/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ Head over to `data/bots/<your_bot>/config/channel-messenger.json` and edit your
4545
```json
4646
{
4747
"$schema": "../../../assets/modules/channel-messenger/config.schema.json",
48-
"verifyToken": "<your_token>",
48+
"accessToken": "<your_token>",
49+
"verifyToken": "<generate a random string>",
4950
"greeting": "This is a greeting message!!! Hello Human!",
5051
"getStarted": "You got started!",
5152
"persistentMenu": [
@@ -76,7 +77,8 @@ The config file for channel-messenger can be found at `data/bots/<your_bot>/conf
7677

7778
| Property | Description |
7879
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
79-
| verifyToken | The Facebook Page Access Token |
80+
| accessToken | The Facebook Page Access Token |
81+
| verifyToken | A random hard-to-guess string of your choosing |
8082
| greeting | The optional greeting message people will see on the welcome screen. Greeting will not appear if left blank. |
8183
| getStarted | The optional message of the welcome screen "Get Started" button. Get Started wont appear if this is left blank. |
8284
| persistentMenu | The optional raw persistent menu object. The menu won't appear if this is left blank. Please refer to Facebook's [Persistent Menu Documentation](https://developers.facebook.com/docs/messenger-platform/send-messages/persistent-menu/) to know more about menu options. |

modules/channel-messenger/src/backend/index.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,31 @@ import * as sdk from 'botpress/sdk'
33

44
import { MessengerService } from './messenger'
55

6+
let service: MessengerService
7+
68
const onServerStarted = async (bp: typeof sdk) => {
7-
const messengerService = new MessengerService(bp)
8-
messengerService.initialize()
9+
service = new MessengerService(bp)
10+
await service.initialize()
911
}
1012

1113
const onServerReady = (bp: typeof sdk) => {}
1214

15+
const onBotMount = async (bp: typeof sdk, botId: string) => {
16+
await service.mountBot(botId)
17+
}
18+
19+
const onBotUnmount = async (bp: typeof sdk, botId: string) => {
20+
await service.unmountBot(botId)
21+
}
22+
1323
const entryPoint: sdk.ModuleEntryPoint = {
1424
onServerReady,
1525
onServerStarted,
26+
onBotMount: onBotMount,
27+
onBotUnmount: onBotUnmount,
1628
definition: {
1729
name: 'channel-messenger',
30+
noInterface: true,
1831
fullName: 'Messenger Channel'
1932
}
2033
}

modules/channel-messenger/src/backend/messenger.ts

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,42 @@ import { Config } from '../config'
1010
const outgoingTypes = ['text', 'typing', 'login_prompt', 'carousel']
1111
type MessengerAction = 'typing_on' | 'typing_off' | 'mark_seen'
1212

13-
export class MessengerService {
14-
private readonly http = axios.create({ baseURL: 'https://graph.facebook.com/v2.6/me' })
13+
type MountedBot = { pageId: string; botId: string; client: MessengerClient }
1514

16-
private messengerClients: { [key: string]: MessengerClient } = {}
17-
private router: Router
15+
export class MessengerService {
16+
private readonly http = axios.create({ baseURL: 'https://graph.facebook.com/v3.2/me' })
17+
private mountedBots: MountedBot[] = []
18+
private router: Router & sdk.http.RouterExtension
19+
private appSecret: string
1820

1921
constructor(private bp: typeof sdk) {}
2022

21-
initialize() {
22-
this.router = this.bp.http.createRouterForBot('channel-messenger', { checkAuthentication: false })
23+
async initialize() {
24+
const config = (await this.bp.config.getModuleConfig('channel-messenger')) as Config
25+
26+
if (!config.verifyToken || config.verifyToken.length < 1) {
27+
throw new Error('You need to set a non-empty value for "verifyToken" in the *global* messenger config')
28+
}
29+
30+
if (!config.appSecret || config.appSecret.length < 1) {
31+
throw new Error(`You need to provide your app's App Secret in the *global* messenger config`)
32+
}
33+
34+
this.appSecret = config.appSecret
35+
36+
this.router = this.bp.http.createRouterForBot('channel-messenger', {
37+
checkAuthentication: false,
38+
enableJsonBodyParser: false // we use our custom json body parser instead, see below
39+
})
40+
41+
this.router.getPublicPath().then(publicPath => {
42+
if (publicPath.indexOf('https://') !== 0) {
43+
this.bp.logger.warn('Messenger requires HTTPS to be setup to work properly. See EXTERNAL_URL botpress config.')
44+
}
45+
46+
this.bp.logger.info(`Messenger Webhook URL is ${publicPath.replace('BOT_ID', '___')}/webhook`)
47+
})
48+
2349
this.router.use(
2450
bodyParser.json({
2551
verify: this._verifySignature.bind(this)
@@ -38,66 +64,106 @@ export class MessengerService {
3864
})
3965
}
4066

41-
getMessengerClient(botId: string): MessengerClient {
42-
if (this.messengerClients[botId]) {
43-
return this.messengerClients[botId]
67+
async mountBot(botId: string) {
68+
const config = (await this.bp.config.getModuleConfigForBot('channel-messenger', botId)) as Config
69+
if (config.enabled) {
70+
if (!config.accessToken) {
71+
return this.bp.logger
72+
.forBot(botId)
73+
.error('You need to configure an Access Token to enable it. Messenger Channel is disabled for this bot.')
74+
}
75+
76+
const { data } = await this.http.get('/', { params: { access_token: config.accessToken } })
77+
78+
if (!data && !data.id) {
79+
return this.bp.logger
80+
.forBot(botId)
81+
.error(
82+
'Could not register bot, are you sure your Access Token is valid? Messenger Channel is disabled for this bot.'
83+
)
84+
}
85+
86+
const pageId = data.id
87+
const client = new MessengerClient(botId, this.bp, this.http)
88+
this.mountedBots.push({ botId: botId, client, pageId })
89+
90+
await client.setupGreeting()
91+
await client.setupGetStarted()
92+
await client.setupPersistentMenu()
93+
}
94+
}
95+
96+
async unmountBot(botId: string) {
97+
this.mountedBots = _.remove(this.mountedBots, x => x.botId === botId)
98+
}
99+
100+
getMessengerClientByBotId(botId: string): MessengerClient {
101+
const entry = _.find(this.mountedBots, x => x.botId === botId)
102+
103+
if (!entry) {
104+
throw new Error(`Can't find a MessengerClient for bot "${botId}"`)
44105
}
45106

46-
this.messengerClients[botId] = new MessengerClient(botId, this.bp, this.http)
47-
return this.messengerClients[botId]
107+
return entry.client
48108
}
49109

50110
// See: https://developers.facebook.com/docs/messenger-platform/webhook#security
51111
private async _verifySignature(req, res, buffer) {
52-
const client = this.getMessengerClient(req.params.botId)
53-
const config = await client.getConfig()
54112
const signatureError = new Error("Couldn't validate the request signature.")
55113

56114
if (!/^\/webhook/i.test(req.path)) {
57115
return
58116
}
59117

118+
console.log(req.path, req.headers, req.body)
119+
60120
const signature = req.headers['x-hub-signature']
61-
if (!signature) {
121+
if (!signature || !this.appSecret) {
62122
throw signatureError
63123
} else {
64124
const [, hash] = signature.split('=')
125+
65126
const expectedHash = crypto
66-
.createHmac('sha1', config.verifyToken)
127+
.createHmac('sha1', this.appSecret)
67128
.update(buffer)
68129
.digest('hex')
69130

70-
if (hash != expectedHash) {
131+
if (hash !== expectedHash) {
71132
throw signatureError
72133
}
73134
}
74135
}
75136

76137
private async _handleIncomingMessage(req, res) {
77138
const body = req.body
78-
const botId = req.params.botId
79-
const client = this.getMessengerClient(botId)
80139

81140
if (body.object !== 'page') {
82-
res.sendStatus(404)
83-
return
141+
//
142+
} else {
143+
res.status(200).send('EVENT_RECEIVED')
84144
}
85145

86146
for (const entry of body.entry) {
87-
// Will only ever contain one message, so we get index 0
88-
const webhookEvent = entry.messaging[0]
89-
const senderId = webhookEvent.sender.id
147+
const pageId = entry.id
148+
const messages = entry.messaging
149+
150+
const bot = _.find<MountedBot>(this.mountedBots, { pageId })
151+
if (!bot) {
152+
continue
153+
}
90154

91-
await client.sendAction(senderId, 'mark_seen')
155+
for (const webhookEvent of messages) {
156+
const senderId = webhookEvent.sender.id
92157

93-
if (webhookEvent.message) {
94-
await this._sendEvent(botId, senderId, webhookEvent.message, { type: 'message' })
95-
} else if (webhookEvent.postback) {
96-
await this._sendEvent(botId, senderId, { text: webhookEvent.postback.payload }, { type: 'callback' })
158+
await bot.client.sendAction(senderId, 'mark_seen')
159+
160+
if (webhookEvent.message) {
161+
await this._sendEvent(req.BOT_ID, senderId, webhookEvent.message, { type: 'message' })
162+
} else if (webhookEvent.postback) {
163+
await this._sendEvent(req.BOT_ID, senderId, { text: webhookEvent.postback.payload }, { type: 'callback' })
164+
}
97165
}
98166
}
99-
100-
res.status(200).send('EVENT_RECEIVED')
101167
}
102168

103169
private async _sendEvent(botId: string, senderId: string, message, args: { type: string }) {
@@ -115,24 +181,19 @@ export class MessengerService {
115181
}
116182

117183
private async _setupWebhook(req, res) {
184+
console.log('setup', req)
118185
const mode = req.query['hub.mode']
119186
const token = req.query['hub.verify_token']
120187
const challenge = req.query['hub.challenge']
121-
const botId = req.params.botId
122188

123-
const client = this.getMessengerClient(botId)
124-
const config = await client.getConfig()
189+
const config = (await this.bp.config.getModuleConfig('channel-messenger')) as Config
125190

126191
if (mode && token && mode === 'subscribe' && token === config.verifyToken) {
127-
this.bp.logger.forBot(botId).debug('Webhook Verified.')
192+
this.bp.logger.debug('Webhook Verified')
128193
res.status(200).send(challenge)
129194
} else {
130195
res.sendStatus(403)
131196
}
132-
133-
await client.setupGreeting()
134-
await client.setupGetStarted()
135-
await client.setupPersistentMenu()
136197
}
137198

138199
private async _handleOutgoingEvent(event: sdk.IO.Event, next: sdk.IO.MiddlewareNextCallback) {
@@ -141,7 +202,7 @@ export class MessengerService {
141202
}
142203

143204
const messageType = event.type === 'default' ? 'text' : event.type
144-
const messenger = this.getMessengerClient(event.botId)
205+
const messenger = this.getMessengerClientByBotId(event.botId)
145206

146207
if (!_.includes(outgoingTypes, messageType)) {
147208
return next(new Error('Unsupported event type: ' + event.type))
@@ -248,6 +309,6 @@ export class MessengerClient {
248309

249310
private async _callEndpoint(endpoint: string, body) {
250311
const config = await this.getConfig()
251-
await this.http.post(endpoint, body, { params: { access_token: config.verifyToken } })
312+
await this.http.post(endpoint, body, { params: { access_token: config.accessToken } })
252313
}
253314
}
Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,68 @@
11
export interface Config {
22
/**
3+
* This this in the LOCAL config (unique to every bot/page)
4+
* Whether or not the messenger module is enabled for this bot
5+
* @default false
6+
*/
7+
enabled: boolean
8+
/**
9+
* This this in the LOCAL config (unique to every bot/page)
310
* The Facebook Page Access Token
411
*/
12+
accessToken: string
13+
/**
14+
* This this in the GLOBAL config (same for all bots)
15+
* Your app's "App Secret"
16+
* Find this secret in your developers.facebook.com -> your app -> Settings -> Basic -> App Secret -> Show
17+
*/
18+
appSecret: string
19+
/**
20+
* Set this in the GLOBAL config (same for all the bots)
21+
* The verify token, should be a random string unique to your server. This is a random (hard to guess) string of your choosing.
22+
* @see https://developers.facebook.com/docs/messenger-platform/getting-started/webhook-setup/#verify_webhook
23+
*/
524
verifyToken: string
625
/**
726
* The greeting message people will see on the welcome screen
27+
* @see https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/greeting
828
*/
929
greeting?: string
1030
/**
1131
* The message of the welcome screen "Get Started" button
32+
* @see https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/get-started-button
1233
*/
1334
getStarted?: string
1435
/**
1536
* The raw persistent menu object
1637
* @see https://developers.facebook.com/docs/messenger-platform/send-messages/persistent-menu/
38+
* @example
39+
* {
40+
* "persistent_menu":[
41+
* {
42+
* "locale":"default",
43+
* "composer_input_disabled": true,
44+
* "call_to_actions":[
45+
* {
46+
* "title":"My Account",
47+
* "type":"nested",
48+
* "call_to_actions":[
49+
* {
50+
* "title":"Pay Bill",
51+
* "type":"postback",
52+
* "payload":"PAYBILL_PAYLOAD"
53+
* },
54+
* {
55+
* "type":"web_url",
56+
* "title":"Latest News",
57+
* "url":"https://www.messenger.com/",
58+
* "webview_height_ratio":"full"
59+
* }
60+
* ]
61+
* }
62+
* ]
63+
* }
64+
* ]
65+
* }
1766
*/
18-
persistentMenu?
67+
persistentMenu?: any // TODO Type me
1968
}

src/bp/core/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const http = (httpServer: HTTPServer): typeof sdk.http => {
3535
deleteShortLink(name: string): void {
3636
httpServer.deleteShortLink(name)
3737
},
38-
createRouterForBot(routerName: string, options?: sdk.RouterOptions): any {
38+
createRouterForBot(routerName: string, options?: sdk.RouterOptions): any & sdk.http.RouterExtension {
3939
const defaultRouterOptions = { checkAuthentication: true, enableJsonBodyParser: true }
4040
return httpServer.createRouterForBot(routerName, options || defaultRouterOptions)
4141
},

src/bp/core/app.inversify.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Logger } from 'botpress/sdk'
22
import { Container } from 'inversify'
3-
import { AppLifecycle } from 'lifecycle'
43

54
import { BotpressAPIProvider } from './api'
65
import { Botpress } from './botpress'
@@ -50,11 +49,6 @@ container.bind<LoggerProvider>(TYPES.LoggerProvider).toProvider<Logger>(context
5049
}
5150
})
5251

53-
container
54-
.bind<AppLifecycle>(TYPES.AppLifecycle)
55-
.to(AppLifecycle)
56-
.inSingletonScope()
57-
5852
container
5953
.bind<LoggerDbPersister>(TYPES.LoggerDbPersister)
6054
.to(LoggerDbPersister)

0 commit comments

Comments
 (0)