feat(#2050): sync matrix avatar for dashboard-provisioned extra accounts
This commit is contained in:
parent
2f6d1788df
commit
ef079bb6b1
1 changed files with 109 additions and 69 deletions
|
|
@ -1642,13 +1642,18 @@ in
|
|||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||
};
|
||||
|
||||
# Path-trigger sibling: re-fires matrix-avatar-sync the moment
|
||||
# `<state>/matrix-token` appears. Same first-boot-ordering pattern
|
||||
# as hive-matrix-daemon above.
|
||||
# Path-trigger sibling: re-fires matrix-avatar-sync the moment a
|
||||
# token appears - both the hive-internal `matrix-token` and any
|
||||
# dashboard-provisioned extra `matrix-token-<name>` (so an external
|
||||
# account logged in after boot gets its avatar without a restart).
|
||||
# Same first-boot-ordering pattern as hive-matrix-daemon above.
|
||||
systemd.paths.matrix-avatar-sync = {
|
||||
description = "trigger matrix-avatar-sync when matrix-token appears";
|
||||
description = "trigger matrix-avatar-sync when a matrix token appears";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token";
|
||||
pathConfig.PathExistsGlob = [
|
||||
"/agents/*/state/matrix-token"
|
||||
"/agents/*/state/matrix-token-*"
|
||||
];
|
||||
};
|
||||
|
||||
# One-shot: hyperhive.icon → matrix profile avatar (two-step media
|
||||
|
|
@ -1681,82 +1686,117 @@ in
|
|||
echo "matrix-avatar-sync: no icon configured; skipping"
|
||||
exit 0
|
||||
fi
|
||||
# Token written by `hive-c0re::matrix::ensure_user_for` to the
|
||||
# agent's bind-mounted state dir. $HYPERHIVE_STATE_DIR is set
|
||||
# system-wide by the meta flake (systemd.globalEnvironment) to
|
||||
# `/agents/<name>/state`.
|
||||
TOKEN_FILE="$HYPERHIVE_STATE_DIR/matrix-token"
|
||||
if [ ! -f "$TOKEN_FILE" ]; then
|
||||
echo "matrix-avatar-sync: no matrix-token at $TOKEN_FILE; skipping"
|
||||
exit 0
|
||||
fi
|
||||
# Hash-based idempotency: skip the upload if the icon hasn't
|
||||
# changed since the last successful sync. Every upload mints a
|
||||
# new mxc:// URI which triggers a profile state event in every
|
||||
# joined room — uploading the same bytes again produces timeline
|
||||
# spam without changing the visible avatar. The hash file lives
|
||||
# in $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
||||
# purge + re-provision gets a fresh upload). Delete to force
|
||||
# re-upload.
|
||||
HASH_FILE="$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash"
|
||||
# spam without changing the visible avatar. Hash files live in
|
||||
# $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
||||
# purge + re-provision gets a fresh upload). One hash file per
|
||||
# account (the mxc:// URI is homeserver-scoped, so each account
|
||||
# uploads to its own homeserver independently). Delete to force
|
||||
# a re-upload.
|
||||
CURRENT_HASH=$(sha256sum "$ICON" | cut -d' ' -f1)
|
||||
if [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE" 2>/dev/null)" = "$CURRENT_HASH" ]; then
|
||||
echo "matrix-avatar-sync: icon unchanged (hash matches); skipping"
|
||||
exit 0
|
||||
fi
|
||||
TOKEN=$(cat "$TOKEN_FILE")
|
||||
# Local tuwunel reachable on shared host netns at the
|
||||
# default matrix-spec port. Override via
|
||||
# `hyperhive.matrix.url` if the operator runs the
|
||||
# homeserver elsewhere.
|
||||
MATRIX_URL=http://localhost:8008
|
||||
# whoami → user_id. Needed to scope the avatar set call.
|
||||
# Tolerant of the homeserver being unreachable (`-f` makes
|
||||
# curl fail on 4xx/5xx; `|| true` swallows the exit).
|
||||
USER_ID=$(curl -sf --max-time 5 \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"$MATRIX_URL/_matrix/client/v3/account/whoami" 2>/dev/null \
|
||||
| jq -r '.user_id // empty' || true)
|
||||
if [ -z "$USER_ID" ]; then
|
||||
echo "matrix-avatar-sync: whoami failed or homeserver unreachable; skipping"
|
||||
exit 0
|
||||
fi
|
||||
# Rasterize SVG → PNG (matrix media accepts any image type
|
||||
# but we already standardise on PNG for the forge sync).
|
||||
|
||||
# Rasterize SVG → PNG ONCE; the same bytes are reused for every
|
||||
# account (matrix media accepts any image type but we already
|
||||
# standardise on PNG for the forge sync).
|
||||
PNG=$(mktemp --suffix=.png)
|
||||
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
|
||||
echo "matrix-avatar-sync: rsvg-convert failed; skipping"
|
||||
rm -f "$PNG"
|
||||
exit 0
|
||||
fi
|
||||
# Step 1: upload bytes → mxc:// URI.
|
||||
MXC=$(curl -sf --max-time 10 \
|
||||
-X POST "$MATRIX_URL/_matrix/media/v3/upload" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: image/png" \
|
||||
--data-binary "@$PNG" 2>/dev/null \
|
||||
| jq -r '.content_uri // empty' || true)
|
||||
|
||||
# sync_one <token-file> <matrix-url> <hash-suffix> <label>
|
||||
# Two-step media upload + avatar_url set for a single account.
|
||||
# Best-effort: every failure path logs + returns 0 so one bad
|
||||
# account never aborts the others.
|
||||
sync_one() {
|
||||
token_file=$1
|
||||
matrix_url=$2
|
||||
hash_suffix=$3
|
||||
label=$4
|
||||
if [ ! -f "$token_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
hash_file="$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash$hash_suffix"
|
||||
if [ -f "$hash_file" ] && [ "$(cat "$hash_file" 2>/dev/null)" = "$CURRENT_HASH" ]; then
|
||||
echo "matrix-avatar-sync[$label]: icon unchanged (hash matches); skipping"
|
||||
return 0
|
||||
fi
|
||||
token=$(cat "$token_file")
|
||||
# whoami → user_id. Needed to scope the avatar set call.
|
||||
# Tolerant of the homeserver being unreachable (`-f` makes
|
||||
# curl fail on 4xx/5xx; `|| true` swallows the exit).
|
||||
user_id=$(curl -sf --max-time 5 \
|
||||
-H "Authorization: Bearer $token" \
|
||||
"$matrix_url/_matrix/client/v3/account/whoami" 2>/dev/null \
|
||||
| jq -r '.user_id // empty' || true)
|
||||
if [ -z "$user_id" ]; then
|
||||
echo "matrix-avatar-sync[$label]: whoami failed or homeserver unreachable; skipping"
|
||||
return 0
|
||||
fi
|
||||
# Step 1: upload bytes → mxc:// URI.
|
||||
mxc=$(curl -sf --max-time 10 \
|
||||
-X POST "$matrix_url/_matrix/media/v3/upload" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: image/png" \
|
||||
--data-binary "@$PNG" 2>/dev/null \
|
||||
| jq -r '.content_uri // empty' || true)
|
||||
if [ -z "$mxc" ]; then
|
||||
echo "matrix-avatar-sync[$label]: media upload failed; skipping"
|
||||
return 0
|
||||
fi
|
||||
# Step 2: set avatar_url on the profile.
|
||||
payload=$(jq -n --arg url "$mxc" '{avatar_url:$url}')
|
||||
code=$(curl -s --max-time 10 \
|
||||
-X PUT "$matrix_url/_matrix/client/v3/profile/$user_id/avatar_url" \
|
||||
-H "Authorization: Bearer $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "matrix-avatar-sync[$label]: avatar set on $user_id"
|
||||
# Persist hash so subsequent runs skip the upload when the
|
||||
# icon hasn't changed.
|
||||
echo "$CURRENT_HASH" > "$hash_file"
|
||||
else
|
||||
echo "matrix-avatar-sync[$label]: avatar PUT returned HTTP $code — skipping (non-fatal)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Hive-internal `main` account: bare `matrix-token`, written by
|
||||
# `hive-c0re::matrix::ensure_user_for` to the agent's
|
||||
# bind-mounted state dir. $HYPERHIVE_STATE_DIR is set system-wide
|
||||
# by the meta flake (systemd.globalEnvironment) to
|
||||
# `/agents/<name>/state`. Local tuwunel reachable on the shared
|
||||
# host netns at the default matrix-spec port.
|
||||
sync_one "$HYPERHIVE_STATE_DIR/matrix-token" "http://localhost:8008" "" "main"
|
||||
|
||||
# Dashboard-provisioned extra accounts: each is a
|
||||
# `matrix-token-<name>` file plus a `matrix-account-<name>.json`
|
||||
# sidecar carrying its (possibly external) homeserver. Mirrors
|
||||
# hive-matrix-mcp::accounts::discover_token_accounts — a token
|
||||
# without a sidecar is skipped because the homeserver is unknown.
|
||||
for token_file in "$HYPERHIVE_STATE_DIR"/matrix-token-*; do
|
||||
[ -f "$token_file" ] || continue
|
||||
name=''${token_file##*/matrix-token-}
|
||||
[ -n "$name" ] || continue
|
||||
sidecar="$HYPERHIVE_STATE_DIR/matrix-account-$name.json"
|
||||
if [ ! -f "$sidecar" ]; then
|
||||
echo "matrix-avatar-sync[$name]: no homeserver sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
homeserver=$(jq -r '.homeserver // empty' "$sidecar" 2>/dev/null || true)
|
||||
if [ -z "$homeserver" ]; then
|
||||
echo "matrix-avatar-sync[$name]: empty homeserver in sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
sync_one "$token_file" "$homeserver" "-$name" "$name"
|
||||
done
|
||||
|
||||
rm -f "$PNG"
|
||||
if [ -z "$MXC" ]; then
|
||||
echo "matrix-avatar-sync: media upload failed; skipping"
|
||||
exit 0
|
||||
fi
|
||||
# Step 2: set avatar_url on the profile.
|
||||
PAYLOAD=$(jq -n --arg url "$MXC" '{avatar_url:$url}')
|
||||
CODE=$(curl -s --max-time 10 \
|
||||
-X PUT "$MATRIX_URL/_matrix/client/v3/profile/$USER_ID/avatar_url" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
|
||||
if [ "$CODE" = "200" ]; then
|
||||
echo "matrix-avatar-sync: avatar set on $USER_ID"
|
||||
# Persist hash so subsequent runs skip the upload when the
|
||||
# icon hasn't changed.
|
||||
echo "$CURRENT_HASH" > "$HASH_FILE"
|
||||
else
|
||||
echo "matrix-avatar-sync: avatar PUT returned HTTP $CODE — skipping (non-fatal)"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue