-- Manages display names and identity visibility in MUC rooms. -- -- Three behaviours: -- -- 1. hideDisplayNameForGuests (outbound filter on stanzas/out, priority -100): -- When room._data.hideDisplayNameForGuests is true, strips identity elements -- (, , , and name/email inside ) from -- presence stanzas before they are delivered to non-moderator occupants. -- Moderator recipients and self-presence are always passed through unchanged. -- Uses session.jitsi_web_query_room / jitsi_web_query_prefix to locate the room. -- -- 2. name-readonly (inbound filter on stanzas/in, priority -100): -- When session.jitsi_meet_context_features['name-readonly'] is true, enforces -- the display name from the JWT token on every outgoing presence from that session: -- * Token has context.user.name → replaces the element with the token name -- * Token has no name → removes the element entirely -- This prevents clients from changing their display name when the token marks -- it as read-only. -- -- 3. Message display-name handling (MUC hooks): -- muc-broadcast-message (priority -1): strips and from -- every message before it is broadcast to occupants, so message payloads -- never carry identity information to live receivers. -- muc-add-history: injects into the -- history copy of each message (cloned from the broadcast stanza), sourced -- from session.jitsi_meet_context_user.name (token users) or the -- element (guests). Late-joining clients receive this annotated history. -- -- Load on the MUC component. local filters = require 'util.filters'; local st = require 'util.stanza'; local util = module:require 'util'; local filter_identity_from_presence = util.filter_identity_from_presence; local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain; local is_admin = util.is_admin; local is_focus = util.is_focus; local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite; local NICK_NS = 'http://jabber.org/protocol/nick'; local DISPLAY_NAME_NS = 'http://jitsi.org/protocol/display-name'; local sessions = prosody.full_sessions; local ignore_jwt_name = module:get_option_boolean('ignore_jwt_name', false); -- we need to get the shared resource for joining moderators, as participants are marked as moderators -- after joining which is after the filter for stanza/out, but we need to know will this participant be a moderator local joining_moderator_participants = module:shared('moderators/joining_moderator_participants'); --- Filter presence sent to non-moderator members of a room when the hideDisplayNameForGuests option is set. function filter_stanza_out(stanza, session) if stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' or is_focus(stanza.attr.from) then return stanza; end local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix); local shouldFilter = false; if room and room._data.hideDisplayNameForGuests == true then local occupant = room:get_occupant_by_real_jid(stanza.attr.to); -- don't touch self-presence if occupant and stanza.attr.from ~= internal_room_jid_match_rewrite(occupant.nick) then shouldFilter = occupant.role ~= 'moderator' and not joining_moderator_participants[occupant.bare_jid]; end end if shouldFilter then return filter_identity_from_presence(stanza); else return stanza; end end -- When a participant has a name-readonly feature enabled, we need to ensure that the -- nick element in the presence stanza is set to the user's name, if it exists. -- If the user does not have a name, we remove the nick element. -- This is to ensure that the name is not changed by the user, as it is read-only. function filter_stanza_in(stanza, session) if not session or not session.jitsi_meet_context_features or session.jitsi_meet_context_features['name-readonly'] ~= true then -- if the name-readonly feature is not set, there is nothing we need to do return stanza; end if stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' or is_focus(stanza.attr.from or '') then return stanza; end -- if the user does not have a name in token and name is readonly, remove any nick element if not session.jitsi_meet_context_user or not session.jitsi_meet_context_user.name then stanza:remove_children('nick', NICK_NS); return stanza; end local nick_element = stanza:get_child('nick', NICK_NS); if nick_element and nick_element:get_text() ~= session.jitsi_meet_context_user.name then stanza:remove_children('nick', NICK_NS); stanza:tag('nick', { xmlns = NICK_NS }):text(session.jitsi_meet_context_user.name):up(); end return stanza; end -- 'muc-add-history' is called in 'muc-broadcast-message' with priority 0 -- we want to clean up message after we have processed it for history, but do it for all messages -- not only those that are added to history (as someone can forge a message and add 'no-store') module:hook('muc-broadcast-message', function(event) -- any message that is about to be delivered, clear up nick and display-name elements event.stanza:remove_children('nick', NICK_NS); event.stanza:remove_children('display-name', DISPLAY_NAME_NS); end, -1); module:hook('muc-add-history', function(event) local room, stanza = event.room, event.stanza; local name; local source; local occupant = room:get_occupant_by_nick(stanza.attr.from); if occupant then local session = sessions[occupant.jid]; if session and session.jitsi_meet_context_user then if ignore_jwt_name then name = occupant:get_presence():get_child_text('nick', NICK_NS); else name = session.jitsi_meet_context_user.name; end source = 'token'; else name = occupant:get_presence():get_child_text('nick', NICK_NS); source = 'guest'; end else -- if no occupant found it is a message from visitor let's check display-name source local display_name_element = stanza:get_child('display-name', DISPLAY_NAME_NS); if display_name_element and display_name_element.attr.source == 'visitor' then name = display_name_element:get_text(); source = display_name_element.attr.source; end end if name then -- clone the stanza, so only history has the display_name extension event.stanza = st.clone(stanza); event.stanza:tag('display-name', { xmlns = DISPLAY_NAME_NS, source = source }):text(name):up(); end end); function filter_session(session) filters.add_filter(session, 'stanzas/out', filter_stanza_out, -100); filters.add_filter(session, 'stanzas/in', filter_stanza_in, -100); end function module.load() filters.add_filter_hook(filter_session); end function module.unload() filters.remove_filter_hook(filter_session); end