Browse Source

feat: Visitors chat (#13112)

* feat(visitors): Visitors chat.

Only live chat is supported, no room history is loaded on visitor nodes.

* feat(visitors): Append display name to promotion requests.

* feat: Indicate visitor's chat messages.

* fix: Fixes preconfigure vnodes script.

* fix: Fixes few cases around routing messages.

Disables private messaging on visitor node and add anonymous name to those visitors without a pre-set name.

* fix: Fixes grouping visitor's messages and some error translations.

* squash: Fixes main.json.
factor2
Дамян Минков 2 years ago
parent
commit
397b94da79
No account linked to committer's email address

+ 1
- 1
lang/main-bg.json View File

@@ -58,7 +58,7 @@
58 58
         "today": "Днес"
59 59
     },
60 60
     "chat": {
61
-        "error": "Грешка: вашето съобщение не бе изпратено, пради: {{error}}",
61
+        "error": "Грешка: вашето съобщение не бе изпратено, поради: {{error}}",
62 62
         "fieldPlaceHolder": "Въведете съобщението",
63 63
         "messageTo": "Лично съобщение до {{recipient}}",
64 64
         "messagebox": "Въведете съобщение",

+ 1
- 7
lang/main-enGB.json View File

@@ -46,21 +46,15 @@
46 46
         "today": "Today"
47 47
     },
48 48
     "chat": {
49
-        "error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
50
-        "fieldPlaceHolder": "",
51
-        "messageTo": "",
52 49
         "messagebox": "Type a message",
53 50
         "nickname": {
54 51
             "popover": "Choose a nickname",
55 52
             "title": "Enter a nickname to use chat",
56 53
             "titleWithPolls": "Enter a nickname to use chat"
57 54
         },
58
-        "noMessagesMessage": "",
59
-        "privateNotice": "",
60 55
         "sendButton": "Send",
61 56
         "title": "Chat",
62
-        "titleWithPolls": "Chat",
63
-        "you": ""
57
+        "titleWithPolls": "Chat"
64 58
     },
65 59
     "chromeExtensionBanner": {
66 60
         "buttonText": "",

+ 1
- 0
lang/main.json View File

@@ -1378,6 +1378,7 @@
1378 1378
         "webAssemblyWarningDescription": "WebAssembly disabled or not supported by this browser"
1379 1379
     },
