nginx: use julm-nix's profile
[sourcephile-nix.git] / nixos / modules / services / networking / prosody.nix
index 184726bcde8ecc71a14002b7f0f255d31071461c..0470cd840e48fd428fc271f654b1f591ddd1fb6b 100644 (file)
-{ config, lib, pkgs, ... }:
-
-with lib;
+{ options, config, pkgs, lib, ... }:
 let
-  cfg = config.services.prosody;
+  inherit (lib) isAttrs isBool isList isInt isString;
+  inherit (lib) mkChangedOptionModule mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkRenamedOptionModule;
+  inherit (lib) all any attrValues boolToString concatStringsSep elem filterAttrs genAttrs hasPrefix listToAttrs literalExpression mapAttrsToList mdDoc optionals optionalAttrs optionalString types unique;
 
-  sslOpts = { ... }: {
+  config' = config;
+  cfg = config.services.prosody;
 
-    options = {
+  luaType = with types; let
+    valueType = nullOr
+      (oneOf [
+        bool
+        int
+        float
+        str
+        path
+        (attrsOf valueType)
+        (listOf valueType)
+      ]) // {
+      description = "Lua value";
+    };
+  in
+  valueType;
+
+  sslOption = mkOption {
+    type = with types; nullOr (submodule {
+      freeformType = luaType;
+      options = {
+
+        key = mkOption {
+          type = types.str;
+          description = mdDoc ''
+            Path to your private key file.
+          '';
+        };
 
-      key = mkOption {
-        type = types.path;
-        description = "Path to the key file.";
-      };
+        certificate = mkOption {
+          type = types.str;
+          description = mdDoc ''
+            Path to your certificate file.
+          '';
+        };
 
-      # TODO: rename to certificate to match the prosody config
-      cert = mkOption {
-        type = types.path;
-        description = "Path to the certificate file.";
-      };
+        cafile = mkOption {
+          type = types.str;
+          default = "/etc/ssl/certs/ca-bundle.crt";
+          description = mdDoc ''
+            Path to a file containing root certificates that you wish Prosody to trust.
+          '';
+        };
 
-      extraOptions = mkOption {
-        type = types.attrs;
-        default = {};
-        description = "Extra SSL configuration options.";
       };
+    });
+    default = null;
+    description = mdDoc ''
+      Advanced SSL/TLS configuration, as described by <https://prosody.im/doc/advanced_ssl_config>.
 
-    };
-  };
-
-  discoOpts = {
-    options = {
-      url = mkOption {
-        type = types.str;
-        description = "URL of the endpoint you want to make discoverable";
-      };
-      description = mkOption {
-        type = types.str;
-        description = "A short description of the endpoint you want to advertise";
-      };
-    };
+      ::: {.note}
+      Prosody passes the contents of the `ssl` option from the config file almost directly to LuaSec, the
+      library used for SSL/TLS support in Prosody. LuaSec accepts a range of options here, mostly things
+      that it passes directly to OpenSSL. It is recommended to leave Prosody's defaults in most cases, unless
+      you know what you are doing.
+      You could easily reduce security or introduce unnecessary compatibility issues with clients and other servers!
+      :::
+    '';
   };
 
-  moduleOpts = {
-    # Required for compliance with https://compliance.conversations.im/about/
-    roster = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allow users to have a roster";
-    };
-
-    saslauth = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Authentication for clients and servers. Recommended if you want to log in.";
-    };
-
-    tls = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Add support for secure TLS on c2s/s2s connections";
-    };
-
-    dialback = mkOption {
-      type = types.bool;
-      default = true;
-      description = "s2s dialback support";
-    };
-
-    disco = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Service discovery";
-    };
-
-    # Not essential, but recommended
-    carbons = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Keep multiple clients in sync";
-    };
-
-    csi = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
-    };
-
-    cloud_notify = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
-    };
-
-    pep = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Enables users to publish their mood, activity, playing music and more";
-    };
-
-    private = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Private XML storage (for room bookmarks, etc.)";
-    };
-
-    blocklist = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allow users to block communications with other users";
-    };
-
-    vcard = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Allow users to set vCards";
-    };
-
-    vcard_legacy = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Converts users profiles and Avatars between old and new formats";
-    };
-
-    bookmarks = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP";
-    };
-
-    # Nice to have
-    version = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Replies to server version requests";
-    };
-
-    uptime = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Report how long server has been running";
-    };
-
-    time = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Let others know the time here on this server";
-    };
+  componentsOption = mkOption {
+    type = with types; attrsOf (submodule ({ name, config, ... }: {
+      options = {
+        module = mkOption {
+          type = with types; nullOr str;
+          default = null;
+          description = mdDoc ''
+            The name of the plugin you wish to use for the component if internal, or `null` if the
+            component is an external component.
+          '';
+        };
 
-    ping = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Replies to XMPP pings with pongs";
-    };
+        settings = mkOption {
+          type = luaType;
+          default = { };
+          description = mdDoc ''
+            Values specified here are applied to a specific component. Refer to <https://prosody.im/doc/components>
+            for additional details.
+          '';
+        };
 
-    register = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allow users to register on this server using a client and change passwords";
-    };
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = mdDoc ''
+            Additional component specific configuration.
+          '';
+        };
 
-    mam = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Store messages in an archive and allow users to access it";
-    };
+      };
 
-    smacks = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allow a client to resume a disconnected session, and prevent message loss";
-    };
+      config.settings = mkMerge [
+        (mkIf (config.module == "http_upload") {
+          http_upload_path = mkIf (config.module == "http_upload_path") cfg.dataDir;
+        })
+        (mkIf (config.module == "muc") {
+          modules_enabled = [ "muc_mam" ];
+        })
+      ];
+    }));
+    default = { };
+    description = mdDoc ''
+      Components are extra services on a server which are available to clients, usually on a subdomain
+      of the main server (such as mycomponent.example.com). Example components might be chatroom
+      servers, user directories, or gateways to other protocols.
+
+      See <https://prosody.im/doc/components> for details.
+    '';
+  };
 
