From 7c14873c081f503577d71243aac457ef5ee1a886 Mon Sep 17 00:00:00 2001 From: Shardul Kurdukar <24shardul@gmail.com> Date: Tue, 10 Oct 2023 00:19:24 +0530 Subject: [PATCH 1/3] Created whatsapp-with-vonage-cpp branch --- .gitignore | 1 + cpp/whatsapp-with-vonage/.gitignore | 39 +++++ cpp/whatsapp-with-vonage/README.md | 104 ++++++++++++++ cpp/whatsapp-with-vonage/src/main.cc | 160 +++++++++++++++++++++ cpp/whatsapp-with-vonage/src/utils.h | 106 ++++++++++++++ cpp/whatsapp-with-vonage/static/index.html | 34 +++++ 6 files changed, 444 insertions(+) create mode 100644 cpp/whatsapp-with-vonage/.gitignore create mode 100644 cpp/whatsapp-with-vonage/README.md create mode 100644 cpp/whatsapp-with-vonage/src/main.cc create mode 100644 cpp/whatsapp-with-vonage/src/utils.h create mode 100644 cpp/whatsapp-with-vonage/static/index.html diff --git a/.gitignore b/.gitignore index 6a7d6d8e..be89c02e 100644 --- a/.gitignore +++ b/.gitignore @@ -121,6 +121,7 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +.vscode # yarn v2 .yarn/cache diff --git a/cpp/whatsapp-with-vonage/.gitignore b/cpp/whatsapp-with-vonage/.gitignore new file mode 100644 index 00000000..8ab9d44f --- /dev/null +++ b/cpp/whatsapp-with-vonage/.gitignore @@ -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 \ No newline at end of file diff --git a/cpp/whatsapp-with-vonage/README.md b/cpp/whatsapp-with-vonage/README.md new file mode 100644 index 00000000..595afcbd --- /dev/null +++ b/cpp/whatsapp-with-vonage/README.md @@ -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 ` | +| 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-) | diff --git a/cpp/whatsapp-with-vonage/src/main.cc b/cpp/whatsapp-with-vonage/src/main.cc new file mode 100644 index 00000000..d95b5281 --- /dev/null +++ b/cpp/whatsapp-with-vonage/src/main.cc @@ -0,0 +1,160 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#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& envVarNames) { + std::vector 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!"); + // context.log(serveStaticFile(context, "index.html")); + std::vector files = listFiles("/usr/local"); + // std::vector 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 requiredFields = {"from", "text"}; + std::vector 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); + 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 diff --git a/cpp/whatsapp-with-vonage/src/utils.h b/cpp/whatsapp-with-vonage/src/utils.h new file mode 100644 index 00000000..5d24b0c5 --- /dev/null +++ b/cpp/whatsapp-with-vonage/src/utils.h @@ -0,0 +1,106 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include +#include + +#include +#include +#include +#include + +static std::string base64Encode(const std::string& in) { + std::string out; + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345" + "6789+/"[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) + out.push_back( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + "+/"[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +static std::string sha256(const char* string) { + char outputBuffer[65]; + unsigned char hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX sha256; + SHA256_Init(&sha256); + SHA256_Update(&sha256, string, strlen(string)); + SHA256_Final(hash, &sha256); + int i = 0; + for (i = 0; i < SHA256_DIGEST_LENGTH; i++) { + sprintf(outputBuffer + (i * 2), "%02x", hash[i]); + } + outputBuffer[64] = 0; + return std::string(outputBuffer); +} + +static Json::Value stringToJson(const std::string& jsonStr) { + Json::Value res; + Json::Reader reader; + bool ok = reader.parse(jsonStr, res, false); + if (!ok) { + throw std::runtime_error("Failed to parse JSON string."); + } + return res; +} + +static std::vector listFiles(std::string path) { + // recurisve traverse + std::vector files; + for (const auto& entry : std::filesystem::directory_iterator(path)) { + if (entry.is_directory()) { + std::vector subFiles = listFiles(entry.path()); + files.insert(files.end(), subFiles.begin(), subFiles.end()); + } else { + files.push_back(entry.path()); + } + } + return files; +} + +static std::string getStaticFile(std::string filename) { + std::string staticPath = "/usr/local/static/" + filename; + std::ifstream file; + file.open(staticPath); + if (!file.is_open()) { + throw std::runtime_error("File not found."); + } + std::string content((std::istreambuf_iterator(file)), + (std::istreambuf_iterator())); + return content; +} + +// static std::string serveStaticFile(runtime::RuntimeContext& context, +// std::string filename) { +// std::ifstream file; +// // std::string path = std::filesystem::current_path(); +// std::string path = "/usr/local/server/src/function"; +// context.log(path); +// context.log("........"); +// // for (const auto& entry : +// std::filesystem::directory_iterator(path)) +// // context.log(entry.path()); +// std::vector files = listFiles(path); +// for (const std::string& file : files) context.log(file); +// file.open(filename); +// if (!file.is_open()) { +// throw std::runtime_error("File not found."); +// } +// std::string content((std::istreambuf_iterator(file)), +// (std::istreambuf_iterator())); +// return content; +// } + +#endif \ No newline at end of file diff --git a/cpp/whatsapp-with-vonage/static/index.html b/cpp/whatsapp-with-vonage/static/index.html new file mode 100644 index 00000000..3852090f --- /dev/null +++ b/cpp/whatsapp-with-vonage/static/index.html @@ -0,0 +1,34 @@ + + + + + + + WhatsApp Bot with Vonage + + + + + +
+
+
+
+