1380 1380
     "visitors": {
1381
+        "chatIndicator": "(visitor)",
1381 1382
         "labelTooltip": "Number of visitors: {{count}}",
1382 1383
         "notification": {
1383 1384
             "description": "To participate raise your hand",

+ 1
- 1
react/features/base/config/constants.ts View File

@@ -64,7 +64,7 @@ export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-bac
64 64
 /**
65 65
  * The toolbar buttons to show when in visitors mode.
66 66
  */
67
-export const VISITORS_MODE_BUTTONS = [ 'hangup', 'raisehand', 'tileview' ];
67
+export const VISITORS_MODE_BUTTONS = [ 'chat', 'hangup', 'raisehand', 'tileview' ];
68 68
 
69 69
 /**
70 70
  * The set of feature flags.

+ 27
- 14
react/features/chat/middleware.ts View File

@@ -6,6 +6,7 @@ import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
6 6
 import { getCurrentConference } from '../base/conference/functions';
7 7
 import { IJitsiConference } from '../base/conference/reducer';
8 8
 import { openDialog } from '../base/dialog/actions';
9
+import i18next from '../base/i18n/i18next';
9 10
 import {
10 11
     JitsiConferenceErrors,
11 12
     JitsiConferenceEvents
@@ -229,10 +230,14 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
229 230
 
230 231
     conference.on(
231 232
         JitsiConferenceEvents.MESSAGE_RECEIVED,
232
-        (id: string, message: string, timestamp: number) => {
233
-            _onConferenceMessageReceived(store, { id,
233
+        // eslint-disable-next-line max-params
234
+        (id: string, message: string, timestamp: number, displayName: string, isGuest?: boolean) => {
235
+            _onConferenceMessageReceived(store, {
236
+                id: id || displayName, // in case of messages coming from visitors we can have unknown id
234 237
                 message,
235 238
                 timestamp,
239
+                displayName,
240
+                isGuest,
236 241
                 privateMessage: false });
237 242
         }
238 243
     );
@@ -291,8 +296,9 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
291 296
  * @param {Object} message - The message object.
292 297
  * @returns {void}
293 298
  */
294
-function _onConferenceMessageReceived(store: IStore, { id, message, timestamp, privateMessage }: {
295
-    id: string; message: string; privateMessage: boolean; timestamp: number; }) {
299
+function _onConferenceMessageReceived(store: IStore, { displayName, id, isGuest, message, timestamp, privateMessage }: {
300
+    displayName?: string; id: string; isGuest?: boolean;
301
+    message: string; privateMessage: boolean; timestamp: number; }) {
296 302
     const isGif = isGifMessage(message);
297 303
 
298 304
     if (isGif) {
@@ -302,7 +308,9 @@ function _onConferenceMessageReceived(store: IStore, { id, message, timestamp, p
302 308
         }
303 309
     }
304 310
     _handleReceivedMessage(store, {
311
+        displayName,
305 312
         id,
313
+        isGuest,
306 314
         message,
307 315
         privateMessage,
308 316
         lobbyChat: false,
@@ -389,13 +397,14 @@ function getLobbyChatDisplayName(state: IReduxState, id: string) {
389 397
  *
390 398
  * @param {Store} store - The Redux store.
391 399
  * @param {Object} message - The message object.
392
- * @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
393
- * @param {boolean} isReaction - Whether or not the message is a reaction message.
400
+ * @param {boolean} shouldPlaySound - Whether to play the incoming message sound.
401
+ * @param {boolean} isReaction - Whether the message is a reaction message.
394 402
  * @returns {void}
395 403
  */
396 404
 function _handleReceivedMessage({ dispatch, getState }: IStore,
397
-        { id, message, privateMessage, timestamp, lobbyChat }: {
398
-        id: string; lobbyChat: boolean; message: string; privateMessage: boolean; timestamp: number; },
405
+        { displayName, id, isGuest, message, privateMessage, timestamp, lobbyChat }: {
406
+        displayName?: string; id: string; isGuest?: boolean; lobbyChat: boolean;
407
+        message: string; privateMessage: boolean; timestamp: number; },
399 408
         shouldPlaySound = true,
400 409
         isReaction = false
401 410
 ) {
@@ -408,14 +417,14 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
408 417
         dispatch(playSound(INCOMING_MSG_SOUND_ID));
409 418
     }
410 419
 
411
-    // Provide a default for for the case when a message is being
420
+    // Provide a default for the case when a message is being
412 421
     // backfilled for a participant that has left the conference.
413 422
     const participant = getParticipantById(state, id) || { local: undefined };
414 423
 
415 424
     const localParticipant = getLocalParticipant(getState);
416
-    const displayName = lobbyChat
425
+    let displayNameToShow = lobbyChat
417 426
         ? getLobbyChatDisplayName(state, id)
418
-        : getParticipantDisplayName(state, id);
427
+        : displayName || getParticipantDisplayName(state, id);
419 428
     const hasRead = participant.local || isChatOpen;
420 429
     const timestampToDate = timestamp ? new Date(timestamp) : new Date();
421 430
     const millisecondsTimestamp = timestampToDate.getTime();
@@ -424,8 +433,12 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
424 433
     const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
425 434
         && !hasRead && !isReaction && !timestamp;
426 435
 
436
+    if (isGuest) {
437
+        displayNameToShow = `${displayNameToShow} ${i18next.t('visitors.chatIndicator')}`;
438
+    }
439
+
427 440
     dispatch(addMessage({
428
-        displayName,
441
+        displayName: displayNameToShow,
429 442
         hasRead,
430 443
         id,
431 444
         messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
@@ -439,7 +452,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
439 452
 
440 453
     if (shouldShowNotification) {
441 454
         dispatch(showMessageNotification({
442
-            title: displayName,
455
+            title: displayNameToShow,
443 456
             description: message
444 457
         }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
445 458
     }
@@ -450,7 +463,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
450 463
         APP.API.notifyReceivedChatMessage({
451 464
             body: message,
452 465
             id,
453
-            nick: displayName,
466
+            nick: displayNameToShow,
454 467
             privateMessage,
455 468
             ts: timestamp
456 469
         });

+ 3
- 1
react/features/video-menu/components/web/ParticipantContextMenu.tsx View File

@@ -25,6 +25,7 @@ import { getQuickActionButtonType, isForceMuted } from '../../../participants-pa
25 25
 // @ts-ignore
26 26
 import { requestRemoteControl, stopController } from '../../../remote-control';
27 27
 import { showOverflowDrawer } from '../../../toolbox/functions.web';
28
+import { iAmVisitor } from '../../../visitors/functions';
28 29
 
29 30
 import CustomOptionButton from './CustomOptionButton';
30 31
 // @ts-ignore
@@ -146,6 +147,7 @@ const ParticipantContextMenu = ({
146 147
     const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
147 148
     const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
148 149
         = useSelector((state: IReduxState) => state['features/base/config']);
150
+    const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
149 151
     const { disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
150 152
     const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
151 153
     const _volume = (participant?.local ?? true ? undefined
@@ -264,7 +266,7 @@ const ParticipantContextMenu = ({
264 266
             participantID = { _getCurrentParticipantId() } />);
265 267
     }
266 268
 
267
-    if (!disablePrivateChat) {
269
+    if (!disablePrivateChat && !visitorsMode) {
268 270
         buttons2.push(<PrivateMessageMenuButton
269 271
             key = 'privateMessage'
270 272
             participantID = { _getCurrentParticipantId() } />

+ 5
- 0
resources/extra-large-conference/pre-configure.sh View File

@@ -26,6 +26,11 @@ do
26 26
     cp prosody.cfg.lua.visitor.template /etc/prosody-v${i}/prosody.cfg.lua
27 27
     sed -i "s/vX/v${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
28 28
     sed -i "s/jitmeet.example.com/$JICOFO_HOSTNAME/g" /etc/prosody-v${i}/prosody.cfg.lua
29
+    # fix the ports
30
+    sed -i "s/52691/5269${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
31
+    sed -i "s/52221/5222${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
32
+    sed -i "s/52801/5280${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
33
+    sed -i "s/52811/5281${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
29 34
 done
30 35
 
31 36
 # Configure jicofo

+ 2
- 0
resources/extra-large-conference/prosody.cfg.lua.visitor.template View File

@@ -35,11 +35,13 @@ modules_enabled = {
35 35
 };
36 36
 
37 37
 s2s_whitelist = {
38
+    'conference.jitmeet.example.com', -- needed for visitors to send messages to main room
38 39
     'visitors.jitmeet.example.com'; -- needed for sending promotion request to visitors.jitmeet.example.com component
39 40
     'jitmeet.example.com'; -- unavailable presences back to main room
40 41
 };
41 42
 
42 43
 s2s_connect_overrides = {
44
+    ["conference.jitmeet.example.com"] = { "127.0.0.1", 5269 }; -- needed for visitors to send messages to main room
43 45
     ["jitmeet.example.com"] = { "127.0.0.1", 5269 }; -- needed for the main room when connecting in to send main participants
44 46
     ["visitors.jitmeet.example.com"] = { "127.0.0.1", 5269 }; -- needed for sending promotion request to visitors.jitmeet.example.com component
45 47
 }

+ 61
- 3
resources/prosody-plugins/mod_fmuc.lua View File

@@ -10,7 +10,6 @@
10 10
 --- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com), as there
11 11
 --- are certain configs for whitelisted domains and connections that are domain based
12 12
 --- TODO: filter presence from main occupants back to main prosody
13
---- TODO: filter messages back to main prosody
14 13
 local jid = require 'util.jid';
15 14
 local st = require 'util.stanza';
16 15
 local new_id = require 'util.id'.medium;
@@ -24,6 +23,8 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
24 23
 local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
25 24
 local main_domain = string.gsub(module.host, muc_domain_prefix..'.', '');
26 25
 
26
+local NICK_NS = 'http://jabber.org/protocol/nick';
27
+
27 28
 -- This is the domain of the main prosody that is federating with us;
28 29
 local fmuc_main_domain;
29 30
 
@@ -138,7 +139,6 @@ module:hook('muc-broadcast-presence', function (event)
138 139
     local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand');
139 140
     -- a promotion detected let's send it to main prosody
140 141
     if raiseHand then
141
-        -- TODO check room= with tenants
142 142
         local iq_id = new_id();
143 143
         sent_iq_cache:set(iq_id, socket.gettime());
144 144
         local promotion_request = st.iq({
@@ -149,7 +149,12 @@ module:hook('muc-broadcast-presence', function (event)
149 149
           :tag('visitors', { xmlns = 'jitsi:visitors',
150 150
                              room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..fmuc_main_domain) })
151 151
           :tag('promotion-request', { xmlns = 'jitsi:visitors', jid = occupant.jid }):up();
152
-        -- TODO what about name ???? it will be coming from the token, but we need to extract it and send it to the moderators
152
+
153
+        local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
154
+        if nick_element then
155
+            promotion_request:add_child(nick_element);
156
+        end
157
+
153 158
         module:send(promotion_request);
154 159
     end
155 160
 
@@ -236,3 +241,56 @@ end
236 241
 process_host_module(main_domain, function(host_module, host)
237 242
     host_module:hook('iq/host', stanza_handler, 10);
238 243
 end);
244
+
245
+-- only live chat is supported for visitors
246
+module:hook("muc-occupant-groupchat", function(event)
247
+    local occupant, room, stanza = event.occupant, event.room, event.stanza;
248
+    local from = stanza.attr.from;
249
+    local occupant_host = jid.host(occupant.bare_jid);
250
+
251
+    -- if there is no occupant this is a message from main, probably coming from other vnode
252
+    if occupant then
253
+        -- we manage nick only for visitors
254
+        if occupant_host ~= fmuc_main_domain then
255
+            -- add to message stanza display name for the visitor
256
+            -- remove existing nick to avoid forgery
257
+            stanza:remove_children('nick', NICK_NS);
258
+            local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
259
+            if nick_element then
260
+                stanza:add_child(nick_element);
261
+            else
262
+                stanza:tag('nick', { xmlns = NICK_NS })
263
+                    :text('anonymous'):up();
264
+            end
265
+        end
266
+
267
+        stanza.attr.from = occupant.nick;
268
+    else
269
+        stanza.attr.from = jid.join(jid.node(from), module.host);
270
+    end
271
+
272
+    -- let's send it to main chat and rest of visitors here
273
+    for _, o in room:each_occupant() do
274
+        -- filter remote occupants
275
+        if jid.host(o.bare_jid) == main_domain then
276
+            room:route_to_occupant(o, stanza)
277
+        end
278
+    end
279
+
280
+    -- send to main participants only messages from local occupants (skip from remote vnodes)
281
+    if occupant and occupant_host ~= fmuc_main_domain then
282
+        local main_message = st.clone(stanza);
283
+        main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..fmuc_main_domain);
284
+        module:send(main_message);
285
+    end
286
+    stanza.attr.from = from; -- something prosody does internally
287
+
288
+    return true;
289
+end, 55); -- prosody check for visitor's chat is prio 50, we want to override it
290
+
291
+module:hook('muc-private-message', function(event)
292
+    -- private messaging is forbidden
293
+    event.origin.send(st.error_reply(event.stanza, "auth", "forbidden",
294
+            "Private messaging is disabled on visitor nodes"));
295
+    return true;
296
+end, 10);

+ 6
- 0
resources/prosody-plugins/mod_muc_meeting_id.lua View File

@@ -99,3 +99,9 @@ function handle_jicofo_unlock(event)
99 99
 end
100 100
 
101 101
 module:hook('jicofo-unlock-room', handle_jicofo_unlock);
102
+
103
+-- make sure we remove nick if someone is sending it with a message to protect
104
+-- forgery of display name
105
+module:hook("muc-occupant-groupchat", function(event)
106
+    event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
107
+end, 45); -- prosody check is prio 50, we want to run after it

+ 49
- 3
resources/prosody-plugins/mod_visitors.lua View File

@@ -4,11 +4,9 @@
4 4
 --- vm1-ip-address conference.visitors1.domain.com
5 5
 --- vm2-ip-address visitors2.domain.com
6 6
 --- vm2-ip-address conference.visitors2.domain.com
7
---- TODO: drop the /etc/hosts changes for https://modules.prosody.im/mod_s2soutinjection.html
8 7
 --- Enable in global modules: 's2s_bidi' and 'certs_all'
9 8
 --- Make sure 's2s' is not in modules_disabled
10 9
 --- Open port 5269 on the provider side and on the firewall on the machine (iptables -I INPUT 4 -p tcp -m tcp --dport 5269 -j ACCEPT)
11
---- TODO: make it work with tenants
12 10
 --- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com)
13 11
 local st = require 'util.stanza';
14 12
 local jid = require 'util.jid';
@@ -127,7 +125,6 @@ module:hook('presence/full', function(event)
127 125
     local stanza = event.stanza;
128 126
     local room_name, from_host = jid.split(stanza.attr.from);
129 127
     if stanza.attr.type == 'unavailable' and from_host ~= main_muc_component_config then
130
-        -- TODO tenants???
131 128
         local room_jid = jid.join(room_name, main_muc_component_config); -- converts from visitor to main room jid
132 129
 
133 130
         local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
@@ -231,4 +228,53 @@ process_host_module(main_muc_component_config, function(host_module, host)
231 228
             module:send(fmuc_pr);
232 229
         end
233 230
     end);
231
+    -- forwards messages from main participants to vnodes
232
+    host_module:hook("muc-occupant-groupchat", function(event)
233
+        local room, stanza, occupant = event.room, event.stanza, event.occupant;
234
+
235
+        if not visitors_nodes[room.jid] then
236
+            return;
237
+        end
238
+
239
+        local vnodes = visitors_nodes[room.jid].nodes;
240
+        local user = jid.node(occupant.nick);
241
+        -- a main participant we need to update all active visitor nodes
242
+        for k in pairs(vnodes) do
243
+            local fmuc_msg = st.clone(stanza);
244
+            fmuc_msg.attr.to = jid.join(user, k);
245
+            fmuc_msg.attr.from = occupant.jid;
246
+            module:send(fmuc_msg);
247
+        end
248
+    end);
249
+    -- receiving messages from visitor nodes and forward them to local main participants
250
+    -- and forward them to the rest of visitor nodes
251
+    host_module:hook("muc-occupant-groupchat", function(event)
252
+        local occupant, room, stanza = event.occupant, event.room, event.stanza;
253
+        local to = stanza.attr.to;
254
+        local from = stanza.attr.from;
255
+        local from_vnode = jid.host(from);
256
+
257
+        if occupant or not (visitors_nodes[to] or visitors_nodes[to].nodes[from_vnode]) then
258
+            return;
259
+        end
260
+
261
+        -- a message from visitor occupant of known visitor node
262
+        stanza.attr.from = to;
263
+        for _, o in room:each_occupant() do
264
+            -- send it to the nick to be able to route it to the room (ljm multiple rooms) from unknown occupant
265
+            room:route_to_occupant(o, stanza);
266
+        end
267
+
268
+        -- now we need to send to rest of visitor nodes
269
+        local vnodes = visitors_nodes[room.jid].nodes;
270
+        for k in pairs(vnodes) do
271
+            if k ~= from_vnode then
272
+                local st_copy = st.clone(stanza);
273
+                st_copy.attr.to = jid.join(jid.node(room.jid), k);
274
+                module:send(st_copy);
275
+            end
276
+        end
277
+
278
+        return true;
279
+    end, 55); -- prosody check for unknown participant chat is prio 50, we want to override it
234 280
 end);

Loading…
Cancel
Save