feat: resolve profiles from appview for display name/handle/avatar
This commit is contained in:
@@ -16,3 +16,4 @@ tracing = "0.1"
|
|||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
|||||||
72
src/main.rs
72
src/main.rs
@@ -274,7 +274,7 @@ struct ErrorResp {
|
|||||||
// --- App State ---
|
// --- App State ---
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
db: Mutex<Connection>,
|
db: Arc<Mutex<Connection>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Auth ---
|
// --- Auth ---
|
||||||
@@ -398,15 +398,54 @@ fn init_db(conn: &Connection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_account(conn: &Connection, did: &str) {
|
fn ensure_account(conn: &Connection, did: &str) {
|
||||||
// handle must be a valid handle format, not a DID
|
let fallback_handle = format!("{}.chat.invalid", did.split(':').last().unwrap_or("unknown"));
|
||||||
let handle = format!("{}.chat.invalid", did.split(':').last().unwrap_or("unknown"));
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO accounts (did, handle, created_at) VALUES (?1, ?2, ?3)",
|
"INSERT OR IGNORE INTO accounts (did, handle, created_at) VALUES (?1, ?2, ?3)",
|
||||||
rusqlite::params![did, handle, now_iso()],
|
rusqlite::params![did, fallback_handle, now_iso()],
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve profile from PDS/appview and update account in DB
|
||||||
|
async fn resolve_and_update_account(db: Arc<Mutex<Connection>>, did: String) {
|
||||||
|
// Skip if already resolved (handle doesn't end with .chat.invalid)
|
||||||
|
{
|
||||||
|
let conn = db.lock().unwrap();
|
||||||
|
let handle: Option<String> = conn.query_row(
|
||||||
|
"SELECT handle FROM accounts WHERE did = ?1",
|
||||||
|
rusqlite::params![did],
|
||||||
|
|row| row.get(0),
|
||||||
|
).ok();
|
||||||
|
if let Some(h) = handle {
|
||||||
|
if !h.ends_with(".chat.invalid") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let appview_url = std::env::var("APPVIEW_URL")
|
||||||
|
.unwrap_or_else(|_| "https://bsky.syu.is".into());
|
||||||
|
let url = format!(
|
||||||
|
"{}/xrpc/app.bsky.actor.getProfile?actor={}",
|
||||||
|
appview_url, did
|
||||||
|
);
|
||||||
|
|
||||||
|
let Ok(resp) = reqwest::get(&url).await else { return };
|
||||||
|
let Ok(profile) = resp.json::<serde_json::Value>().await else { return };
|
||||||
|
|
||||||
|
let handle = profile.get("handle").and_then(|v| v.as_str()).unwrap_or_default();
|
||||||
|
let display_name = profile.get("displayName").and_then(|v| v.as_str());
|
||||||
|
let avatar = profile.get("avatar").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
if !handle.is_empty() {
|
||||||
|
let conn = db.lock().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE accounts SET handle = ?1, display_name = ?2, avatar = ?3 WHERE did = ?4",
|
||||||
|
rusqlite::params![handle, display_name, avatar, did],
|
||||||
|
).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_or_create_convo(conn: &Connection, members: &[String]) -> ConvoView {
|
fn get_or_create_convo(conn: &Connection, members: &[String]) -> ConvoView {
|
||||||
let mut sorted = members.to_vec();
|
let mut sorted = members.to_vec();
|
||||||
sorted.sort();
|
sorted.sort();
|
||||||
@@ -779,10 +818,21 @@ async fn get_convo_for_members(
|
|||||||
message: "Caller must be a member".into(),
|
message: "Caller must be a member".into(),
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
let db = state.db.lock().unwrap();
|
let db = state.db.lock().unwrap();
|
||||||
for m in &members {
|
for m in &members {
|
||||||
ensure_account(&db, m);
|
ensure_account(&db, m);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// Resolve profiles in background
|
||||||
|
for m in &members {
|
||||||
|
let db = state.db.clone();
|
||||||
|
let did = m.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
resolve_and_update_account(db, did).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let db = state.db.lock().unwrap();
|
||||||
let convo = get_or_create_convo(&db, &members);
|
let convo = get_or_create_convo(&db, &members);
|
||||||
Ok(Json(GetConvoResp { convo }))
|
Ok(Json(GetConvoResp { convo }))
|
||||||
}
|
}
|
||||||
@@ -811,6 +861,7 @@ async fn list_convos(
|
|||||||
Query(params): Query<ListConvosParams>,
|
Query(params): Query<ListConvosParams>,
|
||||||
) -> Result<Json<ListConvosResp>, (StatusCode, Json<ErrorResp>)> {
|
) -> Result<Json<ListConvosResp>, (StatusCode, Json<ErrorResp>)> {
|
||||||
let did = require_auth(&headers)?;
|
let did = require_auth(&headers)?;
|
||||||
|
let (convos, member_dids) = {
|
||||||
let db = state.db.lock().unwrap();
|
let db = state.db.lock().unwrap();
|
||||||
let limit = params.limit.unwrap_or(20).min(100);
|
let limit = params.limit.unwrap_or(20).min(100);
|
||||||
let status_filter = params.status.unwrap_or_else(|| "accepted".into());
|
let status_filter = params.status.unwrap_or_else(|| "accepted".into());
|
||||||
@@ -829,6 +880,17 @@ async fn list_convos(
|
|||||||
let convos: Vec<ConvoView> = convo_ids.iter()
|
let convos: Vec<ConvoView> = convo_ids.iter()
|
||||||
.map(|id| load_convo(&db, id, Some(&did))).collect();
|
.map(|id| load_convo(&db, id, Some(&did))).collect();
|
||||||
|
|
||||||
|
let member_dids: Vec<String> = convos.iter()
|
||||||
|
.flat_map(|c| c.members.iter().map(|m| m.did.clone())).collect();
|
||||||
|
|
||||||
|
(convos, member_dids)
|
||||||
|
};
|
||||||
|
// Resolve profiles in background
|
||||||
|
for m in member_dids {
|
||||||
|
let db = state.db.clone();
|
||||||
|
tokio::spawn(async move { resolve_and_update_account(db, m).await; });
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(ListConvosResp { cursor: None, convos }))
|
Ok(Json(ListConvosResp { cursor: None, convos }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1134,7 +1196,7 @@ async fn main() {
|
|||||||
let conn = Connection::open(&db_path).expect("Failed to open database");
|
let conn = Connection::open(&db_path).expect("Failed to open database");
|
||||||
init_db(&conn);
|
init_db(&conn);
|
||||||
|
|
||||||
let state = Arc::new(AppState { db: Mutex::new(conn) });
|
let state = Arc::new(AppState { db: Arc::new(Mutex::new(conn)) });
|
||||||
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
|
|||||||
Reference in New Issue
Block a user