Skip to content

Commit

Permalink
feat: Adds implementation for AV moderation.
Browse files Browse the repository at this point in the history
  • Loading branch information
damencho committed Oct 1, 2021
1 parent 3e69acc commit e933df5
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 8 deletions.
234 changes: 227 additions & 7 deletions src/main/java/org/jitsi/jigasi/AudioModeration.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
import net.java.sip.communicator.util.*;
import org.jitsi.jigasi.sip.*;
import org.jitsi.jigasi.util.*;
import org.jitsi.xmpp.extensions.*;
import org.jitsi.xmpp.extensions.jitsimeet.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.iqrequest.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smackx.disco.*;
import org.jivesoftware.smackx.disco.packet.*;
import org.json.simple.parser.*;
import org.jxmpp.jid.*;

import org.json.simple.*;
Expand Down Expand Up @@ -78,18 +83,45 @@ public class AudioModeration
{
initialAudioMutedExtension.setAudioMuted(false);
}

/**
* The call context used to create this conference, contains info as
* room name and room password and other optional parameters.
*/
private final CallContext callContext;

/**
* We keep track of the AV moderation component to be able to trust the incoming messages.
*/
private String avModerationAddress = null;

/**
* Whether AV moderation is currently enabled.
*/
private boolean avModerationEnabled = false;

/**
* Whether current jigasi provider is allowed to unmute itself. By default, AV moderation is not enabled,
* and we are allowed to unmute.
*/
private boolean isAllowedToUnmute = true;

/**
* We use the same instance of extension so we can remove and add it from the default presence set of
* extensions.
*/
private static final RaiseHandExtension lowerHandExtension = new RaiseHandExtension();

/**
* The listener for incoming messages with AV moderation commands.
*/
private final AVModerationListener avModerationListener;

public AudioModeration(JvbConference jvbConference, SipGatewaySession gatewaySession, CallContext ctx)
{
this.gatewaySession = gatewaySession;
this.jvbConference = jvbConference;
this.callContext = ctx;
this.avModerationListener = new AVModerationListener();
}

/**
Expand All @@ -115,16 +147,21 @@ public void clean()
{
XMPPConnection connection = jvbConference.getConnection();

if (connection == null)
{
// if there is no connection nothing to clear
return;
}

if (muteIqHandler != null)
{
// we need to remove it from the connection, or we break some Smack
// weak references map where the key is connection and the value
// holds a connection and we leak connection/conferences.
if (connection != null)
{
connection.unregisterIQRequestHandler(muteIqHandler);
}
connection.unregisterIQRequestHandler(muteIqHandler);
}

connection.removeAsyncStanzaListener(this.avModerationListener);
}

/**
Expand Down Expand Up @@ -275,6 +312,12 @@ void setChatRoomAudioMuted(boolean muted)
{
// remove the initial extension otherwise it will overwrite our new setting
((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(initialAudioMutedExtension);

if (!muted)
{
// if we are unmuting make sure our raise hand is always lowered
((ChatRoomJabberImpl) mucRoom).addPresencePacketExtensions(lowerHandExtension);
}
}

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

if (!bMuted && this.avModerationEnabled && !isAllowedToUnmute)
{
OperationSetJitsiMeetTools jitsiMeetTools
= this.jvbConference.getXmppProvider().getOperationSet(OperationSetJitsiMeetTools.class);

if (mucRoom instanceof ChatRoomJabberImpl)
{
// remove the default value which is lowering the hand
((ChatRoomJabberImpl) mucRoom).removePresencePacketExtensions(lowerHandExtension);
}

// let's raise hand
jitsiMeetTools.sendPresenceExtension(mucRoom, new RaiseHandExtension().setRaisedHandValue(true));

return false;
}

StanzaCollector collector = null;
try
{
Expand Down Expand Up @@ -356,7 +416,8 @@ && isMutingSupported()
&& sipCall != null
&& sipCall.getCallState() == CallState.CALL_IN_PROGRESS)
{
if (this.requestAudioMuteByJicofo(startAudioMuted))
// we do not want to process start muted if AV moderation is enabled, as we are already muted
if (!this.avModerationEnabled && this.requestAudioMuteByJicofo(true))
{
mute();
}
Expand All @@ -382,7 +443,9 @@ public void mute()

try
{
logger.info(this.callContext + " Sending mute request ");
logger.info(this.callContext + " Sending mute request avModeration:" + this.avModerationEnabled
+ " allowed to unmute:" + this.isAllowedToUnmute);

this.gatewaySession.sendJson(callPeer, SipInfoJsonProtocol.createSIPJSONAudioMuteRequest(true));
}
catch (Exception ex)
Expand All @@ -391,6 +454,56 @@ public void mute()
}
}

/**
* The xmpp provider for JvbConference has registered after connecting.
*/
public void xmppProviderRegistered()
{
// we are here in the RegisterThread, and it is safe to query and wait
// Uses disco info to discover the AV moderation address.
if (this.callContext.getDomain() != null)
{
try
{
long startQuery = System.currentTimeMillis();
DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(this.jvbConference.getConnection())
.discoverInfo(JidCreate.domainBareFrom(this.callContext.getDomain()));

DiscoverInfo.Identity avIdentity =
info.getIdentities().stream().
filter(di -> di.getCategory().equals("component") && di.getType().equals("av_moderation"))
.findFirst().orElse(null);

if (avIdentity != null)
{
this.avModerationAddress = avIdentity.getName();
logger.info(String.format("%s Discovered %s for %oms.",
this.callContext, this.avModerationAddress, System.currentTimeMillis() - startQuery));
}
}
catch(Exception e)
{
logger.error("Error querying for av moderation address", e);
}
}

if (this.avModerationAddress != null)
{
try
{
this.jvbConference.getConnection().addAsyncStanzaListener(
this.avModerationListener,
new AndFilter(
MessageTypeFilter.NORMAL,
FromMatchesFilter.create(JidCreate.domainBareFrom(this.avModerationAddress))));
}
catch(Exception e)
{
logger.error("Error adding AV moderation listener", e);
}
}
}