-    # Admin interfaces
-    admin_adhoc = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Allows administration via an XMPP client that supports ad-hoc commands";
-    };
+  acmeHosts = unique (mapAttrsToList (domain: hostOpts: hostOpts.useACMEHost) (filterAttrs (_: v: v.useACMEHost != null) cfg.virtualHosts));
+in
+{
+  imports = [
+    (mkRenamedOptionModule [ "services" "prosody" "allowRegistration" ] [ "services" "prosody" "settings" "allow_registration" ])
+    (mkRenamedOptionModule [ "services" "prosody" "httpPorts" ] [ "services" "prosody" "settings" "http_ports" ])
+    (mkRenamedOptionModule [ "services" "prosody" "httpInterfaces" ] [ "services" "prosody" "settings" "http_interfaces" ])
+    (mkRenamedOptionModule [ "services" "prosody" "httpsPorts" ] [ "services" "prosody" "settings" "https_ports" ])
+    (mkRenamedOptionModule [ "services" "prosody" "httpsInterfaces" ] [ "services" "prosody" "settings" "https_interfaces" ])
+    (mkRenamedOptionModule [ "services" "prosody" "c2sRequireEncryption" ] [ "services" "prosody" "settings" "c2s_require_encryption" ])
+    (mkRenamedOptionModule [ "services" "prosody" "s2sRequireEncryption" ] [ "services" "prosody" "settings" "s2s_require_encryption" ])
+    (mkRenamedOptionModule [ "services" "prosody" "s2sSecureAuth" ] [ "services" "prosody" "settings" "s2s_secure_auth" ])
+    (mkRenamedOptionModule [ "services" "prosody" "s2sInsecureDomains" ] [ "services" "prosody" "settings" "s2s_insecure_domains" ])
+    (mkRenamedOptionModule [ "services" "prosody" "s2sSecureDomains" ] [ "services" "prosody" "settings" "s2s_secure_domains" ])
+    (mkRenamedOptionModule [ "services" "prosody" "extraModules" ] [ "services" "prosody" "settings" "modules_enabled" ])
+    (mkRenamedOptionModule [ "services" "prosody" "extraPluginPaths" ] [ "services" "prosody" "settings" "plugin_paths" ])
+    (mkRenamedOptionModule [ "services" "prosody" "ssl" "key" ] [ "services" "prosody" "settings" "ssl" "key" ])
+    (mkRenamedOptionModule [ "services" "prosody" "ssl" "cert" ] [ "services" "prosody" "settings" "ssl" "certificate" ])
+    (mkRenamedOptionModule [ "services" "prosody" "ssl" "extraOptions" ] [ "services" "prosody" "settings" "ssl" ])
+    (mkRenamedOptionModule [ "services" "prosody" "admins" ] [ "services" "prosody" "settings" "admins" ])
+    (mkRenamedOptionModule [ "services" "prosody" "authentication" ] [ "services" "prosody" "settings" "authentication" ])
+    (mkChangedOptionModule [ "services" "prosody" "disco_items" ] [ "services" "prosody" "settings" "disco_items" ] (config:
+      map ({ url, description }: [ url description ]) config.services.prosody.disco_items
+    ))
+    (mkChangedOptionModule [ "services" "prosody" "uploadHttp" ] [ "services" "prosody" "components" ] (config:
+      let
+        cfg = config.services.prosody;
+      in
+      {
+        ${cfg.uploadHttp.domain} = {
+          module = "http_upload";
+          # these values are to preserve compatibility with this module pre nixos 23.11
+          settings = {
+            http_upload_file_size_limit = { __compat = true; value = cfg.uploadHttp.uploadFileSizeLimit or "50 * 1024 * 1024"; };
+            http_upload_expire_after = { __compat = true; value = cfg.uploadHttp.uploadExpireAfter or "60 * 60 * 24 * 7"; };
+            http_upload_path = cfg.uploadHttp.httpUploadPath or "/var/lib/prosody";
+          } // optionalAttrs (cfg.uploadHttp ? userQuota) {
+            http_upload_quota = cfg.uploadHttp.userQuota;
+          };
+        };
+      }
+    ))
+    (mkChangedOptionModule [ "services" "prosody" "muc" ] [ "services" "prosody" "components" ] (config:
+      listToAttrs (map
+        (muc: {
+          name = muc.domain;
+          value = {
+            module = "muc";
+            extraConfig = muc.extraConfig or "";
+            # these values are to preserve compatibility with this module pre nixos 23.11
+            settings = {
+              modules_enabled = optionals (muc.vcard_muc or true) [ "vcard_muc" ];
+              name = muc.name or "Prosody Chatrooms";
+              restrict_room_creation = muc.restrictRoomCreation or "local";
+              max_history_messages = muc.maxHistoryMessages or 20;
+              muc_room_locking = muc.roomLocking or true;
+              muc_room_lock_timeout = muc.roomLockTimeout or 300;
+              muc_tombstones = muc.tombstones or true;
+              muc_tombstone_expiry = muc.tombstoneExpiry or 2678400;
+              muc_room_default_public = muc.roomDefaultPublic or true;
+              muc_room_default_members_only = muc.roomDefaultMembersOnly or false;
+              muc_room_default_moderated = muc.roomDefaultModerated or false;
+              muc_room_default_public_jids = muc.roomDefaultPublicJids or false;
+              muc_room_default_change_subject = muc.roomDefaultChangeSubject or false;
+              muc_room_default_history_length = muc.roomDefaultHistoryLength or 20;
+              muc_room_default_language = muc.roomDefaultLanguage or "en";
+            };
+          };
+        })
+        config.services.prosody.muc)
+    ))
+  ];
 
