mautrix-meta: init service
Acked-by: Rutherther <rutherther@proton.me>
This commit is contained in:
parent
404d79c3d0
commit
5effc7956a
@ -91,6 +91,8 @@ In addition to numerous new and upgraded packages, this release has the followin
|
||||
- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable).
|
||||
The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been marked deprecated and will be dropped after 24.05 due to lack of maintenance of the anki-sync-server softwares.
|
||||
|
||||
- [mautrix-meta](https://github.com/mautrix/meta), a Matrix <-> Facebook and Matrix <-> Instagram hybrid puppeting/relaybot bridge. Available as services.mautrix-meta
|
||||
|
||||
- [transfer-sh](https://github.com/dutchcoders/transfer.sh), a tool that supports easy and fast file sharing from the command-line. Available as [services.transfer-sh](#opt-services.transfer-sh.enable).
|
||||
|
||||
- [Suwayomi Server](https://github.com/Suwayomi/Suwayomi-Server), a free and open source manga reader server that runs extensions built for [Tachiyomi](https://tachiyomi.org). Available as [services.suwayomi-server](#opt-services.suwayomi-server.enable).
|
||||
|
@ -648,6 +648,7 @@
|
||||
./services/matrix/hebbot.nix
|
||||
./services/matrix/maubot.nix
|
||||
./services/matrix/mautrix-facebook.nix
|
||||
./services/matrix/mautrix-meta.nix
|
||||
./services/matrix/mautrix-telegram.nix
|
||||
./services/matrix/mautrix-whatsapp.nix
|
||||
./services/matrix/mjolnir.nix
|
||||
|
562
nixos/modules/services/matrix/mautrix-meta.nix
Normal file
562
nixos/modules/services/matrix/mautrix-meta.nix
Normal file
@ -0,0 +1,562 @@
|
||||
{ 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 = "";
|
||||
|
||||
database = {
|
||||
type = "sqlite3-fk-wal";
|
||||
uri = "file:${fullDataDir config}/mautrix-meta.db?_txlock=immediate";
|
||||
};
|
||||
|
||||
bot = {
|
||||
username = "";
|
||||
};
|
||||
|
||||
hostname = "localhost";
|
||||
port = 29319;
|
||||
address = "http://${config.settings.appservice.hostname}:${toString config.settings.appservice.port}";
|
||||
};
|
||||
|
||||
meta = {
|
||||
mode = "";
|
||||
};
|
||||
|
||||
bridge = {
|
||||
# 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;
|
||||
};
|
||||
|
||||
verification_levels = {
|
||||
receive = "cross-signed-tofu";
|
||||
send = "cross-signed-tofu";
|
||||
share = "cross-signed-tofu";
|
||||
};
|
||||
};
|
||||
|
||||
permissions = {};
|
||||
};
|
||||
|
||||
logging = {
|
||||
min_level = "info";
|
||||
writers = lib.singleton {
|
||||
type = "stdout";
|
||||
format = "pretty-colored";
|
||||
time_format = " ";
|
||||
};
|
||||
};
|
||||
};
|
||||
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 meta.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 = {
|
||||
meta.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.meta.mode [ "facebook" "facebook-tor" "messenger" "instagram" ];
|
||||
message = ''
|
||||
The option `services.mautrix-meta.instances.${name}.settings.meta.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.
|
||||
'';
|
||||
}
|
||||
]) 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 = 0027;
|
||||
|
||||
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 = 0027;
|
||||
|
||||
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 = {
|
||||
meta.mode = mkDefault "instagram";
|
||||
|
||||
bridge = {
|
||||
username_template = mkDefault "instagram_{{.}}";
|
||||
};
|
||||
|
||||
appservice = {
|
||||
id = mkDefault "instagram";
|
||||
port = mkDefault 29320;
|
||||
bot = {
|
||||
username = mkDefault "instagrambot";
|
||||
displayname = mkDefault "Instagram bridge bot";
|
||||
avatar = mkDefault "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
facebook = {
|
||||
settings = {
|
||||
meta.mode = mkDefault "facebook";
|
||||
|
||||
bridge = {
|
||||
username_template = mkDefault "facebook_{{.}}";
|
||||
};
|
||||
|
||||
appservice = {
|
||||
id = mkDefault "facebook";
|
||||
port = mkDefault 29321;
|
||||
bot = {
|
||||
username = mkDefault "facebookbot";
|
||||
displayname = mkDefault "Facebook bridge bot";
|
||||
avatar = mkDefault "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ rutherther ];
|
||||
}
|
@ -520,6 +520,8 @@ in {
|
||||
matrix-conduit = handleTest ./matrix/conduit.nix {};
|
||||
matrix-synapse = handleTest ./matrix/synapse.nix {};
|
||||
matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {};
|
||||
mautrix-meta-postgres = handleTest ./matrix/mautrix-meta-postgres.nix {};
|
||||
mautrix-meta-sqlite = handleTest ./matrix/mautrix-meta-sqlite.nix {};
|
||||
mattermost = handleTest ./mattermost.nix {};
|
||||
mealie = handleTest ./mealie.nix {};
|
||||
mediamtx = handleTest ./mediamtx.nix {};
|
||||
|
221
nixos/tests/matrix/mautrix-meta-postgres.nix
Normal file
221
nixos/tests/matrix/mautrix-meta-postgres.nix
Normal file
@ -0,0 +1,221 @@
|
||||
import ../make-test-python.nix ({ pkgs, ... }:
|
||||
let
|
||||
homeserverDomain = "server";
|
||||
homeserverUrl = "http://server:8008";
|
||||
userName = "alice";
|
||||
botUserName = "instagrambot";
|
||||
|
||||
asToken = "this-is-my-totally-randomly-generated-as-token";
|
||||
hsToken = "this-is-my-totally-randomly-generated-hs-token";
|
||||
in
|
||||
{
|
||||
name = "mautrix-meta-postgres";
|
||||
meta.maintainers = pkgs.mautrix-meta.meta.maintainers;
|
||||
|
||||
nodes = {
|
||||
server = { config, pkgs, ... }: {
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
|
||||
ensureUsers = [
|
||||
{
|
||||
name = "mautrix-meta-instagram";
|
||||
ensureDBOwnership = true;
|
||||
}
|
||||
];
|
||||
|
||||
ensureDatabases = [
|
||||
"mautrix-meta-instagram"
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.mautrix-meta-instagram = {
|
||||
wants = [ "postgres.service" ];
|
||||
after = [ "postgres.service" ];
|
||||
};
|
||||
|
||||
services.matrix-synapse = {
|
||||
enable = true;
|
||||
settings = {
|
||||
database.name = "sqlite3";
|
||||
|
||||
enable_registration = true;
|
||||
|
||||
# don't use this in production, always use some form of verification
|
||||
enable_registration_without_verification = true;
|
||||
|
||||
listeners = [ {
|
||||
# The default but tls=false
|
||||
bind_addresses = [
|
||||
"0.0.0.0"
|
||||
];
|
||||
port = 8008;
|
||||
resources = [ {
|
||||
"compress" = true;
|
||||
"names" = [ "client" ];
|
||||
} {
|
||||
"compress" = false;
|
||||
"names" = [ "federation" ];
|
||||
} ];
|
||||
tls = false;
|
||||
type = "http";
|
||||
} ];
|
||||
};
|
||||
};
|
||||
|
||||
services.mautrix-meta.instances.instagram = {
|
||||
enable = true;
|
||||
|
||||
environmentFile = pkgs.writeText ''my-secrets'' ''
|
||||
AS_TOKEN=${asToken}
|
||||
HS_TOKEN=${hsToken}
|
||||
'';
|
||||
|
||||
settings = {
|
||||
homeserver = {
|
||||
address = homeserverUrl;
|
||||
domain = homeserverDomain;
|
||||
};
|
||||
|
||||
appservice = {
|
||||
port = 8009;
|
||||
|
||||
as_token = "$AS_TOKEN";
|
||||
hs_token = "$HS_TOKEN";
|
||||
|
||||
database = {
|
||||
type = "postgres";
|
||||
uri = "postgres:///mautrix-meta-instagram?host=/var/run/postgresql";
|
||||
};
|
||||
|
||||
bot.username = botUserName;
|
||||
};
|
||||
|
||||
bridge.permissions."@${userName}:server" = "user";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 8008 8009 ];
|
||||
};
|
||||
|
||||
client = { pkgs, ... }: {
|
||||
environment.systemPackages = [
|
||||
(pkgs.writers.writePython3Bin "do_test"
|
||||
{
|
||||
libraries = [ pkgs.python3Packages.matrix-nio ];
|
||||
flakeIgnore = [
|
||||
# We don't live in the dark ages anymore.
|
||||
# Languages like Python that are whitespace heavy will overrun
|
||||
# 79 characters..
|
||||
"E501"
|
||||
];
|
||||
} ''
|
||||
import sys
|
||||
import functools
|
||||
import asyncio
|
||||
|
||||
from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse
|
||||
|
||||
|
||||
async def message_callback(matrix: AsyncClient, msg: str, _r, e):
|
||||
print("Received matrix text message: ", e)
|
||||
assert msg in e.body
|
||||
exit(0) # Success!
|
||||
|
||||
|
||||
async def run(homeserver: str):
|
||||
matrix = AsyncClient(homeserver)
|
||||
response = await matrix.register("${userName}", "foobar")
|
||||
print("Matrix register response: ", response)
|
||||
|
||||
# Open a DM with the bridge bot
|
||||
response = await matrix.room_create()
|
||||
print("Matrix create room response:", response)
|
||||
assert isinstance(response, RoomCreateResponse)
|
||||
room_id = response.room_id
|
||||
|
||||
response = await matrix.room_invite(room_id, "@${botUserName}:${homeserverDomain}")
|
||||
assert isinstance(response, RoomInviteResponse)
|
||||
|
||||
callback = functools.partial(
|
||||
message_callback, matrix, "Hello, I'm an Instagram bridge bot."
|
||||
)
|
||||
matrix.add_event_callback(callback, RoomMessageNotice)
|
||||
|
||||
print("Waiting for matrix message...")
|
||||
await matrix.sync_forever(timeout=30000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run(sys.argv[1]))
|
||||
''
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
def extract_token(data):
|
||||
stdout = data[1]
|
||||
stdout = stdout.strip()
|
||||
line = stdout.split('\n')[-1]
|
||||
return line.split(':')[-1].strip("\" '\n")
|
||||
|
||||
def get_token_from(token, file):
|
||||
data = server.execute(f"cat {file} | grep {token}")
|
||||
return extract_token(data)
|
||||
|
||||
def get_as_token_from(file):
|
||||
return get_token_from("as_token", file)
|
||||
|
||||
def get_hs_token_from(file):
|
||||
return get_token_from("hs_token", file)
|
||||
|
||||
config_yaml = "/var/lib/mautrix-meta-instagram/config.yaml"
|
||||
registration_yaml = "/var/lib/mautrix-meta-instagram/meta-registration.yaml"
|
||||
|
||||
expected_as_token = "${asToken}"
|
||||
expected_hs_token = "${hsToken}"
|
||||
|
||||
start_all()
|
||||
|
||||
with subtest("start the server"):
|
||||
# bridge
|
||||
server.wait_for_unit("mautrix-meta-instagram.service")
|
||||
|
||||
# homeserver
|
||||
server.wait_for_unit("matrix-synapse.service")
|
||||
|
||||
server.wait_for_open_port(8008)
|
||||
# Bridge only opens the port after it contacts the homeserver
|
||||
server.wait_for_open_port(8009)
|
||||
|
||||
with subtest("ensure messages can be exchanged"):
|
||||
client.succeed("do_test ${homeserverUrl} >&2")
|
||||
|
||||
with subtest("ensure as_token, hs_token match from environment file"):
|
||||
as_token = get_as_token_from(config_yaml)
|
||||
hs_token = get_hs_token_from(config_yaml)
|
||||
as_token_registration = get_as_token_from(registration_yaml)
|
||||
hs_token_registration = get_hs_token_from(registration_yaml)
|
||||
|
||||
assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})"
|
||||
assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})"
|
||||
assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})"
|
||||
assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})"
|
||||
|
||||
with subtest("ensure as_token and hs_token stays same after restart"):
|
||||
server.systemctl("restart mautrix-meta-instagram")
|
||||
server.wait_for_open_port(8009)
|
||||
|
||||
as_token = get_as_token_from(config_yaml)
|
||||
hs_token = get_hs_token_from(config_yaml)
|
||||
as_token_registration = get_as_token_from(registration_yaml)
|
||||
hs_token_registration = get_hs_token_from(registration_yaml)
|
||||
|
||||
assert as_token == expected_as_token, f"as_token in config should match the one specified (is: {as_token}, expected: {expected_as_token})"
|
||||
assert hs_token == expected_hs_token, f"hs_token in config should match the one specified (is: {hs_token}, expected: {expected_hs_token})"
|
||||
assert as_token_registration == expected_as_token, f"as_token in registration should match the one specified (is: {as_token_registration}, expected: {expected_as_token})"
|
||||
assert hs_token_registration == expected_hs_token, f"hs_token in registration should match the one specified (is: {hs_token_registration}, expected: {expected_hs_token})"
|
||||
'';
|
||||
})
|
247
nixos/tests/matrix/mautrix-meta-sqlite.nix
Normal file
247
nixos/tests/matrix/mautrix-meta-sqlite.nix
Normal file
@ -0,0 +1,247 @@
|
||||
import ../make-test-python.nix ({ pkgs, ... }:
|
||||
let
|
||||
homeserverDomain = "server";
|
||||
homeserverUrl = "http://server:8008";
|
||||
username = "alice";
|
||||
instagramBotUsername = "instagrambot";
|
||||
facebookBotUsername = "facebookbot";
|
||||
in
|
||||
{
|
||||
name = "mautrix-meta-sqlite";
|
||||
meta.maintainers = pkgs.mautrix-meta.meta.maintainers;
|
||||
|
||||
nodes = {
|
||||
server = { config, pkgs, ... }: {
|
||||
services.matrix-synapse = {
|
||||
enable = true;
|
||||
settings = {
|
||||
database.name = "sqlite3";
|
||||
|
||||
enable_registration = true;
|
||||
|
||||
# don't use this in production, always use some form of verification
|
||||
enable_registration_without_verification = true;
|
||||
|
||||
listeners = [ {
|
||||
# The default but tls=false
|
||||
bind_addresses = [
|
||||
"0.0.0.0"
|
||||
];
|
||||
port = 8008;
|
||||
resources = [ {
|
||||
"compress" = true;
|
||||
"names" = [ "client" ];
|
||||
} {
|
||||
"compress" = false;
|
||||
"names" = [ "federation" ];
|
||||
} ];
|
||||
tls = false;
|
||||
type = "http";
|
||||
} ];
|
||||
};
|
||||
};
|
||||
|
||||
services.mautrix-meta.instances.facebook = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
homeserver = {
|
||||
address = homeserverUrl;
|
||||
domain = homeserverDomain;
|
||||
};
|
||||
|
||||
appservice = {
|
||||
port = 8009;
|
||||
|
||||
bot.username = facebookBotUsername;
|
||||
};
|
||||
|
||||
bridge.permissions."@${username}:server" = "user";
|
||||
};
|
||||
};
|
||||
|
||||
services.mautrix-meta.instances.instagram = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
homeserver = {
|
||||
address = homeserverUrl;
|
||||
domain = homeserverDomain;
|
||||
};
|
||||
|
||||
appservice = {
|
||||
port = 8010;
|
||||
|
||||
bot.username = instagramBotUsername;
|
||||
};
|
||||
|
||||
bridge.permissions."@${username}:server" = "user";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 8008 ];
|
||||
};
|
||||
|
||||
client = { pkgs, ... }: {
|
||||
environment.systemPackages = [
|
||||
(pkgs.writers.writePython3Bin "register_user"
|
||||
{
|
||||
libraries = [ pkgs.python3Packages.matrix-nio ];
|
||||
flakeIgnore = [
|
||||
# We don't live in the dark ages anymore.
|
||||
# Languages like Python that are whitespace heavy will overrun
|
||||
# 79 characters..
|
||||
"E501"
|
||||
];
|
||||
} ''
|
||||
import sys
|
||||
import asyncio
|
||||
|
||||
from nio import AsyncClient
|
||||
|
||||
|
||||
async def run(username: str, homeserver: str):
|
||||
matrix = AsyncClient(homeserver)
|
||||
|
||||
response = await matrix.register(username, "foobar")
|
||||
print("Matrix register response: ", response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run(sys.argv[1], sys.argv[2]))
|
||||
''
|
||||
)
|
||||
(pkgs.writers.writePython3Bin "do_test"
|
||||
{
|
||||
libraries = [ pkgs.python3Packages.matrix-nio ];
|
||||
flakeIgnore = [
|
||||
# We don't live in the dark ages anymore.
|
||||
# Languages like Python that are whitespace heavy will overrun
|
||||
# 79 characters..
|
||||
"E501"
|
||||
];
|
||||
} ''
|
||||
import sys
|
||||
import functools
|
||||
import asyncio
|
||||
|
||||
from nio import AsyncClient, RoomMessageNotice, RoomCreateResponse, RoomInviteResponse
|
||||
|
||||
|
||||
async def message_callback(matrix: AsyncClient, msg: str, _r, e):
|
||||
print("Received matrix text message: ", e)
|
||||
assert msg in e.body
|
||||
exit(0) # Success!
|
||||
|
||||
|
||||
async def run(username: str, bot_username: str, homeserver: str):
|
||||
matrix = AsyncClient(homeserver, f"@{username}:${homeserverDomain}")
|
||||
|
||||
response = await matrix.login("foobar")
|
||||
print("Matrix login response: ", response)
|
||||
|
||||
# Open a DM with the bridge bot
|
||||
response = await matrix.room_create()
|
||||
print("Matrix create room response:", response)
|
||||
assert isinstance(response, RoomCreateResponse)
|
||||
room_id = response.room_id
|
||||
|
||||
response = await matrix.room_invite(room_id, f"@{bot_username}:${homeserverDomain}")
|
||||
assert isinstance(response, RoomInviteResponse)
|
||||
|
||||
callback = functools.partial(
|
||||
message_callback, matrix, "Hello, I'm an Instagram bridge bot."
|
||||
)
|
||||
matrix.add_event_callback(callback, RoomMessageNotice)
|
||||
|
||||
print("Waiting for matrix message...")
|
||||
await matrix.sync_forever(timeout=30000)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run(sys.argv[1], sys.argv[2], sys.argv[3]))
|
||||
''
|
||||
)
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
def extract_token(data):
|
||||
stdout = data[1]
|
||||
stdout = stdout.strip()
|
||||
line = stdout.split('\n')[-1]
|
||||
return line.split(':')[-1].strip("\" '\n")
|
||||
|
||||
def get_token_from(token, file):
|
||||
data = server.execute(f"cat {file} | grep {token}")
|
||||
return extract_token(data)
|
||||
|
||||
def get_as_token_from(file):
|
||||
return get_token_from("as_token", file)
|
||||
|
||||
def get_hs_token_from(file):
|
||||
return get_token_from("hs_token", file)
|
||||
|
||||
config_yaml = "/var/lib/mautrix-meta-facebook/config.yaml"
|
||||
registration_yaml = "/var/lib/mautrix-meta-facebook/meta-registration.yaml"
|
||||
|
||||
start_all()
|
||||
|
||||
with subtest("wait for bridges and homeserver"):
|
||||
# bridge
|
||||
server.wait_for_unit("mautrix-meta-facebook.service")
|
||||
server.wait_for_unit("mautrix-meta-instagram.service")
|
||||
|
||||
# homeserver
|
||||
server.wait_for_unit("matrix-synapse.service")
|
||||
|
||||
server.wait_for_open_port(8008)
|
||||
# Bridges only open the port after they contact the homeserver
|
||||
server.wait_for_open_port(8009)
|
||||
server.wait_for_open_port(8010)
|
||||
|
||||
with subtest("register user"):
|
||||
client.succeed("register_user ${username} ${homeserverUrl} >&2")
|
||||
|
||||
with subtest("ensure messages can be exchanged"):
|
||||
client.succeed("do_test ${username} ${facebookBotUsername} ${homeserverUrl} >&2")
|
||||
client.succeed("do_test ${username} ${instagramBotUsername} ${homeserverUrl} >&2")
|
||||
|
||||
with subtest("ensure as_token and hs_token stays same after restart"):
|
||||
generated_as_token_facebook = get_as_token_from(config_yaml)
|
||||
generated_hs_token_facebook = get_hs_token_from(config_yaml)
|
||||
|
||||
generated_as_token_facebook_registration = get_as_token_from(registration_yaml)
|
||||
generated_hs_token_facebook_registration = get_hs_token_from(registration_yaml)
|
||||
|
||||
# Indirectly checks the as token is not set to something like empty string or "null"
|
||||
assert len(generated_as_token_facebook) > 20, f"as_token ({generated_as_token_facebook}) is too short, something went wrong"
|
||||
assert len(generated_hs_token_facebook) > 20, f"hs_token ({generated_hs_token_facebook}) is too short, something went wrong"
|
||||
|
||||
assert generated_as_token_facebook == generated_as_token_facebook_registration, f"as_token should be the same in registration ({generated_as_token_facebook_registration}) and configuration ({generated_as_token_facebook}) files"
|
||||
assert generated_hs_token_facebook == generated_hs_token_facebook_registration, f"hs_token should be the same in registration ({generated_hs_token_facebook_registration}) and configuration ({generated_hs_token_facebook}) files"
|
||||
|
||||
server.systemctl("restart mautrix-meta-facebook")
|
||||
server.systemctl("restart mautrix-meta-instagram")
|
||||
|
||||
server.wait_for_open_port(8009)
|
||||
server.wait_for_open_port(8010)
|
||||
|
||||
new_as_token_facebook = get_as_token_from(config_yaml)
|
||||
new_hs_token_facebook = get_hs_token_from(config_yaml)
|
||||
|
||||
assert generated_as_token_facebook == new_as_token_facebook, f"as_token should stay the same after restart inside the configuration file (is: {new_as_token_facebook}, was: {generated_as_token_facebook})"
|
||||
assert generated_hs_token_facebook == new_hs_token_facebook, f"hs_token should stay the same after restart inside the configuration file (is: {new_hs_token_facebook}, was: {generated_hs_token_facebook})"
|
||||
|
||||
new_as_token_facebook = get_as_token_from(registration_yaml)
|
||||
new_hs_token_facebook = get_hs_token_from(registration_yaml)
|
||||
|
||||
assert generated_as_token_facebook == new_as_token_facebook, f"as_token should stay the same after restart inside the registration file (is: {new_as_token_facebook}, was: {generated_as_token_facebook})"
|
||||
assert generated_hs_token_facebook == new_hs_token_facebook, f"hs_token should stay the same after restart inside the registration file (is: {new_hs_token_facebook}, was: {generated_hs_token_facebook})"
|
||||
|
||||
with subtest("ensure messages can be exchanged after restart"):
|
||||
client.succeed("do_test ${username} ${instagramBotUsername} ${homeserverUrl} >&2")
|
||||
client.succeed("do_test ${username} ${facebookBotUsername} ${homeserverUrl} >&2")
|
||||
'';
|
||||
})
|
Loading…
Reference in New Issue
Block a user