import gleam/erlang/process import gleam/int import gleam/io import gleam/list import gleam/option import gleam/uri import possum/did as at_did import possum/handle as at_handle import atproto import bsky import oauth import tangled const max_concurrent_profile_checks = 40 const oauth_scope = "atproto repo:sh.tangled.graph.follow" const oauth_redirect_uri = "http://127.0.0.1:8080/callback" @external(erlang, "entwine_ffi", "prompt") fn prompt(message: String) -> String fn oauth_client() -> oauth.OAuthClient { let client_id = "http://localhost?" <> uri.query_to_string([ #("redirect_uri", oauth_redirect_uri), #("scope", oauth_scope), ]) oauth.OAuthClient( client_id:, redirect_uri: oauth_redirect_uri, scope: oauth_scope, ) } pub fn find_accounts_to_follow( tangled_profile_holders: List(bsky.Follow), tangled_follows: List(tangled.Follow), ) -> List(bsky.Follow) { let tangled_subjects = tangled_follows |> list.map(fn(follow) { follow.subject }) tangled_profile_holders |> list.filter(fn(follow) { !list.contains(tangled_subjects, follow.subject) }) } fn find_tangled_profile_holders( follows: List(bsky.Follow), ) -> List(bsky.Follow) { let replies = process.new_subject() collect_tangled_profiles(follows, replies, 0, []) } fn collect_tangled_profiles( pending: List(bsky.Follow), replies: process.Subject(#(bsky.Follow, Bool)), active_checks: Int, matching_follows: List(bsky.Follow), ) -> List(bsky.Follow) { case pending { [] if active_checks == 0 -> list.reverse(matching_follows) [follow, ..remaining] if active_checks < max_concurrent_profile_checks -> { let _ = process.spawn_unlinked(fn() { let has_tangled_profile = tangled.is_tangled(follow.subject) process.send(replies, #(follow, has_tangled_profile)) }) collect_tangled_profiles( remaining, replies, active_checks + 1, matching_follows, ) } _ -> { let #(follow, has_tangled_profile) = process.receive_forever(replies) let matching_follows = case has_tangled_profile { True -> [follow, ..matching_follows] False -> matching_follows } collect_tangled_profiles( pending, replies, active_checks - 1, matching_follows, ) } } } fn create_tangled_follows( account_did: at_did.Did, account_pds: String, client: oauth.OAuthClient, credential: oauth.OAuthCredential, follows: List(bsky.Follow), ) -> oauth.OAuthCredential { case follows { [] -> credential [follow, ..remaining] -> { let credential = case tangled.create_tangled_follow( account_did, account_pds, client, credential, follow.subject, ) { Ok(updated_credential) -> { io.println("Follow created") updated_credential } Error(error) -> { io.println(tangled.create_follow_error_message(error)) credential } } create_tangled_follows( account_did, account_pds, client, credential, remaining, ) } } } pub fn main() -> Nil { let raw_handle = prompt("Atmosphere handle: ") let did = atproto.get_did(raw_handle) let pds = atproto.get_pds(did) io.println("Resolved to " <> at_did.to_string(did) <> " on " <> pds <> ".") io.println("Open the authorization URL in your browser to continue.") let client = oauth_client() let credential = oauth.authenticate( "https://" <> pds, at_did.to_string(did), raw_handle, client, ) io.println("Authenticated successfully as " <> credential.subject <> ".") let bsky_follows = bsky.get_bsky_follows(did, pds) let bsky_follow_count = list.length(bsky_follows) io.println(int.to_string(bsky_follow_count) <> " Bluesky follow(s) found.") let tngld_follows = tangled.get_tngld_follows(did, pds) let tngld_follow_count = list.length(tngld_follows) io.println(int.to_string(tngld_follow_count) <> " Tangled follow(s) found.") let tangled_profile_holders = find_tangled_profile_holders(bsky_follows) let bsky_only_follows = find_accounts_to_follow(tangled_profile_holders, tngld_follows) let bsky_only_count = list.length(bsky_only_follows) io.println( int.to_string(bsky_only_count) <> " accounts to follow on Tangled:", ) bsky_only_follows |> list.each(fn(follow) { let label = case atproto.get_handle(follow.subject) { option.Some(handle) -> at_handle.to_string(handle) option.None -> "" } io.println(label <> " (" <> at_did.to_string(follow.subject) <> ")") }) let answer = prompt("Follow on Tangled? (y/n) ") case answer { "y" -> { let _credential = create_tangled_follows(did, pds, client, credential, bsky_only_follows) Nil } "n" -> io.println("Skipped.") _ -> io.println("Invalid answer. Please enter y or n.") } }