slop slop slop sahuuuurrr
0
fork

Configure Feed

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

fix author feed more

dawn 2c2e4e4d 034a37ce

+170 -37
+170 -37
src/main.rs
··· 380 380 } 381 381 } 382 382 383 + fn profile_to_basic(full: serde_json::Value) -> serde_json::Value { 384 + let mut basic = serde_json::json!({ 385 + "did": full["did"], 386 + "handle": full["handle"], 387 + }); 388 + if let Some(v) = full.get("displayName") { 389 + basic["displayName"] = v.clone(); 390 + } 391 + if let Some(v) = full.get("avatar") { 392 + basic["avatar"] = v.clone(); 393 + } 394 + if let Some(v) = full.get("viewer") { 395 + basic["viewer"] = v.clone(); 396 + } 397 + if let Some(v) = full.get("labels") { 398 + basic["labels"] = v.clone(); 399 + } 400 + if let Some(v) = full.get("associated") { 401 + basic["associated"] = v.clone(); 402 + } 403 + if let Some(v) = full.get("createdAt") { 404 + basic["createdAt"] = v.clone(); 405 + } 406 + basic 407 + } 408 + 383 409 async fn get_post_view( 384 410 app_state: &AppState, 385 411 uri_str: &str, ··· 414 440 415 441 let author_profile = get_profile_internal(&app_state, repo.did.as_str(), viewer_did).await?; 416 442 417 - let mut viewer_state = serde_json::json!({}); 443 + let mut viewer_state = serde_json::json!({ 444 + "muted": false, 445 + "blockedBy": false, 446 + }); 418 447 if let Some(viewer) = viewer_did { 419 448 let like = app_state 420 449 .hydrant ··· 448 477 } 449 478 } 450 479 480 + let mut val_json = serde_json::to_value(&record.value).unwrap_or(serde_json::json!({})); 481 + if val_json.get("$type").is_none() { 482 + val_json["$type"] = serde_json::json!("app.bsky.feed.post"); 483 + } 484 + 485 + let created_at = val_json 486 + .get("createdAt") 487 + .and_then(|v| v.as_str()) 488 + .unwrap_or(""); 489 + 451 490 let mut post_view = serde_json::json!({ 491 + "$type": "app.bsky.feed.defs#postView", 452 492 "uri": uri_str, 453 493 "cid": record.cid.to_string(), 454 - "author": author_profile, 455 - "record": record.value, 494 + "author": profile_to_basic(author_profile), 495 + "record": val_json, 456 496 "replyCount": app_state.hydrant.backlinks.count(uri_str.to_string()).source("app.bsky.feed.post").run().await.unwrap_or(0), 457 497 "repostCount": app_state.hydrant.backlinks.count(uri_str.to_string()).source("app.bsky.feed.repost").run().await.unwrap_or(0), 458 498 "likeCount": app_state.hydrant.backlinks.count(uri_str.to_string()).source("app.bsky.feed.like").run().await.unwrap_or(0), 459 - "indexedAt": chrono::Utc::now().to_rfc3339(), 499 + "quoteCount": 0, 500 + "indexedAt": if created_at.is_empty() { chrono::Utc::now().to_rfc3339() } else { created_at.to_string() }, 460 501 "viewer": viewer_state, 461 502 "labels": [], 462 503 }); 463 504 464 - let val_json = serde_json::to_value(&record.value).unwrap_or(serde_json::json!({})); 465 505 if let Some(record_embed) = val_json.get("embed") { 466 506 let t = record_embed 467 507 .get("$type") ··· 512 552 "labels": quoted_post["labels"], 513 553 "indexedAt": quoted_post["indexedAt"], 514 554 "embeds": quoted_post.get("embed").map(|e| vec![e]).unwrap_or_default(), 555 + "likeCount": quoted_post["likeCount"], 556 + "repostCount": quoted_post["repostCount"], 557 + "replyCount": quoted_post["replyCount"], 558 + "quoteCount": quoted_post["quoteCount"], 515 559 } 516 560 }); 517 561 } ··· 685 729 .map(|h| h.to_string()) 686 730 .unwrap_or_else(|| did_str.to_string()); 687 731 688 - let mut viewer_state = serde_json::json!({}); 732 + let mut viewer_state = serde_json::json!({ 733 + "muted": false, 734 + "blockedBy": false, 735 + }); 689 736 if let Some(viewer) = viewer_did { 690 737 if viewer != did_str { 691 738 // following: does viewer follow did_str? ··· 780 827 }); 781 828 782 829 if let Some(rec) = profile_record { 783 - let value = 784 - serde_json::to_value(rec.value).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; 830 + let value = serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 785 831 if let Some(obj) = profile.as_object_mut() { 786 832 if let Some(display_name) = value.get("displayName") { 787 833 obj.insert("displayName".to_string(), display_name.clone()); ··· 810 856 "banner".to_string(), 811 857 serde_json::json!(app_state.cdn("banner", did_str, link)), 812 858 ); 859 + } 860 + if let Some(created_at) = value.get("createdAt") { 861 + obj.insert("createdAt".to_string(), created_at.clone()); 813 862 } 814 863 } 815 864 } 816 865 817 866 Ok(profile) 818 867 } 819 - 820 868 #[derive(Deserialize)] 821 869 struct GetAuthorFeedParams { 822 870 actor: String, ··· 826 874 cursor: Option<String>, 827 875 #[serde(default)] 828 876 filter: Option<String>, 877 + #[serde(default, rename = "includePins")] 878 + include_pins: Option<bool>, 829 879 } 830 880 831 881 async fn get_author_feed( ··· 855 905 856 906 let limit = params.limit.unwrap_or(50).min(100); 857 907 908 + // Note: Official AppView uses timestamp cursors. 909 + // Our local index uses rkeys for list_records. 910 + let rkey_cursor = if let Some(c) = &params.cursor { 911 + if c.len() < 20 { Some(c.as_str()) } else { None } 912 + } else { 913 + None 914 + }; 915 + 858 916 // Get posts 859 917 let posts_list = match repo 860 - .list_records("app.bsky.feed.post", limit, true, params.cursor.as_deref()) 918 + .list_records("app.bsky.feed.post", limit, false, rkey_cursor) 861 919 .await 862 920 { 863 921 Ok(rl) => rl, ··· 869 927 870 928 // Get reposts 871 929 let reposts_list = match repo 872 - .list_records( 873 - "app.bsky.feed.repost", 874 - limit, 875 - true, 876 - params.cursor.as_deref(), 877 - ) 930 + .list_records("app.bsky.feed.repost", limit, false, rkey_cursor) 878 931 .await 879 932 { 880 933 Ok(rl) => rl, ··· 894 947 } 895 948 }; 896 949 950 + let filter = params.filter.as_deref().unwrap_or("posts_with_replies"); 951 + 897 952 let mut all_items = Vec::new(); 898 953 for rec in posts_list.records { 899 954 let val_json = serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); ··· 904 959 .to_string(); 905 960 906 961 // Filter logic 907 - let filter = params.filter.as_deref().unwrap_or("posts_with_replies"); 908 962 if filter == "posts_no_replies" && val_json.get("reply").is_some() { 909 963 continue; 910 964 } 911 965 966 + if filter == "posts_and_author_threads" { 967 + if let Some(reply) = val_json.get("reply") { 968 + let root_uri_str = reply 969 + .get("root") 970 + .and_then(|r| r.get("uri")) 971 + .and_then(|u| u.as_str()) 972 + .unwrap_or(""); 973 + if let Ok(root_uri) = jacquard_common::types::string::AtUri::new(root_uri_str) { 974 + if root_uri.authority().as_str() != did.as_str() { 975 + continue; 976 + } 977 + } else { 978 + continue; 979 + } 980 + } 981 + } 982 + 912 983 all_items.push((created_at, "post", rec)); 913 984 } 914 985 for rec in reposts_list.records { ··· 926 997 all_items.truncate(limit); 927 998 928 999 let mut feed = Vec::new(); 1000 + 1001 + // Handle Pins 1002 + let mut pinned_uri_opt = None; 1003 + if params.include_pins.unwrap_or(false) && params.cursor.is_none() { 1004 + if let Ok(Some(profile_rec)) = repo.get_record("app.bsky.actor.profile", "self").await { 1005 + let prof_val = serde_json::to_value(profile_rec.value).unwrap_or(serde_json::json!({})); 1006 + if let Some(pinned_uri) = prof_val.get("pinnedPost").and_then(|v| v.as_str()) { 1007 + if let Ok(post_view) = 1008 + get_post_view(&app_state, pinned_uri, viewer_did.as_deref()).await 1009 + { 1010 + feed.push(serde_json::json!({ 1011 + "post": post_view, 1012 + })); 1013 + pinned_uri_opt = Some(pinned_uri.to_string()); 1014 + } 1015 + } 1016 + } 1017 + } 1018 + 929 1019 for (created_at, kind, rec) in &all_items { 930 1020 if *kind == "post" { 931 1021 let uri = format!( ··· 933 1023 did.as_str(), 934 1024 rec.rkey.as_str() 935 1025 ); 1026 + 1027 + // Skip if it was already included as a pin 1028 + if Some(&uri) == pinned_uri_opt.as_ref() { 1029 + continue; 1030 + } 1031 + 936 1032 let post = match get_post_view(&app_state, &uri, viewer_did.as_deref()).await { 937 1033 Ok(mut p) => { 938 - p["author"] = author_profile.clone(); 1034 + p["author"] = profile_to_basic(author_profile.clone()); 939 1035 p 940 1036 } 941 1037 Err(_) => { 1038 + let mut val_json = 1039 + serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 1040 + if val_json.get("$type").is_none() { 1041 + val_json["$type"] = serde_json::json!("app.bsky.feed.post"); 1042 + } 942 1043 serde_json::json!({ 1044 + "$type": "app.bsky.feed.defs#postView", 943 1045 "uri": uri, 944 1046 "cid": rec.cid.to_string(), 945 - "author": author_profile.clone(), 946 - "record": rec.value, 1047 + "author": profile_to_basic(author_profile.clone()), 1048 + "record": val_json, 947 1049 "replyCount": app_state.hydrant.backlinks.count(uri.clone()).source("app.bsky.feed.post").run().await.unwrap_or(0), 948 1050 "repostCount": app_state.hydrant.backlinks.count(uri.clone()).source("app.bsky.feed.repost").run().await.unwrap_or(0), 949 1051 "likeCount": app_state.hydrant.backlinks.count(uri.clone()).source("app.bsky.feed.like").run().await.unwrap_or(0), 950 - "indexedAt": chrono::Utc::now().to_rfc3339(), 1052 + "quoteCount": 0, 1053 + "indexedAt": if created_at.is_empty() { chrono::Utc::now().to_rfc3339() } else { created_at.clone() }, 1054 + "viewer": { "muted": false, "blockedBy": false }, 951 1055 "labels": [], 952 1056 }) 953 1057 } 954 1058 }; 955 1059 956 - let val_json = serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 957 - let mut feed_item = serde_json::json!({ "post": post }); 1060 + let mut feed_item = serde_json::json!({ 1061 + "post": post, 1062 + }); 958 1063 1064 + let val_json = serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 959 1065 // Add reply context if it's a reply 960 1066 if let Some(reply) = val_json.get("reply") { 961 1067 if let Some(parent_uri) = reply ··· 998 1104 "post": post_view, 999 1105 "reason": { 1000 1106 "$type": "app.bsky.feed.defs#reasonRepost", 1001 - "by": author_profile.clone(), 1107 + "by": profile_to_basic(author_profile.clone()), 1002 1108 "indexedAt": created_at, 1003 1109 } 1004 1110 })); ··· 1007 1113 } 1008 1114 } 1009 1115 1010 - let next_cursor = all_items.last().map(|i| i.0.clone()); 1116 + let next_cursor = all_items.last().map(|i| i.2.rkey.as_str().to_string()); 1117 + 1118 + feed.reverse(); 1011 1119 1012 1120 Ok(Json(serde_json::json!({ 1013 1121 "feed": feed, ··· 1951 2059 Ok(record_list) => { 1952 2060 for rec in record_list.records { 1953 2061 let rkey = rec.rkey.as_str().to_string(); 2062 + let val_json = 2063 + serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 1954 2064 all_items.push(( 1955 2065 did.clone(), 1956 2066 "app.bsky.feed.post".to_string(), 1957 2067 rkey, 1958 - rec.value, 2068 + val_json, 1959 2069 )); 1960 2070 } 1961 2071 } ··· 1972 2082 Ok(record_list) => { 1973 2083 for rec in record_list.records { 1974 2084 let rkey = rec.rkey.as_str().to_string(); 2085 + let val_json = 2086 + serde_json::to_value(&rec.value).unwrap_or(serde_json::json!({})); 1975 2087 all_items.push(( 1976 2088 did.clone(), 1977 2089 "app.bsky.feed.repost".to_string(), 1978 2090 rkey, 1979 - rec.value, 2091 + val_json, 1980 2092 )); 1981 2093 } 1982 2094 } ··· 1986 2098 } 1987 2099 } 1988 2100 1989 - all_items.sort_by(|a, b| b.2.cmp(&a.2)); 2101 + // Sort by createdAt descending 2102 + all_items.sort_by(|a, b| { 2103 + let ca = a.3.get("createdAt").and_then(|v| v.as_str()).unwrap_or(""); 2104 + let cb = b.3.get("createdAt").and_then(|v| v.as_str()).unwrap_or(""); 2105 + cb.cmp(ca) 2106 + }); 1990 2107 1991 2108 if let Some(c) = params.cursor { 1992 - all_items.retain(|item| item.2 < c); 2109 + all_items.retain(|item| { 2110 + let item_ca = item 2111 + .3 2112 + .get("createdAt") 2113 + .and_then(|v| v.as_str()) 2114 + .unwrap_or(""); 2115 + item_ca < c.as_str() 2116 + }); 1993 2117 } 1994 2118 1995 2119 all_items.truncate(limit); ··· 1998 2122 let mut next_cursor = None; 1999 2123 let viewer_did = get_auth_did(&req); 2000 2124 for (did, col, rkey, value) in all_items { 2001 - next_cursor = Some(rkey.clone()); 2125 + let created_at = value 2126 + .get("createdAt") 2127 + .and_then(|v| v.as_str()) 2128 + .unwrap_or("") 2129 + .to_string(); 2130 + next_cursor = Some(created_at.clone()); 2002 2131 let uri = format!("at://{}/{}/{}", did.as_str(), col, rkey); 2003 2132 2004 - let val_json = serde_json::to_value(&value).unwrap_or(serde_json::json!({})); 2005 - 2006 2133 if col == "app.bsky.feed.post" { 2007 2134 match get_post_view(&app_state, &uri, viewer_did.as_deref()).await { 2008 - Ok(post_view) => feed.push(serde_json::json!({ "post": post_view })), 2135 + Ok(mut post_view) => { 2136 + post_view["author"] = profile_to_basic(post_view["author"].clone()); 2137 + feed.push(serde_json::json!({ "post": post_view })); 2138 + } 2009 2139 Err(e) => tracing::warn!("failed to get post view for {uri}: {e}"), 2010 2140 } 2011 2141 } else if col == "app.bsky.feed.repost" { 2012 - if let Some(subject_uri) = val_json 2142 + if let Some(subject_uri) = value 2013 2143 .get("subject") 2014 2144 .and_then(|s| s.get("uri")) 2015 2145 .and_then(|u| u.as_str()) 2016 2146 { 2017 2147 match get_post_view(&app_state, subject_uri, viewer_did.as_deref()).await { 2018 - Ok(post_view) => { 2148 + Ok(mut post_view) => { 2149 + post_view["author"] = profile_to_basic(post_view["author"].clone()); 2019 2150 match get_profile_internal(&app_state, did.as_str(), viewer_did.as_deref()) 2020 2151 .await 2021 2152 { ··· 2024 2155 "post": post_view, 2025 2156 "reason": { 2026 2157 "$type": "app.bsky.feed.defs#reasonRepost", 2027 - "by": reposter_profile, 2028 - "indexedAt": val_json.get("createdAt").and_then(|c| c.as_str()).map(|s| s.to_string()).unwrap_or_else(|| chrono::Utc::now().to_rfc3339()), 2158 + "by": profile_to_basic(reposter_profile), 2159 + "indexedAt": if created_at.is_empty() { chrono::Utc::now().to_rfc3339() } else { created_at }, 2029 2160 } 2030 2161 })); 2031 2162 } ··· 2043 2174 } 2044 2175 } 2045 2176 } 2177 + 2178 + feed.reverse(); 2046 2179 2047 2180 Ok(Json(serde_json::json!({ 2048 2181 "feed": feed,