-    http_files = mkOption {
-      type = types.bool;
-      default = true;
-      description = "Serve static files from a directory over HTTP";
-    };
+  options.services.prosody = {
+    enable = mkEnableOption "Prosody, the modern XMPP communication server";
 
-    proxy65 = mkOption {
+    xmppComplianceSuite = mkOption {
       type = types.bool;
       default = true;
-      description = "Enables a file transfer proxy service which clients behind NAT can use";
-    };
-
-    admin_telnet = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Opens telnet console interface on localhost port 5582";
+      description = mdDoc ''
+        The XEP-0423 defines a set of recommended XEPs to implement
+        for a server. It's generally a good idea to implement this
+        set of extensions if you want to provide your users with a
+        good XMPP experience.
+
+        This NixOS module aims to provide an "advanced server"
+        experience as per defined in the [XEP-0423](https://xmpp.org/extensions/xep-0423.html) specification.
+
+        Setting this option to `true` will prevent you from building a
+        NixOS configuration which won't comply with this standard.
+        You can explicitly decide to ignore this standard if you
+        know what you are doing by setting this option to `false`.
+      '';
     };
 
-    # HTTP modules
-    bosh = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Enable BOSH clients, aka 'Jabber over HTTP'";
+    package = mkOption {
+      type = types.package;
+      description = mdDoc "Prosody package to use.";
+      default = pkgs.prosody;
+      defaultText = literalExpression "pkgs.prosody";
+      example = literalExpression ''
+        pkgs.prosody.override {
+          withExtraLibs = [ pkgs.luaPackages.lpty ];
+          withCommunityModules = [ "auth_external" ];
+        };
+      '';
     };
 
-    websocket = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Enable WebSocket support";
-    };
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/prosody";
+      description = mdDoc ''
+        The Prosody home directory used to store all data.
 
-    # Other specific functionality
-    limits = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Enable bandwidth limiting for XMPP connections";
+        ::: {.note}
+        If left as the default value this directory will automatically be created
+        before the Prosody server starts, otherwise you are responsible for
+        ensuring the directory exists with appropriate ownership and permissions.
+        :::
+      '';
     };
 
-    groups = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Shared roster support";
-    };
+    user = mkOption {
+      type = types.str;
+      default = "prosody";
+      description = mdDoc ''
+        User account under which Prosody runs.
 
-    server_contact_info = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Publish contact information for this service";
+        ::: {.note}
+        If left as the default value this user will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the user exists before the Prosody service starts.
+        :::
+      '';
     };
 
-    announce = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Send announcement to all online users";
-    };
+    group = mkOption {
+      type = types.str;
+      default = "prosody";
+      description = mdDoc ''
+        Group account under which Prosody runs.
 
-    welcome = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Welcome users who register accounts";
+        ::: {.note}
+        If left as the default value this group will automatically be created
+        on system activation, otherwise you are responsible for
+        ensuring the group exists before the Prosody service starts.
+        :::
+      '';
     };
 
-    watchregistrations = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Alert admins of registrations";
-    };
+    environmentFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      description = mdDoc ''
+        File which may contain secrets in the format of an EnvironmentFile as described
+        by systemd.exec(5). Variables here can be used in the Prosody configuration
+        file by prefixing the environment variable name with `ENV_` in the configuration.
 
-    motd = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Send a message to users when they log in";
+        ::: {.note}
+        Environment variables in this file should *not* begin with an `ENV_` prefix, only
+        when referenced in the Prosody configuration file.
+        :::
+      '';
     };
 
-    legacyauth = mkOption {
+    openFirewall = mkOption {
       type = types.bool;
       default = false;
-      description = "Legacy authentication. Only used by some old clients and bots";
-    };
-  };
+      description = mdDoc ''
+        Whether to automatically open ports specified by the following options:
 
