Skip to content

Commit e933df5

Browse files
committed
feat: Adds implementation for AV moderation.
1 parent 3e69acc commit e933df5

File tree

4 files changed

+276
-8
lines changed

4 files changed

+276
-8
lines changed

src/main/java/org/jitsi/jigasi/AudioModeration.java

Lines changed: 227 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@
2222
import net.java.sip.communicator.util.*;
2323
import org.jitsi.jigasi.sip.*;
2424
import org.jitsi.jigasi.util.*;
25+
import org.jitsi.xmpp.extensions.*;
2526
import org.jitsi.xmpp.extensions.jitsimeet.*;
2627
import org.jivesoftware.smack.*;
28+
import org.jivesoftware.smack.filter.*;
2729
import org.jivesoftware.smack.iqrequest.*;
2830
import org.jivesoftware.smack.packet.*;
31+
import org.jivesoftware.smackx.disco.*;
32+
import org.jivesoftware.smackx.disco.packet.*;
33+
import org.json.simple.parser.*;
2934
import org.jxmpp.jid.*;
3035

3136
import org.json.simple.*;
@@ -78,18 +83,45 @@ public class AudioModeration
7883
{
7984
initialAudioMutedExtension.setAudioMuted(false);
8085
}
81-
8286
/**
8387
* The call context used to create this conference, contains info as
8488
* room name and room password and other optional parameters.
8589
*/
8690
private final CallContext callContext;
8791

92+
/**
93+
* We keep track of the AV moderation component to be able to trust the incoming messages.
94+
*/
95+
private String avModerationAddress = null;
96+
97+
/**
98+
* Whether AV moderation is currently enabled.
99+
*/
100+
private boolean avModerationEnabled = false;
101+
102+
/**
103+
* Whether current jigasi provider is allowed to unmute itself. By default, AV moderation is not enabled,
104+
* and we are allowed to unmute.
105+
*/
106+
private boolean isAllowedToUnmute = true;
107+
108+
/**
109+
* We use the same instance of extension so we can remove and add it from the default presence set of
110+
* extensions.
111+
*/
112+
private static final RaiseHandExtension lowerHandExtension = new RaiseHandExtension();
113+
114+
/**
115+
* The listener for incoming messages with AV moderation commands.
116+
*/
117+
private final AVModerationListener avModerationListener;
118+
88119
public AudioModeration(JvbConference jvbConference, SipGatewaySession gatewaySession, CallContext ctx)
89120
{
90121
this.gatewaySession = gatewaySession;
91122
this.jvbConference = jvbConference;
92123
this.callContext = ctx;
124+
this.avModerationListener = new AVModerationListener();
93125
}
94126

