Priority 1 - Production Quality: - Revert Matrix homeserver log level from debug to info - Reduces log volume by ~70% (22k+ lines/day to <7k) - Improves performance and reduces disk usage Priority 2 - Technical Debt: - Automate sender_localpart fix in mautrix-slack.nix - Eliminates manual sed command on fresh deployments - Fix verified working (tested 2025-10-26) - Update CLAUDE.md to document automated solution Priority 3 - Project Hygiene: - Remove unused mautrix-whatsapp and mautrix-gmessages imports - Archive old configurations to docs/examples/alternative-deployments/ - Remove stale staging/ directories from 001 extraction workflow - Update deployment documentation in tasks.md and quickstart.md - Add deployment status notes to spec files Files Modified: - modules/dev-services.nix: log level debug → info - modules/mautrix-slack.nix: automatic sender_localpart fix - hosts/ops-jrz1.nix: remove unused bridge imports - CLAUDE.md: update Known Issues, add Resolved Issues section - specs/002-*/: add deployment status notes - configurations/ → docs/examples/alternative-deployments/ Tested and Verified: - All services running (matrix, bridge, forgejo, postgresql, nginx) - Bridge authenticated and message flow working - sender_localpart fix generates correct registration file
398 lines
12 KiB
Nix
398 lines
12 KiB
Nix
# mautrix-slack Matrix-Slack bridge
|
|
# Bridges Slack to Matrix via appservice
|
|
# Implementation follows mautrix-gmessages pattern for config management
|
|
{ config, pkgs, pkgs-unstable, lib, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.mautrix-slack;
|
|
yamlFormat = pkgs.formats.yaml { };
|
|
|
|
exampleConfigPath = "${cfg.package}/example-config.yaml";
|
|
|
|
configOverrides = {
|
|
homeserver = {
|
|
address = cfg.matrix.homeserverUrl;
|
|
domain = cfg.matrix.serverName;
|
|
};
|
|
appservice = {
|
|
address = "http://${cfg.appservice.hostname}:${toString cfg.appservice.port}";
|
|
hostname = cfg.appservice.hostname;
|
|
port = cfg.appservice.port;
|
|
id = cfg.appservice.id;
|
|
bot = {
|
|
username = cfg.appservice.senderLocalpart;
|
|
displayname = cfg.appservice.botDisplayName;
|
|
avatar = cfg.appservice.botAvatar;
|
|
};
|
|
username_template = "${cfg.appservice.userPrefix}{{.}}";
|
|
};
|
|
database = {
|
|
type = cfg.database.type;
|
|
uri = cfg.database.uri;
|
|
max_open_conns = cfg.database.maxOpenConnections;
|
|
max_idle_conns = cfg.database.maxIdleConnections;
|
|
};
|
|
bridge = {
|
|
command_prefix = cfg.bridge.commandPrefix;
|
|
permissions = cfg.bridge.permissions;
|
|
};
|
|
encryption = {
|
|
allow = cfg.encryption.enable;
|
|
default = cfg.encryption.default;
|
|
require = cfg.encryption.require;
|
|
};
|
|
logging = {
|
|
min_level = cfg.logging.level;
|
|
};
|
|
};
|
|
|
|
in
|
|
{
|
|
options.services.mautrix-slack = {
|
|
enable = mkEnableOption "mautrix-slack Matrix-Slack bridge";
|
|
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs-unstable.mautrix-slack;
|
|
description = "Package providing the bridge executable.";
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "mautrix_slack";
|
|
description = "System user for the bridge.";
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "mautrix_slack";
|
|
description = "Primary group for the bridge.";
|
|
};
|
|
|
|
dataDir = mkOption {
|
|
type = types.path;
|
|
default = "/var/lib/mautrix_slack";
|
|
description = "State directory.";
|
|
};
|
|
|
|
extraPackages = mkOption {
|
|
type = types.listOf types.package;
|
|
default = [];
|
|
description = "Additional packages exposed to the service PATH.";
|
|
};
|
|
|
|
extraConfig = mkOption {
|
|
type = yamlFormat.type;
|
|
default = {};
|
|
description = "Optional YAML fragment merged into the config.";
|
|
};
|
|
|
|
# Matrix homeserver configuration
|
|
matrix = {
|
|
homeserverUrl = mkOption {
|
|
type = types.str;
|
|
default = "http://127.0.0.1:8008";
|
|
description = "Matrix homeserver URL for bridge connections";
|
|
};
|
|
|
|
serverName = mkOption {
|
|
type = types.str;
|
|
description = "Matrix server name for bridge users";
|
|
};
|
|
};
|
|
|
|
# Database configuration
|
|
database = {
|
|
type = mkOption {
|
|
type = types.str;
|
|
default = "postgres";
|
|
description = "Database type (sqlite3 or postgres)";
|
|
};
|
|
|
|
uri = mkOption {
|
|
type = types.str;
|
|
default = "postgresql:///mautrix_slack?host=/run/postgresql";
|
|
description = "Database connection string";
|
|
};
|
|
|
|
maxOpenConnections = mkOption {
|
|
type = types.int;
|
|
default = 32;
|
|
description = "Maximum number of open database connections";
|
|
};
|
|
|
|
maxIdleConnections = mkOption {
|
|
type = types.int;
|
|
default = 4;
|
|
description = "Maximum number of idle database connections";
|
|
};
|
|
};
|
|
|
|
# Logging configuration
|
|
logging = {
|
|
level = mkOption {
|
|
type = types.str;
|
|
default = "info";
|
|
description = "Log level (debug, info, warn, error)";
|
|
};
|
|
};
|
|
|
|
# Bridge behavior configuration
|
|
bridge = {
|
|
commandPrefix = mkOption {
|
|
type = types.str;
|
|
default = "!slack";
|
|
description = "Command prefix for bridge commands";
|
|
};
|
|
|
|
permissions = mkOption {
|
|
type = types.attrsOf types.str;
|
|
default = {
|
|
"example.com" = "user";
|
|
};
|
|
description = "Bridge permissions mapping";
|
|
};
|
|
};
|
|
|
|
# Appservice configuration
|
|
appservice = {
|
|
id = mkOption {
|
|
type = types.str;
|
|
default = "slack";
|
|
description = "Appservice ID";
|
|
};
|
|
|
|
hostname = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
description = "Hostname for the appservice";
|
|
};
|
|
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 29319;
|
|
description = "Port for the appservice API";
|
|
};
|
|
|
|
senderLocalpart = mkOption {
|
|
type = types.str;
|
|
default = "slackbot";
|
|
description = "Localpart of the bridge bot user";
|
|
};
|
|
|
|
botDisplayName = mkOption {
|
|
type = types.str;
|
|
default = "Slack Bridge Bot";
|
|
description = "Display name for the bridge bot";
|
|
};
|
|
|
|
botAvatar = mkOption {
|
|
type = types.str;
|
|
default = "";
|
|
description = "Avatar MXC URI for the bridge bot";
|
|
};
|
|
|
|
userPrefix = mkOption {
|
|
type = types.str;
|
|
default = "slack_";
|
|
description = "Prefix for Matrix users bridged from Slack";
|
|
};
|
|
};
|
|
|
|
# Encryption configuration
|
|
encryption = {
|
|
enable = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = "Enable Matrix end-to-end encryption";
|
|
};
|
|
|
|
default = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Enable encryption by default in new portals";
|
|
};
|
|
|
|
require = mkOption {
|
|
type = types.bool;
|
|
default = false;
|
|
description = "Require encryption in all portals";
|
|
};
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
# User and group
|
|
users.users.${cfg.user} = {
|
|
isSystemUser = true;
|
|
group = cfg.group;
|
|
extraGroups = [ "matrix-appservices" ];
|
|
home = cfg.dataDir;
|
|
createHome = true;
|
|
};
|
|
users.groups.${cfg.group} = {};
|
|
users.groups.matrix-appservices = {};
|
|
|
|
# Systemd service with gmessages-style configuration
|
|
systemd.services.mautrix-slack = {
|
|
description = "mautrix-slack Matrix-Slack bridge";
|
|
after = [ "network.target" ]
|
|
++ optional (cfg.database.type == "postgres") "postgresql.service";
|
|
wants = [ "network-online.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
SupplementaryGroups = [ "matrix-appservices" ];
|
|
WorkingDirectory = cfg.dataDir;
|
|
|
|
# Use StateDirectory for persistent config and data management
|
|
StateDirectory = cfg.user;
|
|
StateDirectoryMode = "0750";
|
|
|
|
# Two-stage pre-start: directory setup as root, then config generation as service user
|
|
ExecStartPre = [
|
|
# Stage 1: Create config directory as root (+ prefix)
|
|
"+${pkgs.coreutils}/bin/mkdir -p /var/lib/${cfg.user}/config"
|
|
"+${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} /var/lib/${cfg.user}"
|
|
"+${pkgs.coreutils}/bin/chmod 750 /var/lib/${cfg.user}"
|
|
"+${pkgs.coreutils}/bin/chmod 750 /var/lib/${cfg.user}/config"
|
|
|
|
# Stage 2: Generate configuration using example template
|
|
"${pkgs.writeShellScript "mautrix-slack-prepare-config" ''
|
|
set -euo pipefail
|
|
|
|
CONFIG_DIR=/var/lib/${cfg.user}/config
|
|
CONFIG_PATH="$CONFIG_DIR/config.yaml"
|
|
REG_PATH="/var/lib/matrix-appservices/mautrix_slack_registration.yaml"
|
|
|
|
# Always regenerate config from example
|
|
cd "$CONFIG_DIR"
|
|
rm -f config.yaml
|
|
${cfg.package}/bin/mautrix-slack -c config.yaml -e
|
|
|
|
${pkgs.python3.withPackages (ps: [ ps.pyyaml ])}/bin/python3 <<'PY'
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import pwd
|
|
import grp
|
|
import yaml
|
|
|
|
CONFIG_PATH = pathlib.Path('config.yaml')
|
|
REG_PATH = pathlib.Path('/var/lib/matrix-appservices/mautrix_slack_registration.yaml')
|
|
|
|
with CONFIG_PATH.open('r', encoding='utf-8') as fh:
|
|
config = yaml.safe_load(fh)
|
|
|
|
overrides = json.loads(${lib.escapeShellArg (builtins.toJSON configOverrides)})
|
|
extra_cfg = json.loads(${lib.escapeShellArg (builtins.toJSON cfg.extraConfig)})
|
|
|
|
def deep_update(target, data):
|
|
for key, value in data.items():
|
|
if isinstance(value, dict) and value is not None:
|
|
if key not in target or not isinstance(target.get(key), dict):
|
|
target[key] = {}
|
|
deep_update(target[key], value)
|
|
else:
|
|
target[key] = value
|
|
|
|
deep_update(config, overrides)
|
|
if extra_cfg:
|
|
deep_update(config, extra_cfg)
|
|
|
|
# Handle permissions explicitly to ensure example values are replaced
|
|
perms = overrides.get('bridge', {}).get('permissions')
|
|
if perms is not None:
|
|
config.setdefault('bridge', {})['permissions'] = perms
|
|
|
|
# Read tokens from registration file if it exists
|
|
if REG_PATH.exists():
|
|
with REG_PATH.open('r', encoding='utf-8') as fh:
|
|
registration = yaml.safe_load(fh)
|
|
|
|
if 'as_token' in registration:
|
|
config.setdefault('appservice', {})['as_token'] = registration['as_token']
|
|
if 'hs_token' in registration:
|
|
config.setdefault('appservice', {})['hs_token'] = registration['hs_token']
|
|
|
|
with CONFIG_PATH.open('w', encoding='utf-8') as fh:
|
|
yaml.safe_dump(config, fh, default_flow_style=False, sort_keys=False)
|
|
|
|
uid = pwd.getpwnam('${cfg.user}').pw_uid
|
|
gid_service = grp.getgrnam('${cfg.group}').gr_gid
|
|
gid_shared = grp.getgrnam('matrix-appservices').gr_gid
|
|
|
|
os.chown(CONFIG_PATH, uid, gid_service)
|
|
os.chmod(CONFIG_PATH, 0o640)
|
|
PY
|
|
|
|
# Generate registration file if it doesn't exist
|
|
if [ ! -f "$REG_PATH" ]; then
|
|
mkdir -p $(dirname "$REG_PATH")
|
|
${cfg.package}/bin/mautrix-slack -c config.yaml -g -r "$REG_PATH"
|
|
|
|
# Fix sender_localpart to match config (bridge generates random value)
|
|
# See: https://github.com/mautrix/slack/issues - registration -g ignores config.yaml
|
|
${pkgs.gnused}/bin/sed -i "s/^sender_localpart: .*/sender_localpart: ${cfg.appservice.senderLocalpart}/" "$REG_PATH"
|
|
|
|
chown ${cfg.user}:matrix-appservices "$REG_PATH"
|
|
chmod 640 "$REG_PATH"
|
|
fi
|
|
''}"
|
|
];
|
|
|
|
# Start mautrix-slack with state directory config
|
|
ExecStart = "${cfg.package}/bin/mautrix-slack -c /var/lib/${cfg.user}/config/config.yaml";
|
|
|
|
# Restart policy
|
|
Restart = "always";
|
|
RestartSec = "10s";
|
|
|
|
# Security hardening
|
|
NoNewPrivileges = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
|
|
# Allow writing to data and shared appservices directory
|
|
ReadWritePaths = [
|
|
cfg.dataDir
|
|
"/var/lib/matrix-appservices"
|
|
];
|
|
|
|
# Resource limits
|
|
MemoryMax = "256M";
|
|
|
|
# Process security
|
|
UMask = "0027";
|
|
LockPersonality = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
|
|
# Logging
|
|
StandardOutput = "journal";
|
|
StandardError = "journal";
|
|
SyslogIdentifier = "mautrix-slack";
|
|
};
|
|
|
|
# Add extra packages to PATH if specified
|
|
path = [ pkgs.coreutils ] ++ cfg.extraPackages;
|
|
};
|
|
|
|
# Directory permissions
|
|
systemd.tmpfiles.rules = [
|
|
"d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -"
|
|
"d /var/lib/matrix-appservices 0770 root matrix-appservices -"
|
|
];
|
|
};
|
|
}
|