WhatsApp Bot with Vonage

+ +
+

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

+
+
+
+ + From 0fbf6076af8aed37c4833145b6318c705474b958 Mon Sep 17 00:00:00 2001 From: Shardul Kurdukar <24shardul@gmail.com> Date: Sat, 28 Oct 2023 15:23:55 +0530 Subject: [PATCH 2/3] fixed the code for static assets --- cpp/whatsapp-with-vonage/README.md | 14 ++-- cpp/whatsapp-with-vonage/src/main.cc | 68 +++++++------------ cpp/whatsapp-with-vonage/src/utils.h | 99 ++++++++++++++++------------ 3 files changed, 86 insertions(+), 95 deletions(-) diff --git a/cpp/whatsapp-with-vonage/README.md b/cpp/whatsapp-with-vonage/README.md index 595afcbd..2c284e54 100644 --- a/cpp/whatsapp-with-vonage/README.md +++ b/cpp/whatsapp-with-vonage/README.md @@ -53,13 +53,13 @@ Sample `401` Response: ## ⚙️ Configuration -| Setting | Value | -| ----------------- | ------------- | -| Runtime | Node (18.0) | -| Entrypoint | `src/main.js` | -| Build Commands | `npm install` | -| Permissions | `any` | -| Timeout (Seconds) | 15 | +| 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 diff --git a/cpp/whatsapp-with-vonage/src/main.cc b/cpp/whatsapp-with-vonage/src/main.cc index d95b5281..8a94a3ef 100644 --- a/cpp/whatsapp-with-vonage/src/main.cc +++ b/cpp/whatsapp-with-vonage/src/main.cc @@ -18,62 +18,39 @@ namespace runtime { class Handler { public: - static void checkEnvVars(runtime::RuntimeContext& context, - const std::vector& envVarNames) { - std::vector 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!"); - // context.log(serveStaticFile(context, "index.html")); - std::vector files = listFiles("/usr/local"); - // std::vector files = listFiles("/usr/local"); - for (const std::string& file : files) context.log(file); + 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"); - context.log(indexHtml); - return res.send(indexHtml); + 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.toStyledString()); - + context.log("Payload from webhook\n" + payload.toStyledString()); std::vector requiredFields = {"from", "text"}; std::vector 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) { @@ -81,6 +58,7 @@ class Handler { if (i++ < missingFields.size() - 1) error += ", "; } context.error(error); + Json::Value response; response["error"] = error; response["ok"] = false; return res.json(response, 400); @@ -89,15 +67,14 @@ class Handler { 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); 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."); @@ -138,7 +115,6 @@ class Handler { 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; diff --git a/cpp/whatsapp-with-vonage/src/utils.h b/cpp/whatsapp-with-vonage/src/utils.h index 5d24b0c5..11eb67b0 100644 --- a/cpp/whatsapp-with-vonage/src/utils.h +++ b/cpp/whatsapp-with-vonage/src/utils.h @@ -56,51 +56,66 @@ static Json::Value stringToJson(const std::string& jsonStr) { return res; } -static std::vector listFiles(std::string path) { - // recurisve traverse - std::vector files; - for (const auto& entry : std::filesystem::directory_iterator(path)) { - if (entry.is_directory()) { - std::vector subFiles = listFiles(entry.path()); - files.insert(files.end(), subFiles.begin(), subFiles.end()); - } else { - files.push_back(entry.path()); - } - } - return files; -} +static const char* getStaticFile(std::string filename) { + // Serving string directly because of the issue with serving static assets + // in cpp template + const char* content = R"( + + + + + + + WhatsApp Bot with Vonage -static std::string getStaticFile(std::string filename) { - std::string staticPath = "/usr/local/static/" + filename; - std::ifstream file; - file.open(staticPath); - if (!file.is_open()) { - throw std::runtime_error("File not found."); - } - std::string content((std::istreambuf_iterator(file)), - (std::istreambuf_iterator())); + + + + +
+
+
+
+

