Browse Source

feat: A/V moderation (prosody module) (#9106)

* feat(prosody-modules): Moves a function for getting room to util.

* feat: Audio/Video moderation.

* squash: Fix docs.

* squash: Changes a field name in the message for adding jid to whitelist.

* squash: Moves to boolean from boolean string.

* squash: Only moderators get whitelist on join.

* squash: Check whether in room and moderator.

* squash: Send to participants only message about approval.

Skips sending the whole list.

* feat: Separates enable/disable by media type.

Adds actor to the messages to inform who enabled it.

* squash: Fixes reporting disable of the feature.

* squash: Fixes init of av_moderation_actors.

* squash: Fixes av_moderation_actor jid to be room jid.

* squash: Fixes comments.

* squash: Fixes warning about shadowing definition.

* squash: Updates ljm.

* fix: Fixes auto-granting from jicofo.

* squash: Further simplify...
j8
Дамян Минков 4 years ago
parent
commit
5c08b1ec5b
No account linked to committer's email address

+ 5
- 0
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example View File

@@ -35,6 +35,7 @@ VirtualHost "jitmeet.example.com"
35 35
         key = "/etc/prosody/certs/jitmeet.example.com.key";
36 36
         certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
37 37
     }
38
+    av_moderation_component = "avmoderation.jitmeet.example.com"
38 39
     speakerstats_component = "speakerstats.jitmeet.example.com"
39 40
     conference_duration_component = "conferenceduration.jitmeet.example.com"
40 41
     -- we need bosh
@@ -46,6 +47,7 @@ VirtualHost "jitmeet.example.com"
46 47
         "external_services";
47 48
         "conference_duration";
48 49
         "muc_lobby_rooms";
50
+        "av_moderation";
49 51
     }
50 52
     c2s_require_encryption = false
51 53
     lobby_muc = "lobby.jitmeet.example.com"
@@ -86,6 +88,9 @@ Component "speakerstats.jitmeet.example.com" "speakerstats_component"
86 88
 Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
87 89
     muc_component = "conference.jitmeet.example.com"
88 90
 
91
+Component "avmoderation.jitmeet.example.com" "av_moderation_component"
92
+    muc_component = "conference.jitmeet.example.com"
93
+
89 94
 Component "lobby.jitmeet.example.com" "muc"
90 95
     storage = "memory"
91 96
     restrict_room_creation = true

+ 2
- 2
package-lock.json View File

@@ -11058,8 +11058,8 @@
11058 11058
       }
11059 11059
     },
