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

11 KiB

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 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

Language: Go Location: /home/dan/proj/musiclink Connects to: Matterbridge API via WebSocket Uses: idonthavespotify API for link conversion

NixOS Configuration

Matterbridge Service

{ 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 -"
  ];
}
{ 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)

# =============================================================================
# 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 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:

systemd.services.matterbridge.serviceConfig = {
  EnvironmentFile = "/run/secrets/matterbridge.env";
};
# /run/secrets/matterbridge.env
MATTERBRIDGE_SLACK_TOKEN=xoxb-...

Then in matterbridge.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

# 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:

# 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:

For most use cases, the hosted API at idonthavespotify.sjdonado.com is sufficient.

Questions?