Jitsi monitoring mod, room list , occupant list

After many try & fail with mod_muc_size , here is my contribution if it can help :

goal : Having a brief status of the server, in one call , list of current rooms and for each , list of user and details. that we can call by a cron curl

json structure :

[[[{
“roomname”:“TheRoomName”,
“domain”:“xxxx”,
“NBparticipant”:“1”,
“roomjid”:“TheRoomName@xxxxx”,
“NBaudiomuted”:“0”,
“NBvideomuted”:“0”,
“participant”:[
{“audiomuted”:“false”,
“jid”:“theRoomName@xxxxxx/f920dae4”,
“display_name”:“ANickname”,
“videomuted”:“false”,
“role”:“moderator”}]
}]],
etc. for each room

How to use :
Its a refactor of mod_muc_size so … choose :

1- create a new module file in /usr/share/jitsi-meet/prosody-plugins
give a name such as “mod_muc_status.lua” , and add the module in you prosody virtualhost module enabled (“muc_status”)

OR

2- you have mod_muc_size but not working, save it and replace all the code :slight_smile:

then restart prosody, jicofo etc .

then get …:5280/status?domain=mydomain

The code :

-- Prosody IM
-- Copyright (C) 2017 Atlassian
--

local jid = require "util.jid";
local it = require "util.iterators";
local json = require "util.json";
local iterators = require "util.iterators";
local array = require"util.array";

local have_async = pcall(require, "util.async");
if not have_async then
    module:log("error", "requires a version of Prosody with util.async");
    return;
end

local async_handler_wrapper = module:require "util".async_handler_wrapper;

local tostring = tostring;
local neturl = require "net.url";
local parse = neturl.parseQuery;

-- option to enable/disable room API token verifications
local enableTokenVerification
    = module:get_option_boolean("enable_roomsize_token_verification", false);

local token_util = module:require "token/util".new(module);
local get_room_from_jid = module:require "util".get_room_from_jid;

-- no token configuration but required
if token_util == nil and enableTokenVerification then
    log("error", "no token configuration but it is required");
    return;
end

-- required parameter for custom muc component prefix,
-- defaults to "conference"
local muc_domain_prefix
    = module:get_option_string("muc_mapper_domain_prefix", "conference");

--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_address the full room address jid
-- @return true if values are ok or false otherwise
function verify_token(token, room_address)


    if not enableTokenVerification then
        return true;
    end

    -- if enableTokenVerification is enabled and we do not have token
    -- stop here, cause the main virtual host can have guest access enabled
    -- (allowEmptyToken = true) and we will allow access to rooms info without
    -- a token
    if token == nil then
        log("warn", "no token provided");
        return false;
    end

    local session = {};
    session.auth_token = token;
    local verified, reason = token_util:process_and_verify_token(session);
    if not verified then
        log("warn", "not a valid token %s", tostring(reason));
        return false;
    end

    if not token_util:verify_room(session, room_address) then
        log("warn", "Token %s not allowed to join: %s",
            tostring(token), tostring(room_address));
        return false;
    end

    return true;
end



-- Get full list from "all_rooms", then iterate to get each room details 
function get_all(event)

        if (not event.request.url.query) then
                return { status_code = 400; };
        end

        local params = parse(event.request.url.query);
        local domain_name = params["domain"];
--        local subdomain = params["subdomain"];

        local host=muc_domain_prefix.."."..domain_name

        local component = hosts[host];

        local allrooms=array() -- store the result of all_rooms()
        local state=array() -- store the final json

        if component then
                local muc = component.modules.muc
                if muc and rawget(muc,"all_rooms") then
                        allrooms= muc.all_rooms();
                end
        end

        -- iterate the rooms
        for room in allrooms  do

                        local room_jid = room.jid;
                        local getroomstate=get_room(room_jid) -- use de get_room() below 
                        state:push({getroomstate})
        end


        return { status_code = 200; body = json.encode(state) };

end



-- get all room details by the get_room_from_jid() util function
function get_room (room_address)


        local room_name,domain_name=jid.split(room_address) -- split to get readable var

        local room = get_room_from_jid(room_address); -- get it

        local participant_count = 0; -- how many
        local audiomuted_count = 0; -- how many muted 
        local videomuted_count = 0; -- how many video muted

        local occupants_json = array(); -- store the occupant list & details
        local room_json = array(); -- store the room details

        if room then
                
                local occupants = room._occupants;  --_occupants is an object of room 
                
                if occupants then
                    for _, occupant in room:each_occupant() do
                        
                        -- filter focus as we keep it as hidden participant
                        if string.sub(occupant.nick,-string.len("/focus"))~="/focus" then

                            for _, pr in occupant:each_session() do

                                local nick = pr:get_child_text("nick", "http://jabber.org/protocol/nick") or "";
                                local audiomuted = pr:get_child_text("audiomuted", "http://jitsi.org/jitmeet/audio") or "";
                                local videomuted = pr:get_child_text("videomuted", "http://jitsi.org/jitmeet/video") or "";
                --              local email = pr:get_child_text("email") or "";  -- GRPD PRIVACY PROBLEMATICS

                                occupants_json:push({
                                    jid = tostring(occupant.nick),
                --                  email = tostring(email), -- GRPD PRIVACY PROBLEMATICS
                                    display_name = tostring(nick),
                                    role = tostring(occupant.role),
                                    audiomuted = tostring(audiomuted),
                                    videomuted = tostring(videomuted)});

                                -- just count 
                                participant_count=participant_count+1


                                if audiomuted=="true"  then    -- yep its not a real boolean :)
                                        audiomuted_count=audiomuted_count+1
                                end

                                if videomuted=="true"  then
                                        videomuted_count=videomuted_count+1
								end

                            end
                        end
                    end
    			end

                --- Build the room json

                -- TODO : the above commented line fail because its not the good node
