From 0f9e268830d25f4ec51f84532d5180f675ca7771 Mon Sep 17 00:00:00 2001 From: Kfir Zvi Date: Wed, 15 Mar 2023 21:52:21 +0200 Subject: [PATCH 1/2] Added implementation in java for the `sendMessage` function. + tweeter implementation is missing. relates to issue appwrite/appwrite#3959 --- java/sendMessage/Index.java | 164 +++++++++++++++++++++++++++++++++++ java/sendMessage/README.md | 123 ++++++++++++++++++++++++++ java/sendMessage/deps.gradle | 3 + 3 files changed, 290 insertions(+) create mode 100644 java/sendMessage/Index.java create mode 100644 java/sendMessage/README.md create mode 100644 java/sendMessage/deps.gradle diff --git a/java/sendMessage/Index.java b/java/sendMessage/Index.java new file mode 100644 index 00000000..0475bab0 --- /dev/null +++ b/java/sendMessage/Index.java @@ -0,0 +1,164 @@ +import com.google.gson.Gson; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; + +private static class MessageSendingFailedException extends RuntimeException { + public MessageSendingFailedException(String message) { + super(message); + } + + public MessageSendingFailedException(String message, Throwable cause) { + super(message, cause); + } +} + +public RuntimeResponse main(RuntimeRequest req, RuntimeResponse res) { + Map, Map>> messageDispatcher = Map.of( + "Discord", this::sendDiscordMessage, + "Email", this::sendEmailMessage, + "SMS", this::sendSMSMessage, + "Twitter", this::sendTwitterMessage + ); + + String payloadString = req.getPayload(); + Gson gson = new Gson(); + Map payload = gson.fromJson(payloadString, Map.class); + + try { + Optional.ofNullable(messageDispatcher.get((String) payload.get("type"))) + .orElseThrow(() -> new IllegalArgumentException(String.format("No such type: %s", payload.get("type")))) + .accept(payload, req.getVariables()); + return res.json(Map.of("success", true)); + } catch (IllegalArgumentException exception) { + return res.json(Map.of( + "success", false, + "message", exception.getMessage() + )); + } +} + +private void sendDiscordMessage(Map messageBody, Map variables) { + String discordWebhookURL = variables.get("DISCORD_WEBHOOK_URL"); + + Gson gson = new Gson(); + String message = getMessageField("message"); + String body = gson.toJson(Map.of("content", message)); + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(discordWebhookURL)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() > 299) { + throw new MessageSendingFailedException("Request failed: " + response.body()); + } + } catch (IOException | InterruptedException e) { + throw new MessageSendingFailedException(e.getMessage(), e); + } +} + +private void sendEmailMessage(Map messageBody, Map variables) { + String mailgunDomain = variables.get("MAILGUN_DOMAIN"); + String mailgunApiKey = variables.get("MAILGUN_API_KEY"); + + Gson gson = new Gson(); + + String receiver = getMessageField(messageBody, "receiver"); // "hello@example.com", + String message = getMessageField(messageBody, "message"); // "Programming is fun!", + String subject = getMessageField(messageBody, "subject"); // "Programming is funny!" + + String body = gson.toJson(Map.of( + "from", "", + "to", receiver, + "subject", subject, + "text",message) + ); + + HttpClient client = HttpClient.newHttpClient(); + String uri = String.format("https://api.mailgun.net/v3/%s/messages", mailgunDomain); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Content-Type", "application/json") + .header("Authorization", String.format("APIKEY %s", mailgunApiKey)) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() > 299) { + throw new MessageSendingFailedException("Request failed: " + response.body()); + } + } catch (IOException | InterruptedException e) { + throw new MessageSendingFailedException(e.getMessage(), e); + } +} + +private void sendSMSMessage(Map messageBody, Map variables) { + String twilioAccountSid = variables.get("TWILIO_ACCOUNT_SID"); + String twilioAuthToken = variables.get("TWILIO_AUTH_TOKEN"); + String twilioSender = variables.get("TWILIO_SENDER"); + + Gson gson = new Gson(); + + String receiver = getMessageField(messageBody, "receiver"); // "hello@example.com", + String message = getMessageField(messageBody, "message"); // "Programming is fun!", + + String body = gson.toJson(Map.of( + "From", twilioSender, + "To", receiver, + "Body", message) + ); + + HttpClient client = HttpClient.newHttpClient(); + String uri = String.format("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", twilioAccountSid); + String authorizationHeader = "BASIC " + Base64.getEncoder().encodeToString(String.format("%s:%s", twilioAccountSid, twilioAuthToken).getBytes()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Content-Type", "application/json") + .header("Authorization", authorizationHeader) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() > 299) { + throw new MessageSendingFailedException("Request failed: " + response.body()); + } + } catch (IOException | InterruptedException e) { + throw new MessageSendingFailedException(e.getMessage(), e); + } +} + +private void sendTwitterMessage(Map messageBody, Map variables) { + String twitterApiKey = variables.get("TWITTER_API_KEY"); + String twitterApiKeySecret = variables.get("TWITTER_API_KEY_SECRET"); + String twitterAccessToken = variables.get("TWITTER_ACCESS_TOKEN"); + String twitterAccessTokenSecret = variables.get("TWITTER_ACCESS_TOKEN_SECRET"); + + Gson gson = new Gson(); + + String receiver = getMessageField(messageBody, "receiver"); // "hello@example.com", + String message = getMessageField(messageBody, "message"); // "Programming is fun!", + + // TODO: add use of twitter API to send a tweet +} + +private String getMessageField(Map messageBody, String field) { + return Optional.ofNullable(messageBody.get(field)).orElseThrow(() -> new IllegalArgumentException(String.format("%s field not specified", field)); +} \ No newline at end of file diff --git a/java/sendMessage/README.md b/java/sendMessage/README.md new file mode 100644 index 00000000..015a4321 --- /dev/null +++ b/java/sendMessage/README.md @@ -0,0 +1,123 @@ +# SendMessage() + +A Java Cloud Function for sending a message using a specific channel to a receiver + +Supported channels are `SMS`, `Email` ,`Disocrd` and `Twitter`. + +_SMS Example payload_ + +```json +{ "type": "SMS", "receiver": "+123456789", "message": "Programming is fun!" } +``` + +_Email Example payload_ + +```json +{ + "type": "Email", + "receiver": "hello@example.com", + "message": "Programming is fun!", + "subject": "Programming is funny!" +} +``` + +_Discord Example payload_ + +```json +{ + "type": "Discord", + "message": "Hi" +} +``` + +_Twitter Example payload_ + +```json +{ + "type": "Twitter", + "receiver": "", + "message": "Programming is fun!" +} +``` + +_Successful function response:_ + +```json +{ + "success": true +} +``` + +_Error function response:_ + +```json +{ + "success": false, + "message": "Failed to send message,check webhook URL" +} +``` + +## 📝 Variables + +List of variables used by this cloud function: + +Mailgun + +- **MAILGUN_API_KEY** - API Key for Mailgun +- **MAILGUN_DOMAIN** - Domain Name from Mailgun + +Discord + +- **DISCORD_WEBHOOK_URL** - Webhook URL for Discord + +Twilio + +- **TWILIO_ACCOUNT_SID** - Acount SID from Twilio +- **TWILIO_AUTH_TOKEN** - Auth Token from Twilio +- **TWILIO_SENDER** - Sender Phone Number from Twilio + +Twitter + +- **TWITTER_API_KEY** - API Key for Twitter +- **TWITTER_API_KEY_SECRET** - API Key Secret for Twitter +- **TWITTER_ACCESS_TOKEN** - Access Token from Twitter +- **TWITTER_ACCESS_TOKEN_SECRET** - Access Token Secret from Twitter + +## 🚀 Deployment + +1. Clone this repository, and enter this function folder: + +``` +$ git clone https://github.com/open-runtimes/examples.git && cd examples +$ cd java/sendMessage +``` + +2. Enter this function folder and build the code: + +```bash +docker run -e INTERNAL_RUNTIME_ENTRYPOINT=Index.java --rm --interactive --tty --volume $PWD:/usr/code openruntimes/java:v2-18.0 sh /usr/local/src/build.sh +``` + +As a result, a `code.tar.gz` file will be generated. + +3. Start the Open Runtime: + +```bash +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/java:v2-18.0 sh /usr/local/src/start.sh +``` + +Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/python-3.10/example). + +4. Curl Command ( Email ) + +```bash +curl -X POST http://localhost:3000/ -d '{"variables": {"MAILGUN_API_KEY":"YOUR_MAILGUN_API_KEY","MAILGUN_DOMAIN":"YOUR_MAILGUN_DOMAIN"},"payload": "{\"type\": \"Email\",\"receiver\": \"hello@example.com\",\"message\": \"Programming is fun!\",\"subject\": \"Programming is funny!\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" +``` + +```bash +curl -X POST http://localhost:3000/ -d '{"variables": {"DISCORD_WEBHOOK_URL":"YOUR_DISCORD_WEBHOOK_URL"},"payload": "{\"type\": \"Discord\", \"message\": \"Programming is fun!\"}"}' -H "X-Internal-Challenge: secret-key" -H "Content-Type: application/json" +``` + +## 📝 Notes + +- This function is designed for use with Appwrite Cloud Functions. You can learn more about it in [Appwrite docs](https://appwrite.io/docs/functions). diff --git a/java/sendMessage/deps.gradle b/java/sendMessage/deps.gradle new file mode 100644 index 00000000..a2e7f58b --- /dev/null +++ b/java/sendMessage/deps.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation 'com.google.code.gson:gson:2.9.0' +} \ No newline at end of file From 62213f22c1dc205f4c2cec43034b94c6acc65997 Mon Sep 17 00:00:00 2001 From: Kfir Zvi Date: Wed, 12 Apr 2023 04:30:15 +0300 Subject: [PATCH 2/2] Added implementation of the Twitter message type --- java/sendMessage/Index.java | 22 ++++++++++++++++------ java/sendMessage/README.md | 1 - java/sendMessage/deps.gradle | 8 ++++++-- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/java/sendMessage/Index.java b/java/sendMessage/Index.java index 0475bab0..6c43db6d 100644 --- a/java/sendMessage/Index.java +++ b/java/sendMessage/Index.java @@ -11,6 +11,8 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; +import io.github.redouane59.twitter.TwitterClient; +import io.github.redouane59.twitter.signature.TwitterCredentials; private static class MessageSendingFailedException extends RuntimeException { public MessageSendingFailedException(String message) { @@ -51,7 +53,7 @@ private void sendDiscordMessage(Map messageBody, Map messageBody, Map messageBody, String field) { - return Optional.ofNullable(messageBody.get(field)).orElseThrow(() -> new IllegalArgumentException(String.format("%s field not specified", field)); + return Optional.ofNullable((String)messageBody.get(field)).orElseThrow(() -> new IllegalArgumentException(String.format("%s field not specified", field))); } \ No newline at end of file diff --git a/java/sendMessage/README.md b/java/sendMessage/README.md index 015a4321..7653403e 100644 --- a/java/sendMessage/README.md +++ b/java/sendMessage/README.md @@ -35,7 +35,6 @@ _Twitter Example payload_ ```json { "type": "Twitter", - "receiver": "", "message": "Programming is fun!" } ``` diff --git a/java/sendMessage/deps.gradle b/java/sendMessage/deps.gradle index a2e7f58b..7a032e19 100644 --- a/java/sendMessage/deps.gradle +++ b/java/sendMessage/deps.gradle @@ -1,3 +1,7 @@ dependencies { - implementation 'com.google.code.gson:gson:2.9.0' -} \ No newline at end of file + implementation "io.github.redouane59.twitter:twittered:1.27" + implementation 'org.slf4j:slf4j-api:1.7.30' + implementation 'org.slf4j:slf4j-simple:1.7.30' +} + +tasks.withType(Jar) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } \ No newline at end of file