Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

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

add authors param to listRecords to filter records by multiple dids

Chad Miller 080d8fe1 0ed4b660

+113 -21
+1
api/scripts/generate-typescript.ts
··· 161 161 isExported: true, 162 162 properties: [ 163 163 { name: "author", type: "string", hasQuestionToken: true }, 164 + { name: "authors", type: "string[]", hasQuestionToken: true }, 164 165 { name: "limit", type: "number", hasQuestionToken: true }, 165 166 { name: "cursor", type: "string", hasQuestionToken: true }, 166 167 { name: "sort", type: "`${TSortField}:${'asc' | 'desc'}` | `${TSortField}:${'asc' | 'desc'},${TSortField}:${'asc' | 'desc'}`", hasQuestionToken: true },
+39 -14
api/src/database.rs
··· 591 591 collection: &str, 592 592 query: &str, 593 593 field: Option<&str>, 594 + _authors: Option<&Vec<String>>, // TODO: Authors filtering for search not yet implemented 594 595 limit: Option<i32>, 595 596 cursor: Option<&str>, 596 597 sort: Option<&str> ··· 600 601 // For lexicon collection, we filter by slice directly 601 602 if collection == "social.slices.lexicon" { 602 603 let order_by = parse_sort_parameter(sort); // Use simple parsing for lexicon collections 604 + 605 + // Note: For lexicon searches, authors filtering is not commonly used as lexicons are typically not user-specific 606 + // But we include it for completeness 603 607 let records = match (cursor, field) { 604 608 (Some(cursor_str), Some(field_name)) => { 605 609 // Try to parse compound cursor, fallback to old cursor format ··· 793 797 slice_uri: &str, 794 798 collection: &str, 795 799 repo: Option<&str>, 800 + authors: Option<&Vec<String>>, 796 801 limit: Option<i32>, 797 802 cursor: Option<&str>, 798 803 sort: Option<&str> ··· 802 807 // For lexicon collection, we filter by slice directly 803 808 if collection == "social.slices.lexicon" { 804 809 let order_by = parse_sort_parameter(sort); // Use simple parsing for lexicon collections 810 + 811 + 812 + // Determine the author list to use 813 + let author_list: Option<Vec<String>> = if let Some(authors_list) = authors { 814 + Some(authors_list.clone()) 815 + } else if let Some(repo_did) = repo { 816 + Some(vec![repo_did.to_string()]) 817 + } else { 818 + None 819 + }; 820 + 805 821 // For lexicon records, filter by slice 806 - let records = match (cursor, repo) { 807 - (Some(cursor_time), Some(repo_did)) => { 822 + let records = match (cursor, author_list.as_ref()) { 823 + (Some(cursor_time), Some(author_list)) => { 808 824 let cursor_dt = cursor_time.parse::<chrono::DateTime<chrono::Utc>>() 809 825 .unwrap_or_else(|_| chrono::Utc::now()); 810 826 sqlx::query_as!( ··· 812 828 r#" 813 829 SELECT uri, cid, did, collection, json, "indexed_at" as indexed_at 814 830 FROM record 815 - WHERE collection = $1 AND json->>'slice' = $2 AND "indexed_at" < $3 AND did = $4 831 + WHERE collection = $1 AND json->>'slice' = $2 AND "indexed_at" < $3 AND did = ANY($4) 816 832 ORDER BY "indexed_at" DESC 817 833 LIMIT $5 818 834 "#, 819 835 collection, 820 836 slice_uri, 821 837 cursor_dt, 822 - repo_did, 838 + &author_list[..], 823 839 limit as i64 824 840 ) 825 841 .fetch_all(&self.pool) ··· 845 861 .fetch_all(&self.pool) 846 862 .await? 847 863 }, 848 - (None, Some(repo_did)) => { 864 + (None, Some(author_list)) => { 849 865 let sql = format!( 850 866 r#" 851 867 SELECT uri, cid, did, collection, json, indexed_at 852 868 FROM record 853 - WHERE collection = $1 AND json->>'slice' = $2 AND did = $3 869 + WHERE collection = $1 AND json->>'slice' = $2 AND did = ANY($3) 854 870 ORDER BY {} 855 871 LIMIT $4 856 872 "#, ··· 859 875 sqlx::query_as::<_, Record>(&sql) 860 876 .bind(collection) 861 877 .bind(slice_uri) 862 - .bind(repo_did) 878 + .bind(author_list) 863 879 .bind(limit as i64) 864 880 .fetch_all(&self.pool) 865 881 .await? ··· 915 931 // Get lexicon-aware ORDER BY clause for non-lexicon collections 916 932 let order_by = parse_sort_parameter_with_lexicon(&self.pool, slice_uri, collection, sort).await; 917 933 934 + // Determine the author list to use 935 + let author_list: Option<Vec<String>> = if let Some(authors_list) = authors { 936 + Some(authors_list.clone()) 937 + } else if let Some(repo_did) = repo { 938 + Some(vec![repo_did.to_string()]) 939 + } else { 940 + None 941 + }; 942 + 918 943 // Now fetch the records with cursor-based pagination 919 - let records = match (cursor, repo) { 920 - (Some(cursor_time), Some(repo_did)) => { 944 + let records = match (cursor, author_list.as_ref()) { 945 + (Some(cursor_time), Some(author_list)) => { 921 946 let cursor_dt = cursor_time.parse::<chrono::DateTime<chrono::Utc>>() 922 947 .unwrap_or_else(|_| chrono::Utc::now()); 923 948 let query = format!( 924 - "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND indexed_at < $2 AND did = $3 ORDER BY {} LIMIT $4", 949 + "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND indexed_at < $2 AND did = ANY($3) ORDER BY {} LIMIT $4", 925 950 order_by 926 951 ); 927 952 sqlx::query_as::<_, Record>(&query) 928 953 .bind(collection) 929 954 .bind(cursor_dt) 930 - .bind(repo_did) 955 + .bind(author_list) 931 956 .bind(limit as i64) 932 957 .fetch_all(&self.pool) 933 958 .await? ··· 946 971 .fetch_all(&self.pool) 947 972 .await? 948 973 }, 949 - (None, Some(repo_did)) => { 974 + (None, Some(author_list)) => { 950 975 let query = format!( 951 - "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND did = $2 ORDER BY {} LIMIT $3", 976 + "SELECT uri, cid, did, collection, json, indexed_at FROM record WHERE collection = $1 AND did = ANY($2) ORDER BY {} LIMIT $3", 952 977 order_by 953 978 ); 954 979 sqlx::query_as::<_, Record>(&query) 955 980 .bind(collection) 956 - .bind(repo_did) 981 + .bind(author_list) 957 982 .bind(limit as i64) 958 983 .fetch_all(&self.pool) 959 984 .await?
+23 -1
api/src/handler_openapi_spec.rs
··· 212 212 items: None, 213 213 properties: None, 214 214 required: None, 215 - default: None, 215 + default: None, 216 + }, 217 + example: None, 218 + }, 219 + OpenApiParameter { 220 + name: "authors".to_string(), 221 + location: "query".to_string(), 222 + description: "Filter by multiple author DIDs (comma-separated)".to_string(), 223 + required: false, 224 + schema: OpenApiSchema { 225 + schema_type: "array".to_string(), 226 + format: None, 227 + items: Some(Box::new(OpenApiSchema { 228 + schema_type: "string".to_string(), 229 + format: None, 230 + items: None, 231 + properties: None, 232 + required: None, 233 + default: None, 234 + })), 235 + properties: None, 236 + required: None, 237 + default: None, 216 238 }, 217 239 example: None, 218 240 },
+1
api/src/handler_records.rs
··· 26 26 &params.slice, 27 27 &params.collection, 28 28 params.repo.as_deref(), 29 + None, // No authors parameter for this endpoint yet 29 30 params.limit, 30 31 params.cursor.as_deref(), 31 32 None, // No sort parameter for this endpoint yet
+49 -6
api/src/handler_xrpc_dynamic.rs
··· 3 3 http::{HeaderMap, StatusCode}, 4 4 response::Json, 5 5 }; 6 - use serde::Deserialize; 6 + use serde::{Deserialize, Deserializer}; 7 7 use chrono::Utc; 8 8 use atproto_client::com::atproto::repo::{CreateRecordRequest, PutRecordRequest, DeleteRecordRequest, create_record, put_record, delete_record, CreateRecordResponse, PutRecordResponse}; 9 9 ··· 11 11 use crate::models::{ListRecordsOutput, Record}; 12 12 use crate::AppState; 13 13 14 + // Custom deserializer to handle comma-separated strings as Vec<String> 15 + fn deserialize_comma_separated<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error> 16 + where 17 + D: Deserializer<'de>, 18 + { 19 + let opt_str: Option<String> = Option::deserialize(deserializer)?; 20 + Ok(opt_str.map(|s| s.split(',').map(|s| s.trim().to_string()).collect())) 21 + } 22 + 14 23 #[derive(Deserialize)] 15 24 pub struct DynamicListParams { 16 25 pub author: Option<String>, 26 + #[serde(deserialize_with = "deserialize_comma_separated")] 27 + pub authors: Option<Vec<String>>, 17 28 pub limit: Option<i32>, 18 29 pub cursor: Option<String>, 19 30 pub slice: String, ··· 47 58 // Parse the XRPC method (e.g., "social.grain.gallery.list") 48 59 if method.ends_with(".list") { 49 60 let collection = method.trim_end_matches(".list").to_string(); 50 - dynamic_list_records_impl(collection, state, params).await 61 + dynamic_list_records_handler(collection, state, params).await 51 62 } else if method.ends_with(".get") { 52 63 let collection = method.trim_end_matches(".get").to_string(); 53 64 dynamic_get_record_impl(collection, state, params).await ··· 82 93 } 83 94 } 84 95 96 + // Handler for list records that properly deserializes query parameters 97 + async fn dynamic_list_records_handler( 98 + collection: String, 99 + state: AppState, 100 + params: serde_json::Value, 101 + ) -> Result<Json<serde_json::Value>, StatusCode> { 102 + // Handle manual parameter extraction for authors since Query<serde_json::Value> doesn't use custom deserializers 103 + let mut manual_params = DynamicListParams { 104 + author: params.get("author").and_then(|v| v.as_str()).map(|s| s.to_string()), 105 + authors: None, 106 + limit: params.get("limit").and_then(|v| v.as_i64()).map(|i| i as i32), 107 + cursor: params.get("cursor").and_then(|v| v.as_str()).map(|s| s.to_string()), 108 + slice: params.get("slice").and_then(|v| v.as_str()).ok_or(StatusCode::BAD_REQUEST)?.to_string(), 109 + sort: params.get("sort").and_then(|v| v.as_str()).map(|s| s.to_string()), 110 + }; 111 + 112 + // Handle authors parameter manually - split comma-separated string 113 + if let Some(authors_str) = params.get("authors").and_then(|v| v.as_str()) { 114 + manual_params.authors = Some(authors_str.split(',').map(|s| s.trim().to_string()).collect()); 115 + } 116 + 117 + dynamic_list_records_impl(collection, state, manual_params).await 118 + } 85 119 86 120 // Implementation for list records 87 121 async fn dynamic_list_records_impl( 88 122 collection: String, 89 123 state: AppState, 90 - params: serde_json::Value, 124 + dynamic_params: DynamicListParams, 91 125 ) -> Result<Json<serde_json::Value>, StatusCode> { 92 - let dynamic_params: DynamicListParams = serde_json::from_value(params) 93 - .map_err(|_| StatusCode::BAD_REQUEST)?; 126 + 127 + // Convert author or authors to the authors parameter 128 + let authors_param = if let Some(authors) = &dynamic_params.authors { 129 + Some(authors) 130 + } else if let Some(author) = &dynamic_params.author { 131 + Some(&vec![author.clone()]) 132 + } else { 133 + None 134 + }; 94 135 95 136 // Use slice-aware method that filters by collection belonging to the slice 96 137 match state.database.get_slice_collection_records( 97 138 &dynamic_params.slice, 98 139 &collection, 99 - dynamic_params.author.as_deref(), 140 + None, // repo parameter is replaced by authors 141 + authors_param, 100 142 dynamic_params.limit, 101 143 dynamic_params.cursor.as_deref(), 102 144 dynamic_params.sort.as_deref(), ··· 138 180 &collection, 139 181 &search_params.query, 140 182 search_params.field.as_deref(), 183 + None, // authors parameter not yet supported for search 141 184 search_params.limit, 142 185 search_params.cursor.as_deref(), 143 186 search_params.sort.as_deref(),