Skip to content

Commit

Permalink
feat(whatsapp): support multiple entries
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Aug 22, 2023
1 parent 516790d commit 7905226
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 43 deletions.
2 changes: 1 addition & 1 deletion adapters/matrix/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@satorijs/adapter-matrix",
"description": "Matrix Adapter for Satorijs",
"version": "3.3.0",
"version": "3.4.0",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion adapters/whatsapp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@satorijs/adapter-whatsapp",
"description": "WhatsApp Adapter for Satorijs",
"version": "1.0.3",
"version": "1.0.4",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
Expand Down
112 changes: 71 additions & 41 deletions adapters/whatsapp/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,31 @@
import { Adapter, Context, Logger, Quester, Schema } from '@satorijs/satori'
import { Adapter, Context, Logger, Quester, remove, Schema } from '@satorijs/satori'
import { Internal } from './internal'
import { WhatsAppBot } from './bot'
import { WebhookBody } from './types'
import { decodeMessage } from './utils'
import internal from 'stream'
import crypto from 'crypto'

export class WhatsAppAdapter extends Adapter<WhatsAppBot> {
static reusable = true

public bots: WhatsAppBot[] = []
public logger = new Logger('whatsapp')

constructor(private ctx: Context, public config: WhatsAppAdapter.Config) {
super()

const http = ctx.http.extend({
headers: {
Authorization: `Bearer ${config.systemToken}`,
},
}).extend(config)
const internal = new Internal(http)

ctx.on('ready', async () => {
const data = await internal.getPhoneNumbers(config.id)
for (const item of data) {
const bot = new WhatsAppBot(ctx, {
selfId: item.id,
})
bot.adapter = this
bot.internal = internal
this.bots.push(bot)
bot.online()
}
})
class HttpServer {
private logger = new Logger('whatsapp')
private adapters: WhatsAppAdapter[] = []

constructor(private ctx: Context) {
// https://developers.facebook.com/docs/graph-api/webhooks/getting-started
// https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-whatsapp/
ctx.router.post('/whatsapp', async (ctx) => {
const receivedSignature = ctx.get('X-Hub-Signature-256').split('sha256=')[1]
const received = ctx.get('X-Hub-Signature-256').split('sha256=')[1]
if (!received) return ctx.status = 403

const payload = ctx.request.rawBody

const generatedSignature = crypto
.createHmac('sha256', this.config.secret)
.update(payload)
.digest('hex')
if (receivedSignature !== generatedSignature) return ctx.status = 403
const adapters = this.adapters.filter((adapter) => {
const expected = crypto
.createHmac('sha256', adapter.config.secret)
.update(payload)
.digest('hex')
return expected === received
})
if (!adapters.length) return ctx.status = 403

const parsed = ctx.request.body as WebhookBody
this.logger.debug(require('util').inspect(parsed, false, null, true))
Expand All @@ -55,7 +34,7 @@ export class WhatsAppAdapter extends Adapter<WhatsAppBot> {
if (parsed.object !== 'whatsapp_business_account') return
for (const entry of parsed.entry) {
const phone_number_id = entry.changes[0].value.metadata.phone_number_id
const bot = this.bots.find((bot) => bot.selfId === phone_number_id)
const bot = this.getBot(phone_number_id)
const session = await decodeMessage(bot, entry)
if (session.length) session.forEach(bot.dispatch.bind(bot))
this.logger.debug('handling bot: %s', bot.sid)
Expand All @@ -67,15 +46,20 @@ export class WhatsAppAdapter extends Adapter<WhatsAppBot> {
this.logger.debug(require('util').inspect(ctx.query, false, null, true))
const verifyToken = ctx.query['hub.verify_token']
const challenge = ctx.query['hub.challenge']
if (verifyToken !== this.config.verifyToken) return ctx.status = 403
ctx.body = challenge
ctx.status = 200
for (const adapter of this.adapters) {
if (adapter.config.verifyToken === verifyToken) {
ctx.body = challenge
ctx.status = 200
return
}
}
return ctx.status = 403
})

ctx.router.get('/whatsapp/assets/:self_id/:media_id', async (ctx) => {
const mediaId = ctx.params.media_id
const selfId = ctx.params.self_id
const bot = this.bots.find((bot) => bot.selfId === selfId)
const bot = this.getBot(selfId)
if (!bot) return ctx.status = 404

const fetched = await bot.internal.getMedia(mediaId)
Expand All @@ -91,6 +75,52 @@ export class WhatsAppAdapter extends Adapter<WhatsAppBot> {
ctx.status = 200
})
}

getBot(selfId: string) {
for (const adapter of this.adapters) {
for (const bot of adapter.bots) {
if (bot.selfId === selfId) return bot
}
}
}

fork(ctx: Context, adapter: WhatsAppAdapter) {
this.adapters.push(adapter)
ctx.on('dispose', () => {
remove(this.adapters, adapter)
})
}
}

export class WhatsAppAdapter extends Adapter<WhatsAppBot> {
static reusable = true

public bots: WhatsAppBot[] = []

constructor(ctx: Context, public config: WhatsAppAdapter.Config) {
super()
ctx.plugin(HttpServer, this)

const http = ctx.http.extend({
headers: {
Authorization: `Bearer ${config.systemToken}`,
},
}).extend(config)
const internal = new Internal(http)

ctx.on('ready', async () => {
const data = await internal.getPhoneNumbers(config.id)
for (const item of data) {
const bot = new WhatsAppBot(ctx, {
selfId: item.id,
})
this.bots.push(bot)
bot.adapter = this
bot.internal = internal
bot.online()
}
})
}
}

export namespace WhatsAppAdapter {
Expand Down

0 comments on commit 7905226

Please sign in to comment.