Refactored the way that conference transfer between the parent room and breakout room is requested.
This commit is contained in:
parent
6bcfd75a7c
commit
2fb347bc25
@ -81,7 +81,7 @@ class BigBlueButtonInGW(
|
||||
def handleJsonMessage(json: String) {
|
||||
JsonMessageDecoder.decode(json) match {
|
||||
case Some(validMsg) => forwardMessage(validMsg)
|
||||
case None => log.error("Unhandled message: {}", json)
|
||||
case None => log.error("Unhandled json message: {}", json)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -580,7 +580,7 @@ class MessageSenderActor(val service: MessageSender)
|
||||
}
|
||||
|
||||
private def handleTransferUserToMeeting(msg: TransferUserToMeeting) {
|
||||
val m = new TransferUserToVoiceConfRequestMessage(msg.voiceConfId, msg.breakoutVoiceConfId, msg.userId, msg.forward);
|
||||
val m = new TransferUserToVoiceConfRequestMessage(msg.voiceConfId, msg.targetVoiceConfId, msg.userId);
|
||||
service.send(MessagingConstants.TO_VOICE_CONF_SYSTEM_CHAN, m.toJson())
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ object UserMessagesProtocol extends DefaultJsonProtocol {
|
||||
implicit val createBreakoutRoomsFormat = jsonFormat3(CreateBreakoutRooms)
|
||||
implicit val breakoutRoomsListMessageFormat = jsonFormat1(BreakoutRoomsListMessage)
|
||||
implicit val requestBreakoutJoinURLInMessageFormat = jsonFormat3(RequestBreakoutJoinURLInMessage)
|
||||
implicit val transferUserToMeetingRequestFormat = jsonFormat4(TransferUserToMeetingRequest)
|
||||
implicit val transferUserToMeetingRequestFormat = jsonFormat3(TransferUserToMeetingRequest)
|
||||
implicit val endBreakoutRoomsFormat = jsonFormat1(EndAllBreakoutRooms)
|
||||
implicit val inMsgHeaderFormat = jsonFormat1(InMessageHeader)
|
||||
implicit val outMsgHeaderFormat = jsonFormat1(OutMsgHeader)
|
||||
|
@ -60,7 +60,7 @@ case class EndAllBreakoutRooms(meetingId: String) extends InMessage
|
||||
// Sent by breakout actor to tell meeting actor that breakout room has been ended
|
||||
case class BreakoutRoomEnded(meetingId: String, breakoutRoomId: String) extends InMessage
|
||||
// Sent by user actor to ask for voice conference transfer
|
||||
case class TransferUserToMeetingRequest(meetingId: String, breakoutId: String, userId: String, listen: Boolean) extends InMessage
|
||||
case class TransferUserToMeetingRequest(meetingId: String, targetMeetingId: String, userId: String) extends InMessage
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// Lock
|
||||
|
@ -69,7 +69,7 @@ case class MuteVoiceUser(meetingID: String, recorded: Boolean, requesterID: Stri
|
||||
case class UserVoiceMuted(meetingID: String, recorded: Boolean, confNum: String, user: UserVO) extends IOutMessage
|
||||
case class UserVoiceTalking(meetingID: String, recorded: Boolean, confNum: String, user: UserVO) extends IOutMessage
|
||||
case class EjectVoiceUser(meetingID: String, recorded: Boolean, requesterID: String, userId: String, voiceConfId: String, voiceUserId: String) extends IOutMessage
|
||||
case class TransferUserToMeeting(voiceConfId: String, breakoutVoiceConfId: String, userId: String, forward: Boolean) extends IOutMessage
|
||||
case class TransferUserToMeeting(voiceConfId: String, targetVoiceConfId: String, userId: String) extends IOutMessage
|
||||
case class UserJoinedVoice(meetingID: String, recorded: Boolean, confNum: String, user: UserVO) extends IOutMessage
|
||||
case class UserLeftVoice(meetingID: String, recorded: Boolean, confNum: String, user: UserVO) extends IOutMessage
|
||||
|
||||
|
@ -28,12 +28,12 @@ trait BreakoutRoomApp extends SystemConfiguration {
|
||||
}
|
||||
presURL
|
||||
}
|
||||
|
||||
def handleBreakoutRoomsList(msg:BreakoutRoomsListMessage) {
|
||||
|
||||
def handleBreakoutRoomsList(msg: BreakoutRoomsListMessage) {
|
||||
val breakoutRooms = breakoutModel.getRooms().toVector map { r => new BreakoutRoomBody(r.name, r.id) }
|
||||
outGW.send(new BreakoutRoomsListOutMessage(mProps.meetingID, breakoutRooms));
|
||||
}
|
||||
|
||||
|
||||
def handleCreateBreakoutRooms(msg: CreateBreakoutRooms) {
|
||||
var i = 0
|
||||
for (room <- msg.rooms) {
|
||||
@ -80,7 +80,7 @@ trait BreakoutRoomApp extends SystemConfiguration {
|
||||
def sendBreakoutRoomStarted(meetingId: String, breakoutName: String, breakoutId: String, voiceConfId: String) {
|
||||
outGW.send(new BreakoutRoomStartedOutMessage(meetingId, mProps.recorded, new BreakoutRoomBody(breakoutName, breakoutId)))
|
||||
}
|
||||
|
||||
|
||||
def handleBreakoutRoomEnded(msg: BreakoutRoomEnded) {
|
||||
breakoutModel.remove(msg.breakoutRoomId)
|
||||
outGW.send(new BreakoutRoomEndedOutMessage(msg.meetingId, msg.breakoutRoomId))
|
||||
@ -99,6 +99,32 @@ trait BreakoutRoomApp extends SystemConfiguration {
|
||||
new BreakoutRoomUsersUpdate(mProps.externalMeetingID, mProps.meetingID, breakoutUsers)))
|
||||
}
|
||||
|
||||
def handleTransferUserToMeeting(msg: TransferUserToMeetingRequest) {
|
||||
var targetVoiceBridge: String = msg.targetMeetingId
|
||||
// If the current room is a parent room we fetch the voice bridge from the breakout room
|
||||
if (!mProps.isBreakout) {
|
||||
breakoutModel.getBreakoutRoom(msg.targetMeetingId) match {
|
||||
case Some(b) => {
|
||||
targetVoiceBridge = b.voiceConfId;
|
||||
}
|
||||
case None => // do nothing
|
||||
}
|
||||
} // if it is a breakout room, the target voice bridge is the same after removing the last digit
|
||||
else {
|
||||
targetVoiceBridge = mProps.voiceBridge.dropRight(1)
|
||||
}
|
||||
// We check the iser from the mode
|
||||
usersModel.getUser(msg.userId) match {
|
||||
case Some(u) => {
|
||||
if (u.voiceUser.joined) {
|
||||
log.info("Transferring user userId=" + u.userID + " from voiceBridge=" + mProps.voiceBridge + " to targetVoiceConf=" + targetVoiceBridge)
|
||||
outGW.send(new TransferUserToMeeting(mProps.voiceBridge, targetVoiceBridge, u.voiceUser.userId))
|
||||
}
|
||||
}
|
||||
case None => // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
def handleEndAllBreakoutRooms(msg: EndAllBreakoutRooms) {
|
||||
breakoutModel.getRooms().foreach { room =>
|
||||
outGW.send(new EndBreakoutRoom(room.id))
|
||||
|
@ -161,24 +161,6 @@ trait UsersApp {
|
||||
case None => // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
def handleTransferUserToMeeting(msg: TransferUserToMeetingRequest) {
|
||||
log.info("Received transfer user to meeting request. meetingId=" + mProps.meetingID + " breakoutId=" + msg.breakoutId + " userId=" + msg.userId + " toBreakout=" + msg.listen)
|
||||
breakoutModel.getBreakoutRoom(msg.breakoutId) match {
|
||||
case Some(b) => {
|
||||
usersModel.getUser(msg.userId) match {
|
||||
case Some(u) => {
|
||||
if (u.voiceUser.joined) {
|
||||
log.info("Transferring user between meetingId=" + mProps.meetingID + " userId=" + u.userID + " and breakoutId=" + b.id)
|
||||
outGW.send(new TransferUserToMeeting(mProps.voiceBridge, b.voiceConfId, u.voiceUser.userId, msg.listen))
|
||||
}
|
||||
}
|
||||
case None => // do nothing
|
||||
}
|
||||
}
|
||||
case None => // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
def handleGetLockSettings(msg: GetLockSettings) {
|
||||
//println("*************** Reply with current lock settings ********************")
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.bigbluebutton.freeswitch.pubsub.receivers;
|
||||
|
||||
|
||||
import org.bigbluebutton.common.messages.EjectAllUsersFromVoiceConfRequestMessage;
|
||||
import org.bigbluebutton.common.messages.EjectUserFromVoiceConfRequestMessage;
|
||||
import org.bigbluebutton.common.messages.GetUsersFromVoiceConfRequestMessage;
|
||||
@ -15,16 +14,18 @@ import com.google.gson.JsonParser;
|
||||
|
||||
public class RedisMessageReceiver {
|
||||
|
||||
public static final String TO_VOICE_CONF_CHANNEL = "bigbluebutton:to-voice-conf";
|
||||
public static final String TO_VOICE_CONF_PATTERN = TO_VOICE_CONF_CHANNEL + ":*";
|
||||
public static final String TO_VOICE_CONF_SYSTEM_CHAN = TO_VOICE_CONF_CHANNEL + ":system";
|
||||
|
||||
public static final String TO_VOICE_CONF_CHANNEL = "bigbluebutton:to-voice-conf";
|
||||
public static final String TO_VOICE_CONF_PATTERN = TO_VOICE_CONF_CHANNEL
|
||||
+ ":*";
|
||||
public static final String TO_VOICE_CONF_SYSTEM_CHAN = TO_VOICE_CONF_CHANNEL
|
||||
+ ":system";
|
||||
|
||||
private final FreeswitchApplication fsApp;
|
||||
|
||||
|
||||
public RedisMessageReceiver(FreeswitchApplication fsApp) {
|
||||
this.fsApp = fsApp;
|
||||
}
|
||||
|
||||
|
||||
public void handleMessage(String pattern, String channel, String message) {
|
||||
if (channel.equalsIgnoreCase(TO_VOICE_CONF_SYSTEM_CHAN)) {
|
||||
JsonParser parser = new JsonParser();
|
||||
@ -36,67 +37,73 @@ public class RedisMessageReceiver {
|
||||
if (header.has("name")) {
|
||||
String messageName = header.get("name").getAsString();
|
||||
switch (messageName) {
|
||||
case EjectAllUsersFromVoiceConfRequestMessage.EJECT_ALL_VOICE_USERS_REQUEST:
|
||||
processEjectAllVoiceUsersRequestMessage(message);
|
||||
break;
|
||||
case EjectUserFromVoiceConfRequestMessage.EJECT_VOICE_USER_REQUEST:
|
||||
processEjectVoiceUserRequestMessage(message);
|
||||
break;
|
||||
case GetUsersFromVoiceConfRequestMessage.GET_VOICE_USERS_REQUEST:
|
||||
processGetVoiceUsersRequestMessage(message);
|
||||
break;
|
||||
case MuteUserInVoiceConfRequestMessage.MUTE_VOICE_USER_REQUEST:
|
||||
processMuteVoiceUserRequestMessage(message);
|
||||
break;
|
||||
case TransferUserToVoiceConfRequestMessage.TRANSFER_USER_TO_VOICE_CONF_REQUEST:
|
||||
processTransferUserToVoiceConfRequestMessage(message);
|
||||
break;
|
||||
case StartRecordingVoiceConfRequestMessage.START_RECORD_VOICE_CONF_REQUEST:
|
||||
processStartRecordingVoiceConfRequestMessage(message);
|
||||
break;
|
||||
case StopRecordingVoiceConfRequestMessage.STOP_RECORD_VOICE_CONF_REQUEST:
|
||||
processStopRecordingVoiceConfRequestMessage(message);
|
||||
break;
|
||||
case EjectAllUsersFromVoiceConfRequestMessage.EJECT_ALL_VOICE_USERS_REQUEST:
|
||||
processEjectAllVoiceUsersRequestMessage(message);
|
||||
break;
|
||||
case EjectUserFromVoiceConfRequestMessage.EJECT_VOICE_USER_REQUEST:
|
||||
processEjectVoiceUserRequestMessage(message);
|
||||
break;
|
||||
case GetUsersFromVoiceConfRequestMessage.GET_VOICE_USERS_REQUEST:
|
||||
processGetVoiceUsersRequestMessage(message);
|
||||
break;
|
||||
case MuteUserInVoiceConfRequestMessage.MUTE_VOICE_USER_REQUEST:
|
||||
processMuteVoiceUserRequestMessage(message);
|
||||
break;
|
||||
case TransferUserToVoiceConfRequestMessage.TRANSFER_USER_TO_VOICE_CONF_REQUEST:
|
||||
processTransferUserToVoiceConfRequestMessage(message);
|
||||
break;
|
||||
case StartRecordingVoiceConfRequestMessage.START_RECORD_VOICE_CONF_REQUEST:
|
||||
processStartRecordingVoiceConfRequestMessage(message);
|
||||
break;
|
||||
case StopRecordingVoiceConfRequestMessage.STOP_RECORD_VOICE_CONF_REQUEST:
|
||||
processStopRecordingVoiceConfRequestMessage(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processEjectAllVoiceUsersRequestMessage(String json) {
|
||||
EjectAllUsersFromVoiceConfRequestMessage msg = EjectAllUsersFromVoiceConfRequestMessage.fromJson(json);
|
||||
EjectAllUsersFromVoiceConfRequestMessage msg = EjectAllUsersFromVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.ejectAll(msg.voiceConfId);
|
||||
}
|
||||
|
||||
|
||||
private void processEjectVoiceUserRequestMessage(String json) {
|
||||
EjectUserFromVoiceConfRequestMessage msg = EjectUserFromVoiceConfRequestMessage.fromJson(json);
|
||||
EjectUserFromVoiceConfRequestMessage msg = EjectUserFromVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.eject(msg.voiceConfId, msg.voiceUserId);
|
||||
}
|
||||
|
||||
|
||||
private void processGetVoiceUsersRequestMessage(String json) {
|
||||
GetUsersFromVoiceConfRequestMessage msg = GetUsersFromVoiceConfRequestMessage.fromJson(json);
|
||||
GetUsersFromVoiceConfRequestMessage msg = GetUsersFromVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.getAllUsers(msg.voiceConfId);
|
||||
}
|
||||
|
||||
|
||||
private void processMuteVoiceUserRequestMessage(String json) {
|
||||
MuteUserInVoiceConfRequestMessage msg = MuteUserInVoiceConfRequestMessage.fromJson(json);
|
||||
MuteUserInVoiceConfRequestMessage msg = MuteUserInVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.muteUser(msg.voiceConfId, msg.voiceUserId, msg.mute);
|
||||
}
|
||||
|
||||
|
||||
private void processTransferUserToVoiceConfRequestMessage(String json) {
|
||||
TransferUserToVoiceConfRequestMessage msg = TransferUserToVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.transferUserToMeeting(msg.voiceConfId, msg.targetVoiceConfId,
|
||||
msg.voiceUserId, msg.forward);
|
||||
msg.voiceUserId);
|
||||
}
|
||||
|
||||
|
||||
private void processStartRecordingVoiceConfRequestMessage(String json) {
|
||||
StartRecordingVoiceConfRequestMessage msg = StartRecordingVoiceConfRequestMessage.fromJson(json);
|
||||
StartRecordingVoiceConfRequestMessage msg = StartRecordingVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.startRecording(msg.voiceConfId, msg.meetingId);
|
||||
}
|
||||
|
||||
|
||||
private void processStopRecordingVoiceConfRequestMessage(String json) {
|
||||
StopRecordingVoiceConfRequestMessage msg = StopRecordingVoiceConfRequestMessage.fromJson(json);
|
||||
StopRecordingVoiceConfRequestMessage msg = StopRecordingVoiceConfRequestMessage
|
||||
.fromJson(json);
|
||||
fsApp.stopRecording(msg.voiceConfId, msg.meetingId, msg.recordStream);
|
||||
}
|
||||
}
|
||||
|
@ -74,9 +74,9 @@ public class FreeswitchApplication {
|
||||
}
|
||||
|
||||
public void transferUserToMeeting(String voiceConfId,
|
||||
String targetVoiceConfId, String voiceUserId, Boolean forward) {
|
||||
String targetVoiceConfId, String voiceUserId) {
|
||||
TransferUsetToMeetingCommand tutmc = new TransferUsetToMeetingCommand(
|
||||
voiceConfId, targetVoiceConfId, voiceUserId, forward, USER);
|
||||
voiceConfId, targetVoiceConfId, voiceUserId, USER);
|
||||
queueMessage(tutmc);
|
||||
}
|
||||
|
||||
|
@ -23,25 +23,17 @@ public class TransferUsetToMeetingCommand extends FreeswitchCommand {
|
||||
|
||||
private final String targetRoom;
|
||||
private final String participant;
|
||||
private final Boolean forward;
|
||||
|
||||
public TransferUsetToMeetingCommand(String room, String targetRoom,
|
||||
String participant, Boolean forward, String requesterId) {
|
||||
String participant, String requesterId) {
|
||||
super(room, requesterId);
|
||||
this.targetRoom = targetRoom;
|
||||
this.participant = participant;
|
||||
this.forward = forward;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandArgs() {
|
||||
String action = "";
|
||||
if (forward)
|
||||
action = room + SPACE + "transfer" + SPACE + targetRoom;
|
||||
else {
|
||||
action = targetRoom + SPACE + "transfer" + SPACE + room;
|
||||
}
|
||||
|
||||
return action + SPACE + participant;
|
||||
return room + SPACE + "transfer" + SPACE + targetRoom + SPACE
|
||||
+ participant;
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,5 @@ public class MuteUserInVoiceConfRequestMessage {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -30,19 +30,16 @@ public class TransferUserToVoiceConfRequestMessage {
|
||||
public static final String VOICE_CONF_ID = "voice_conf_id";
|
||||
public static final String TARGET_VOICE_CONF_ID = "target_voice_conf_id";
|
||||
public static final String VOICE_USER_ID = "voice_user_id";
|
||||
public static final String FORWARD = "forward";
|
||||
|
||||
public final String voiceConfId;
|
||||
public final String targetVoiceConfId;
|
||||
public final String voiceUserId;
|
||||
public final Boolean forward;
|
||||
|
||||
public TransferUserToVoiceConfRequestMessage(String voiceConfId,
|
||||
String breakoutVoiceConfId, String voiceUserId, Boolean toBreakout) {
|
||||
String breakoutVoiceConfId, String voiceUserId) {
|
||||
this.voiceConfId = voiceConfId;
|
||||
this.targetVoiceConfId = breakoutVoiceConfId;
|
||||
this.voiceUserId = voiceUserId;
|
||||
this.forward = toBreakout;
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
@ -50,7 +47,6 @@ public class TransferUserToVoiceConfRequestMessage {
|
||||
payload.put(VOICE_CONF_ID, voiceConfId);
|
||||
payload.put(TARGET_VOICE_CONF_ID, targetVoiceConfId);
|
||||
payload.put(VOICE_USER_ID, voiceUserId);
|
||||
payload.put(FORWARD, forward);
|
||||
|
||||
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(
|
||||
TRANSFER_USER_TO_VOICE_CONF_REQUEST, VERSION, null);
|
||||
@ -71,17 +67,14 @@ public class TransferUserToVoiceConfRequestMessage {
|
||||
if (TRANSFER_USER_TO_VOICE_CONF_REQUEST.equals(messageName)) {
|
||||
if (payload.has(VOICE_CONF_ID)
|
||||
&& payload.has(TARGET_VOICE_CONF_ID)
|
||||
&& payload.has(VOICE_USER_ID)
|
||||
&& payload.has(FORWARD)) {
|
||||
&& payload.has(VOICE_USER_ID)) {
|
||||
String id = payload.get(VOICE_CONF_ID).getAsString();
|
||||
String targetVoiceConfId = payload.get(
|
||||
TARGET_VOICE_CONF_ID).getAsString();
|
||||
String voiceUserId = payload.get(VOICE_USER_ID)
|
||||
.getAsString();
|
||||
Boolean forward = payload.get(FORWARD)
|
||||
.getAsBoolean();
|
||||
return new TransferUserToVoiceConfRequestMessage(id,
|
||||
targetVoiceConfId, voiceUserId, forward);
|
||||
targetVoiceConfId, voiceUserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,14 @@ package org.bigbluebutton.messages.payload;
|
||||
|
||||
public class ListenInOnBreakoutPayload {
|
||||
|
||||
public final String meetingId;
|
||||
public final String breakoutId;
|
||||
public final String userId;
|
||||
public final Boolean listen;
|
||||
|
||||
public ListenInOnBreakoutPayload(String meetingId, String breakoutId, String userId, Boolean listen) {
|
||||
this.meetingId = meetingId;
|
||||
this.breakoutId = breakoutId;
|
||||
this.userId = userId;
|
||||
this.listen = listen;
|
||||
}
|
||||
public final String meetingId;
|
||||
public final String targetMeetingId;
|
||||
public final String userId;
|
||||
|
||||
public ListenInOnBreakoutPayload(String meetingId, String breakoutId,
|
||||
String userId) {
|
||||
this.meetingId = meetingId;
|
||||
this.targetMeetingId = breakoutId;
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,11 @@ package org.bigbluebutton.main.model.users
|
||||
}
|
||||
|
||||
public function listenInOnBreakout(e:BreakoutRoomEvent):void {
|
||||
sender.listenInOnBreakout(_conferenceParameters.meetingID, e.breakoutId, _conferenceParameters.userid, e.listen);
|
||||
if (e.listen) {
|
||||
sender.listenInOnBreakout(_conferenceParameters.meetingID, e.breakoutId, _conferenceParameters.userid);
|
||||
} else {
|
||||
sender.listenInOnBreakout(e.breakoutId, _conferenceParameters.meetingID, _conferenceParameters.userid);
|
||||
}
|
||||
UserManager.getInstance().getConference().setBreakoutRoomInListen(e.listen, e.breakoutId);
|
||||
}
|
||||
|
||||
|
@ -126,7 +126,7 @@ package org.bigbluebutton.modules.users.services
|
||||
);
|
||||
}
|
||||
|
||||
public function listenInOnBreakout(meetingId:String, breakoutId:String, userId:String, listen:Boolean):void {
|
||||
public function listenInOnBreakout(meetingId:String, targetMeetingId:String, userId:String):void {
|
||||
var _nc:ConnectionManager = BBB.initConnectionManager();
|
||||
_nc.sendMessage("breakoutroom.listenInOnBreakout", function(result:String):void
|
||||
{
|
||||
@ -135,7 +135,7 @@ package org.bigbluebutton.modules.users.services
|
||||
{ // status - On error occurred
|
||||
LOGGER.error(status);
|
||||
},
|
||||
JSON.stringify({meetingId: meetingId, breakoutId: breakoutId, userId: userId, listen: listen})
|
||||
JSON.stringify({meetingId: meetingId, targetMeetingId: targetMeetingId, userId: userId})
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user