diff --git a/java/sendMessage/Index.java b/java/sendMessage/Index.java new file mode 100644 index 00000000..6c43db6d --- /dev/null +++ b/java/sendMessage/Index.java @@ -0,0 +1,174 @@ +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; +import io.github.redouane59.twitter.TwitterClient; +import io.github.redouane59.twitter.signature.TwitterCredentials; + +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(messageBody,"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"); + + String message = getMessageField(messageBody, "message"); // "Programming is fun!", + + try { + TwitterClient twitterClient = new TwitterClient(TwitterCredentials.builder() + .apiKey(twitterApiKey) + .apiSecretKey(twitterApiKeySecret) + .accessToken(twitterAccessToken) + .accessTokenSecret(twitterAccessTokenSecret) + .build()); + + twitterClient.postTweet(message); + } catch(Exception e) { + throw new MessageSendingFailedException("There seems to have been a problem either with posting the tweet or with using Twitter's API.", e); + } +} + +private String getMessageField(Map messageBody, String 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 new file mode 100644 index 00000000..7653403e --- /dev/null +++ b/java/sendMessage/README.md @@ -0,0 +1,122 @@ +# 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", + "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..7a032e19 --- /dev/null +++ b/java/sendMessage/deps.gradle @@ -0,0 +1,7 @@ +dependencies { + 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