Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/mautrix-discord: init #284421

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions maintainers/maintainer-list.nix
Expand Up @@ -16480,6 +16480,13 @@
githubId = 521306;
name = "Rob Glossop";
};
robin = {
name = "Robin";
email = "robin@robin.town";
matrix = "@robin:robin.town";
github = "robintown";
githubId = 48614497;
};
roblabla = {
email = "robinlambertz+dev@gmail.com";
github = "roblabla";
Expand Down
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2405.section.md
Expand Up @@ -66,6 +66,8 @@ In addition to numerous new and upgraded packages, this release has the followin

- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).

- [mautrix-discord](https://go.mau.fi/mautrix-discord/), a Matrix to Discord hybrid puppeting/relaybot bridge.

- systemd's gateway, upload, and remote services, which provides ways of sending journals across the network. Enable using [services.journald.gateway](#opt-services.journald.gateway.enable), [services.journald.upload](#opt-services.journald.upload.enable), and [services.journald.remote](#opt-services.journald.remote.enable).

- [GNS3](https://www.gns3.com/), a network software emulator. Available as [services.gns3-server](#opt-services.gns3-server.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Expand Up @@ -644,6 +644,7 @@
./services/matrix/dendrite.nix
./services/matrix/hebbot.nix
./services/matrix/maubot.nix
./services/matrix/mautrix-discord.nix
./services/matrix/mautrix-facebook.nix
./services/matrix/mautrix-telegram.nix
./services/matrix/mautrix-whatsapp.nix
Expand Down
151 changes: 151 additions & 0 deletions nixos/modules/services/matrix/mautrix-discord.nix
@@ -0,0 +1,151 @@
{ config, pkgs, lib, ... }:

let
cfg = config.services.mautrix-discord;
dataDir = "/var/lib/mautrix-discord";
registrationFile = "${dataDir}/discord-registration.yaml";
settingsFormat = pkgs.formats.yaml { };
settingsFile = settingsFormat.generate "mautrix-discord-config.yaml" cfg.settings;
runtimeSettingsFile = "${dataDir}/config.yaml";
in {
options = {
services.mautrix-discord = {
enable = lib.mkEnableOption "Matrix to Discord hybrid puppeting/relaybot bridge";

package = lib.mkOption {
type = lib.types.package;
default = pkgs.mautrix-discord;
defaultText = lib.literalExpression "pkgs.mautrix-discord";
description = "The mautrix-discord package to use.";
};

settings = lib.mkOption rec {
apply = lib.recursiveUpdate default;
inherit (settingsFormat) type;
default = {
homeserver = {
software = "standard";
};

appservice = rec {
database = {
type = "sqlite3";
uri = "file:${dataDir}/mautrix-discord.db";
};
port = 8080;
address = "http://localhost:${toString port}";
};

bridge = {
permissions."*" = "relay";
double_puppet_server_map = {};
login_shared_secret_map = {};
};

logging = {
directory = "";
file_name_format = ""; # Disable file logging
file_date_format = "2006-01-02";
file_mode = 384;
timestamp_format = "Jan _2, 2006 15:04:05";
print_level = "warn";
print_json = false;
file_json = false;
};
};
description = ''
Bridge 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).
'';
};

serviceDependencies = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = lib.optional config.services.matrix-synapse.enable "matrix-synapse.service";
defaultText = lib.literalExpression ''
optional config.services.matrix-synapse.enable "matrix-synapse.service"
'';
description = "List of Systemd services to require and wait for when starting the application service.";
};
};
};

config = lib.mkIf cfg.enable {
systemd.services.mautrix-discord = lib.mkIf cfg.enable {
description = "Matrix to Discord hybrid puppeting/relaybot bridge";

wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
after = [ "network-online.target" ] ++ cfg.serviceDependencies;

preStart = ''
# Generate the appservice's registration file if absent
if [ ! -f '${registrationFile}' ]; then
${lib.getExe cfg.package} \
--config '${settingsFile}' \
--registration '${registrationFile}' \
--generate-registration
fi

old_umask=$(umask)
umask 0177
# Extract the AS and HS tokens from the registration and add them to the settings file
${lib.getExe pkgs.yq} -y ".appservice.as_token = $(${lib.getExe pkgs.yq} .as_token ${registrationFile}) | .appservice.hs_token = $(${lib.getExe pkgs.yq} .hs_token ${registrationFile})" ${settingsFile} > ${runtimeSettingsFile}
umask $old_umask
'';

serviceConfig =
let
needsPrivileges = cfg.settings.appservice.port < 1024;
capabilities = [ (if needsPrivileges then "CAP_NET_BIND_SERVICE" else "") ];
in {
Type = "simple";
Restart = "always";

DynamicUser = true;
WorkingDirectory = cfg.package;
StateDirectory = baseNameOf dataDir;
UMask = "0007";

ExecStart = ''
${lib.getExe cfg.package} \
--config ${runtimeSettingsFile} \
--no-update
'';

TemporaryFileSystem = [ "/" ];
BindPaths = [ dataDir ];
BindReadOnlyPaths = [ builtins.storeDir ];
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = !needsPrivileges;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" ];
};
};
};

meta.maintainers = with lib.maintainers; [ robin ];
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Expand Up @@ -517,6 +517,7 @@ in {
matrix-synapse = handleTest ./matrix/synapse.nix {};
matrix-synapse-workers = handleTest ./matrix/synapse-workers.nix {};
mattermost = handleTest ./mattermost.nix {};
mautrix-discord = handleTest ./matrix/mautrix-discord.nix {};
mealie = handleTest ./mealie.nix {};
mediamtx = handleTest ./mediamtx.nix {};
mediatomb = handleTest ./mediatomb.nix {};
Expand Down
154 changes: 154 additions & 0 deletions nixos/tests/matrix/mautrix-discord.nix
@@ -0,0 +1,154 @@
import ../make-test-python.nix ({ pkgs, ... }:
let
homeserverUrl = "http://homeserver:8008";
in
{
name = "mautrix-discord";
meta.maintainers = pkgs.mautrix-discord.meta.maintainers;

nodes = {
homeserver = { pkgs, ... }: {
# We'll switch to this once the registration is copied into place
specialisation.running.configuration = {
services.matrix-synapse = {
enable = true;
settings = {
database.name = "sqlite3";
app_service_config_files = [ "/discord-registration.yaml" ];

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";
} ];
};
};

networking.firewall.allowedTCPPorts = [ 8008 ];
};
};

bridge = { pkgs, ... }: {
services.mautrix-discord = {
enable = true;

settings = {
homeserver = {
address = homeserverUrl;
domain = "homeserver";
};

appservice = {
address = "http://bridge:8009";
port = 8009;
};

bridge.permissions."@alice:homeserver" = "user";
};
};

networking.firewall.allowedTCPPorts = [ 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("alice", "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, "@discordbot:homeserver")
assert isinstance(response, RoomInviteResponse)

callback = functools.partial(
message_callback, matrix, "Hello, I'm a Discord 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 = ''
import pathlib
import os

start_all()

with subtest("start the bridge"):
bridge.wait_for_unit("mautrix-discord.service")

with subtest("copy the registration file"):
bridge.copy_from_vm("/var/lib/mautrix-discord/discord-registration.yaml")
homeserver.copy_from_host(
str(pathlib.Path(os.environ.get("out", os.getcwd())) / "discord-registration.yaml"), "/"
)
homeserver.succeed("chmod 444 /discord-registration.yaml")

with subtest("start the homeserver"):
homeserver.succeed(
"/run/current-system/specialisation/running/bin/switch-to-configuration test >&2"
)

homeserver.wait_for_unit("matrix-synapse.service")
homeserver.wait_for_open_port(8008)
# Bridge only opens the port after it contacts the homeserver
bridge.wait_for_open_port(8009)

with subtest("ensure messages can be exchanged"):
client.succeed("do_test ${homeserverUrl} >&2")
'';
})
10 changes: 7 additions & 3 deletions pkgs/servers/mautrix-discord/default.nix
Expand Up @@ -4,6 +4,7 @@
, olm
, nix-update-script
, testers
, nixosTests
, mautrix-discord
}:

Expand All @@ -29,8 +30,11 @@ buildGoModule rec {

passthru = {
updateScript = nix-update-script { };
tests.version = testers.testVersion {
package = mautrix-discord;
tests = {
mautrix-discord = nixosTests.mautrix-discord;
version = testers.testVersion {
package = mautrix-discord;
};
};
};

Expand All @@ -39,7 +43,7 @@ buildGoModule rec {
homepage = "https://github.com/mautrix/discord";
changelog = "https://github.com/mautrix/discord/blob/${src.rev}/CHANGELOG.md";
license = licenses.agpl3Only;
maintainers = with maintainers; [ MoritzBoehme ];
maintainers = with maintainers; [ MoritzBoehme robin ];
mainProgram = "mautrix-discord";
};
}