push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
{
config,
options,
pkgs,
lib,
...
}:
let
dataDir = "/var/lib/matrix-appservice-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
cfg = config.services.matrix-appservice-discord;
opt = options.services.matrix-appservice-discord;
# TODO: switch to configGen.json once RFC42 is implemented
settingsFile = pkgs.writeText "matrix-appservice-discord-settings.json" (
builtins.toJSON cfg.settings
);
in
{
options = {
services.matrix-appservice-discord = {
enable = lib.mkEnableOption "a bridge between Matrix and Discord";
package = lib.mkPackageOption pkgs "matrix-appservice-discord" { };
settings = lib.mkOption rec {
# TODO: switch to lib.types.config.json as prescribed by RFC42 once it's implemented
type = lib.types.attrs;
apply = lib.recursiveUpdate default;
default = {
database = {
filename = "${dataDir}/discord.db";
};
# empty values necessary for registration file generation
# actual values defined in environmentFile
auth = {
clientID = "";
botToken = "";
};
};
example = lib.literalExpression ''
{
bridge = {
domain = "public-domain.tld";
homeserverUrl = "http://public-domain.tld:8008";
};
}
'';
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in
[config.sample.yaml](https://github.com/Half-Shot/matrix-appservice-discord/blob/master/config/config.sample.yaml).
{option}`config.bridge.domain` and {option}`config.bridge.homeserverUrl`
should be set to match the public host name of the Matrix homeserver for webhooks and avatars to work.
Secret tokens should be specified using {option}`environmentFile`
instead of this world-readable attribute set.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing environment variables to be passed to the matrix-appservice-discord service,
in which secret tokens can be specified securely by defining values for
`APPSERVICE_DISCORD_AUTH_CLIENT_I_D` and
`APPSERVICE_DISCORD_AUTH_BOT_TOKEN`.
'';
};
url = lib.mkOption {
type = lib.types.str;
default = "http://localhost:${toString cfg.port}";
defaultText = lib.literalExpression ''"http://localhost:''${toString config.${opt.port}}"'';
description = ''
The URL where the application service is listening for HS requests.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 9005; # from https://github.com/Half-Shot/matrix-appservice-discord/blob/master/package.json#L11
description = ''
Port number on which the bridge should listen for internal communication with the Matrix homeserver.
'';
};
localpart = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
The user_id localpart to assign to the AS.
'';
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
defaultText = lib.literalExpression ''
lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
'';
description = ''
List of Systemd services to require and wait for when starting the application service,
such as the Matrix homeserver if it's running on the same host.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.matrix-appservice-discord = {
description = "A bridge between Matrix and Discord.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
if [ ! -f '${registrationFile}' ]; then
${cfg.package}/bin/matrix-appservice-discord \
--generate-registration \
--url=${lib.escapeShellArg cfg.url} \
${
lib.optionalString (cfg.localpart != null) "--localpart=${lib.escapeShellArg cfg.localpart}"
} \
--config='${settingsFile}' \
--file='${registrationFile}'
fi
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
DynamicUser = true;
PrivateTmp = true;
WorkingDirectory = "${cfg.package}/${cfg.package.passthru.nodeAppDir}";
StateDirectory = baseNameOf dataDir;
UMask = "0027";
EnvironmentFile = cfg.environmentFile;
ExecStart = ''
${cfg.package}/bin/matrix-appservice-discord \
--file='${registrationFile}' \
--config='${settingsFile}' \
--port='${toString cfg.port}'
'';
};
};
};
meta.maintainers = with lib.maintainers; [ euxane ];
}

View File

@@ -0,0 +1,284 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.matrix-appservice-irc;
pkg = pkgs.matrix-appservice-irc;
bin = "${pkg}/bin/matrix-appservice-irc";
jsonType = (pkgs.formats.json { }).type;
configFile =
pkgs.runCommand "matrix-appservice-irc.yml"
{
# Because this program will be run at build time, we need `nativeBuildInputs`
nativeBuildInputs = [
(pkgs.python3.withPackages (ps: [ ps.jsonschema ]))
pkgs.remarshal
];
preferLocalBuild = true;
config = builtins.toJSON cfg.settings;
passAsFile = [ "config" ];
}
''
# The schema is given as yaml, we need to convert it to json
remarshal --if yaml --of json -i ${pkg}/config.schema.yml -o config.schema.json
python -m jsonschema config.schema.json -i $configPath
cp "$configPath" "$out"
'';
registrationFile = "/var/lib/matrix-appservice-irc/registration.yml";
in
{
options.services.matrix-appservice-irc = with lib.types; {
enable = lib.mkEnableOption "the Matrix/IRC bridge";
port = lib.mkOption {
type = port;
description = "The port to listen on";
default = 8009;
};
needBindingCap = lib.mkOption {
type = bool;
description = "Whether the daemon needs to bind to ports below 1024 (e.g. for the ident service)";
default = false;
};
passwordEncryptionKeyLength = lib.mkOption {
type = ints.unsigned;
description = "Length of the key to encrypt IRC passwords with";
default = 4096;
example = 8192;
};
registrationUrl = lib.mkOption {
type = str;
description = ''
The URL where the application service is listening for homeserver requests,
from the Matrix homeserver perspective.
'';
example = "http://localhost:8009";
};
localpart = lib.mkOption {
type = str;
description = "The user_id localpart to assign to the appservice";
default = "appservice-irc";
};
settings = lib.mkOption {
description = ''
Configuration for the appservice, see
<https://github.com/matrix-org/matrix-appservice-irc/blob/${pkgs.matrix-appservice-irc.version}/config.sample.yaml>
for supported values
'';
default = { };
type = submodule {
freeformType = jsonType;
options = {
homeserver = lib.mkOption {
description = "Homeserver configuration";
default = { };
type = submodule {
freeformType = jsonType;
options = {
url = lib.mkOption {
type = str;
description = "The URL to the home server for client-server API calls";
};
domain = lib.mkOption {
type = str;
description = ''
The 'domain' part for user IDs on this home server. Usually
(but not always) is the "domain name" part of the homeserver URL.
'';
};
};
};
};
database = lib.mkOption {
default = { };
description = "Configuration for the database";
type = submodule {
freeformType = jsonType;
options = {
engine = lib.mkOption {
type = str;
description = "Which database engine to use";
default = "nedb";
example = "postgres";
};
connectionString = lib.mkOption {
type = str;
description = "The database connection string";
default = "nedb://var/lib/matrix-appservice-irc/data";
example = "postgres://username:password@host:port/databasename";
};
};
};
};
ircService = lib.mkOption {
default = { };
description = "IRC bridge configuration";
type = submodule {
freeformType = jsonType;
options = {
passwordEncryptionKeyPath = lib.mkOption {
type = str;
description = ''
Location of the key with which IRC passwords are encrypted
for storage. Will be generated on first run if not present.
'';
default = "/var/lib/matrix-appservice-irc/passkey.pem";
};
servers = lib.mkOption {
type = submodule { freeformType = jsonType; };
description = "IRC servers to connect to";
};
mediaProxy = {
signingKeyPath = lib.mkOption {
type = path;
default = "/var/lib/matrix-appservice-irc/media-signingkey.jwk";
description = ''
Path to the signing key file for authenticated media.
'';
};
ttlSeconds = lib.mkOption {
type = ints.unsigned;
default = 3600;
example = 0;
description = ''
Lifetime in seconds, that generated URLs stay valid.
Set the lifetime to 0 to prevent URLs from becoming invalid.
'';
};
bindPort = lib.mkOption {
type = port;
default = 11111;
description = ''
Port that the media proxy binds to.
'';
};
publicUrl = lib.mkOption {
type = str;
example = "https://matrix.example.com/media";
description = ''
URL under which the media proxy is publicly acccessible.
'';
};
};
};
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.matrix-appservice-irc = {
description = "Matrix-IRC bridge";
before = [ "matrix-synapse.service" ]; # So the registration can be used by Synapse
after = lib.optionals (cfg.settings.database.engine == "postgres") [
"postgresql.target"
];
wantedBy = [ "multi-user.target" ];
preStart = ''
umask 077
# Generate key for crypting passwords
if ! [ -f "${cfg.settings.ircService.passwordEncryptionKeyPath}" ]; then
${pkgs.openssl}/bin/openssl genpkey \
-out "${cfg.settings.ircService.passwordEncryptionKeyPath}" \
-outform PEM \
-algorithm RSA \
-pkeyopt "rsa_keygen_bits:${toString cfg.passwordEncryptionKeyLength}"
fi
# Generate registration file
if ! [ -f "${registrationFile}" ]; then
# The easy case: the file has not been generated yet
${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
else
# The tricky case: we already have a generation file. Because the NixOS configuration might have changed, we need to
# regenerate it. But this would give the service a new random ID and tokens, so we need to back up and restore them.
# 1. Backup
id=$(grep "^id:.*$" ${registrationFile})
hs_token=$(grep "^hs_token:.*$" ${registrationFile})
as_token=$(grep "^as_token:.*$" ${registrationFile})
# 2. Regenerate
${bin} --generate-registration --file ${registrationFile} --config ${configFile} --url ${cfg.registrationUrl} --localpart ${cfg.localpart}
# 3. Restore
sed -i "s/^id:.*$/$id/g" ${registrationFile}
sed -i "s/^hs_token:.*$/$hs_token/g" ${registrationFile}
sed -i "s/^as_token:.*$/$as_token/g" ${registrationFile}
fi
if ! [ -f "${cfg.settings.ircService.mediaProxy.signingKeyPath}" ]; then
${lib.getExe pkgs.nodejs} ${pkg}/lib/generate-signing-key.js > "${cfg.settings.ircService.mediaProxy.signingKeyPath}"
fi
# Allow synapse access to the registration
if ${pkgs.getent}/bin/getent group matrix-synapse > /dev/null; then
chgrp matrix-synapse ${registrationFile}
chmod g+r ${registrationFile}
fi
'';
serviceConfig = rec {
Type = "simple";
ExecStart = "${bin} --config ${configFile} --file ${registrationFile} --port ${toString cfg.port}";
ProtectHome = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
StateDirectory = "matrix-appservice-irc";
StateDirectoryMode = "755";
User = "matrix-appservice-irc";
Group = "matrix-appservice-irc";
CapabilityBoundingSet = [ "CAP_CHOWN" ] ++ lib.optional (cfg.needBindingCap) "CAP_NET_BIND_SERVICE";
AmbientCapabilities = CapabilityBoundingSet;
NoNewPrivileges = true;
LockPersonality = true;
RestrictRealtime = true;
PrivateMounts = true;
SystemCallFilter = [
"@system-service @pkey"
"~@privileged @resources"
"@chown"
];
SystemCallArchitectures = "native";
# AF_UNIX is required to connect to a postgres socket.
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
};
};
users.groups.matrix-appservice-irc = { };
users.users.matrix-appservice-irc = {
description = "Service user for the Matrix-IRC bridge";
group = "matrix-appservice-irc";
isSystemUser = true;
};
};
# uses attributes of the linked package
meta.buildDocsInSandbox = false;
}

View File

@@ -0,0 +1,190 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.matrix-conduit;
format = pkgs.formats.toml { };
configFile = format.generate "conduit.toml" cfg.settings;
in
{
meta.maintainers = with lib.maintainers; [
pstn
SchweGELBin
];
options.services.matrix-conduit = {
enable = lib.mkEnableOption "matrix-conduit";
extraEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra Environment variables to pass to the conduit server.";
default = { };
example = {
RUST_BACKTRACE = "yes";
};
};
package = lib.mkPackageOption pkgs "matrix-conduit" { };
secretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/matrix-conduit.env";
description = ''
Path to a file containing sensitive environment as described in {manpage}`systemd.exec(5).
Some variables that can be considered secrets are:
- CONDUIT_JWT_SECRET:
The secret used to enable JWT login. Without it a 400 error will be returned.
- CONDUIT_TURN_SECRET:
The TURN secret
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
global.server_name = lib.mkOption {
type = lib.types.str;
example = "example.com";
description = "The server_name is the name of this server. It is used as a suffix for user # and room ids.";
};
global.port = lib.mkOption {
type = lib.types.port;
default = 6167;
description = "The port Conduit will be running on. You need to set up a reverse proxy in your web server (e.g. apache or nginx), so all requests to /_matrix on port 443 and 8448 will be forwarded to the Conduit instance running on this port";
};
global.max_request_size = lib.mkOption {
type = lib.types.ints.positive;
default = 20000000;
description = "Max request size in bytes. Don't forget to also change it in the proxy.";
};
global.allow_registration = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether new users can register on this server.";
};
global.allow_encryption = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
};
global.allow_federation = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether this server federates with other servers.
'';
};
global.trusted_servers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "matrix.org" ];
description = "Servers trusted with signing server keys.";
};
global.address = lib.mkOption {
type = lib.types.str;
default = "::1";
description = "Address to listen on for connections by the reverse proxy/tls terminator.";
};
global.database_path = lib.mkOption {
type = lib.types.str;
default = "/var/lib/matrix-conduit/";
readOnly = true;
description = ''
Path to the conduit database, the directory where conduit will save its data.
Note that due to using the DynamicUser feature of systemd, this value should not be changed
and is set to be read only.
'';
};
global.database_backend = lib.mkOption {
type = lib.types.enum [
"sqlite"
"rocksdb"
];
default = "sqlite";
example = "rocksdb";
description = ''
The database backend for the service. Switching it on an existing
instance will require manual migration of data.
'';
};
global.allow_check_for_updates = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to allow Conduit to automatically contact
<https://conduit.rs> hourly to check for important Conduit news.
Disabled by default because nixpkgs handles updates.
'';
};
};
};
default = { };
description = ''
Generates the conduit.toml configuration file. Refer to
<https://docs.conduit.rs/configuration.html>
for details on supported values.
Note that database_path can not be edited because the service's reliance on systemd StateDir.
For secrets use the `secretFile` option instead.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.conduit = {
description = "Conduit Matrix Server";
documentation = [ "https://gitlab.com/famedly/conduit/" ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = lib.mkMerge [
{ CONDUIT_CONFIG = configFile; }
cfg.extraEnvironment
];
serviceConfig = {
DynamicUser = true;
User = "conduit";
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
StateDirectory = "matrix-conduit";
StateDirectoryMode = "0700";
ExecStart = "${cfg.package}/bin/conduit";
Restart = "on-failure";
RestartSec = 10;
UMask = "077";
}
// lib.optionalAttrs (cfg.secretFile != null) {
EnvironmentFile = cfg.secretFile;
};
unitConfig = {
StartLimitBurst = 5;
};
};
};
}

View File

@@ -0,0 +1,268 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.matrix-continuwuity;
defaultUser = "continuwuity";
defaultGroup = "continuwuity";
format = pkgs.formats.toml { };
configFile = format.generate "continuwuity.toml" cfg.settings;
in
{
meta.maintainers = with lib.maintainers; [
nyabinary
snaki
];
options.services.matrix-continuwuity = {
enable = lib.mkEnableOption "continuwuity";
user = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
The user {command}`continuwuity` is run as.
'';
default = defaultUser;
};
group = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
The group {command}`continuwuity` is run as.
'';
default = defaultGroup;
};
extraEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra Environment variables to pass to the continuwuity server.";
default = { };
example = {
RUST_BACKTRACE = "yes";
};
};
package = lib.mkPackageOption pkgs "matrix-continuwuity" { };
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
global.server_name = lib.mkOption {
type = lib.types.nonEmptyStr;
example = "example.com";
description = "The server_name is the name of this server. It is used as a suffix for user and room ids.";
};
global.address = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
default = null;
example = [
"127.0.0.1"
"::1"
];
description = ''
Addresses (IPv4 or IPv6) to listen on for connections by the reverse proxy/tls terminator.
If set to `null`, continuwuity will listen on IPv4 and IPv6 localhost.
Must be `null` if `unix_socket_path` is set.
'';
};
global.port = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ 6167 ];
description = ''
The port(s) continuwuity will be running on.
You need to set up a reverse proxy in your web server (e.g. apache or nginx),
so all requests to /_matrix on port 443 and 8448 will be forwarded to the continuwuity
instance running on this port.
'';
};
global.unix_socket_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Listen on a UNIX socket at the specified path. If listening on a UNIX socket,
listening on an address will be disabled. The `address` option must be set to
`null` (the default value). The option {option}`services.continuwuity.group` must
be set to a group your reverse proxy is part of.
This will automatically add a system user "continuwuity" to your system if
{option}`services.continuwuity.user` is left at the default, and a "continuwuity"
group if {option}`services.continuwuity.group` is left at the default.
'';
};
global.unix_socket_perms = lib.mkOption {
type = lib.types.ints.positive;
default = 660;
description = "The default permissions (in octal) to create the UNIX socket with.";
};
global.max_request_size = lib.mkOption {
type = lib.types.ints.positive;
default = 20000000;
description = "Max request size in bytes. Don't forget to also change it in the proxy.";
};
global.allow_registration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether new users can register on this server.
Registration with token requires `registration_token` or `registration_token_file` to be set.
If set to true without a token configured, and
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
is set to true, users can freely register.
'';
};
global.allow_encryption = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
};
global.allow_federation = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether this server federates with other servers.
'';
};
global.trusted_servers = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ "matrix.org" ];
description = ''
Servers listed here will be used to gather public keys of other servers
(notary trusted key servers).
Currently, continuwuity doesn't support inbound batched key requests, so
this list should only contain other Synapse servers.
Example: `[ "matrix.org" "constellatory.net" "tchncs.de" ]`
'';
};
global.database_path = lib.mkOption {
readOnly = true;
type = lib.types.path;
default = "/var/lib/continuwuity/";
description = ''
Path to the continuwuity database, the directory where continuwuity will save its data.
Note that database_path cannot be edited because of the service's reliance on systemd StateDir.
'';
};
global.allow_announcements_check = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
If enabled, continuwuity will send a simple GET request periodically to
<https://continuwuity.org/.well-known/continuwuity/announcements> for any new announcements made.
'';
};
};
};
default = { };
# TOML does not allow null values, so we use null to omit those fields
apply = lib.filterAttrsRecursive (_: v: v != null);
description = ''
Generates the continuwuity.toml configuration file. Refer to
<https://continuwuity.org/configuration.html>
for details on supported values.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.settings ? global.unix_socket_path) || !(cfg.settings ? global.address);
message = ''
In `services.continuwuity.settings.global`, `unix_socket_path` and `address` cannot be set at the
same time.
Leave one of the two options unset or explicitly set them to `null`.
'';
}
{
assertion = cfg.user != defaultUser -> config ? users.users.${cfg.user};
message = "If `services.continuwuity.user` is changed, the configured user must already exist.";
}
{
assertion = cfg.group != defaultGroup -> config ? users.groups.${cfg.group};
message = "If `services.continuwuity.group` is changed, the configured group must already exist.";
}
];
users.users = lib.mkIf (cfg.user == defaultUser) {
${defaultUser} = {
group = cfg.group;
home = cfg.settings.global.database_path;
isSystemUser = true;
};
};
users.groups = lib.mkIf (cfg.group == defaultGroup) {
${defaultGroup} = { };
};
systemd.services.continuwuity = {
description = "Continuwuity Matrix Server";
documentation = [ "https://continuwuity.org/" ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = lib.mkMerge [
{ CONTINUWUITY_CONFIG = configFile; }
cfg.extraEnvironment
];
startLimitBurst = 5;
startLimitIntervalSec = 60;
serviceConfig = {
DynamicUser = true;
User = cfg.user;
Group = cfg.group;
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
PrivateIPC = true;
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service @resources"
"~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc"
];
SystemCallErrorNumber = "EPERM";
StateDirectory = "continuwuity";
StateDirectoryMode = "0700";
RuntimeDirectory = "continuwuity";
RuntimeDirectoryMode = "0750";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
RestartSec = 10;
};
};
};
}

View File

@@ -0,0 +1,345 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.dendrite;
settingsFormat = pkgs.formats.yaml { };
configurationYaml = settingsFormat.generate "dendrite.yaml" cfg.settings;
workingDir = "/var/lib/dendrite";
in
{
options.services.dendrite = {
enable = lib.mkEnableOption "matrix.org dendrite";
httpPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = 8008;
description = ''
The port to listen for HTTP requests on.
'';
};
httpsPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
description = ''
The port to listen for HTTPS requests on.
'';
};
tlsCert = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/server.cert";
default = null;
description = ''
The path to the TLS certificate.
```
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
```
'';
};
tlsKey = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/server.key";
default = null;
description = ''
The path to the TLS key.
```
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
```
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/var/lib/dendrite/registration_secret";
default = null;
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`.
Secrets may be passed to the service without adding them to the world-readable
Nix store, by specifying placeholder variables as the option value in Nix and
setting these variables accordingly in the environment file. Currently only used
for the registration secret to allow secure registration when
client_api.registration_disabled is true.
```
# snippet of dendrite-related config
services.dendrite.settings.client_api.registration_shared_secret = "$REGISTRATION_SHARED_SECRET";
```
```
# content of the environment file
REGISTRATION_SHARED_SECRET=verysecretpassword
```
Note that this file needs to be available on the host on which
`dendrite` is running.
'';
};
loadCredential = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "private_key:/path/to/my_private_key" ];
description = ''
This can be used to pass secrets to the systemd service without adding them to
the nix store.
To use the example setting, see the example of
{option}`services.dendrite.settings.global.private_key`.
See the LoadCredential section of systemd.exec manual for more information.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.global = {
server_name = lib.mkOption {
type = lib.types.str;
example = "example.com";
description = ''
The domain name of the server, with optional explicit port.
This is used by remote servers to connect to this server.
This is also the last part of your UserID.
'';
};
private_key = lib.mkOption {
type = lib.types.either lib.types.path (lib.types.strMatching "^\\$CREDENTIALS_DIRECTORY/.+");
example = "$CREDENTIALS_DIRECTORY/private_key";
description = ''
The path to the signing private key file, used to sign
requests and events.
```
nix-shell -p dendrite --command "generate-keys --private-key matrix_key.pem"
```
'';
};
trusted_third_party_id_servers = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ "matrix.org" ];
default = [
"matrix.org"
"vector.im"
];
description = ''
Lists of domains that the server will trust as identity
servers to verify third party identifiers such as phone
numbers and email addresses
'';
};
};
options.app_service_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:federationapi.db";
description = ''
Database for the Appservice API.
'';
};
};
options.client_api = {
registration_disabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to disable user registration to the server
without the shared secret.
'';
};
};
options.federation_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:federationapi.db";
description = ''
Database for the Federation API.
'';
};
};
options.key_server.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:keyserver.db";
description = ''
Database for the Key Server (for end-to-end encryption).
'';
};
};
options.relay_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:relayapi.db";
description = ''
Database for the Relay Server.
'';
};
};
options.media_api = {
database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:mediaapi.db";
description = ''
Database for the Media API.
'';
};
};
base_path = lib.mkOption {
type = lib.types.str;
default = "${workingDir}/media_store";
description = ''
Storage path for uploaded media.
'';
};
};
options.room_server.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:roomserver.db";
description = ''
Database for the Room Server.
'';
};
};
options.sync_api.database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:syncserver.db";
description = ''
Database for the Sync API.
'';
};
};
options.sync_api.search = {
enabled = lib.mkEnableOption "Dendrite's full-text search engine";
index_path = lib.mkOption {
type = lib.types.str;
default = "${workingDir}/searchindex";
description = ''
The path the search index will be created in.
'';
};
language = lib.mkOption {
type = lib.types.str;
default = "en";
description = ''
The language most likely to be used on the server - used when indexing, to
ensure the returned results match expectations. A full list of possible languages
can be found at <https://github.com/blevesearch/bleve/tree/master/analysis/lang>
'';
};
};
options.user_api = {
account_database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:userapi_accounts.db";
description = ''
Database for the User API, accounts.
'';
};
};
device_database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:userapi_devices.db";
description = ''
Database for the User API, devices.
'';
};
};
};
options.mscs = {
database = {
connection_string = lib.mkOption {
type = lib.types.str;
default = "file:mscs.db";
description = ''
Database for exerimental MSC's.
'';
};
};
};
};
default = { };
description = ''
Configuration for dendrite, see:
<https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.yaml>
for available options with which to populate settings.
'';
};
openRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow open registration without secondary verification (reCAPTCHA).
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.httpsPort != null -> (cfg.tlsCert != null && cfg.tlsKey != null);
message = ''
If Dendrite is configured to use https, tlsCert and tlsKey must be provided.
nix-shell -p dendrite --command "generate-keys --tls-cert server.crt --tls-key server.key"
'';
}
{
assertion = !(cfg.settings.sync_api.search ? enable);
message = ''
The `services.dendrite.settings.sync_api.search.enable` option
has been renamed to `services.dendrite.settings.sync_api.search.enabled`.
'';
}
];
systemd.services.dendrite = {
description = "Dendrite Matrix homeserver";
after = [
"network.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
DynamicUser = true;
StateDirectory = "dendrite";
WorkingDirectory = workingDir;
RuntimeDirectory = "dendrite";
RuntimeDirectoryMode = "0700";
LimitNOFILE = 65535;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
LoadCredential = cfg.loadCredential;
ExecStartPre = [
''
${pkgs.envsubst}/bin/envsubst \
-i ${configurationYaml} \
-o /run/dendrite/dendrite.yaml
''
];
ExecStart = lib.strings.concatStringsSep " " (
[
"${pkgs.dendrite}/bin/dendrite"
"--config /run/dendrite/dendrite.yaml"
]
++ lib.optionals (cfg.httpPort != null) [
"--http-bind-address :${builtins.toString cfg.httpPort}"
]
++ lib.optionals (cfg.httpsPort != null) [
"--https-bind-address :${builtins.toString cfg.httpsPort}"
"--tls-cert ${cfg.tlsCert}"
"--tls-key ${cfg.tlsKey}"
]
++ lib.optionals cfg.openRegistration [
"--really-enable-open-registration"
]
);
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
Restart = "on-failure";
};
};
};
meta.maintainers = lib.teams.matrix.members;
}