11060 11060
     "lib-jitsi-meet": {
11061
-      "version": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
11062
-      "from": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
11061
+      "version": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
11062
+      "from": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
11063 11063
       "requires": {
11064 11064
         "@jitsi/js-utils": "1.0.2",
11065 11065
         "@jitsi/sdp-interop": "1.0.3",

+ 1
- 1
package.json View File

@@ -54,7 +54,7 @@
54 54
     "jquery-i18next": "1.2.1",
55 55
     "js-md5": "0.6.1",
56 56
     "jwt-decode": "2.2.0",
57
-    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
57
+    "lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
58 58
     "libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
59 59
     "lodash": "4.17.21",
60 60
     "moment": "2.29.1",

+ 26
- 0
resources/prosody-plugins/mod_av_moderation.lua View File

@@ -0,0 +1,26 @@
1
+local formdecode = require 'util.http'.formdecode;
2
+
3
+local avmoderation_component = module:get_option_string('av_moderation_component', 'avmoderation'..module.host);
4
+
5
+-- Advertise AV Moderation so client can pick up the address and use it
6
+module:add_identity('component', 'av_moderation', avmoderation_component);
7
+
8
+-- Extract 'room' param from URL when session is created
9
+function update_session(event)
10
+    local session = event.session;
11
+
12
+    if session.jitsi_web_query_room then
13
+        -- no need for an update
14
+        return;
15
+    end
16
+
17
+    local query = event.request.url.query;
18
+    if query ~= nil then
19
+        local params = formdecode(query);
20
+        -- The room name and optional prefix from the web query
21
+        session.jitsi_web_query_room = params.room;
22
+        session.jitsi_web_query_prefix = params.prefix or '';
23
+    end
24
+end
25
+module:hook_global('bosh-session', update_session);
26
+module:hook_global('websocket-session', update_session);

+ 250
- 0
resources/prosody-plugins/mod_av_moderation_component.lua View File

@@ -0,0 +1,250 @@
1
+local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
2
+local is_healthcheck_room = module:require 'util'.is_healthcheck_room;
3
+local json = require 'util.json';
4
+local st = require 'util.stanza';
5
+
6
+local muc_component_host = module:get_option_string('muc_component');
7
+if muc_component_host == nil then
8
+    log('error', 'No muc_component specified. No muc to operate on!');
9
+    return;
10
+end
11
+
12
+module:log('info', 'Starting av_moderation for %s', muc_component_host);
13
+
14
+-- Sends a json-message to the destination jid
15
+-- @param to_jid the destination jid
16
+-- @param json_message the message content to send
17
+function send_json_message(to_jid, json_message)
18
+    local stanza = st.message({ from = module.host; to = to_jid; })
19
+         :tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
20
+    module:send(stanza);
21
+end
22
+
23
+-- Notifies that av moderation has been enabled or disabled
24
+-- @param jid the jid to notify, if missing will notify all occupants
25
+-- @param enable whether it is enabled or disabled
26
+-- @param room the room
27
+-- @param actorJid the jid that is performing the enable/disable operation (the muc jid)
28
+-- @param mediaType the media type for the moderation
29
+function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
30
+    local body_json = {};
31
+    body_json.type = 'av_moderation';
32
+    body_json.enabled = enable;
33
+    body_json.room = room.jid;
34
+    body_json.actor = actorJid;
35
+    body_json.mediaType = mediaType;
36
+    local body_json_str = json.encode(body_json);
37
+
38
+    if jid then
39
+        send_json_message(jid, body_json_str)
40
+    else
41
+        for _, occupant in room:each_occupant() do
42
+            send_json_message(occupant.jid, body_json_str)
43
+        end
44
+    end
45
+end
46
+
47
+-- Notifies about a jid added to the whitelist. Notifies all moderators and admin and the jid itself
48
+-- @param jid the jid to notify about the change
49
+-- @param moderators whether to notify all moderators in the room
50
+-- @param room the room where to send it
51
+-- @param mediaType used only when a participant is approved (not sent to moderators)
52
+function notify_whitelist_change(jid, moderators, room, mediaType)
53
+    local body_json = {};
54
+    body_json.type = 'av_moderation';
55
+    body_json.room = room.jid;
56
+    body_json.whitelists = room.av_moderation;
57
+    local moderators_body_json_str = json.encode(body_json);
58
+    body_json.whitelists = nil;
59
+    body_json.approved = true; -- we want to send to participants only that they were approved to unmute
60
+    body_json.mediaType = mediaType;
61
+    local participant_body_json_str = json.encode(body_json);
62
+
63
+    for _, occupant in room:each_occupant() do
64
+        if moderators and occupant.role == 'moderator' then
65
+            send_json_message(occupant.jid, moderators_body_json_str);
66
+        elseif occupant.jid == jid then
67
+            send_json_message(occupant.jid, participant_body_json_str);
68
+        end
69
+    end
70
+end
71
+
72
+-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
73
+-- jids to the whitelist
74
+function on_message(event)
75
+    local session = event.origin;
76
+
77
+    -- Check the type of the incoming stanza to avoid loops:
78
+    if event.stanza.attr.type == 'error' then
79
+        return; -- We do not want to reply to these, so leave.
80
+    end
81
+
82
+    if not session or not session.jitsi_web_query_room then
83
+        return false;
84
+    end
85
+
86
+    local moderation_command = event.stanza:get_child('av_moderation');
87
+
88
+    if moderation_command then
89
+        -- get room name with tenant and find room
90
+        local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
91
+
92
+        if not room then
93
+            module:log('warn', 'No room found found for %s/%s',
94
+                    session.jitsi_web_query_prefix, session.jitsi_web_query_room);
95
+            return false;
96
+        end
97
+
98
+        -- check that the participant requesting is a moderator and is an occupant in the room
99
+        local from = event.stanza.attr.from;
100
+        local occupant = room:get_occupant_by_real_jid(from);
101
+        if not occupant then
102
+            log('warn', 'No occupant %s found for %s', from, room.jid);
103
+            return false;
104
+        end
105
+        if occupant.role ~= 'moderator' then
106
+            log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
107
+            return false;
108
+        end
109
+
110
+        local mediaType = moderation_command.attr.mediaType;
111
+        if mediaType then
112
+            if mediaType ~= 'audio' and mediaType ~= 'video' then
113
+                module:log('warn', 'Wrong mediaType %s for %s', mediaType, room.jid);
114
+                return false;
115
+            end
116
+        else
117
+            module:log('warn', 'Missing mediaType for %s', room.jid);
118
+            return false;
119
+        end
120
+
121
+        if moderation_command.attr.enable ~= nil then
122
+            local enabled;
123
+            if moderation_command.attr.enable == 'true' then
124
+                enabled = true;
125
+                if room.av_moderation and room.av_moderation[mediaType] then
126
+                    module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
127
+                    return true;
128
+                else
129
+                    room.av_moderation = {};
130
+                    room.av_moderation_actors = {};
131
+                    room.av_moderation[mediaType] = {};
132
+                    room.av_moderation_actors[mediaType] = occupant.nick;
133
+                end
134
+            else
135
+                enabled = false;
136
+                if not room.av_moderation or not room.av_moderation[mediaType] then
137
+                    module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
138
+                    return true;
139
+                else
140
+                    room.av_moderation[mediaType] = nil;
141
+                    room.av_moderation_actors[mediaType] = nil;
142
+
143
+                    -- clears room.av_moderation if empty
144
+                    local is_empty = false;
145
+                    for key,_ in pairs(room.av_moderation) do
146
+                        if room.av_moderation[key] then
147
+                            is_empty = true;
148
+                        end
149
+                    end
150
+                    if is_empty then
151
+                        room.av_moderation = nil;
152
+                    end
153
+                end
154
+            end
155
+
156
+            -- send message to all occupants
157
+            notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
158
+            return true;
159
+        elseif moderation_command.attr.jidToWhitelist and room.av_moderation then
160
+            local occupant_jid = moderation_command.attr.jidToWhitelist;
161
+            -- check if jid is in the room, if so add it to whitelist
162
+            -- inform all moderators and admins and the jid
163
+            local occupant_to_add = room:get_occupant_by_nick(occupant_jid);
164
+
165
+            if not occupant_to_add then
166
+                module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
167
+                return false;
168
+            end
169
+
170
+            local whitelist = room.av_moderation[mediaType];
171
+            if not whitelist then
172
+                whitelist = {};
173
+                room.av_moderation[mediaType] = whitelist;
174
+            end
175
+            table.insert(whitelist, occupant_jid);
176
+
177
+            notify_whitelist_change(occupant_to_add.jid, true, room, mediaType);
178
+
179
+            return true;
180
+        end
181
+    end
182
+
183
+    -- return error
184
+    return false
185
+end
186
+
187
+-- handles new occupants to inform them about the state enabled/disabled, new moderators also get and the whitelist
188
+function occupant_joined(event)
189
+    local room, occupant = event.room, event.occupant;
190
+
191
+    if is_healthcheck_room(room.jid) then
192
+        return;
193
+    end
194
+
195
+    if room.av_moderation then
196
+        for _,mediaType in pairs({'audio', 'video'}) do
197
+            if room.av_moderation[mediaType] then
198
+                notify_occupants_enable(
199
+                    occupant.jid, true, room, room.av_moderation_actors[mediaType], mediaType);
200
+            end
201
+        end
202
+
203
+        -- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
204
+        -- from allowners module) but iterating over room occupants returns the correct role
205
+        for _, room_occupant in room:each_occupant() do
206
+            -- if moderator send the whitelist
207
+            if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator'  then
208
+                notify_whitelist_change(room_occupant.jid, false, room);
209
+            end
210
+        end
211
+    end
212
+end
213
+
214
+-- when a occupant was granted moderator we need to update him with the whitelist
215
+function occupant_affiliation_changed(event)
216
+    -- the actor can be nil if is coming from allowners or similar module we want to skip it here
217
+    -- as we will handle it in occupant_joined
218
+    if event.actor and event.affiliation == 'owner' and event.room.av_moderation then
219
+        local room = event.room;
220
+        -- event.jid is the bare jid of participant
221
+        for _, occupant in room:each_occupant() do
222
+            if occupant.bare_jid == event.jid then
223
+                notify_whitelist_change(occupant.jid, false, room);
224
+            end
225
+        end
226
+    end
227
+end
228
+
229
+-- we will receive messages from the clients
230
+module:hook('message/host', on_message);
231
+
232
+-- executed on every host added internally in prosody, including components
233
+function process_host(host)
234
+    if host == muc_component_host then -- the conference muc component
235
+        module:log('info','Hook to muc events on %s', host);
236
+
237
+        local muc_module = module:context(host);
238
+        muc_module:hook('muc-occupant-joined', occupant_joined, -2); -- make sure it runs after allowners or similar
239
+        muc_module:hook('muc-set-affiliation', occupant_affiliation_changed, -1);
240
+    end
241
+end
242
+
243
+if prosody.hosts[muc_component_host] == nil then
244
+    module:log('info', 'No muc component found, will listen for it: %s', muc_component_host);
245
+
246
+    -- when a host or component is added
247
+    prosody.events.add_handler('host-activated', process_host);
248
+else
249
+    process_host(muc_component_host);
250
+end

+ 5
- 16
resources/prosody-plugins/mod_muc_poltergeist.lua View File

@@ -1,5 +1,5 @@
1 1
 local bare = require "util.jid".bare;
2
-local get_room_from_jid = module:require "util".get_room_from_jid;
2
+local get_room_by_name_and_subdomain = module:require "util".get_room_by_name_and_subdomain;
3 3
 local jid = require "util.jid";
4 4
 local neturl = require "net.url";
5 5
 local parse = neturl.parseQuery;
@@ -39,21 +39,6 @@ local disableTokenVerification
39 39
 
40 40
 -- poltergaist management functions
41 41
 
42
-function get_room(room_name, group)
43
-    local room_address = jid.join(room_name, module:get_host());
44
-    -- if there is a group we are in multidomain mode and that group is not
45
-    -- our parent host
46
-    if group and group ~= "" and group ~= parentHostName then
47
-        room_address = "["..group.."]"..room_address;
48
-    end
49
-
50
-    return get_room_from_jid(room_address);
51
-end
52
-
53 42
 --- Verifies room name, domain name with the values in the token
54 43
 -- @param token the token we received
55 44
 -- @param room_name the room name
@@ -105,7 +90,7 @@ end
105 90
 prosody.events.add_handler("pre-jitsi-authentication", function(session)
106 91
 
107 92
     if (session.jitsi_meet_context_user) then
108
-        local room = get_room(
93
+        local room = get_room_by_name_and_subdomain(
109 94
             session.jitsi_web_query_room,
110 95
             session.jitsi_web_query_prefix);
111 96
 
@@ -194,7 +179,7 @@ function handle_create_poltergeist (event)
194 179
 
195 180
     -- If the provided room conference doesn't exist then we
196 181
     -- can't add a poltergeist to it.
197
-    local room = get_room(room_name, group);
182
+    local room = get_room_by_name_and_subdomain(room_name, group);
198 183
     if (not room) then
199 184
         log("error", "no room found %s", room_name);
200 185
         return { status_code = 404; };
@@ -257,7 +242,7 @@ function handle_update_poltergeist (event)
257 242
         return { status_code = 403; };
258 243
     end
259 244
 
260
-    local room = get_room(room_name, group);
245
+    local room = get_room_by_name_and_subdomain(room_name, group);
261 246
     if (not room) then
262 247
         log("error", "no room found %s", room_name);
263 248
         return { status_code = 404; };
@@ -299,7 +284,7 @@ function handle_remove_poltergeist (event)
299 284
         return { status_code = 403; };
300 285
     end
301 286
 
302
-    local room = get_room(room_name, group);
287
+    local room = get_room_by_name_and_subdomain(room_name, group);
303 288
     if (not room) then
304 289
         log("error", "no room found %s", room_name);
305 290
         return { status_code = 404; };

+ 22
- 8
resources/prosody-plugins/util.lib.lua View File

@@ -8,23 +8,19 @@ local http_headers = {
8 8
     ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
9 9
 };
10 10
 
11
-local muc_domain_prefix
12
-    = module:get_option_string("muc_mapper_domain_prefix", "conference");
11
+local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
13 12
 
14 13
 -- defaults to module.host, the module that uses the utility
15
-local muc_domain_base
16
-    = module:get_option_string("muc_mapper_domain_base", module.host);
14
+local muc_domain_base = module:get_option_string("muc_mapper_domain_base", module.host);
17 15
 
18 16
 -- The "real" MUC domain that we are proxying to
19
-local muc_domain = module:get_option_string(
20
-    "muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
17
+local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
21 18
 
22 19
 local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
23 20
 local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
24 21
 -- The pattern used to extract the target subdomain
25 22
 -- (e.g. extract 'foo' from 'conference.foo.example.com')
26
-local target_subdomain_pattern
27
-    = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
23
+local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
28 24
 
29 25
 -- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
30 26
 local roomless_iqs = {};
@@ -120,6 +116,23 @@ function get_room_from_jid(room_jid)
120 116
     end
121 117
 end
122 118
 
119
+-- Returns the room if available, work and in multidomain mode
120
+-- @param room_name the name of the room
121
+-- @param group name of the group (optional)
122
+-- @return returns room if found or nil
123
+function get_room_by_name_and_subdomain(room_name, subdomain)
124
+    local room_address;
125
+
126
+    -- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
127
+    if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
128
+        room_address = "["..subdomain.."]"..room_address;
129
+    else
130
+        room_address = jid.join(room_name, muc_domain);
131
+    end
132
+
133
+    return get_room_from_jid(room_address);
134
+end
135
+
123 136
 function async_handler_wrapper(event, handler)
124 137
     if not have_async then
125 138
         module:log("error", "requires a version of Prosody with util.async");
@@ -347,6 +360,7 @@ return {
347 360
     is_feature_allowed = is_feature_allowed;
348 361
     is_healthcheck_room = is_healthcheck_room;
349 362
     get_room_from_jid = get_room_from_jid;
363
+    get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
350 364
     async_handler_wrapper = async_handler_wrapper;
351 365
     presence_check_status = presence_check_status;
352 366
     room_jid_match_rewrite = room_jid_match_rewrite;

Loading…
Cancel
Save