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 1 commit
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 | Node (18.0) |
| Entrypoint | `src/main.js` |
| Build Commands | `npm 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-) |
160 changes: 160 additions & 0 deletions cpp/whatsapp-with-vonage/src/main.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#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 void checkEnvVars(runtime::RuntimeContext& context,
const std::vector<std::string>& envVarNames) {
std::vector<std::string> missingVars;
context.log("Checking for missing environment variables:");
for (const std::string& varName : envVarNames) {
context.log(varName.c_str());
const char* varValue = std::getenv(varName.c_str());
if (varValue == nullptr) {
context.error(varName.c_str());
missingVars.push_back(varName);
}
}

if (!missingVars.empty()) {
context.error("Error: Missing environment variables:");
for (const std::string& missingVar : missingVars) {
std::cerr << " " << missingVar;
}
std::cerr << std::endl;
throw std::runtime_error("Missing environment variables.");
}
}

static RuntimeOutput main(RuntimeContext& context) {
RuntimeRequest req = context.req;
RuntimeResponse res = context.res;

context.log("Hello, Logs!");
// checkEnvVars(context, {
// "VONAGE_API_KEY",
// "VONAGE_API_SECRET",
// "VONAGE_API_SIGNATURE_SECRET",
// "VONAGE_WHATSAPP_NUMBER",
// });
// // If something goes wrong, log an error
context.error("Hello, Errors!");
FreSauce marked this conversation as resolved.
Show resolved Hide resolved
// context.log(serveStaticFile(context, "index.html"));
std::vector<std::string> files = listFiles("/usr/local");
FreSauce marked this conversation as resolved.
Show resolved Hide resolved
// std::vector<std::string> files = listFiles("/usr/local");
for (const std::string& file : files) context.log(file);
if (req.method == "GET") {
std::string indexHtml = getStaticFile("index.html");
context.log(indexHtml);
return res.send(indexHtml);
}

Json::Value payload = stringToJson(req.bodyRaw);
context.log(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()) {
Json::Value response;
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);
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);

auto verifier = jwt::verify().allow_algorithm(
jwt::algorithm::hs256(std::getenv("VONAGE_API_SIGNATURE_SECRET")));

auto decoded = jwt::decode(token);
FreSauce marked this conversation as resolved.
Show resolved Hide resolved
verifier.verify(decoded);
std::string bodyHash = sha256(req.bodyRaw.c_str());
Json::Value jwtBody = stringToJson(decoded.get_payload());
context.log(jwtBody.toStyledString());
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);
/* Check for errors */
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