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*";
|
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Path-trigger sibling: re-fires matrix-avatar-sync the moment
|
# Path-trigger sibling: re-fires matrix-avatar-sync the moment a
|
||||||
# `<state>/matrix-token` appears. Same first-boot-ordering pattern
|
# token appears - both the hive-internal `matrix-token` and any
|
||||||
# as hive-matrix-daemon above.
|
# 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 = {
|
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" ];
|
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
|
# One-shot: hyperhive.icon → matrix profile avatar (two-step media
|
||||||
|
|
@ -1681,82 +1686,117 @@ in
|
||||||
echo "matrix-avatar-sync: no icon configured; skipping"
|
echo "matrix-avatar-sync: no icon configured; skipping"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
# Hash-based idempotency: skip the upload if the icon hasn't
|
||||||
# changed since the last successful sync. Every upload mints a
|
# changed since the last successful sync. Every upload mints a
|
||||||
# new mxc:// URI which triggers a profile state event in every
|
# new mxc:// URI which triggers a profile state event in every
|
||||||
# joined room — uploading the same bytes again produces timeline
|
# joined room — uploading the same bytes again produces timeline
|
||||||
# spam without changing the visible avatar. The hash file lives
|
# spam without changing the visible avatar. Hash files live in
|
||||||
# in $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
# $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
||||||
# purge + re-provision gets a fresh upload). Delete to force
|
# purge + re-provision gets a fresh upload). One hash file per
|
||||||
# re-upload.
|
# account (the mxc:// URI is homeserver-scoped, so each account
|
||||||
HASH_FILE="$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash"
|
# uploads to its own homeserver independently). Delete to force
|
||||||
|
# a re-upload.
|
||||||
CURRENT_HASH=$(sha256sum "$ICON" | cut -d' ' -f1)
|
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"
|
# Rasterize SVG → PNG ONCE; the same bytes are reused for every
|
||||||
exit 0
|
# account (matrix media accepts any image type but we already
|
||||||
fi
|
# standardise on PNG for the forge sync).
|
||||||
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).
|
|
||||||
PNG=$(mktemp --suffix=.png)
|
PNG=$(mktemp --suffix=.png)
|
||||||
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
|
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
|
||||||
echo "matrix-avatar-sync: rsvg-convert failed; skipping"
|
echo "matrix-avatar-sync: rsvg-convert failed; skipping"
|
||||||
rm -f "$PNG"
|
rm -f "$PNG"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
# Step 1: upload bytes → mxc:// URI.
|
|
||||||
MXC=$(curl -sf --max-time 10 \
|
# sync_one <token-file> <matrix-url> <hash-suffix> <label>
|
||||||
-X POST "$MATRIX_URL/_matrix/media/v3/upload" \
|
# Two-step media upload + avatar_url set for a single account.
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
# Best-effort: every failure path logs + returns 0 so one bad
|
||||||
-H "Content-Type: image/png" \
|
# account never aborts the others.
|
||||||
--data-binary "@$PNG" 2>/dev/null \
|
sync_one() {
|
||||||
| jq -r '.content_uri // empty' || true)
|
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"
|
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