95127
/**
@@ -115,16 +147,21 @@ public void clean()
115147
{
116148
XMPPConnection connection = jvbConference.getConnection();
117149

150+
if (connection == null)
151+
{
152+
// if there is no connection nothing to clear
153+
return;
154+
}
155+
118156
if (muteIqHandler != null)
119157
{
120158
// we need to remove it from the connection, or we break some Smack
121159
// weak references map where the key is connection and the value
122160
// holds a connection and we leak connection/conferences.
123-
if (connection != null)
124-
{
125-
connection.unregisterIQRequestHandler(muteIqHandler);
126-
}
161+
connection.unregisterIQRequestHandler(muteIqHandler);
127162
}
163+
164+
connection.removeAsyncStanzaListener(this.avModerationListener);
128165
}
129166

130167
/**
@@ -275,6 +312,12 @@ void setChatRoomAudioMuted(boolean muted)
275312
{
276313
// remove the initial extension otherwise it will overwrite our new setting
277314
((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(initialAudioMutedExtension);
315+
316+
if (!muted)
317+
{
318+
// if we are unmuting make sure our raise hand is always lowered
319+
((ChatRoomJabberImpl) mucRoom).addPresencePacketExtensions(lowerHandExtension);
320+
}
278321
}
279322

280323
AudioMutedExtension audioMutedExtension = new AudioMutedExtension();
@@ -300,6 +343,23 @@ public boolean requestAudioMuteByJicofo(boolean bMuted)
300343
{
301344
ChatRoom mucRoom = this.jvbConference.getJvbRoom();
302345

346+
if (!bMuted && this.avModerationEnabled && !isAllowedToUnmute)
347+
{
348+
OperationSetJitsiMeetTools jitsiMeetTools
349+
= this.jvbConference.getXmppProvider().getOperationSet(OperationSetJitsiMeetTools.class);
350+
351+
if (mucRoom instanceof ChatRoomJabberImpl)
352+
{
353+
// remove the default value which is lowering the hand
354+
((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(lowerHandExtension);
355+
}
356+
357+
// let's raise hand
358+
jitsiMeetTools.sendPresenceExtension(mucRoom, new RaiseHandExtension().setRaisedHandValue(true));
359+
360+
return false;
361+
}
362+
303363
StanzaCollector collector = null;
304364
try
305365
{
@@ -356,7 +416,8 @@ && isMutingSupported()
356416
&& sipCall != null
357417
&& sipCall.getCallState() == CallState.CALL_IN_PROGRESS)
358418
{
359-
if (this.requestAudioMuteByJicofo(startAudioMuted))
419+
// we do not want to process start muted if AV moderation is enabled, as we are already muted
420+
if (!this.avModerationEnabled && this.requestAudioMuteByJicofo(true))
360421
{
361422
mute();
362423
}
@@ -382,7 +443,9 @@ public void mute()
382443

383444
try
384445
{
385-
logger.info(this.callContext + " Sending mute request ");
446+
logger.info(this.callContext + " Sending mute request avModeration:" + this.avModerationEnabled
447+
+ " allowed to unmute:" + this.isAllowedToUnmute);
448+
386449
this.gatewaySession.sendJson(callPeer, SipInfoJsonProtocol.createSIPJSONAudioMuteRequest(true));
387450
}
388451
catch (Exception ex)
@@ -391,6 +454,56 @@ public void mute()
391454
}
392455
}
393456

457+
/**
458+
* The xmpp provider for JvbConference has registered after connecting.
459+
*/
460+
public void xmppProviderRegistered()
461+
{
462+
// we are here in the RegisterThread, and it is safe to query and wait
463+
// Uses disco info to discover the AV moderation address.
464+
if (this.callContext.getDomain() != null)
465+
{
466+
try
467+
{
468+
long startQuery = System.currentTimeMillis();
469+
DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(this.jvbConference.getConnection())
470+
.discoverInfo(JidCreate.domainBareFrom(this.callContext.getDomain()));
471+
472+
DiscoverInfo.Identity avIdentity =
473+
info.getIdentities().stream().
474+
filter(di -> di.getCategory().equals("component") && di.getType().equals("av_moderation"))
475+
.findFirst().orElse(null);
476+
477+
if (avIdentity != null)
478+
{
479+
this.avModerationAddress = avIdentity.getName();
480+
logger.info(String.format("%s Discovered %s for %oms.",
481+
this.callContext, this.avModerationAddress, System.currentTimeMillis() - startQuery));
482+
}
483+
}
484+
catch(Exception e)
485+
{
486+
logger.error("Error querying for av moderation address", e);
487+
}
488+
}
489+
490+
if (this.avModerationAddress != null)
491+
{
492+
try
493+
{
494+
this.jvbConference.getConnection().addAsyncStanzaListener(
495+
this.avModerationListener,
496+
new AndFilter(
497+
MessageTypeFilter.NORMAL,
498+
FromMatchesFilter.create(JidCreate.domainBareFrom(this.avModerationAddress))));
499+
}
500+
catch(Exception e)
501+
{
502+
logger.error("Error adding AV moderation listener", e);
503+
}
504+
}
505+
}
506+
394507
/**
395508
* Handles mute requests received by jicofo if enabled.
396509
*/
@@ -442,4 +555,111 @@ private IQ handleMuteIq(MuteIq muteIq)
442555
return IQ.createResultIQ(muteIq);
443556
}
444557
}
558+
559+
/**
560+
* Added to presence to raise hand.
561+
*/
562+
private static class RaiseHandExtension
563+
extends AbstractPacketExtension
564+
{
565+
/**
566+
* The namespace of this packet extension.
567+
*/
568+
public static final String NAMESPACE = "jabber:client";
569+
570+
/**
571+
* XML element name of this packet extension.
572+
*/
573+
public static final String ELEMENT_NAME = "jitsi_participant_raisedHand";
574+
575+
/**
576+
* Creates a {@link org.jitsi.xmpp.extensions.jitsimeet.TranslationLanguageExtension} instance.
577+
*/
578+
public RaiseHandExtension()
579+
{
580+
super(NAMESPACE, ELEMENT_NAME);
581+
}
582+
583+
/**
584+
* Sets user's audio muted status.
585+
*
586+
* @param value <tt>true</tt> or <tt>false</tt> which indicates audio
587+
* muted status of the user.
588+
*/
589+
public ExtensionElement setRaisedHandValue(Boolean value)
590+
{
591+
setText(value ? value.toString() : null);
592+
593+
return this;
594+
}
595+
}
596+
597+
/**
598+
* Listens for incoming messages with AV moderation commands.
599+
*/
600+
private class AVModerationListener
601+
implements StanzaListener
602+
{
603+
@Override
604+
public void processStanza(Stanza packet)
605+
{
606+
JsonMessageExtension jsonMsg = packet.getExtension(
607+
JsonMessageExtension.ELEMENT_NAME, JsonMessageExtension.NAMESPACE);
608+
609+
if (jsonMsg == null)
610+
{
611+
return;
612+
}
613+
614+
try
615+
{
616+
Object o = new JSONParser().parse(jsonMsg.getJson());
617+
618+
if (o instanceof JSONObject)
619+
{
620+
JSONObject data = (JSONObject) o;
621+
622+
if (data.get("type").equals("av_moderation"))
623+
{
624+
Object enabledObj = data.get("enabled");
625+
Object approvedObj = data.get("approved");
626+
Object removedObj = data.get("removed");
627+
Object mediaTypeObj = data.get("mediaType");
628+
629+
// we are interested only in audio moderation
630+
if (mediaTypeObj == null || !mediaTypeObj.equals("audio"))
631+
{
632+
return;
633+
}
634+
635+
if (enabledObj != null)
636+
{
637+
avModerationEnabled = (Boolean) enabledObj;
638+
logger.info(callContext + " AV moderation has been enabled:" + avModerationEnabled);
639+
640+
// we will receive separate message when we are allowed to unmute
641+
isAllowedToUnmute = false;
642+
643+
gatewaySession.sendJson(
644+
SipInfoJsonProtocol.createAVModerationEnabledNotification(avModerationEnabled));
645+
}
646+
else if (removedObj != null && (Boolean) removedObj)
647+
{
648+
isAllowedToUnmute = false;
649+
gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationDeniedNotification());
650+
}
651+
else if (approvedObj != null && (Boolean) approvedObj)
652+
{
653+
isAllowedToUnmute = true;
654+
gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationApprovedNotification());
655+
}
656+
}
657+
}
658+
}
659+
catch(Exception e)
660+
{
661+
logger.error(callContext + " Error parsing", e);
662+
}
663+
}
664+
}
445665
}

