{ config, lib, pkgs, ... }: with lib; let hostname = config.instance.hostname; siteOpts = { ... }: with types; { options = { enableACME = mkOption { type = bool; description = "Use ACME to get SSL certificates for this site."; default = true; }; site-config = mkOption { type = attrs; description = "Site-specific configuration."; }; }; }; site-copy = site: "ejabberd-${site}"; concatMapAttrs = f: attrs: foldr (a: b: a // b) {} (mapAttrs f attrs); concatMapAttrsToList = f: attr: attrValues (concatMapAttrs f attr); host-domains = config.fudo.acme.host-domains.${hostname}; siteCerts = site: let certPath = config.fudo.acme.local-copies.${site-copy site}.path; in [ "${certPath}/fullchain.pem" "${certPath}/privkey.pem" "${certPath}/chain.pem" ]; siteCertService = site: config.fudo.acme.local-copies.${site-copy site}.service; config-file-template = let jabber-config = { loglevel = cfg.log-level; access_rules = { c2s = { allow = "all"; }; announce = { allow = "admin"; }; configure = { allow = "admin"; }; pubsub_createnode = { allow = "local"; }; }; acl = { admin = { user = concatMap (admin: map (site: "${admin}@${site}") (attrNames cfg.sites)) cfg.admins; }; }; hosts = attrNames cfg.sites; listen = [{ port = cfg.port; module = "ejabberd_c2s"; ip = cfg.listen-ip; starttls = true; starttls_required = true; }]; certfiles = concatMapAttrsToList (site: siteOpts: if (siteOpts.enableACME) then (siteCerts site) else []) cfg.sites; host_config = mapAttrs (site: siteOpts: siteOpts.site-config) cfg.sites; }; config-file = builtins.toJSON jabber-config; in pkgs.writeText "ejabberd.config.yml.template" config-file; enter-secrets = template: secrets: target: let secret-readers = concatStringsSep "\n" (mapAttrsToList (secret: file: "${secret}=$(cat ${file})") secrets); secret-swappers = map (secret: "sed s/${secret}/\$${secret}/g") (attrNames secrets); swapper = concatStringsSep " | " secret-swappers; in pkgs.writeShellScript "ejabberd-generate-config.sh" '' cat ${template} | ${swapper} > ${target} chown ${cfg.user}:${cfg.group} ${target} ''; cfg = config.fudo.jabber; in { options.fudo.jabber = with types; { enable = mkEnableOption "Enable ejabberd server."; port = mkOption { type = port; description = "Port on which to listen for Jabber connections."; default = 5222; }; user = mkOption { type = str; description = "User as which to run the ejabberd server."; default = "ejabberd"; }; group = mkOption { type = str; description = "Group as which to run the ejabberd server."; default = "ejabberd"; }; admins = mkOption { type = str; description = "List of admin users for the server."; default = []; }; sites = mkOption { type = attrsOf (submodule siteOpts); description = "List of sites on which to listen for Jabber connections."; }; secret-files = mkOption { type = attrsOf str; description = "Map of secret-name to file. File contents will be subbed for the name in the config."; default = {}; }; config-file = mkOption { type = str; description = "Location at which to generate the configuration file."; default = "/var/run/ejabberd/ejabberd.yaml"; }; }; config = mkIf cfg.enable { users = { users.${cfg.user} = { isSystemUser = true; }; groups.${cfg.group} = { members = [ cfg.user ]; }; }; fudo.acme.local-copies = mapAttrs' (site: siteCfg: nameValuePair (site-copy site) mkif siteCfg.enableACME { domain = site; user = cfg.user; group = cfg.group; }) cfg.sites; systemd = { tmpfiles.rules = [ "D '${dirOf cfg.config-file}' 0550 ${cfg.user} ${cfg.group} - -" ]; services.ejabberd = let config-generator = enter-secrets config-file-template cfg.secret-files cfg.config-file; in { wants = map (site: siteCertService site) (attrNames cfg.sites); environment = cfg.secret-files; serviceConfig = { ExecStartPre = mkAfter "${config-generator}"; }; }; }; services.ejabberd = { enable = true; user = cfg.user; group = cfg.group; configFile = cfg.config-file; }; }; }