View File

@@ -0,0 +1,62 @@
# Draupnir (Matrix Moderation Bot) {#module-services-draupnir}
This chapter will show you how to set up your own, self-hosted
[Draupnir](https://github.com/the-draupnir-project/Draupnir) instance.
As an all-in-one moderation tool, it can protect your server from
malicious invites, spam messages, and whatever else you don't want.
In addition to server-level protection, Draupnir is great for communities
wanting to protect their rooms without having to use their personal
accounts for moderation.
The bot by default includes support for bans, redactions, anti-spam,
server ACLs, room directory changes, room alias transfers, account
deactivation, room shutdown, and more. (This depends on homeserver configuration and implementation.)
See the [README](https://github.com/the-draupnir-project/draupnir#readme)
page and the [Moderator's guide](https://the-draupnir-project.github.io/draupnir-documentation/moderator/setting-up-and-configuring)
for additional instructions on how to setup and use Draupnir.
For [additional settings](#opt-services.draupnir.settings)
see [the default configuration](https://github.com/the-draupnir-project/Draupnir/blob/main/config/default.yaml).
## Draupnir Setup {#module-services-draupnir-setup}
First create a new unencrypted, private room which will be used as the management room for Draupnir.
This is the room in which moderators will interact with Draupnir and where it will log possible errors and debugging information.
You'll need to set this room ID or alias in [services.draupnir.settings.managementRoom](#opt-services.draupnir.settings.managementRoom).
Next, create a new user for Draupnir on your homeserver, if one does not already exist.
The Draupnir Matrix user expects to be free of any rate limiting.
See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286)
for an example on how to achieve this.
If you want Draupnir to be able to deactivate users, move room aliases, shut down rooms, etc.
you'll need to make the Draupnir user a Matrix server admin.
Now invite the Draupnir user to the management room.
Draupnir will automatically try to join this room on startup.
```nix
{
services.draupnir = {
enable = true;
settings = {
homeserverUrl = "https://matrix.org";
managementRoom = "!yyy:example.org";
};
secrets = {
accessToken = "/path/to/secret/containing/access-token";
};
};
}
```
### Element Matrix Services (EMS) {#module-services-draupnir-setup-ems}
If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/)
server, you will need to consent to the terms and conditions. Upon startup, an error
log entry with a URL to the consent page will be generated.

View File

@@ -0,0 +1,257 @@
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.services.draupnir;
opt = options.services.draupnir;
format = pkgs.formats.yaml { };
configFile = format.generate "draupnir.yaml" cfg.settings;
inherit (lib)
literalExpression
mkEnableOption
mkOption
mkPackageOption
mkRemovedOptionModule
mkRenamedOptionModule
types
;
in
{
imports = [
# Removed options for those migrating from the Mjolnir module
(mkRenamedOptionModule
[ "services" "draupnir" "dataPath" ]
[ "services" "draupnir" "settings" "dataPath" ]
)
(mkRenamedOptionModule
[ "services" "draupnir" "homeserverUrl" ]
[ "services" "draupnir" "settings" "homeserverUrl" ]
)
(mkRenamedOptionModule
[ "services" "draupnir" "managementRoom" ]
[ "services" "draupnir" "settings" "managementRoom" ]
)
(mkRenamedOptionModule
[ "services" "draupnir" "accessTokenFile" ]
[ "services" "draupnir" "secrets" "accessToken" ]
)
(mkRemovedOptionModule [ "services" "draupnir" "pantalaimon" ] ''
`services.draupnir.pantalaimon.*` has been removed because it depends on the deprecated and vulnerable
libolm library for end-to-end encryption and upstream support for Pantalaimon in Draupnir is limited.
See <https://the-draupnir-project.github.io/draupnir-documentation/bot/encryption> for details.
If you nontheless require E2EE via Pantalaimon, you can configure `services.pantalaimon-headless.instances`
yourself and use that with `services.draupnir.settings.pantalaimon` and `services.draupnir.secrets.pantalaimon.password`.
'')
];
options.services.draupnir = {
enable = mkEnableOption "Draupnir, a moderations bot for Matrix";
package = mkPackageOption pkgs "draupnir" { };
settings = mkOption {
example = literalExpression ''
{
homeserverUrl = "https://matrix.org";
managementRoom = "#moderators:example.org";
autojoinOnlyIfManager = true;
automaticallyRedactForReasons = [ "spam" "advertising" ];
}
'';
description = ''
Free-form settings written to Draupnir's configuration file.
See [Draupnir's default configuration](https://github.com/the-draupnir-project/Draupnir/blob/main/config/default.yaml) for available settings.
'';
default = { };
type = types.submodule {
freeformType = format.type;
options = {
homeserverUrl = mkOption {
type = types.str;
example = "https://matrix.org";
description = ''
Base URL of the Matrix homeserver that provides the Client-Server API.
::: {.note}
When using Pantalaimon, set this to the Pantalaimon URL and
{option}`${opt.settings}.rawHomeserverUrl` to the public URL.
:::
'';
};
rawHomeserverUrl = mkOption {
type = types.str;
example = "https://matrix.org";
default = cfg.settings.homeserverUrl;
defaultText = literalExpression "config.${opt.settings}.homeserverUrl";
description = ''
Public base URL of the Matrix homeserver that provides the Client-Server API when using the Draupnir's
[Report forwarding feature](https://the-draupnir-project.github.io/draupnir-documentation/bot/homeserver-administration#report-forwarding).
::: {.warning}
When using Pantalaimon, do not set this to the Pantalaimon URL!
:::
'';
};
managementRoom = mkOption {
type = types.str;
example = "#moderators:example.org";
description = ''
The room ID or alias where moderators can use the bot's functionality.
The bot has no access controls, so anyone in this room can use the bot - secure this room!
Do not enable end-to-end encryption for this room, unless set up with Pantalaimon.
::: {.warning}
When using a room alias, make sure the alias used is on the local homeserver!
This prevents an issue where the control room becomes undefined when the alias can't be resolved.
:::
'';
};
dataPath = mkOption {
type = types.path;
readOnly = true;
default = "/var/lib/draupnir";
description = ''
The path Draupnir will store its state/data in.
::: {.warning}
This option is read-only.
:::
::: {.note}
If you want to customize where this data is stored, use a bind mount.
:::
'';
};
};
};
};
secrets = {
accessToken = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing the access token for Draupnir's Matrix account
to be used in place of {option}`${opt.settings}.accessToken`.
'';
};
pantalaimon.password = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing the password for Draupnir's Matrix account when used in
conjunction with Pantalaimon to be used in place of
{option}`${opt.settings}.pantalaimon.password`.
::: {.warning}
Take note that upstream has limited Pantalaimon and E2EE support:
<https://the-draupnir-project.github.io/draupnir-documentation/bot/encryption> and
<https://the-draupnir-project.github.io/draupnir-documentation/shared/dogfood#e2ee-support>.
:::
'';
};
web.synapseHTTPAntispam.authorization = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
File containing the secret token when using the Synapse HTTP Antispam module
to be used in place of
{option}`${opt.settings}.web.synapseHTTPAntispam.authorization`.
See <https://the-draupnir-project.github.io/draupnir-documentation/bot/synapse-http-antispam> for details.
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
# Removed option for those migrating from the Mjolnir module - mkRemovedOption module does *not* work with submodules.
assertion = !(cfg.settings ? protectedRooms);
message = "Unset ${opt.settings}.protectedRooms, as it is unsupported on Draupnir. Add these rooms via `!draupnir rooms add` instead.";
}
];
systemd.services.draupnir = {
description = "Draupnir - a moderation bot for Matrix";
wants = [
"network-online.target"
"matrix-synapse.service"
"conduit.service"
"dendrite.service"
];
after = [
"network-online.target"
"matrix-synapse.service"
"conduit.service"
"dendrite.service"
];
wantedBy = [ "multi-user.target" ];
startLimitIntervalSec = 0;
serviceConfig = {
ExecStart = toString (
[
(lib.getExe cfg.package)
"--draupnir-config"
configFile
]
++ lib.optionals (cfg.secrets.accessToken != null) [
"--access-token-path"
"%d/access_token"
]
++ lib.optionals (cfg.secrets.pantalaimon.password != null) [
"--pantalaimon-password-path"
"%d/pantalaimon_password"
]
++ lib.optionals (cfg.secrets.web.synapseHTTPAntispam.authorization != null) [
"--http-antispam-authorization-path"
"%d/http_antispam_authorization"
]
);
WorkingDirectory = "/var/lib/draupnir";
StateDirectory = "draupnir";
StateDirectoryMode = "0700";
ProtectHome = true;
PrivateDevices = true;
Restart = "on-failure";
RestartSec = "5s";
DynamicUser = true;
LoadCredential =
lib.optionals (cfg.secrets.accessToken != null) [
"access_token:${cfg.secrets.accessToken}"
]
++ lib.optionals (cfg.secrets.pantalaimon.password != null) [
"pantalaimon_password:${cfg.secrets.pantalaimon.password}"
]
++ lib.optionals (cfg.secrets.web.synapseHTTPAntispam.authorization != null) [
"http_antispam_authorization:${cfg.secrets.web.synapseHTTPAntispam.authorization}"
];
};
};
};
meta = {
doc = ./draupnir.md;
maintainers = with lib.maintainers; [
RorySys
emilylange
];
};
}

View File

@@ -0,0 +1,86 @@
{
lib,
config,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkOption
mkIf
types
;
format = pkgs.formats.toml { };
cfg = config.services.hebbot;
settingsFile = format.generate "config.toml" cfg.settings;
mkTemplateOption =
templateName:
mkOption {
type = types.path;
description = ''
A path to the Markdown file for the ${templateName}.
'';
};
in
{
meta.maintainers = [ lib.maintainers.raitobezarius ];
options.services.hebbot = {
enable = mkEnableOption "hebbot";
package = lib.mkPackageOption pkgs "hebbot" { };
botPasswordFile = mkOption {
type = types.path;
description = ''
A path to the password file for your bot.
Consider using a path that does not end up in your Nix store
as it would be world readable.
'';
};
templates = {
project = mkTemplateOption "project template";
report = mkTemplateOption "report template";
section = mkTemplateOption "section template";
};
settings = mkOption {
type = format.type;
default = { };
description = ''
Configuration for Hebbot, see, for examples:
- <https://github.com/matrix-org/twim-config/blob/master/config.toml>
- <https://gitlab.gnome.org/Teams/Websites/thisweek.gnome.org/-/blob/main/hebbot/config.toml>
'';
};
};
config = mkIf cfg.enable {
systemd.services.hebbot = {
description = "hebbot - a TWIM-style Matrix bot written in Rust";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
ln -sf ${cfg.templates.project} ./project_template.md
ln -sf ${cfg.templates.report} ./report_template.md
ln -sf ${cfg.templates.section} ./section_template.md
ln -sf ${settingsFile} ./config.toml
'';
script = ''
export BOT_PASSWORD="$(cat $CREDENTIALS_DIRECTORY/bot-password-file)"
${lib.getExe cfg.package}
'';
serviceConfig = {
DynamicUser = true;
Restart = "on-failure";
LoadCredential = "bot-password-file:${cfg.botPasswordFile}";
RestartSec = "10s";
StateDirectory = "hebbot";
WorkingDirectory = "/var/lib/hebbot";
};
};
};
}

View File

@@ -0,0 +1,127 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.matrix-hookshot;
settingsFormat = pkgs.formats.yaml { };
configFile = settingsFormat.generate "matrix-hookshot-config.yml" cfg.settings;
in
{
options = {
services.matrix-hookshot = {
enable = lib.mkEnableOption "matrix-hookshot, a bridge between Matrix and project management services";
package = lib.mkPackageOption pkgs "matrix-hookshot" { };
registrationFile = lib.mkOption {
type = lib.types.path;
description = ''
Appservice registration file.
As it contains secret tokens, you may not want to add this to the publicly readable Nix store.
'';
example = lib.literalExpression ''
pkgs.writeText "matrix-hookshot-registration" \'\'
id: matrix-hookshot
as_token: aaaaaaaaaa
hs_token: aaaaaaaaaa
namespaces:
rooms: []
users:
- regex: "@_webhooks_.*:foobar"
exclusive: true
sender_localpart: hookshot
url: "http://localhost:9993"
rate_limited: false
\'\'
'';
};
settings = lib.mkOption {
description = ''
{file}`config.yml` configuration as a Nix attribute set.
For details please see the [documentation](https://matrix-org.github.io/matrix-hookshot/latest/setup/sample-configuration.html).
'';
example = {
bridge = {
domain = "example.com";
url = "http://localhost:8008";
mediaUrl = "https://example.com";
port = 9993;
bindAddress = "127.0.0.1";
};
listeners = [
{
port = 9000;
bindAddress = "0.0.0.0";
resources = [ "webhooks" ];
}
{
port = 9001;
bindAddress = "localhost";
resources = [
"metrics"
"provisioning"
];
}
];
};
default = { };
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
passFile = lib.mkOption {
type = lib.types.path;
default = "/var/lib/matrix-hookshot/passkey.pem";
description = ''
A passkey used to encrypt tokens stored inside the bridge.
File will be generated if not found.
'';
};
};
};
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
defaultText = lib.literalExpression ''
lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
'';
description = ''
List of Systemd services to require and wait for when starting the application service,
such as the Matrix homeserver if it's running on the same host.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.matrix-hookshot = {
description = "a bridge between Matrix and multiple project management services";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
if [ ! -f '${cfg.settings.passFile}' ]; then
mkdir -p $(dirname '${cfg.settings.passFile}')
${pkgs.openssl}/bin/openssl genpkey -out '${cfg.settings.passFile}' -outform PEM -algorithm RSA -pkeyopt rsa_keygen_bits:4096
fi
'';
serviceConfig = {
Type = "simple";
Restart = "always";
ExecStart = "${cfg.package}/bin/matrix-hookshot ${configFile} ${cfg.registrationFile}";
};
};
};
meta.maintainers = with lib.maintainers; [ flandweber ];
}

View File

@@ -0,0 +1,91 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.lk-jwt-service;
in
{
meta.maintainers = [ lib.maintainers.quadradical ];
options.services.lk-jwt-service = {
enable = lib.mkEnableOption "lk-jwt-service";
package = lib.mkPackageOption pkgs "lk-jwt-service" { };
livekitUrl = lib.mkOption {
type = lib.types.strMatching "^wss?://.*";
example = "wss://example.com/livekit/sfu";
description = ''
The public websocket URL for livekit.
The proto needs to be either `wss://` (recommended) or `ws://` (insecure).
'';
};
keyFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to a file containing the credential mapping (`<keyname>: <secret>`) to access LiveKit.
Example:
`lk-jwt-service: f6lQGaHtM5HfgZjIcec3cOCRfiDqIine4CpZZnqdT5cE`
For more information, see <https://github.com/element-hq/lk-jwt-service#configuration>.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Port that lk-jwt-service should listen on.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.lk-jwt-service = {
description = "Minimal service to issue LiveKit JWTs for MatrixRTC";
documentation = [ "https://github.com/element-hq/lk-jwt-service" ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = {
LIVEKIT_URL = cfg.livekitUrl;
LIVEKIT_JWT_PORT = toString cfg.port;
LIVEKIT_KEY_FILE = "/run/credentials/lk-jwt-service.service/livekit-secrets";
};
serviceConfig = {
LoadCredential = [ "livekit-secrets:${cfg.keyFile}" ];
ExecStart = lib.getExe cfg.package;
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
ProtectHome = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
Restart = "on-failure";
RestartSec = 5;
UMask = "077";
};
};
};
}

View File

@@ -0,0 +1,127 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.matrix-alertmanager;
rooms = room: lib.concatStringsSep "/" (room.receivers ++ [ room.roomId ]);
concatenatedRooms = lib.concatStringsSep "|" (map rooms cfg.matrixRooms);
in
{
meta.maintainers = [ lib.maintainers.erethon ];
options.services.matrix-alertmanager = {
enable = lib.mkEnableOption "matrix-alertmanager";
package = lib.mkPackageOption pkgs "matrix-alertmanager" { };
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "Port that matrix-alertmanager listens on.";
};
homeserverUrl = lib.mkOption {
type = lib.types.str;
description = "URL of the Matrix homeserver to use.";
example = "https://matrix.example.com";
};
matrixUser = lib.mkOption {
type = lib.types.str;
description = "Matrix user to use for the bot.";
example = "@alertmanageruser:example.com";
};
matrixRooms = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
receivers = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of receivers for this room";
};
roomId = lib.mkOption {
type = lib.types.str;
description = "Matrix room ID";
apply =
x:
assert lib.assertMsg (lib.hasPrefix "!" x) "Matrix room ID must start with a '!'. Got: ${x}";
x;
};
};
}
);
description = ''
Combination of Alertmanager receiver(s) and rooms for the bot to join.
Each Alertmanager receiver can be mapped to post to a matrix room.
Note, you must use a room ID and not a room alias/name. Room IDs start
with a "!".
'';
example = [
{
receivers = [
"receiver1"
"receiver2"
];
roomId = "!roomid@example.com";
}
{
receivers = [ "receiver3" ];
roomId = "!differentroomid@example.com";
}
];
};
mention = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Makes the bot mention @room when posting an alert";
};
tokenFile = lib.mkOption {
type = lib.types.pathWith {
inStore = false;
absolute = true;
};
description = "File that contains a valid Matrix token for the Matrix user.";
};
secretFile = lib.mkOption {
type = lib.types.pathWith {
inStore = false;
absolute = true;
};
description = "File that contains a secret for the Alertmanager webhook.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.matrix-alertmanager = {
description = "A bot to receive Alertmanager webhook events and forward them to chosen rooms.";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
Restart = "always";
RestartSec = "10s";
LoadCredential = [
"token:${cfg.tokenFile}"
"secret:${cfg.secretFile}"
];
};
environment = {
APP_PORT = toString cfg.port;
MATRIX_HOMESERVER_URL = cfg.homeserverUrl;
MATRIX_ROOMS = concatenatedRooms;
MATRIX_USER = cfg.matrixUser;
MENTION_ROOM = if cfg.mention then "1" else "0";
NODE_ENV = "production";
};
script = ''
# shellcheck disable=SC2155
export APP_ALERTMANAGER_SECRET=$(cat "''${CREDENTIALS_DIRECTORY}/secret")
# shellcheck disable=SC2155
export MATRIX_TOKEN=$(cat "''${CREDENTIALS_DIRECTORY}/token")
exec ${lib.getExe cfg.package}
'';
};
};
}