--              local createdms=room:get_child_text("created-ms", "http://jabber.org/protocol/focus") or "";

				-- Build de final json
                room_json:push({
                        roomjid=tostring(room_address),
                        roomname=tostring(room_name),
                        domain=tostring(domain_name),
--                      created=tostring(createdms),
                        NBparticipant=tostring(participant_count),
                        NBaudiomuted=tostring(audiomuted_count),
                        NBvideomuted=tostring(videomuted_count),
                        participant=occupants_json})  -- add the occupant json
        end
        

    return room_json

end;


-- get the full status by .....:5280/status
function module.load()
    module:depends("http");
        module:provides("http", {
                default_path = "/";
                route = {
                        ["GET status"] = function (event) return async_handler_wrapper(event,get_all) end;
                        ["GET sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end;
                };
        });
end

Todo :

  • get the create timestamp of the room
  • get the prosody username of the moderator
  • basic secure access
  • sum stats

Hope i will work for you. This code is as it is . Use at your own risk …

Thanks to hkhait & tmaass for some great ideas

[EDIT] Be aware that its a great door for room-bombing . So YOU MUST at least change the route such as :

["GET statusMYSECRETROUTE"] = function .....

4 Likes

Thank you for sharing your code.

Hi
I have created a new dir becuase there was no prosody-plugins

cd /usr/share/jitsi-meet/
mkdir prosody-plugins
nano /usr/share/jitsi-meet/prosody-plugins/mod_muc_status.lua // created new file then copy paste the code you shared.
after that;
nano /etc/prosody/conf.avail/[hostname].cfg.lua
added “muc_status” under enabled modules
restarted jicofo,prosody,videobridge
went to domain https://fqdn.com :5280/status?domain= fqdn

nothing to see there. also allowed 5280 port in ufw.

Can you give more information about it. how the config file will recognise the mod_muc_status file if we can’t give a path in config file …

I need help please

in my debian , /usr/share/jitsi-meet/prosody-plugins/ exist and hold all jitsi prosody specific plugin
so i think you must find where they are. Search for mod_muc_size.lua

Im not a good devop, so if you encounter errors, first, try to enable mod_muc_size.lua , then you will be able to activate my script that is just an improvment
you will find many post about the mod_muc_size and problem about it , and how to solve it. At least, you must be able to get …x:5280/sessions

Thank you that works.

Thank you very much! This is great!

In my case, I went the “mod_muc_status.lua” way (1),
I also needed to add an “end” line 221

http://0.0.0.0:5280/status?domain= (or meet.jitsi)
results in “404 Not found” while /sessions is working.

Any idea?

Thanks again

yes :5280/sessions is working

/sessions is through muc_size too

@ledahu is your code based on the latest update (a few days ago) or on an older version?
Because when I diff it with muc_size, I see tiny details that shouldn’t be needed to change like:
return 400; <=> return { status_code = 400; };

And when I comment muc_size and only keep muc_status, it fails.
So i guess it silently ignores muc_status in my case.

my code is from , i guess, the last muc_size, but my code is “du code bourrin” in french (bourrin = farming horse ). I mean, i removed all that was unecessary in my point of view. i mean, a 400 error is not an option for me: it works 200 , or it fail 500 :slight_smile: = the log to correct it

Please, look at the prosody.log or prosody.err when something fail, all your solutions are in .

@ledahu merci ! J’avais pas fait le lien, “Le dahu”, hahaha, bons souvenirs.
(I’ll speak French since it’ll be easier for both of us, for all others, I’ll report eventual improvements in English at the end)
Désolé d’avance pour la longueur du message, tellement de flou.

Sur docker, y’a pas prosody.err, ni prosody.log, et /var/log/prosody est vide, j’ai juste le log du container. C’est pénible.
Bref, je vois juste qu’il ne charge pas muc_status (plus de /sessions, 404 avec Unknown host) mais sans savoir pourquoi.
Du coup je me demande si dans tes changements (on se tutoie ?) il n’y a pas quelque chose qui coince avec la dernière version de Jitsi.
Je ne connais pas lua, est-ce que ce serait possible d’avoir ton muc_size original pour faire un dif avec le mien ?
Autre point, sur docker, domain c’est jitsi.meet (réseau interne dans docker) mais le serveur est accessible par le domain dans etc/host, donc j’hésite sur lequel il faut utiliser. D’autant que j’utilise le sous-domaine de mon nom de domaine :slight_smile:
A tout hasard, est-ce que tu l’as testé avec docker ? Juste pour être sûr qu’il n’y a pas un autre problème avec ça.
Quand il me manquait l’app_id (je n’utilise pas JWT, juste internals), il me disait ne pas trouver muc_status dans modules/ et j’ai noté qu’il y a du “muc_” dans modules/ et plugins/, pourquoi ?
Du coup, je l’ai copié dans les 2, mais ça ne change rien.

Ouf, c’est fini.

Mon muc_size (jitsi-meet docker 4101-2)

mod_muc_size
-- Prosody IM
-- Copyright (C) 2017 Atlassian
--

local jid = require "util.jid";
local it = require "util.iterators";
local json = require "util.json";
local iterators = require "util.iterators";
local array = require"util.array";
local wrap_async_run = module:require "util".wrap_async_run;

local tostring = tostring;
local neturl = require "net.url";
local parse = neturl.parseQuery;

-- option to enable/disable room API token verifications
local enableTokenVerification
    = module:get_option_boolean("enable_roomsize_token_verification", false);

local token_util = module:require "token/util".new(module);
local get_room_from_jid = module:require "util".get_room_from_jid;

-- no token configuration but required
if token_util == nil and enableTokenVerification then
    log("error", "no token configuration but it is required");
    return;
end

-- required parameter for custom muc component prefix,
-- defaults to "conference"
local muc_domain_prefix
    = module:get_option_string("muc_mapper_domain_prefix", "conference");

--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_address the full room address jid
-- @return true if values are ok or false otherwise
function verify_token(token, room_address)
    if not enableTokenVerification then
        return true;
    end

    -- if enableTokenVerification is enabled and we do not have token
    -- stop here, cause the main virtual host can have guest access enabled
    -- (allowEmptyToken = true) and we will allow access to rooms info without
    -- a token
    if token == nil then
        log("warn", "no token provided");
        return false;
    end

    local session = {};
    session.auth_token = token;
    local verified, reason = token_util:process_and_verify_token(session);
    if not verified then
        log("warn", "not a valid token %s", tostring(reason));
        return false;
    end

    if not token_util:verify_room(session, room_address) then
        log("warn", "Token %s not allowed to join: %s",
            tostring(token), tostring(room_address));
        return false;
    end

    return true;
end

--- Handles request for retrieving the room size
-- @param event the http event, holds the request query
-- @return GET response, containing a json with participants count,
--         tha value is without counting the focus.
function handle_get_room_size(event)
    if (not event.request.url.query) then
        return 400;
    end

	local params = parse(event.request.url.query);
	local room_name = params["room"];
	local domain_name = params["domain"];
    local subdomain = params["subdomain"];

    local room_address
        = jid.join(room_name, muc_domain_prefix.."."..domain_name);

    if subdomain and subdomain ~= "" then
        room_address = "["..subdomain.."]"..room_address;
    end

    if not verify_token(params["token"], room_address) then
        return 403;
    end

	local room = get_room_from_jid(room_address);
	local participant_count = 0;

	log("debug", "Querying room %s", tostring(room_address));

	if room then
		local occupants = room._occupants;
		if occupants then
			participant_count = iterators.count(room:each_occupant());
		end
		log("debug",
            "there are %s occupants in room", tostring(participant_count));
	else
		log("debug", "no such room exists");
		return 404;
	end

	if participant_count > 1 then
		participant_count = participant_count - 1;
	end

	return [[{"participants":]]..participant_count..[[}]];
end

--- Handles request for retrieving the room participants details
-- @param event the http event, holds the request query
-- @return GET response, containing a json with participants details
function handle_get_room (event)
    if (not event.request.url.query) then
        return 400;
    end

	local params = parse(event.request.url.query);
	local room_name = params["room"];
	local domain_name = params["domain"];
    local subdomain = params["subdomain"];
    local room_address
        = jid.join(room_name, muc_domain_prefix.."."..domain_name);

    if subdomain ~= "" then
        room_address = "["..subdomain.."]"..room_address;
    end

    if not verify_token(params["token"], room_address) then
        return 403;
    end

	local room = get_room_from_jid(room_address);
	local participant_count = 0;
	local occupants_json = array();

	log("debug", "Querying room %s", tostring(room_address));

	if room then
		local occupants = room._occupants;
		if occupants then
			participant_count = iterators.count(room:each_occupant());
			for _, occupant in room:each_occupant() do
			    -- filter focus as we keep it as hidden participant
			    if string.sub(occupant.nick,-string.len("/focus"))~="/focus" then
				    for _, pr in occupant:each_session() do
					local nick = pr:get_child_text("nick", "http://jabber.org/protocol/nick") or "";
					local email = pr:get_child_text("email") or "";
					occupants_json:push({
					    jid = tostring(occupant.nick),
					    email = tostring(email),
					    display_name = tostring(nick)});
				    end
			    end
			end
		end
		log("debug",
            "there are %s occupants in room", tostring(participant_count));
	else
		log("debug", "no such room exists");
		return 404;
	end

	if participant_count > 1 then
		participant_count = participant_count - 1;
	end

	return json.encode(occupants_json);
end;

function module.load()
    module:depends("http");
	module:provides("http", {
		default_path = "/";
		route = {
			["GET room-size"] = function (event) return wrap_async_run(event,handle_get_room_size) end;
			["GET sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end;
			["GET room"] = function (event) return wrap_async_run(event,handle_get_room) end;
		};
	});
end

Je vais parler dans mon english de base, ca sera mieux pour les autres :slight_smile:

I do not know very well docker (in fact not at all)

but as i saw your code i will said

  • mod_muc_status.lua must be in the same directory thant mod_muc_size
  • there is 2 directory for .lua : official prosody mod and jitsi prosody mod (plugins)

My code is improvment of


My code (and the last mod_muc_size) use

local async_handler_wrapper = module:require "util".async_handler_wrapper;

you have

local wrap_async_run = module:require "util".wrap_async_run;

and then in the routes, its not the same…

may be its the point ?

i think you must find how to enable muc_size on docker (many post on this thread) , then implement my code.
Im sorry to cant help you more :frowning:

@ledahu Thanks!
/sessions was working with the muc_size (I added to my previous post) BEFORE (and after) I installed muc_status by itself.
Only muc_status is not working for me.
/sessions is still working fine with muc_size.
My problem is with status?domain=…
Which domain, what part of the code is not working …

I’m starting to learn prosody lua to solve this: https://prosody.im/doc/developers/modules

When a module is loaded in prosody.cfg.lua it’s visible by all VirtualHosts, docker logs:

|guest.meet.jitsi:hello     info|helloworld,|
|recorder.meet.jitsi:hello  info|helloworld,|
|auth.meet.jitsi:hello  info|helloworld,|
|meet.jitsi:hello    info|helloworld|

Indeed, prosody-plugins folder is for modules you add in jitsi-meet.cfg.lua

muc.meet.jitsi:hello          info	helloworld

BTW, j’avais jitsi installé par apt-get, mais je suis passé sur docker pour des raisons de sécurité.

OK, I’ll diff your muc_size with mine and a newer docker version to see differences and will try to understand what could be done.

OK, I’m getting to it, now I can get /all-rooms from muc_size, thanks to @tmaass.

Switching to mod_muc_status.lua,
I have an error in your status code @ledahu:

Encountered error: /prosody-plugins/mod_muc_status.lua:107: attempt to call a table value 

line 107 is:

    for room in allrooms  do

If lua is like javascript, it tries this loop based on local allrooms=array() (line 96) without waiting for allrooms= muc.all_rooms(); (line 102)
OR
something in if component is false in my case?

Do you know if, in lua, allrooms will be waited to get populated before line 107?
Thanks!

Finally got it working with these following changes, starting from line 79 in mod_muc_staus.lua:

function get_raw_rooms(ahost)
	local component = hosts[ahost];
	if component then
		local muc = component.modules.muc
		if muc and rawget(muc,"all_rooms") then
			return muc.all_rooms();
		end
	end
end

-- Get full list from "all_rooms", then iterate to get each room details 
function get_all(event)

        if (not event.request.url.query) then
            log("error","NO full list from all_room");
                return { status_code = 400; };
        end

        local params = parse(event.request.url.query);
        local domain_name = params["domain"];
--        local subdomain = params["subdomain"];

        local host=muc_domain_prefix.."."..domain_name

        local component = hosts[host];

        local state=array() -- to store the final json

        local raw_rooms = get_raw_rooms(domain_name);

        -- iterate the rooms
        for room in raw_rooms do
                        local room_jid = room.jid;
                        local getroomstate = get_room(room_jid) -- use de get_room() below 
                        state:push({getroomstate})
        end

        local result_json={
            state = state;
        };
        return { status_code = 200; body = json.encode(result_json) };
end

Lua is like javascript, you need to declare a final variable that get fed along the code.
If I only use local result_json=state; in my code above, I get a [] result.
I guess there’s a more elegant way to code it.

Thank you again @ledahu, going the muc_status way is great when you update.

Cool you succeed .
The question is why

local allrooms=array() 
if component then
                local muc = component.modules.muc
                if muc and rawget(muc,"all_rooms") then
                        allrooms= muc.all_rooms();
                end
        end
 for room in allrooms  do ....

works for me (declaration , fill and loop on the array) and not for you …
luarock version (im on 5.2) ? Prosody version (im on 0.11.5) ?

I mean, your code is just initializing all_room with a function ! And there is no async call and wait promise, so … weird

Whatever, cool it works !

Hi, im a German and my english is not very good. So please excuse me.

I dont get this monitoring running.
I’m not a profession Programmer and i dont have many expirience with linux.

I do the following steps:

cd /usr/share/jitsi-meet/prosody-plugins

nano /usr/share/jitsi-meet/prosody-plugins/mod_muc_status.lua //
created new file then copy paste the code on the top you shared.
nano /usr/share/jitsi-meet/prosody-plugins/mod_muc_size.lua
– copy and paste from this link
https://github.com/jitsi/jitsi-meet/blob/master/resources/prosody-plugins/mod_muc_size.lua

nano /etc/prosody/conf.avail/[hostname].cfg.lua
added “muc_status” and "mux_size in Line 42+43 (Are that the right positions?)

restarted jicofo,prosody,videobridge

went to domain “X:5280/sessions”
Example:" https://example.com:5280/sessions"
“X:5280/status?domain=example”

Both dont work.

Where can i find the error log for the lua scripts?
Prosody Error Log:

Apr 11 13:27:57 modulemanager error Error initializing module ‘muc_size’ on ‘robnexjitsi__de’: /usr/lib/prosody/util/startup.lua:144: module ‘luajwtjitsi’ not fou>
no field package.preload[‘luajwtjitsi’]
no file ‘/usr/lib/prosody/luajwtjitsi.lua’
no file ‘/usr/local/share/lua/5.2/luajwtjitsi.lua’
no file ‘/usr/local/share/lua/5.2/luajwtjitsi/init.lua’
no file ‘/usr/local/lib/lua/5.2/luajwtjitsi.lua’
no file ‘/usr/local/lib/lua/5.2/luajwtjitsi/init.lua’
no file ‘/usr/share/lua/5.2/luajwtjitsi.lua’
no file ‘/usr/share/lua/5.2/luajwtjitsi/init.lua’
no file ‘/var/lib/prosody/.luarocks/share/lua/5.2/luajwtjitsi.lua’
no file ‘/var/lib/prosody/.luarocks/share/lua/5.2/luajwtjitsi/init.lua’
no file ‘/usr/lib/prosody/luajwtjitsi.so’
no file ‘/usr/local/lib/lua/5.2/luajwtjitsi.so’
no file ‘/usr/lib/x86_64-linux-gnu/lua/5.2/luajwtjitsi.so’
no file ‘/usr/lib/lua/5.2/luajwtjitsi.so’
no file ‘/usr/local/lib/lua/5.2/loadall.so’
no file ‘/var/lib/prosody/.luarocks/lib/lua/5.2/luajwtjitsi.so’
stack traceback:
[C]: in function ‘_real_require’
/usr/lib/prosody/util/startup.lua:144: in function ‘require’
/usr/share/jitsi-meet/prosody-plugins/token/util.lib.lua:7: in main chunk
(…tail calls…)
/usr/share/jitsi-meet/prosody-plugins/mod_muc_size.lua:27: in main chunk
[C]: in function ‘xpcall’
/usr/lib/prosody/core/modulemanager.lua:178: in function ‘do_load_module’
/usr/lib/prosody/core/modulemanager.lua:256: in function ‘load’
/usr/lib/prosody/core/modulemanager.lua:78: in function ‘?’
/usr/lib/prosody/util/events.lua:79: in function </usr/lib/prosody/util/events.lua:75>
(…tail calls…)
/usr/lib/prosody/core/hostmanager.lua:108: in function ‘activate’
/usr/lib/prosody/core/hostmanager.lua:58: in function ‘?’
/usr/lib/prosody/util/events.lua:79: in function </usr/lib/prosody/util/events.lua:75>
(…tail calls…)
/usr/lib/prosody/util/startup.lua:330: in function ‘prepare_to_start’
/usr/lib/prosody/util/startup.lua:551: in function ‘f’
/usr/lib/prosody/util/async.lua:139: in function ‘func’
/usr/lib/prosody/util/async.lua:127: in function </usr/lib/prosody/util/async.lua:125>

Greetings

hi

all seems fine, but you need luajwtlitsi

may be this post will help you (seems you already have 5.2)

EDIT: did you tru without the muc_size and only muc_status ? same error ?

without muc_size and muc_status i get no error

only get this one:
Error binding encrypted port for https: No certificate present in SSL/TLS configuration for https port 5281

System Infos:
Jitsi install guide from:
https://crosstalksolutions.com/how-to-install-jitsi-server-on-ubuntu-19-10/

Ubuntu: 19.10
nginx version: nginx/1.16.1 (Ubuntu)
Lua 5.2.4
Luarocks 3.3.1