Find your Bluesky friends on Tangled!
tangled bsky atproto possum gleam
0

Configure Feed

Select the types of activity you want to include in your feed.

fmt

+77 -98
+1 -4
src/atproto_records.gleam
··· 48 48 ) 49 49 let request = 50 50 request 51 - |> request.set_header( 52 - "authorization", 53 - "DPoP " <> credential.access_token, 54 - ) 51 + |> request.set_header("authorization", "DPoP " <> credential.access_token) 55 52 |> request.set_header("dpop", dpop_proof) 56 53 57 54 case httpc.send(request) {
+8 -6
src/entwine.gleam
··· 13 13 import tangled 14 14 15 15 const max_concurrent_profile_checks = 40 16 + 16 17 const oauth_scope = "atproto repo:sh.tangled.graph.follow" 18 + 17 19 const oauth_redirect_uri = "http://127.0.0.1:8080/callback" 18 20 19 21 @external(erlang, "entwine_ffi", "prompt") ··· 27 29 #("scope", oauth_scope), 28 30 ]) 29 31 30 - oauth.OAuthClient(client_id:, redirect_uri: oauth_redirect_uri, scope: oauth_scope) 32 + oauth.OAuthClient( 33 + client_id:, 34 + redirect_uri: oauth_redirect_uri, 35 + scope: oauth_scope, 36 + ) 31 37 } 32 38 33 39 pub fn find_accounts_to_follow( ··· 135 141 io.println("Open the authorization URL in your browser to continue.") 136 142 137 143 let credential = 138 - oauth.authenticate( 139 - "https://" <> pds, 140 - at_did.to_string(did), 141 - oauth_client(), 142 - ) 144 + oauth.authenticate("https://" <> pds, at_did.to_string(did), oauth_client()) 143 145 io.println("Authenticated successfully as " <> credential.subject <> ".") 144 146 145 147 let bsky_follows = bsky.get_bsky_follows(did, pds)
+62 -85
src/oauth.gleam
··· 1 + import gleam/bit_array 1 2 import gleam/dict 2 3 import gleam/dynamic/decode 3 4 import gleam/erlang/process 5 + import gleam/http 4 6 import gleam/http/request 5 7 import gleam/http/response 6 - import gleam/http 7 8 import gleam/httpc 8 9 import gleam/json 9 10 import gleam/list 10 11 import gleam/option.{type Option} 11 12 import gleam/string 12 - import gleam/uri 13 13 import gleam/time/timestamp 14 - import gleam/bit_array 14 + import gleam/uri 15 15 16 + import gose 16 17 import gose/jose/jwk 17 18 import gose/jose/jws 18 - import gose 19 19 20 20 import kryptos/crypto 21 21 import kryptos/ec ··· 48 48 } 49 49 50 50 pub type ParResponse { 51 - ParResponse( 52 - request_uri: String, 53 - expires_in: Int, 54 - dpop_nonce: Option(String), 55 - ) 51 + ParResponse(request_uri: String, expires_in: Int, dpop_nonce: Option(String)) 56 52 } 57 53 58 54 pub type TokenResponse { ··· 80 76 ) 81 77 } 82 78 83 - 84 - 85 79 pub fn with_dpop_nonce( 86 80 credential: OAuthCredential, 87 81 dpop_nonce: Option(String), ··· 91 85 92 86 fn random_string() -> String { 93 87 crypto.random_bytes(32) 94 - |> bit_array.base64_url_encode(False) 88 + |> bit_array.base64_url_encode(False) 95 89 } 96 90 97 91 pub fn get_auth_server(pds: String) -> String { ··· 142 136 decode.list(decode.string), 143 137 ) 144 138 145 - decode.success( 146 - OAuthServerMetadata( 147 - issuer: issuer, 148 - authorization_endpoint: authoriization_endpoint, 149 - token_endpoint: token_endpoint, 150 - pushed_authorization_request_endpoint: par_endpoint, 151 - dpop_signing_alg_values_supported: dpop_algorithms, 152 - ) 153 - ) 139 + decode.success(OAuthServerMetadata( 140 + issuer: issuer, 141 + authorization_endpoint: authoriization_endpoint, 142 + token_endpoint: token_endpoint, 143 + pushed_authorization_request_endpoint: par_endpoint, 144 + dpop_signing_alg_values_supported: dpop_algorithms, 145 + )) 154 146 } 155 147 156 148 let assert Ok(metadata) = json.parse(metadata_response.body, metadata_decoder) ··· 162 154 let assert Ok(h) = hash.new(hash.Sha256) 163 155 164 156 let challenge = 165 - h 166 - |> hash.update(<<verifier:utf8>>) 167 - |> hash.final() 168 - |> bit_array.base64_url_encode(False) 157 + h 158 + |> hash.update(<<verifier:utf8>>) 159 + |> hash.final() 160 + |> bit_array.base64_url_encode(False) 169 161 170 162 #(verifier, challenge) 171 163 } ··· 174 166 gose.generate_ec(ec.P256) 175 167 } 176 168 177 - pub fn create_dpop_proof(key: gose.Key(String), method: String, url: String, nonce: Option(String), access_token: Option(String)) -> String { 169 + pub fn create_dpop_proof( 170 + key: gose.Key(String), 171 + method: String, 172 + url: String, 173 + nonce: Option(String), 174 + access_token: Option(String), 175 + ) -> String { 178 176 let jti = random_string() 179 - let #(iat, _) = timestamp.system_time() 177 + let #(iat, _) = 178 + timestamp.system_time() 180 179 |> timestamp.to_unix_seconds_and_nanoseconds() 181 180 182 - let payload = 183 - [ 181 + let payload = [ 184 182 #("jti", json.string(jti)), 185 183 #("htm", json.string(method)), 186 184 #("htu", json.string(url)), ··· 190 188 let payload = case nonce { 191 189 option.None -> payload 192 190 193 - option.Some(nonce_value) -> 194 - [#("nonce", json.string(nonce_value)), ..payload] 191 + option.Some(nonce_value) -> [ 192 + #("nonce", json.string(nonce_value)), 193 + ..payload 194 + ] 195 195 } 196 196 197 197 let payload = case access_token { ··· 224 224 |> jws.with_typ("dpop+jwt") 225 225 |> jws.with_header("jwk", public_jwk) 226 226 227 - let assert Ok(signed) = 228 - jws.sign(unsigned, key: key, payload: payload_bits) 227 + let assert Ok(signed) = jws.sign(unsigned, key: key, payload: payload_bits) 229 228 230 229 let assert Ok(dpop_proof) = jws.serialize_compact(signed) 231 230 dpop_proof 232 231 } 233 232 234 - pub fn push_authorization_request(par_endpoint: String, client_id: String, redirect_uri: String, scope: String, state: String, code_challenge: String, dpop_key: gose.Key(String)) -> ParResponse { 235 - let dpop_proof = create_dpop_proof( 236 - dpop_key, 237 - "POST", 238 - par_endpoint, 239 - option.None, 240 - option.None, 241 - ) 233 + pub fn push_authorization_request( 234 + par_endpoint: String, 235 + client_id: String, 236 + redirect_uri: String, 237 + scope: String, 238 + state: String, 239 + code_challenge: String, 240 + dpop_key: gose.Key(String), 241 + ) -> ParResponse { 242 + let dpop_proof = 243 + create_dpop_proof(dpop_key, "POST", par_endpoint, option.None, option.None) 242 244 243 245 let form_body = 244 246 uri.query_to_string([ ··· 255 257 let assert Ok(par_request) = request.from_uri(par_uri) 256 258 257 259 let par_request = 258 - request.Request( 259 - ..par_request, 260 - method: http.Post, 261 - body: form_body, 262 - ) 260 + request.Request(..par_request, method: http.Post, body: form_body) 263 261 264 262 let par_request = 265 263 par_request 266 - |> request.set_header( 267 - "content-type", 268 - "application/x-www-form-urlencoded", 269 - ) 264 + |> request.set_header("content-type", "application/x-www-form-urlencoded") 270 265 |> request.set_header("dpop", dpop_proof) 271 266 272 267 let assert Ok(par_response) = httpc.send(par_request) ··· 278 273 decode.success(#(request_uri, expires_in)) 279 274 } 280 275 281 - let assert Ok(#(request_uri, expires_in)) = json.parse(par_response.body, par_response_decoder) 276 + let assert Ok(#(request_uri, expires_in)) = 277 + json.parse(par_response.body, par_response_decoder) 282 278 283 279 let dpop_nonce = 284 280 response.get_header(par_response, "dpop-nonce") ··· 286 282 287 283 ParResponse(request_uri:, expires_in:, dpop_nonce:) 288 284 } 289 - 290 - 291 285 292 286 pub fn build_authorization_url( 293 287 authorization_endpoint: String, ··· 316 310 state: String, 317 311 ) -> Option(OAuthSession) { 318 312 dict.get(sessions, state) 319 - |> option.from_result 313 + |> option.from_result 320 314 } 321 315 322 316 pub fn parse_callback_url(callback_url: String) -> #(String, String, String) { ··· 341 335 dpop_nonce: Option(String), 342 336 ) -> TokenResponse { 343 337 let dpop_proof = 344 - create_dpop_proof( 345 - dpop_key, 346 - "POST", 347 - token_endpoint, 348 - dpop_nonce, 349 - option.None, 350 - ) 338 + create_dpop_proof(dpop_key, "POST", token_endpoint, dpop_nonce, option.None) 351 339 352 340 let form_body = 353 341 uri.query_to_string([ ··· 362 350 let assert Ok(token_request) = request.from_uri(token_uri) 363 351 364 352 let token_request = 365 - request.Request( 366 - ..token_request, 367 - method: http.Post, 368 - body: form_body, 369 - ) 353 + request.Request(..token_request, method: http.Post, body: form_body) 370 354 371 355 let token_request = 372 - token_request 373 - |> request.set_header( 374 - "content-type", 375 - "application/x-www-form-urlencoded", 376 - ) 356 + token_request 357 + |> request.set_header("content-type", "application/x-www-form-urlencoded") 377 358 |> request.set_header("dpop", dpop_proof) 378 359 379 360 let assert Ok(token_response) = httpc.send(token_request) ··· 381 362 let dpop_nonce = 382 363 response.get_header(token_response, "dpop-nonce") 383 364 |> option.from_result 384 - 385 365 386 366 let token_response_decoder = { 387 367 use access_token <- decode.field("access_token", decode.string) ··· 395 375 use scope <- decode.field("scope", decode.string) 396 376 use sub <- decode.field("sub", decode.string) 397 377 398 - decode.success( 399 - TokenResponse( 400 - access_token:, 401 - token_type:, 402 - expires_in:, 403 - refresh_token:, 404 - scope:, 405 - sub:, 406 - dpop_nonce:, 407 - ), 408 - ) 378 + decode.success(TokenResponse( 379 + access_token:, 380 + token_type:, 381 + expires_in:, 382 + refresh_token:, 383 + scope:, 384 + sub:, 385 + dpop_nonce:, 386 + )) 409 387 } 410 388 411 389 let assert Ok(tokens) = ··· 461 439 462 440 let oauth_callback.Callback(code:, state: returned_state, issuer:) = 463 441 process.receive_forever(callback_messages) 464 - let assert option.Some(session) = 465 - get_oauth_session(sessions, returned_state) 442 + let assert option.Some(session) = get_oauth_session(sessions, returned_state) 466 443 let assert True = issuer == session.issuer 467 444 468 445 let tokens =
+4 -1
src/oauth_callback.gleam
··· 57 57 } 58 58 } 59 59 60 - fn html_response(status: Int, body: String) -> response.Response(mist.ResponseData) { 60 + fn html_response( 61 + status: Int, 62 + body: String, 63 + ) -> response.Response(mist.ResponseData) { 61 64 response.new(status) 62 65 |> response.set_header("content-type", "text/html; charset=utf-8") 63 66 |> response.set_body(mist.Bytes(bytes_tree.from_string(body)))
+2 -2
test/entwine_test.gleam
··· 30 30 "urn:example:request-123", 31 31 ) 32 32 33 - assert authorization_url == 34 - "https://pds.cute.haus/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fscope%3Datproto&request_uri=urn%3Aexample%3Arequest-123" 33 + assert authorization_url 34 + == "https://pds.cute.haus/oauth/authorize?client_id=http%3A%2F%2Flocalhost%3Fscope%3Datproto&request_uri=urn%3Aexample%3Arequest-123" 35 35 } 36 36 37 37 pub fn create_pkce_returns_matching_challenge_test() {