-  toLua = x:
-    if builtins.isString x then ''"${x}"''
-    else if builtins.isBool x then boolToString x
-    else if builtins.isInt x then toString x
-    else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
-    else throw "Invalid Lua value";
-
-  settingsToLua = prefix: settings: generators.toKeyValue {
-      listsAsDuplicateKeys = false;
-      mkKeyValue = k:
-        generators.mkKeyValueDefault {
-          mkValueString = toLua;
-          } " = " (prefix + k);
-    }
-    (filterAttrs (k: v: v != null) settings);
-
-  createSSLOptsStr = o: ''
-    ssl = {
-      cafile = "/etc/ssl/certs/ca-bundle.crt";
-      key = "${o.key}";
-      certificate = "${o.cert}";
-      ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
-    };
-  '';
-
-  mucOpts = { ... }: {
-    options = {
-      domain = mkOption {
-        type = types.str;
-        description = "Domain name of the MUC";
-      };
-      name = mkOption {
-        type = types.str;
-        description = "The name to return in service discovery responses for the MUC service itself";
-        default = "Prosody Chatrooms";
-      };
-      restrictRoomCreation = mkOption {
-        type = types.enum [ true false "admin" "local" ];
-        default = false;
-        description = "Restrict room creation to server admins";
-      };
-      maxHistoryMessages = mkOption {
-        type = types.int;
-        default = 20;
-        description = "Specifies a limit on what each room can be configured to keep";
-      };
-      roomLocking = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Enables room locking, which means that a room must be
-          configured before it can be used. Locked rooms are invisible
-          and cannot be entered by anyone but the creator
-        '';
-      };
-      roomLockTimeout = mkOption {
-        type = types.int;
-        default = 300;
-        description = ''
-          Timout after which the room is destroyed or unlocked if not
-          configured, in seconds
-       '';
-      };
-      tombstones = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          When a room is destroyed, it leaves behind a tombstone which
-          prevents the room being entered or recreated. It also allows
-          anyone who was not in the room at the time it was destroyed
-          to learn about it, and to update their bookmarks. Tombstones
-          prevents the case where someone could recreate a previously
-          semi-anonymous room in order to learn the real JIDs of those
-          who often join there.
-        '';
-      };
-      tombstoneExpiry = mkOption {
-        type = types.int;
-        default = 2678400;
-        description = ''
-          This settings controls how long a tombstone is considered
-          valid. It defaults to 31 days. After this time, the room in
-          question can be created again.
-        '';
-      };
-
-      vcard_muc = mkOption {
-        type = types.bool;
-        default = true;
-      description = "Adds the ability to set vCard for Multi User Chat rooms";
-      };
-
-      # Extra parameters. Defaulting to prosody default values.
-      # Adding them explicitly to make them visible from the options
-      # documentation.
-      #
-      # See https://prosody.im/doc/modules/mod_muc for more details.
-      roomDefaultPublic = mkOption {
-        type = types.bool;
-        default = true;
-        description = "If set, the MUC rooms will be public by default.";
-      };
-      roomDefaultMembersOnly = mkOption {
-        type = types.bool;
-        default = false;
-        description = "If set, the MUC rooms will only be accessible to the members by default.";
-      };
-      roomDefaultModerated = mkOption {
-        type = types.bool;
-        default = false;
-        description = "If set, the MUC rooms will be moderated by default.";
-      };
-      roomDefaultPublicJids = mkOption {
-        type = types.bool;
-        default = false;
-        description = "If set, the MUC rooms will display the public JIDs by default.";
-      };
-      roomDefaultChangeSubject = mkOption {
-        type = types.bool;
-        default = false;
-        description = "If set, the rooms will display the public JIDs by default.";
-      };
-      roomDefaultHistoryLength = mkOption {
-        type = types.int;
-        default = 20;
-        description = "Number of history message sent to participants by default.";
-      };
-      roomDefaultLanguage = mkOption {
-        type = types.str;
-        default = "en";
-        description = "Default room language.";
-      };
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Additional MUC specific configuration";
-      };
-    };
-  };
-
-  uploadHttpOpts = { ... }: {
-    options = {
-      domain = mkOption {
-        type = types.nullOr types.str;
-        description = "Domain name for the http-upload service";
-      };
-      uploadFileSizeLimit = mkOption {
-        type = types.str;
-        default = "50 * 1024 * 1024";
-        description = "Maximum file size, in bytes. Defaults to 50MB.";
-      };
-      uploadExpireAfter = mkOption {
-        type = types.str;
-        default = "60 * 60 * 24 * 7";
-        description = "Max age of a file before it gets deleted, in seconds.";
-      };
-      userQuota = mkOption {
-        type = types.nullOr types.int;
-        default = null;
-        example = 1234;
-        description = ''
-          Maximum size of all uploaded files per user, in bytes. There
-          will be no quota if this option is set to null.
-        '';
-      };
-      httpUploadPath = mkOption {
-        type = types.str;
-        description = ''
-          Directory where the uploaded files will be stored
-          when the http_upload module is used.
-          By default, uploaded files are put in a sub-directory of the
-          default Prosody storage path (usually /var/lib/prosody).
-        '';
-        default = "/var/lib/prosody";
-      };
-    };
-  };
-
-  httpFileShareOpts = { ... }: {
-    freeformType = with types;
-      let atom = oneOf [ int bool str (listOf atom) ]; in
-      attrsOf (nullOr atom);
-    options.domain = mkOption {
-      type = with types; nullOr str;
-      description = "Domain name for a http_file_share service.";
+        - {option}`services.prosody.settings.c2s_ports`
+        - {option}`services.prosody.settings.s2s_ports`
+        - {option}`services.prosody.settings.https_ports`
+      '';
     };
-  };
-
-  vHostOpts = { ... }: {
 
-    options = {
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = luaType;
+        options = {
 
-      # TODO: require attribute
-      domain = mkOption {
-        type = types.str;
-        description = "Domain name";
-      };
+          modules_enabled = mkOption {
+            type = with types; listOf str;
+            apply = x: unique x;
+            default = [ ];
+            description = mdDoc ''
+              List of modules to load for all virtual hosts.
+            '';
+          };
 
-      enabled = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the virtual host";
-      };
+          modules_disabled = mkOption {
+            type = with types; listOf str;
+            apply = x: unique x;
+            default = [ ];
+            description = mdDoc ''
+              Allows you to disable the loading of a list of modules for all virtual hosts
+              if those modules are set in the global settings.
+            '';
+          };
 
-      ssl = mkOption {
-        type = types.nullOr (types.submodule sslOpts);
-        default = null;
-        description = "Paths to SSL files";
-      };
+          ssl = sslOption;
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Additional virtual host specific configuration";
+        };
       };
+      default = { };
+      description = mdDoc ''
+        Values specified here are applied to the whole server, and are the default for all
+        virtual hosts. Refer to <https://prosody.im/doc/configure> for additional details.
 
+        ::: {.note}
+        It's also possible to refer to environment variables (defined in
+        [services.prosody.environmentFile](#opt-services.prosody.environmentFile)) using the
+        syntax `"ENV_VARIABLE_NAME"` where the environment file contains a `VARIABLE_NAME` entry.
+        :::
+      '';
+      example = literalExpression ''
+        {
+          modules_enabled = [ "turn_external" ];
+          turn_external_host = "turn.example.com";
+          turn_external_port = 3478;
+          turn_external_secret = "ENV_TURN_EXTERNAL_SECRET";
+        }
+      '';
     };
 
