feat(#2050): set matrix avatar in the daemon over the live client

This commit is contained in:
damocles 2026-06-27 19:35:21 +02:00 committed by mara
commit c99fa714d9
8 changed files with 118 additions and 210 deletions

View file

@ -184,3 +184,57 @@ async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDevi
let device_id: OwnedDeviceId = device_id_raw.into();
Ok((user_id, device_id))
}
/// Best-effort: set this account's matrix avatar from the rasterized icon
/// PNG. The path comes from `HIVE_ICON_PNG` (a nix-built derivation the
/// harness forwards into the daemon env; absent when no icon resolves).
/// Idempotent via a per-account `avatar-icon-hash` file in the account's
/// sdk `state_dir` — a re-upload only happens when the icon bytes change,
/// so daemon restarts don't re-spam the homeserver.
///
/// This replaces the old `matrix-avatar-sync` systemd curl oneshot: the
/// daemon already holds an authenticated `Client` pointed at the correct
/// homeserver, so it uploads over the live connection — no hardcoded URL,
/// no token re-read, no token-file globbing. Any failure is logged and
/// swallowed: avatar trouble must never break account bring-up or sync.
pub async fn sync_avatar(client: &Client, state_dir: &Path, account: &str) {
let Ok(png_path) = std::env::var("HIVE_ICON_PNG") else {
return; // no icon forwarded → nothing to sync
};
let bytes = match fs::read(&png_path).await {
Ok(b) => b,
Err(e) => {
tracing::warn!(account, path = %png_path, error = %e, "matrix avatar: icon PNG unreadable; skipping");
return;
}
};
// A non-crypto content hash is enough to answer "did the icon change?".
let hash = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut h);
format!("{:016x}", h.finish())
};
let hash_file = state_dir.join("avatar-icon-hash");
if let Ok(prev) = fs::read_to_string(&hash_file).await
&& prev.trim() == hash
{
tracing::debug!(account, "matrix avatar: icon unchanged; skipping upload");
return;
}
match client
.account()
.upload_avatar(&mime::IMAGE_PNG, bytes)
.await
{
Ok(mxc) => {
tracing::info!(account, mxc = %mxc, "matrix avatar set");
if let Err(e) = fs::write(&hash_file, &hash).await {
tracing::warn!(account, error = %e, "matrix avatar: set ok but hash write failed (re-uploads next start)");
}
}
Err(e) => {
tracing::warn!(account, error = %format!("{e:#}"), "matrix avatar: upload failed; skipping (non-fatal)");
}
}
}

View file

@ -196,6 +196,10 @@ async fn bring_up_account(
client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary)
.await
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
// Best-effort: sync the agent icon to this account's matrix avatar over
// the live (authenticated, correct-homeserver) Client. Replaces the old
// curl oneshot; failures are swallowed inside sync_avatar.
client::sync_avatar(&client, &cfg.state_dir, &cfg.name).await;
timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone());
let sync_client = client.clone();