Skip to content

Commit 81c52e9

Browse files
authored
Merge pull request #246 from tejas-raskar/feat-implement-whatsapp-with-vonage-dart
Feat: Implement whatsapp with vonage dart
2 parents 3d125a3 + 35c0268 commit 81c52e9

7 files changed

Lines changed: 272 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# See https://www.dartlang.org/guides/libraries/private-files
2+
3+
# Files and directories created by pub
4+
.dart_tool/
5+
.packages
6+
build/
7+
# If you're building an application, you may want to check-in your pubspec.lock
8+
pubspec.lock
9+
10+
# Directory created by dartdoc
11+
# If you don't generate documentation locally you can remove this line.
12+
doc/api/
13+
14+
# dotenv environment variables file
15+
.env*
16+
17+
# Avoid committing generated Javascript files:
18+
*.dart.js
19+
*.info.json # Produced by the --dump-info flag.
20+
*.js # When generated by dart2js. Don't specify *.js if your
21+
# project includes source files written in JavaScript.
22+
*.js_
23+
*.js.deps
24+
*.js.map
25+
26+
.flutter-plugins
27+
.flutter-plugins-dependencies
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# 💬 Dart WhatsApp Bot with Vonage Function
2+
3+
Simple bot to answer WhatsApp messages.
4+
5+
## 🧰 Usage
6+
7+
### GET /
8+
9+
HTML form for interacting with the function.
10+
11+
### POST /
12+
13+
Receives a message, validates its signature, and sends a response back to the sender.
14+
15+
**Parameters**
16+
17+
| Name | Description | Location | Type | Sample Value |
18+
| ------------- | ---------------------------------- | -------- | ------------------- | -------------------- |
19+
| Content-Type | Content type of the request | Header | `application/json ` | N/A |
20+
| Authorization | Webhook signature for verification | Header | String | `Bearer <signature>` |
21+
| from | Sender's identifier. | Body | String | `12345` |
22+
| text | Text content of the message. | Body | String | `Hello!` |
23+
24+
> All parameters are coming from Vonage webhook. Exact documentation can be found in [Vonage API Docs](https://developer.vonage.com/en/api/messages-olympus#inbound-message).
25+
26+
**Response**
27+
28+
Sample `200` Response:
29+
30+
```json
31+
{
32+
"ok": true
33+
}
34+
```
35+
36+
Sample `400` Response:
37+
38+
```json
39+
{
40+
"ok": false,
41+
"error": "Missing required parameter: from"
42+
}
43+
```
44+
45+
Sample `401` Response:
46+
47+
```json
48+
{
49+
"ok": false,
50+
"error": "Payload hash mismatch."
51+
}
52+
```
53+
54+
## ⚙️ Configuration
55+
56+
| Setting | Value |
57+
| ----------------- | ------------- |
58+
| Runtime | Dart (3.1) |
59+
| Entrypoint | `lib/main.dart` |
60+
| Build Commands | `pub get` |
61+
| Permissions | `any` |
62+
| Timeout (Seconds) | 15 |
63+
64+
## 🔒 Environment Variables
65+
66+
### VONAGE_API_KEY
67+
68+
API Key to use the Vonage API.
69+
70+
| Question | Answer |
71+
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
72+
| Required | Yes |
73+
| Sample Value | `62...97` |
74+
| Documentation | [Vonage: Q&A](https://api.support.vonage.com/hc/en-us/articles/204014493-How-do-I-find-my-Voice-API-key-and-API-secret-) |
75+
76+
### VONAGE_API_SECRET
77+
78+
Secret to use the Vonage API.
79+
80+
| Question | Answer |
81+
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
82+
| Required | Yes |
83+
| Sample Value | `Zjc...5PH` |
84+
| Documentation | [Vonage: Q&A](https://api.support.vonage.com/hc/en-us/articles/204014493-How-do-I-find-my-Voice-API-key-and-API-secret-) |
85+
86+
### VONAGE_API_SIGNATURE_SECRET
87+
88+
Secret to verify the webhooks sent by Vonage.
89+
90+
| Question | Answer |
91+
| ------------- | -------------------------------------------------------------------------------------------------------------- |
92+
| Required | Yes |
93+
| Sample Value | `NXOi3...IBHDa` |
94+
| Documentation | [Vonage: Webhooks](https://developer.vonage.com/en/getting-started/concepts/webhooks#decoding-signed-webhooks) |
95+
96+
### VONAGE_WHATSAPP_NUMBER
97+
98+
Vonage WhatsApp number to send messages from.
99+
100+
| Question | Answer |
101+
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
102+
| Required | Yes |
103+
| Sample Value | `+14000000102` |
104+
| Documentation | [Vonage: Q&A](https://api.support.vonage.com/hc/en-us/articles/4431993282580-Where-do-I-find-my-WhatsApp-Number-Certificate-) |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include: package:lints/recommended.yaml
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
import 'package:crypto/crypto.dart';
4+
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
5+
import 'package:whatsapp_with_vonage/utils.dart';
6+
import 'package:http/http.dart' as http;
7+
8+
Future<dynamic> main(final context) async {
9+
throwIfMissing(Platform.environment, [
10+
'VONAGE_API_KEY',
11+
'VONAGE_API_SECRET',
12+
'VONAGE_API_SIGNATURE_SECRET',
13+
'VONAGE_WHATSAPP_NUMBER'
14+
]);
15+
16+
if (context.req.method == 'GET') {
17+
return context.res.send(getStaticFile('index.html'), 200,
18+
{'Content-Type': 'text/html; charset=utf-8'});
19+
}
20+
21+
final token = context.req.headers['authorization'].split(' ')[1];
22+
23+
if (token == null) {
24+
return context.res.json({'ok': false, 'error': 'Unauthorized'}, 401);
25+
}
26+
27+
if (context.req.body['from'] == null || context.req.body['text'] == null) {
28+
return context.res.json({'ok': false, 'error': 'Missing required fields.'}, 400);
29+
}
30+
31+
final jwt = JWT.verify(token, SecretKey(Platform.environment['VONAGE_API_SIGNATURE_SECRET']!));
32+
33+
if (jwt.payload['payload_hash'] == null) {
34+
return context.res.json({'ok': false, 'error': 'Missing payload hash.'}, 400);
35+
}
36+
37+
final payloadHash = sha256.convert(utf8.encode(context.req.bodyRaw)).toString();
38+
39+
if (jwt.payload['payload_hash'] != payloadHash) {
40+
return context.res.json({'ok': false, 'error': 'Payload hash mismatch.'}, 401);
41+
}
42+
43+
final basicAuthToken = base64Encode(utf8.encode('${Platform.environment['VONAGE_API_KEY']}:${Platform.environment['VONAGE_API_SECRET']}'));
44+
45+
await http.post(
46+
Uri.parse('https://messages-sandbox.nexmo.com/v1/messages'),
47+
headers: {
48+
'Content-Type': 'application/json',
49+
'Authorization': 'Basic $basicAuthToken',
50+
},
51+
body: jsonEncode({
52+
'from': Platform.environment['VONAGE_WHATSAPP_NUMBER'],
53+
'to': context.req.body['from'],
54+
'message_type': 'text',
55+
'text': 'Hi there! You sent me: ${context.req.body['text']}',
56+
'channel': 'whatsapp',
57+
}),
58+
);
59+
60+
return context.res.json({'ok': true});
61+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'dart:io';
2+
import 'package:path/path.dart' as p;
3+
4+
final staticFolder = p.join(p.dirname(Platform.script.toFilePath()), '../static');
5+
6+
/// Throws an error if any of the keys are missing from the object
7+
/// @param obj - The object to check
8+
/// @param keys - The list of keys to check for
9+
/// @throws Exception
10+
void throwIfMissing(Map<String, String> obj, List<String> keys) {
11+
final missing = <String>[];
12+
for (final key in keys) {
13+
if (!obj.containsKey(key) || obj[key] == null) {
14+
missing.add(key);
15+
}
16+
}
17+
18+
if (missing.isNotEmpty) {
19+
throw StateError('Missing environment variables: ${missing.join(', ')}');
20+
}
21+
}
22+
23+
/// Returns the contents of a file in the static folder
24+
/// @param {string} fileName
25+
/// @returns {string} Contents of static/{fileName}
26+
String getStaticFile(String fileName) {
27+
final file = File(p.join(staticFolder, fileName));
28+
return file.readAsStringSync();
29+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: whatsapp_with_vonage
2+
version: 1.0.0
3+
4+
environment:
5+
sdk: ^2.17.0
6+
7+
dependencies:
8+
crypto: ^3.0.2
9+
http: ^0.13.5
10+
path: ^1.8.3
11+
convert: ^3.1.0
12+
dart_jsonwebtoken: ^2.12.0
13+
14+
15+
dev_dependencies:
16+
lints: ^2.0.0
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>WhatsApp Bot with Vonage</title>
8+
9+
<link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink" />
10+
<link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink-icons" />
11+
</head>
12+
<body>
13+
<main class="main-content">
14+
<div class="top-cover u-padding-block-end-56">
15+
<div class="container">
16+
<div
17+
class="u-flex u-gap-16 u-flex-justify-center u-margin-block-start-16"
18+
>
19+
<h1 class="heading-level-1">WhatsApp Bot with Vonage</h1>
20+
<code class="u-un-break-text"></code>
21+
</div>
22+
<p
23+
class="body-text-1 u-normal u-margin-block-start-8"
24+
style="max-width: 50rem"
25+
>
26+
This function listens to incoming webhooks from Vonage regarding
27+
WhatsApp messages, and responds to them. To use the function, send
28+
message to the WhatsApp user provided by Vonage.
29+
</p>
30+
</div>
31+
</div>
32+
</main>
33+
</body>
34+
</html>

0 commit comments

Comments
 (0)