-  };
-
-in
-
-{
-
-  ###### interface
-
-  options = {
-
-    services.prosody = {
+    components = componentsOption;
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Whether to enable the prosody server";
-      };
-
-      xmppComplianceSuite = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          The XEP-0423 defines a set of recommended XEPs to implement
-          for a server. It's generally a good idea to implement this
-          set of extensions if you want to provide your users with a
-          good XMPP experience.
-
-          This NixOS module aims to provide a "advanced server"
-          experience as per defined in the XEP-0423[1] specification.
-
-          Setting this option to true will prevent you from building a
-          NixOS configuration which won't comply with this standard.
-          You can explicitely decide to ignore this standard if you
-          know what you are doing by setting this option to false.
-
-          [1] https://xmpp.org/extensions/xep-0423.html
-        '';
-      };
-
-      package = mkOption {
-        type = types.package;
-        description = "Prosody package to use";
-        default = pkgs.prosody;
-        defaultText = literalExpression "pkgs.prosody";
-        example = literalExpression ''
-          pkgs.prosody.override {
-            withExtraLibs = [ pkgs.luaPackages.lpty ];
-            withCommunityModules = [ "auth_external" ];
+    virtualHosts = let config' = config; in mkOption {
+      type = with types; attrsOf (submodule ({ name, config, ... }: {
+        options = {
+          domain = mkOption {
+            type = types.str;
+            default = name;
+            description = mdDoc "Domain name.";
           };
-        '';
-      };
-
-      dataDir = mkOption {
-        type = types.path;
-        description = "Directory where Prosody stores its data";
-        default = "/var/lib/prosody";
-      };
-
-      disco_items = mkOption {
-        type = types.listOf (types.submodule discoOpts);
-        default = [];
-        description = "List of discoverable items you want to advertise.";
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "prosody";
-        description = "User account under which prosody runs.";
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "prosody";
-        description = "Group account under which prosody runs.";
-      };
-
-      allowRegistration = mkOption {
-        type = types.bool;
-        default = false;
-        description = "Allow account creation";
-      };
 
-      # HTTP server-related options
-      httpPorts = mkOption {
-        type = types.listOf types.int;
-        description = "Listening HTTP ports list for this service.";
-        default = [ 5280 ];
-      };
-
-      httpInterfaces = mkOption {
-        type = types.listOf types.str;
-        default = [ "*" "::" ];
-        description = "Interfaces on which the HTTP server will listen on.";
-      };
-
-      httpsPorts = mkOption {
-        type = types.listOf types.int;
-        description = "Listening HTTPS ports list for this service.";
-        default = [ 5281 ];
-      };
-
-      httpsInterfaces = mkOption {
-        type = types.listOf types.str;
-        default = [ "*" "::" ];
-        description = "Interfaces on which the HTTPS server will listen on.";
-      };
-
-      c2sRequireEncryption = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Force clients to use encrypted connections? This option will
-          prevent clients from authenticating unless they are using encryption.
-        '';
-      };
-
-      s2sRequireEncryption = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Force servers to use encrypted connections? This option will
-          prevent servers from authenticating unless they are using encryption.
-          Note that this is different from authentication.
-        '';
-      };
-
-      s2sSecureAuth = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Force certificate authentication for server-to-server connections?
-          This provides ideal security, but requires servers you communicate
-          with to support encryption AND present valid, trusted certificates.
-          For more information see https://prosody.im/doc/s2s#security
-        '';
-      };
-
-      s2sInsecureDomains = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "insecure.example.com" ];
-        description = ''
-          Some servers have invalid or self-signed certificates. You can list
-          remote domains here that will not be required to authenticate using
-          certificates. They will be authenticated using DNS instead, even
-          when s2s_secure_auth is enabled.
-        '';
-      };
+          useACMEHost = mkOption {
+            type = with types; nullOr str;
+            default = null;
+            description = mdDoc ''
+              A host of an existing Let's Encrypt certificate to use.
+
+              ::: {.note}
+              Note that this option does not create any certificates, nor it does add subdomains to existing
+              ones – you will need to create them manually using [](#opt-security.acme.certs).
+              :::
+            '';
+          };
 
-      s2sSecureDomains = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "jabber.org" ];
-        description = ''
-          Even if you leave s2s_secure_auth disabled, you can still require valid
-          certificates for some domains by specifying a list here.
-        '';
-      };
+          components = componentsOption;
+
+          settings = mkOption {
+            type = types.submodule {
+              freeformType = luaType;
+              options = {
+
+                enabled = mkOption {
+                  type = types.bool;
+                  default = true;
+                  description = mdDoc ''
+                    Specifies whether this host is enabled or not. Disabled hosts are not loaded and
+                    do not accept connections while Prosody is running.
+                  '';
+                };
+
+                modules_enabled = mkOption {
+                  type = with types; listOf str;
+                  apply = x: unique x;
+                  default = [ ];
+                  description = mdDoc ''
+                    List of modules to load for the virtual host.
+                  '';
+                };
+
+                modules_disabled = mkOption {
+                  type = with types; listOf str;
+                  apply = x: unique x;
+                  default = [ ];
+                  description = mdDoc ''
+                    Allows you to disable the loading of a list of modules for a particular host.
+                  '';
+                };
+
+                ssl = sslOption;
+              };
+            };
+            default = { };
+            description = mdDoc ''
+              Values specified here are applied to a specific virtual host and will override values set
+              in the global [settings](#opt-services.prosody.settings) option. Refer to <https://prosody.im/doc/configure>
+              for additional details.
+            '';
+          };
 
+          extraConfig = mkOption {
+            type = types.lines;
+            default = "";
+            description = mdDoc ''
+              Additional virtual host specific configuration.
+            '';
+          };
 
-      modules = moduleOpts;
+          # options to preserve compatibility with this module pre nixos 23.11
 
-      extraModules = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = "Enable custom modules";
-      };
+          enabled = mkOption {
+            type = with types; nullOr bool;
+            default = null;
+            description = mdDoc "Whether to enable the virtual host.";
+          };
 
