musiclink/docs/platform-setup.md
Meta-Repo Bot b6cf5fdfa7 chore: Harden systemd service and pin nixpkgs to stable
Updates deployment configuration:
- Adds strict systemd sandboxing (ProtectSystem, DynamicUser, etc)
- Pins flake input to nixos-24.11 for stability
- Updates docs to reflect hardening
2026-01-16 22:51:51 +00:00

338 lines
11 KiB
Markdown

# MusicLink Bot - Platform Setup Guide
## Overview
MusicLink is a bot that detects music links (Spotify, YouTube, Apple Music, etc.) in chat messages and responds with equivalent links on other streaming services.
It uses the [idonthavespotify](https://github.com/sjdonado/idonthavespotify) API for link conversion, so **no Spotify/YouTube API credentials are required**.
## Architecture
```
┌─────────────────────────── jrz1 (NixOS VPS) ───────────────────────────┐
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │
│ │ matterbridge.service │ │ musiclink.service │ │
│ │ │ │ │ │
│ │ Bridges chat platforms │ WS │ Detects music links and │ │
│ │ and exposes local API │◄─────►│ responds with alternatives │ │
│ │ │ │ │ │
│ │ - Slack bridge │ │ - Link detection (regex) │ │
│ │ - API bridge (:4242) │ │ - Calls idonthavespotify │ │
│ │ - (Discord, Matrix │ │ API for conversion │ │
│ │ can be added later) │ │ │ │
│ └────────────┬────────────┘ └──────────────┬──────────────┘ │
│ │ │ │
└───────────────┼────────────────────────────────────┼────────────────────┘
│ │
▼ ▼
┌───────────┐ ┌────────────────────┐
│ Slack │ │ idonthavespotify │
└───────────┘ │ (external API) │
└────────────────────┘
```
### How It Works
1. User posts a music link in Slack (e.g., `https://open.spotify.com/track/abc123`)
2. Matterbridge forwards the message to musiclink via WebSocket
3. MusicLink detects the music URL and calls the idonthavespotify API
4. The API returns equivalent links for Spotify, YouTube, Apple Music, Tidal, Deezer, etc.
5. MusicLink formats and sends the response back through matterbridge to Slack
### Why Matterbridge?
Matterbridge acts as a universal adapter. Instead of writing platform-specific code for each chat service, we:
1. Run matterbridge (connects to Slack, Discord, Matrix, etc.)
2. Matterbridge exposes a simple WebSocket API locally
3. Our bot connects to that API and receives/sends messages as JSON
This means:
- **Adding a new platform** = config change in matterbridge (no code changes)
- **Bot code stays simple** = just WebSocket + JSON, works from any language
- **Already in nixpkgs** = `services.matterbridge` module ready to use
### Why idonthavespotify API?
- **No API credentials needed** - No Spotify Developer account, no YouTube API keys
- **Supports 8 platforms** - Spotify, YouTube, Apple Music, Deezer, Tidal, SoundCloud, Qobuz, Bandcamp
- **Simple integration** - Single HTTP POST, returns all platform links
- **Open source** - Can self-host if needed later
## Components
### 1. Matterbridge (existing nixpkgs module)
**Package:** `pkgs.matterbridge` (v1.26.0)
**Module:** `services.matterbridge`
**Docs:** https://github.com/42wim/matterbridge/wiki
### 2. MusicLink Bot (this repo)
**Language:** Go
**Location:** `/home/dan/proj/musiclink`
**Connects to:** Matterbridge API via WebSocket
**Uses:** idonthavespotify API for link conversion
## NixOS Configuration
### Matterbridge Service
```nix
{ config, pkgs, ... }:
{
services.matterbridge = {
enable = true;
configPath = "/var/lib/matterbridge/matterbridge.toml";
};
# Ensure config directory exists
systemd.tmpfiles.rules = [
"d /var/lib/matterbridge 0750 matterbridge matterbridge -"
];
}
```
### MusicLink Service
```nix
{ config, pkgs, ... }:
let
musiclink = pkgs.buildGoModule {
pname = "musiclink";
version = "0.1.0";
src = /home/dan/proj/musiclink; # or fetchFromGitHub
vendorHash = null; # update after first build
};
in
{
systemd.services.musiclink = {
description = "MusicLink Bot";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "matterbridge.service" ];
requires = [ "matterbridge.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${musiclink}/bin/musiclink -config /var/lib/musiclink/config.toml";
Restart = "always";
RestartSec = "5s";
# Hardening
DynamicUser = true;
StateDirectory = "musiclink";
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
};
};
systemd.tmpfiles.rules = [
"d /var/lib/musiclink 0750 musiclink musiclink -"
];
}
```
## Configuration Files
### Matterbridge (`/var/lib/matterbridge/matterbridge.toml`)
```toml
# =============================================================================
# Matterbridge Configuration
# Bridges Slack to local API for musiclink bot
# =============================================================================
# -----------------------------------------------------------------------------
# Slack Connection
# -----------------------------------------------------------------------------
[slack.workspace]
# Bot User OAuth Token (starts with xoxb-)
# Get from: https://api.slack.com/apps → Your App → OAuth & Permissions
Token = "xoxb-YOUR-SLACK-BOT-TOKEN"
# Show username before messages
PrefixMessagesWithNick = true
# Optional: customize how remote users appear
RemoteNickFormat = "[{PROTOCOL}] {NICK}"
# -----------------------------------------------------------------------------
# Local API Bridge (for musiclink bot)
# -----------------------------------------------------------------------------
[api.musiclink]
# Only bind to localhost (not exposed externally)
BindAddress = "127.0.0.1:4242"
# Shared secret for bot authentication
Token = "GENERATE-A-RANDOM-TOKEN-HERE"
# Message buffer size
Buffer = 1000
# -----------------------------------------------------------------------------
# Gateway Configuration
# Routes messages between bridges
# -----------------------------------------------------------------------------
[[gateway]]
name = "main"
enable = true
# Slack channels to bridge
[[gateway.inout]]
account = "slack.workspace"
channel = "music" # Change to your channel name
# API endpoint for musiclink bot
[[gateway.inout]]
account = "api.musiclink"
channel = "api"
```
### MusicLink (`/var/lib/musiclink/config.toml`)
```toml
# =============================================================================
# MusicLink Bot Configuration
#
# Uses idonthavespotify API - no external API credentials needed!
# =============================================================================
[matterbridge]
# Must match matterbridge API bridge config
url = "ws://127.0.0.1:4242/api/websocket"
token = "GENERATE-A-RANDOM-TOKEN-HERE" # Same as matterbridge [api.musiclink].Token
gateway = "main"
# Bot identity
username = "MusicLink"
avatar = "" # Optional URL
```
That's the entire config - no Spotify/YouTube API keys required!
## Secrets Management
The only secrets needed are:
- **Slack bot token** (`xoxb-...`)
- **Matterbridge↔MusicLink shared token** (generate a random string)
Options for managing secrets:
1. **sops-nix** - Encrypted secrets in repo, decrypted at deploy time
2. **agenix** - Age-encrypted secrets
3. **Environment files** - `EnvironmentFile=` in systemd service
Example with environment file:
```nix
systemd.services.matterbridge.serviceConfig = {
EnvironmentFile = "/run/secrets/matterbridge.env";
};
```
```bash
# /run/secrets/matterbridge.env
MATTERBRIDGE_SLACK_TOKEN=xoxb-...
```
Then in matterbridge.toml:
```toml
[slack.workspace]
Token = "${MATTERBRIDGE_SLACK_TOKEN}"
```
## Slack App Setup
1. Go to https://api.slack.com/apps
2. Create New App → From scratch
3. App name: "MusicLink", select workspace
4. **OAuth & Permissions:**
- Bot Token Scopes needed:
- `channels:history` - Read messages
- `channels:read` - See channel list
- `chat:write` - Send messages
- `users:read` - Get user info
5. **Install to Workspace**
6. Copy **Bot User OAuth Token** (`xoxb-...`)
7. Invite bot to channel: `/invite @MusicLink`
## Deployment Checklist
- [ ] Slack app created and bot token obtained
- [ ] Generate random token for matterbridge↔musiclink auth
- [ ] Add matterbridge NixOS config
- [ ] Create `/var/lib/matterbridge/matterbridge.toml`
- [ ] Add musiclink NixOS config
- [ ] Create `/var/lib/musiclink/config.toml`
- [ ] Deploy NixOS config (`nixos-rebuild switch`)
- [ ] Verify services: `systemctl status matterbridge musiclink`
- [ ] Test: Post a Spotify link in the configured Slack channel
## Logs & Debugging
```bash
# Check service status
systemctl status matterbridge
systemctl status musiclink
# View logs
journalctl -u matterbridge -f
journalctl -u musiclink -f
# Test matterbridge API directly
curl -H "Authorization: Bearer YOUR-TOKEN" http://localhost:4242/api/health
# Test idonthavespotify API directly
curl -X POST 'https://idonthavespotify.sjdonado.com/api/search?v=1' \
-H 'Content-Type: application/json' \
-d '{"link":"https://open.spotify.com/track/4iV5W9uYEdYUVa79Axb7Rh"}'
```
## Adding More Platforms Later
To add Discord, Matrix, or other platforms, just update `matterbridge.toml`:
```toml
# Add Discord
[discord.myserver]
Token = "discord-bot-token"
Server = "server-id"
# Add to gateway
[[gateway.inout]]
account = "discord.myserver"
channel = "music"
```
No changes needed to musiclink bot - matterbridge handles the bridging.
## Self-Hosting idonthavespotify (Optional)
If you want to avoid the external API dependency, idonthavespotify can be self-hosted:
- Repo: https://github.com/sjdonado/idonthavespotify
- Requires: Docker or Bun runtime
- Note: Self-hosting still requires Spotify/Tidal API credentials
For most use cases, the hosted API at `idonthavespotify.sjdonado.com` is sufficient.
## Questions?
- Matterbridge docs: https://github.com/42wim/matterbridge/wiki
- idonthavespotify: https://github.com/sjdonado/idonthavespotify
- MusicLink repo: `/home/dan/proj/musiclink`