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

Feat sendmessage swift #167

Open
wants to merge 16 commits 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
Empty file added swift/send-message/.gitignore
Empty file.
22 changes: 22 additions & 0 deletions swift/send-message/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "send-message",
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "2.0.3"),
],
targets: [
.executableTarget(
name: "send-message",
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Crypto", package: "swift-crypto"),
],
path: "Sources"
),
]
)
128 changes: 128 additions & 0 deletions swift/send-message/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# sendMessage()

A Swift Cloud Function for sending a message using a specific channel to a receiver

Supported channels are `SMS`, `Email` ,`Discord` and `Twitter`.

_SMS Example payload_

```json
{
"type": "SMS",
"recipient": "+123456789",
"content": "Programming is fun!"
}
```

_Email Example payload_

```json
{
"type": "Email",
"recipient": "hello@example.com",
"content": "Programming is fun!",
"subject": "Programming is funny!"
}
```

_Discord Example payload_

```json
{
"type": "Discord",
"content": "Hi",
"recipient": "username"
}
```

_Twitter Example payload_

```json
{
"type": "Twitter",
"recipient": "",
"content": "Programming is fun!"
}
```
- User specified in receiver will be tagged at the beginning of the tweet as a 'reply'. If no user specified, tweet will be a standard tweet.


_Successful function response:_

```json
{
"success": true
}
```

_Error function response:_

```json
{
"success": false,
"message": "Misconfigurtion Error: Missing environment variables."
}
```

## 📝 Variables

List of variables used by this cloud function:

Mailgun

- **MAILGUN_API_KEY** - API Key for Mailgun
- **MAILGUN_DOMAIN** - Domain Name from Mailgun
- **MAILGUN_FROM_EMAIL_ADDRESS** - Email value for sender

- Sender email address defaults to "me@samples.mailgun.org" if left empty. Format value as "Name <name@domain.com>" to display a name and email or "name@domain.com" for just the email.

Discord

- **DISCORD_BOT_TOKEN** - API token for Discord bot
- **DISCORD_GUILD_ID** - Discord server ID

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:

```bash
$ git clone https://github.com/open-runtimes/examples.git && cd examples
$ cd swift/send-message
```

2. Build the code:

