Merge pull request 'feat(cinny-web): serve avatar decorations from chat.lotusguild.org/decorations (#22)' (#35) from feat/decorations-package into main
Lint / Shell (shellcheck) (push) Successful in 11s
Lint / JS (eslint) (push) Successful in 8s
Lint / No secrets in webhook configs (push) Successful in 8s
Lint / Landing page is rendered (matrix (push) Successful in 5s
Lint / Python (ruff) (push) Successful in 4s
Lint / Python deps (pip-audit) (push) Successful in 35s
Lint / Secret scan (gitleaks) (push) Successful in 5s

This commit was merged in pull request #35.
This commit is contained in:
2026-10-19 12:35:22 -04:00
5 changed files with 131 additions and 1 deletions
+10
View File
@@ -472,6 +472,16 @@ chmod 600 /etc/cinny-monitor.env
**Cinny-build webhook token** (header `X-Build-Token` on the `cinny-build` hook): `CINNY_BUILD_TOKEN` in LXC 106's `/etc/webhook/secrets.env`, not in git. Reachable on `10.10.10.6:9001` (`webhook-lotus`); `:9000` is bound to `127.0.0.1`. Note: LotusBot doesn't implement `!cinny-update` at the moment, so nothing calls this hook.
### Avatar decorations (matrix #22)
Served from **`https://chat.lotusguild.org/decorations/`**: `<slug>.png` and `thumbs/<slug>.webp`, `Cache-Control: public, max-age=604800`, no cookies. They used to come through Nextcloud's public DAV, which started a PHP session per image and sent no cache headers.
- **Storage:** Gitea generic package `avatar-decorations` (`https://code.lotusguild.org/api/packages/LotusGuild/generic/avatar-decorations/<version>/`), in ≤150 MB tar parts (the proxy refuses larger uploads). Nothing goes in git except the pin.
- **Pin:** `cinny/decorations.sha256`, the sha256 of each part, which names the version.
- **Deploy:** `deploy/lxc106-cinny.sh` installs the pinned version when the pin changes (or nothing is installed yet). It downloads the parts, checks them against the pin and against the `MANIFEST.sha256` inside, unpacks to `/var/www/decorations-<version>`, and atomically repoints `/var/www/decorations`. The previous version is kept for rollback; a failed download or checksum leaves the live set unchanged. nginx: `location ^~ /decorations/` in `cinny/nginx.conf`.
- **Updating the set:** put the files in a folder (`<slug>.png` + `thumbs/<slug>.webp`) and run `GITEA_TOKEN=… deploy/publish-decorations.sh <folder> <new-version>`. That rewrites the pin; commit it. Add or remove the catalog entries in cinny's `src/app/features/lotus/avatarDecorations.ts`. Use a new version for every change: an image replaced under the same name can be cached for up to a week.
- **Rollback:** revert `cinny/decorations.sha256`, or on LXC 106 run `ln -sfn /var/www/decorations-<old>/decorations /var/www/decorations`.
### Webhook secrets — rotating
Secrets used to be committed in `deploy/hooks-lxc*.json` in this public repo; all exposed ones were rotated on 2026-09-23 and moved to `secrets.env`. To rotate one:
+4
View File
@@ -0,0 +1,4 @@
a7aacaa65cb06e4b1c990b9253362f2eb148aae72e2e11c396d34e899792d68d avatar-decorations-1.0.0.part1.tar
a6919d8bb8e1c2f838b0f9acdbc1131cbfd7bfe5fe9c373b25f406474bd41095 avatar-decorations-1.0.0.part2.tar
fb7624f2470659d933463187d2afe73428d5554291f62484c8a28fac6196f427 avatar-decorations-1.0.0.part3.tar
909e07db0134b29258514acf4d598bb467aff8b157f0767b6c3afeadc1eb048c avatar-decorations-1.0.0.part4.tar
+11
View File
@@ -107,6 +107,17 @@ server {
proxy_connect_timeout 5;
}
# [matrix #22] Avatar decorations, installed by deploy/lxc106-cinny.sh from
# the Gitea generic package (outside the web root, so app deploys'
# rsync --delete never touches them). ^~ beats the image regex below: these
# can be replaced in place under the same name, so a week, not immutable.
location ^~ /decorations/ {
alias /var/www/decorations/;
include snippets/cinny-security-headers.conf;
add_header Cache-Control "public, max-age=604800" always;
location ~ /MANIFEST\.sha256$ { return 404; }
}
location / {
# add_header here drops the server-level headers, so include them again.
include snippets/cinny-security-headers.conf;
+50 -1
View File
@@ -16,7 +16,7 @@ echo "=== $(date) === LXC106 deploy triggered ==="
if [ ! -d "$REPO_DIR/.git" ]; then
git clone "$CLONE_URL" "$REPO_DIR"
CHANGED="cinny/config.json cinny/nginx.conf cinny/upstream-check.sh cinny/lotus-build.sh cinny/lotus_deploy.sh deploy/hooks-lxc106.json systemd/cinny-upstream-check.cron"
CHANGED="cinny/config.json cinny/decorations.sha256 cinny/nginx.conf cinny/upstream-check.sh cinny/lotus-build.sh cinny/lotus_deploy.sh deploy/hooks-lxc106.json systemd/cinny-upstream-check.cron"
else
cd "$REPO_DIR"
git fetch --all
@@ -63,6 +63,55 @@ PY
fi
fi
# [matrix #22] Avatar decorations: a versioned Gitea generic package, pinned
# (version + sha256 of each part) in cinny/decorations.sha256, installed to
# /var/www/decorations-<version> and served by nginx at /decorations/. Runs on a
# pin change or when nothing is installed yet. A failed download or checksum
# leaves the live set untouched; the previous version is kept for rollback.
install_decorations() {
local pin="$REPO_DIR/cinny/decorations.sha256"
local ver base tmp dest
ver=$(grep -oE 'avatar-decorations-[0-9][^ ]*\.part1\.tar' "$pin" | sed -E 's/^avatar-decorations-(.*)\.part1\.tar$/\1/')
[ -n "$ver" ] || { echo "✗ decorations: no version in $pin"; return 1; }
dest="/var/www/decorations-$ver"
if [ -d "$dest/decorations" ] && [ "$(readlink -f /var/www/decorations)" = "$dest/decorations" ]; then
echo "decorations $ver already live"; return 0
fi
base="https://code.lotusguild.org/api/packages/LotusGuild/generic/avatar-decorations/$ver"
tmp=$(mktemp -d /var/www/.decorations-XXXXXX)
# Every step checks its own exit status: `set -e` does not apply inside a
# subshell on the left of `||`, so a failed checksum would be ignored.
if ! (
cd "$tmp" || exit 1
cp "$pin" SHA256SUMS || exit 1
while read -r _ f; do
curl -fsSL --retry 3 -o "$f" "$base/$f" || exit 1
done < SHA256SUMS
sha256sum -c --quiet SHA256SUMS || exit 1
while read -r _ f; do
tar -xf "$f" || exit 1
rm -f "$f"
done < SHA256SUMS
cd decorations || exit 1
sha256sum -c --quiet MANIFEST.sha256 || exit 1
); then
echo "✗ decorations $ver: download/verify failed — live set unchanged"
rm -rf "$tmp"
return 1
fi
rm -rf "$dest"; mkdir -p "$dest"; mv "$tmp/decorations" "$dest/decorations"; rm -rf "$tmp"
chmod -R a+rX "$dest"
ln -sfn "$dest/decorations" /var/www/decorations.new && mv -T /var/www/decorations.new /var/www/decorations
# Keep the live version and the one before it; drop older ones.
find /var/www -maxdepth 1 -name 'decorations-*' -type d -printf '%T@ %p\n' | sort -rn \
| tail -n +3 | cut -d' ' -f2- | xargs -r rm -rf
echo "✓ decorations $ver live at /var/www/decorations ($(find "$dest/decorations" -maxdepth 1 -name '*.png' | wc -l) images)"
}
if echo "$CHANGED" | grep -q '^cinny/decorations.sha256' || [ ! -e /var/www/decorations ]; then
echo "Deploying avatar decorations..."
install_decorations || true
fi
if echo "$CHANGED" | grep -q '^cinny/nginx.conf'; then
echo "Deploying cinny nginx site config..."
# Back up the live config, swap in the repo copy, and validate before
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# Publish a new avatar-decorations version to the Gitea generic package
# registry and write the pin that LXC 106's deploy installs (matrix #22).
#
# GITEA_TOKEN=… deploy/publish-decorations.sh <dir> <version>
#
# <dir> holds <slug>.png files and thumbs/<slug>.webp (the same layout the app
# requests). The set is split into ≤150 MB tar parts (the registry's proxy
# refuses large uploads), each part is uploaded, and cinny/decorations.sha256
# is rewritten. Commit that file to deploy; nothing changes until you do.
set -euo pipefail
SRC=${1:?usage: publish-decorations.sh <dir> <version>}
VER=${2:?usage: publish-decorations.sh <dir> <version>}
: "${GITEA_TOKEN:?set GITEA_TOKEN (package write scope)}"
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
BASE="https://code.lotusguild.org/api/packages/LotusGuild/generic/avatar-decorations/$VER"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/decorations/thumbs"
cp "$SRC"/*.png "$WORK/decorations/"
cp "$SRC"/thumbs/*.webp "$WORK/decorations/thumbs/"
cd "$WORK"
for f in decorations/*.png; do
s=$(basename "$f" .png)
[ -f "decorations/thumbs/$s.webp" ] || { echo "missing thumbs/$s.webp" >&2; exit 1; }
done
(cd decorations && find . -type f \( -name '*.png' -o -name '*.webp' \) | sort | xargs sha256sum > MANIFEST.sha256)
python3 - "$VER" <<'PY'
import os, subprocess, sys
ver, root, limit = sys.argv[1], 'decorations', 150 * 1024 * 1024
files = sorted(os.path.join(root, f) for f in os.listdir(root) if f.endswith('.png'))
first = [os.path.join(root, 'MANIFEST.sha256')] + sorted(os.path.join(root, 'thumbs', t) for t in os.listdir(os.path.join(root, 'thumbs')))
parts, cur, size = [], list(first), sum(os.path.getsize(f) for f in first)
for f in files:
if size + os.path.getsize(f) > limit and cur:
parts.append(cur); cur, size = [], 0
cur.append(f); size += os.path.getsize(f)
parts.append(cur)
for i, p in enumerate(parts, 1):
lst = f'part{i}.list'
open(lst, 'w').write('\n'.join(p) + '\n')
subprocess.run(['tar', '--sort=name', '--owner=0', '--group=0', '--mtime=@0', '-cf',
f'avatar-decorations-{ver}.part{i}.tar', '-T', lst], check=True)
PY
sha256sum avatar-decorations-"$VER".part*.tar > SHA256SUMS
for f in avatar-decorations-"$VER".part*.tar SHA256SUMS; do
code=$(curl -s -o /dev/null -w '%{http_code}' -X PUT -H "Authorization: token $GITEA_TOKEN" \
--upload-file "$f" "$BASE/$f")
[ "$code" = 201 ] || { echo "upload of $f failed: HTTP $code" >&2; exit 1; }
echo "uploaded $f"
done
cp SHA256SUMS "$REPO_ROOT/cinny/decorations.sha256"
echo "Pinned $VER in cinny/decorations.sha256: commit it to deploy."