view mod_muc_mav/mod_muc_mav.lua @ 6530:8e87fbbb6cd3

mod_muc_mav: Add module for XEP-0463: MUC Affiliations Versioning
author Marvin W <hg@larma.de>
date Mon, 04 May 2026 20:55:39 +0200
parents
children 4d7e2d0db2ab
line wrap: on
line source

local st = require "util.stanza";
local hashes = require "util.hashes";
local base64 = require "util.encodings".base64;

local mod_muc = module:depends"muc";

local xmlns_mav = "urn:xmpp:muc:affiliations:1";
local xmlns_muc = "http://jabber.org/protocol/muc";
local xmlns_muc_user = "http://jabber.org/protocol/muc#user";

local mav_versions = {}

local function get_affiliations(room)
	local jid_affiliations = {}
	local x = st.stanza("x", {xmlns = xmlns_muc_user;});
	for jid, affiliation, affiliation_data in room:each_affiliation() do
		table.insert(jid_affiliations, jid.."\0"..affiliation);
		local nick = affiliation_data and affiliation_data.reserved_nickname;
		x:tag("item", {affiliation = affiliation or "none"; jid = jid; nick = nick;}):up();
	end
	table.sort(jid_affiliations);
	local hash_input = table.concat(jid_affiliations, "\0");
	local hash = base64.encode(hashes.sha256(hash_input))
	x:tag("mav", {xmlns = xmlns_mav; ["until"] = hash;}):up();
	return hash, x;
end

local function handle_stanza_with_x(room, stanza, mav_current)
	local muc_x = stanza:get_child("x", xmlns_muc_user);
	local is_initial = false;
	for child in muc_x:childtags("status") do
		if child.attr.code == "110" then is_initial = true; end;
	end
	if is_initial then
		muc_x:tag("mav", {xmlns = xmlns_mav; ["until"] = mav_current;}):up();
	else
		local mav_new = get_affiliations(room);
		if mav_current ~= mav_new then
			module:log("info", "Update version of %s from %s to %s in affiliation update", stanza.attr.to, mav_current, mav_new);
			muc_x:tag("mav", {xmlns = xmlns_mav; ["since"] = mav_current; ["until"] = mav_new; }):up();
			mav_versions[room.jid.."\0"..stanza.attr.to] = mav_new;
		end
	end
end

local function room_route_stanza(room, stanza)
	local muc_x = stanza:get_child("x", xmlns_muc_user);
	if muc_x then
		local mav_current = mav_versions[room.jid.."\0"..stanza.attr.to];
		if mav_current then
			if stanza.name == "presence" or stanza.name == "message" then
				handle_stanza_with_x(room, stanza, mav_current);
			end
		end
	end
	module:send(stanza);
end

-- TODO: Stop this hack
local original_room_route_stanza = mod_muc.room_mt.route_stanza;
mod_muc.room_mt.route_stanza = room_route_stanza;
function module.unload()
	mod_muc.room_mt.route_stanza = original_room_route_stanza;
end


module:hook("muc-disco#info", function(event)
	event.reply:tag("feature", {var = xmlns_mav}):up();
end);

module:hook("muc-occupant-pre-join", function(event)
	local room, stanza = event.room, event.stanza;
	local muc_x = stanza:get_child("x", xmlns_muc);
	if not muc_x then return; end
	local mav_child = muc_x:get_child("mav", xmlns_mav);
	if not mav_child then return; end
	local mav_since = mav_child.attr["since"];
	mav_versions[room.jid.."\0"..stanza.attr.from] = mav_since;
	local mav_current, x = get_affiliations(room);
	if mav_current ~= mav_since then
		-- TODO: Support for diffs
		-- Send full response
		module:log("info", "Old version of %s is %s, sending full response for %s", stanza.attr.from, mav_since, mav_current);
		local announce_msg = st.message({ from = room.jid, to = stanza.attr.from }):add_child(x);
		room:route_stanza(announce_msg);
		mav_versions[room.jid.."\0"..stanza.attr.from] = mav_current;
	else
		module:log("info", "Old version of %s is %s, still current", stanza.attr.from, mav_since);
	end
end, -100);

module:hook("muc-occupant-left", function(event)
	local room, occupant = event.room, event.occupant;
	for jid in occupant:each_session() do
		mav_versions[room.jid.."\0"..jid] = nil;
	end
end);

module:hook("muc-occupant-pre-leave", function(event)
	local room, stanza = event.room, event.stanza;
	mav_versions[room.jid.."\0"..stanza.attr.from] = nil;
end);