-      extraPluginPaths = mkOption {
-        type = types.listOf types.path;
-        default = [];
-        description = "Addtional path in which to look find plugins/modules";
-      };
+          ssl = mkOption {
+            type = types.nullOr (types.submodule {
+              options = {
+                key = mkOption {
+                  type = types.path;
+                  description = lib.mdDoc "Path to the key file.";
+                };
+
+                cert = mkOption {
+                  type = types.path;
+                  description = lib.mdDoc "Path to the certificate file.";
+                };
+
+                extraOptions = mkOption {
+                  type = types.attrs;
+                  default = { };
+                  description = lib.mdDoc "Extra SSL configuration options.";
+                };
+              };
+            });
+            default = null;
+            description = mdDoc "Paths to SSL files.";
+          };
 
-      uploadHttp = mkOption {
-        description = ''
-          Configures the old Prosody builtin HTTP server to handle user uploads.
-        '';
-        type = types.nullOr (types.submodule uploadHttpOpts);
-        default = null;
-        example = {
-          domain = "uploads.my-xmpp-example-host.org";
         };
-      };
 
-      httpFileShare = mkOption {
-        description = ''
-          Configures the http_file_share module to handle user uploads.
-        '';
-        type = types.nullOr (types.submodule httpFileShareOpts);
-        default = null;
-        example = {
-          domain = "uploads.my-xmpp-example-host.org";
+        config.settings = {
+          ssl = mkMerge [
+            (mkIf (config.ssl != null) ({
+              key = config.ssl.key;
+              certificate = config.ssl.cert;
+            } // config.ssl.extraOptions))
+            (mkIf (config.useACMEHost != null) {
+              key = "${config'.security.acme.certs.${config.useACMEHost}.directory}/key.pem";
+              certificate = "${config'.security.acme.certs.${config.useACMEHost}.directory}/fullchain.pem";
+            })
+          ];
+
+          disco_items =
+            mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") config.components) ++
+            mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") config.components)
+          ;
+
+          enabled = mkIf (config.enabled != null) config.enabled;
         };
+      }));
+      default = {
+        localhost = { };
       };
-
-      muc = mkOption {
-        type = types.listOf (types.submodule mucOpts);
-        default = [ ];
-        example = [ {
-          domain = "conference.my-xmpp-example-host.org";
-        } ];
-        description = "Multi User Chat (MUC) configuration";
-      };
-
-      virtualHosts = mkOption {
-
-        description = "Define the virtual hosts";
-
-        type = with types; attrsOf (submodule vHostOpts);
-
-        example = {
-          myhost = {
-            domain = "my-xmpp-example-host.org";
-            enabled = true;
-          };
-        };
-
-        default = {
-          localhost = {
-            domain = "localhost";
-            enabled = true;
+      description = mdDoc ''
+        A host in Prosody is a domain on which user accounts can be created. For example if you want your users to have addresses
+        like `john.smith@example.com` then you need to add a host `example.com`.
+      '';
+      example = literalExpression ''
+        {
+          "example.net" = {
+            useACMEHost = "example.net";
+            settings = {
+              admins = [ "admin1@example.net" "admin2@example.net" ];
+              c2s_require_encryption = true;
+              modules_enabled = [
+                "announce"
+              ];
+            };
           };
-        };
-
-      };
-
-      ssl = mkOption {
-        type = types.nullOr (types.submodule sslOpts);
-        default = null;
-        description = "Paths to SSL files";
-      };
-
-      admins = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "admin1@example.com" "admin2@example.com" ];
-        description = "List of administrators of the current host";
-      };
+        }
+      '';
+    };
 
-      authentication = mkOption {
-        type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ];
-        default = "internal_hashed";
-        example = "internal_plain";
-        description = "Authentication mechanism used for logins.";
-      };
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = mdDoc ''
+        Additional prosody configuration.
+      '';
+    };
 
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = "Additional prosody configuration";
-      };
+    # options to preserve compatibility with this module pre nixos 23.11
 
+    modules = mkOption {
+      type = types.attrsOf types.bool;
+      default = { };
+      description = mdDoc ''
+      '';
     };
   };
 
