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: added whatsapp-with-vonage function template in C++ #232

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ dist

# Stores VSCode versions used for testing VSCode extensions
.vscode-test
.vscode

# yarn v2
.yarn/cache
Expand Down
39 changes: 39 additions & 0 deletions cpp/whatsapp-with-vonage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Prerequisites
*.d

# Compiled Object files
*.slo
*.lo
*.o
*.obj

# Precompiled Headers
*.gch
*.pch

# Compiled Dynamic libraries
*.so
*.dylib
*.dll

# Fortran module files
*.mod
*.smod

# Compiled Static libraries
*.lai
*.la
*.a
*.lib

# Executables
*.exe
*.out
*.app

# Specific files
appwrite.json
RuntimeContext.h
RuntimeOutput.h
RuntimeResponse.h
RuntimeRequest.h
104 changes: 104 additions & 0 deletions cpp/whatsapp-with-vonage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 💬 C++ 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 | C++ (cpp-20) |
| Entrypoint | `src/main.cc` |
| Build Commands | `git clone https://github.com/Thalhammer/jwt-cpp.git && cd jwt-cpp && mkdir build && cd build && cmake .. && make -j\"$(nproc)\" && make install` |
| 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-) |
139 changes: 139 additions & 0 deletions cpp/whatsapp-with-vonage/src/main.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include <curl/curl.h>
#include <json/value.h>
#include <jwt-cpp/jwt.h>

#include <any>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>

#include "../RuntimeContext.h"
#include "../RuntimeOutput.h"
#include "../RuntimeRequest.h"
#include "../RuntimeResponse.h"
#include "utils.h"

namespace runtime {
class Handler {
public:
static RuntimeOutput main(RuntimeContext& context) {
RuntimeRequest req = context.req;
RuntimeResponse res = context.res;

std::string error = checkEnvVars({
"VONAGE_API_KEY",
"VONAGE_API_SECRET",
"VONAGE_API_SIGNATURE_SECRET",
"VONAGE_WHATSAPP_NUMBER",
});
if (error != "") {
context.error(error);
Json::Value response;
response["error"] = error;
response["ok"] = false;
return res.json(response, 500);
}

if (req.method == "GET") {
std::string indexHtml = getStaticFile("index.html");
Json::Value headers;
headers["Content-Type"] = "text/html; charset=utf-8";
return res.send(indexHtml, 200, headers);
}

Json::Value payload = stringToJson(req.bodyRaw);
context.log("Payload from webhook\n" + payload.toStyledString());
std::vector<std::string> requiredFields = {"from", "text"};
std::vector<std::string> missingFields;

for (const std::string& field : requiredFields)
if (!payload.isMember(field)) missingFields.push_back(field);
if (!missingFields.empty()) {
std::string error = "Missing required fields";
int i = 0;
for (const std::string& field : missingFields) {
error += field;
if (i++ < missingFields.size() - 1) error += ", ";
}
context.error(error);
Json::Value response;
response["error"] = error;
response["ok"] = false;
return res.json(response, 400);
}

std::string token = req.headers["authorization"].asString();
int space = token.find(" ");
token = token.substr(space + 1);
jwt::verifier<jwt::default_clock, jwt::traits::kazuho_picojson>
verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256(
std::getenv("VONAGE_API_SIGNATURE_SECRET")));

jwt::decoded_jwt<jwt::traits::kazuho_picojson> decoded =
jwt::decode(token);
verifier.verify(decoded);

std::string bodyHash = sha256(req.bodyRaw.c_str());
Json::Value jwtBody = stringToJson(decoded.get_payload());

if (strcmp(jwtBody["payload_hash"].asCString(), bodyHash.c_str()) !=
0) {
context.error("Payload hash mismatch.");
Json::Value response;
response["error"] = "Payload hash mismatch.";
response["ok"] = false;
return res.json(response, 401);
}

std::string apiKey = std::getenv("VONAGE_API_KEY");
std::string apiSecret = std::getenv("VONAGE_API_SECRET");
std::string basicAuthToken = base64Encode(apiKey + ":" + apiSecret);

CURL* curl;
CURLcode ret;
curl = curl_easy_init();
struct curl_slist* headers = NULL;
std::string url = "https://messages-sandbox.nexmo.com/v1/messages";

if (curl) {
headers =
curl_slist_append(headers, "Content-Type: application/json");
std::string authHeader = "Authorization: Basic " + basicAuthToken;
headers = curl_slist_append(headers, authHeader.c_str());
curl_easy_setopt(curl, CURLOPT_URL,
"https://messages-sandbox.nexmo.com/v1/messages");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
Json::Value reqBody;
reqBody["from"] = getenv("VONAGE_WHATSAPP_NUMBER");
reqBody["to"] = payload["from"];
reqBody["message_type"] = "text";
reqBody["text"] =
"Hi there! You sent me: " + payload["text"].asString();
reqBody["channel"] = "whatsapp";

std::string data = reqBody.toStyledString();
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, data.length());
curl_easy_setopt(curl, CURLOPT_POST, 1);
ret = curl_easy_perform(curl);
if (ret != CURLE_OK) {
context.error(curl_easy_strerror(ret));
Json::Value response;
response["error"] = curl_easy_strerror(ret);
response["ok"] = false;
return res.json(response, 500);
}
curl_easy_cleanup(curl);
curl = NULL;
curl_slist_free_all(headers);
headers = NULL;
}

Json::Value response;
response["ok"] = true;
return res.json(response);
}
};
} // namespace runtime