View File

@@ -0,0 +1,107 @@
# Maubot {#module-services-maubot}
[Maubot](https://github.com/maubot/maubot) is a plugin-based bot
framework for Matrix.
## Configuration {#module-services-maubot-configuration}
1. Set [](#opt-services.maubot.enable) to `true`. The service will use
SQLite by default.
2. If you want to use PostgreSQL instead of SQLite, do this:
```nix
{ services.maubot.settings.database = "postgresql://maubot@localhost/maubot"; }
```
If the PostgreSQL connection requires a password, you will have to
add it later on step 8.
3. If you plan to expose your Maubot interface to the web, do something
like this:
```nix
{
services.nginx.virtualHosts."matrix.example.org".locations = {
"/_matrix/maubot/" = {
proxyPass = "http://127.0.0.1:${toString config.services.maubot.settings.server.port}";
proxyWebsockets = true;
};
};
services.maubot.settings.server.public_url = "matrix.example.org";
# do the following only if you want to use something other than /_matrix/maubot...
services.maubot.settings.server.ui_base_path = "/another/base/path";
}
```
4. Optionally, set `services.maubot.pythonPackages` to a list of python3
packages to make available for Maubot plugins.
5. Optionally, set `services.maubot.plugins` to a list of Maubot
plugins (full list available at <https://plugins.maubot.xyz/>):
```nix
{
services.maubot.plugins = with config.services.maubot.package.plugins; [
reactbot
# This will only change the default config! After you create a
# plugin instance, the default config will be copied into that
# instance's config in Maubot's database, and further base config
# changes won't affect the running plugin.
(rss.override {
base_config = {
update_interval = 60;
max_backoff = 7200;
spam_sleep = 2;
command_prefix = "rss";
admins = [ "@chayleaf:pavluk.org" ];
};
})
];
# ...or...
services.maubot.plugins = config.services.maubot.package.plugins.allOfficialPlugins;
# ...or...
services.maubot.plugins = config.services.maubot.package.plugins.allPlugins;
# ...or...
services.maubot.plugins = with config.services.maubot.package.plugins; [
(weather.override {
# you can pass base_config as a string
base_config = ''
default_location: New York
default_units: M
default_language:
show_link: true
show_image: false
'';
})
];
}
```
6. Start Maubot at least once before doing the following steps (it's
necessary to generate the initial config).
7. If your PostgreSQL connection requires a password, add
`database: postgresql://user:password@localhost/maubot`
to `/var/lib/maubot/config.yaml`. This overrides the Nix-provided
config. Even then, don't remove the `database` line from Nix config
so the module knows you use PostgreSQL!
8. To create a user account for logging into Maubot web UI and
configuring it, generate a password using the shell command
`mkpasswd -R 12 -m bcrypt`, and edit `/var/lib/maubot/config.yaml`
with the following:
```yaml
admins:
admin_username: $2b$12$g.oIStUeUCvI58ebYoVMtO/vb9QZJo81PsmVOomHiNCFbh0dJpZVa
```
Where `admin_username` is your username, and `$2b...` is the bcrypted
password.
9. Optional: if you want to be able to register new users with the
Maubot CLI (`mbc`), and your homeserver is private, add your
homeserver's registration key to `/var/lib/maubot/config.yaml`:
```yaml
homeservers:
matrix.example.org:
url: https://matrix.example.org
secret: your-very-secret-key
```
10. Restart Maubot after editing `/var/lib/maubot/config.yaml`, and
Maubot will be available at
`https://matrix.example.org/_matrix/maubot`. If you want to use the
`mbc` CLI, it's available using the `maubot` package (`nix-shell -p
maubot`).

View File

@@ -0,0 +1,472 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.maubot;
wrapper1 = if cfg.plugins == [ ] then cfg.package else cfg.package.withPlugins (_: cfg.plugins);
wrapper2 =
if cfg.pythonPackages == [ ] then wrapper1 else wrapper1.withPythonPackages (_: cfg.pythonPackages);
settings = lib.recursiveUpdate cfg.settings {
plugin_directories.trash =
if cfg.settings.plugin_directories.trash == null then
"delete"
else
cfg.settings.plugin_directories.trash;
server.unshared_secret = "generate";
};
finalPackage = wrapper2.withBaseConfig settings;
isPostgresql = db: builtins.isString db && lib.hasPrefix "postgresql://" db;
isLocalPostgresDB =
db:
isPostgresql db
&& builtins.any (x: lib.hasInfix x db) [
"@127.0.0.1/"
"@::1/"
"@[::1]/"
"@localhost/"
];
parsePostgresDB =
db:
let
noSchema = lib.removePrefix "postgresql://" db;
in
{
username = builtins.head (lib.splitString "@" noSchema);
database = lib.last (lib.splitString "/" noSchema);
};
postgresDBs = builtins.filter isPostgresql [
cfg.settings.database
cfg.settings.crypto_database
cfg.settings.plugin_databases.postgres
];
localPostgresDBs = builtins.filter isLocalPostgresDB postgresDBs;
parsedLocalPostgresDBs = map parsePostgresDB localPostgresDBs;
parsedPostgresDBs = map parsePostgresDB postgresDBs;
hasLocalPostgresDB = localPostgresDBs != [ ];
in
{
options.services.maubot = with lib; {
enable = mkEnableOption "maubot";
package = lib.mkPackageOption pkgs "maubot" { };
plugins = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with config.services.maubot.package.plugins; [
xyz.maubot.reactbot
xyz.maubot.rss
];
'';
description = ''
List of additional maubot plugins to make available.
'';
};
pythonPackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression ''
with pkgs.python3Packages; [
aiohttp
];
'';
description = ''
List of additional Python packages to make available for maubot.
'';
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/maubot";
description = ''
The directory where maubot stores its stateful data.
'';
};
extraConfigFile = mkOption {
type = types.str;
default = "./config.yaml";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/config.yaml"'';
description = ''
A file for storing secrets. You can pass homeserver registration keys here.
If it already exists, **it must contain `server.unshared_secret`** which is used for signing API keys.
If `configMutable` is not set to true, **maubot user must have write access to this file**.
'';
};
configMutable = mkOption {
type = types.bool;
default = false;
description = ''
Whether maubot should write updated config into `extraConfigFile`. **This will make your Nix module settings have no effect besides the initial config, as extraConfigFile takes precedence over NixOS settings!**
'';
};
settings = mkOption {
default = { };
description = ''
YAML settings for maubot. See the
[example configuration](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml)
for more info.
Secrets should be passed in by using `extraConfigFile`.
'';
type =
with types;
submodule {
options = {
database = mkOption {
type = str;
default = "sqlite:maubot.db";
example = "postgresql://username:password@hostname/dbname";
description = ''
The full URI to the database. SQLite and Postgres are fully supported.
Other DBMSes supported by SQLAlchemy may or may not work.
'';
};
crypto_database = mkOption {
type = str;
default = "default";
example = "postgresql://username:password@hostname/dbname";
description = ''
Separate database URL for the crypto database. By default, the regular database is also used for crypto.
'';
};
database_opts = mkOption {
type = types.attrs;
default = { };
description = ''
Additional arguments for asyncpg.create_pool() or sqlite3.connect()
'';
};
plugin_directories = mkOption {
default = { };
description = "Plugin directory paths";
type = submodule {
options = {
upload = mkOption {
type = types.str;
default = "./plugins";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
description = ''
The directory where uploaded new plugins should be stored.
'';
};
load = mkOption {
type = types.listOf types.str;
default = [ "./plugins" ];
defaultText = literalExpression ''[ "''${config.services.maubot.dataDir}/plugins" ]'';
description = ''
The directories from which plugins should be loaded. Duplicate plugin IDs will be moved to the trash.
'';
};
trash = mkOption {
type = with types; nullOr str;
default = "./trash";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/trash"'';
description = ''
The directory where old plugin versions and conflicting plugins should be moved. Set to null to delete files immediately.
'';
};
};
};
};
plugin_databases = mkOption {
description = "Plugin database settings";
default = { };
type = submodule {
options = {
sqlite = mkOption {
type = types.str;
default = "./plugins";
defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
description = ''
The directory where SQLite plugin databases should be stored.
'';
};
postgres = mkOption {
type = types.nullOr types.str;
default = if isPostgresql cfg.settings.database then "default" else null;
defaultText = literalExpression ''if isPostgresql config.services.maubot.settings.database then "default" else null'';
description = ''
The connection URL for plugin database. See [example config](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml) for exact format.
'';
};
postgres_max_conns_per_plugin = mkOption {
type = types.nullOr types.int;
default = 3;
description = ''
Maximum number of connections per plugin instance.
'';
};
postgres_opts = mkOption {
type = types.attrs;
default = { };
description = ''
Overrides for the default database_opts when using a non-default postgres connection URL.
'';
};
};
};
};
server = mkOption {
default = { };
description = "Listener config";
type = submodule {
options = {
hostname = mkOption {
type = types.str;
default = "127.0.0.1";
description = ''
The IP to listen on
'';
};
port = mkOption {
type = types.port;
default = 29316;
description = ''
The port to listen on
'';
};
public_url = mkOption {
type = types.str;
default = "http://${cfg.settings.server.hostname}:${toString cfg.settings.server.port}";
defaultText = literalExpression ''"http://''${config.services.maubot.settings.server.hostname}:''${toString config.services.maubot.settings.server.port}"'';
description = ''
Public base URL where the server is visible.
'';
};
ui_base_path = mkOption {
type = types.str;
default = "/_matrix/maubot";
description = ''
The base path for the UI.
'';
};
plugin_base_path = mkOption {
type = types.str;
default = "${config.services.maubot.settings.server.ui_base_path}/plugin/";
defaultText = literalExpression ''
"''${config.services.maubot.settings.server.ui_base_path}/plugin/"
'';
description = ''
The base path for plugin endpoints. The instance ID will be appended directly.
'';
};
override_resource_path = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Override path from where to load UI resources.
'';
};
};
};
};
homeservers = mkOption {
type = types.attrsOf (
types.submodule {
options = {
url = mkOption {
type = types.str;
description = ''
Client-server API URL
'';
};
};
}
);
default = {
"matrix.org" = {
url = "https://matrix-client.matrix.org";
};
};
description = ''
Known homeservers. This is required for the `mbc auth` command and also allows more convenient access from the management UI.
If you want to specify registration secrets, pass this via extraConfigFile instead.
'';
};
admins = mkOption {
type = types.attrsOf types.str;
default = {
root = "";
};
description = ''
List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password
to prevent normal login. Root is a special user that can't have a password and will always exist.
'';
};
api_features = mkOption {
type = types.attrsOf bool;
default = {
login = true;
plugin = true;
plugin_upload = true;
instance = true;
instance_database = true;
client = true;
client_proxy = true;
client_auth = true;
dev_open = true;
log = true;
};
description = ''
API feature switches.
'';
};
logging = mkOption {
type = types.attrs;
description = ''
Python logging configuration. See [section 16.7.2 of the Python
documentation](https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema)
for more info.
'';
default = {
version = 1;
formatters = {
colored = {
"()" = "maubot.lib.color_log.ColorFormatter";
format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
};
normal = {
format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
};
};
handlers = {
file = {
class = "logging.handlers.RotatingFileHandler";
formatter = "normal";
filename = "./maubot.log";
maxBytes = 10485760;
backupCount = 10;
};
console = {
class = "logging.StreamHandler";
formatter = "colored";
};
};
loggers = {
maubot = {
level = "DEBUG";
};
mau = {
level = "DEBUG";
};
aiohttp = {
level = "INFO";
};
};
root = {
level = "DEBUG";
handlers = [
"file"
"console"
];
};
};
};
};
};
};
};
config = lib.mkIf cfg.enable {
warnings = lib.optional (builtins.any (x: x.username != x.database) parsedLocalPostgresDBs) ''
The Maubot database username doesn't match the database name! This means the user won't be automatically
granted ownership of the database. Consider changing either the username or the database name.
'';
assertions = [
{
assertion = builtins.all (x: !lib.hasInfix ":" x.username) parsedPostgresDBs;
message = ''
Putting database passwords in your Nix config makes them world-readable. To securely put passwords
in your Maubot config, change /var/lib/maubot/config.yaml after running Maubot at least once as
described in the NixOS manual.
'';
}
{
assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
message = ''
Cannot deploy maubot with a configuration for a local postgresql database and a missing postgresql service.
'';
}
];
services.postgresql = lib.mkIf hasLocalPostgresDB {
enable = true;
ensureDatabases = map (x: x.database) parsedLocalPostgresDBs;
ensureUsers = lib.flip map parsedLocalPostgresDBs (x: {
name = x.username;
ensureDBOwnership = lib.mkIf (x.username == x.database) true;
});
};
users.users.maubot = {
group = "maubot";
home = cfg.dataDir;
# otherwise StateDirectory is enough
createHome = lib.mkIf (cfg.dataDir != "/var/lib/maubot") true;
isSystemUser = true;
};
users.groups.maubot = { };
systemd.services.maubot = rec {
description = "maubot - a plugin-based Matrix bot system written in Python";
after = [ "network.target" ] ++ wants ++ lib.optional hasLocalPostgresDB "postgresql.target";
# all plugins get automatically disabled if maubot starts before synapse
wants = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
wantedBy = [ "multi-user.target" ];
preStart = ''
if [ ! -f "${cfg.extraConfigFile}" ]; then
echo "server:" > "${cfg.extraConfigFile}"
echo " unshared_secret: $(head -c40 /dev/random | base32 | ${pkgs.gawk}/bin/awk '{print tolower($0)}')" > "${cfg.extraConfigFile}"
chmod 640 "${cfg.extraConfigFile}"
fi
'';
serviceConfig = {
ExecStart =
"${finalPackage}/bin/maubot --config ${cfg.extraConfigFile}"
+ lib.optionalString (!cfg.configMutable) " --no-update";
User = "maubot";
Group = "maubot";
Restart = "on-failure";
RestartSec = "10s";
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/maubot") "maubot";
WorkingDirectory = cfg.dataDir;
};
};
};
meta.maintainers = with lib.maintainers; [ chayleaf ];
meta.doc = ./maubot.md;
}

View File

@@ -0,0 +1,559 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.mautrix-discord;
dataDir = cfg.dataDir;
format = pkgs.formats.yaml { };
registrationFile = "${dataDir}/discord-registration.yaml";
settingsFile = "${dataDir}/config.yaml";
settingsFileUnformatted = format.generate "discord-config-unsubstituted.yaml" cfg.settings;
in
{
options = {
services.mautrix-discord = {
enable = lib.mkEnableOption "Mautrix-Discord, a Matrix-Discord puppeting/relay-bot bridge";
package = lib.mkPackageOption pkgs "mautrix-discord" { };
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 = ''
fullDataDiration.
See [example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml)
for more information.
'';
};
appservice = lib.mkOption {
type = lib.types.attrs;
default = {
address = "http://localhost:29334";
hostname = "0.0.0.0";
port = 29334;
database = {
type = "sqlite3";
uri = "file:/var/lib/mautrix-discord/mautrix-discord.db?_txlock=immediate";
max_open_conns = 20;
max_idle_conns = 2;
max_conn_idle_time = null;
max_conn_lifetime = null;
};
id = "discord";
bot = {
username = "discordbot";
displayname = "Discord bridge bot";
avatar = "mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC";
};
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";
};
defaultText = lib.literalExpression ''
{
address = "http://localhost:29334";
hostname = "0.0.0.0";
port = 29334;
database = {
type = "sqlite3";
uri = "file:''${config.services.mautrix-discord.dataDir}/mautrix-discord.db?_txlock=immediate";
max_open_conns = 20;
max_idle_conns = 2;
max_conn_idle_time = null;
max_conn_lifetime = null;
};
id = "discord";
bot = {
username = "discordbot";
displayname = "Discord bridge bot";
avatar = "mxc://maunium.net/nIdEykemnwdisvHbpxflpDlC";
};
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 [example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml)
for more information.
'';
};
bridge = lib.mkOption {
type = lib.types.attrs;
default = {
username_template = "discord_{{.}}";
displayname_template = "{{if .Webhook}}Webhook{{else}}{{or .GlobalName .Username}}{{if .Bot}} (bot){{end}}{{end}}";
channel_name_template = "{{if or (eq .Type 3) (eq .Type 4)}}{{.Name}}{{else}}#{{.Name}}{{end}}";
guild_name_template = "{{.Name}}";
private_chat_portal_meta = "default";
public_address = null;
avatar_proxy_key = "generate";
portal_message_buffer = 128;
startup_private_channel_create_limit = 5;
delivery_receipts = false;
message_status_events = false;
message_error_notices = true;
restricted_rooms = true;
autojoin_thread_on_open = true;
embed_fields_as_tables = true;
mute_channels_on_create = false;
sync_direct_chat_list = false;
resend_bridge_info = false;
custom_emoji_reactions = true;
delete_portal_on_channel_delete = false;
delete_guild_on_leave = true;
federate_rooms = true;
prefix_webhook_messages = true;
enable_webhook_avatars = false;
use_discord_cdn_upload = true;
#proxy =
cache_media = "unencrypted";
direct_media = {
enabled = false;
#server_name = "discord-media.example.com";
#well_known_response =
allow_proxy = true;
server_key = "generate";
};
animated_sticker = {
target = "webp";
args = {
width = 320;
height = 320;
fps = 25;
};
};
double_puppet_server_map = {
#"example.com" = "https://example.com";
};
double_puppet_allow_discovery = false;
login_shared_secret_map = {
#"example.com" = "foobar";
};
command_prefix = "!discord";
management_room_text = {
welcome = "Hello, I'm a Discord bridge bot.";
welcome_connected = "Use `help` for help.";
welcome_unconnected = "Use `help` for help or `login` to log in.";
additional_help = "";
};
backfill = {
forward_limits = {
initial = {
dm = 0;
channel = 0;
thread = 0;
};
missed = {
dm = 0;
channel = 0;
thread = 0;
};
max_guild_members = -1;
};
};
encryption = {
allow = false;
default = false;
appservice = false;
msc4190 = 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";
#"example.com" = "user";
#"@admin:example.com": "admin";
};
};
description = ''
Bridge configuration.
See [example-config.yaml](https://github.com/mautrix/discord/blob/main/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 [example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml)
for more information.
'';
};
};
};
default = { };
example = lib.literalExpression ''
{
homeserver = {
address = "http://localhost:8008";
domain = "public-domain.tld";
};
appservice.public = {
prefix = "/public";
external = "https://public-appservice-address/public";
};
bridge.permissions = {
"example.com" = "user";
"@admin:example.com" = "admin";
};
}
'';
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in
[example-config.yaml](https://github.com/mautrix/discord/blob/main/example-config.yaml).
'';
};
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-discord";
defaultText = "/var/lib/mautrix-discord";
description = ''
Directory to store the bridge's configuration and database files.
This directory will be created if it does not exist.
'';
};
# TODO: Get upstream to add an environment File option. Refer to https://github.com/NixOS/nixpkgs/pull/404871#issuecomment-2895663652 and https://github.com/mautrix/discord/issues/187
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-discord.dataDir`.
Can be used for storing the secrets without making them available in the Nix store.
For example, you can set `services.mautrix-discord.settings.appservice.as_token = "$MAUTRIX_DISCORD_APPSERVICE_AS_TOKEN"`
and then specify `MAUTRIX_DISCORD_APPSERVICE_AS_TOKEN="{token}"` in the environment file.
This value will get substituted into the configuration file as a token.
'';
};
serviceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "mautrix-discord.service";
description = ''
The systemd unit (a service or a target) for other services to depend on if they
need to be started after matrix-synapse.
This option is useful as the actual parent unit for all matrix-synapse processes
changes when configuring workers.
'';
};
registrationServiceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
default = "mautrix-discord-registration.service";
description = ''
The registration service that generates the registration file.
Systemd unit (a service or a target) for other services to depend on if they
need to be started after mautrix-discord registration service.
This option is useful as the actual parent unit for all matrix-synapse processes
changes when configuring workers.
'';
};
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.enable "matrix-conduit.service")
++ (lib.lists.optional config.services.dendrite.enable "dendrite.service");
defaultText = ''
[ cfg.registrationServiceUnit ] ++
(lib.lists.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit) ++
(lib.lists.optional config.services.matrix-conduit.enable "matrix-conduit.service") ++
(lib.lists.optional 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-discord.settings.homeserver.domain` and
`services.mautrix-discord.settings.homeserver.address` have to be set.
'';
}
{
assertion = cfg.settings.bridge.permissions or { } != { };
message = ''
The option `services.mautrix-discord.settings.bridge.permissions` has to be set.
'';
}
];
users.users.mautrix-discord = {
isSystemUser = true;
group = "mautrix-discord";
extraGroups = [ "mautrix-discord-registration" ];
home = dataDir;
description = "Mautrix-Discord bridge user";
};
users.groups.mautrix-discord = { };
users.groups.mautrix-discord-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-discord mautrix-discord -"
];
systemd.services = {
matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-discord-registration" ];
# Make synapse depend on the registration service when auto-registering
wants = [ "mautrix-discord-registration.service" ];
after = [ "mautrix-discord-registration.service" ];
};
mautrix-discord-registration = {
description = "Mautrix-Discord 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-discord \
--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 --generate-registration does not affect config.yaml
cp '${settingsFile}' '${settingsFile}.tmp'
echo "Generating registration file"
mautrix-discord \
--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-discord-registration '${registrationFile}'
chmod 640 '${registrationFile}'
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
UMask = 27;
User = "mautrix-discord";
Group = "mautrix-discord";
SystemCallFilter = [ "@system-service" ];
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ dataDir ];
StateDirectory = "mautrix-discord";
EnvironmentFile = cfg.environmentFile;
};
restartTriggers = [ settingsFileUnformatted ];
};
mautrix-discord = {
description = "Mautrix-Discord, a Matrix-Discord puppeting/relaybot bridge";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
path = [
pkgs.lottieconverter
pkgs.ffmpeg-headless
];
serviceConfig = {
Type = "simple";
User = "mautrix-discord";
Group = "mautrix-discord";
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 ];
};
};
meta = {
maintainers = with lib.maintainers; [
mistyttm
];
};
};
}

View File

@@ -0,0 +1,623 @@
{
config,
pkgs,
lib,
...
}:
let
settingsFormat = pkgs.formats.yaml { };
upperConfig = config;
cfg = config.services.mautrix-meta;
upperCfg = cfg;
fullDataDir = cfg: "/var/lib/${cfg.dataDir}";
settingsFile = cfg: "${fullDataDir cfg}/config.yaml";
settingsFileUnsubstituted = cfg: settingsFormat.generate "mautrix-meta-config.yaml" cfg.settings;
metaName = name: "mautrix-meta-${name}";
enabledInstances = lib.filterAttrs (
name: config: config.enable
) config.services.mautrix-meta.instances;
registerToSynapseInstances = lib.filterAttrs (
name: config: config.enable && config.registerToSynapse
) config.services.mautrix-meta.instances;
in
{
options = {
services.mautrix-meta = {
package = lib.mkPackageOption pkgs "mautrix-meta" { };
instances = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
options = {
enable = lib.mkEnableOption "Mautrix-Meta, a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge";
dataDir = lib.mkOption {
type = lib.types.str;
default = metaName name;
description = ''
Path to the directory with database, registration, and other data for the bridge service.
This path is relative to `/var/lib`, it cannot start with `../` (it cannot be outside of `/var/lib`).
'';
};
registrationFile = lib.mkOption {
type = lib.types.path;
readOnly = true;
description = ''
Path to the yaml registration file of the appservice.
'';
};
registerToSynapse = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to add registration file to `services.matrix-synapse.settings.app_service_config_files` and
make Synapse wait for registration service.
'';
};
settings = lib.mkOption rec {
apply = lib.recursiveUpdate default;
inherit (settingsFormat) type;
default = {
homeserver = {
software = "standard";
domain = "";
address = "";
};
appservice = {
id = "";
bot = {
username = "";
};
hostname = "localhost";
port = 29319;
address = "http://${config.settings.appservice.hostname}:${toString config.settings.appservice.port}";
};
bridge = {
permissions = { };
};
database = {
type = "sqlite3-fk-wal";
uri = "file:${fullDataDir config}/mautrix-meta.db?_txlock=immediate";
};
# Enable encryption by default to make the bridge more secure
encryption = {
allow = true;
default = true;
require = true;
# Recommended options from mautrix documentation
# for additional security.
delete_keys = {
dont_store_outbound = true;
ratchet_on_decrypt = true;
delete_fully_used_on_decrypt = true;
delete_prev_on_new_session = true;
delete_on_device_delete = true;
periodically_delete_expired = true;
delete_outdated_inbound = true;
};
# TODO: This effectively disables encryption. But this is the value provided when a <0.4 config is migrated. Changing it will corrupt the database.
# https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L24
# If you wish to encrypt the local database you should set this to an environment variable substitution and reset the bridge or somehow migrate the DB.
pickle_key = "mautrix.bridge.e2ee";
verification_levels = {
receive = "cross-signed-tofu";
send = "cross-signed-tofu";
share = "cross-signed-tofu";
};
};
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
};
network = {
mode = "";
};
};
defaultText = ''
{
homeserver = {
software = "standard";
address = "https://''${config.settings.homeserver.domain}";
};
appservice = {
database = {
type = "sqlite3-fk-wal";
uri = "file:''${fullDataDir config}/mautrix-meta.db?_txlock=immediate";
};
hostname = "localhost";
port = 29319;
address = "http://''${config.settings.appservice.hostname}:''${toString config.settings.appservice.port}";
};
bridge = {
# Require encryption by default to make the bridge more secure
encryption = {
allow = true;
default = true;
require = true;
# Recommended options from mautrix documentation
# for optimal security.
delete_keys = {
dont_store_outbound = true;
ratchet_on_decrypt = true;
delete_fully_used_on_decrypt = true;
delete_prev_on_new_session = true;
delete_on_device_delete = true;
periodically_delete_expired = true;
delete_outdated_inbound = true;
};
verification_levels = {
receive = "cross-signed-tofu";
send = "cross-signed-tofu";
share = "cross-signed-tofu";
};
};
};
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
};
};
'';
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in
[example-config.yaml](https://github.com/mautrix/meta/blob/main/example-config.yaml).
Secret tokens should be specified using {option}`environmentFile`
instead
'';
};
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-meta.dataDir`.
Can be used for storing the secrets without making them available in the Nix store.
For example, you can set `services.mautrix-meta.settings.appservice.as_token = "$MAUTRIX_META_APPSERVICE_AS_TOKEN"`
and then specify `MAUTRIX_META_APPSERVICE_AS_TOKEN="{token}"` in the environment file.
This value will get substituted into the configuration file as as token.
'';
};
serviceDependencies = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
config.registrationServiceUnit
]
++ (lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit)
++ (lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service")
++ (lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
defaultText = ''
[ config.registrationServiceUnit ] ++
(lib.lists.optional upperConfig.services.matrix-synapse.enable upperConfig.services.matrix-synapse.serviceUnit) ++
(lib.lists.optional upperConfig.services.matrix-conduit.enable "matrix-conduit.service") ++
(lib.lists.optional upperConfig.services.dendrite.enable "dendrite.service");
'';
description = ''
List of Systemd services to require and wait for when starting the application service.
'';
};
serviceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
The systemd unit (a service or a target) for other services to depend on if they
need to be started after matrix-synapse.
This option is useful as the actual parent unit for all matrix-synapse processes
changes when configuring workers.
'';
};
registrationServiceUnit = lib.mkOption {
type = lib.types.str;
readOnly = true;
description = ''
The registration service that generates the registration file.
Systemd unit (a service or a target) for other services to depend on if they
need to be started after mautrix-meta registration service.
This option is useful as the actual parent unit for all matrix-synapse processes
changes when configuring workers.
'';
};
};
config = {
serviceUnit = (metaName name) + ".service";
registrationServiceUnit = (metaName name) + "-registration.service";
registrationFile = (fullDataDir config) + "/meta-registration.yaml";
};
}
)
);
description = ''
Configuration of multiple `mautrix-meta` instances.
`services.mautrix-meta.instances.facebook` and `services.mautrix-meta.instances.instagram`
come preconfigured with network.mode, appservice.id, bot username, display name and avatar.
'';
example = ''
{
facebook = {
enable = true;
settings = {
homeserver.domain = "example.com";
};
};
instagram = {
enable = true;
settings = {
homeserver.domain = "example.com";
};
};
messenger = {
enable = true;
settings = {
network.mode = "messenger";
homeserver.domain = "example.com";
appservice = {
id = "messenger";
bot = {
username = "messengerbot";
displayname = "Messenger bridge bot";
avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
};
};
};
};
}
'';
};
};
};
config = lib.mkMerge [
(lib.mkIf (enabledInstances != { }) {
assertions = lib.mkMerge (
lib.attrValues (
lib.mapAttrs (name: cfg: [
{
assertion = cfg.settings.homeserver.domain != "" && cfg.settings.homeserver.address != "";
message = ''
The options with information about the homeserver:
`services.mautrix-meta.instances.${name}.settings.homeserver.domain` and
`services.mautrix-meta.instances.${name}.settings.homeserver.address` have to be set.
'';
}
{
assertion = builtins.elem cfg.settings.network.mode [
"facebook"
"facebook-tor"
"messenger"
"instagram"
];
message = ''
The option `services.mautrix-meta.instances.${name}.settings.network.mode` has to be set
to one of: facebook, facebook-tor, messenger, instagram.
This configures the mode of the bridge.
'';
}
{
assertion = cfg.settings.bridge.permissions != { };
message = ''
The option `services.mautrix-meta.instances.${name}.settings.bridge.permissions` has to be set.
'';
}
{
assertion = cfg.settings.appservice.id != "";
message = ''
The option `services.mautrix-meta.instances.${name}.settings.appservice.id` has to be set.
'';
}
{
assertion = cfg.settings.appservice.bot.username != "";
message = ''
The option `services.mautrix-meta.instances.${name}.settings.appservice.bot.username` has to be set.
'';
}
{
assertion = !(cfg.settings ? bridge.disable_xma);
message = ''
The option `bridge.disable_xma` has been moved to `network.disable_xma_always`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
'';
}
{
assertion = !(cfg.settings ? bridge.displayname_template);
message = ''
The option `bridge.displayname_template` has been moved to `network.displayname_template`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
'';
}
{
assertion = !(cfg.settings ? meta);
message = ''
The options in `meta` have been moved to `network`. Please [migrate your configuration](https://github.com/mautrix/meta/releases/tag/v0.4.0). You may wish to use [the auto-migration code](https://github.com/mautrix/meta/blob/f5440b05aac125b4c95b1af85635a717cbc6dd0e/cmd/mautrix-meta/legacymigrate.go#L23) for reference.
'';
}
]) enabledInstances
)
);
users.users = lib.mapAttrs' (
name: cfg:
lib.nameValuePair "mautrix-meta-${name}" {
isSystemUser = true;
group = "mautrix-meta";
extraGroups = [ "mautrix-meta-registration" ];
description = "Mautrix-Meta-${name} bridge user";
}
) enabledInstances;
users.groups.mautrix-meta = { };
users.groups.mautrix-meta-registration = {
members = lib.lists.optional config.services.matrix-synapse.enable "matrix-synapse";
};
services.matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (
let
registrationFiles = lib.attrValues (
lib.mapAttrs (name: cfg: cfg.registrationFile) registerToSynapseInstances
);
in
{
settings.app_service_config_files = registrationFiles;
}
);
systemd.services = lib.mkMerge [
{
matrix-synapse = lib.mkIf (config.services.matrix-synapse.enable) (
let
registrationServices = lib.attrValues (
lib.mapAttrs (name: cfg: cfg.registrationServiceUnit) registerToSynapseInstances
);
in
{
wants = registrationServices;
after = registrationServices;
}
);
}
(lib.mapAttrs' (
name: cfg:
lib.nameValuePair "${metaName name}-registration" {
description = "Mautrix-Meta registration generation service - ${metaName name}";
path = [
pkgs.yq
pkgs.envsubst
upperCfg.package
];
script = ''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
rm -f '${settingsFile cfg}'
old_umask=$(umask)
umask 0177
envsubst \
-o '${settingsFile cfg}' \
-i '${settingsFileUnsubstituted cfg}'
config_has_tokens=$(yq '.appservice | has("as_token") and has("hs_token")' '${settingsFile cfg}')
registration_already_exists=$([[ -f '${cfg.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 cfg}' '${cfg.registrationFile}' \
> '${settingsFile cfg}.tmp'
mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
fi
# make sure --generate-registration does not affect config.yaml
cp '${settingsFile cfg}' '${settingsFile cfg}.tmp'
echo "Generating registration file"
mautrix-meta \
--generate-registration \
--config='${settingsFile cfg}.tmp' \
--registration='${cfg.registrationFile}'
rm '${settingsFile cfg}.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 cfg}' '${cfg.registrationFile}' \
> '${settingsFile cfg}.tmp'
mv '${settingsFile cfg}.tmp' '${settingsFile cfg}'
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 cfg}' '${cfg.registrationFile}' \
> '${cfg.registrationFile}.tmp'
mv '${cfg.registrationFile}.tmp' '${cfg.registrationFile}'
fi
umask $old_umask
chown :mautrix-meta-registration '${cfg.registrationFile}'
chmod 640 '${cfg.registrationFile}'
'';
serviceConfig = {
Type = "oneshot";
UMask = 27;
User = "mautrix-meta-${name}";
Group = "mautrix-meta";
SystemCallFilter = [ "@system-service" ];
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = fullDataDir cfg;
StateDirectory = cfg.dataDir;
EnvironmentFile = cfg.environmentFile;
};
restartTriggers = [ (settingsFileUnsubstituted cfg) ];
}
) enabledInstances)
(lib.mapAttrs' (
name: cfg:
lib.nameValuePair "${metaName name}" {
description = "Mautrix-Meta bridge - ${metaName name}";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
serviceConfig = {
Type = "simple";
User = "mautrix-meta-${name}";
Group = "mautrix-meta";
PrivateUsers = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [ "@system-service" ];
UMask = 27;
WorkingDirectory = fullDataDir cfg;
ReadWritePaths = fullDataDir cfg;
StateDirectory = cfg.dataDir;
EnvironmentFile = cfg.environmentFile;
ExecStart = lib.escapeShellArgs [
(lib.getExe upperCfg.package)
"--config=${settingsFile cfg}"
];
};
restartTriggers = [ (settingsFileUnsubstituted cfg) ];
}
) enabledInstances)
];
})
{
services.mautrix-meta.instances =
let
inherit (lib.modules) mkDefault;
in
{
instagram = {
settings = {
network.mode = mkDefault "instagram";
appservice = {
id = mkDefault "instagram";
port = mkDefault 29320;
bot = {
username = mkDefault "instagrambot";
displayname = mkDefault "Instagram bridge bot";
avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
};
username_template = mkDefault "instagram_{{.}}";
};
};
};
facebook = {
settings = {
network.mode = mkDefault "facebook";
appservice = {
id = mkDefault "facebook";
port = mkDefault 29321;
bot = {
username = mkDefault "facebookbot";
displayname = mkDefault "Facebook bridge bot";
avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
};
username_template = mkDefault "facebook_{{.}}";
};
};
};
};
}
];
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,32 @@
# Mautrix-Signal {#module-services-mautrix-signal}
[Mautrix-Signal](https://github.com/mautrix/signal) is a Matrix-Signal puppeting bridge.
## Configuration {#module-services-mautrix-signal-configuration}
1. Set [](#opt-services.mautrix-signal.enable) to `true`. The service will use
SQLite by default.
2. To create your configuration check the default configuration for
[](#opt-services.mautrix-signal.settings). To obtain the complete default
configuration, run
`nix-shell -p mautrix-signal --run "mautrix-signal -c default.yaml -e"`.
::: {.warning}
Mautrix-Signal allows for some options like `encryption.pickle_key`,
`provisioning.shared_secret`, allow the value `generate` to be set.
Since the configuration file is regenerated on every start of the
service, the generated values would be discarded and might break your
installation. Instead, set those values via
[](#opt-services.mautrix-signal.environmentFile).
:::
## Migrating from an older configuration {#module-services-mautrix-signal-migrate-configuration}
With Mautrix-Signal v0.7.0 the configuration has been rearranged. Mautrix-Signal
performs an automatic configuration migration so your pre-0.7.0 configuration
should just continue to work.
In case you want to update your NixOS configuration, compare the migrated configuration
at `/var/lib/mautrix-signal/config.yaml` with the default configuration
(`nix-shell -p mautrix-signal --run "mautrix-signal -c example.yaml -e"`) and
update your module configuration accordingly.

View File

@@ -0,0 +1,277 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.mautrix-signal;
dataDir = "/var/lib/mautrix-signal";
registrationFile = "${dataDir}/signal-registration.yaml";
settingsFile = "${dataDir}/config.yaml";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-signal-config-unsubstituted.json" cfg.settings;
settingsFormat = pkgs.formats.json { };
appservicePort = 29328;
# to be used with a list of lib.mkIf values
optOneOf = lib.lists.findFirst (value: value.condition) (lib.mkIf false null);
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
defaultConfig = {
network = {
displayname_template = "{{or .ProfileName .PhoneNumber \"Unknown user\"}}";
};
bridge = {
command_prefix = "!signal";
relay.enabled = true;
permissions."*" = "relay";
};
database = {
type = "sqlite3";
uri = "file:${dataDir}/mautrix-signal.db?_txlock=immediate";
};
homeserver.address = "http://localhost:8448";
appservice = {
hostname = "[::]";
port = appservicePort;
id = "signal";
bot = {
username = "signalbot";
displayname = "Signal Bridge Bot";
};
as_token = "";
hs_token = "";
username_template = "signal_{{.}}";
};
double_puppet = {
servers = { };
secrets = { };
};
# By default, the following keys/secrets are set to `generate`. This would break when the service
# is restarted, since the previously generated configuration will be overwritten everytime.
# If encryption is enabled, it's recommended to set those keys via `environmentFile`.
encryption.pickle_key = "";
provisioning.shared_secret = "";
public_media.signing_key = "";
direct_media.server_key = "";
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
};
};
in
{
options.services.mautrix-signal = {
enable = lib.mkEnableOption "mautrix-signal, a Matrix-Signal puppeting bridge";
package = lib.mkPackageOption pkgs "mautrix-signal" { };
settings = lib.mkOption {
apply = lib.recursiveUpdate defaultConfig;
type = settingsFormat.type;
default = defaultConfig;
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in the example configuration.
Get an example configuration by executing `mautrix-signal -c example.yaml --generate-example-config`
Secret tokens should be specified using {option}`environmentFile`
instead of this world-readable attribute set.
'';
example = {
bridge = {
private_chat_portal_meta = true;
mute_only_on_create = false;
permissions = {
"example.com" = "user";
};
};
database = {
type = "postgres";
uri = "postgresql:///mautrix_signal?host=/run/postgresql";
};
homeserver = {
address = "http://[::1]:8008";
domain = "my-domain.tld";
};
appservice = {
id = "signal";
ephemeral_events = false;
};
matrix.message_status_events = true;
provisioning = {
shared_secret = "disable";
};
backfill.enabled = true;
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$ENCRYPTION_PICKLE_KEY";
};
};
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing environment variables to be passed to the mautrix-signal service.
If an environment variable `MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET` is set,
then its value will be used in the configuration file for the option
`double_puppet.secrets` without leaking it to the store, using the configured
`homeserver.domain` as key.
'';
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
default =
(lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit)
++ (lib.optional config.services.matrix-conduit.enable "conduit.service");
defaultText = lib.literalExpression ''
(optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit)
++ (optional config.services.matrix-conduit.enable "conduit.service")
'';
description = ''
List of systemd units to require and wait for when starting the application service.
'';
};
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`.
'';
};
};
config = lib.mkIf cfg.enable {
users.users.mautrix-signal = {
isSystemUser = true;
group = "mautrix-signal";
home = dataDir;
description = "Mautrix-Signal bridge user";
};
users.groups.mautrix-signal = { };
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
settings.app_service_config_files = [ registrationFile ];
};
systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-signal" ];
};
# Note: this is defined here to avoid the docs depending on `config`
services.mautrix-signal.settings.homeserver = optOneOf (
with config.services;
[
(lib.mkIf matrix-synapse.enable (mkDefaults {
domain = matrix-synapse.settings.server_name;
}))
(lib.mkIf matrix-conduit.enable (mkDefaults {
domain = matrix-conduit.settings.global.server_name;
address = "http://localhost:${toString matrix-conduit.settings.global.port}";
}))
]
);
systemd.services.mautrix-signal = {
description = "mautrix-signal, a Matrix-Signal puppeting bridge.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
# ffmpeg is required for conversion of voice messages
path = [ pkgs.ffmpeg-headless ];
preStart = ''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${cfg.package}/bin/mautrix-signal \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
chmod 640 ${registrationFile}
umask 0177
# 1. Overwrite registration tokens in config
# 2. If environment variable MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET
# is set, set it as the login shared secret value for the configured
# homeserver domain.
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]
| if env.MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET then .double_puppet.secrets.[.homeserver.domain] = env.MAUTRIX_SIGNAL_BRIDGE_LOGIN_SHARED_SECRET else . end' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
'';
serviceConfig = {
User = "mautrix-signal";
Group = "mautrix-signal";
EnvironmentFile = cfg.environmentFile;
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
${cfg.package}/bin/mautrix-signal \
--config='${settingsFile}' \
--registration='${registrationFile}'
'';
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [ "@system-service" ];
Type = "simple";
UMask = 27;
};
restartTriggers = [ settingsFileUnsubstituted ];
};
};
meta = {
buildDocsInSandbox = false;
doc = ./mautrix-signal.md;
maintainers = with lib.maintainers; [
pentane
frederictobiasc
];
};
}

View File

@@ -0,0 +1,259 @@
{
config,
pkgs,
lib,
...
}:
let
dataDir = "/var/lib/mautrix-telegram";
registrationFile = "${dataDir}/telegram-registration.yaml";
cfg = config.services.mautrix-telegram;
settingsFormat = pkgs.formats.json { };
settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
settingsFile = "${dataDir}/config.json";
in
{
options = {
services.mautrix-telegram = {
enable = lib.mkEnableOption "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge";
package = lib.mkPackageOption pkgs "mautrix-telegram" { };
settings = lib.mkOption rec {
apply = lib.recursiveUpdate default;
inherit (settingsFormat) type;
default = {
homeserver = {
software = "standard";
};
appservice = rec {
database = "sqlite:///${dataDir}/mautrix-telegram.db";
database_opts = { };
hostname = "0.0.0.0";
port = 8080;
address = "http://localhost:${toString port}";
};
bridge = {
permissions."*" = "relaybot";
relaybot.whitelist = [ ];
double_puppet_server_map = { };
login_shared_secret_map = { };
};
logging = {
version = 1;
formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
handlers.console = {
class = "logging.StreamHandler";
formatter = "precise";
};
loggers = {
mau.level = "INFO";
telethon.level = "INFO";
# prevent tokens from leaking in the logs:
# https://github.com/tulir/mautrix-telegram/issues/351
aiohttp.level = "WARNING";
};
# log to console/systemd instead of file
root = {
level = "INFO";
handlers = [ "console" ];
};
};
};
example = lib.literalExpression ''
{
homeserver = {
address = "http://localhost:8008";
domain = "public-domain.tld";
};
appservice.public = {
prefix = "/public";
external = "https://public-appservice-address/public";
};
bridge.permissions = {
"example.com" = "full";
"@admin:example.com" = "admin";
};
telegram = {
connection.use_ipv6 = true;
};
}
'';
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in
[example-config.yaml](https://github.com/mautrix/telegram/blob/master/mautrix_telegram/example-config.yaml).
Secret tokens should be specified using {option}`environmentFile`
instead of this world-readable attribute set.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing environment variables to be passed to the mautrix-telegram service,
in which secret tokens can be specified securely by defining values for e.g.
`MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN`,
`MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN`,
`MAUTRIX_TELEGRAM_TELEGRAM_API_ID`,
`MAUTRIX_TELEGRAM_TELEGRAM_API_HASH` and optionally
`MAUTRIX_TELEGRAM_TELEGRAM_BOT_TOKEN`.
These environment variables can also be used to set other options by
replacing hierarchy levels by `.`, converting the name to uppercase
and prepending `MAUTRIX_TELEGRAM_`.
For example, the first value above maps to
{option}`settings.appservice.as_token`.
The environment variable values can be prefixed with `json::` to have
them be parsed as JSON. For example, `login_shared_secret_map` can be
set as follows:
`MAUTRIX_TELEGRAM_BRIDGE_LOGIN_SHARED_SECRET_MAP=json::{"example.com":"secret"}`.
'';
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
defaultText = lib.literalExpression ''
lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
'';
description = ''
List of Systemd services to require and wait for when starting the application service.
'';
};
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`.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users.mautrix-telegram = {
isSystemUser = true;
group = "mautrix-telegram";
home = dataDir;
description = "Mautrix-Telegram bridge user";
};
users.groups.mautrix-telegram = { };
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
settings.app_service_config_files = [ registrationFile ];
};
systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-telegram" ];
};
systemd.services.mautrix-telegram = {
description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
path = [
pkgs.lottieconverter
pkgs.ffmpeg-headless
];
# mautrix-telegram tries to generate a dotfile in the home directory of
# the running user if using a postgresql database:
#
# File "python3.10/site-packages/asyncpg/connect_utils.py", line 257, in _dot_postgre>
# return (pathlib.Path.home() / '.postgresql' / filename).resolve()
# File "python3.10/pathlib.py", line 1000, in home
# return cls("~").expanduser()
# File "python3.10/pathlib.py", line 1440, in expanduser
# raise RuntimeError("Could not determine home directory.")
# RuntimeError: Could not determine home directory.
environment.HOME = dataDir;
preStart = ''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${cfg.package}/bin/mautrix-telegram \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
old_umask=$(umask)
umask 0177
# 1. Overwrite registration tokens in config
# is set, set it as the login shared secret value for the configured
# homeserver domain.
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
''
+ lib.optionalString (cfg.package ? alembic) ''
# run automatic database init and migration scripts
${cfg.package.alembic}/bin/alembic -x config='${settingsFile}' upgrade head
'';
serviceConfig = {
User = "mautrix-telegram";
Group = "mautrix-telegram";
Type = "simple";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
PrivateTmp = true;
WorkingDirectory = cfg.package; # necessary for the database migration scripts to be found
StateDirectory = baseNameOf dataDir;
UMask = "0027";
EnvironmentFile = cfg.environmentFile;
ExecStart = ''
${cfg.package}/bin/mautrix-telegram \
--config='${settingsFile}'
'';
};
};
};
meta.maintainers = with lib.maintainers; [
euxane
vskilet
];
}