-
-  ###### implementation
-
   config = mkIf cfg.enable {
 
-    assertions = let
-      genericErrMsg = ''
+    assertions =
+      let
+        hasAnyModules = mods: b: any (e: elem e.module mods) (attrValues b.components);
+        virtualHosts = filterAttrs (_: v: v.settings.enabled) cfg.virtualHosts;
 
-          Having a server not XEP-0423-compliant might make your XMPP
-          experience terrible. See the NixOS manual for further
-          informations.
+        # ensure a given module (e.g. muc or http_upload) is applied or available to every virtual host
+        mkAssertion = modules: message: {
+          assertion = cfg.xmppComplianceSuite -> hasAnyModules modules cfg || all (hasAnyModules modules) (attrValues virtualHosts);
+          message = message + ''
 
-          If you know what you're doing, you can disable this warning by
-          setting config.services.prosody.xmppComplianceSuite to false.
-      '';
-      errors = [
-        { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
-          message = ''
-            You need to setup at least a MUC domain to comply with
-            XEP-0423.
-          '' + genericErrMsg;}
-        { assertion = cfg.uploadHttp != null || cfg.httpFileShare != null || !cfg.xmppComplianceSuite;
-          message = ''
-            You need to setup the http_upload or http_file_share modules through
-            config.services.prosody.uploadHttp
-            or config.services.prosody.httpFileShare
-            to comply with XEP-0423.
-          '' + genericErrMsg;}
+            Having a server not XEP-0423-compliant might make your XMPP
+            experience terrible. See the NixOS manual for further
+            information.
+
+            If you know what you're doing, you can disable this warning by
+            setting config.services.prosody.xmppComplianceSuite to false.
+          '';
+        };
+      in
+      [
+        (mkAssertion [ "muc" ] ''
+          You need to setup at least a MUC domain to comply with
+          XEP-0423
+        '')
+
+        (mkAssertion [ "http_upload" "http_file_share" ] ''
+          You need to setup the http_upload or http_file_share module through
+          config.services.prosody.components to comply with
+          XEP-0423.
+        '')
       ];
-    in errors;
 
-    environment.systemPackages = [ cfg.package ];
+    warnings =
+      optionals (cfg.modules != { }) [ "The option `services.prosody.modules' has been and split into two separate options: `services.prosody.settings.modules_enabled' and `services.prosody.settings.modules_disabled'." ] ++
+      mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.enabled' has been renamed to `services.prosody.virtualHosts.${k}.settings.enabled'.") (filterAttrs (_: v: v.enabled != null) cfg.virtualHosts) ++
+      mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.ssl' has been renamed to `services.prosody.virtualHosts.${k}.settings.ssl'.") (filterAttrs (_: v: v.ssl != null) cfg.virtualHosts)
+    ;
+
+    services.prosody.settings = {
+      log = mkDefault "*syslog";
+      data_path = mkForce cfg.dataDir;
+      network_backend = "event";
+      pidfile = "/run/prosody/prosody.pid";
+
+      authentication = mkDefault "internal_hashed";
+      reload_modules = mkIf (acmeHosts != [ ]) [ "tls" ];
+
+      modules_enabled = [
+        # required for compliance with https://compliance.conversations.im/about/
+        "dialback"
+        "disco"
+        "roster"
+        "saslauth"
+        "tls"
+
+        # not essential, but recommended
+        "blocklist"
+        "bookmarks"
+        "carbons"
+        "cloud_notify"
+        "csi"
+        "pep"
+        "private"
+        "vcard_legacy"
+
+        # nice to have
+        "mam"
+        "ping"
+        "register"
+        "smacks"
+        "time"
+        "uptime"
+        "version"
+
+        # admin interfaces
+        "admin_adhoc"
+        "http_files"
+        "proxy65"
+      ] ++ cfg.package.communityModules
+      ++ mapAttrsToList (k: _: k) (filterAttrs (_: v: v == true) cfg.modules);
+
+      modules_disabled = mapAttrsToList (k: _: k) (filterAttrs (_: v: v == false) cfg.modules);
+
+      disco_items =
+        mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") cfg.components) ++
+        mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") cfg.components)
+      ;
+
+      # mod_tls configuration
+      c2s_require_encryption = mkDefault true;
+      s2s_require_encryption = mkDefault true;
+
+      # upstream defaults - useful for `services.prosody.openFirewall` logic
+      c2s_ports = mkDefault [ 5222 ];
+      s2s_ports = mkDefault [ 5269 ];
+      https_ports = mkDefault [ 5281 ];
+      # TODO:
+      # proxy65_ports = [ 5000 ];
+    };
 
+    environment.systemPackages = [ cfg.package ];
     environment.etc."prosody/prosody.cfg.lua".text =
       let
-        httpDiscoItems =
-          optional (cfg.uploadHttp != null)
-            { url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";} ++
-          optional (cfg.httpFileShare != null)
-            { url = cfg.httpFileShare.domain; description = "HTTP file share endpoint";};
-        mucDiscoItems = builtins.foldl'
-            (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc)
-            []
-            cfg.muc;
-        discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
-      in ''
-
-      pidfile = "/run/prosody/prosody.pid"
-
-      log = "*syslog"
-
-      data_path = "${cfg.dataDir}"
-      plugin_paths = {
-        ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
-      }
-
-      ${ optionalString  (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
-
-      admins = ${toLua cfg.admins}
-
-      -- we already build with libevent, so we can just enable it for a more performant server
-      use_libevent = true
-
-      modules_enabled = {
-
-        ${ lib.concatStringsSep "\n  " (lib.mapAttrsToList
-          (name: val: optionalString val "${toLua name};")
-        cfg.modules) }
-        ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
-        ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
-      };
-
-      disco_items = {
-      ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
-      };
-
-      allow_registration = ${toLua cfg.allowRegistration}
-
-      c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
-
-      s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
-
-      s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
-
-      s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
-
-      s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
-
-      authentication = ${toLua cfg.authentication}
-
-      http_interfaces = ${toLua cfg.httpInterfaces}
-
-      https_interfaces = ${toLua cfg.httpsInterfaces}
-
-      http_ports = ${toLua cfg.httpPorts}
-
-      https_ports = ${toLua cfg.httpsPorts}
-
-      ${ cfg.extraConfig }
+        toFormat = attrs: concatStringsSep "\n" (mapAttrsToList (k: v: ''${k} = ${toStr v}'') (filterAttrs (_: v: v != null) attrs));
+
+        toStr = v:
+          if isString v then
+          # prosody will directly pass environment variables into its configuration file which have `ENV_` as a prefix
+            if hasPrefix "ENV_" v then v else ''"${toString v}"''
+          else if isBool v then boolToString v
+          else if isInt v then toString v
+          else if isList v then ''{ ${concatStringsSep ", " (map (n: toStr n) v)} }''
+          else if isAttrs v then
+            if v ? __compat then v.value else ''{ ${concatStringsSep ", " (mapAttrsToList (a: b: ''["${a}"] = ${toStr b}'') v)} }''
+          else throw "Invalid Lua value";
+
+        componentsToStr = components:
+          concatStringsSep "\n" (
+            mapAttrsToList
+              (domain: component:
+                ''
+                  Component "${domain}" ${optionalString (component.module != null) "\"${component.module}\""}
+                  ${toFormat component.settings}
+                  ${component.extraConfig}
+                ''
+              )
+              components
+          );
+
+        virtualHostsToStr = virtualHosts:
+          concatStringsSep "\n" (
+            mapAttrsToList
+              (_: virtualHost:
+                ''
+                  VirtualHost "${virtualHost.domain}"
+                  ${toFormat virtualHost.settings}
+                  ${virtualHost.extraConfig}
+
+                  ${componentsToStr virtualHost.components}
+                ''
+              )
+              virtualHosts
+          );
+      in
+      ''
+        ${toFormat cfg.settings}
+        ${cfg.extraConfig}
+
+        ${componentsToStr cfg.components}
+        ${virtualHostsToStr cfg.virtualHosts}
+      '';
 
-      ${lib.concatMapStrings (muc: ''
-        Component ${toLua muc.domain} "muc"
-            modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } }
-            name = ${toLua muc.name}
-            restrict_room_creation = ${toLua muc.restrictRoomCreation}
-            max_history_messages = ${toLua muc.maxHistoryMessages}
-            muc_room_locking = ${toLua muc.roomLocking}
-            muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
-            muc_tombstones = ${toLua muc.tombstones}
-            muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
-            muc_room_default_public = ${toLua muc.roomDefaultPublic}
-            muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
-            muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
-            muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
-            muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
-            muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
-            muc_room_default_language = ${toLua muc.roomDefaultLanguage}
-            ${ muc.extraConfig }
-        '') cfg.muc}
+    systemd.services.prosody = {
+      description = "Prosody XMPP server";
+      wantedBy = [ "multi-user.target" ];
+      before = map (domain: "acme-${domain}.service") acmeHosts;
+      after = [ "network-online.target" ] ++ map (domain: "acme-selfsigned-${domain}.service") acmeHosts;
+      wants = [ "network-online.target" ] ++ map (domain: "acme-finished-${domain}.target") acmeHosts;
 
-      ${ lib.optionalString (cfg.uploadHttp != null) ''
-        Component ${toLua cfg.uploadHttp.domain} "http_upload"
-            http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit}
-            http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter}
-            ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"}
-            http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath}
-      ''}
+      restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
+      serviceConfig = mkMerge [
+        {
+          User = cfg.user;
+          Group = cfg.group;
+          Type = "forking";
+          PIDFile = cfg.settings.pidfile;
+          ExecStart = "${cfg.package}/bin/prosodyctl start";
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
+
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          PrivateMounts = true;
+          PrivateTmp = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+        }
+        (mkIf (cfg.dataDir == "/var/lib/prosody") {
+          StateDirectory = "prosody";
+          StateDirectoryMode = "0750";
+        })
+        (mkIf (cfg.settings.pidfile == "/run/prosody/prosody.pid") {
+          RuntimeDirectory = [ "prosody" ];
+        })
+      ];
+    };
 
