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

Feature: Add WhatsApp with Vonage for Java #262

Open
wants to merge 1 commit 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions java/whatsapp-with-vonage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# Ignore Gradle project-specific cache directory
.gradle

# Ignore Gradle build output directory
build
103 changes: 103 additions & 0 deletions java/whatsapp-with-vonage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ⚡ Java 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 | Java (18) |
| Entrypoint | `src/Main.java` |
| 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-) |
6 changes: 6 additions & 0 deletions java/whatsapp-with-vonage/deps.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
dependencies {
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-gson:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
}
130 changes: 130 additions & 0 deletions java/whatsapp-with-vonage/src/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package io.openruntimes.java.src;

import io.openruntimes.java.RuntimeContext;
import io.openruntimes.java.RuntimeOutput;

import java.util.Map;
import java.util.HashMap;

import com.google.gson.Gson;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpRequest;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;

import java.io.IOException;

import java.util.Base64;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.gson.io.GsonDeserializer;

import javax.crypto.SecretKey;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;

import java.util.Map;
import java.util.HashMap;

public class Main {

public RuntimeOutput main(RuntimeContext context) throws Exception {
String reqVariables[] = {
"VONAGE_API_KEY",
"VONAGE_API_SECRET",
"VONAGE_API_SIGNATURE_SECRET",
"VONAGE_WHATSAPP_NUMBER"
};
Utils.throw_if_missing(System.getenv(), reqVariables);

if (context.getReq().getMethod().equals("GET")) {
return context.getRes().send("Got GET Request");
}

Gson gson = new Gson();
Map<String, Object> responseMap = new HashMap<String, Object>();
responseMap.put("ok", true);

Map<String, Object> body;
if(context.getReq().getBody() instanceof Map){
body = (Map<String, Object>)context.getReq().getBody();
}else{
responseMap.put("ok", false);
responseMap.put("error", "Invalid Body.");
return context.getRes().json(responseMap, 400);
}
Map<String, String> headers = context.getReq().getHeaders();

try {
String token = (headers.get("authorization") != null && !headers.get("authorization").isEmpty()) ? headers.get("authorization").split(" ")[1] : "";
SecretKey key = Keys.hmacShaKeyFor(System.getenv("VONAGE_API_SIGNATURE_SECRET").getBytes());
Jws<Claims> decoded = Jwts.parser().json(new GsonDeserializer(gson)).setSigningKey(key).build().parseClaimsJws(token);

try{
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(context.getReq().getBodyRaw().getBytes(StandardCharsets.UTF_8));
StringBuilder hexStringBuilder = new StringBuilder();
for (byte b : hashBytes) {
hexStringBuilder.append(String.format("%02x", b));
}

if(!decoded.getBody().get("payload_hash").equals(hexStringBuilder.toString())){
responseMap.put("ok", false);
responseMap.put("error", "Payload hash mismatch.");
return context.getRes().json(responseMap, 401);
}
}catch(NoSuchAlgorithmException e){
responseMap.put("ok", false);
responseMap.put("error", "Payload hash mismatch.");
return context.getRes().json(responseMap, 401);
}
}catch (JwtException e) {
responseMap.put("ok", false);
responseMap.put("error", "Invalid Token");
return context.getRes().json(responseMap, 401);
}
try{
String reqHeader[] = {"from", "text"};
Utils.throw_if_missing(body, reqHeader);
}catch(Exception e){
responseMap.put("ok", false);
responseMap.put("error", e.getMessage());
return context.getRes().json(responseMap, 400);
}

try{
Map<String, String> data = new HashMap<String, String>();
data.put("from", System.getenv("VONAGE_WHATSAPP_NUMBER"));
data.put("to", System.getenv("TO_NUMBER"));
data.put("message_type", "text");
data.put("text", "Hi there! You sent me: "+body.get("text"));
data.put("channel", "whatsapp");

String basicAuth= System.getenv("VONAGE_API_KEY") + ":" + System.getenv("VONAGE_API_SECRET");
String basicAuthToken = "Basic " + Base64.getEncoder().encodeToString(basicAuth.getBytes());

HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://messages-sandbox.nexmo.com/v1/messages"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("Authorization", basicAuthToken)
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(data)))
.build();

HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());

}catch(URISyntaxException | IOException | InterruptedException e){
responseMap.put("ok", false);
responseMap.put("error", e.getMessage());
return context.getRes().json(responseMap, 400);
}
return context.getRes().json(responseMap);
}
}
46 changes: 46 additions & 0 deletions java/whatsapp-with-vonage/src/Utils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.openruntimes.java.src;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;

public class Utils {
/**
* Returns the contents of a file in the static folder
*
* @param fileName The name of the file
* @return Contents of static/{fileName}
* @throws IOException If an I/O error occurs reading from the file
*/
public static String getStaticFile(String fileName) throws IOException {
Path staticFolder = Paths.get("../static/");
Path filePath = staticFolder.resolve(fileName);
List<String> lines = Files.readAllLines(filePath);
return String.join("\n", lines);
}

/**
* Throws an error if any of the keys are missing from the map
*
* @param map The map to check for missing keys
* @param keys The array of keys to check for
* @throws Exception If any required fields are missing
*/
public static void throw_if_missing(Map<String, ?> map, String[] keys) throws Exception {
List<String> missing = new ArrayList<String>();
for (String key : keys) {
if (!map.containsKey(key) || map.get(key) == null) {
missing.add(key);
}
}
if(missing.size() > 0){
throw new Exception("Missing required fields: "+String.join(", ", missing));
}
}
}
34 changes: 34 additions & 0 deletions java/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>