"Fossies" - the Fresh Open Source Software Archive 
Member "jitsi-meet-7688/resources/prosody-plugins/mod_muc_lobby_rooms.lua" (1 Dec 2023, 20334 Bytes) of package /linux/misc/jitsi-meet-7688.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
1 -- This module added under the main virtual host domain
2 -- It needs a lobby muc component
3 --
4 -- VirtualHost "jitmeet.example.com"
5 -- modules_enabled = {
6 -- "muc_lobby_rooms"
7 -- }
8 -- lobby_muc = "lobby.jitmeet.example.com"
9 -- main_muc = "conference.jitmeet.example.com"
10 --
11 -- Component "lobby.jitmeet.example.com" "muc"
12 -- storage = "memory"
13 -- muc_room_cache_size = 1000
14 -- restrict_room_creation = true
15 -- muc_room_locking = false
16 -- muc_room_default_public_jids = true
17 --
18 -- we use async to detect Prosody 0.10 and earlier
19 local have_async = pcall(require, 'util.async');
20
21 if not have_async then
22 module:log('warn', 'Lobby rooms will not work with Prosody version 0.10 or less.');
23 return;
24 end
25
26 module:depends("jitsi_session");
27
28 local jid_split = require 'util.jid'.split;
29 local jid_bare = require 'util.jid'.bare;
30 local json = require 'util.json';
31 local filters = require 'util.filters';
32 local st = require 'util.stanza';
33 local muc_util = module:require "muc/util";
34 local valid_affiliations = muc_util.valid_affiliations;
35 local MUC_NS = 'http://jabber.org/protocol/muc';
36 local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info';
37 local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required';
38 local LOBBY_IDENTITY_TYPE = 'lobbyrooms';
39 local NOTIFY_JSON_MESSAGE_TYPE = 'lobby-notify';
40 local NOTIFY_LOBBY_ENABLED = 'LOBBY-ENABLED';
41 local NOTIFY_LOBBY_ACCESS_GRANTED = 'LOBBY-ACCESS-GRANTED';
42 local NOTIFY_LOBBY_ACCESS_DENIED = 'LOBBY-ACCESS-DENIED';
43
44 local util = module:require "util";
45 local ends_with = util.ends_with;
46 local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
47 local is_healthcheck_room = util.is_healthcheck_room;
48 local presence_check_status = util.presence_check_status;
49
50 local main_muc_component_config = module:get_option_string('main_muc');
51 if main_muc_component_config == nil then
52 module:log('error', 'lobby not enabled missing main_muc config');
53 return ;
54 end
55 local lobby_muc_component_config = module:get_option_string('lobby_muc');
56 if lobby_muc_component_config == nil then
57 module:log('error', 'lobby not enabled missing lobby_muc config');
58 return ;
59 end
60
61 local whitelist;
62 local check_display_name_required;
63 local function load_config()
64 whitelist = module:get_option_set('muc_lobby_whitelist', {});
65 check_display_name_required
66 = module:get_option_boolean('muc_lobby_check_display_name_required', true);
67 end
68 load_config();
69
70 local lobby_muc_service;
71 local main_muc_service;
72
73 function broadcast_json_msg(room, from, json_msg)
74 json_msg.type = NOTIFY_JSON_MESSAGE_TYPE;
75
76 local occupant = room:get_occupant_by_real_jid(from);
77 if occupant then
78 room:broadcast_message(
79 st.message({ type = 'groupchat', from = occupant.nick })
80 :tag('json-message', {xmlns='http://jitsi.org/jitmeet'})
81 :text(json.encode(json_msg)):up());
82 end
83 end
84
85 -- Sends a json message notifying for lobby enabled/disable
86 -- the message from is the actor that did the operation
87 function notify_lobby_enabled(room, actor, value)
88 broadcast_json_msg(room, actor, {
89 event = NOTIFY_LOBBY_ENABLED,
90 value = value
91 });
92 end
93
94 -- Sends a json message notifying that the jid was granted/denied access in lobby
95 -- the message from is the actor that did the operation
96 function notify_lobby_access(room, actor, jid, display_name, granted)
97 local notify_json = {
98 value = jid,
99 name = display_name
100 };
101 if granted then
102 notify_json.event = NOTIFY_LOBBY_ACCESS_GRANTED;
103 else
104 notify_json.event = NOTIFY_LOBBY_ACCESS_DENIED;
105 end
106
107 broadcast_json_msg(room, actor, notify_json);
108 end
109
110 function filter_stanza(stanza)
111 if not stanza.attr or not stanza.attr.from or not main_muc_service or not lobby_muc_service then
112 return stanza;
113 end
114 -- Allow self-presence (code=110)
115 local node, from_domain = jid_split(stanza.attr.from);
116
117 if from_domain == lobby_muc_component_config then
118 if stanza.name == 'presence' then
119 local muc_x = stanza:get_child('x', MUC_NS..'#user');
120 if not muc_x or presence_check_status(muc_x, '110') then
121 return stanza;
122 end
123
124 local lobby_room_jid = jid_bare(stanza.attr.from);
125 local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
126 if not lobby_room then
127 module:log('warn', 'No lobby room found %s', lobby_room_jid);
128 return stanza;
129 end
130
131 -- check is an owner, only owners can receive the presence
132 -- do not forward presence of owners (other than unavailable)
133 local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
134 local item = muc_x:get_child('item');
135 if not room
136 or stanza.attr.type == 'unavailable'
137 or (room.get_affiliation(room, stanza.attr.to) == 'owner'
138 and room.get_affiliation(room, item.attr.jid) ~= 'owner') then
139 return stanza;
140 end
141
142 local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
143 local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
144 if not from_occupant then
145 if is_to_moderator then
146 return stanza;
147 end
148
149 module:log('warn', 'No lobby occupant found %s', stanza.attr.from);
150 return nil;
151 end
152
153 local from_real_jid;
154 for real_jid in from_occupant:each_session() do
155 from_real_jid = real_jid;
156 end
157
158 if is_to_moderator and lobby_room:get_affiliation(from_real_jid) ~= 'owner' then
159 return stanza;
160 end
161 elseif stanza.name == 'iq' and stanza:get_child('query', DISCO_INFO_NS) then
162 -- allow disco info from the lobby component
163 return stanza;
164 elseif stanza.name == 'message' then
165 -- allow messages to or from moderator
166 local lobby_room_jid = jid_bare(stanza.attr.from);
167 local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
168
169 if not lobby_room then
170 module:log('warn', 'No lobby room found %s', stanza.attr.from);
171 return nil;
172 end
173
174 local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
175 local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
176
177 local from_real_jid;
178 if from_occupant then
179 for real_jid in from_occupant:each_session() do
180 from_real_jid = real_jid;
181 end
182 end
183
184 local is_from_moderator = lobby_room:get_affiliation(from_real_jid) == 'owner';
185
186 if is_to_moderator or is_from_moderator then
187 return stanza;
188 end
189 return nil;
190 end
191
192 return nil;
193 else
194 return stanza;
195 end
196 end
197 function filter_session(session)
198 -- domain mapper is filtering on default priority 0, and we need it after that
199 filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
200 end
201
202 -- actor can be null if called from backend (another module using hook create-lobby-room)
203 function attach_lobby_room(room, actor)
204 local node = jid_split(room.jid);
205 local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
206 if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
207 local new_room = lobby_muc_service.create_room(lobby_room_jid);
208 -- set persistent the lobby room to avoid it to be destroyed
209 -- there are cases like when selecting new moderator after the current one leaves
210 -- which can leave the room with no occupants and it will be destroyed and we want to
211 -- avoid lobby destroy while it is enabled
212 new_room:set_persistent(true);
213 module:log("info","Lobby room jid = %s created from:%s", lobby_room_jid, actor);
214 new_room.main_room = room;
215 room._data.lobbyroom = new_room.jid;
216 room:save(true);
217 return true
218 end
219 return false
220 end
221
222 -- destroys lobby room for the supplied main room
223 function destroy_lobby_room(room, newjid, message)
224 if not message then
225 message = 'Lobby room closed.';
226 end
227 if lobby_muc_service and room and room._data.lobbyroom then
228 local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
229 if lobby_room_obj then
230 lobby_room_obj:set_persistent(false);
231 lobby_room_obj:destroy(newjid, message);
232 end
233 room._data.lobbyroom = nil;
234 end
235 end
236
237 -- process a host module directly if loaded or hooks to wait for its load
238 function process_host_module(name, callback)
239 local function process_host(host)
240 if host == name then
241 callback(module:context(host), host);
242 end
243 end
244
245 if prosody.hosts[name] == nil then
246 module:log('debug', 'No host/component found, will wait for it: %s', name)
247
248 -- when a host or component is added
249 prosody.events.add_handler('host-activated', process_host);
250 else
251 process_host(name);
252 end
253 end
254
255 -- operates on already loaded lobby muc module
256 function process_lobby_muc_loaded(lobby_muc, host_module)
257 module:log('debug', 'Lobby muc loaded');
258 lobby_muc_service = lobby_muc;
259
260 -- enable filtering presences in the lobby muc rooms
261 filters.add_filter_hook(filter_session);
262
263 -- Advertise lobbyrooms support on main domain so client can pick up the address and use it
264 module:add_identity('component', LOBBY_IDENTITY_TYPE, lobby_muc_component_config);
265
266 -- Tag the disco#info response with a feature that display name is required
267 -- when the conference name from the web request has a lobby enabled.
268 host_module:hook('host-disco-info-node', function (event)
269 local session, reply, node = event.origin, event.reply, event.node;
270 if node == LOBBY_IDENTITY_TYPE
271 and session.jitsi_web_query_room
272 and check_display_name_required then
273 local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
274
275 if room and room._data.lobbyroom then
276 reply:tag('feature', { var = DISPLAY_NAME_REQUIRED_FEATURE }):up();
277 end
278 end
279 event.exists = true;
280 end);
281
282 local room_mt = lobby_muc_service.room_mt;
283 -- we base affiliations (roles) in lobby muc component to be based on the roles in the main muc
284 room_mt.get_affiliation = function(room, jid)
285 if not room.main_room then
286 module:log('error', 'No main room(%s) for %s!', room.jid, jid);
287 return 'none';
288 end
289
290 -- moderators in main room are moderators here
291 local role = room.main_room.get_affiliation(room.main_room, jid);
292 if role then
293 return role;
294 end
295
296 return 'none';
297 end
298
299 -- listens for kicks in lobby room, 307 is the status for kick according to xep-0045
300 host_module:hook('muc-broadcast-presence', function (event)
301 local actor, occupant, room, x = event.actor, event.occupant, event.room, event.x;
302 if presence_check_status(x, '307') then
303 local display_name = occupant:get_presence():get_child_text(
304 'nick', 'http://jabber.org/protocol/nick');
305 -- we need to notify in the main room
306 notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
307 end
308 end);
309 end
310
311 -- process or waits to process the lobby muc component
312 process_host_module(lobby_muc_component_config, function(host_module, host)
313 -- lobby muc component created
314 module:log('info', 'Lobby component loaded %s', host);
315
316 local muc_module = prosody.hosts[host].modules.muc;
317 if muc_module then
318 process_lobby_muc_loaded(muc_module, host_module);
319 else
320 module:log('debug', 'Will wait for muc to be available');
321 prosody.hosts[host].events.add_handler('module-loaded', function(event)
322 if (event.module == 'muc') then
323 process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
324 end
325 end);
326 end
327 end);
328
329 -- process or waits to process the main muc component
330 process_host_module(main_muc_component_config, function(host_module, host)
331 main_muc_service = prosody.hosts[host].modules.muc;
332
333 -- hooks when lobby is enabled to create its room, only done here or by admin
334 host_module:hook('muc-config-submitted', function(event)
335 local actor, room = event.actor, event.room;
336 local actor_node = jid_split(actor);
337 if actor_node == 'focus' then
338 return;
339 end
340 local members_only = event.fields['muc#roomconfig_membersonly'] and true or nil;
341 if members_only then
342 local lobby_created = attach_lobby_room(room, actor);
343 if lobby_created then
344 module:fire_event('jitsi-lobby-enabled', { room = room; });
345 event.status_codes['104'] = true;
346 notify_lobby_enabled(room, actor, true);
347 end
348 elseif room._data.lobbyroom then
349 destroy_lobby_room(room, room.jid);
350 module:fire_event('jitsi-lobby-disabled', { room = room; });
351 notify_lobby_enabled(room, actor, false);
352 end
353 end);
354 host_module:hook('muc-room-destroyed',function(event)
355 local room = event.room;
356 if room._data.lobbyroom then
357 destroy_lobby_room(room, nil);
358 end
359 end);
360 host_module:hook('muc-disco#info', function (event)
361 local room = event.room;
362 if (room._data.lobbyroom and room:get_members_only()) then
363 table.insert(event.form, {
364 name = 'muc#roominfo_lobbyroom';
365 label = 'Lobby room jid';
366 value = '';
367 });
368 event.formdata['muc#roominfo_lobbyroom'] = room._data.lobbyroom;
369 end
370 end);
371
372 host_module:hook('muc-occupant-pre-join', function (event)
373 local occupant, room, stanza = event.occupant, event.room, event.stanza;
374
375 if is_healthcheck_room(room.jid) or not room:get_members_only() or ends_with(occupant.nick, '/focus') then
376 return;
377 end
378
379 local join = stanza:get_child('x', MUC_NS);
380 if not join then
381 return;
382 end
383
384 local invitee = event.stanza.attr.from;
385 local invitee_bare_jid = jid_bare(invitee);
386 local _, invitee_domain = jid_split(invitee);
387 local whitelistJoin = false;
388
389 -- whitelist participants
390 if whitelist:contains(invitee_domain) or whitelist:contains(invitee_bare_jid) then
391 whitelistJoin = true;
392 end
393
394 local password = join:get_child_text('password', MUC_NS);
395 if password and room:get_password() and password == room:get_password() then
396 whitelistJoin = true;
397 end
398
399 if whitelistJoin then
400 local affiliation = room:get_affiliation(invitee);
401 -- if it was already set to be whitelisted member
402 if not affiliation or affiliation == 'none' or affiliation == 'member' then
403 occupant.role = 'participant';
404 room:set_affiliation(true, invitee_bare_jid, 'member');
405 room:save_occupant(occupant);
406
407 return;
408 end
409 end
410
411 -- Check for display name if missing return an error
412 local displayName = stanza:get_child_text('nick', 'http://jabber.org/protocol/nick');
413 if (not displayName or #displayName == 0) and not room._data.lobby_skip_display_name_check then
414 local reply = st.error_reply(stanza, 'modify', 'not-acceptable');
415 reply.tags[1].attr.code = '406';
416 reply:tag('displayname-required', { xmlns = 'http://jitsi.org/jitmeet', lobby = 'true' }):up():up();
417
418 event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
419 return true;
420 end
421
422 -- we want to add the custom lobbyroom field to fill in the lobby room jid
423 local invitee = event.stanza.attr.from;
424 local affiliation = room:get_affiliation(invitee);
425 if not affiliation or affiliation == 'none' then
426 local reply = st.error_reply(stanza, 'auth', 'registration-required');
427 reply.tags[1].attr.code = '407';
428 if room._data.lobby_extra_reason then
429 reply:tag(room._data.lobby_extra_reason, { xmlns = 'http://jitsi.org/jitmeet' }):up();
430 end
431 reply:tag('lobbyroom', { xmlns = 'http://jitsi.org/jitmeet' }):text(room._data.lobbyroom):up():up();
432
433 -- TODO: Drop this tag at some point (when all mobile clients and jigasi are updated), as this violates the rfc
434 reply:tag('lobbyroom'):text(room._data.lobbyroom):up();
435
436 event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
437 return true;
438 end
439 end, -4); -- the default hook on members_only module is on -5
440
441 -- listens for invites for participants to join the main room
442 host_module:hook('muc-invite', function(event)
443 local room, stanza = event.room, event.stanza;
444 local invitee = stanza.attr.to;
445 local from = stanza:get_child('x', 'http://jabber.org/protocol/muc#user')
446 :get_child('invite').attr.from;
447
448 if lobby_muc_service and room._data.lobbyroom then
449 local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
450 if lobby_room_obj then
451 local occupant = lobby_room_obj:get_occupant_by_real_jid(invitee);
452 if occupant then
453 local display_name = occupant:get_presence():get_child_text(
454 'nick', 'http://jabber.org/protocol/nick');
455
456 notify_lobby_access(room, from, occupant.nick, display_name, true);
457 end
458 end
459 end
460 end);
461 end);
462
463 function handle_create_lobby(event)
464 local room = event.room;
465
466 -- since this is called by backend rather than triggered by UI, we need to handle a few additional things:
467 -- 1. Make sure existing participants are already members or they will get kicked out when set_members_only(true)
468 -- 2. Trigger a 104 (config change) status message so UI state is properly updated for existing users
469
470 -- make sure all existing occupants are members
471 for _, occupant in room:each_occupant() do
472 local affiliation = room:get_affiliation(occupant.bare_jid);
473 if valid_affiliations[affiliation or "none"] < valid_affiliations.member then
474 room:set_affiliation(true, occupant.bare_jid, 'member');
475 end
476 end
477 -- Now it is safe to set the room to members only
478 room:set_members_only(true);
479 room._data.lobby_extra_reason = event.reason;
480 room._data.lobby_skip_display_name_check = event.skip_display_name_check;
481
482 -- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
483 room:broadcast_message(
484 st.message({ type='groupchat', from=room.jid })
485 :tag('x', { xmlns='http://jabber.org/protocol/muc#user' })
486 :tag('status', { code='104' })
487 );
488
489 -- Attach the lobby room.
490 attach_lobby_room(room);
491 end
492
493 function handle_destroy_lobby(event)
494 local room = event.room;
495
496 -- since this is called by backend rather than triggered by UI, we need to
497 -- trigger a 104 (config change) status message so UI state is properly updated for existing users (and jicofo)
498 destroy_lobby_room(room, event.newjid, event.message);
499
500 -- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
501 room:broadcast_message(
502 st.message({ type='groupchat', from=room.jid })
503 :tag('x', { xmlns='http://jabber.org/protocol/muc#user' })
504 :tag('status', { code='104' })
505 );
506 end
507
508 module:hook_global('config-reloaded', load_config);
509 module:hook_global('create-lobby-room', handle_create_lobby);
510 module:hook_global('destroy-lobby-room', handle_destroy_lobby);