-      ${ lib.optionalString (cfg.httpFileShare != null) ''
-        Component ${toLua cfg.httpFileShare.domain} "http_file_share"
-        ${settingsToLua "  http_file_share_" (cfg.httpFileShare // { domain = null; })}
-      ''}
+    security.acme.certs = genAttrs acmeHosts (_: {
+      reloadServices = [ "prosody.service" ];
+    });
 
-      ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
-        VirtualHost "${v.domain}"
-          enabled = ${boolToString v.enabled};
-          ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
-          ${ v.extraConfig }
-        '') cfg.virtualHosts) }
-    '';
+    networking.firewall = optionalAttrs cfg.openFirewall {
+      allowedTCPPorts = cfg.settings.c2s_ports
+        ++ cfg.settings.s2s_ports
+        ++ cfg.settings.https_ports
+        # TODO: cfg.settings.proxy65_ports
+      ;
+    };
 
     users.users.prosody = mkIf (cfg.user == "prosody") {
       uid = config.ids.uids.prosody;
       description = "Prosody user";
-      createHome = true;
       inherit (cfg) group;
-      home = "${cfg.dataDir}";
+      home = cfg.dataDir;
     };
 
     users.groups.prosody = mkIf (cfg.group == "prosody") {
       gid = config.ids.gids.prosody;
     };
-
-    systemd.services.prosody = {
-      description = "Prosody XMPP server";
-      after = [ "network-online.target" ];
-      wants = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
-      serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        Type = "forking";
-        RuntimeDirectory = [ "prosody" ];
-        PIDFile = "/run/prosody/prosody.pid";
-        ExecStart = "${cfg.package}/bin/prosodyctl start";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-
-        MemoryDenyWriteExecute = true;
-        PrivateDevices = true;
-        PrivateMounts = true;
-        PrivateTmp = true;
-        ProtectControlGroups = true;
-        ProtectHome = true;
-        ProtectHostname = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        RestrictSUIDSGID = true;
-      };
-    };
-
   };
-  meta.doc = ./prosody.xml;
+
+  meta.doc = ./prosody.md;
 }