"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);