src/main/java/org/jitsi/jigasi/JvbConference.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,8 @@ public synchronized void registrationStateChanged(
648648
&& mucRoom == null
649649
&& evt.getNewState() == RegistrationState.REGISTERED)
650650
{
651+
this.getAudioModeration().xmppProviderRegistered();
652+
651653
// Join the MUC
652654
joinConferenceRoom();
653655

src/main/java/org/jitsi/jigasi/SipGatewaySession.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ public void sendJson(CallPeer callPeer, JSONObject jsonObject)
11341134
* @param jsonObject JSONObject to be sent.
11351135
* @throws OperationFailedException failed sending the json.
11361136
*/
1137-
private void sendJson(JSONObject jsonObject)
1137+
public void sendJson(JSONObject jsonObject)
11381138
throws OperationFailedException
11391139
{
11401140
this.sendJson(sipCall.getCallPeers().next(), jsonObject);

src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public static class MESSAGE_TYPE
6161
public static final int LOBBY_LEFT = 5;
6262
public static final int LOBBY_ALLOWED_JOIN = 6;
6363
public static final int LOBBY_REJECTED_JOIN = 7;
64+
public static final int AV_MODERATION_ENABLED = 8;
65+
public static final int AV_MODERATION_APPROVED = 9;
66+
public static final int AV_MODERATION_DENIED = 10;
6467
}
6568

6669
private static class MESSAGE_HEADER
@@ -247,4 +250,47 @@ public static JSONObject createSIPJSONAudioMuteRequest(boolean muted)
247250

248251
return createSIPJSON("muteRequest", muteSettingsJson, null);
249252
}
253+
254+
/**
255+
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
256+
*
257+
* @return JSONObject representing a message to be sent over SIP.
258+
*/
259+
public static JSONObject createAVModerationEnabledNotification(boolean value)
260+
{
261+
JSONObject obj = new JSONObject();
262+
263+
obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_ENABLED);
264+
obj.put(MESSAGE_HEADER.MESSAGE_DATA, value);
265+
266+
return obj;
267+
}
268+
269+
/**
270+
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
271+
*
272+
* @return JSONObject representing a message to be sent over SIP.
273+
*/
274+
public static JSONObject createAVModerationApprovedNotification()
275+
{
276+
JSONObject obj = new JSONObject();
277+
278+
obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_APPROVED);
279+
280+
return obj;
281+
}
282+
283+
/**
284+
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
285+
*
286+
* @return JSONObject representing a message to be sent over SIP.
287+
*/
288+
public static JSONObject createAVModerationDeniedNotification()
289+
{
290+
JSONObject obj = new JSONObject();
291+
292+
obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_DENIED);
293+
294+
return obj;
295+
}
250296
}

0 commit comments

Comments
 (0)