WhatsApp Bot with Vonage

+ +
+

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

+
+
+
+ + + )"; return content; } -// static std::string serveStaticFile(runtime::RuntimeContext& context, -// std::string filename) { -// std::ifstream file; -// // std::string path = std::filesystem::current_path(); -// std::string path = "/usr/local/server/src/function"; -// context.log(path); -// context.log("........"); -// // for (const auto& entry : -// std::filesystem::directory_iterator(path)) -// // context.log(entry.path()); -// std::vector files = listFiles(path); -// for (const std::string& file : files) context.log(file); -// file.open(filename); -// if (!file.is_open()) { -// throw std::runtime_error("File not found."); -// } -// std::string content((std::istreambuf_iterator(file)), -// (std::istreambuf_iterator())); -// return content; -// } +static std::string checkEnvVars(const std::vector& envVarNames) { + std::vector missingVars; + for (const std::string& varName : envVarNames) { + const char* varValue = std::getenv(varName.c_str()); + if (varValue == nullptr) { + missingVars.push_back(varName); + } + } + if (!missingVars.empty()) { + std::string error = "Missing environment variables: "; + for (const std::string& missingVar : missingVars) { + error += missingVar; + error += ", "; + } + error = error.substr(0, error.length() - 2); + return error; + } + return ""; +} #endif \ No newline at end of file From f92b1b21ce2091fdae391267f18bd93cfd10dede Mon Sep 17 00:00:00 2001 From: Shardul Kurdukar <24shardul@gmail.com> Date: Tue, 31 Oct 2023 21:36:22 +0530 Subject: [PATCH 3/3] added specific types --- cpp/whatsapp-with-vonage/src/main.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cpp/whatsapp-with-vonage/src/main.cc b/cpp/whatsapp-with-vonage/src/main.cc index 8a94a3ef..8797268f 100644 --- a/cpp/whatsapp-with-vonage/src/main.cc +++ b/cpp/whatsapp-with-vonage/src/main.cc @@ -67,9 +67,12 @@ class Handler { 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); + jwt::verifier + verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256( + std::getenv("VONAGE_API_SIGNATURE_SECRET"))); + + jwt::decoded_jwt decoded = + jwt::decode(token); verifier.verify(decoded); std::string bodyHash = sha256(req.bodyRaw.c_str());