/**
* Handles mute requests received by jicofo if enabled.
*/
Expand Down Expand Up @@ -442,4 +555,111 @@ private IQ handleMuteIq(MuteIq muteIq)
return IQ.createResultIQ(muteIq);
}
}

/**
* Added to presence to raise hand.
*/
private static class RaiseHandExtension
extends AbstractPacketExtension
{
/**
* The namespace of this packet extension.
*/
public static final String NAMESPACE = "jabber:client";

/**
* XML element name of this packet extension.
*/
public static final String ELEMENT_NAME = "jitsi_participant_raisedHand";

/**
* Creates a {@link org.jitsi.xmpp.extensions.jitsimeet.TranslationLanguageExtension} instance.
*/
public RaiseHandExtension()
{
super(NAMESPACE, ELEMENT_NAME);
}

/**
* Sets user's audio muted status.
*
* @param value <tt>true</tt> or <tt>false</tt> which indicates audio
* muted status of the user.
*/
public ExtensionElement setRaisedHandValue(Boolean value)
{
setText(value ? value.toString() : null);

return this;
}
}

/**
* Listens for incoming messages with AV moderation commands.
*/
private class AVModerationListener
implements StanzaListener
{
@Override
public void processStanza(Stanza packet)
{
JsonMessageExtension jsonMsg = packet.getExtension(
JsonMessageExtension.ELEMENT_NAME, JsonMessageExtension.NAMESPACE);

if (jsonMsg == null)
{
return;
}

try
{
Object o = new JSONParser().parse(jsonMsg.getJson());

if (o instanceof JSONObject)
{
JSONObject data = (JSONObject) o;

if (data.get("type").equals("av_moderation"))
{
Object enabledObj = data.get("enabled");
Object approvedObj = data.get("approved");
Object removedObj = data.get("removed");
Object mediaTypeObj = data.get("mediaType");

// we are interested only in audio moderation
if (mediaTypeObj == null || !mediaTypeObj.equals("audio"))
{
return;
}

if (enabledObj != null)
{
avModerationEnabled = (Boolean) enabledObj;
logger.info(callContext + " AV moderation has been enabled:" + avModerationEnabled);

// we will receive separate message when we are allowed to unmute
isAllowedToUnmute = false;

gatewaySession.sendJson(
SipInfoJsonProtocol.createAVModerationEnabledNotification(avModerationEnabled));
}
else if (removedObj != null && (Boolean) removedObj)
{
isAllowedToUnmute = false;
gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationDeniedNotification());
}
else if (approvedObj != null && (Boolean) approvedObj)
{
isAllowedToUnmute = true;
gatewaySession.sendJson(SipInfoJsonProtocol.createAVModerationApprovedNotification());
}
}
}
}
catch(Exception e)
{
logger.error(callContext + " Error parsing", e);
}
}
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/jitsi/jigasi/JvbConference.java
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@ public synchronized void registrationStateChanged(
&& mucRoom == null
&& evt.getNewState() == RegistrationState.REGISTERED)
{
this.getAudioModeration().xmppProviderRegistered();

// Join the MUC
joinConferenceRoom();

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jitsi/jigasi/SipGatewaySession.java
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ public void sendJson(CallPeer callPeer, JSONObject jsonObject)
* @param jsonObject JSONObject to be sent.
* @throws OperationFailedException failed sending the json.
*/
private void sendJson(JSONObject jsonObject)
public void sendJson(JSONObject jsonObject)
throws OperationFailedException
{
this.sendJson(sipCall.getCallPeers().next(), jsonObject);
Expand Down
46 changes: 46 additions & 0 deletions src/main/java/org/jitsi/jigasi/sip/SipInfoJsonProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public static class MESSAGE_TYPE
public static final int LOBBY_LEFT = 5;
public static final int LOBBY_ALLOWED_JOIN = 6;
public static final int LOBBY_REJECTED_JOIN = 7;
public static final int AV_MODERATION_ENABLED = 8;
public static final int AV_MODERATION_APPROVED = 9;
public static final int AV_MODERATION_DENIED = 10;
}

private static class MESSAGE_HEADER
Expand Down Expand Up @@ -247,4 +250,47 @@ public static JSONObject createSIPJSONAudioMuteRequest(boolean muted)

return createSIPJSON("muteRequest", muteSettingsJson, null);
}

/**
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
*
* @return JSONObject representing a message to be sent over SIP.
*/
public static JSONObject createAVModerationEnabledNotification(boolean value)
{
JSONObject obj = new JSONObject();

obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_ENABLED);
obj.put(MESSAGE_HEADER.MESSAGE_DATA, value);

return obj;
}

/**
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
*
* @return JSONObject representing a message to be sent over SIP.
*/
public static JSONObject createAVModerationApprovedNotification()
{
JSONObject obj = new JSONObject();

obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_APPROVED);

return obj;
}

/**
* Creates new JSONObject to notify that AV moderation is enabled/disabled.
*
* @return JSONObject representing a message to be sent over SIP.
*/
public static JSONObject createAVModerationDeniedNotification()
{
JSONObject obj = new JSONObject();

obj.put(MESSAGE_HEADER.MESSAGE_TYPE, MESSAGE_TYPE.AV_MODERATION_DENIED);

return obj;
}
}

0 comments on commit e933df5

Please sign in to comment.