"Fossies" - the Fresh Open Source Software Archive 
Member "jitsi-meet-7323/resources/prosody-plugins/mod_muc_breakout_rooms.lua" (7 Jun 2023, 19762 Bytes) of package /linux/misc/jitsi-meet-7323.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.
See also the last
Fossies "Diffs" side-by-side code changes report for "mod_muc_breakout_rooms.lua":
jitsi-meet_8319_vs_jitsi-meet_8615.
1 -- This module is added under the main virtual host domain
2 -- It needs a breakout rooms muc component
3 --
4 -- VirtualHost "jitmeet.example.com"
5 -- modules_enabled = {
6 -- "muc_breakout_rooms"
7 -- }
8 -- breakout_rooms_muc = "breakout.jitmeet.example.com"
9 -- main_muc = "muc.jitmeet.example.com"
10 --
11 -- Component "breakout.jitmeet.example.com" "muc"
12 -- restrict_room_creation = true
13 -- storage = "memory"
14 -- admins = { "focusUser@auth.jitmeet.example.com" }
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', 'Breakout rooms will not work with Prosody version 0.10 or less.');
23 return;
24 end
25
26 local jid_node = require 'util.jid'.node;
27 local jid_host = require 'util.jid'.host;
28 local jid_split = require 'util.jid'.split;
29 local json = require 'util.json';
30 local st = require 'util.stanza';
31 local uuid_gen = require 'util.uuid'.generate;
32
33 local util = module:require 'util';
34 local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
35 local is_healthcheck_room = util.is_healthcheck_room;
36
37 local BREAKOUT_ROOMS_IDENTITY_TYPE = 'breakout_rooms';
38 -- only send at most this often updates on breakout rooms to avoid flooding.
39 local BROADCAST_ROOMS_INTERVAL = .3;
40 -- close conference after this amount of seconds if all leave.
41 local ROOMS_TTL_IF_ALL_LEFT = 5;
42 local JSON_TYPE_ADD_BREAKOUT_ROOM = 'features/breakout-rooms/add';
43 local JSON_TYPE_MOVE_TO_ROOM_REQUEST = 'features/breakout-rooms/move-to-room';
44 local JSON_TYPE_REMOVE_BREAKOUT_ROOM = 'features/breakout-rooms/remove';
45 local JSON_TYPE_UPDATE_BREAKOUT_ROOMS = 'features/breakout-rooms/update';
46
47 local main_muc_component_config = module:get_option_string('main_muc');
48 if main_muc_component_config == nil then
49 module:log('error', 'breakout rooms not enabled missing main_muc config');
50 return ;
51 end
52 local breakout_rooms_muc_component_config = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
53
54 module:depends('jitsi_session');
55
56 local breakout_rooms_muc_service;
57 local main_muc_service;
58
59 -- Maps a breakout room jid to the main room jid
60 local main_rooms_map = {};
61
62 -- Utility functions
63
64 function get_main_room_jid(room_jid)
65 local _, host = jid_split(room_jid);
66
67 return
68 host == main_muc_component_config
69 and room_jid
70 or main_rooms_map[room_jid];
71 end
72
73 function get_main_room(room_jid)
74 local main_room_jid = get_main_room_jid(room_jid);
75
76 return main_muc_service.get_room_from_jid(main_room_jid), main_room_jid;
77 end
78
79 function get_room_from_jid(room_jid)
80 local host = jid_host(room_jid);
81
82 return
83 host == main_muc_component_config
84 and main_muc_service.get_room_from_jid(room_jid)
85 or breakout_rooms_muc_service.get_room_from_jid(room_jid);
86 end
87
88 function send_json_msg(to_jid, json_msg)
89 local stanza = st.message({ from = breakout_rooms_muc_component_config; to = to_jid; })
90 :tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_msg):up();
91 module:send(stanza);
92 end
93
94 function get_participants(room)
95 local participants = {};
96
97 if room then
98 for room_nick, occupant in room:each_occupant() do
99 -- Filter focus as we keep it as a hidden participant
100 if jid_node(occupant.jid) ~= 'focus' then
101 local display_name = occupant:get_presence():get_child_text(
102 'nick', 'http://jabber.org/protocol/nick');
103 local real_nick = internal_room_jid_match_rewrite(room_nick);
104 participants[real_nick] = {
105 jid = occupant.jid,
106 role = occupant.role,
107 displayName = display_name
108 };
109 end
110 end
111 end
112
113 return participants;
114 end
115
116 function broadcast_breakout_rooms(room_jid)
117 local main_room = get_main_room(room_jid);
118
119 if not main_room or main_room.broadcast_timer then
120 return;
121 end
122
123 -- Only send each BROADCAST_ROOMS_INTERVAL seconds to prevent flooding of messages.
124 main_room.broadcast_timer = module:add_timer(BROADCAST_ROOMS_INTERVAL, function()
125 local main_room, main_room_jid = get_main_room(room_jid);
126
127 if not main_room then
128 return;
129 end
130
131 main_room.broadcast_timer = nil;
132
133 local real_jid = internal_room_jid_match_rewrite(main_room_jid);
134 local real_node = jid_node(real_jid);
135 local rooms = {
136 [real_node] = {
137 isMainRoom = true,
138 id = real_node,
139 jid = real_jid,
140 name = main_room._data.subject,
141 participants = get_participants(main_room)
142 };
143 }
144
145 for breakout_room_jid, subject in pairs(main_room._data.breakout_rooms or {}) do
146 local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
147 local breakout_room_node = jid_node(breakout_room_jid)
148
149 rooms[breakout_room_node] = {
150 id = breakout_room_node,
151 jid = breakout_room_jid,
152 name = subject,
153 participants = {}
154 }
155
156 -- The room may not physically exist yet.
157 if breakout_room then
158 rooms[breakout_room_node].participants = get_participants(breakout_room);
159 end
160 end
161
162 local json_msg = json.encode({
163 type = BREAKOUT_ROOMS_IDENTITY_TYPE,
164 event = JSON_TYPE_UPDATE_BREAKOUT_ROOMS,
165 roomCounter = main_room._data.breakout_rooms_counter,
166 rooms = rooms
167 });
168
169 for _, occupant in main_room:each_occupant() do
170 if jid_node(occupant.jid) ~= 'focus' then
171 send_json_msg(occupant.jid, json_msg)
172 end
173 end
174
175 for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
176 local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
177 if room then
178 for _, occupant in room:each_occupant() do
179 if jid_node(occupant.jid) ~= 'focus' then
180 send_json_msg(occupant.jid, json_msg)
181 end
182 end
183 end
184 end
185 end);
186 end
187
188
189 -- Managing breakout rooms
190
191 function create_breakout_room(room_jid, subject)
192 local main_room, main_room_jid = get_main_room(room_jid);
193 local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
194
195 if not main_room._data.breakout_rooms then
196 main_room._data.breakout_rooms = {};
197 main_room._data.breakout_rooms_counter = 0;
198 end
199 main_room._data.breakout_rooms_counter = main_room._data.breakout_rooms_counter + 1;
200 main_room._data.breakout_rooms[breakout_room_jid] = subject;
201 main_room._data.breakout_rooms_active = true;
202 -- Make room persistent - not to be destroyed - if all participants join breakout rooms.
203 main_room:set_persistent(true);
204 main_room:save(true);
205
206 main_rooms_map[breakout_room_jid] = main_room_jid;
207 broadcast_breakout_rooms(main_room_jid);
208 end
209
210 function destroy_breakout_room(room_jid, message)
211 local main_room, main_room_jid = get_main_room(room_jid);
212
213 if room_jid == main_room_jid then
214 return;
215 end
216
217 local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
218
219 if breakout_room then
220 message = message or 'Breakout room removed.';
221 breakout_room:destroy(main_room and main_room_jid or nil, message);
222 end
223 if main_room then
224 if main_room._data.breakout_rooms then
225 main_room._data.breakout_rooms[room_jid] = nil;
226 end
227 main_room:save(true);
228
229 main_rooms_map[room_jid] = nil;
230 broadcast_breakout_rooms(main_room_jid);
231 end
232 end
233
234
235 -- Handling events
236
237 function on_message(event)
238 local session = event.origin;
239
240 -- Check the type of the incoming stanza to avoid loops:
241 if event.stanza.attr.type == 'error' then
242 return; -- We do not want to reply to these, so leave.
243 end
244
245 if not session or not session.jitsi_web_query_room then
246 return false;
247 end
248
249 local message = event.stanza:get_child(BREAKOUT_ROOMS_IDENTITY_TYPE);
250
251 if not message then
252 return false;
253 end
254
255 -- get room name with tenant and find room
256 local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
257
258 if not room then
259 module:log('warn', 'No room found found for %s/%s',
260 session.jitsi_web_query_prefix, session.jitsi_web_query_room);
261 return false;
262 end
263
264 -- check that the participant requesting is a moderator and is an occupant in the room
265 local from = event.stanza.attr.from;
266 local occupant = room:get_occupant_by_real_jid(from);
267
268 if not occupant then
269 -- Check if the participant is in any breakout room.
270 for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
271 local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
272 if breakout_room then
273 occupant = breakout_room:get_occupant_by_real_jid(from);
274 if occupant then
275 break;
276 end
277 end
278 end
279 if not occupant then
280 module:log('warn', 'No occupant %s found for %s', from, room.jid);
281 return false;
282 end
283 end
284
285 if occupant.role ~= 'moderator' then
286 module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
287 return false;
288 end
289
290 if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
291 create_breakout_room(room.jid, message.attr.subject);
292 return true;
293 elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
294 destroy_breakout_room(message.attr.breakoutRoomJid);
295 return true;
296 elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
297 local participant_jid = message.attr.participantJid;
298 local target_room_jid = message.attr.roomJid;
299
300 local json_msg = json.encode({
301 type = BREAKOUT_ROOMS_IDENTITY_TYPE,
302 event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
303 roomJid = target_room_jid
304 });
305
306 send_json_msg(participant_jid, json_msg)
307 return true;
308 end
309
310 -- return error.
311 return false;
312 end
313
314 function on_breakout_room_pre_create(event)
315 local breakout_room = event.room;
316 local main_room, main_room_jid = get_main_room(breakout_room.jid);
317
318 -- Only allow existent breakout rooms to be started.
319 -- Authorisation of breakout rooms is done by their random uuid name
320 if main_room and main_room._data.breakout_rooms and main_room._data.breakout_rooms[breakout_room.jid] then
321 breakout_room._data.subject = main_room._data.breakout_rooms[breakout_room.jid];
322 breakout_room.save();
323 else
324 module:log('debug', 'Invalid breakout room %s will not be created.', breakout_room.jid);
325 breakout_room:destroy(main_room_jid, 'Breakout room is invalid.');
326 return true;
327 end
328 end
329
330 function on_occupant_joined(event)
331 local room = event.room;
332
333 if is_healthcheck_room(room.jid) then
334 return;
335 end
336
337 local main_room = get_main_room(room.jid);
338
339 if main_room and main_room._data.breakout_rooms_active then
340 if jid_node(event.occupant.jid) ~= 'focus' then
341 broadcast_breakout_rooms(room.jid);
342 end
343
344 -- Prevent closing all rooms if a participant has joined (see on_occupant_left).
345 if main_room.close_timer then
346 main_room.close_timer:stop();
347 main_room.close_timer = nil;
348 end
349 end
350 end
351
352 function exist_occupants_in_room(room)
353 if not room then
354 return false;
355 end
356 for _, occupant in room:each_occupant() do
357 if jid_node(occupant.jid) ~= 'focus' then
358 return true;
359 end
360 end
361
362 return false;
363 end
364
365 function exist_occupants_in_rooms(main_room)
366 if exist_occupants_in_room(main_room) then
367 return true;
368 end
369 for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
370 local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
371 if exist_occupants_in_room(room) then
372 return true;
373 end
374 end
375
376 return false;
377 end
378
379 function on_occupant_left(event)
380 local room_jid = event.room.jid;
381
382 if is_healthcheck_room(room_jid) then
383 return;
384 end
385
386 local main_room = get_main_room(room_jid);
387
388 if not main_room then
389 return;
390 end
391
392 if main_room._data.breakout_rooms_active and jid_node(event.occupant.jid) ~= 'focus' then
393 broadcast_breakout_rooms(room_jid);
394 end
395
396 -- Close the conference if all left for good.
397 if main_room._data.breakout_rooms_active and not main_room.close_timer and not exist_occupants_in_rooms(main_room) then
398 main_room.close_timer = module:add_timer(ROOMS_TTL_IF_ALL_LEFT, function()
399 -- we need to look up again the room as till the timer is fired, the room maybe already destroyed/recreated
400 -- and we will have the old instance
401 local main_room, main_room_jid = get_main_room(room_jid);
402 if main_room and main_room.close_timer then
403 module:log('info', 'Closing conference %s as all left for good.', main_room_jid);
404 main_room:set_persistent(false);
405 main_room:destroy(nil, 'All occupants left.');
406 end
407 end);
408 end
409 end
410
411 function on_main_room_destroyed(event)
412 local main_room = event.room;
413
414 if is_healthcheck_room(main_room.jid) then
415 return;
416 end
417
418 for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
419 destroy_breakout_room(breakout_room_jid, event.reason)
420 end
421 end
422
423
424 -- Module operations
425
426 -- process a host module directly if loaded or hooks to wait for its load
427 function process_host_module(name, callback)
428 local function process_host(host)
429 if host == name then
430 callback(module:context(host), host);
431 end
432 end
433
434 if prosody.hosts[name] == nil then
435 module:log('debug', 'No host/component found, will wait for it: %s', name)
436
437 -- when a host or component is added
438 prosody.events.add_handler('host-activated', process_host);
439 else
440 process_host(name);
441 end
442 end
443
444
445 -- operates on already loaded breakout rooms muc module
446 function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
447 module:log('debug', 'Breakout rooms muc loaded');
448
449 -- Advertise the breakout rooms component so clients can pick up the address and use it
450 module:add_identity('component', BREAKOUT_ROOMS_IDENTITY_TYPE, breakout_rooms_muc_component_config);
451
452 breakout_rooms_muc_service = breakout_rooms_muc;
453 module:log("info", "Hook to muc events on %s", breakout_rooms_muc_component_config);
454 host_module:hook('message/host', on_message);
455 host_module:hook('muc-occupant-joined', on_occupant_joined);
456 host_module:hook('muc-occupant-left', on_occupant_left);
457 host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
458
459 host_module:hook('muc-disco#info', function (event)
460 local room = event.room;
461 local main_room, main_room_jid = get_main_room(room.jid);
462
463 -- Breakout room metadata.
464 table.insert(event.form, {
465 name = 'muc#roominfo_isbreakout';
466 label = 'Is this a breakout room?';
467 type = "boolean";
468 });
469 event.formdata['muc#roominfo_isbreakout'] = true;
470 table.insert(event.form, {
471 name = 'muc#roominfo_breakout_main_room';
472 label = 'The main room associated with this breakout room';
473 });
474 event.formdata['muc#roominfo_breakout_main_room'] = main_room_jid;
475
476 -- If the main room has a lobby, make it so this breakout room also uses it.
477 if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then
478 table.insert(event.form, {
479 name = 'muc#roominfo_lobbyroom';
480 label = 'Lobby room jid';
481 });
482 event.formdata['muc#roominfo_lobbyroom'] = main_room._data.lobbyroom;
483 end
484 end);
485
486 host_module:hook("muc-config-form", function(event)
487 local room = event.room;
488 local _, main_room_jid = get_main_room(room.jid);
489
490 -- Breakout room metadata.
491 table.insert(event.form, {
492 name = 'muc#roominfo_isbreakout';
493 label = 'Is this a breakout room?';
494 type = "boolean";
495 value = true;
496 });
497
498 table.insert(event.form, {
499 name = 'muc#roominfo_breakout_main_room';
500 label = 'The main room associated with this breakout room';
501 value = main_room_jid;
502 });
503 end);
504
505 local room_mt = breakout_rooms_muc_service.room_mt;
506
507 room_mt.get_members_only = function(room)
508 local main_room = get_main_room(room.jid);
509
510 if not main_room then
511 module:log('error', 'No main room (%s)!', room.jid);
512 return false;
513 end
514
515 return main_room.get_members_only(main_room)
516 end
517
518 -- we base affiliations (roles) in breakout rooms muc component to be based on the roles in the main muc
519 room_mt.get_affiliation = function(room, jid)
520 local main_room, _ = get_main_room(room.jid);
521
522 if not main_room then
523 module:log('error', 'No main room(%s) for %s!', room.jid, jid);
524 return 'none';
525 end
526
527 -- moderators in main room are moderators here
528 local role = main_room.get_affiliation(main_room, jid);
529 if role then
530 return role;
531 end
532
533 return 'none';
534 end
535 end
536
537 -- process or waits to process the breakout rooms muc component
538 process_host_module(breakout_rooms_muc_component_config, function(host_module, host)
539 module:log('info', 'Breakout rooms component created %s', host);
540
541 local muc_module = prosody.hosts[host].modules.muc;
542
543 if muc_module then
544 process_breakout_rooms_muc_loaded(muc_module, host_module);
545 else
546 module:log('debug', 'Will wait for muc to be available');
547 prosody.hosts[host].events.add_handler('module-loaded', function(event)
548 if (event.module == 'muc') then
549 process_breakout_rooms_muc_loaded(prosody.hosts[host].modules.muc, host_module);
550 end
551 end);
552 end
553 end);
554
555 -- operates on already loaded main muc module
556 function process_main_muc_loaded(main_muc, host_module)
557 module:log('debug', 'Main muc loaded');
558
559 main_muc_service = main_muc;
560 module:log("info", "Hook to muc events on %s", main_muc_component_config);
561 host_module:hook('muc-occupant-joined', on_occupant_joined);
562 host_module:hook('muc-occupant-left', on_occupant_left);
563 host_module:hook('muc-room-destroyed', on_main_room_destroyed);
564 end
565
566 -- process or waits to process the main muc component
567 process_host_module(main_muc_component_config, function(host_module, host)
568 local muc_module = prosody.hosts[host].modules.muc;
569
570 if muc_module then
571 process_main_muc_loaded(muc_module, host_module);
572 else
573 module:log('debug', 'Will wait for muc to be available');
574 prosody.hosts[host].events.add_handler('module-loaded', function(event)
575 if (event.module == 'muc') then
576 process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
577 end
578 end);
579 end
580 end);