Refresh musiclink integration docs and tooling

Use local musiclink flake input with Go 1.24.

Add matterbridge patch, routing docs, and deploy check script.
This commit is contained in:
Dan 2026-01-21 22:52:39 -08:00
parent 8918b62765
commit ae16db4898
8 changed files with 298 additions and 11 deletions

55
HANDOFF.md Normal file
View file

@ -0,0 +1,55 @@
# Handoff Summary
**Goal:** Resolve MusicLink cross-room reposting and plan Matrix-native routing
**Timestamp:** 2026-01-21T17:41:35-08:00
## Current State
**MusicLink Bot Status:**
- Running as `@musiclink:clarun.xyz` (display name: "musiclink").
- Matterbridge patch applied for WebSocket payload size + link preview suppression.
- Matterbridge still running with `-debug` flag.
**Observed Issue:**
- Messages are reposted across rooms (including DM portal) because Matterbridge gateways are fan-out buses when multiple rooms share a gateway.
- Slack errors "Your message was not bridged: You're not logged in" were caused by missing relay user in portal room. Relay must be set per room with `!slack set-relay`.
**Rooms in Current Gateway:**
- `!whU7Geg7JPrBL5wHcW:clarun.xyz`
- `!dT40EUcemb8e6bPiig:clarun.xyz` (Dan/Vlad DM portal)
- `!DPQveBnfuDrbgOe6dm:clarun.xyz` (#music portal)
**Constraint:**
- Only one Slack app login/token is available (single Slack identity).
## Decisions & Direction
- Short-term isolation via one gateway per room is possible, but we decided to pursue a **Matrix-native MusicLink** implementation long-term to avoid Matterbridge fan-out entirely.
- Stakeholder summary and RFC created in ops-jrz1; design doc created in musiclink repo.
## Docs Added
- `docs/rfc-musiclink-room-routing.md` (ops-jrz1)
- `docs/musiclink-room-routing-stakeholder-summary.md` (ops-jrz1)
- `docs/design-matrix-native-routing.md` (musiclink repo)
## Next Steps
- [ ] Implement Matrix-native MusicLink in `/home/dan/proj/musiclink`:
- Choose SDK (mautrix-go vs matrix-nio).
- Add room allowlist config + sync state.
- Reply in same room/thread; ignore self messages.
- [ ] Plan migration: run Matrix-native mode in parallel, then disable Matterbridge.
- [ ] (Optional short-term) Remove DM portal from Matterbridge gateway to stop cross-posting.
- [ ] Remove `-debug` flag from `modules/musiclink.nix` once stable.
## Notes / References
- Relay login in mautrix-slack DB: `TSW76H2Q0-U01MYESTVSL` (Dan/Vlad). Portal `!DPQveBnfuDrbgOe6dm:clarun.xyz` had no relay set.
- `mautrix-slack` log showed errors: `Failed to get user login ... not logged in` for sender `@musiclink`.
## Repository Context
**Repo Root:** /home/dan/proj/ops-jrz1
**Branch:** main

View file

@ -0,0 +1,50 @@
# MusicLink Room Routing: Stakeholder Summary
## Executive Summary
We identified unintended cross-posting of Slack/Matrix messages caused by Matterbridges gateway fan-out behavior. Messages posted in one monitored Slack room can appear in other Matrix portal rooms (including DMs), creating a privacy and trust risk. This is expected Matterbridge behavior when multiple rooms share a single gateway. We must route everything through **one Slack login/token**, which limits per-room identity options.
**Immediate recommendation:** isolate rooms by using **one gateway per room** to prevent cross-posting. This can be done in a single Matterbridge process with multiple gateway blocks.
## Current Situation (Whats Happening)
- Slack messages in a monitored room are bridged into the Matrix portal room by mautrix-slack.
- Matterbridge treats all `inout` rooms in one gateway as a **fan-out bus** and broadcasts messages to every other room in that gateway.
- Result: messages from public rooms can appear in private DMs (and vice versa).
## Impact
- **Privacy risk:** DM or private room content can leak into other rooms.
- **Trust risk:** users see unexpected reposts attributed to MusicLink.
- **Operational risk:** unclear routing makes debugging and auditing harder.
## Constraints
- Only **one Slack login/token** is available for all rooms. No per-room Slack identities.
## Options Overview
### Option A — Keep Current Fan-Out
- **Pros:** Lowest effort.
- **Cons:** Cross-posting continues; privacy risk remains. Not acceptable for production.
### Option B — One Gateway per Room (Recommended)
- **What it means:** Split rooms into separate gateways so messages only stay within their originating room.
- **Pros:** Stops cross-posting; isolates failures; preserves current MusicLink deployment model.
- **Cons:** More configuration; needs templating/automation; may require multiple MusicLink instances or distinct API targets (ports/tokens) per gateway.
- **Note:** Can still run in a single Matterbridge process; does not require multiple systemd units unless desired.
### Option C — Native Matrix Support (Longer-Term)
- **What it means:** MusicLink becomes a native Matrix bot, posting replies only in the originating room.
- **Pros:** Simplifies architecture; eliminates Matterbridge fan-out issues; clearer routing.
- **Cons:** Requires code changes and Matrix client management; higher effort.
### Option D — Matterbridge Routing Extensions (Longer-Term)
- **What it means:** Patch or extend Matterbridge to support per-room routing rules.
- **Pros:** Could preserve single gateway.
- **Cons:** Uncertain feasibility; upstream changes required.
## Recommendation
- **Immediate:** Implement **Option B** to eliminate privacy leakage and restore predictable routing.
- **Long-term:** Evaluate **Option C** for a simpler, more reliable architecture.
## Open Questions
- How many Slack rooms require MusicLink coverage in the near term?
- Should we run multiple MusicLink instances or add per-gateway API targets?
- What level of monitoring and alerting is required per room/gateway?

View file

@ -0,0 +1,64 @@
# RFC: MusicLink Room Routing and Fan-Out Behavior
## Summary
MusicLink is deployed via Matterbridge as a Matrix-mediated bridge for Slack rooms. We observed that messages from one Slack room are reposted by MusicLink into other Matrix portal rooms (including DMs). This is caused by Matterbridges gateway fan-out behavior when multiple Matrix rooms are configured as `inout` in the same gateway.
This RFC documents the issue, impact, and options for isolation while keeping multi-room coverage.
## Current Architecture
```
Slack Room (e.g. #music)
-> mautrix-slack (Matrix portal room)
-> Matterbridge (single gateway)
-> MusicLink (API)
-> Matterbridge (same gateway)
-> All other Matrix rooms in the gateway
```
### Current Matterbridge Gateway
In `modules/musiclink.nix`, multiple Matrix rooms are configured as `[[gateway.inout]]` under the same `musiclink-gateway`:
- `!whU7Geg7JPrBL5wHcW:clarun.xyz`
- `!dT40EUcemb8e6bPiig:clarun.xyz` (Dan/Vlad DM portal)
- `!DPQveBnfuDrbgOe6dm:clarun.xyz` (#music portal)
Matterbridge treats a gateway as a **fan-out bus**: any message arriving from one room is broadcast to every other room in that gateway (except the source). This is expected behavior and not a MusicLink feature.
## Observed Issue
A reply in the Slack `#music` thread was bridged into the Matrix portal for `#music`. Matterbridge then broadcast that message into the Dan/Vlad DM portal room, appearing as though MusicLink “reposted” a random message.
## Root Cause
Matterbridge lacks per-room routing rules within a gateway. It cannot forward only to the originating Slack room. When multiple Matrix rooms share the same gateway, cross-posting is unavoidable.
## Constraints
- We must route everything through a **single Slack login/token** (one Slack user identity). Multiple Slack app tokens or per-room Slack identities are not available.
## Options
### Option A: Keep Current Fan-Out Behavior
- **Pros:** Single service, simplest configuration.
- **Cons:** Cross-posting between rooms/DMs continues.
### Option B: One Gateway Per Room (Recommended)
- Create separate Matterbridge gateways (or instances) per Matrix portal room.
- Each gateway connects to its own MusicLink instance (or a shared MusicLink if it can target a gateway/room).
- **Pros:** No cross-posting; clean isolation.
- **Cons:** More configuration and systemd units.
### Option C: Replace Matterbridge With Direct Multi-Room Support
- Modify MusicLink to join Matrix rooms directly and post responses only in the originating room.
- **Pros:** Correct routing without fan-out.
- **Cons:** Requires changes to MusicLink code and Matrix auth handling.
### Option D: Investigate Matterbridge Routing Extensions
- Explore upstream support for per-room routing rules or custom patches.
- **Pros:** Potentially retains single gateway.
- **Cons:** No known support today; likely requires upstream changes.
## Recommendation
Adopt **Option B** (one gateway per room) to preserve multi-room coverage without cross-posting. This keeps MusicLink behavior predictable and avoids DM leakage. If MusicLink can be updated to support multiple rooms directly, Option C could supersede this.
## Open Questions
- How many Slack rooms require MusicLink coverage in the near term?
- Are we willing to run multiple MusicLink instances, or should we refactor MusicLink to support per-room targeting?

View file

@ -47,17 +47,17 @@
"utils": "utils"
},
"locked": {
"lastModified": 1768952774,
"narHash": "sha256-g+DH3mTdJyBlQTBplTaCyEfVM44+XV9/YPoIFOfMB9E=",
"lastModified": 1769064317,
"narHash": "sha256-K5YH9EebSfhIBLmoIdJOJSAdOwH3dwAWUSOhtN4Rj/s=",
"ref": "refs/heads/main",
"rev": "e5957cf182934c5b076c862afea9257b6c35d532",
"revCount": 4,
"rev": "aa54485f5d3058a58918e337c38f267d6d7e99e2",
"revCount": 11,
"type": "git",
"url": "ssh://forgejo@git.clarun.xyz/dan/musiclink.git"
"url": "file:///home/dan/proj/musiclink"
},
"original": {
"type": "git",
"url": "ssh://forgejo@git.clarun.xyz/dan/musiclink.git"
"url": "file:///home/dan/proj/musiclink"
}
},
"nixpkgs": {

View file

@ -21,7 +21,7 @@
};
musiclink = {
url = "git+ssh://forgejo@git.clarun.xyz/dan/musiclink.git";
url = "git+file:///home/dan/proj/musiclink";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@ -39,7 +39,10 @@
};
opencode = inputs.opencode.packages.${system}.default;
beads = inputs.beads.packages.${system}.default;
musiclink = inputs.musiclink.packages.${system}.default;
musiclink = inputs.musiclink.packages.${system}.default.overrideAttrs (old: {
nativeBuildInputs = [ pkgs-unstable.go_1_24 ]
++ (old.nativeBuildInputs or []);
});
in {
# Pre-deploy checks: nix flake check
checks.${system} = {
@ -81,7 +84,7 @@
};
opencode = inputs.opencode.packages.x86_64-linux.default;
beads = inputs.beads.packages.x86_64-linux.default;
musiclink = inputs.musiclink.packages.x86_64-linux.default;
musiclink = musiclink;
};
modules = [
./configuration.nix
@ -105,7 +108,7 @@
};
opencode = inputs.opencode.packages.x86_64-linux.default;
beads = inputs.beads.packages.x86_64-linux.default;
musiclink = inputs.musiclink.packages.x86_64-linux.default;
musiclink = musiclink;
};
modules = [
./configuration.nix

View file

@ -7,6 +7,11 @@ let
musiclinkPkg = musiclink; # musiclink input passed via specialArgs is the package set? No, usually it's the flake itself if not mapped.
# But in flake.nix we did: musiclink = inputs.musiclink.packages.x86_64-linux.default;
# So `musiclink` here is the package.
matterbridgePkg = pkgs.matterbridge.overrideAttrs (old: {
patches = (old.patches or []) ++ [
./patches/matterbridge-api-websocket-max-message.patch
];
});
in {
options.services.musiclink = {
@ -85,10 +90,20 @@ account = "api.local"
[[gateway.inout]]
account = "matrix.clarun"
channel = "!whU7Geg7JPrBL5wHcW:clarun.xyz"
# Additional test room
[[gateway.inout]]
account = "matrix.clarun"
channel = "!dT40EUcemb8e6bPiig:clarun.xyz"
# #music room (bridged from Slack)
[[gateway.inout]]
account = "matrix.clarun"
channel = "!DPQveBnfuDrbgOe6dm:clarun.xyz"
EOF
'';
ExecStart = "${pkgs.matterbridge}/bin/matterbridge -conf /var/lib/musiclink-matterbridge/matterbridge.toml";
ExecStart = "${matterbridgePkg}/bin/matterbridge -conf /var/lib/musiclink-matterbridge/matterbridge.toml -debug";
Restart = "always";
RestartSec = "10s";

View file

@ -0,0 +1,49 @@
diff --git a/bridge/api/api.go b/bridge/api/api.go
--- a/bridge/api/api.go
+++ b/bridge/api/api.go
@@ -40,6 +40,7 @@
e.HidePort = true
b.mrouter = melody.New()
+ b.mrouter.Config.MaxMessageSize = 64 * 1024
b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) {
message := config.Message{}
err := json.Unmarshal(msg, &message)
diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go
--- a/bridge/matrix/matrix.go
+++ b/bridge/matrix/matrix.go
@@ -363,7 +363,14 @@
)
err = b.retry(func() error {
- resp, err = b.mc.SendText(channel, body)
+ content := map[string]interface{}{
+ "msgtype": "m.text",
+ "body": body,
+ }
+ if msg.Protocol == "api" {
+ content["com.beeper.linkpreviews"] = []interface{}{}
+ }
+ resp, err = b.mc.SendMessageEvent(channel, "m.room.message", content)
return err
})
@@ -381,7 +388,16 @@
)
err = b.retry(func() error {
- resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
+ content := map[string]interface{}{
+ "msgtype": "m.text",
+ "body": body,
+ "format": "org.matrix.custom.html",
+ "formatted_body": formattedBody,
+ }
+ if msg.Protocol == "api" {
+ content["com.beeper.linkpreviews"] = []interface{}{}
+ }
+ resp, err = b.mc.SendMessageEvent(channel, "m.room.message", content)
return err
})

51
scripts/check-deploy Executable file
View file

@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail
HOST="${1:-ops-jrz1}"
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
echo "== Local working tree =="
git status -sb
echo
echo "== Commits ahead of origin/main =="
if git rev-parse --verify origin/main >/dev/null 2>&1; then
git log --oneline origin/main..HEAD || true
else
echo "origin/main not found (run: git fetch origin)"
fi
echo
echo "== Local HEAD =="
local_head="$(git rev-parse HEAD)"
echo "$local_head"
echo
echo "== Remote configuration revision ($HOST) =="
remote_rev="$(ssh "root@${HOST}" 'nixos-version --configuration-revision' || true)"
if [[ -z "$remote_rev" || "$remote_rev" == "" ]]; then
echo "(empty)"
else
echo "$remote_rev"
fi
echo
echo "== Comparison =="
if [[ -n "$remote_rev" && "$remote_rev" == "$local_head" ]]; then
echo "OK: remote matches local HEAD"
else
echo "NOTE: remote does not match local HEAD"
echo "Remote: ${remote_rev:-<empty>}"
echo "Local: $local_head"
fi
echo
echo "== Remote current system =="
ssh "root@${HOST}" 'readlink /run/current-system'