feat: add mautrix mattermost

This commit is contained in:
Dominik Polakovics Polakovics 2026-03-02 13:55:15 +01:00
parent 7882b04089
commit d725df2606
7 changed files with 570 additions and 42 deletions

View file

@ -0,0 +1,400 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.mautrix-mattermost;
dataDir = cfg.dataDir;
format = pkgs.formats.yaml { };
registrationFile = "${dataDir}/mattermost-registration.yaml";
settingsFile = "${dataDir}/config.yaml";
settingsFileUnformatted = format.generate "mattermost-config-unsubstituted.yaml" cfg.settings;
in
{
options = {
services.mautrix-mattermost = {
enable = lib.mkEnableOption "Mautrix-Mattermost, a Matrix-Mattermost puppeting/relay-bot bridge";
package = lib.mkOption {
type = lib.types.package;
default = pkgs.mautrix-mattermost;
defaultText = lib.literalExpression "pkgs.mautrix-mattermost";
description = "The mautrix-mattermost package to use.";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
config = {
_module.args = { inherit cfg lib; };
};
options = {
homeserver = lib.mkOption {
type = lib.types.attrs;
default = {
software = "standard";
status_endpoint = null;
message_send_checkpoint_endpoint = null;
async_media = false;
websocket = false;
ping_interval_seconds = 0;
};
description = ''
Homeserver configuration.
See the mautrix-mattermost example-config.yaml for more information.
'';
};
appservice = lib.mkOption {
type = lib.types.attrs;
default = {
address = "http://localhost:29335";
hostname = "0.0.0.0";
port = 29335;
database = {
type = "sqlite3";
uri = "file:${dataDir}/mautrix-mattermost.db?_txlock=immediate";
max_open_conns = 20;
max_idle_conns = 2;
max_conn_idle_time = null;
max_conn_lifetime = null;
};
id = "mattermost";
bot = {
username = "mattermostbot";
displayname = "Mattermost bridge bot";
avatar = "";
};
ephemeral_events = true;
async_transactions = false;
as_token = "This value is generated when generating the registration";
hs_token = "This value is generated when generating the registration";
};
description = ''
Appservice configuration.
See the mautrix-mattermost example-config.yaml for more information.
'';
};
bridge = lib.mkOption {
type = lib.types.attrs;
default = {
username_template = "mattermost_{{.}}";
command_prefix = "!mm";
double_puppet_server_map = { };
double_puppet_allow_discovery = false;
login_shared_secret_map = { };
management_room_text = {
welcome = "Hello, I'm a Mattermost bridge bot.";
welcome_connected = "Use `help` for help.";
welcome_unconnected = "Use `help` for help or `login` to log in.";
additional_help = "";
};
encryption = {
allow = false;
default = false;
appservice = false;
require = false;
allow_key_sharing = false;
plaintext_mentions = false;
delete_keys = {
delete_outbound_on_ack = false;
dont_store_outbound = false;
ratchet_on_decrypt = false;
delete_fully_used_on_decrypt = false;
delete_prev_on_new_session = false;
delete_on_device_delete = false;
periodically_delete_expired = false;
delete_outdated_inbound = false;
};
verification_levels = {
receive = "unverified";
send = "unverified";
share = "cross-signed-tofu";
};
rotation = {
enable_custom = false;
milliseconds = 604800000;
messages = 100;
disable_device_change_key_rotation = false;
};
};
provisioning = {
prefix = "/_matrix/provision";
shared_secret = "generate";
debug_endpoints = false;
};
permissions = {
"*" = "relay";
};
};
description = ''
Bridge configuration.
See the mautrix-mattermost example-config.yaml for more information.
'';
};
logging = lib.mkOption {
type = lib.types.attrs;
default = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
};
description = ''
Logging configuration.
See the mautrix-mattermost example-config.yaml for more information.
'';
};
};
};
default = { };
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
'';
};
registerToSynapse = lib.mkOption {
type = lib.types.bool;
default = config.services.matrix-synapse.enable;
defaultText = lib.literalExpression "config.services.matrix-synapse.enable";
description = ''
Whether to add the bridge's app service registration file to
`services.matrix-synapse.settings.app_service_config_files`.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/mautrix-mattermost";
description = ''
Directory to store the bridge's configuration and database files.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing environment variables to substitute when copying the configuration
out of Nix store to the `services.mautrix-mattermost.dataDir`.
Can be used for storing the secrets without making them available in the Nix store.
'';
};
serviceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "mautrix-mattermost.service";
description = "The systemd unit for the bridge service.";
};
registrationServiceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "mautrix-mattermost-registration.service";
description = "The registration service that generates the registration file.";
};
serviceDependencies = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
cfg.registrationServiceUnit
]
++ (lib.lists.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit)
++ (lib.lists.optional (config.services ? matrix-conduit && config.services.matrix-conduit.enable) "matrix-conduit.service")
++ (lib.lists.optional (config.services ? dendrite && config.services.dendrite.enable) "dendrite.service");
description = ''
List of Systemd services to require and wait for when starting the application service.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
cfg.settings.homeserver.domain or "" != "" && cfg.settings.homeserver.address or "" != "";
message = ''
The options with information about the homeserver:
`services.mautrix-mattermost.settings.homeserver.domain` and
`services.mautrix-mattermost.settings.homeserver.address` have to be set.
'';
}
{
assertion = cfg.settings.bridge.permissions or { } != { };
message = ''
The option `services.mautrix-mattermost.settings.bridge.permissions` has to be set.
'';
}
];
users.users.mautrix-mattermost = {
isSystemUser = true;
group = "mautrix-mattermost";
extraGroups = [ "mautrix-mattermost-registration" ];
home = dataDir;
description = "Mautrix-Mattermost bridge user";
};
users.groups.mautrix-mattermost = { };
users.groups.mautrix-mattermost-registration = {
members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse";
};
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
settings.app_service_config_files = [ registrationFile ];
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 770 mautrix-mattermost mautrix-mattermost -"
];
systemd.services = {
matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-mattermost-registration" ];
wants = [ "mautrix-mattermost-registration.service" ];
after = [ "mautrix-mattermost-registration.service" ];
};
mautrix-mattermost-registration = {
description = "Mautrix-Mattermost registration generation service";
wantedBy = lib.mkIf cfg.registerToSynapse [ "multi-user.target" ];
before = lib.mkIf cfg.registerToSynapse [ "matrix-synapse.service" ];
path = [
pkgs.yq
pkgs.envsubst
cfg.package
];
script = ''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnformatted}'
config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile}')
registration_already_exists=$([[ -f '${registrationFile}' ]] && echo "true" || echo "false")
echo "There are tokens in the config: $config_has_tokens"
echo "Registration already existed: $registration_already_exists"
# tokens not configured from config/environment file, and registration file
# is already generated, override tokens in config to make sure they are not lost
if [[ $config_has_tokens == "false" && $registration_already_exists == "true" ]]; then
echo "Copying as_token, hs_token from registration into configuration"
yq -sY '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]' '${settingsFile}' '${registrationFile}' \
> '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
fi
# make sure --generate-registration does not affect config.yaml
cp '${settingsFile}' '${settingsFile}.tmp'
echo "Generating registration file"
mautrix-mattermost \
--generate-registration \
--config='${settingsFile}.tmp' \
--registration='${registrationFile}'
rm '${settingsFile}.tmp'
# no tokens configured, and new were just generated by generate registration for first time
if [[ $config_has_tokens == "false" && $registration_already_exists == "false" ]]; then
echo "Copying newly generated as_token, hs_token from registration into configuration"
yq -sY '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]' '${settingsFile}' '${registrationFile}' \
> '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
fi
# Make sure correct tokens are in the registration file
if [[ $config_has_tokens == "true" || $registration_already_exists == "true" ]]; then
echo "Copying as_token, hs_token from configuration to the registration file"
yq -sY '.[1].as_token = .[0].appservice.as_token
| .[1].hs_token = .[0].appservice.hs_token
| .[1]' '${settingsFile}' '${registrationFile}' \
> '${registrationFile}.tmp'
mv '${registrationFile}.tmp' '${registrationFile}'
fi
umask $old_umask
chown :mautrix-mattermost-registration '${registrationFile}'
chmod 640 '${registrationFile}'
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
UMask = 27;
User = "mautrix-mattermost";
Group = "mautrix-mattermost";
SystemCallFilter = [ "@system-service" ];
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ dataDir ];
StateDirectory = "mautrix-mattermost";
EnvironmentFile = cfg.environmentFile;
};
restartTriggers = [ settingsFileUnformatted ];
};
mautrix-mattermost = {
description = "Mautrix-Mattermost, a Matrix-Mattermost puppeting/relaybot bridge";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
serviceConfig = {
Type = "simple";
User = "mautrix-mattermost";
Group = "mautrix-mattermost";
PrivateUsers = true;
Restart = "on-failure";
RestartSec = 30;
WorkingDirectory = dataDir;
ExecStart = ''
${lib.getExe cfg.package} \
--config='${settingsFile}'
'';
EnvironmentFile = cfg.environmentFile;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateDevices = true;
PrivateTmp = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
LockPersonality = true;
ProtectKernelLogs = true;
ProtectHostname = true;
ProtectClock = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = "@system-service";
ReadWritePaths = [ cfg.dataDir ];
};
restartTriggers = [ settingsFileUnformatted ];
};
};
};
}