summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--nixos/modules/services/networking/prosody.nix372
-rw-r--r--nixos/tests/xmpp/prosody.nix85
-rw-r--r--nixos/tests/xmpp/xmpp-sendmessage.nix105
-rw-r--r--pkgs/servers/xmpp/prosody/default.nix15
4 files changed, 490 insertions, 87 deletions
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index 7a503e71166..8058172cb08 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -1,9 +1,7 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
-
   cfg = config.services.prosody;
 
   sslOpts = { ... }: {
@@ -30,8 +28,21 @@ let
     };
   };
 
+  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";
+      };
+    };
+  };
+
   moduleOpts = {
-    # Generally required
+    # Required for compliance with https://compliance.conversations.im/about/
     roster = mkOption {
       type = types.bool;
       default = true;
@@ -69,6 +80,18 @@ let
       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;
@@ -89,10 +112,22 @@ let
 
     vcard = mkOption {
       type = types.bool;
-      default = true;
+      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;
@@ -126,10 +161,16 @@ let
 
     mam = mkOption {
       type = types.bool;
-      default = false;
+      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";
+    };
+
     # Admin interfaces
     admin_adhoc = mkOption {
       type = types.bool;
@@ -137,6 +178,18 @@ let
       description = "Allows administration via an XMPP client that supports ad-hoc commands";
     };
 
+    http_files = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Serve static files from a directory over HTTP";
+    };
+
+    proxy65 = 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;
@@ -156,12 +209,6 @@ let
       description = "Enable WebSocket support";
     };
 
-    http_files = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Serve static files from a directory over HTTP";
-    };
-
     # Other specific functionality
     limits = mkOption {
       type = types.bool;
@@ -210,13 +257,6 @@ let
       default = false;
       description = "Legacy authentication. Only used by some old clients and bots";
     };
-
-    proxy65 = mkOption {
-      type = types.bool;
-      default = false;
-      description = "Enables a file transfer proxy service which clients behind NAT can use";
-    };
-
   };
 
   toLua = x:
@@ -235,6 +275,153 @@ let
     };
   '';
 
+  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.";
+      };
+    };
+  };
+
+  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. By
+          default, uploaded files are put in a sub-directory of the
+          default Prosody storage path (usually /var/lib/prosody).
+        '';
+        default = "/var/lib/prosody";
+      };
+    };
+  };
+
   vHostOpts = { ... }: {
 
     options = {
@@ -283,6 +470,27 @@ in
         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";
@@ -302,6 +510,12 @@ in
         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";
@@ -320,6 +534,31 @@ in
         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;
@@ -387,6 +626,26 @@ in
         description = "Addtional path in which to look find plugins/modules";
       };
 
+      uploadHttp = mkOption {
+        description = ''
+          Configures the Prosody builtin HTTP server to handle user uploads.
+        '';
+        type = types.nullOr (types.submodule uploadHttpOpts);
+        default = null;
+        example = {
+          domain = "uploads.my-xmpp-example-host.org";
+        };
+      };
+
+      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";
@@ -443,9 +702,44 @@ in
 
   config = mkIf cfg.enable {
 
+    assertions = let
+      genericErrMsg = ''
+
+          Having a server not XEP-0423-compliant might make your XMPP
+          experience terrible. See the NixOS manual for further
+          informations.
+
+          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.xmppComplianceSuite;
+          message = ''
+            You need to setup the uploadHttp module through
+            config.services.prosody.uploadHttp to comply with
+            XEP-0423.
+          '' + genericErrMsg;}
+      ];
+    in errors;
+
     environment.systemPackages = [ cfg.package ];
 
-    environment.etc."prosody/prosody.cfg.lua".text = ''
+    environment.etc."prosody/prosody.cfg.lua".text =
+      let
+        httpDiscoItems = if (cfg.uploadHttp != null)
+            then [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}]
+            else [];
+        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"
 
@@ -472,6 +766,10 @@ in
         ${ 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}
@@ -486,6 +784,42 @@ in
 
       authentication = ${toLua cfg.authentication}
 
+      http_interfaces = ${toLua cfg.httpInterfaces}
+
+      https_interfaces = ${toLua cfg.httpsInterfaces}
+
+      http_ports = ${toLua cfg.httpPorts}
+
+      https_ports = ${toLua cfg.httpsPorts}
+
+      ${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}
+
+          '') cfg.muc}
+
+      ${ 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}
+      ''}
+
       ${ cfg.extraConfig }
 
       ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
