changeset 6422:c41615686ce4

mod_muc_auto_role: Set default role for MUC occupants based on policies
author Matthew Wild <mwild1@gmail.com>
date Thu, 05 Mar 2026 14:35:43 +0000
parents 653ac8702509
children d748985de48f
files mod_muc_auto_role/README.md mod_muc_auto_role/mod_muc_auto_role.lua
diffstat 2 files changed, 103 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_auto_role/README.md	Thu Mar 05 14:35:43 2026 +0000
@@ -0,0 +1,55 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: 'Set default role for MUC occupants based on policies'
+...
+
+Introduction
+============
+
+This module allows you to apply policies to occupants joining a MUC room, and
+choose an appropriate initial role for them.
+
+Details
+=======
+
+Depending on configuration, someone who joins a MUC will usually be assigned
+one of two roles: "visitor", or "participant".
+
+The "visitor" role does not have permission to speak in moderated rooms. The
+"participant" role may speak in moderated rooms.
+
+By default, Prosody assigns "participant" when a MUC is not moderated, and
+"visitor" when it is moderated. This allows existing occupants to continue
+speaking when a MUC is switched from unmoderated to moderated (e.g. in
+response to abuse).
+
+This module overrides the default behaviour, using other signals such as
+[XEP-0489: Reporting Account Affiliations](https://xmpp.org/extensions/xep-0489.html),
+(which is implemented in Prosody in [mod_report_affiliations](https://modules.prosody.im/mod_report_affiliations))
+to decide which role to assign by default.
+
+This allows a smarter approach than simply assigning everyone "visitor" or
+"participant".
+
+Configuration
+=============
+
+Add the module to the MUC host (not the global modules\_enabled):
+
+        Component "conference.example.com" "muc"
+            modules_enabled = { "muc_auto_role" }
+
+You can define (globally or per-MUC component) the following options:
+
+  Name                             Default      Description
+  -------------------------------  -----------  ----------------------------------------------------
+  muc_auto_role_trusted_servers    `{}`         A list of trusted servers to use RAA info from
+  muc_auto_role_trust_threshold    `51`         The minimum RAA trust score which will be considered trusted (for `"participant"` role)
+  muc_auto_role_untrusted_servers  `{}`         A list of servers which will always use the `"visitor"` role.
+  muc_default_role                 `"visitor"`  The default role to assign when no other policy fits
+
+Compatibility
+=============
+
+Requires Prosody 13.0+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_auto_role/mod_muc_auto_role.lua	Thu Mar 05 14:35:43 2026 +0000
@@ -0,0 +1,48 @@
+local jid = require "prosody.util.jid";
+
+local xmlns_aff = "urn:xmpp:raa:0";
+
+local raa_servers = module:get_option_set("muc_auto_role_trusted_servers", {});
+local trust_threshold = module:get_option_integer("muc_auto_role_trust_threshold", 51, 0, 100);
+
+local untrusted_servers = module:get_option_set("muc_auto_role_untrusted_servers", {});
+
+local default_role = module:get_option_enum("muc_default_role", "visitor", "participant");
+
+-- TODO: This should be a config option in mod_muc
+module:hook("muc-get-default-role", function ()
+	return default_role;
+end, 5);
+
+module:hook("muc-occupant-pre-join", function (event)
+	local stanza = event.stanza;
+
+	local from_host = jid.host(stanza.attr.from);
+
+	if untrusted_servers:contains(from_host) then
+		event.occupant.role = "visitor";
+	end
+
+	if not raa_servers:contains(from_host) then
+		return;
+	end
+
+	local raa = stanza:get_child("info", xmlns_aff);
+	if not raa then
+		return;
+	end
+
+	local aff = raa.attr.affiliation;
+	local trust = tonumber(raa.attr.trust) or 50;
+
+	if aff == "member" or aff == "admin" or trust >= trust_threshold then
+		event.occupant.role = "participant";
+	else
+		event.occupant.role = "visitor";
+	end
+
+	module:log("debug", "Assigning <%s> role '%s' due to affiliation %s and trust score %d of <%s>",
+		event.occupant.nick, event.occupant.role, aff, trust, stanza.attr.from
+	);
+
+end);