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

Add Support for WhatsApp Channel via WhatsApp Cloud API #676

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f6cb184
whatsapp interfaces
ivarunseth Nov 2, 2023
e16b168
whatsapp channel config
ivarunseth Nov 2, 2023
d2872f0
whatsapp channel service
ivarunseth Nov 2, 2023
5720a5b
whatsapp channel stream
ivarunseth Nov 2, 2023
da3d2cf
whatsapp channel context
ivarunseth Nov 2, 2023
6c8c714
whatsapp channel api
ivarunseth Nov 2, 2023
68c71c1
whatsapp channel
ivarunseth Nov 2, 2023
c9399af
whatsapp renderers
ivarunseth Nov 2, 2023
6e5176c
whatsapp senders
ivarunseth Nov 2, 2023
4d8db87
updated index.ts
ivarunseth Nov 2, 2023
658e741
added whatsapp keyword in client package.json
ivarunseth Nov 2, 2023
c62c1dc
added WhatsappChannel in server channel service
ivarunseth Nov 2, 2023
40eca81
add optional markRead config for whatsapp channel
ivarunseth Nov 3, 2023
9350efa
add choice renderer & handled interactive messages
ivarunseth Nov 10, 2023
7c39d8b
Merge branch 'botpress:master' into whatsapp-channel
ivarunseth Dec 26, 2023
6f18e8b
Merge branch 'botpress:master' into whatsapp-channel
ivarunseth Jun 18, 2024
b0e4ed0
Update README.md
ivarunseth Jun 18, 2024
4a6b615
Update README.md
ivarunseth Jun 18, 2024
728a4ba
Create README.md
ivarunseth Jun 18, 2024
94275f0
Update README.md
ivarunseth Jun 19, 2024
843e6ca
added whatsapp interfaces for media and interactive messages
Aug 27, 2024
25df139
using whatsapp message interface for type safety
Aug 27, 2024
34f57f6
updated function to handle incoming messages to bot. Also added suppo…
Aug 27, 2024
0976daa
Updated graph api version to v20.0 & added try catch block for reques…
Aug 27, 2024
7d07343
setting preview_url to true in case outgoing text message has a url, …
Aug 27, 2024
2965cec
Added support for more than 10 choices by formatting them as text sim…
Aug 27, 2024
350323c
Added carousel renderer implemented using whatsapp interactive messages.
Aug 27, 2024
8224ebe
updated README.md file
Aug 27, 2024
0282a8b
using whatsapp interactice reply buttons and list messages if possibl…
ivarunseth Sep 21, 2024
adafa05
truncating length of text strings according to whatsapp cloud api pos…
ivarunseth Sep 21, 2024
fe2080d
updated logic of receive function to handle index responses in the api.
ivarunseth Sep 21, 2024
73befc7
added function to prepare index responses in case fallback logic is u…
ivarunseth Sep 21, 2024
d90c244
binded the prepareIndexResponse function to stream.
ivarunseth Sep 21, 2024
e644410
Create whatsapp.md
ivarunseth Oct 2, 2024
02315f6
Update readme.md
ivarunseth Oct 2, 2024
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The botpress messaging server provides a standardized messaging api to communica
- Twilio
- Smooch
- Vonage
- Whatsapp

## Getting started

Expand Down
1 change: 1 addition & 0 deletions docs/channels/v1/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Telegram](./telegram.md)
- [Twilio](./twilio.md)
- [Vonage](./vonage.md)
- [Whatsapp](./whatsapp.md)

## Development

Expand Down
89 changes: 89 additions & 0 deletions docs/channels/v1/whatsapp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Whatsapp

## Requirements

You will need a Meta app to connect your bot to Whatsapp.

### Create a Meta App

