this repo has no description
1use crate::state::AppState;
2use axum::{
3 extract::State,
4 http::StatusCode,
5 response::{IntoResponse, Response},
6 Json,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::{json, Value};
10
11const APP_BSKY_NAMESPACE: &str = "app.bsky";
12
13#[derive(Serialize)]
14pub struct GetPreferencesOutput {
15 pub preferences: Vec<Value>,
16}
17
18pub async fn get_preferences(
19 State(state): State<AppState>,
20 headers: axum::http::HeaderMap,
21) -> Response {
22 let token = match crate::auth::extract_bearer_token_from_header(
23 headers.get("Authorization").and_then(|h| h.to_str().ok()),
24 ) {
25 Some(t) => t,
26 None => {
27 return (
28 StatusCode::UNAUTHORIZED,
29 Json(json!({"error": "AuthenticationRequired"})),
30 )
31 .into_response();
32 }
33 };
34
35 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
36 Ok(user) => user,
37 Err(_) => {
38 return (
39 StatusCode::UNAUTHORIZED,
40 Json(json!({"error": "AuthenticationFailed"})),
41 )
42 .into_response();
43 }
44 };
45
46 let user_id: uuid::Uuid =
47 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
48 .fetch_optional(&state.db)
49 .await
50 {
51 Ok(Some(id)) => id,
52 _ => {
53 return (
54 StatusCode::INTERNAL_SERVER_ERROR,
55 Json(json!({"error": "InternalError", "message": "User not found"})),
56 )
57 .into_response();
58 }
59 };
60
61 let prefs_result = sqlx::query!(
62 "SELECT name, value_json FROM account_preferences WHERE user_id = $1",
63 user_id
64 )
65 .fetch_all(&state.db)
66 .await;
67
68 let prefs = match prefs_result {
69 Ok(rows) => rows,
70 Err(_) => {
71 return (
72 StatusCode::INTERNAL_SERVER_ERROR,
73 Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})),
74 )
75 .into_response();
76 }
77 };
78
79 let preferences: Vec<Value> = prefs
80 .into_iter()
81 .filter(|row| {
82 row.name == APP_BSKY_NAMESPACE || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
83 })
84 .filter_map(|row| {
85 if row.name == "app.bsky.actor.defs#declaredAgePref" {
86 return None;
87 }
88 serde_json::from_value(row.value_json).ok()
89 })
90 .collect();
91
92 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
93}
94
95#[derive(Deserialize)]
96pub struct PutPreferencesInput {
97 pub preferences: Vec<Value>,
98}
99
100pub async fn put_preferences(
101 State(state): State<AppState>,
102 headers: axum::http::HeaderMap,
103 Json(input): Json<PutPreferencesInput>,
104) -> Response {
105 let token = match crate::auth::extract_bearer_token_from_header(
106 headers.get("Authorization").and_then(|h| h.to_str().ok()),
107 ) {
108 Some(t) => t,
109 None => {
110 return (
111 StatusCode::UNAUTHORIZED,
112 Json(json!({"error": "AuthenticationRequired"})),
113 )
114 .into_response();
115 }
116 };
117
118 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
119 Ok(user) => user,
120 Err(_) => {
121 return (
122 StatusCode::UNAUTHORIZED,
123 Json(json!({"error": "AuthenticationFailed"})),
124 )
125 .into_response();
126 }
127 };
128
129 let user_id: uuid::Uuid =
130 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
131 .fetch_optional(&state.db)
132 .await
133 {
134 Ok(Some(id)) => id,
135 _ => {
136 return (
137 StatusCode::INTERNAL_SERVER_ERROR,
138 Json(json!({"error": "InternalError", "message": "User not found"})),
139 )
140 .into_response();
141 }
142 };
143
144 for pref in &input.preferences {
145 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
146 Some(t) => t,
147 None => {
148 return (
149 StatusCode::BAD_REQUEST,
150 Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})),
151 )
152 .into_response();
153 }
154 };
155
156 if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
157 return (
158 StatusCode::BAD_REQUEST,
159 Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})),
160 )
161 .into_response();
162 }
163
164 if pref_type == "app.bsky.actor.defs#declaredAgePref" {
165 return (
166 StatusCode::BAD_REQUEST,
167 Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})),
168 )
169 .into_response();
170 }
171 }
172
173 let mut tx = match state.db.begin().await {
174 Ok(tx) => tx,
175 Err(_) => {
176 return (
177 StatusCode::INTERNAL_SERVER_ERROR,
178 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})),
179 )
180 .into_response();
181 }
182 };
183
184 let delete_result = sqlx::query!(
185 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
186 user_id,
187 APP_BSKY_NAMESPACE,
188 format!("{}.%", APP_BSKY_NAMESPACE)
189 )
190 .execute(&mut *tx)
191 .await;
192
193 if delete_result.is_err() {
194 let _ = tx.rollback().await;
195 return (
196 StatusCode::INTERNAL_SERVER_ERROR,
197 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})),
198 )
199 .into_response();
200 }
201
202 for pref in input.preferences {
203 let pref_type = pref.get("$type").and_then(|t| t.as_str()).unwrap();
204
205 let insert_result = sqlx::query!(
206 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
207 user_id,
208 pref_type,
209 pref
210 )
211 .execute(&mut *tx)
212 .await;
213
214 if insert_result.is_err() {
215 let _ = tx.rollback().await;
216 return (
217 StatusCode::INTERNAL_SERVER_ERROR,
218 Json(json!({"error": "InternalError", "message": "Failed to save preference"})),
219 )
220 .into_response();
221 }
222 }
223
224 if let Err(_) = tx.commit().await {
225 return (
226 StatusCode::INTERNAL_SERVER_ERROR,
227 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})),
228 )
229 .into_response();
230 }
231
232 StatusCode::OK.into_response()
233}