Skip to content

Commit 08b23b2

Browse files
committed
initial sdks
0 parents  commit 08b23b2

12 files changed

+1229
-0
lines changed

.github/workflows/test.yml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: test
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- main
8+
pull_request:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: erlef/setup-beam@v1
16+
with:
17+
otp-version: "26.0.2"
18+
gleam-version: "1.4.1"
19+
rebar3-version: "3"
20+
# elixir-version: "1.15.4"
21+
- run: gleam deps download
22+
- run: gleam test
23+
- run: gleam format --check src test

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# midas_sdk
2+
3+
[![Package Version](https://img.shields.io/hexpm/v/midas_sdk)](https://hex.pm/packages/midas_sdk)
4+
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/midas_sdk/)
5+
6+
A set of SDK's for building tasks with midas.
7+
Note this is not meant to be an exhaustive set for any given usecase.
8+
Instead if functions as quick implemetations of scripts and automations,
9+
this is the usecase midas wants to champion using Gleam for.
10+
This package can also be used as examples for how to implement sdk's that work with midas.
11+
12+
```sh
13+
gleam add midas_sdk@1
14+
```
15+
```gleam
16+
import midas_sdk
17+
18+
pub fn main() {
19+
// TODO: An example of the project in use
20+
}
21+
```
22+
23+
Further documentation can be found at <https://hexdocs.pm/midas_sdk>.
24+
25+
## Development
26+
27+
```sh
28+
gleam run # Run the project
29+
gleam test # Run the tests
30+
```

gleam.toml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name = "midas_sdk"
2+
version = "0.1.0"
3+
target = "javascript"
4+
5+
description = "A bag of SDK's that can be used with midas."
6+
licences = ["Apache-2.0"]
7+
repository = { type = "github", user = "midas-framework", repo = "midas_sdk" }
8+
links = [{ title = "midas", href = "https://github.com/midas-framework/midas" }]
9+
10+
[dependencies]
11+
gleam_stdlib = ">= 0.34.0 and < 2.0.0"
12+
gleam_http = ">= 3.7.0 and < 4.0.0"
13+
midas = ">= 1.0.0 and < 2.0.0"
14+
gleam_json = ">= 1.0.0 and < 3.0.0"
15+
snag = ">= 0.3.0 and < 1.0.0"
16+
17+
[dev-dependencies]
18+
gleeunit = ">= 1.0.0 and < 2.0.0"

manifest.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file was generated by Gleam
2+
# You typically do not need to edit this file
3+
4+
packages = [
5+
{ name = "gleam_http", version = "3.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "EA66440C2269F7CED0F6845E5BD0DB68095775D627FA709A841CA78A398D6D56" },
6+
{ name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" },
7+
{ name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" },
8+
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
9+
{ name = "midas", version = "0.4.7", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_json", "gleam_stdlib", "snag"], otp_app = "midas", source = "hex", outer_checksum = "B0B547EA5139E63B79B3CE3246098F9EA1162E2B96A7D4938E3EB8736FF3AE9E" },
10+
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
11+
]
12+
13+
[requirements]
14+
gleam_http = { version = ">= 3.7.0 and < 4.0.0" }
15+
gleam_json = { version = ">= 2.0.0 and < 3.0.0" }
16+
gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" }
17+
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
18+
midas = { version = ">= 0.4.7 and < 1.0.0" }
19+
snag = { version = ">= 0.3.0 and < 1.0.0" }

src/midas/sdk/google.gleam

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import gleam/bit_array
2+
import gleam/dynamic
3+
import gleam/http/request
4+
import gleam/http/response
5+
import gleam/int
6+
import gleam/json
7+
import gleam/list
8+
import gleam/option.{None, Some}
9+
import gleam/result.{try}
10+
import gleam/string
11+
import gleam/uri.{Uri}
12+
import midas/task as t
13+
import snag
14+
15+
pub type App {
16+
App(client_id: String, redirect_uri: String)
17+
}
18+
19+
const auth_host = "accounts.google.com"
20+
21+
const auth_path = "/o/oauth2/auth"
22+
23+
pub fn authenticate(app, scopes) {
24+
let App(client_id, redirect_uri) = app
25+
let state = int.to_string(int.random(1_000_000_000))
26+
let url = auth_url(client_id, redirect_uri, scopes, state)
27+
use redirect <- t.do(t.follow(url))
28+
use #(access_token, token_type, returned_state) <- t.try(
29+
auth_redirect(redirect) |> result.map_error(snag.new),
30+
)
31+
use Nil <- t.try(case returned_state == state {
32+
True -> Ok(Nil)
33+
False -> Error(snag.new("returned state was not equal to sent state"))
34+
})
35+
36+
use Nil <- t.try(case token_type == "Bearer" {
37+
True -> Ok(Nil)
38+
False -> Error(snag.new("returned token_type was not 'Bearer'"))
39+
})
40+
t.Done(access_token)
41+
}
42+
43+
fn auth_url(client_id, redirect_uri, scopes, state) {
44+
let query = [
45+
#("client_id", client_id),
46+
#("response_type", "token"),
47+
#("redirect_uri", redirect_uri),
48+
#("state", state),
49+
#("scope", string.join(scopes, " ")),
50+
]
51+
let query = Some(uri.query_to_string(query))
52+
Uri(Some("https"), None, Some(auth_host), None, auth_path, query, None)
53+
|> uri.to_string
54+
}
55+
56+
pub fn auth_redirect(redirect) {
57+
let Uri(fragment: fragment, ..) = redirect
58+
use hash <- try(case fragment {
59+
Some(hash) -> Ok(hash)
60+
None -> Error("uri did not have a fragment")
61+
})
62+
use parts <- try(
63+
uri.parse_query(hash)
64+
|> result.replace_error("Failed to parse query: " <> hash),
65+
)
66+
use access_token <- try(key_find(parts, "access_token"))
67+
use token_type <- try(key_find(parts, "token_type"))
68+
use returned_state <- try(key_find(parts, "state"))
69+
// other parts expires_in and scope
70+
Ok(#(access_token, token_type, returned_state))
71+
}
72+
73+
fn key_find(items, key) {
74+
list.key_find(items, key)
75+
|> result.replace_error("Did not find key: " <> key)
76+
}
77+
78+
const openid_host = "openidconnect.googleapis.com"
79+
80+
const userinfo_path = "/v1/userinfo"
81+
82+
pub fn userinfo(token) {
83+
let request = userinfo_request(token)
84+
use response <- t.do(t.fetch(request))
85+
use response <- t.try(userinfo_response(response))
86+
t.Done(response)
87+
}
88+
89+
pub fn userinfo_request(token) {
90+
request.new()
91+
|> request.set_host(openid_host)
92+
|> request.prepend_header("Authorization", string.append("Bearer ", token))
93+
|> request.set_path(userinfo_path)
94+
|> request.set_body(<<>>)
95+
}
96+
97+
pub fn userinfo_response(response: response.Response(BitArray)) {
98+
use json <- try(
99+
bit_array.to_string(response.body)
100+
|> result.replace_error(snag.new("not utf8 encoded")),
101+
)
102+
use message <- try(
103+
json.decode_bits(response.body, dynamic.field("email", dynamic.string))
104+
|> result.map_error(fn(reason) {
105+
snag.new(string.inspect(reason))
106+
|> snag.layer("failed to decode message")
107+
}),
108+
)
109+
Ok(message)
110+
}

src/midas/sdk/google/gmail.gleam

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import gleam/bit_array
2+
import gleam/dynamic
3+
import gleam/http
4+
import gleam/http/request
5+
import gleam/http/response
6+
import gleam/json
7+
import gleam/result.{try}
8+
import gleam/string
9+
import midas/task as t
10+
import snag
11+
12+
const api_host = "gmail.googleapis.com"
13+
14+
fn base_request(token) {
15+
request.new()
16+
|> request.set_host(api_host)
17+
|> request.prepend_header("Authorization", string.append("Bearer ", token))
18+
}
19+
20+
fn get(token, path) {
21+
base_request(token)
22+
|> request.set_path(path)
23+
|> request.set_body(<<>>)
24+
}
25+
26+
fn post(token, path, mime, content) {
27+
base_request(token)
28+
|> request.set_method(http.Post)
29+
|> request.set_path(path)
30+
|> request.prepend_header("content-type", mime)
31+
|> request.set_body(content)
32+
}
33+
34+
pub fn send(token, from, to, message) {
35+
let request = send_request(token, from, to, message)
36+
use response <- t.do(t.fetch(request))
37+
use response <- t.try(send_response(response))
38+
t.Done(response)
39+
}
40+
41+
pub fn send_request(token, from, to, message) {
42+
let path = "/gmail/v1/users/" <> from <> "/messages/send"
43+
let mime = "application/json; charset=UTF-8"
44+
let email =
45+
"From: "
46+
<> from
47+
<> "\r\nTo: "
48+
<> to
49+
<> "\r\nContent-Type: text/html; charset=utf-8 "
50+
<> "\r\n"
51+
<> message
52+
let raw = bit_array.base64_encode(<<email:utf8>>, True)
53+
let body = json.object([#("raw", json.string(raw))]) |> json.to_string
54+
post(token, path, mime, <<body:utf8>>)
55+
|> request.set_query([#("alt", "json")])
56+
}
57+
58+
pub fn send_response(response: response.Response(BitArray)) {
59+
use json <- try(
60+
bit_array.to_string(response.body)
61+
|> result.replace_error(snag.new("not utf8 encoded")),
62+
)
63+
use message <- try(
64+
json.decode_bits(response.body, message_decoder)
65+
|> result.map_error(fn(reason) {
66+
snag.new(string.inspect(reason))
67+
|> snag.layer("failed to decode message")
68+
}),
69+
)
70+
Ok(message)
71+
}
72+
73+
pub type Message {
74+
Message(id: String, thread_id: String)
75+
}
76+
77+
fn message_decoder(raw) {
78+
dynamic.decode2(
79+
Message,
80+
dynamic.field("id", dynamic.string),
81+
dynamic.field("threadId", dynamic.string),
82+
// dynamic.field("labelIds", dynamic.list(dynamic.string)),
83+
)(raw)
84+
}
85+
86+
pub fn list_messages(token, user_id) {
87+
let request = list_messages_request(token, user_id)
88+
use response <- t.do(t.fetch(request))
89+
use response <- t.try(list_messages_response(response))
90+
t.Done(response)
91+
}
92+
93+
pub fn list_messages_request(token, user_id) {
94+
let path = "/gmail/v1/users/" <> user_id <> "/messages"
95+
get(token, path)
96+
}
97+
98+
pub fn list_messages_response(response: response.Response(BitArray)) {
99+
use json <- try(
100+
bit_array.to_string(response.body)
101+
|> result.replace_error(snag.new("not utf8 encoded")),
102+
)
103+
let decoder = dynamic.field("messages", dynamic.list(message_decoder))
104+
use message <- try(
105+
json.decode_bits(response.body, decoder)
106+
|> result.map_error(fn(reason) {
107+
snag.new(string.inspect(reason))
108+
|> snag.layer("failed to decode messages")
109+
}),
110+
)
111+
Ok(message)
112+
}

0 commit comments

Comments
 (0)