2025-05-07 18:14:39 +10:00
{
lib ,
config ,
pkgs ,
. . .
} :
let
cfg = config . services . mautrix-discord ;
dataDir = cfg . dataDir ;
format = pkgs . formats . yaml { } ;
registrationFile = " ${ dataDir } / d i s c o r d - r e g i s t r a t i o n . y a m l " ;
settingsFile = " ${ dataDir } / c o n f i g . y a m l " ;
settingsFileUnformatted = format . generate " d i s c o r d - c o n f i g - u n s u b s t i t u t e d . y a m l " cfg . settings ;
in
{
options = {
services . mautrix-discord = {
enable = lib . mkEnableOption " M a u t r i x - D i s c o r d , a M a t r i x - D i s c o r d p u p p e t i n g / r e l a y - b o t b r i d g e " ;
package = lib . mkOption {
type = lib . types . package ;
default = pkgs . mautrix-discord ;
defaultText = lib . literalExpression " p k g s . m a u t r i x - d i s c o r d " ;
description = ''
The mautrix-discord package to use .
'' ;
} ;
settings = lib . mkOption {
type = lib . types . submodule {
freeformType = format . type ;
config = {
_module . args = { inherit cfg lib ; } ;
} ;
options = {
homeserver = lib . mkOption {
type = lib . types . attrs ;
default = {
software = " s t a n d a r d " ;
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 = {
2025-07-19 17:26:41 +02:00
address = " h t t p : / / l o c a l h o s t : 2 9 3 3 4 " ;
hostname = " 0 . 0 . 0 . 0 " ;
port = 29334 ;
database = {
type = " s q l i t e 3 " ;
uri = " f i l e : / v a r / l i b / m a u t r i x - d i s c o r d / m a u t r i x - d i s c o r d . d b ? _ t x l o c k = i m m e d i a t e " ;
max_open_conns = 20 ;
max_idle_conns = 2 ;
max_conn_idle_time = null ;
max_conn_lifetime = null ;
} ;
2025-05-07 18:14:39 +10:00
id = " d i s c o r d " ;
bot = {
username = " d i s c o r d b o t " ;
displayname = " D i s c o r d b r i d g e b o t " ;
avatar = " m x c : / / m a u n i u m . n e t / n I d E y k e m n w d i s v H b p x f l p D l C " ;
} ;
2025-07-19 17:26:41 +02:00
ephemeral_events = true ;
async_transactions = false ;
as_token = " T h i s v a l u e i s g e n e r a t e d w h e n g e n e r a t i n g t h e r e g i s t r a t i o n " ;
hs_token = " T h i s v a l u e i s g e n e r a t e d w h e n g e n e r a t i n g t h e r e g i s t r a t i o n " ;
2025-05-07 18:14:39 +10:00
} ;
defaultText = lib . literalExpression ''
{
2025-07-19 17:26:41 +02:00
address = " h t t p : / / l o c a l h o s t : 2 9 3 3 4 " ;
hostname = " 0 . 0 . 0 . 0 " ;
port = 29334 ;
database = {
type = " s q l i t e 3 " ;
uri = " f i l e : ' ' ${ config . services . mautrix-discord . dataDir } / m a u t r i x - d i s c o r d . d b ? _ t x l o c k = i m m e d i a t e " ;
max_open_conns = 20 ;
max_idle_conns = 2 ;
max_conn_idle_time = null ;
max_conn_lifetime = null ;
} ;
2025-05-07 18:14:39 +10:00
id = " d i s c o r d " ;
bot = {
username = " d i s c o r d b o t " ;
displayname = " D i s c o r d b r i d g e b o t " ;
avatar = " m x c : / / m a u n i u m . n e t / n I d E y k e m n w d i s v H b p x f l p D l C " ;
} ;
2025-07-19 17:26:41 +02:00
ephemeral_events = true ;
async_transactions = false ;
as_token = " T h i s v a l u e i s g e n e r a t e d w h e n g e n e r a t i n g t h e r e g i s t r a t i o n " ;
hs_token = " T h i s v a l u e i s g e n e r a t e d w h e n g e n e r a t i n g t h e r e g i s t r a t i o n " ;
2025-05-07 18:14:39 +10:00
}
'' ;
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 = " d i s c o r d _ { { . } } " ;
2025-07-19 15:38:05 +02:00
displayname_template = " { { i f . W e b h o o k } } W e b h o o k { { e l s e } } { { o r . G l o b a l N a m e . U s e r n a m e } } { { i f . B o t } } ( b o t ) { { e n d } } { { e n d } } " ;
2025-05-07 18:14:39 +10:00
channel_name_template = " { { i f o r ( e q . T y p e 3 ) ( e q . T y p e 4 ) } } { { . N a m e } } { { e l s e } } # { { . N a m e } } { { e n d } } " ;
guild_name_template = " { { . N a m e } } " ;
private_chat_portal_meta = " d e f a u l t " ;
public_address = null ;
avatar_proxy_key = " g e n e r a t e " ;
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 ;
2025-07-19 15:38:05 +02:00
prefix_webhook_messages = true ;
enable_webhook_avatars = false ;
2025-05-07 18:14:39 +10:00
use_discord_cdn_upload = true ;
2025-07-19 17:26:41 +02:00
#proxy =
2025-05-07 18:14:39 +10:00
cache_media = " u n e n c r y p t e d " ;
direct_media = {
enabled = false ;
2025-07-19 17:26:41 +02:00
#server_name = "discord-media.example.com";
#well_known_response =
2025-05-07 18:14:39 +10:00
allow_proxy = true ;
server_key = " g e n e r a t e " ;
} ;
animated_sticker = {
target = " w e b p " ;
args = {
width = 320 ;
height = 320 ;
fps = 25 ;
} ;
} ;
2025-07-19 17:26:41 +02:00
double_puppet_server_map = {
#"example.com" = "https://example.com";
} ;
double_puppet_allow_discovery = false ;
login_shared_secret_map = {
#"example.com" = "foobar";
} ;
2025-05-07 18:14:39 +10:00
command_prefix = " ! d i s c o r d " ;
management_room_text = {
welcome = " H e l l o , I ' m a D i s c o r d b r i d g e b o t . " ;
welcome_connected = " U s e ` h e l p ` f o r h e l p . " ;
welcome_unconnected = " U s e ` h e l p ` f o r h e l p o r ` l o g i n ` t o l o g i n . " ;
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 = " u n v e r i f i e d " ;
send = " u n v e r i f i e d " ;
share = " c r o s s - s i g n e d - t o f u " ;
} ;
rotation = {
enable_custom = false ;
milliseconds = 604800000 ;
messages = 100 ;
disable_device_change_key_rotation = false ;
} ;
} ;
provisioning = {
prefix = " / _ m a t r i x / p r o v i s i o n " ;
shared_secret = " g e n e r a t e " ;
debug_endpoints = false ;
} ;
permissions = {
" * " = " r e l a y " ;
2025-07-19 17:26:41 +02:00
#"example.com" = "user";
#"@admin:example.com": "admin";
2025-05-07 18:14:39 +10:00
} ;
} ;
description = ''
Bridge configuration .
See [ example-config . yaml ] ( https://github.com/mautrix/discord/blob/main/example-config.yaml )
for more information .
'' ;
} ;
2025-07-19 17:26:41 +02:00
logging = lib . mkOption {
type = lib . types . attrs ;
default = {
min_level = " i n f o " ;
writers = lib . singleton {
type = " s t d o u t " ;
format = " p r e t t y - c o l o r e d " ;
time_format = " " ;
} ;
} ;
description = ''
Logging configuration .
See [ example-config . yaml ] ( https://github.com/mautrix/discord/blob/main/example-config.yaml )
for more information .
'' ;
} ;
2025-05-07 18:14:39 +10:00
} ;
} ;
default = { } ;
example = lib . literalExpression ''
{
homeserver = {
address = " h t t p : / / l o c a l h o s t : 8 0 0 8 " ;
domain = " p u b l i c - d o m a i n . t l d " ;
} ;
appservice . public = {
prefix = " / p u b l i c " ;
external = " h t t p s : / / p u b l i c - a p p s e r v i c e - a d d r e s s / p u b l i c " ;
} ;
bridge . permissions = {
2025-07-19 17:26:41 +02:00
" e x a m p l e . c o m " = " u s e r " ;
2025-05-07 18:14:39 +10:00
" @ a d m i n : e x a m p l e . c o m " = " a d m i n " ;
} ;
}
'' ;
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 " c o n f i g . s e r v i c e s . m a t r i x - s y n a p s e . e n a b l e " ;
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 = " / v a r / l i b / m a u t r i x - d i s c o r d " ;
defaultText = " / v a r / l i b / m a u t r i x - d i s c o r d " ;
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 = " $ M A U T R I X _ D I S C O R D _ A P P S E R V I C E _ A S _ T O K E N " `
and then specify ` MAUTRIX_DISCORD_APPSERVICE_AS_TOKEN = " { t o k e n } " ` 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 = " m a u t r i x - d i s c o r d . s e r v i c e " ;
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 = " m a u t r i x - d i s c o r d - r e g i s t r a t i o n . s e r v i c e " ;
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 " m a t r i x - c o n d u i t . s e r v i c e " )
++ ( lib . lists . optional config . services . dendrite . enable " d e n d r i t e . s e r v i c e " ) ;
defaultText = ''
[ cfg . registrationServiceUnit ] ++
( lib . lists . optional config . services . matrix-synapse . enable config . services . matrix-synapse . serviceUnit ) ++
( lib . lists . optional config . services . matrix-conduit . enable " m a t r i x - c o n d u i t . s e r v i c e " ) ++
( lib . lists . optional config . services . dendrite . enable " d e n d r i t e . s e r v i c e " ) ;
'' ;
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 = " m a u t r i x - d i s c o r d " ;
extraGroups = [ " m a u t r i x - d i s c o r d - r e g i s t r a t i o n " ] ;
home = dataDir ;
description = " M a u t r i x - D i s c o r d b r i d g e u s e r " ;
} ;
users . groups . mautrix-discord = { } ;
users . groups . mautrix-discord-registration = {
members = lib . lists . optional config . services . matrix-synapse . enable " m a t r i x - s y n a p s e " ;
} ;
services . matrix-synapse = lib . mkIf cfg . registerToSynapse {
settings . app_service_config_files = [ registrationFile ] ;
} ;
systemd . tmpfiles . rules = [
" d ${ cfg . dataDir } 7 7 0 m a u t r i x - d i s c o r d m a u t r i x - d i s c o r d - "
] ;
systemd . services = {
matrix-synapse = lib . mkIf cfg . registerToSynapse {
serviceConfig . SupplementaryGroups = [ " m a u t r i x - d i s c o r d - r e g i s t r a t i o n " ] ;
# Make synapse depend on the registration service when auto-registering
wants = [ " m a u t r i x - d i s c o r d - r e g i s t r a t i o n . s e r v i c e " ] ;
after = [ " m a u t r i x - d i s c o r d - r e g i s t r a t i o n . s e r v i c e " ] ;
} ;
mautrix-discord-registration = {
description = " M a u t r i x - D i s c o r d r e g i s t r a t i o n g e n e r a t i o n s e r v i c e " ;
wantedBy = lib . mkIf cfg . registerToSynapse [ " m u l t i - u s e r . t a r g e t " ] ;
before = lib . mkIf cfg . registerToSynapse [ " m a t r i x - s y n a p s e . s e r v i c e " ] ;
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 ( " a s _ t o k e n " ) and has ( " h s _ t o k e n " ) ' ' $ { settingsFile } ' )
registration_already_exists = $ ( [ [ - f ' $ { registrationFile } ' ] ] && echo " t r u e " || echo " f a l s e " )
echo " T h e r e a r e t o k e n s i n t h e c o n f i g : $ c o n f i g _ h a s _ t o k e n s "
echo " R e g i s t r a t i o n a l r e a d y e x i s t e d : $ r e g i s t r a t i o n _ a l r e a d y _ e x i s t s "
# 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 = = " f a l s e " && $ registration_already_exists = = " t r u e " ] ] ; then
echo " C o p y i n g a s _ t o k e n , h s _ t o k e n f r o m r e g i s t r a t i o n i n t o c o n f i g u r a t i o n "
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 " G e n e r a t i n g r e g i s t r a t i o n f i l e "
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 = = " f a l s e " && $ registration_already_exists = = " f a l s e " ] ] ; then
echo " C o p y i n g n e w l y g e n e r a t e d a s _ t o k e n , h s _ t o k e n f r o m r e g i s t r a t i o n i n t o c o n f i g u r a t i o n "
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 " G e n e r a t i n g r e g i s t r a t i o n f i l e "
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 = = " f a l s e " && $ registration_already_exists = = " f a l s e " ] ] ; then
echo " C o p y i n g n e w l y g e n e r a t e d a s _ t o k e n , h s _ t o k e n f r o m r e g i s t r a t i o n i n t o c o n f i g u r a t i o n "
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 = = " t r u e " || $ registration_already_exists = = " t r u e " ] ] ; then
echo " C o p y i n g a s _ t o k e n , h s _ t o k e n f r o m c o n f i g u r a t i o n t o t h e r e g i s t r a t i o n f i l e "
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 = " o n e s h o t " ;
RemainAfterExit = true ;
UMask = 27 ;
User = " m a u t r i x - d i s c o r d " ;
Group = " m a u t r i x - d i s c o r d " ;
SystemCallFilter = [ " @ s y s t e m - s e r v i c e " ] ;
ProtectSystem = " s t r i c t " ;
ProtectHome = true ;
ReadWritePaths = [ dataDir ] ;
StateDirectory = " m a u t r i x - d i s c o r d " ;
EnvironmentFile = cfg . environmentFile ;
} ;
restartTriggers = [ settingsFileUnformatted ] ;
} ;
mautrix-discord = {
description = " M a u t r i x - D i s c o r d , a M a t r i x - D i s c o r d p u p p e t i n g / r e l a y b o t b r i d g e " ;
wantedBy = [ " m u l t i - u s e r . t a r g e t " ] ;
wants = [ " n e t w o r k - o n l i n e . t a r g e t " ] ++ cfg . serviceDependencies ;
after = [ " n e t w o r k - o n l i n e . t a r g e t " ] ++ cfg . serviceDependencies ;
path = [
pkgs . lottieconverter
pkgs . ffmpeg-headless
] ;
serviceConfig = {
Type = " s i m p l e " ;
User = " m a u t r i x - d i s c o r d " ;
Group = " m a u t r i x - d i s c o r d " ;
PrivateUsers = true ;
Restart = " o n - f a i l u r e " ;
RestartSec = 30 ;
WorkingDirectory = dataDir ;
ExecStart = ''
$ { lib . getExe cfg . package } \
- - config = ' $ { settingsFile } '
'' ;
EnvironmentFile = cfg . environmentFile ;
ProtectSystem = " s t r i c t " ;
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 = " n a t i v e " ;
SystemCallErrorNumber = " E P E R M " ;
SystemCallFilter = " @ s y s t e m - s e r v i c e " ;
ReadWritePaths = [ cfg . dataDir ] ;
} ;
restartTriggers = [ settingsFileUnformatted ] ;
} ;
} ;
meta = {
maintainers = with lib . maintainers ; [
mistyttm
] ;
} ;
} ;
}