```bash
docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/swift:v2-5.5 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/swift:v2-5.5 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 Swift runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/swift-5.5).

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\",\"recipient\": \"hello@example.com\",\"content\": \"Programming is fun!\",\"subject\": \"Programming is funny!\"}"}' -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).
121 changes: 121 additions & 0 deletions swift/send-message/Sources/DiscordMessenger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import AsyncHTTPClient
import Foundation
import NIO
import NIOFoundationCompat

let createDMURL = "https://discord.com/api/v10/users/@me/channels"

class DiscordMessenger: Messenger{
private let discordBotToken: String
private let discordGuildID: String
private let httpClient: HTTPClient

init(_ env_vars: [String: String], httpClient: HTTPClient) throws {
guard let discordBotToken = env_vars["DISCORD_BOT_TOKEN"],
let discordGuildID = env_vars["DISCORD_GUILD_ID"] else {
throw MessengerError.misconfigurationError(error: "Missing environment variables.")
}
self.discordBotToken = discordBotToken
self.discordGuildID = discordGuildID
self.httpClient = httpClient
}

private func getIDFromUsername(json: [[String:Any]], username: String) throws -> String{
for user in json{
let currentUsername = ((user["user"]) as! [String: Any])["username"] as! String

if currentUsername == username{
return ((user["user"]) as! [String: Any])["id"] as! String
}
}
throw MessengerError.misconfigurationError(error: "No users with username \(username) found in server")
}

//Returns a recipient id given their username
private func getRecipientID(username: String) async throws -> String{
let targetURL = "https://discord.com/api/v10/guilds/\(discordGuildID)/members/search?query=\(username)&limit=1000"
var request = HTTPClientRequest(url: targetURL)

request.method = .GET
request.headers.add(name: "Authorization", value: discordBotToken)

let response:HTTPClientResponse
do{
response = try await httpClient.execute(request, timeout: .seconds(30))
}catch{
throw MessengerError.providerError(error: "Request url not responding or connection timed out")
}

let responseBody: ByteBuffer = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
let responseBodyJson = try JSONSerialization.jsonObject(with: responseBody.getData(at: 0, length: responseBody.readableBytes)!, options: [])

//If no user is returned we still get a status 200. searchUsersByUsername() will throw is misconfiguration error if there are no users that match the username
if response.status != .ok {
throw MessengerError.validationError(error: "Unable to get recipient id, API Status Code: \(response.status), API Error Message: \((responseBodyJson as! [String: Any])["message"] ?? "none")")
}

return try getIDFromUsername(json: responseBodyJson as! Array<[String: Any]>, username: username)
}

//Returns dm channel ID. If there isn't already a dm channel with a user one is made.
private func createDM(recipient_id: String) async throws -> String{
var request = HTTPClientRequest(url: createDMURL)
let jsonRecipientID: [String: String] = ["recipient_id": "\(recipient_id)"]

request.method = .POST
request.headers.add(name: "Content-Type", value: "application/json")
request.headers.add(name: "Authorization", value: discordBotToken)
request.body = .bytes(ByteBuffer(data: (try JSONSerialization.data(withJSONObject: jsonRecipientID))))

let response:HTTPClientResponse
do{
response = try await httpClient.execute(request, timeout: .seconds(30))
}catch{
throw MessengerError.providerError(error: "Request url not responding or connection timed out")
}

let responseBody: ByteBuffer = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
let responseBodyJson = try JSONSerialization.jsonObject(with: responseBody.getData(at: 0, length: responseBody.readableBytes)!, options: []) as! [String: Any]

if response.status != .ok {
throw MessengerError.validationError(error: "Unable to create dm, API Status Code: \(response.status), API Error Message: \(responseBodyJson["message"] ?? "none")")
}

return responseBodyJson["id"] as! String
}

//Sends a message given a Message with members recipient and message.
//Returns an Error type if there is an error, otherwise returns nil
public func sendMessage(messageRequest: Message) async -> Error?{
let dmChannelID:String
do{
dmChannelID = try await createDM(recipient_id: getRecipientID(username: messageRequest.recipient))
}catch{
return error
}

let targetURL = "https://discord.com/api/v10/channels/\(dmChannelID)/messages"
var request = HTTPClientRequest(url: targetURL)
let jsonMessage: [String: String] = ["content": "\(messageRequest.content)"]

request.method = .POST
request.headers.add(name: "Content-Type", value: "application/json")
request.headers.add(name: "Authorization", value: discordBotToken)

do{
request.body = .bytes(ByteBuffer(data: (try JSONSerialization.data(withJSONObject: jsonMessage))))
let response = try await httpClient.execute(request, timeout: .seconds(30))
if response.status == .ok {
//if everything appears to have worked, return nil
return nil
}
let responseBody: ByteBuffer = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
let responseBodyJson = try JSONSerialization.jsonObject(with: responseBody.getData(at: 0, length: responseBody.readableBytes)!, options: []) as! [String: Any]
return MessengerError.validationError(error: "Unable to send dm, API Status Code: \(response.status), API Error Message: \(responseBodyJson["message"] ?? "none")")
}catch{
return MessengerError.providerError(error: "Request url not responding or connection timed out")
}

}

}
56 changes: 56 additions & 0 deletions swift/send-message/Sources/EmailMessenger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import AsyncHTTPClient
import Foundation
import NIO
import NIOFoundationCompat

class EmailMessenger: Messenger{

private let mailgunAPIKey: String
private let mailgunDomain: String
private let fromEmailAddress: String
private let httpClient: HTTPClient

init(_ env_vars: [String: String], httpClient: HTTPClient) throws{
guard let mailgunAPIKey = env_vars["MAILGUN_API_KEY"],
let mailgunDomain = env_vars["MAILGUN_DOMAIN"] else {
throw MessengerError.misconfigurationError(error: "Missing environment variables.")
}
self.mailgunAPIKey = mailgunAPIKey
self.mailgunDomain = mailgunDomain

//sender of the message's email and optionally their name. Can be a non-existent email as long as it is formatted correctly.
//for example “Bob <bob@example.com>” or "me@samples.mailgun.org" or "5555@5555.5555".
fromEmailAddress = env_vars["MAILGUN_FROM_EMAIL_ADDRESS"] ?? "me@samples.mailgun.org"
self.httpClient = httpClient
}

public func sendMessage(messageRequest: Message) async -> Error? {
let targetURL:String = "https://api.mailgun.net/v3/\(mailgunDomain)/messages"
let auth:String = "api:\(mailgunAPIKey)".data(using: .utf8)!.base64EncodedString()
var request = HTTPClientRequest(url: targetURL)
request.method = .POST
request.headers.add(name: "Content-Type", value: "application/x-www-form-urlencoded")
request.headers.add(name: "Authorization", value: "Basic \(auth)")

let bodyString: String = "from=\(fromEmailAddress)&to=\(messageRequest.recipient)&subject=\(messageRequest.subject ?? "")&text=\(messageRequest.content)"
request.body = .bytes(ByteBuffer(bytes: Data(bodyString.utf8)))
do{
let response = try await httpClient.execute(request, timeout: .seconds(30))
if response.status == .ok {
return nil
} else {
let responseBody: ByteBuffer = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
//Some errors, normally ones with authorization, return strings and other errors return json. This do catch handles both cases
do{
let responseBodyJson = try JSONSerialization.jsonObject(with: responseBody.getData(at: 0, length: responseBody.readableBytes)!, options: [])
return MessengerError.validationError(error: "Unable to send email, API Status Code: \(response.status), API Error Message: \((responseBodyJson as! [String: Any])["message"] ?? "none")")
}catch{
let responseBodyString = String(data: Data(buffer: responseBody), encoding: .utf8) ?? "Failed to convert data to UTF-8 string"
return MessengerError.validationError(error: "Unable to send email, API Status Code: \(response.status), API Error Message: \(responseBodyString)")
}
}
}catch{
return MessengerError.providerError(error: "Request url not responding or connection timed out")
}
}
}
Loading