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

Feat: Implement whatsapp with vonage dart #246

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
27 changes: 27 additions & 0 deletions dart/whatsapp_with_vonage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://www.dartlang.org/guides/libraries/private-files

# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock

# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

# dotenv environment variables file
.env*

# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map

.flutter-plugins
.flutter-plugins-dependencies
104 changes: 104 additions & 0 deletions dart/whatsapp_with_vonage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 💬 Dart WhatsApp Bot with Vonage Function

Simple bot to answer WhatsApp messages.

## 🧰 Usage

### GET /

HTML form for interacting with the function.

### POST /

Receives a message, validates its signature, and sends a response back to the sender.

**Parameters**

| Name | Description | Location | Type | Sample Value |
| ------------- | ---------------------------------- | -------- | ------------------- | -------------------- |
| Content-Type | Content type of the request | Header | `application/json ` | N/A |
| Authorization | Webhook signature for verification | Header | String | `Bearer <signature>` |
| from | Sender's identifier. | Body | String | `12345` |
| text | Text content of the message. | Body | String | `Hello!` |

> 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).

**Response**

Sample `200` Response:

```json
{
"ok": true
}
```

Sample `400` Response:

```json
{
"ok": false,
"error": "Missing required parameter: from"
}
```

Sample `401` Response:

```json
{
"ok": false,
"error": "Payload hash mismatch."
}
```

## ⚙️ Configuration

| Setting | Value |
| ----------------- | ------------- |
| Runtime | Dart (3.1) |
| Entrypoint | `lib/main.dart` |
| Build Commands | `pub get` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |

## 🔒 Environment Variables

### VONAGE_API_KEY

API Key to use the Vonage API.

| Question | Answer |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Required | Yes |
| Sample Value | `62...97` |
| 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-) |

### VONAGE_API_SECRET

Secret to use the Vonage API.

| Question | Answer |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Required | Yes |
| Sample Value | `Zjc...5PH` |
| 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-) |

### VONAGE_API_SIGNATURE_SECRET

Secret to verify the webhooks sent by Vonage.

| Question | Answer |
| ------------- | -------------------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `NXOi3...IBHDa` |
| Documentation | [Vonage: Webhooks](https://developer.vonage.com/en/getting-started/concepts/webhooks#decoding-signed-webhooks) |

### VONAGE_WHATSAPP_NUMBER

Vonage WhatsApp number to send messages from.

| Question | Answer |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `+14000000102` |
| Documentation | [Vonage: Q&A](https://api.support.vonage.com/hc/en-us/articles/4431993282580-Where-do-I-find-my-WhatsApp-Number-Certificate-) |
1 change: 1 addition & 0 deletions dart/whatsapp_with_vonage/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
61 changes: 61 additions & 0 deletions dart/whatsapp_with_vonage/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:whatsapp_with_vonage/utils.dart';
import 'package:http/http.dart' as http;

Future<dynamic> main(final context) async {
throwIfMissing(Platform.environment, [
'VONAGE_API_KEY',
'VONAGE_API_SECRET',
'VONAGE_API_SIGNATURE_SECRET',
'VONAGE_WHATSAPP_NUMBER'
]);
tejas-raskar marked this conversation as resolved.
Show resolved Hide resolved

if (context.req.method == 'GET') {
return context.res.send(getStaticFile('index.html'), 200,
{'Content-Type': 'text/html; charset=utf-8'});
}

final token = context.req.headers['authorization'].split(' ')[1];

if (token == null) {
return context.res.json({'ok': false, 'error': 'Unauthorized'}, 401);
}

if (context.req.body['from'] == null || context.req.body['text'] == null) {
return context.res.json({'ok': false, 'error': 'Missing required fields.'}, 400);
}

final jwt = JWT.verify(token, SecretKey(Platform.environment['VONAGE_API_SIGNATURE_SECRET']!));

if (jwt.payload['payload_hash'] == null) {
return context.res.json({'ok': false, 'error': 'Missing payload hash.'}, 400);
}

final payloadHash = sha256.convert(utf8.encode(context.req.bodyRaw)).toString();

if (jwt.payload['payload_hash'] != payloadHash) {
return context.res.json({'ok': false, 'error': 'Payload hash mismatch.'}, 401);
}

final basicAuthToken = base64Encode(utf8.encode('${Platform.environment['VONAGE_API_KEY']}:${Platform.environment['VONAGE_API_SECRET']}'));

await http.post(
Uri.parse('https://messages-sandbox.nexmo.com/v1/messages'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic $basicAuthToken',
},
body: jsonEncode({
'from': Platform.environment['VONAGE_WHATSAPP_NUMBER'],
'to': context.req.body['from'],
'message_type': 'text',
'text': 'Hi there! You sent me: ${context.req.body['text']}',
'channel': 'whatsapp',
}),
);

return context.res.json({'ok': true});
}
31 changes: 31 additions & 0 deletions dart/whatsapp_with_vonage/lib/utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'dart:io';
import 'package:path/path.dart' as p;

final staticFolder = p.join(p.dirname(Platform.script.toFilePath()), '../static');

/// Throws an error if any of the keys are missing from the object
/// Parameters:
/// obj - The object to check
/// keys - The list of keys to check for
/// throws Exception
tejas-raskar marked this conversation as resolved.
Show resolved Hide resolved
void throwIfMissing(Map<String, String> obj, List<String> keys) {
final missing = <String>[];
for (final key in keys) {
if (!obj.containsKey(key) || obj[key] == null) {
missing.add(key);
}
}

if (missing.isNotEmpty) {
throw StateError('Missing environment variables: ${missing.join(', ')}');
}
}

/// Returns the contents of a file in the static folder
/// Parameters:
/// fileName - The name of the file to read
/// returns Contents of static/{fileName}
tejas-raskar marked this conversation as resolved.
Show resolved Hide resolved
String getStaticFile(String fileName) {
final file = File(p.join(staticFolder, fileName));
return file.readAsStringSync();
}
16 changes: 16 additions & 0 deletions dart/whatsapp_with_vonage/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: whatsapp_with_vonage
version: 1.0.0

environment:
sdk: ^2.17.0

dependencies:
crypto: ^3.0.2
http: ^0.13.5
path: ^1.8.3
convert: ^3.1.0
dart_jsonwebtoken: ^2.12.0


dev_dependencies:
lints: ^2.0.0
34 changes: 34 additions & 0 deletions dart/whatsapp_with_vonage/static/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WhatsApp Bot with Vonage</title>

<link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink" />
<link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink-icons" />
</head>
<body>
<main class="main-content">
<div class="top-cover u-padding-block-end-56">
<div class="container">
<div
class="u-flex u-gap-16 u-flex-justify-center u-margin-block-start-16"
>
<h1 class="heading-level-1">WhatsApp Bot with Vonage</h1>
<code class="u-un-break-text"></code>
</div>
<p
class="body-text-1 u-normal u-margin-block-start-8"
style="max-width: 50rem"
>
This function listens to incoming webhooks from Vonage regarding
WhatsApp messages, and responds to them. To use the function, send
message to the WhatsApp user provided by Vonage.
</p>
</div>
</div>
</main>
</body>
</html>