To create a Meta app, go to the [Meta for Developers website](https://developers.facebook.com/) and log in with your Facebook account. Select **My Apps** from the top menu, and create a new app. For more details and assistance, visit the [Meta developer documentation](https://developers.facebook.com/docs/development).

## Channel Configuration

### API version

The whatsapp channel is made to interact with version 20.0 or higher of the Whatsapp Cloud API. If it is not the default version so it must be changed in your app's settings.

1. Go to your Meta App.
2. In the left sidebar, expand the **Settings** menu and select **Advanced**.
3. In the **Upgrade API version** section, select v20.0 or higher as the API version.
4. Click on **Save changes**.

### Add Whatsapp Product

Whatsapp is not added by default in your Meta App, so it must be added manually.

1. In the left sidebar, click on **Dashboard**.
2. In the **Add products** section, click on **Set Up** button on Whatsapp.

### App ID and Secret

The `appId` and `appSecret` are used to validate webhook requests.

1. In the left sidebar, expand the **Settings** menu and select **Basic**. Here you can find the **App ID** and **App secret**.
2. Click on the **Show** button in the **App secret** text box. Copy the **appId** and **appSecret** to your channel configuration.

### Phone Number ID and Access Token

The `phoneNumberId` and `accessToken` are used to send messages to the Whatsapp Cloud API.

1. In the left sidebar, expand the **Whatsapp** menu and select **API Setup**.
2. Click on **Generate access token**. Copy this token and paste it in the **accessToken** channel configuration.
3. Copy the **Phone number ID** and paste it in you **phoneNumberId** channel configuration.

### Verify Token

The `verifyToken` is used by Meta to verify that you are the real owner of the provided webhook.

You can generate any random alphanumerical string for this configuration. Paste it in your **verifyToken** channel configuration.

### Save Configuration

_Note: It is important you save your configuration before configuring the webhook, otherwise Whatsapp will be unable to validate the webhook url._

1. Edit your bot config.

```json
{
// ... other data
"messaging": {
"channels": {
"whatsapp": {
"version": "1.0.0",
"enabled": true,
"phoneNumberId": "phone_number_id",
"accessToken": "your_access_token",
"appId": "app_id",
"appSecret": "your_app_secret",
"verifyToken": "your_verify_token"
}
// ... other channels can also be configured here
}
}
}
```

2. Restart Botpress.
3. You should see your webhook endpoint in the console on startup.

## Webhook Configuration

To receive messages from Whatsapp, you will need to setup a webhook.

1. Go to your Meta App.
2. In the left sidebar, expand the **Whatsapp** menu and select **Configuration**.
3. In the **Webhooks** section, click **Add Callback URL**.
4. Set the webhook URL to: `<EXTERNAL_URL>/api/v1/messaging/webhooks/v1/<YOUR_BOT_ID>/whatsapp`.
5. Copy paste the `verifyToken` you generated earlier.
6. Click on **Verify and save**. Make sure your channel configuration was saved before doing this step, otherwise the webhook validation will fail.
7. In the **Webhook fields** below, subscribe to **messages** to your webhook.
1 change: 1 addition & 0 deletions packages/channels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Telegram
- Twilio
- Vonage
- Whatsapp

## Development

Expand Down
1 change: 1 addition & 0 deletions packages/channels/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './teams/channel'
export * from './telegram/channel'
export * from './twilio/channel'
export * from './vonage/channel'
export * from './whatsapp/channel'
31 changes: 31 additions & 0 deletions packages/channels/src/whatsapp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Whatsapp (v1.0.0)

### Sending

| Channels | Whatsapp | Details |
| -------- | :-------: | :-------------------------------- |
| Text | ✅ | |
| Image | ✅ | |
| Choice | ✅ | |
| Dropdown | ✅ | |
| Card | ✅ | |
| Carousel | ✅ | |
| File | ✅ | File sent as URL |
| Audio | ✅ | Audio sent as URL |
| Video | ✅ | Video sent as URL |
| Location | ✅ | Location sent as Google Maps Link |

### Receiving

| Channels | Whatsapp | Details |
| ------------- | :-------: | :------ |
| Text | ✅ | |
| Quick Reply | ✅ | |
| Postback | ✅ | |
| Say Something | ✅ | |
| Voice | ❌ | |
| Image | ❌ | |
| File | ❌ | |
| Audio | ❌ | |
| Video | ❌ | |
| Location | ❌ | |
105 changes: 105 additions & 0 deletions packages/channels/src/whatsapp/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import crypto from 'crypto'
import express, { Response, Request, NextFunction } from 'express'
import { IncomingMessage } from 'http'
import { ChannelApi, ChannelApiManager, ChannelApiRequest } from '../base/api'
import { IndexChoiceType } from '../base/context'
import { WhatsappService } from './service'
import { WhatsappIncomingMessage, WhatsappPayload } from './whatsapp'

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

router.post('/whatsapp', this.auth.bind(this))
router.post('/whatsapp', 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 WhatsappPayload

for (const entry of payload.entry) {
if (entry.changes && entry.changes.length > 0) {
const change = entry.changes[0]
if (change.field && change.field === 'messages') {
const value = change.value
if (value && 'messages' in value && value.messages && value.messages.length > 0) {
for (const message of value.messages) {
await this.receive(req.scope, message)
}
}
}
}
}
res.status(200).send('EVENT_RECEIVED')
}

private async receive(scope: string, message: WhatsappIncomingMessage) {
if (message && message.id && message.type && message.from) {
const endpoint = this.extractEndpoint(message)
let content: any
if (message.type === 'text' && message.text && message.text.body) {
const index = Number(message.text.body)
content = this.service.handleIndexResponse(scope, index, endpoint.identity, endpoint.sender) || {
type: 'text',
text: message.text.body
}
} else if (message.type === 'interactive' && message.interactive) {
const reply = message.interactive.button_reply || message.interactive.list_reply
if (reply) {
const [type, payload] = reply.id.split('::')
if (type === IndexChoiceType.PostBack) {
content = {type, payload}
} else if (type === IndexChoiceType.SaySomething) {
content = {type, text: payload}
} else if (type === IndexChoiceType.QuickReply) {
content = {type, text: reply.title, payload}
} else if (type === IndexChoiceType.OpenUrl) {
content = {type: IndexChoiceType.SaySomething, text: payload}
}
}
}
await this.service.receive(scope, endpoint, content)
}
}

private extractEndpoint(message: WhatsappIncomingMessage) {
return {
identity: '*',
sender: message.from,
thread: '*'
}
}
}
28 changes: 28 additions & 0 deletions packages/channels/src/whatsapp/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChannelTemplate } from '../base/channel'
import { WhatsappApi } from './api'
import { WhatsappConfig, WhatsappConfigSchema } from './config'
import { WhatsappService } from './service'
import { WhatsappStream } from './stream'

