this repo has no description
1use crate::api::read_after_write::{
2 FeedOutput, FeedViewPost, LikeRecord, PostView, RecordDescript, extract_repo_rev,
3 format_munged_response, get_local_lag, get_records_since_rev, proxy_to_appview_via_registry,
4};
5use crate::state::AppState;
6use axum::{
7 Json,
8 extract::{Query, State},
9 http::StatusCode,
10 response::{IntoResponse, Response},
11};
12use serde::Deserialize;
13use serde_json::Value;
14use std::collections::HashMap;
15use tracing::warn;
16
17#[derive(Deserialize)]
18pub struct GetActorLikesParams {
19 pub actor: String,
20 pub limit: Option<u32>,
21 pub cursor: Option<String>,
22}
23
24fn insert_likes_into_feed(feed: &mut Vec<FeedViewPost>, likes: &[RecordDescript<LikeRecord>]) {
25 for like in likes {
26 let like_time = &like.indexed_at.to_rfc3339();
27 let idx = feed
28 .iter()
29 .position(|fi| &fi.post.indexed_at < like_time)
30 .unwrap_or(feed.len());
31 let placeholder_post = PostView {
32 uri: like.record.subject.uri.clone(),
33 cid: like.record.subject.cid.clone(),
34 author: crate::api::read_after_write::AuthorView {
35 did: String::new(),
36 handle: String::new(),
37 display_name: None,
38 avatar: None,
39 extra: HashMap::new(),
40 },
41 record: Value::Null,
42 indexed_at: like.indexed_at.to_rfc3339(),
43 embed: None,
44 reply_count: 0,
45 repost_count: 0,
46 like_count: 0,
47 quote_count: 0,
48 extra: HashMap::new(),
49 };
50 feed.insert(
51 idx,
52 FeedViewPost {
53 post: placeholder_post,
54 reply: None,
55 reason: None,
56 feed_context: None,
57 extra: HashMap::new(),
58 },
59 );
60 }
61}
62
63pub async fn get_actor_likes(
64 State(state): State<AppState>,
65 headers: axum::http::HeaderMap,
66 Query(params): Query<GetActorLikesParams>,
67) -> Response {
68 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
69 let auth_user = if let Some(h) = auth_header {
70 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
71 crate::auth::validate_bearer_token(&state.db, &token)
72 .await
73 .ok()
74 } else {
75 None
76 }
77 } else {
78 None
79 };
80 let auth_did = auth_user.as_ref().map(|u| u.did.clone());
81 let auth_key_bytes = auth_user.as_ref().and_then(|u| u.key_bytes.clone());
82 let mut query_params = HashMap::new();
83 query_params.insert("actor".to_string(), params.actor.clone());
84 if let Some(limit) = params.limit {
85 query_params.insert("limit".to_string(), limit.to_string());
86 }
87 if let Some(cursor) = ¶ms.cursor {
88 query_params.insert("cursor".to_string(), cursor.clone());
89 }
90 let proxy_result = match proxy_to_appview_via_registry(
91 &state,
92 "app.bsky.feed.getActorLikes",
93 &query_params,
94 auth_did.as_deref().unwrap_or(""),
95 auth_key_bytes.as_deref(),
96 )
97 .await
98 {
99 Ok(r) => r,
100 Err(e) => return e,
101 };
102 if !proxy_result.status.is_success() {
103 return proxy_result.into_response();
104 }
105 let rev = match extract_repo_rev(&proxy_result.headers) {
106 Some(r) => r,
107 None => return proxy_result.into_response(),
108 };
109 let mut feed_output: FeedOutput = match serde_json::from_slice(&proxy_result.body) {
110 Ok(f) => f,
111 Err(e) => {
112 warn!("Failed to parse actor likes response: {:?}", e);
113 return proxy_result.into_response();
114 }
115 };
116 let requester_did = match &auth_did {
117 Some(d) => d.clone(),
118 None => return (StatusCode::OK, Json(feed_output)).into_response(),
119 };
120 let actor_did = if params.actor.starts_with("did:") {
121 params.actor.clone()
122 } else {
123 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
124 let suffix = format!(".{}", hostname);
125 let short_handle = if params.actor.ends_with(&suffix) {
126 params.actor.strip_suffix(&suffix).unwrap_or(¶ms.actor)
127 } else {
128 ¶ms.actor
129 };
130 match sqlx::query_scalar!("SELECT did FROM users WHERE handle = $1", short_handle)
131 .fetch_optional(&state.db)
132 .await
133 {
134 Ok(Some(did)) => did,
135 Ok(None) => return (StatusCode::OK, Json(feed_output)).into_response(),
136 Err(e) => {
137 warn!("Database error resolving actor handle: {:?}", e);
138 return proxy_result.into_response();
139 }
140 }
141 };
142 if actor_did != requester_did {
143 return (StatusCode::OK, Json(feed_output)).into_response();
144 }
145 let local_records = match get_records_since_rev(&state, &requester_did, &rev).await {
146 Ok(r) => r,
147 Err(e) => {
148 warn!("Failed to get local records: {}", e);
149 return proxy_result.into_response();
150 }
151 };
152 if local_records.likes.is_empty() {
153 return (StatusCode::OK, Json(feed_output)).into_response();
154 }
155 insert_likes_into_feed(&mut feed_output.feed, &local_records.likes);
156 let lag = get_local_lag(&local_records);
157 format_munged_response(feed_output, lag)
158}