View File

@@ -0,0 +1,32 @@
# Mautrix-Whatsapp {#module-services-mautrix-whatsapp}
[Mautrix-Whatsapp](https://github.com/mautrix/whatsapp) is a Matrix-Whatsapp puppeting bridge.
## Configuration {#module-services-mautrix-whatsapp-configuration}
1. Set [](#opt-services.mautrix-whatsapp.enable) to `true`. The service will use
SQLite by default.
2. To create your configuration check the default configuration for
[](#opt-services.mautrix-whatsapp.settings). To obtain the complete default
configuration, run
`nix-shell -p mautrix-whatsapp --run "mautrix-whatsapp -c default.yaml -e"`.
::: {.warning}
Mautrix-Whatsapp allows for some options like `encryption.pickle_key`,
`provisioning.shared_secret`, to allow the value `generate` to be set.
Since the configuration file is regenerated on every start of the
service, the generated values would be discarded and might break your
installation. Instead, set those values via
[](#opt-services.mautrix-whatsapp.environmentFile).
:::
## Migrating from an older configuration {#module-services-mautrix-whatsapp-migrate-configuration}
With Mautrix-Whatsapp v0.11.0 the configuration has been rearranged. Mautrix-Whatsapp
performs an automatic configuration migration so your pre-0.7.0 configuration
should just continue to work.
In case you want to update your NixOS configuration, compare the migrated configuration
at `/var/lib/mautrix-whatsapp/config.yaml` with the default configuration
(`nix-shell -p mautrix-whatsapp --run "mautrix-whatsapp -c example.yaml -e"`) and
update your module configuration accordingly.

View File

@@ -0,0 +1,279 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.mautrix-whatsapp;
dataDir = "/var/lib/mautrix-whatsapp";
registrationFile = "${dataDir}/whatsapp-registration.yaml";
settingsFile = "${dataDir}/config.yaml";
settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" cfg.settings;
settingsFormat = pkgs.formats.json { };
appservicePort = 29318;
# to be used with a list of lib.mkIf values
optOneOf = lib.lists.findFirst (value: value.condition) (lib.mkIf false null);
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
defaultConfig = {
network = {
displayname_template = "{{or .BusinessName .PushName .Phone}} (WA)";
identity_change_notices = true;
history_sync = {
request_full_sync = true;
};
};
bridge = {
command_prefix = "!wa";
relay.enabled = true;
permissions."*" = "relay";
};
database = {
type = "sqlite3-fk-wal";
uri = "file:${dataDir}/mautrix-whatsapp.db?_txlock=immediate";
};
homeserver.address = "http://localhost:8448";
appservice = {
hostname = "[::]";
port = appservicePort;
id = "whatsapp";
bot = {
username = "whatsappbot";
displayname = "WhatsApp Bridge Bot";
};
as_token = "";
hs_token = "";
username_template = "whatsapp_{{.}}";
};
double_puppet = {
servers = { };
secrets = { };
};
# By default, the following keys/secrets are set to `generate`. This would break when the service
# is restarted, since the previously generated configuration will be overwritten everytime.
# If encryption is enabled, it's recommended to set those keys via `environmentFile`.
encryption.pickle_key = "";
provisioning.shared_secret = "";
public_media.signing_key = "";
direct_media.server_key = "";
logging = {
min_level = "info";
writers = lib.singleton {
type = "stdout";
format = "pretty-colored";
time_format = " ";
};
};
};
in
{
options.services.mautrix-whatsapp = {
enable = lib.mkEnableOption "mautrix-whatsapp, a Matrix-WhatsApp puppeting bridge";
package = lib.mkPackageOption pkgs "mautrix-whatsapp" { };
settings = lib.mkOption {
apply = lib.recursiveUpdate defaultConfig;
type = settingsFormat.type;
default = defaultConfig;
description = ''
{file}`config.yaml` configuration as a Nix attribute set.
Configuration options should match those described in the example configuration.
Get an example configuration by executing `mautrix-whatsapp -c example.yaml --generate-example-config`
Secret tokens should be specified using {option}`environmentFile`
instead of this world-readable attribute set.
'';
example = {
bridge = {
private_chat_portal_meta = true;
mute_only_on_create = false;
permissions = {
"example.com" = "user";
};
};
database = {
type = "postgres";
uri = "postgresql:///mautrix_whatsapp?host=/run/postgresql";
};
homeserver = {
address = "http://[::1]:8008";
domain = "my-domain.tld";
};
appservice = {
id = "whatsapp";
ephemeral_events = false;
};
matrix.message_status_events = true;
provisioning = {
shared_secret = "disable";
};
backfill.enabled = true;
encryption = {
allow = true;
default = true;
require = true;
pickle_key = "$ENCRYPTION_PICKLE_KEY";
};
};
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing environment variables to be passed to the mautrix-whatsapp service.
If an environment variable `MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET` is set,
then its value will be used in the configuration file for the option
`double_puppet.secrets` without leaking it to the store, using the configured
`homeserver.domain` as key.
'';
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
default =
lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
++ lib.optional config.services.matrix-conduit.enable "conduit.service";
defaultText = lib.literalExpression ''
optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit
++ optional config.services.matrix-conduit.enable "conduit.service"
'';
description = ''
List of systemd units to require and wait for when starting the application service.
'';
};
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`.
'';
};
};
config = lib.mkIf cfg.enable {
users.users.mautrix-whatsapp = {
isSystemUser = true;
group = "mautrix-whatsapp";
home = dataDir;
description = "Mautrix-WhatsApp bridge user";
};
users.groups.mautrix-whatsapp = { };
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
settings.app_service_config_files = [ registrationFile ];
};
systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
serviceConfig.SupplementaryGroups = [ "mautrix-whatsapp" ];
};
# Note: this is defined here to avoid the docs depending on `config`
services.mautrix-whatsapp.settings.homeserver = optOneOf (
with config.services;
[
(lib.mkIf matrix-synapse.enable (mkDefaults {
domain = matrix-synapse.settings.server_name;
}))
(lib.mkIf matrix-conduit.enable (mkDefaults {
domain = matrix-conduit.settings.global.server_name;
address = "http://localhost:${toString matrix-conduit.settings.global.port}";
}))
]
);
systemd.services.mautrix-whatsapp = {
description = "Mautrix-WhatsApp, a Matrix-WhatsApp puppeting bridge";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
# ffmpeg is required for conversion of voice messages
path = [ pkgs.ffmpeg-headless ];
preStart = ''
# substitute the settings file by environment variables
# in this case read from EnvironmentFile
test -f '${settingsFile}' && rm -f '${settingsFile}'
old_umask=$(umask)
umask 0177
${pkgs.envsubst}/bin/envsubst \
-o '${settingsFile}' \
-i '${settingsFileUnsubstituted}'
umask $old_umask
# generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${cfg.package}/bin/mautrix-whatsapp \
--generate-registration \
--config='${settingsFile}' \
--registration='${registrationFile}'
fi
chmod 640 ${registrationFile}
umask 0177
# 1. Overwrite registration tokens in config
# 2. If environment variable MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET
# is set, set it as the login shared secret value for the configured
# homeserver domain.
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
| .[0].appservice.hs_token = .[1].hs_token
| .[0]
| if env.MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET then .double_puppet.secrets.[.homeserver.domain] = env.MAUTRIX_WHATSAPP_BRIDGE_LOGIN_SHARED_SECRET else . end' \
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
mv '${settingsFile}.tmp' '${settingsFile}'
umask $old_umask
'';
serviceConfig = {
User = "mautrix-whatsapp";
Group = "mautrix-whatsapp";
EnvironmentFile = cfg.environmentFile;
StateDirectory = baseNameOf dataDir;
WorkingDirectory = dataDir;
ExecStart = ''
${cfg.package}/bin/mautrix-whatsapp \
--config='${settingsFile}' \
--registration='${registrationFile}'
'';
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [ "@system-service" ];
Type = "simple";
UMask = 27;
};
restartTriggers = [ settingsFileUnsubstituted ];
};
};
meta = {
buildDocsInSandbox = false;
doc = ./mautrix-whatsapp.md;
maintainers = with lib.maintainers; [
pentane
frederictobiasc
];
};
}

View File

@@ -0,0 +1,106 @@
# Mjolnir (Matrix Moderation Tool) {#module-services-mjolnir}
This chapter will show you how to set up your own, self-hosted
[Mjolnir](https://github.com/matrix-org/mjolnir) instance.
As an all-in-one moderation tool, it can protect your server from
malicious invites, spam messages, and whatever else you don't want.
In addition to server-level protection, Mjolnir is great for communities
wanting to protect their rooms without having to use their personal
accounts for moderation.
The bot by default includes support for bans, redactions, anti-spam,
server ACLs, room directory changes, room alias transfers, account
deactivation, room shutdown, and more.
See the [README](https://github.com/matrix-org/mjolnir#readme)
page and the [Moderator's guide](https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md)
for additional instructions on how to setup and use Mjolnir.
For [additional settings](#opt-services.mjolnir.settings)
see [the default configuration](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml).
## Mjolnir Setup {#module-services-mjolnir-setup}
First create a new Room which will be used as a management room for Mjolnir. In
this room, Mjolnir will log possible errors and debugging information. You'll
need to set this Room-ID in [services.mjolnir.managementRoom](#opt-services.mjolnir.managementRoom).
Next, create a new user for Mjolnir on your homeserver, if not present already.
The Mjolnir Matrix user expects to be free of any rate limiting.
See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286)
for an example on how to achieve this.
If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
you'll need to make the Mjolnir user a Matrix server admin.
Now invite the Mjolnir user to the management room.
It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon),
so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
To enable the Pantalaimon E2E Proxy for mjolnir, enable
[services.mjolnir.pantalaimon](#opt-services.mjolnir.pantalaimon.enable). This will
autoconfigure a new Pantalaimon instance, which will connect to the homeserver
set in [services.mjolnir.homeserverUrl](#opt-services.mjolnir.homeserverUrl) and Mjolnir itself
will be configured to connect to the new Pantalaimon instance.
```nix
{
services.mjolnir = {
enable = true;
homeserverUrl = "https://matrix.domain.tld";
pantalaimon = {
enable = true;
username = "mjolnir";
passwordFile = "/run/secrets/mjolnir-password";
};
protectedRooms = [ "https://matrix.to/#/!xxx:domain.tld" ];
managementRoom = "!yyy:domain.tld";
};
}
```
### Element Matrix Services (EMS) {#module-services-mjolnir-setup-ems}
If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/)
server, you will need to consent to the terms and conditions. Upon startup, an error
log entry with a URL to the consent page will be generated.
## Synapse Antispam Module {#module-services-mjolnir-matrix-synapse-antispam}
A Synapse module is also available to apply the same rulesets the bot
uses across an entire homeserver.
To use the Antispam Module, add `matrix-synapse-plugins.matrix-synapse-mjolnir-antispam`
to the Synapse plugin list and enable the `mjolnir.Module` module.
```nix
{
services.matrix-synapse = {
plugins = with pkgs; [ matrix-synapse-plugins.matrix-synapse-mjolnir-antispam ];
extraConfig = ''
modules:
- module: mjolnir.Module
config:
# Prevent servers/users in the ban lists from inviting users on this
# server to rooms. Default true.
block_invites: true
# Flag messages sent by servers/users in the ban lists as spam. Currently
# this means that spammy messages will appear as empty to users. Default
# false.
block_messages: false
# Remove users from the user directory search by filtering matrix IDs and
# display names by the entries in the user ban list. Default false.
block_usernames: false
# The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
# this list cannot be room aliases or permalinks. This server is expected
# to already be joined to the room - Mjolnir will not automatically join
# these rooms.
ban_lists:
- "!roomid:example.org"
'';
};
}
```

View File

@@ -0,0 +1,266 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.mjolnir;
yamlConfig = {
inherit (cfg) dataPath managementRoom protectedRooms;
accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig"
homeserverUrl =
if cfg.pantalaimon.enable then
"http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}"
else
cfg.homeserverUrl;
rawHomeserverUrl = cfg.homeserverUrl;
pantalaimon = {
inherit (cfg.pantalaimon) username;
use = cfg.pantalaimon.enable;
password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig"
};
};
moduleConfigFile = pkgs.writeText "module-config.yaml" (
lib.generators.toYAML { } (
lib.filterAttrs (_: v: v != null) (
lib.fold lib.recursiveUpdate { } [
yamlConfig
cfg.settings
]
)
)
);
# these config files will be merged one after the other to build the final config
configFiles = [
"${pkgs.mjolnir}/lib/node_modules/mjolnir/config/default.yaml"
moduleConfigFile
];
# this will generate the default.yaml file with all configFiles as inputs and
# replace all secret strings using replace-secret
generateConfig = pkgs.writeShellScript "mjolnir-generate-config" (
let
yqEvalStr = lib.concatImapStringsSep " * " (
pos: _: "select(fileIndex == ${toString (pos - 1)})"
) configFiles;
yqEvalArgs = lib.concatStringsSep " " configFiles;
in
''
set -euo pipefail
umask 077
# mjolnir will try to load a config from "./config/default.yaml" in the working directory
# -> let's place the generated config there
mkdir -p ${cfg.dataPath}/config
# merge all config files into one, overriding settings of the previous one with the next config
# e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml
${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml
${lib.optionalString (cfg.accessTokenFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml
''}
${lib.optionalString (cfg.pantalaimon.passwordFile != null) ''
${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml
''}
''
);
in
{
options.services.mjolnir = {
enable = lib.mkEnableOption "Mjolnir, a moderation tool for Matrix";
homeserverUrl = lib.mkOption {
type = lib.types.str;
default = "https://matrix.org";
description = ''
Where the homeserver is located (client-server URL).
If `pantalaimon.enable` is `true`, this option will become the homeserver to which `pantalaimon` connects.
The listen address of `pantalaimon` will then become the `homeserverUrl` of `mjolnir`.
'';
};
accessTokenFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
File containing the matrix access token for the `mjolnir` user.
'';
};
pantalaimon = lib.mkOption {
description = ''
`pantalaimon` options (enables E2E Encryption support).
This will create a `pantalaimon` instance with the name "mjolnir".
'';
default = { };
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption ''
ignoring the accessToken. If true, accessToken is ignored and the username/password below will be
used instead. The access token of the bot will be stored in the dataPath
'';
username = lib.mkOption {
type = lib.types.str;
description = "The username to login with.";
};
passwordFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
File containing the matrix password for the `mjolnir` user.
'';
};
options = lib.mkOption {
type = lib.types.submodule (import ./pantalaimon-options.nix);
default = { };
description = ''
passthrough additional options to the `pantalaimon` service.
'';
};
};
};
};
dataPath = lib.mkOption {
type = lib.types.path;
default = "/var/lib/mjolnir";
description = ''
The directory the bot should store various bits of information in.
'';
};
managementRoom = lib.mkOption {
type = lib.types.str;
default = "#moderators:example.org";
description = ''
The room ID where people can use the bot. The bot has no access controls, so
anyone in this room can use the bot - secure your room!
This should be a room alias or room ID - not a matrix.to URL.
Note: `mjolnir` is fairly verbose - expect a lot of messages from it.
'';
};
protectedRooms = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = lib.literalExpression ''
[
"https://matrix.to/#/#yourroom:example.org"
"https://matrix.to/#/#anotherroom:example.org"
]
'';
description = ''
A list of rooms to protect (matrix.to URLs).
'';
};
settings = lib.mkOption {
default = { };
type = (pkgs.formats.yaml { }).type;
example = lib.literalExpression ''
{
autojoinOnlyIfManager = true;
automaticallyRedactForReasons = [ "spam" "advertising" ];
}
'';
description = ''
Additional settings (see [mjolnir default config](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml) for available settings). These settings will override settings made by the module config.
'';
};
};
config = lib.mkIf config.services.mjolnir.enable {
assertions = [
{
assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null);
message = "Specify pantalaimon.passwordFile";
}
{
assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null);
message = "Do not specify accessTokenFile when using pantalaimon";
}
{
assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null);
message = "Specify accessTokenFile when not using pantalaimon";
}
];
# This defaults to true in the application,
# which breaks older configs using pantalaimon or access tokens
services.mjolnir.settings.encryption.use = lib.mkDefault false;
services.pantalaimon-headless.instances."mjolnir" =
lib.mkIf cfg.pantalaimon.enable {
homeserver = cfg.homeserverUrl;
}
// cfg.pantalaimon.options;
systemd.services.mjolnir = {
description = "mjolnir - a moderation tool for Matrix";
wants = [
"network-online.target"
]
++ lib.optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
after = [
"network-online.target"
]
++ lib.optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''${pkgs.mjolnir}/bin/mjolnir --mjolnir-config ./config/default.yaml'';
ExecStartPre = [ generateConfig ];
WorkingDirectory = cfg.dataPath;
StateDirectory = "mjolnir";
StateDirectoryMode = "0700";
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
NoNewPrivileges = true;
PrivateDevices = true;
User = "mjolnir";
Restart = "on-failure";
/*
TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME"
DynamicUser = true;
LoadCredential = [] ++
lib.optionals (cfg.accessTokenFile != null) [
"access_token:${cfg.accessTokenFile}"
] ++
lib.optionals (cfg.pantalaimon.passwordFile != null) [
"pantalaimon_password:${cfg.pantalaimon.passwordFile}"
];
*/
};
};
users = {
users.mjolnir = {
group = "mjolnir";
isSystemUser = true;
};
groups.mjolnir = { };
};
};
meta = {
doc = ./mjolnir.md;
maintainers = with lib.maintainers; [ jojosch ];
};
}

View File

@@ -0,0 +1,78 @@
{
config,
lib,
name,
...
}:
{
options = {
dataPath = lib.mkOption {
type = lib.types.path;
default = "/var/lib/pantalaimon-${name}";
description = ''
The directory where `pantalaimon` should store its state such as the database file.
'';
};
logLevel = lib.mkOption {
type = lib.types.enum [
"info"
"warning"
"error"
"debug"
];
default = "warning";
description = ''
Set the log level of the daemon.
'';
};
homeserver = lib.mkOption {
type = lib.types.str;
example = "https://matrix.org";
description = ''
The URI of the homeserver that the `pantalaimon` proxy should
forward requests to, without the matrix API path but including
the http(s) schema.
'';
};
ssl = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether or not SSL verification should be enabled for outgoing
connections to the homeserver.
'';
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = ''
The address where the daemon will listen to client connections
for this homeserver.
'';
};
listenPort = lib.mkOption {
type = lib.types.port;
default = 8009;
description = ''
The port where the daemon will listen to client connections for
this homeserver. Note that the listen address/port combination
needs to be lib.unique between different homeservers.
'';
};
extraSettings = lib.mkOption {
type = lib.types.attrs;
default = { };
description = ''
Extra configuration options. See
[pantalaimon(5)](https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md)
for available options.
'';
};
};
}

View File

@@ -0,0 +1,74 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.pantalaimon-headless;
iniFmt = pkgs.formats.ini { };
mkConfigFile =
name: instanceConfig:
iniFmt.generate "pantalaimon.conf" {
Default = {
LogLevel = instanceConfig.logLevel;
Notifications = false;
};
${name} = (
lib.recursiveUpdate {
Homeserver = instanceConfig.homeserver;
ListenAddress = instanceConfig.listenAddress;
ListenPort = instanceConfig.listenPort;
SSL = instanceConfig.ssl;
# Set some settings to prevent user interaction for headless operation
IgnoreVerification = true;
UseKeyring = false;
} instanceConfig.extraSettings
);
};
mkPantalaimonService =
name: instanceConfig:
lib.nameValuePair "pantalaimon-${name}" {
description = "pantalaimon instance ${name} - E2EE aware proxy daemon for matrix clients";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''${pkgs.pantalaimon-headless}/bin/pantalaimon --config ${mkConfigFile name instanceConfig} --data-path ${instanceConfig.dataPath}'';
Restart = "on-failure";
DynamicUser = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectHome = true;
ProtectSystem = "strict";
StateDirectory = "pantalaimon-${name}";
};
};
in
{
options.services.pantalaimon-headless.instances = lib.mkOption {
default = { };
type = lib.types.attrsOf (lib.types.submodule (import ./pantalaimon-options.nix));
description = ''
Declarative instance config.
Note: to use pantalaimon interactively, e.g. for a Matrix client which does not
support End-to-end encryption (like `fractal`), refer to the home-manager module.
'';
};
config = lib.mkIf (config.services.pantalaimon-headless.instances != { }) {
systemd.services = lib.mapAttrs' mkPantalaimonService config.services.pantalaimon-headless.instances;
};
meta = {
maintainers = with lib.maintainers; [ jojosch ];
};
}

View File

@@ -0,0 +1,164 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.synapse-auto-compressor;
synapseConfig = config.services.matrix-synapse;
postgresEnabled = config.services.postgresql.enable;
synapseUsesPostgresql = synapseConfig.settings.database.name == "psycopg2";
synapseUsesLocalPostgresql =
let
args = synapseConfig.settings.database.args;
in
synapseUsesPostgresql
&& postgresEnabled
&& (
!(args ? host)
|| (builtins.elem args.host [
"localhost"
"127.0.0.1"
"::1"
])
);
in
{
options = {
services.synapse-auto-compressor = {
enable = lib.mkEnableOption "synapse-auto-compressor";
package = lib.mkPackageOption pkgs "rust-synapse-state-compress" { };
postgresUrl = lib.mkOption {
default =
let
args = synapseConfig.settings.database.args;
in
if synapseConfig.enable then
''postgresql://${args.user}${lib.optionalString (args ? password) (":" + args.password)}@${
lib.escapeURL (if (args ? host) then args.host else "/run/postgresql")
}${lib.optionalString (args ? port) (":" + args.port)}/${args.database}''
else
null;
defaultText = lib.literalExpression ''
let
synapseConfig = config.services.matrix-synapse;
args = synapseConfig.settings.database.args;
in
if synapseConfig.enable then
'''postgresql://''${args.user}''${lib.optionalString (args ? password) (":" + args.password)}@''${
lib.escapeURL (if (args ? host) then args.host else "/run/postgresql")
}''${lib.optionalString (args ? port) (":" + args.port)}''${args.database}'''
else
null;
'';
type = lib.types.str;
example = "postgresql://username:password@mydomain.com:port/database";
description = ''
Connection string to postgresql in the
[rust `postgres` crate config format](https://docs.rs/postgres/latest/postgres/config/struct.Config.html).
The module will attempt to build a URL-style connection string out of the `services.matrix-synapse.settings.database.args`
if a local synapse is enabled.
'';
};
startAt = lib.mkOption {
default = "weekly";
type = with lib.types; either str (listOf str);
description = "How often to run this service in systemd calendar syntax (see {manpage}`systemd.time(7)`)";
example = "daily";
};
settings = {
chunk_size = lib.mkOption {
type = lib.types.int;
default = 500;
description = ''
The number of state groups to work on at once. All of the entries from `state_groups_state` are requested
from the database for state groups that are worked on. Therefore small chunk sizes may be needed on
machines with low memory.
Note: if the compressor fails to find space savings on the chunk as a whole
(which may well happen in rooms with lots of backfill in) then the entire chunk is skipped.
'';
};
chunks_to_compress = lib.mkOption {
type = lib.types.int;
default = 100;
description = ''
`chunks_to_compress` chunks of size `chunk_size` will be compressed. The higher this number is set to,
the longer the compressor will run for.
'';
};
levels = lib.mkOption {
type = with lib.types; listOf int;
default = [
100
50
25
];
description = ''
Sizes of each new level in the compression algorithm, as a comma-separated list. The first entry in
the list is for the lowest, most granular level, with each subsequent entry being for the next highest
level. The number of entries in the list determines the number of levels that will be used. The sum of
the sizes of the levels affects the performance of fetching the state from the database, as the sum of
the sizes is the upper bound on the number of iterations needed to fetch a given set of state.
'';
};
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = synapseConfig.enable && synapseUsesPostgresql;
message = "`services.synapse-auto-compressor` requires local synapse to use postgresql as a database backend";
}
];
systemd.services.synapse-auto-compressor = {
description = "synapse-auto-compressor";
requires = lib.optionals synapseUsesLocalPostgresql [
"postgresql.target"
];
inherit (cfg) startAt;
serviceConfig = {
Type = "oneshot";
DynamicUser = true;
User = "matrix-synapse";
PrivateTmp = true;
ExecStart = utils.escapeSystemdExecArgs [
"${cfg.package}/bin/synapse_auto_compressor"
"-p"
cfg.postgresUrl
"-c"
cfg.settings.chunk_size
"-n"
cfg.settings.chunks_to_compress
"-l"
(lib.concatStringsSep "," (builtins.map builtins.toString cfg.settings.levels))
];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateUsers = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
ProcSubset = "pid";
ProtectProc = "invisible";
ProtectSystem = "strict";
ProtectHome = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
};
};
};
}

View File

@@ -0,0 +1,244 @@
# Matrix {#module-services-matrix}
[Matrix](https://matrix.org/) is an open standard for
interoperable, decentralised, real-time communication over IP. It can be used
to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things
communication - or anywhere you need a standard HTTP API for publishing and
subscribing to data whilst tracking the conversation history.
This chapter will show you how to set up your own, self-hosted Matrix
homeserver using the Synapse reference homeserver, and how to serve your own
copy of the Element web client. See the
[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
overview page for links to Element Apps for Android and iOS,
desktop clients, as well as bridges to other networks and other projects
around Matrix.
## Synapse Homeserver {#module-services-matrix-synapse}
[Synapse](https://github.com/element-hq/synapse) is
the reference homeserver implementation of Matrix from the core development
team at matrix.org.
Before deploying synapse server, a postgresql database must be set up.
For that, please make sure that postgresql is running and the following
SQL statements to create a user & database called `matrix-synapse` were
executed before synapse starts up:
```sql
CREATE ROLE "matrix-synapse";
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
```
Usually, it's sufficient to do this once manually before
continuing with the installation.
Please make sure to set a different password.
The following configuration example will set up a
synapse server for the `example.org` domain, served from
the host `myhostname.example.org`. For more information,
please refer to the
[installation instructions of Synapse](https://element-hq.github.io/synapse/latest/setup/installation.html) .
```nix
{
pkgs,
lib,
config,
...
}:
let
fqdn = "${config.networking.hostName}.${config.networking.domain}";
baseUrl = "https://${fqdn}";
clientConfig."m.homeserver".base_url = baseUrl;
serverConfig."m.server" = "${fqdn}:443";
mkWellKnown = data: ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
in
{
networking.hostName = "myhostname";
networking.domain = "example.org";
networking.firewall.allowedTCPPorts = [
80
443
];
services.postgresql.enable = true;
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
virtualHosts = {
# If the A and AAAA DNS records on example.org do not point on the same host as the
# records for myhostname.example.org, you can easily move the /.well-known
# virtualHost section of the code to the host that is serving example.org, while
# the rest stays on myhostname.example.org with no other changes required.
# This pattern also allows to seamlessly move the homeserver from
# myhostname.example.org to myotherhost.example.org by only changing the
# /.well-known redirection target.
"${config.networking.domain}" = {
enableACME = true;
forceSSL = true;
# This section is not needed if the server_name of matrix-synapse is equal to
# the domain (i.e. example.org from @foo:example.org) and the federation port
# is 8448.
# Further reference can be found in the docs about delegation under
# https://element-hq.github.io/synapse/latest/delegate.html
locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
# This is usually needed for homeserver discovery (from e.g. other Matrix clients).
# Further reference can be found in the upstream docs at
# https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
};
"${fqdn}" = {
enableACME = true;
forceSSL = true;
# It's also possible to do a redirect here or something else, this vhost is not
# needed for Matrix. It's recommended though to *not put* element
# here, see also the section about Element.
locations."/".extraConfig = ''
return 404;
'';
# Forward all Matrix API calls to the synapse Matrix homeserver. A trailing slash
# *must not* be used here.
locations."/_matrix".proxyPass = "http://[::1]:8008";
# Forward requests for e.g. SSO and password-resets.
locations."/_synapse/client".proxyPass = "http://[::1]:8008";
};
};
};
services.matrix-synapse = {
enable = true;
settings.server_name = config.networking.domain;
# The public base URL value must match the `base_url` value set in `clientConfig` above.
# The default value here is based on `server_name`, so if your `server_name` is different
# from the value of `fqdn` above, you will likely run into some mismatched domain names
# in client applications.
settings.public_baseurl = baseUrl;
settings.listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [
"client"
"federation"
];
compress = true;
}
];
}
];
};
}
```
## Registering Matrix users {#module-services-matrix-register-users}
If you want to run a server with public registration by anybody, you can
then enable `services.matrix-synapse.settings.enable_registration = true;`.
Otherwise, or you can generate a registration secret with
{command}`pwgen -s 64 1` and set it with
[](#opt-services.matrix-synapse.settings.registration_shared_secret).
To create a new user or admin from the terminal your client listener
must be configured to use TCP sockets. Then you can run the following
after you have set the secret and have rebuilt NixOS:
```ShellSession
$ nix-shell -p matrix-synapse
$ register_new_matrix_user -k your-registration-shared-secret http://localhost:8008
New user localpart: your-username
Password:
Confirm password:
Make admin [no]:
Success!
```
In the example, this would create a user with the Matrix Identifier
`@your-username:example.org`.
::: {.warning}
When using [](#opt-services.matrix-synapse.settings.registration_shared_secret), the secret
will end up in the world-readable store. Instead it's recommended to deploy the secret
in an additional file like this:
- Create a file with the following contents:
```
registration_shared_secret: your-very-secret-secret
```
- Deploy the file with a secret-manager such as
[{option}`deployment.keys`](https://nixops.readthedocs.io/en/latest/overview.html#managing-keys)
from {manpage}`nixops(1)` or [sops-nix](https://github.com/Mic92/sops-nix/) to
e.g. {file}`/run/secrets/matrix-shared-secret` and ensure that it's readable
by `matrix-synapse`.
- Include the file like this in your configuration:
```nix
{
services.matrix-synapse.extraConfigFiles = [ "/run/secrets/matrix-shared-secret" ];
}
```
:::
::: {.note}
It's also possible to user alternative authentication mechanism such as
[LDAP (via `matrix-synapse-ldap3`)](https://github.com/matrix-org/matrix-synapse-ldap3)
or [OpenID](https://element-hq.github.io/synapse/latest/openid.html).
:::
## Element (formerly known as Riot) Web Client {#module-services-matrix-element-web}
[Element Web](https://github.com/element-hq/element-web) is
the reference web client for Matrix and developed by the core team at
matrix.org. Element was formerly known as Riot.im, see the
[Element introductory blog post](https://element.io/blog/welcome-to-element/)
for more information. The following snippet can be optionally added to the code before
to complete the synapse installation with a web client served at
`https://element.myhostname.example.org` and
`https://element.example.org`. Alternatively, you can use the hosted
copy at <https://app.element.io/>,
or use other web clients or native client applications. Due to the
`/.well-known` urls set up done above, many clients should
fill in the required connection details automatically when you enter your
Matrix Identifier. See
[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
for a list of existing clients and their supported featureset.
```nix
{
services.nginx.virtualHosts."element.${fqdn}" = {
enableACME = true;
forceSSL = true;
serverAliases = [ "element.${config.networking.domain}" ];
root = pkgs.element-web.override {
conf = {
default_server_config = clientConfig; # see `clientConfig` from the snippet above.
};
};
};
}
```
::: {.note}
The Element developers do not recommend running Element and your Matrix
homeserver on the same fully-qualified domain name for security reasons. In
the example, this means that you should not reuse the
`myhostname.example.org` virtualHost to also serve Element,
but instead serve it on a different subdomain, like
`element.example.org` in the example. See the
[Element Important Security Notes](https://github.com/element-hq/element-web/tree/v1.10.0#important-security-notes)
for more information on this subject.
:::

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,268 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.matrix-tuwunel;
defaultUser = "tuwunel";
defaultGroup = "tuwunel";
format = pkgs.formats.toml { };
configFile = format.generate "tuwunel.toml" cfg.settings;
in
{
meta.maintainers = with lib.maintainers; [
scvalex
];
options.services.matrix-tuwunel = {
enable = lib.mkEnableOption "tuwunel";
package = lib.mkPackageOption pkgs "matrix-tuwunel" { };
user = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
The user {command}`tuwunel` is run as. If left as the default, the user will
automatically be created by the service.
'';
example = "conduit";
default = defaultUser;
};
group = lib.mkOption {
type = lib.types.nonEmptyStr;
description = ''
The group {command}`tuwunel` is run as. If left as the default, the group will
automatically be created by the service.
'';
example = "conduit";
default = defaultGroup;
};
stateDirectory = lib.mkOption {
type = lib.types.nonEmptyStr;
default = "tuwunel";
example = "matrix-conduit";
description = ''
The name of the directory under /var/lib/ where the database will be stored.
Note that `stateDirectory` cannot be changed once created because of the service's reliance on
systemd `StateDirectory`.
'';
};
extraEnvironment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
description = "Extra Environment variables to pass to the tuwunel server.";
default = { };
example = {
RUST_BACKTRACE = "yes";
};
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
global.server_name = lib.mkOption {
type = lib.types.nonEmptyStr;
example = "example.com";
description = "The server_name is the name of this server. It is used as a suffix for user and room ids.";
};
global.address = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.nonEmptyStr);
default = null;
example = [
"127.0.0.1"
"::1"
];
description = ''
Addresses (IPv4 or IPv6) to listen on for connections by the reverse proxy/tls terminator.
If set to `null`, tuwunel will listen on IPv4 and IPv6 localhost.
Must be `null` if `unix_socket_path` is set.
'';
};
global.port = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ 6167 ];
description = ''
The port(s) tuwunel will be running on.
You need to set up a reverse proxy in your web server (e.g. apache or nginx),
so all requests to /_matrix on port 443 and 8448 will be forwarded to the tuwunel
instance running on this port.
'';
};
global.unix_socket_path = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Listen on a UNIX socket at the specified path. If listening on a UNIX socket,
listening on an address will be disabled. The `address` option must be set to
`null` (the default value). The option {option}`services.tuwunel.group` must
be set to a group your reverse proxy is part of.
'';
};
global.unix_socket_perms = lib.mkOption {
type = lib.types.ints.positive;
default = 660;
description = "The default permissions (in octal) to create the UNIX socket with.";
};
global.max_request_size = lib.mkOption {
type = lib.types.ints.positive;
default = 20000000;
description = "Max request size in bytes. Don't forget to also change it in the proxy.";
};
global.allow_registration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether new users can register on this server.
Registration with token requires `registration_token` or `registration_token_file` to be set.
If set to true without a token configured, and
`yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
is set to true, users can freely register.
'';
};
global.allow_encryption = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether new encrypted rooms can be created. Note: existing rooms will continue to work.";
};
global.allow_federation = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether this server federates with other servers.
'';
};
global.trusted_servers = lib.mkOption {
type = lib.types.listOf lib.types.nonEmptyStr;
default = [ "matrix.org" ];
description = ''
Servers listed here will be used to gather public keys of other servers
(notary trusted key servers).
Currently, tuwunel doesn't support inbound batched key requests, so
this list should only contain other Synapse servers.
Example: `[ "matrix.org" "constellatory.net" "tchncs.de" ]`
'';
};
};
};
default = { };
# TOML does not allow null values, so we use null to omit those fields
apply = lib.filterAttrsRecursive (_: v: v != null);
description = ''
Generates the tuwunel.toml configuration file. Refer to
<https://matrix-construct.github.io/tuwunel/configuration.html>
for details on supported values.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.settings ? global.unix_socket_path) || !(cfg.settings ? global.address);
message = ''
In `services.matrix-tuwunel.settings.global`, `unix_socket_path` and `address` cannot be set at the
same time.
Leave one of the two options unset or explicitly set them to `null`.
'';
}
{
assertion = cfg.user != defaultUser -> config ? users.users.${cfg.user};
message = "If `services.matrix-tuwunel.user` is changed, the configured user must already exist.";
}
{
assertion = cfg.group != defaultGroup -> config ? users.groups.${cfg.group};
message = "If `services.matrix-tuwunel.group` is changed, the configured group must already exist.";
}
{
assertion = "/var/lib/${cfg.settings.global.database_path}" != cfg.stateDirectory;
message = "The `services.matrix-tuwunel.stateDirectory` and `services.matrix-tuwunel.settings.global.database_path` options must match.";
}
];
users.users = lib.mkIf (cfg.user == defaultUser) {
${defaultUser} = {
group = cfg.group;
home = cfg.settings.global.database_path;
isSystemUser = true;
};
};
users.groups = lib.mkIf (cfg.group == defaultGroup) {
${defaultGroup} = { };
};
services.matrix-tuwunel.settings.global.database_path = "/var/lib/${cfg.stateDirectory}/";
systemd.services.tuwunel = {
description = "Tuwunel Matrix Server";
documentation = [ "https://matrix-construct.github.io/tuwunel/" ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = lib.mkMerge [
{ TUWUNEL_CONFIG = configFile; }
cfg.extraEnvironment
];
startLimitBurst = 5;
startLimitIntervalSec = 60;
serviceConfig = {
DynamicUser = true;
User = cfg.user;
Group = cfg.group;
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
PrivateIPC = true;
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service @resources"
"~@clock @debug @module @mount @reboot @swap @cpu-emulation @obsolete @timer @chown @setuid @privileged @keyring @ipc"
];
SystemCallErrorNumber = "EPERM";
StateDirectory = cfg.stateDirectory;
StateDirectoryMode = "0700";
RuntimeDirectory = "tuwunel";
RuntimeDirectoryMode = "0750";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
RestartSec = 10;
};
};
};
}