diff --git a/nixos/tests/xmpp/prosody.nix b/nixos/tests/xmpp/prosody.nix
index 9d1374bff6b..e7755e24bab 100644
--- a/nixos/tests/xmpp/prosody.nix
+++ b/nixos/tests/xmpp/prosody.nix
@@ -1,27 +1,80 @@
-import ../make-test-python.nix {
-  name = "prosody";
+let
+  cert = pkgs: pkgs.runCommandNoCC "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=uploads.example.com/CN=conference.example.com'
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+  createUsers = pkgs: pkgs.writeScriptBin "create-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Creates and set password for the 2 xmpp test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
+
+    prosodyctl register cthon98 example.com nothunter2
+    prosodyctl register azurediamond example.com hunter2
+  '';
+  delUsers = pkgs: pkgs.writeScriptBin "delete-prosody-users" ''
+    #!${pkgs.bash}/bin/bash
+    set -e
+
+    # Deletes the test users.
+    #
+    # Doing that in a bash script instead of doing that in the test
+    # script allow us to easily provision the users when running that
+    # test interactively.
 
+    prosodyctl deluser cthon98@example.com
+    prosodyctl deluser azurediamond@example.com
+  '';
+in import ../make-test-python.nix {
+  name = "prosody";
   nodes = {
-    client = { nodes, pkgs, ... }: {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
+      networking.extraHosts = ''
+        ${nodes.server.config.networking.primaryIPAddress} example.com
+        ${nodes.server.config.networking.primaryIPAddress} conference.example.com
+        ${nodes.server.config.networking.primaryIPAddress} uploads.example.com
+      '';
       environment.systemPackages = [
         (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; })
       ];
     };
     server = { config, pkgs, ... }: {
+      security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ];
+      console.keyMap = "fr-bepo";
       networking.extraHosts = ''
         ${config.networking.primaryIPAddress} example.com
+        ${config.networking.primaryIPAddress} conference.example.com
+        ${config.networking.primaryIPAddress} uploads.example.com
       '';
       networking.firewall.enable = false;
+      environment.systemPackages = [
+        (createUsers pkgs)
+        (delUsers pkgs)
+      ];
       services.prosody = {
         enable = true;
-        # TODO: use a self-signed certificate
-        c2sRequireEncryption = false;
-        extraConfig = ''
-          storage = "sql"
-        '';
-        virtualHosts.test = {
+        ssl.cert = "${cert pkgs}/cert.pem";
+        ssl.key = "${cert pkgs}/key.pem";
+        virtualHosts.example = {
           domain = "example.com";
           enabled = true;
+          ssl.cert = "${cert pkgs}/cert.pem";
+          ssl.key = "${cert pkgs}/key.pem";
+        };
+        muc = [
+          {
+            domain = "conference.example.com";
+          }
+        ];
+        uploadHttp = {
+          domain = "uploads.example.com";
         };
       };
     };
@@ -31,16 +84,8 @@ import ../make-test-python.nix {
     server.wait_for_unit("prosody.service")
     server.succeed('prosodyctl status | grep "Prosody is running"')
 
-    # set password to 'nothunter2' (it's asked twice)
-    server.succeed("yes nothunter2 | prosodyctl adduser cthon98@example.com")
-    # set password to 'y'
-    server.succeed("yes | prosodyctl adduser azurediamond@example.com")
-    # correct password to "hunter2"
-    server.succeed("yes hunter2 | prosodyctl passwd azurediamond@example.com")
-
-    client.succeed("send-message")
-
-    server.succeed("prosodyctl deluser cthon98@example.com")
-    server.succeed("prosodyctl deluser azurediamond@example.com")
+    server.succeed("create-prosody-users")
+    client.succeed('send-message 2>&1 | grep "XMPP SCRIPT TEST SUCCESS"')
+    server.succeed("delete-prosody-users")
   '';
 }
diff --git a/nixos/tests/xmpp/xmpp-sendmessage.nix b/nixos/tests/xmpp/xmpp-sendmessage.nix
index 2a075a01813..349b9c6f38e 100644
--- a/nixos/tests/xmpp/xmpp-sendmessage.nix
+++ b/nixos/tests/xmpp/xmpp-sendmessage.nix
@@ -1,46 +1,61 @@
-{ writeScriptBin, python3, connectTo ? "localhost" }:
-writeScriptBin "send-message" ''
-  #!${(python3.withPackages (ps: [ ps.sleekxmpp ])).interpreter}
-  # Based on the sleekxmpp send_client example, look there for more details:
-  # https://github.com/fritzy/SleekXMPP/blob/develop/examples/send_client.py
-  import sleekxmpp
-
-  class SendMsgBot(sleekxmpp.ClientXMPP):
-      """
-      A basic SleekXMPP bot that will log in, send a message,
-      and then log out.
-      """
-      def __init__(self, jid, password, recipient, message):
-          sleekxmpp.ClientXMPP.__init__(self, jid, password)
-
-          self.recipient = recipient
-          self.msg = message
-
-          self.add_event_handler("session_start", self.start, threaded=True)
-
-      def start(self, event):
-          self.send_presence()
-          self.get_roster()
-
-          self.send_message(mto=self.recipient,
-                            mbody=self.msg,
-                            mtype='chat')
-
-          self.disconnect(wait=True)
-
-
-  if __name__ == '__main__':
-      xmpp = SendMsgBot("cthon98@example.com", "nothunter2", "azurediamond@example.com", "hey, if you type in your pw, it will show as stars")
-      xmpp.register_plugin('xep_0030') # Service Discovery
-      xmpp.register_plugin('xep_0199') # XMPP Ping
-
-      # TODO: verify certificate
-      # If you want to verify the SSL certificates offered by a server:
-      # xmpp.ca_certs = "path/to/ca/cert"
-
-      if xmpp.connect(('${connectTo}', 5222)):
-          xmpp.process(block=True)
-      else:
-          print("Unable to connect.")
-          sys.exit(1)
+{ writeScriptBin, writeText, python3, connectTo ? "localhost" }:
+let
+  dummyFile = writeText "dummy-file" ''
+    Dear dog,
+
+    Please find this *really* important attachment.
+
+    Yours truly,
+    John
+  '';
+in writeScriptBin "send-message" ''
+#!${(python3.withPackages (ps: [ ps.slixmpp ])).interpreter}
+import logging
+import sys
+from types import MethodType
+
+from slixmpp import ClientXMPP
+from slixmpp.exceptions import IqError, IqTimeout
+
+
+class CthonTest(ClientXMPP):
+
+    def __init__(self, jid, password):
+        ClientXMPP.__init__(self, jid, password)
+        self.add_event_handler("session_start", self.session_start)
+
+    async def session_start(self, event):
+        log = logging.getLogger(__name__)
+        self.send_presence()
+        self.get_roster()
+        # Sending a test message
+        self.send_message(mto="azurediamond@example.com", mbody="Hello, this is dog.", mtype="chat")
+        log.info('Message sent')
+
+        # Test http upload (XEP_0363)
+        def timeout_callback(arg):
+            log.error("ERROR: Cannot upload file. XEP_0363 seems broken")
+            sys.exit(1)
+        url = await self['xep_0363'].upload_file("${dummyFile}",timeout=10, timeout_callback=timeout_callback)
+        log.info('Upload success!')
+        # Test MUC
+        self.plugin['xep_0045'].join_muc('testMucRoom', 'cthon98', wait=True)
+        log.info('MUC join success!')
+        log.info('XMPP SCRIPT TEST SUCCESS')
+        self.disconnect(wait=True)
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG,
+                        format='%(levelname)-8s %(message)s')
+
+    ct = CthonTest('cthon98@example.com', 'nothunter2')
+    ct.register_plugin('xep_0071')
+    ct.register_plugin('xep_0128')
+    # HTTP Upload
+    ct.register_plugin('xep_0363')
+    # MUC
+    ct.register_plugin('xep_0045')
+    ct.connect(("server", 5222))
+    ct.process(forever=False)
 ''
diff --git a/pkgs/servers/xmpp/prosody/default.nix b/pkgs/servers/xmpp/prosody/default.nix
index 0d552e9e112..702da004b42 100644
--- a/pkgs/servers/xmpp/prosody/default.nix
+++ b/pkgs/servers/xmpp/prosody/default.nix
@@ -1,4 +1,4 @@
-{ stdenv, fetchurl, libidn, openssl, makeWrapper, fetchhg
+{ stdenv, fetchurl, lib, libidn, openssl, makeWrapper, fetchhg
 , lua5, luasocket, luasec, luaexpat, luafilesystem, luabitop
 , withLibevent ? true, luaevent ? null
 , withDBI ? true, luadbi ? null
@@ -16,7 +16,16 @@ with stdenv.lib;
 stdenv.mkDerivation rec {
   version = "0.11.5"; # also update communityModules
   pname = "prosody";
-
+  # The following community modules are necessary for the nixos module
+  # prosody module to comply with XEP-0423 and provide a working
+  # default setup.
+  nixosModuleDeps = [
+    "bookmarks"
+    "cloud_notify"
+    "vcard_muc"
+    "smacks"
+    "http_upload"
+  ];
   src = fetchurl {
     url = "https://prosody.im/downloads/source/${pname}-${version}.tar.gz";
     sha256 = "12s0hn6hvjbi61cdw3165l6iw0878971dmlvfg663byjsmjvvy2m";
@@ -52,7 +61,7 @@ stdenv.mkDerivation rec {
   postInstall = ''
       ${concatMapStringsSep "\n" (module: ''
         cp -r $communityModules/mod_${module} $out/lib/prosody/modules/
-      '') (withCommunityModules ++ withOnlyInstalledCommunityModules)}
+      '') (lib.lists.unique(nixosModuleDeps ++ withCommunityModules ++ withOnlyInstalledCommunityModules))}
       wrapProgram $out/bin/prosody \
         --prefix LUA_PATH ';' "$LUA_PATH" \
         --prefix LUA_CPATH ';' "$LUA_CPATH"