Mercurial > prosody-modules
view mod_groups_internal/mod_groups_internal.lua @ 6082:1a6cd0bbb7ab
mod_compliance_2023: Add 2023 Version of the compliance module, basis is the 2021 Version.
diff --git a/mod_compliance_2023/README.md b/mod_compliance_2023/README.md
new file mode 100644
--- /dev/null
+++ b/mod_compliance_2023/README.md
@@ -0,0 +1,22 @@
+---
+summary: XMPP Compliance Suites 2023 self-test
+labels:
+- Stage-Beta
+rockspec:
+ dependencies:
+ - mod_cloud_notify
+
+...
+
+Compare the list of enabled modules with
+[XEP-0479: XMPP Compliance Suites 2023] and produce basic report to the
+Prosody log file.
+
+If installed with the Prosody plugin installer then all modules needed for a green checkmark should be included. (With prosody 0.12 only [mod_cloud_notify] is not included with prosody and we need the community module)
+
+# Compatibility
+
+ Prosody-Version Status
+ --------------- ----------------------
+ trunk Works as of 2024-12-21
+ 0.12 Works
diff --git a/mod_compliance_2023/mod_compliance_2023.lua b/mod_compliance_2023/mod_compliance_2023.lua
new file mode 100644
--- /dev/null
+++ b/mod_compliance_2023/mod_compliance_2023.lua
@@ -0,0 +1,79 @@
+-- Copyright (c) 2021 Kim Alvefur
+--
+-- This module is MIT licensed.
+
+local hostmanager = require "core.hostmanager";
+
+local array = require "util.array";
+local set = require "util.set";
+
+local modules_enabled = module:get_option_inherited_set("modules_enabled");
+
+for host in pairs(hostmanager.get_children(module.host)) do
+ local component = module:context(host):get_option_string("component_module");
+ if component then
+ modules_enabled:add(component);
+ modules_enabled:include(module:context(host):get_option_set("modules_enabled", {}));
+ end
+end
+
+local function check(suggested, alternate, ...)
+ if set.intersection(modules_enabled, set.new({suggested; alternate; ...})):empty() then return suggested; end
+ return false;
+end
+
+local compliance = {
+ array {"Server"; check("tls"); check("disco")};
+
+ array {"Advanced Server"; check("pep", "pep_simple")};
+
+ array {"Web"; check("bosh"); check("websocket")};
+
+ -- No Server requirements for Advanced Web
+
+ array {"IM"; check("vcard_legacy", "vcard"); check("carbons"); check("http_file_share", "http_upload")};
+
+ array {
+ "Advanced IM";
+ check("vcard_legacy", "vcard");
+ check("blocklist");
+ check("muc");
+ check("private");
+ check("smacks");
+ check("mam");
+ check("bookmarks");
+ };
+
+ array {"Mobile"; check("smacks"); check("csi_simple", "csi_battery_saver")};
+
+ array {"Advanced Mobile"; check("cloud_notify")};
+
+ array {"A/V Calling"; check("turn_external", "external_services", "turncredentials", "extdisco")};
+
+};
+
+function check_compliance()
+ local compliant = true;
+ for _, suite in ipairs(compliance) do
+ local section = suite:pop(1);
+ if module:get_option_boolean("compliance_" .. section:lower():gsub("%A", "_"), true) then
+ local missing = set.new(suite:filter(function(m) return type(m) == "string" end):map(function(m) return "mod_" .. m end));
+ if suite[1] then
+ if compliant then
+ compliant = false;
+ module:log("warn", "Missing some modules for XMPP Compliance 2023");
+ end
+ module:log("info", "%s Compliance: %s", section, missing);
+ end
+ end
+ end
+
+ if compliant then module:log("info", "XMPP Compliance 2023: Compliant ✔️"); end
+end
+
+if prosody.start_time then
+ check_compliance()
+else
+ module:hook_global("server-started", check_compliance);
+end
+
| author | Menel <menel@snikket.de> |
|---|---|
| date | Sun, 22 Dec 2024 16:06:28 +0100 |
| parents | 865c77b5c6dc |
| children | f80db102fdb1 |
line wrap: on
line source
local rostermanager = require"core.rostermanager"; local modulemanager = require"core.modulemanager"; local array = require "util.array"; local id = require "util.id"; local jid = require "util.jid"; local st = require "util.stanza"; local jid_join = jid.join; local host = module.host; local group_info_store = module:open_store("group_info", "keyval+"); local group_members_store = module:open_store("groups"); local group_memberships = module:open_store("groups", "map"); local muc_host_name = module:get_option("groups_muc_host", "groups."..host); local muc_host = nil; local is_contact_subscribed = rostermanager.is_contact_subscribed; -- Make a *one-way* subscription. User will see when contact is online, -- contact will not see when user is online. local function subscribe(user, user_jid, contact, contact_jid, group_name) -- Update user's roster to say subscription request is pending... rostermanager.set_contact_pending_out(user, host, contact_jid); -- Update contact's roster to say subscription request is pending... rostermanager.set_contact_pending_in(contact, host, user_jid); -- Update contact's roster to say subscription request approved... rostermanager.subscribed(contact, host, user_jid); -- Update user's roster to say subscription request approved... rostermanager.process_inbound_subscription_approval(user, host, contact_jid); if group_name then local user_roster = rostermanager.load_roster(user, host); user_roster[contact_jid].groups[group_name] = true; end -- Push updates to both rosters rostermanager.roster_push(user, host, contact_jid); rostermanager.roster_push(contact, host, user_jid); end local function user_groups(username) return pairs(group_memberships:get_all(username) or {}); end local function do_single_group_subscriptions(username, group_id) local members = group_members_store:get(group_id); if not members then return; end local group_name = group_info_store:get_key(group_id, "name"); local user_jid = jid_join(username, host); for membername in pairs(members) do if membername ~= username then local member_jid = jid_join(membername, host); if not is_contact_subscribed(username, host, member_jid) then module:log("debug", "[group %s] Subscribing %s to %s", member_jid, user_jid); subscribe(membername, member_jid, username, user_jid, group_name); end if not is_contact_subscribed(membername, host, user_jid) then module:log("debug", "[group %s] Subscribing %s to %s", user_jid, member_jid); subscribe(username, user_jid, membername, member_jid, group_name); end end end end local function do_all_group_subscriptions_by_user(username) for group_id in user_groups(username) do do_single_group_subscriptions(username, group_id); end end local function do_all_group_subscriptions_by_group(group_id) local members = get_members(group_id) if not members then return end for membername in pairs(members) do do_single_group_subscriptions(membername, group_id); end end module:hook("resource-bind", function(event) module:log("debug", "Updating group subscriptions..."); do_all_group_subscriptions_by_user(event.session.username); end); local function _create_muc_room(name) if not muc_host_name then module:log("error", "cannot create group MUC: no MUC host configured") return nil, "service-unavailable" end if not muc_host then module:log("error", "cannot create group MUC: MUC host %s not configured properly", muc_host_name) return nil, "internal-server-error" end local muc_jid = jid.prep(id.short() .. "@" .. muc_host_name); local room = muc_host.create_room(muc_jid) if not room then return nil, "internal-server-error" end local ok = pcall(function () room:set_public(false); room:set_persistent(true); room:set_members_only(true); room:set_allow_member_invites(false); room:set_moderated(false); room:set_whois("anyone"); room:set_name(name); end); if not ok then module:log("error", "Failed to configure group MUC %s", muc_jid); room:destroy(); return nil, "internal-server-error"; end return muc_jid, room; end --luacheck: ignore 131 function create(group_info, create_default_muc, group_id) if not group_info.name then return nil, "group-name-required"; end if group_id then if exists(group_id) then return nil, "conflict" end else group_id = id.short(); end local muc_jid = nil local room = nil if create_default_muc then muc_jid, room = _create_muc_room(group_info.name); if not muc_jid then -- MUC creation failed, fail to create group delete(group_id) return nil, room; end end local ok = group_info_store:set(group_id, { name = group_info.name; muc_jid = muc_jid; }); if not ok then if room then room:destroy() end return nil, "internal-server-error"; end return group_id; end function get_info(group_id) return group_info_store:get(group_id); end function set_info(group_id, info) if not info then return nil, "bad-request" end if not info.name or #info.name == 0 then return nil, "bad-request" end -- TODO: we should probably prohibit changing/removing the MUC JID of -- an existing group. if info.muc_jid then local room = muc_host.get_room_from_jid(info.muc_jid); room:set_name(info.name); end local ok = group_info_store:set(group_id, info); if not ok then return nil, "internal-server-error"; end return true end function get_members(group_id) return group_members_store:get(group_id) or {}; end function exists(group_id) return not not get_info(group_id); end function get_user_groups(username) local groups = {}; do local group_set = group_memberships:get_all(username); if group_set then for group_id in pairs(group_set) do table.insert(groups, group_id); end end end return groups; end function delete(group_id) if group_members_store:set(group_id, nil) then local group_info = get_info(group_id); if group_info and group_info.muc_jid then local room = muc_host.get_room_from_jid(group_info.muc_jid) if room then room:destroy() end end return group_info_store:set(group_id, nil); end return nil, "internal-server-error"; end function add_member(group_id, username, delay_update) local group_info = group_info_store:get(group_id); if not group_info then return nil, "group-not-found"; end if not group_memberships:set(group_id, username, {}) then return nil, "internal-server-error"; end if group_info.muc_jid then local room = muc_host.get_room_from_jid(group_info.muc_jid); if room then local user_jid = username .. "@" .. host; room:set_affiliation(true, user_jid, "member"); module:send(st.message( { from = group_info.muc_jid, to = user_jid } ):tag("x", { xmlns = "jabber:x:conference", jid = group_info.muc_jid }):up()); module:log("debug", "set user %s to be member in %s and sent invite", username, group_info.muc_jid); else module:log("warn", "failed to update affiliation for %s in %s", username, group_info.muc_jid); end elseif group_info.mucs then local user_jid = username .. "@" .. host; for i = #group_info.mucs, 1, -1 do local muc_jid = group_info.mucs[i]; local room = muc_host.get_room_from_jid(muc_jid); if not room or room._data.destroyed then -- MUC no longer available, for some reason -- Let's remove it from the circle metadata... table.remove(group_info.mucs, i); group_info_store:set_key(group_id, "mucs", group_info.mucs); else room:set_affiliation(true, user_jid, "member"); module:send(st.message( { from = muc_jid, to = user_jid } ):tag("x", { xmlns = "jabber:x:conference", jid = muc_jid }):up()); module:log("debug", "set user %s to be member in %s and sent invite", username, muc_jid); end end end module:fire_event( "group-user-added", { id = group_id, user = username, host = host, group_info = group_info, } ) if not delay_update then do_all_group_subscriptions_by_group(group_id); end return true; end function remove_member(group_id, username) local group_info = group_info_store:get(group_id); if not group_info then return nil, "group-not-found"; end if not group_memberships:set(group_id, username, nil) then return nil, "internal-server-error"; end if group_info.muc_jid then local room = muc_host.get_room_from_jid(group_info.muc_jid); if room then local user_jid = username .. "@" .. host; room:set_affiliation(true, user_jid, nil); else module:log("warn", "failed to update affiliation for %s in %s", username, group_info.muc_jid); end elseif group_info.mucs then local user_jid = username .. "@" .. host; for _, muc_jid in ipairs(group_info.mucs) do local room = muc_host.get_room_from_jid(muc_jid); if room then room:set_affiliation(true, user_jid, nil); else module:log("warn", "failed to update affiliation for %s in %s", username, muc_jid); end end end module:fire_event( "group-user-removed", { id = group_id, user = username, host = host, group_info = group_info, } ) return true; end function sync(group_id) do_all_group_subscriptions_by_group(group_id); end function add_group_chat(group_id, name) local group_info = group_info_store:get(group_id); local mucs = group_info.mucs or {}; -- Create the MUC local muc_jid, room = _create_muc_room(name); if not muc_jid then return nil, room; end room:save(); -- This ensures the room is committed to storage table.insert(mucs, muc_jid); if group_info.muc_jid then -- COMPAT include old muc_jid into array table.insert(mucs, group_info.muc_jid); end local store_ok, store_err = group_info_store:set_key(group_id, "mucs", mucs); if not store_ok then module:log("error", "Failed to store new MUC association: %s", store_err); room:destroy(); return nil, "internal-server-error"; end -- COMPAT: clear old muc_jid (it's now in mucs array) if group_info.muc_jid then module:log("debug", "Clearing old single-MUC JID"); group_info.muc_jid = nil; group_info_store:set_key(group_id, "muc_jid", nil); end -- Make existing group members, members of the MUC for username in pairs(get_members(group_id)) do local user_jid = username .. "@" ..module.host; room:set_affiliation(true, user_jid, "member"); module:send(st.message( { from = muc_jid, to = user_jid } ):tag("x", { xmlns = "jabber:x:conference", jid = muc_jid }):up()); module:log("debug", "set user %s to be member in %s and sent invite", user_jid, muc_jid); end -- Notify other modules (such as mod_groups_muc_bookmarks) local muc = { jid = muc_jid; name = name; }; module:fire_event("group-chat-added", { group_id = group_id; group_info = group_info; muc = muc; }); return muc; end function remove_group_chat(group_id, muc_id) local group_info = group_info_store:get(group_id); if not group_info then return nil, "group-not-found"; end local mucs = group_info.mucs; if not mucs then if not group_info.muc_jid then return true; end -- COMPAT with old single-MUC groups - upgrade to new format mucs = {}; end if group_info.muc_jid then table.insert(mucs, group_info.muc_jid); end local removed; for i, muc_jid in ipairs(mucs) do if muc_id == jid.node(muc_jid) then removed = table.remove(mucs, i); break; end end if removed then if not group_info_store:set_key(group_id, "mucs", mucs) then return nil, "internal-server-error"; end if group_info.muc_jid then -- COMPAT: Now we've set the array, clean up muc_jid group_info.muc_jid = nil; group_info_store:set_key(group_id, "muc_jid", nil); end module:log("debug", "Updated group MUC list"); local room = muc_host.get_room_from_jid(removed); if room then room:destroy(); else module:log("warn", "Removing a group chat, but associated MUC not found (%s)", removed); end module:fire_event( "group-chat-removed", { group_id = group_id; group_info = group_info; muc = { id = muc_id; jid = removed; }; } ); else module:log("warn", "Removal of a group chat that can't be found - %s", muc_id); end return true; end function get_group_chats(group_id) local group_info, err = group_info_store:get(group_id); if not group_info then module:log("debug", "Unable to load group info: %s - %s", group_id, err); return nil; end local mucs = group_info.mucs or {}; -- COMPAT with single-MUC groups if group_info.muc_jid then table.insert(mucs, group_info.muc_jid); end return array.map(mucs, function (muc_jid) local room = muc_host.get_room_from_jid(muc_jid); return { id = jid.node(muc_jid); jid = muc_jid; name = room and room:get_name() or group_info.name; deleted = not room or room._data.destroyed; }; end); end function emit_member_events(group_id) local group_info, err = get_info(group_id) if group_info == nil then return false, err end for username in pairs(get_members(group_id)) do module:fire_event( "group-user-added", { id = group_id, user = username, host = host, group_info = group_info, } ) end return true end -- Returns iterator over group ids function groups() return group_info_store:items(); end local function setup() if not muc_host_name then module:log("info", "MUC management disabled (groups_muc_host set to nil)"); return; end local target_module = modulemanager.get_module(muc_host_name, "muc"); if not target_module then module:log("error", "host %s is not a MUC host -- group management will not work correctly; check your groups_muc_host setting!", muc_host_name); else module:log("debug", "found MUC host at %s", muc_host_name); muc_host = target_module; end end module:hook_global("user-deleted", function(event) if event.host ~= module.host then return end local username = event.username; for group_id in user_groups(username) do remove_member(group_id, username); end end); if prosody.start_time then -- server already started setup(); else module:hook_global("server-started", setup); end
