voice-bridge: Discord voice <-> Lotus voice room (patched nether-voicebridge)
Lint / Shell (shellcheck) (push) Successful in 2m0s
Lint / JS (eslint) (push) Successful in 2m1s
Lint / No secrets in webhook configs (push) Successful in 18s
Lint / Landing page is rendered (matrix (push) Successful in 18s
Lint / Python (ruff) (push) Successful in 25s
Lint / Secret scan (gitleaks) (push) Successful in 14s
Lint / Shell (shellcheck) (pull_request) Successful in 9s
Lint / JS (eslint) (pull_request) Successful in 9s
Lint / No secrets in webhook configs (pull_request) Successful in 7s
Lint / Python deps (pip-audit) (push) Successful in 1m7s
Lint / Landing page is rendered (matrix (pull_request) Successful in 4s
Lint / Python (ruff) (pull_request) Successful in 6s
Lint / Secret scan (gitleaks) (pull_request) Successful in 6s
Lint / Python deps (pip-audit) (pull_request) Successful in 30s

Production setup for the Discord voice bridge on LXC 151: config and
appservice-registration templates (secrets rendered on the server), an
idempotent install script (one Synapse restart on first install, creates
the bot account so it can be invited), and a hardened systemd unit.

voice-limit-guard: VOICE_LIMIT_EXEMPT_USERS lets the bridge's own bot
listen in a call without taking a slot; relayed Discord speakers still
count. Unit tests added (61 pass); verified live on the dev stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
Lotus CI
2026-10-29 12:51:56 -04:00
co-authored by Claude Opus 5.5
parent db2f523028
commit 46d0e98424
8 changed files with 258 additions and 3 deletions
+34
View File
@@ -0,0 +1,34 @@
# Lotus Guild voice bridge (nether-voicebridge, Lotus-patched build) — LXC 151.
# Rendered by voice-bridge/install.sh into /etc/lotus-voice/config.toml (0600).
# @AS_TOKEN@ and @PUPPET_TOKENS@ are filled in on the server; never commit them.
data_dir = "/var/lib/lotus-voice"
[server.lotus]
homeserver_url = "http://127.0.0.1:8008"
# MXIDs and .well-known discovery (https://matrix.lotusguild.org) use this domain.
matrix_domain = "matrix.lotusguild.org"
# Bot = @lotus-voice_bot, Discord speakers = @lotus-voice_<discord id>.
sender_localpart = "lotus-voice"
appservice_id = "lotus-voice"
appservice_token = "@AS_TOKEN@"
[puppets]
# Lotus Voice 1–3 (Discord apps 1565219639640858746 / 1565220427297722378 /
# 1565220532830470207). One per Matrix person speaking at the same time.
tokens = [@PUPPET_TOKENS@]
nickname_prefix = "[Matrix] "
[[bridge]]
name = "discord-voice-bridge"
server = "lotus"
# Matrix: "Discord Voice Bridge" voice room.
room_id = "!yS_ZIHL3sOKQJBt4NiIQyiHKt3cv6HrIDeq7OxDpSM0"
[bridge.discord]
# Lotus Guild server, voice channel "matrix-bridge".
guild_id = "1565159371355197570"
channel_id = "1565159372122882071"
[bridge.matrix]
# Shown as a call participant in encrypted calls (it receives the media keys).
bot_display_name = "Discord Voice Bridge"
bot_avatar_path = "/opt/lotus-voice/avatar.png"
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# One-time install / upgrade of the Lotus voice bridge on LXC 151.
# Run on the Proxmox node hosting LXC 151 (and LXC 109 is not needed: the bridge
# keeps its state in sqlite under /var/lib/lotus-voice):
#
# bash install.sh <path to lotus-voice-bridge binary> <secrets file>
#
# The secrets file holds the three Lotus Voice bot tokens as BOT1=…, BOT2=…, BOT3=…
# (shell syntax); it is copied in, used to render the config, then deleted.
# Needs this repo's main pulled to /opt/matrix-config on LXC 151 (auto-deploy).
# First install restarts Synapse once (new appservice registration). Re-running
# with only a new binary just swaps it and restarts the bridge. Safe to re-run.
set -euo pipefail
BIN=${1:?usage: install.sh <binary> <secrets file>}
SECRETS=${2:?usage: install.sh <binary> <secrets file>}
CT=151
[ -x "$BIN" ] || { echo "binary not found/executable: $BIN" >&2; exit 1; }
echo "== [1/4] copy binary + secrets into LXC $CT"
pct push "$CT" "$BIN" /root/lotus-voice-bridge.new --perms 0755
pct push "$CT" "$SECRETS" /root/lotus-voice.secrets --perms 0600
echo "== [2/4] install files, user, config, registration"
pct exec "$CT" -- bash -s <<'IN'
set -euo pipefail
SRC=/opt/matrix-config
id lotus-voice >/dev/null 2>&1 || useradd --system --home /var/lib/lotus-voice --shell /usr/sbin/nologin lotus-voice
install -d -o lotus-voice -g lotus-voice -m 0750 /var/lib/lotus-voice /etc/lotus-voice
install -d -m 0755 /opt/lotus-voice
mv /root/lotus-voice-bridge.new /opt/lotus-voice/lotus-voice-bridge
install -m 0644 "$SRC/discord-bridge/bot-avatar.png" /opt/lotus-voice/avatar.png
REG=/etc/matrix-synapse/lotus-voice-registration.yaml
if [ ! -f "$REG" ]; then
AS=$(openssl rand -hex 32); HS=$(openssl rand -hex 32)
sed -e "s/@AS_TOKEN@/$AS/" -e "s/@HS_TOKEN@/$HS/" "$SRC/voice-bridge/registration.yaml.template" > "$REG"
chown root:matrix-synapse "$REG"; chmod 0640 "$REG"
echo "registration written"
fi
AS=$(awk '/^as_token:/ {gsub(/"/, "", $2); print $2; exit}' "$REG")
set -a; . /root/lotus-voice.secrets; set +a
rm -f /root/lotus-voice.secrets
for v in BOT1 BOT2 BOT3; do [ -n "${!v:-}" ] || { echo "secrets file is missing $v" >&2; exit 1; }; done
TOKENS="\"$BOT1\", \"$BOT2\", \"$BOT3\""
umask 077
sed -e "s/@AS_TOKEN@/$AS/" -e "s|@PUPPET_TOKENS@|$TOKENS|" "$SRC/voice-bridge/config.toml.template" > /etc/lotus-voice/config.toml
unset BOT1 BOT2 BOT3 TOKENS
chown lotus-voice:lotus-voice /etc/lotus-voice/config.toml
install -m 0644 "$SRC/systemd/lotus-voice-bridge.service" /etc/systemd/system/lotus-voice-bridge.service
systemctl daemon-reload
echo "files ok"
IN
echo "== [3/4] register with Synapse (restart only if newly added)"
pct exec "$CT" -- bash -s <<'RS'
set -euo pipefail
HSY=/etc/matrix-synapse/homeserver.yaml
if grep -q lotus-voice-registration.yaml "$HSY"; then
echo "already registered — no Synapse restart"
else
cp "$HSY" "$HSY.bak-$(date +%Y%m%d%H%M%S)-voicebridge"
sed -i '/^ - \/etc\/matrix-synapse\/discord-registration.yaml/a \ - /etc/matrix-synapse/lotus-voice-registration.yaml' "$HSY"
grep -A4 '^app_service_config_files' "$HSY"
systemctl restart matrix-synapse
for i in $(seq 1 60); do curl -fsS -o /dev/null http://127.0.0.1:8008/health && break; sleep 2; done
curl -fsS http://127.0.0.1:8008/health && echo " (synapse healthy)"
fi
# Create the bot account now so it can be invited before the bridge first starts.
AS=$(awk '/^as_token:/ {gsub(/"/, "", $2); print $2; exit}' /etc/matrix-synapse/lotus-voice-registration.yaml)
curl -sS -X POST -H "Authorization: Bearer $AS" -H 'Content-Type: application/json' \
-d '{"type":"m.login.application_service","username":"lotus-voice_bot","inhibit_login":true}' \
http://127.0.0.1:8008/_matrix/client/v3/register | grep -qE 'user_id|M_USER_IN_USE' && echo "bot account @lotus-voice_bot ready"
RS
echo "== [4/4] (re)start the bridge"
pct exec "$CT" -- bash -s <<'ST'
set -euo pipefail
systemctl enable lotus-voice-bridge >/dev/null 2>&1
systemctl restart lotus-voice-bridge
sleep 8
systemctl --no-pager --lines=0 status lotus-voice-bridge | head -5
journalctl -u lotus-voice-bridge --since "-15 sec" --no-pager -o cat | grep -E "puppet connected|initial call snapshot|LiveKit focus|bridge failed|ERROR" | sed -E 's/\x1b\[[0-9;]*m//g' | cut -c1-160 | head -10
ST
echo
echo "Done. In Lotus, invite @lotus-voice_bot:matrix.lotusguild.org to the Discord Voice Bridge room."
+15
View File
@@ -0,0 +1,15 @@
# Synapse appservice registration for the voice bridge. Rendered by install.sh to
# /etc/matrix-synapse/lotus-voice-registration.yaml with fresh random tokens.
# The bridge talks to Synapse only as a client (as_token + ?user_id=), so no URL.
id: lotus-voice
url: null
as_token: "@AS_TOKEN@"
hs_token: "@HS_TOKEN@"
sender_localpart: lotus-voice_as
namespaces:
users:
- exclusive: true
regex: "@lotus-voice_.*:matrix\\.lotusguild\\.org"
aliases: []
rooms: []
rate_limited: false