Skip to content
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
174 changes: 174 additions & 0 deletions java/sendMessage/Index.java
Original file line number Diff line number Diff line change
@@ -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<String, BiConsumer<Map<String, Object>, Map<String, String>>> 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()
));
}
Comment on lines +44 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't catch an exception if the API call fails due to a bad API key. Instead, the output is:

{
  "stdout": "",
  "stderr": "io.openruntimes.java.Wrapper$MessageSendingFailedException: Request failed: Forbidden\n\tat io.openruntimes.java.Wrapper.sendEmailMessage(Wrapper.java:109)\n\tat io.openruntimes.java.Wrapper.main(Wrapper.java:45)\n\tat io.openruntimes.java.Server.execute(Server.java:43)\n\tat org.rapidoid.http.handler.optimized.DelegatingParamsAwareReqRespHandler.handleReq(DelegatingParamsAwareReqRespHandler.java:49)\n\tat org.rapidoid.http.handler.AbstractHttpHandlerDecorator.handleReqAndPostProcess(AbstractHttpHandlerDecorator.java:48)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator$3.invokeNext(HttpManagedHandlerDecorator.java:161)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator$3.invoke(HttpManagedHandlerDecorator.java:121)\n\tat org.rapidoid.http.handler.HttpAuthWrapper.wrap(HttpAuthWrapper.java:70)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator.wrap(HttpManagedHandlerDecorator.java:185)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator.handleWithWrappers(HttpManagedHandlerDecorator.java:100)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator.access$200(HttpManagedHandlerDecorator.java:39)\n\tat org.rapidoid.http.handler.HttpManagedHandlerDecorator$2.run(HttpManagedHandlerDecorator.java:83)\n\tat org.rapidoid.job.PredefinedContextJobWrapper.run(PredefinedContextJobWrapper.java:56)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\n"
}

}

private void sendDiscordMessage(Map<String, Object> messageBody, Map<String, String> 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<String> 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<String, Object> messageBody, Map<String, String> variables) {
String mailgunDomain = variables.get("MAILGUN_DOMAIN");
String mailgunApiKey = variables.get("MAILGUN_API_KEY");

Gson gson = new Gson();

String receiver = getMessageField(messageBody, "receiver"); // "[email protected]",
String message = getMessageField(messageBody, "message"); // "Programming is fun!",
String subject = getMessageField(messageBody, "subject"); // "Programming is funny!"

String body = gson.toJson(Map.of(
"from", "<[email protected]>",
"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<String> 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<String, Object> messageBody, Map<String, String> 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"); // "[email protected]",
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<String> 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<String, Object> messageBody, Map<String, String> 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<String, Object> messageBody, String field) {
return Optional.ofNullable((String)messageBody.get(field)).orElseThrow(() -> new IllegalArgumentException(String.format("%s field not specified", field)));
}
122 changes: 122 additions & 0 deletions java/sendMessage/README.md
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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\": \"[email protected]\",\"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).
7 changes: 7 additions & 0 deletions java/sendMessage/deps.gradle
Original file line number Diff line number Diff line change
@@ -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 }