slack status without the slack status.zzstoatzz.io/
quickslice

fix: workaround getFollows scope issue by calling public API directly

Both getProfile and getFollows are public endpoints that work without auth,
but when using OAuth, getFollows requires a scope that doesn't exist yet.
This workaround calls the public API directly for getFollows to bypass
the OAuth scope validation issue.

Fixes #33

+150 -36
+1
CLAUDE.md
··· 1 + - fly logs is a blocking command, you need to run it in the background
+108 -6
Cargo.lock
··· 62 62 "flate2", 63 63 "foldhash", 64 64 "futures-core", 65 - "h2", 65 + "h2 0.3.26", 66 66 "http 0.2.12", 67 67 "httparse", 68 68 "httpdate", ··· 477 477 "quote", 478 478 "syn", 479 479 ] 480 + 481 + [[package]] 482 + name = "atomic-waker" 483 + version = "1.1.2" 484 + source = "registry+https://github.com/rust-lang/crates.io-index" 485 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 480 486 481 487 [[package]] 482 488 name = "atrium-api" ··· 1489 1495 ] 1490 1496 1491 1497 [[package]] 1498 + name = "h2" 1499 + version = "0.4.12" 1500 + source = "registry+https://github.com/rust-lang/crates.io-index" 1501 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 1502 + dependencies = [ 1503 + "atomic-waker", 1504 + "bytes", 1505 + "fnv", 1506 + "futures-core", 1507 + "futures-sink", 1508 + "http 1.2.0", 1509 + "indexmap", 1510 + "slab", 1511 + "tokio", 1512 + "tokio-util", 1513 + "tracing", 1514 + ] 1515 + 1516 + [[package]] 1492 1517 name = "hashbrown" 1493 1518 version = "0.14.5" 1494 1519 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1666 1691 "bytes", 1667 1692 "futures-channel", 1668 1693 "futures-util", 1694 + "h2 0.4.12", 1669 1695 "http 1.2.0", 1670 1696 "http-body", 1671 1697 "httparse", ··· 1677 1703 ] 1678 1704 1679 1705 [[package]] 1706 + name = "hyper-rustls" 1707 + version = "0.27.7" 1708 + source = "registry+https://github.com/rust-lang/crates.io-index" 1709 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 1710 + dependencies = [ 1711 + "http 1.2.0", 1712 + "hyper", 1713 + "hyper-util", 1714 + "rustls 0.23.28", 1715 + "rustls-pki-types", 1716 + "tokio", 1717 + "tokio-rustls 0.26.2", 1718 + "tower-service", 1719 + ] 1720 + 1721 + [[package]] 1680 1722 name = "hyper-tls" 1681 1723 version = "0.6.0" 1682 1724 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2278 2320 "hickory-resolver", 2279 2321 "log", 2280 2322 "rand 0.8.5", 2323 + "reqwest", 2281 2324 "rocketman", 2282 2325 "serde", 2283 2326 "serde_json", ··· 2676 2719 "async-compression", 2677 2720 "base64 0.22.1", 2678 2721 "bytes", 2722 + "encoding_rs", 2679 2723 "futures-core", 2680 2724 "futures-util", 2725 + "h2 0.4.12", 2681 2726 "http 1.2.0", 2682 2727 "http-body", 2683 2728 "http-body-util", 2684 2729 "hyper", 2730 + "hyper-rustls", 2685 2731 "hyper-tls", 2686 2732 "hyper-util", 2687 2733 "ipnet", ··· 2697 2743 "serde_json", 2698 2744 "serde_urlencoded", 2699 2745 "sync_wrapper", 2746 + "system-configuration", 2700 2747 "tokio", 2701 2748 "tokio-native-tls", 2702 2749 "tokio-util", ··· 2822 2869 dependencies = [ 2823 2870 "log", 2824 2871 "ring", 2825 - "rustls-webpki", 2872 + "rustls-webpki 0.101.7", 2826 2873 "sct", 2827 2874 ] 2828 2875 2829 2876 [[package]] 2877 + name = "rustls" 2878 + version = "0.23.28" 2879 + source = "registry+https://github.com/rust-lang/crates.io-index" 2880 + checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" 2881 + dependencies = [ 2882 + "once_cell", 2883 + "rustls-pki-types", 2884 + "rustls-webpki 0.103.3", 2885 + "subtle", 2886 + "zeroize", 2887 + ] 2888 + 2889 + [[package]] 2830 2890 name = "rustls-native-certs" 2831 2891 version = "0.6.3" 2832 2892 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2869 2929 checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 2870 2930 dependencies = [ 2871 2931 "ring", 2932 + "untrusted", 2933 + ] 2934 + 2935 + [[package]] 2936 + name = "rustls-webpki" 2937 + version = "0.103.3" 2938 + source = "registry+https://github.com/rust-lang/crates.io-index" 2939 + checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 2940 + dependencies = [ 2941 + "ring", 2942 + "rustls-pki-types", 2872 2943 "untrusted", 2873 2944 ] 2874 2945 ··· 3163 3234 ] 3164 3235 3165 3236 [[package]] 3237 + name = "system-configuration" 3238 + version = "0.6.1" 3239 + source = "registry+https://github.com/rust-lang/crates.io-index" 3240 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3241 + dependencies = [ 3242 + "bitflags", 3243 + "core-foundation", 3244 + "system-configuration-sys", 3245 + ] 3246 + 3247 + [[package]] 3248 + name = "system-configuration-sys" 3249 + version = "0.6.0" 3250 + source = "registry+https://github.com/rust-lang/crates.io-index" 3251 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 3252 + dependencies = [ 3253 + "core-foundation-sys", 3254 + "libc", 3255 + ] 3256 + 3257 + [[package]] 3166 3258 name = "tagptr" 3167 3259 version = "0.2.0" 3168 3260 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3313 3405 source = "registry+https://github.com/rust-lang/crates.io-index" 3314 3406 checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 3315 3407 dependencies = [ 3316 - "rustls", 3408 + "rustls 0.21.12", 3409 + "tokio", 3410 + ] 3411 + 3412 + [[package]] 3413 + name = "tokio-rustls" 3414 + version = "0.26.2" 3415 + source = "registry+https://github.com/rust-lang/crates.io-index" 3416 + checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 3417 + dependencies = [ 3418 + "rustls 0.23.28", 3317 3419 "tokio", 3318 3420 ] 3319 3421 ··· 3325 3427 dependencies = [ 3326 3428 "futures-util", 3327 3429 "log", 3328 - "rustls", 3430 + "rustls 0.21.12", 3329 3431 "rustls-native-certs", 3330 3432 "tokio", 3331 - "tokio-rustls", 3433 + "tokio-rustls 0.24.1", 3332 3434 "tungstenite", 3333 3435 "webpki-roots", 3334 3436 ] ··· 3465 3567 "httparse", 3466 3568 "log", 3467 3569 "rand 0.8.5", 3468 - "rustls", 3570 + "rustls 0.21.12", 3469 3571 "sha1", 3470 3572 "thiserror", 3471 3573 "url",
+1
Cargo.toml
··· 28 28 async-sqlite = "0.5.0" 29 29 async-trait = "0.1.88" 30 30 rand = "0.8" 31 + reqwest = { version = "0.12", features = ["json"] } 31 32 32 33 [build-dependencies] 33 34 askama = "0.13"
+40 -30
src/main.rs
··· 699 699 #[get("/api/following")] 700 700 async fn get_following( 701 701 session: Session, 702 - oauth_client: web::Data<OAuthClientType>, 702 + _oauth_client: web::Data<OAuthClientType>, 703 703 ) -> Result<impl Responder> { 704 704 // Check if user is logged in 705 705 let did = match session.get::<Did>("did").ok().flatten() { ··· 711 711 } 712 712 }; 713 713 714 - // Restore OAuth session 715 - let bsky_session = match oauth_client.restore(&did).await { 716 - Ok(session) => session, 717 - Err(err) => { 718 - log::error!("Failed to restore session: {}", err); 719 - return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 720 - "error": "Failed to restore session" 721 - }))); 722 - } 723 - }; 714 + // WORKAROUND: Call public API directly for getFollows since OAuth scope isn't working 715 + // Both getProfile and getFollows are public endpoints that don't require auth 716 + // but when called through OAuth, getFollows requires a scope that doesn't exist yet 724 717 725 - let agent = Agent::new(bsky_session); 718 + let mut all_follows = Vec::new(); 719 + let mut cursor: Option<String> = None; 726 720 727 - // Fetch follows from Bluesky 728 - let mut all_follows = Vec::new(); 729 - let mut cursor = None; 721 + // Use reqwest to call the public API directly 722 + let client = reqwest::Client::new(); 730 723 731 724 loop { 732 - let params = atrium_api::app::bsky::graph::get_follows::ParametersData { 733 - actor: atrium_api::types::string::AtIdentifier::Did(did.clone()), 734 - limit: None, // Use default limit 735 - cursor: cursor.clone(), 736 - }; 725 + let mut url = format!( 726 + "https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows?actor={}", 727 + did.as_str() 728 + ); 737 729 738 - match agent.api.app.bsky.graph.get_follows(params.into()).await { 730 + if let Some(c) = &cursor { 731 + url.push_str(&format!("&cursor={}", c)); 732 + } 733 + 734 + match client.get(&url).send().await { 739 735 Ok(response) => { 740 - // Extract DIDs from the follows 741 - for follow in &response.follows { 742 - all_follows.push(follow.did.to_string()); 743 - } 736 + match response.json::<serde_json::Value>().await { 737 + Ok(json) => { 738 + // Extract follows 739 + if let Some(follows) = json["follows"].as_array() { 740 + for follow in follows { 741 + if let Some(did_str) = follow["did"].as_str() { 742 + all_follows.push(did_str.to_string()); 743 + } 744 + } 745 + } 744 746 745 - // Check if there are more pages 746 - cursor = response.cursor.clone(); 747 - if cursor.is_none() { 748 - break; 747 + // Check for cursor 748 + cursor = json["cursor"].as_str().map(|s| s.to_string()); 749 + if cursor.is_none() { 750 + break; 751 + } 752 + } 753 + Err(err) => { 754 + log::error!("Failed to parse follows response: {}", err); 755 + return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 756 + "error": "Failed to parse follows" 757 + }))); 758 + } 749 759 } 750 760 } 751 761 Err(err) => { 752 - log::error!("Failed to fetch follows: {}", err); 762 + log::error!("Failed to fetch follows from public API: {}", err); 753 763 return Ok(HttpResponse::InternalServerError().json(serde_json::json!({ 754 764 "error": "Failed to fetch follows" 755 765 })));