export class WhatsappChannel extends ChannelTemplate<
WhatsappConfig,
WhatsappService,
WhatsappApi,
WhatsappStream
> {
get meta() {
return {
id: '1a01c610-e7eb-4c47-97de-66ab348f473f',
name: 'whatsapp',
version: '1.0.0',
schema: WhatsappConfigSchema,
initiable: true,
lazy: true
}
}

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

export interface WhatsappConfig extends ChannelConfig {
appId: string
appSecret: string
verifyToken: string
accessToken: string
phoneNumberId: string
markRead: boolean
}

export const WhatsappConfigSchema = {
appId: Joi.string().required(),
appSecret: Joi.string().required(),
verifyToken: Joi.string().required(),
accessToken: Joi.string().required(),
phoneNumberId: Joi.string().required(),
markRead: Joi.boolean()
}
10 changes: 10 additions & 0 deletions packages/channels/src/whatsapp/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ChannelContext, IndexChoiceOption } from '../base/context'
import { WhatsappState } from './service'
import { WhatsappStream } from './stream'
import { WhatsappOutgoingMessage } from './whatsapp'

export type WhatsappContext = ChannelContext<WhatsappState> & {
messages: WhatsappOutgoingMessage[]
stream: WhatsappStream
prepareIndexResponse(scope: string, identity: string, sender: string, options: IndexChoiceOption[]): void
}
14 changes: 14 additions & 0 deletions packages/channels/src/whatsapp/renderers/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AudioRenderer } from '../../base/renderers/audio'
import { AudioContent } from '../../content/types'
import { WhatsappContext } from '../context'

export class WhatsappAudioRenderer extends AudioRenderer {
renderAudio(context: WhatsappContext, payload: AudioContent) {
context.messages.push({
type: 'audio',
audio: {
link: payload.audio
}
})
}
}
Loading