summaryrefslogtreecommitdiff
path: root/modules/matrix-appservices
diff options
context:
space:
mode:
authorParthiv Seetharaman <pachum99@myrdd.info>2022-03-13 01:31:05 +0000
committerParthiv Seetharaman <pachum99@myrdd.info>2022-03-13 01:31:05 +0000
commitbcfc0748e3c4ab71466662c8f217537f7d55ff6e (patch)
tree88f7bbca39ee6bfbef508cdf677b7ff031ecdcfa /modules/matrix-appservices
parentcc70149fc40ef67ee8b90a039ba388b3ae82eb82 (diff)
parent125aeaa50baf9cd0a40ece816760081eb594c51e (diff)
Merge branch 'allow-more-modules' into 'main'
modules: allow for more modules to be added See merge request coffeetables/nix-matrix-appservices!5
Diffstat (limited to 'modules/matrix-appservices')
-rw-r--r--modules/matrix-appservices/as-formats.nix160
-rw-r--r--modules/matrix-appservices/as-options.nix144
-rw-r--r--modules/matrix-appservices/default.nix180
3 files changed, 484 insertions, 0 deletions
diff --git a/modules/matrix-appservices/as-formats.nix b/modules/matrix-appservices/as-formats.nix
new file mode 100644
index 0000000..fa7a4cf
--- /dev/null
+++ b/modules/matrix-appservices/as-formats.nix
@@ -0,0 +1,160 @@
+{ name, systemConfig, asConfig, lib, pkgs, ... }:
+
+with lib;
+let
+ inherit (systemConfig.services.matrix-appservices)
+ homeserverURL
+ homeserverDomain;
+ package = asConfig.package;
+ pname = getName package;
+ command = "${package}/bin/${pname}";
+
+ mautrix = {
+ startupScript = ''
+ ${command} --config=$SETTINGS_FILE \
+ --registration=$REGISTRATION_FILE
+ '';
+
+ settings = {
+ homeserver = {
+ address = homeserverURL;
+ domain = homeserverDomain;
+ };
+
+ appservice = with asConfig; {
+ address = "http://${host}:${toString port}";
+
+ hostname = host;
+ inherit port;
+
+ state_store_path = "$DIR/mx-state.json";
+ # mautrix stores the registration tokens in the config file
+ as_token = "$AS_TOKEN";
+ hs_token = "$HS_TOKEN";
+ };
+
+ bridge = {
+ username_template = "${name}_{userid}";
+ permissions = {
+ ${homeserverDomain} = "user";
+ };
+ };
+ };
+ };
+
+in
+{
+ other = {
+ description = ''
+ No defaults will be set.
+ '';
+ };
+
+ matrix-appservice = {
+ startupScript = ''
+ ${command} \
+ --config=$SETTINGS_FILE \
+ --port=$(echo ${asConfig.listenAddress} | sed 's/.*://') \
+ --file=$REGISTRATION_FILE
+ '';
+
+ description = ''
+ For bridges based on the matrix-appservice-bridge library. The settings for these
+ bridges are NOT configured automatically, because of the various differences
+ between them.
+ '';
+ };
+
+ mx-puppet = {
+ startupScript = ''
+ ${command} \
+ --config=$SETTINGS_FILE \
+ --registration-file=$REGISTRATION_FILE
+ '';
+
+ registrationData =
+ let
+ # mx-puppet virtual users are always created based on the package name
+ botName = removePrefix "mx-puppet-" pname;
+ in
+ {
+ id = "${botName}-puppet";
+ sender_localpart = "_${botName}puppet_bot";
+ protocols = [ ];
+ namespaces = {
+ rooms = [ ];
+ users = [
+ {
+ regex = "@_${botName}puppet_.*:${homeserverDomain}";
+ exclusive = true;
+ }
+ ];
+ aliases = [
+ {
+ regex = "#_${botName}puppet_.*:${homeserverDomain}";
+ exclusive = true;
+ }
+ ];
+ };
+ };
+
+ settings = {
+ bridge = {
+ inherit (asConfig) port;
+ bindAddress = asConfig.host;
+ domain = homeserverDomain;
+ homeserverUrl = homeserverURL;
+ };
+ database.filename = "$DIR/database.db";
+ provisioning.whitelist = [ "@.*:${homeserverDomain}" ];
+ relay.whitelist = [ "@.*:${homeserverDomain}" ];
+ selfService.whitelist = [ "@.*:${homeserverDomain}" ];
+ logging = {
+ lineDateFormat = "";
+ files = [ ];
+ };
+ };
+
+ serviceConfig.WorkingDirectory =
+ "${package}/lib/node_modules/${pname}";
+
+ description = ''
+ For bridges based on the mx-puppet-bridge library. The settings will be
+ configured to use a sqlite database. Make sure to override database.filename,
+ if you plan to use another database.
+ '';
+
+ };
+
+ mautrix-go = {
+ inherit (mautrix) startupScript;
+
+ settings = recursiveUpdate mautrix.settings {
+ bridge.username_template = "${name}_{{.}}";
+ appservice.database = {
+ type = "sqlite3";
+ uri = "$DIR/database.db";
+ };
+ };
+
+ description = ''
+ The settings are configured to use a sqlite database. The startupScript will
+ create a new config file on every run to set the tokens, because mautrix
+ requires them to be in the config file.
+ '';
+ };
+
+ mautrix-python = {
+ settings = recursiveUpdate mautrix.settings {
+ appservice.database = "sqlite:///$DIR/database.db";
+ };
+
+ startupScript = optionalString (package ? alembic)
+ "${package.alembic}/bin/alembic -x config=$SETTINGS_FILE upgrade head\n"
+ + mautrix.startupScript;
+ description = ''
+ Same properties as mautrix-go. This will also upgrade the database on every run
+ '';
+ };
+
+}
diff --git a/modules/matrix-appservices/as-options.nix b/modules/matrix-appservices/as-options.nix
new file mode 100644
index 0000000..2afbbbf
--- /dev/null
+++ b/modules/matrix-appservices/as-options.nix
@@ -0,0 +1,144 @@
+{ systemConfig, lib, pkgs, ... }:
+with lib;
+types.submodule ({ config, name, ... }:
+ let
+ inherit (systemConfig.services.matrix-appservices)
+ homeserverDomain;
+
+ asFormats = (import ./as-formats.nix) {
+ inherit name lib pkgs systemConfig;
+ asConfig = config;
+ };
+ asFormat = asFormats.${config.format};
+ settingsFormat = pkgs.formats.json { };
+ in
+ {
+ options = rec {
+
+ format = mkOption {
+ type = types.enum (mapAttrsToList (n: _: n) asFormats);
+ default = "other";
+ description = ''
+ Format of the appservice, used to set option defaults for appservice.
+ This is usually determined by the library the appservice is based on.
+
+ Below are descriptions for each format
+
+ '' + (concatStringsSep "\n" (mapAttrsToList
+ (n: v: "${n}: ${v.description}")
+ asFormats));
+ };
+
+ package = mkOption {
+ type = types.nullOr types.package;
+ default = null;
+ example = "pkgs.mautrix-whatsapp";
+ description = ''
+ The package for the appservice. Used by formats except 'other'.
+ This is unecessary if startupScript is set.
+ '';
+ };
+
+ settings = mkOption rec {
+ type = settingsFormat.type;
+ apply = recursiveUpdate default;
+ default = asFormat.settings or { };
+ defaultText = "Format will attempt to configure database and allow homeserver users";
+ example = literalExpression ''
+ {
+ bridge = {
+ domain = "public-domain.tld";
+ homeserverUrl = "http://public-domain.tld:8008";
+ };
+ }
+ '';
+ description = ''
+ Appservice configuration as a Nix attribute set.
+ All environment variables will be substituted.
+ Including:
+ - $DIR which refers to the appservice's data directory.
+ - $AS_TOKEN, $HS_TOKEN which refers to the Appservice and
+ Homeserver registration tokens.
+
+ Secret tokens, should be specified in serviceConfig.EnvironmentFile
+ instead of this world-readable attribute set.
+
+ Configuration options should match those described as per your appservice's settings
+ Check out the confg sample for this.
+
+ '';
+ };
+
+ registrationData = mkOption {
+ type = settingsFormat.type;
+ default = asFormat.registrationData or {
+ namespaces = {
+ users = [
+ {
+ regex = "@${name}_.*:${homeserverDomain}";
+ exclusive = true;
+ }
+ {
+ regex = "@${name}bot:${homeserverDomain}";
+ exclusive = true;
+ }
+ ];
+ };
+ };
+ defaultText = ''
+ Reserve usernames under the homeserver that start with
+ this appservice's name followed by an _ or "bot"
+ '';
+ description = ''
+ Data to set in the registration file for the appservice. The default
+ set or the format should usually deal with this.
+ '';
+ };
+
+ host = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = ''
+ The host the appservice will listen on.
+ Will need to specified in config, but most formats will do it for you using
+ this option.
+ '';
+ };
+
+ port = mkOption {
+ type = types.port;
+ description = ''
+ The port the appservice will listen on.
+ Will need to specified in config, but most formats will do it for you using
+ this option.
+ '';
+ };
+
+ startupScript = mkOption {
+ type = types.str;
+ default = asFormat.startupScript or "";
+ description = ''
+ Script that starts the appservice.
+ The settings file will be available as $SETTINGS_FILE
+ and the registration file as $REGISTRATION_FILE
+ '';
+ };
+
+ serviceConfig = mkOption rec {
+ type = types.attrs;
+ apply = x: default // x;
+ default = asFormat.serviceConfig or { };
+ description = ''
+ Overrides for settings in the service's serviceConfig
+ '';
+ };
+
+ serviceDependencies = mkOption {
+ type = types.listOf types.str;
+ default = [ ];
+ description = ''
+ Services started before this appservice
+ '';
+ };
+ };
+ })
diff --git a/modules/matrix-appservices/default.nix b/modules/matrix-appservices/default.nix
new file mode 100644
index 0000000..f57284d
--- /dev/null
+++ b/modules/matrix-appservices/default.nix
@@ -0,0 +1,180 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+ cfg = config.services.matrix-appservices;
+ asOpts = import ./as-options.nix {
+ inherit lib pkgs;
+ systemConfig = config;
+ };
+ mkService = name: opts:
+ with opts;
+ let
+ settingsFormat = pkgs.formats.json { };
+ dataDir = "/var/lib/matrix-as-${name}";
+ registrationFile = "${dataDir}/${name}-registration.yaml";
+ # Replace all references to $DIR to the dat directory
+ settingsData = settingsFormat.generate "config.json" settings;
+ settingsFile = "${dataDir}/config.json";
+ serviceDeps = [ "network-online.target" ] ++ serviceDependencies;
+
+ registrationContent = {
+ id = name;
+ url = "http://${host}:${toString port}";
+ as_token = "$AS_TOKEN";
+ hs_token = "$HS_TOKEN";
+ sender_localpart = "$SENDER_LOCALPART";
+ rate_limited = false;
+ } // registrationData;
+ in
+ {
+ description = "A matrix appservice for ${name}.";
+
+ wantedBy = [ "multi-user.target" ];
+ wants = serviceDeps;
+ after = serviceDeps;
+ # Appservices don't need synapse up, but synapse exists if registration files are missing
+ before = mkIf (cfg.homeserver != null) [ "${cfg.homeserver}.service" ];
+
+ path = [ pkgs.yq ];
+ environment = {
+ DIR = dataDir;
+ SETTINGS_FILE = settingsFile;
+ REGISTRATION_FILE = registrationFile;
+ };
+
+ preStart = ''
+ if [ ! -f ${registrationFile} ]; then
+ AS_TOKEN=$(cat /proc/sys/kernel/random/uuid) \
+ HS_TOKEN=$(cat /proc/sys/kernel/random/uuid) \
+ SENDER_LOCALPART=$(cat /proc/sys/kernel/random/uuid) \
+ ${pkgs.envsubst}/bin/envsubst \
+ -i ${settingsFormat.generate "config.json" registrationContent} \
+ -o ${registrationFile}
+
+ chmod 640 ${registrationFile}
+ fi
+
+ AS_TOKEN=$(cat ${registrationFile} | yq .as_token | tr -d '"') \
+ HS_TOKEN=$(cat ${registrationFile} | yq .hs_token | tr -d '"') \
+ ${pkgs.envsubst}/bin/envsubst -i ${settingsData} -o ${settingsFile}
+ chmod 640 ${settingsFile}
+ '';
+
+ script = startupScript;
+
+ serviceConfig = {
+ Type = "simple";
+ Restart = "always";
+
+ ProtectSystem = "strict";
+ PrivateTmp = true;
+ ProtectHome = true;
+ ProtectKernelTunables = true;
+ ProtectKernelModules = true;
+ ProtectControlGroups = true;
+
+ User = "matrix-as-${name}";
+ Group = "matrix-as-${name}";
+ WorkingDirectory = dataDir;
+ StateDirectory = baseNameOf dataDir;
+ StateDirectoryMode = "0750";
+ UMask = 0027;
+ } // opts.serviceConfig;
+ };
+
+in
+{
+ options = {
+ services.matrix-appservices = {
+ services = mkOption {
+ type = types.attrsOf asOpts;
+ default = { };
+ example = literalExpression ''
+ whatsapp = {
+ format = "mautrix-go";
+ package = pkgs.mautrix-whatsapp;
+ };
+ '';
+ description = ''
+ Appservices to setup.
+ Each appservice will be started as a systemd service with the prefix matrix-as.
+ And its data will be stored in /var/lib/matrix-as-name.
+ '';
+ };
+
+ homeserver = mkOption {
+ type = types.enum [ "matrix-synapse" "dendrite" null ];
+ default = "matrix-synapse";
+ description = ''
+ The homeserver software the appservices connect to. This will ensure appservices
+ start after the homeserver and it will be used by the addRegistrationFiles option.
+ '';
+ };
+
+ homeserverURL = mkOption {
+ type = types.str;
+ default = "https://${cfg.homeserverDomain}";
+ description = ''
+ URL of the homeserver the apservices connect to
+ '';
+ };
+
+ homeserverDomain = mkOption {
+ type = types.str;
+ default = if config.networking.domain != null then config.networking.domain else "";
+ defaultText = "\${config.networking.domain}";
+ description = ''
+ Domain of the homeserver the appservices connect to
+ '';
+ };
+
+ addRegistrationFiles = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to add the application service registration files to the homeserver configuration.
+ It is recommended to verify appservice files, located in /var/lib/matrix-as-*, before adding them
+ '';
+ };
+ };
+ };
+
+ config = mkIf (cfg.services != { }) {
+
+ assertions = mapAttrsToList
+ (n: v: {
+ assertion = v.format == "other" || v.package != null;
+ message = "A package must be provided if a custom format is set";
+ })
+ cfg.services;
+
+ users.users = mapAttrs'
+ (n: v: nameValuePair "matrix-as-${n}" {
+ group = "matrix-as-${n}";
+ isSystemUser = true;
+ })
+ cfg.services;
+ users.groups = mapAttrs' (n: v: nameValuePair "matrix-as-${n}" { }) cfg.services;
+
+ # Create a service for each appservice
+ systemd.services = (mapAttrs' (n: v: nameValuePair "matrix-as-${n}" (mkService n v)) cfg.services) // {
+ # Add the matrix service to the groups of all appservices to give access to the registration file
+ matrix-synapse.serviceConfig.SupplementaryGroups = mapAttrsToList (n: v: "matrix-as-${n}") cfg.services;
+ dendrite.serviceConfig.SupplementaryGroups = mapAttrsToList (n: v: "matrix-as-${n}") cfg.services;
+ };
+
+ services =
+ let
+ registrationFiles = mapAttrsToList (n: _: "/var/lib/matrix-as-${n}/${n}-registration.yaml")
+ (filterAttrs (_: v: v.registrationData != { }) cfg.services);
+ in
+ mkIf cfg.addRegistrationFiles {
+ matrix-synapse.app_service_config_files = mkIf (cfg.homeserver == "matrix-synapse") registrationFiles;
+ dendrite.settings.app_service_api.config_files = mkIf (cfg.homeserver == "dendrite") registrationFiles;
+ };
+ };
+
+ meta.maintainers = with maintainers; [ pacman99 Flakebi ];
+
+}