SEBSERV-204 fixed
This commit is contained in:
parent
2ae1b928f9
commit
0638bcafd6
19 changed files with 417 additions and 66 deletions
|
@ -182,6 +182,7 @@ public final class API {
|
||||||
public static final String EXAM_PROCTORING_COLLECTING_ROOMS_SEGMENT = "/collecting-rooms";
|
public static final String EXAM_PROCTORING_COLLECTING_ROOMS_SEGMENT = "/collecting-rooms";
|
||||||
public static final String EXAM_PROCTORING_OPEN_BREAK_OUT_ROOM_SEGMENT = "/open";
|
public static final String EXAM_PROCTORING_OPEN_BREAK_OUT_ROOM_SEGMENT = "/open";
|
||||||
public static final String EXAM_PROCTORING_CLOSE_ROOM_SEGMENT = "/close";
|
public static final String EXAM_PROCTORING_CLOSE_ROOM_SEGMENT = "/close";
|
||||||
|
public static final String EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT = "/notify-open-room";
|
||||||
public static final String EXAM_PROCTORING_RECONFIGURATION_ATTRIBUTES = "/reconfiguration-attributes";
|
public static final String EXAM_PROCTORING_RECONFIGURATION_ATTRIBUTES = "/reconfiguration-attributes";
|
||||||
public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections";
|
public static final String EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT = "/room-connections";
|
||||||
public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room";
|
public static final String EXAM_PROCTORING_ACTIVATE_TOWNHALL_ROOM = "activate-towhall-room";
|
||||||
|
|
|
@ -41,7 +41,9 @@ public class ProctoringServlet extends HttpServlet {
|
||||||
|
|
||||||
private final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver;
|
private final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver;
|
||||||
|
|
||||||
public ProctoringServlet(final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver) {
|
public ProctoringServlet(
|
||||||
|
final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver) {
|
||||||
|
|
||||||
this.proctoringWindowScriptResolver = proctoringWindowScriptResolver;
|
this.proctoringWindowScriptResolver = proctoringWindowScriptResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +80,7 @@ public class ProctoringServlet extends HttpServlet {
|
||||||
} else {
|
} else {
|
||||||
resp.getOutputStream().println(script);
|
resp.getOutputStream().println(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAuthenticated(
|
private boolean isAuthenticated(
|
||||||
|
|
|
@ -392,14 +392,14 @@ public class MonitoringClientConnection implements TemplateComposer {
|
||||||
.publish();
|
.publish();
|
||||||
}
|
}
|
||||||
|
|
||||||
actionBuilder
|
// actionBuilder
|
||||||
.newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_EXAM_ROOM_PROCTORING)
|
// .newAction(ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION_EXAM_ROOM_PROCTORING)
|
||||||
.withEntityKey(parentEntityKey)
|
// .withEntityKey(parentEntityKey)
|
||||||
.withExec(action -> this.monitoringProctoringService.openExamCollectionProctorScreen(
|
// .withExec(action -> this.monitoringProctoringService.openExamCollectionProctorScreen(
|
||||||
action,
|
// action,
|
||||||
connectionData))
|
// connectionData))
|
||||||
.noEventPropagation()
|
// .noEventPropagation()
|
||||||
.publish();
|
// .publish();
|
||||||
|
|
||||||
clientConnectionDetails.setStatusChangeListener(ccd -> {
|
clientConnectionDetails.setStatusChangeListener(ccd -> {
|
||||||
this.pageService.firePageEvent(
|
this.pageService.firePageEvent(
|
||||||
|
|
|
@ -171,8 +171,8 @@ public class PageServiceImpl implements PageService {
|
||||||
return FormTooltipMode.INPUT;
|
return FormTooltipMode.INPUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
public <T extends PageEvent> void firePageEvent(final T event, final PageContext pageContext) {
|
public <T extends PageEvent> void firePageEvent(final T event, final PageContext pageContext) {
|
||||||
final Class<? extends PageEvent> typeClass = event.getClass();
|
final Class<? extends PageEvent> typeClass = event.getClass();
|
||||||
final List<PageEventListener<T>> listeners = new ArrayList<>();
|
final List<PageEventListener<T>> listeners = new ArrayList<>();
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Component
|
||||||
|
@GuiProfile
|
||||||
|
public class NotifyProctoringRoomOpened extends RestCall<Void> {
|
||||||
|
|
||||||
|
public NotifyProctoringRoomOpened() {
|
||||||
|
super(new TypeKey<>(
|
||||||
|
CallType.UNDEFINED,
|
||||||
|
EntityType.EXAM_PROCTOR_DATA,
|
||||||
|
new TypeReference<Void>() {
|
||||||
|
}),
|
||||||
|
HttpMethod.POST,
|
||||||
|
MediaType.APPLICATION_FORM_URLENCODED,
|
||||||
|
API.EXAM_PROCTORING_ENDPOINT
|
||||||
|
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||||
|
+ API.EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctorin
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable;
|
||||||
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.NotifyProctoringRoomOpened;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
@Component
|
@Component
|
||||||
|
@ -489,6 +490,12 @@ public class MonitoringProctoringService {
|
||||||
.getProctoringGUIService()
|
.getProctoringGUIService()
|
||||||
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
|
.registerProctoringWindow(String.valueOf(room.examId), room.name, room.name);
|
||||||
|
|
||||||
|
this.pageService.getRestService().getBuilder(NotifyProctoringRoomOpened.class)
|
||||||
|
.withURIVariable(API.PARAM_MODEL_ID, String.valueOf(proctoringSettings.examId))
|
||||||
|
.withQueryParam(ProctoringRoomConnection.ATTR_ROOM_NAME, room.name)
|
||||||
|
.call()
|
||||||
|
.onError(error -> log.error("Failed to notify proctoring room opened: ", error));
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@ -202,16 +203,13 @@ public class ProctoringGUIService {
|
||||||
roomData.roomName,
|
roomData.roomName,
|
||||||
error.getMessage()));
|
error.getMessage()));
|
||||||
closeWindow(windowName);
|
closeWindow(windowName);
|
||||||
} else {
|
|
||||||
log.warn("No proctoring room window with name: {} found for closing.", windowName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
this.collectingRoomsActionState.clear();
|
this.collectingRoomsActionState.clear();
|
||||||
if (!this.openWindows.isEmpty()) {
|
if (!this.openWindows.isEmpty()) {
|
||||||
this.openWindows
|
new HashSet<>(this.openWindows.entrySet())
|
||||||
.entrySet()
|
|
||||||
.stream()
|
.stream()
|
||||||
.forEach(entry -> closeRoomWindow(entry.getKey()));
|
.forEach(entry -> closeRoomWindow(entry.getKey()));
|
||||||
|
|
||||||
|
|
|
@ -117,6 +117,13 @@ public interface RemoteProctoringRoomDAO {
|
||||||
* @param examId the exam identifier
|
* @param examId the exam identifier
|
||||||
* @param roomId the room record identifier (PK)
|
* @param roomId the room record identifier (PK)
|
||||||
* @return Result refer to the actual collecting room record or to an error when happened */
|
* @return Result refer to the actual collecting room record or to an error when happened */
|
||||||
Result<RemoteProctoringRoom> releasePlaceInCollectingRoom(final Long examId, Long roomId);
|
Result<RemoteProctoringRoom> releasePlaceInCollectingRoom(Long examId, Long roomId);
|
||||||
|
|
||||||
|
/** Get currently active break-out rooms for given connectionToken
|
||||||
|
*
|
||||||
|
* @param examId The exam identifier of the connection
|
||||||
|
* @param connectionTokens The connection token of the client connection
|
||||||
|
* @return Result refer to active break-out rooms or to an error when happened */
|
||||||
|
Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(String connectionToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,6 +268,8 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
||||||
final Optional<RemoteProctoringRoomRecord> room =
|
final Optional<RemoteProctoringRoomRecord> room =
|
||||||
this.remoteProctoringRoomRecordMapper.selectByExample()
|
this.remoteProctoringRoomRecordMapper.selectByExample()
|
||||||
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
|
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
|
||||||
|
.and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isEqualTo(0))
|
||||||
|
.and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isNull())
|
||||||
.build()
|
.build()
|
||||||
.execute()
|
.execute()
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -306,6 +308,20 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
||||||
.onError(TransactionHandler::rollback);
|
.onError(TransactionHandler::rollback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Result<Collection<RemoteProctoringRoom>> getBreakoutRooms(final String connectionToken) {
|
||||||
|
return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper
|
||||||
|
.selectByExample()
|
||||||
|
.where(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isEqualTo(0))
|
||||||
|
.and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isLike(connectionToken))
|
||||||
|
.build()
|
||||||
|
.execute()
|
||||||
|
.stream()
|
||||||
|
.map(this::toDomainModel)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
|
private RemoteProctoringRoom toDomainModel(final RemoteProctoringRoomRecord record) {
|
||||||
final String breakOutConnections = record.getBreakOutConnections();
|
final String breakOutConnections = record.getBreakOutConnections();
|
||||||
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)
|
final Collection<String> connections = StringUtils.isNotBlank(breakOutConnections)
|
||||||
|
@ -330,6 +346,8 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
|
||||||
|
|
||||||
final Long roomNumber = this.remoteProctoringRoomRecordMapper.countByExample()
|
final Long roomNumber = this.remoteProctoringRoomRecordMapper.countByExample()
|
||||||
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
|
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
|
||||||
|
.and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isEqualTo(0))
|
||||||
|
.and(RemoteProctoringRoomRecordDynamicSqlSupport.breakOutConnections, isNull())
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,10 @@ public interface ExamProctoringRoomService {
|
||||||
* @return Result refer to the given exam or to an error when happened */
|
* @return Result refer to the given exam or to an error when happened */
|
||||||
Result<Exam> disposeRoomsForExam(Exam exam);
|
Result<Exam> disposeRoomsForExam(Exam exam);
|
||||||
|
|
||||||
|
/** Indicates whether the town-hall for given exam is active or not
|
||||||
|
*
|
||||||
|
* @param examId the exam identifier
|
||||||
|
* @return true if the town-hall for given exam is currently actice */
|
||||||
boolean isTownhallRoomActive(final Long examId);
|
boolean isTownhallRoomActive(final Long examId);
|
||||||
|
|
||||||
/** This creates a town-hall room for a specific exam. The exam must be active and running
|
/** This creates a town-hall room for a specific exam. The exam must be active and running
|
||||||
|
@ -76,6 +80,10 @@ public interface ExamProctoringRoomService {
|
||||||
* @return Result refer to the RemoteProctoringRoom data or to an error when happened */
|
* @return Result refer to the RemoteProctoringRoom data or to an error when happened */
|
||||||
Result<RemoteProctoringRoom> getTownhallRoomData(final Long examId);
|
Result<RemoteProctoringRoom> getTownhallRoomData(final Long examId);
|
||||||
|
|
||||||
|
/** Used to close a active town-hall for given exam.
|
||||||
|
*
|
||||||
|
* @param examId The exam identifier
|
||||||
|
* @return Result refer to the room key of the closed town-hall or to an error when happened. */
|
||||||
Result<EntityKey> closeTownhallRoom(Long examId);
|
Result<EntityKey> closeTownhallRoom(Long examId);
|
||||||
|
|
||||||
/** Used to create a break out room for all active SEB clients given by the connectionTokens.
|
/** Used to create a break out room for all active SEB clients given by the connectionTokens.
|
||||||
|
@ -109,9 +117,16 @@ public interface ExamProctoringRoomService {
|
||||||
* @param roomName The room name
|
* @param roomName The room name
|
||||||
* @param attributes the reconfiguration attributes
|
* @param attributes the reconfiguration attributes
|
||||||
* @return Result refer to an empty value or to an error when happened */
|
* @return Result refer to an empty value or to an error when happened */
|
||||||
Result<Void> sendReconfigurationInstructions(
|
Result<Void> sendReconfigurationInstructions(Long examId, String roomName, Map<String, String> attributes);
|
||||||
final Long examId,
|
|
||||||
final String roomName,
|
/** Notifies that a specified proctoring room has been opened by a proctor.
|
||||||
final Map<String, String> attributes);
|
*
|
||||||
|
* This can be used to do instruct connection SEB clients of the room to do some initial actions,
|
||||||
|
* sending join instruction for the room to the SEB clients for example.
|
||||||
|
*
|
||||||
|
* @param examId The exam identifier of the proctoring room
|
||||||
|
* @param roomName The name of the proctoring room
|
||||||
|
* @return Result refer to void or to an error when happened */
|
||||||
|
Result<Void> notifyRoomOpened(Long examId, String roomName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,14 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom;
|
||||||
|
|
||||||
|
@ -43,32 +44,85 @@ public interface ExamProctoringService {
|
||||||
String roomName,
|
String roomName,
|
||||||
String subject);
|
String subject);
|
||||||
|
|
||||||
|
/** Get specified proctoring room connection data for a given connected SEB client
|
||||||
|
*
|
||||||
|
* @param proctoringSettings The proctoring service settings of the exam where the client belogs to
|
||||||
|
* @param connectionToken The connection token of the SEB client connection (identification)
|
||||||
|
* @param roomName The name of the room to connect to
|
||||||
|
* @param subject The room subject (display name of the room when the client enter the room)
|
||||||
|
* @return Result refer to the proctoring room connection data or to an error when happened */
|
||||||
Result<ProctoringRoomConnection> getClientRoomConnection(
|
Result<ProctoringRoomConnection> getClientRoomConnection(
|
||||||
ProctoringServiceSettings proctoringSettings,
|
ProctoringServiceSettings proctoringSettings,
|
||||||
String connectionToken,
|
String connectionToken,
|
||||||
String roomName,
|
String roomName,
|
||||||
String subject);
|
String subject);
|
||||||
|
|
||||||
|
/** Used to create join-instruction attribute for joining a given room
|
||||||
|
* This attributes are added to the join-instruction that is sent to the SEB client
|
||||||
|
*
|
||||||
|
* @param proctoringConnection the proctoring room connection data of the room to join
|
||||||
|
* @return Map containing additional join-instruction attributes that are added to the join-instruction */
|
||||||
Map<String, String> createJoinInstructionAttributes(ProctoringRoomConnection proctoringConnection);
|
Map<String, String> createJoinInstructionAttributes(ProctoringRoomConnection proctoringConnection);
|
||||||
|
|
||||||
|
/** Dispose or delete all rooms or meetings or other data on the proctoring service side for
|
||||||
|
* a given exam.
|
||||||
|
*
|
||||||
|
* @param examId The exam identifier
|
||||||
|
* @param proctoringSettings The proctoring service settings
|
||||||
|
* @return Result that is empty or refer to an error if happened */
|
||||||
Result<Void> disposeServiceRoomsForExam(Long examId, ProctoringServiceSettings proctoringSettings);
|
Result<Void> disposeServiceRoomsForExam(Long examId, ProctoringServiceSettings proctoringSettings);
|
||||||
|
|
||||||
default String verifyRoomName(final String requestedRoomName, final String connectionToken) {
|
/** Creates a new collecting room on proctoring service side.
|
||||||
if (StringUtils.isNotBlank(requestedRoomName)) {
|
*
|
||||||
return requestedRoomName;
|
* @param proctoringSettings The proctoring service settings to connect to the service
|
||||||
}
|
* @param roomNumber the collecting room number
|
||||||
|
* @return Result refer to the new room or to an error when happened */
|
||||||
throw new RuntimeException("Test Why: " + connectionToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<NewRoom> newCollectingRoom(ProctoringServiceSettings proctoringSettings, Long roomNumber);
|
Result<NewRoom> newCollectingRoom(ProctoringServiceSettings proctoringSettings, Long roomNumber);
|
||||||
|
|
||||||
|
/** Create a new break-out room on service side.
|
||||||
|
*
|
||||||
|
* @param proctoringSettings The proctoring service settings to connect to the service
|
||||||
|
* @param subject The subject of the new break-out room
|
||||||
|
* @return Result refer to the new room or to an error when happened */
|
||||||
Result<NewRoom> newBreakOutRoom(ProctoringServiceSettings proctoringSettings, String subject);
|
Result<NewRoom> newBreakOutRoom(ProctoringServiceSettings proctoringSettings, String subject);
|
||||||
|
|
||||||
|
/** Dispose or delete a given break-out room on proctoring service side.
|
||||||
|
*
|
||||||
|
* @param proctoringSettings The proctoring service settings to connect to the service
|
||||||
|
* @param roomName the room name
|
||||||
|
* @return Result refer to void or to an error when happened */
|
||||||
Result<Void> disposeBreakOutRoom(ProctoringServiceSettings proctoringSettings, String roomName);
|
Result<Void> disposeBreakOutRoom(ProctoringServiceSettings proctoringSettings, String roomName);
|
||||||
|
|
||||||
Map<String, String> getDefaultInstructionAttributes();
|
/** Used to get the default SEB client proctoring reconfiguration instruction attributes for the specified
|
||||||
|
* proctoring service.
|
||||||
|
*
|
||||||
|
* @return Map containing the default SEB client instruction attributes */
|
||||||
|
Map<String, String> getDefaultReconfigInstructionAttributes();
|
||||||
|
|
||||||
Map<String, String> getInstructionAttributes(Map<String, String> attributes);
|
/** Used to map SEB client proctoring reconfiguration instruction attributes from SEB Server API name
|
||||||
|
* to SEB key name.
|
||||||
|
*
|
||||||
|
* @param attributes Map containing the internal SEB Server API names of the attributes
|
||||||
|
* @return Map containing the external SEB settings attribute names */
|
||||||
|
Map<String, String> mapReconfigInstructionAttributes(Map<String, String> attributes);
|
||||||
|
|
||||||
|
/** Gets called when a proctor opened a break-out room.
|
||||||
|
* This can be used to do some post processing after a proctor opened a break-out room
|
||||||
|
*
|
||||||
|
* @param proctoringSettings The proctoring service settings to connect to the service
|
||||||
|
* @param room The room data of the break-out room
|
||||||
|
* @return Result refer to void or to an error when happened */
|
||||||
|
Result<Void> notifyBreakOutRoomOpened(ProctoringServiceSettings proctoringSettings, RemoteProctoringRoom room);
|
||||||
|
|
||||||
|
/** Gets called when a proctor opened a collecting room.
|
||||||
|
* This can be used to do some post processing after a proctor opened a collecting room
|
||||||
|
*
|
||||||
|
* @param proctoringSettings The proctoring service settings to connect to the service
|
||||||
|
* @param room The room data of the collecting room
|
||||||
|
* @return Result refer to void or to an error when happened */
|
||||||
|
Result<Void> notifyCollectingRoomOpened(
|
||||||
|
ProctoringServiceSettings proctoringSettings,
|
||||||
|
RemoteProctoringRoom room,
|
||||||
|
Collection<ClientConnection> clientConnections);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ class ExamSessionControlTask implements DisposableBean {
|
||||||
final String updateId = this.examUpdateHandler.createUpdateId();
|
final String updateId = this.examUpdateHandler.createUpdateId();
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("Run exam runtime update task with Id: {}", updateId);
|
log.debug("Run exam update task with Id: {}", updateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
controlExamStart(updateId);
|
controlExamStart(updateId);
|
||||||
|
@ -121,8 +121,8 @@ class ExamSessionControlTask implements DisposableBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void controlExamStart(final String updateId) {
|
private void controlExamStart(final String updateId) {
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.debug("Check starting exams: {}", updateId);
|
log.trace("Check starting exams: {}", updateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -145,8 +145,8 @@ class ExamSessionControlTask implements DisposableBean {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void controlExamEnd(final String updateId) {
|
private void controlExamEnd(final String updateId) {
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
log.debug("Check ending exams: {}", updateId);
|
log.trace("Check ending exams: {}", updateId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -280,6 +280,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
cc.getExamId(),
|
cc.getExamId(),
|
||||||
cc.getRemoteProctoringRoomId());
|
cc.getRemoteProctoringRoomId());
|
||||||
|
|
||||||
|
this.cleanupBreakOutRooms(cc);
|
||||||
|
|
||||||
this.clientConnectionDAO
|
this.clientConnectionDAO
|
||||||
.removeFromProctoringRoom(cc.getId(), cc.getConnectionToken())
|
.removeFromProctoringRoom(cc.getId(), cc.getConnectionToken())
|
||||||
.onError(error -> log.error("Failed to remove client connection form room: ", error))
|
.onError(error -> log.error("Failed to remove client connection form room: ", error))
|
||||||
|
@ -334,6 +336,37 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
return Result.ofRuntimeError("No active town-hall for exam: " + examId);
|
return Result.ofRuntimeError("No active town-hall for exam: " + examId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> notifyRoomOpened(final Long examId, final String roomName) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
final ProctoringServiceSettings proctoringSettings = this.examAdminService
|
||||||
|
.getProctoringServiceSettings(examId)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final ExamProctoringService examProctoringService = this.examAdminService
|
||||||
|
.getExamProctoringService(proctoringSettings.serverType)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final RemoteProctoringRoom room = this.remoteProctoringRoomDAO
|
||||||
|
.getRoom(examId, roomName)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
if (room.townhallRoom || !room.breakOutConnections.isEmpty()) {
|
||||||
|
examProctoringService
|
||||||
|
.notifyBreakOutRoomOpened(proctoringSettings, room)
|
||||||
|
.getOrThrow();
|
||||||
|
} else {
|
||||||
|
final Collection<ClientConnection> clientConnections = this.clientConnectionDAO
|
||||||
|
.getCollectingRoomConnections(examId, roomName)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
examProctoringService
|
||||||
|
.notifyCollectingRoomOpened(proctoringSettings, room, clientConnections)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void closeTownhall(
|
private void closeTownhall(
|
||||||
final Long examId,
|
final Long examId,
|
||||||
final ProctoringServiceSettings proctoringSettings,
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
@ -348,7 +381,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
this.sendReconfigurationInstructions(
|
this.sendReconfigurationInstructions(
|
||||||
examId,
|
examId,
|
||||||
connectionTokens,
|
connectionTokens,
|
||||||
examProctoringService.getDefaultInstructionAttributes());
|
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||||
|
|
||||||
// Close and delete town-hall room
|
// Close and delete town-hall room
|
||||||
this.remoteProctoringRoomDAO
|
this.remoteProctoringRoomDAO
|
||||||
|
@ -383,7 +416,35 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
this.sendReconfigurationInstructions(
|
this.sendReconfigurationInstructions(
|
||||||
examId,
|
examId,
|
||||||
connectionTokens,
|
connectionTokens,
|
||||||
examProctoringService.getDefaultInstructionAttributes());
|
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupBreakOutRooms(final ClientConnectionRecord cc) {
|
||||||
|
|
||||||
|
// check if there is a break-out room with matching single connection token
|
||||||
|
final Collection<RemoteProctoringRoom> roomsToCleanup = this.remoteProctoringRoomDAO
|
||||||
|
.getBreakoutRooms(cc.getConnectionToken())
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
roomsToCleanup.stream().forEach(room -> {
|
||||||
|
final ExamProctoringService examProctoringService = this.examAdminService
|
||||||
|
.getExamProctoringService(room.examId)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final ProctoringServiceSettings proctoringSettings = this.examAdminService
|
||||||
|
.getProctoringServiceSettings(room.examId)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
// Dispose the proctoring room on service side
|
||||||
|
examProctoringService
|
||||||
|
.disposeBreakOutRoom(proctoringSettings, room.name)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
// Delete room on persistent
|
||||||
|
this.remoteProctoringRoomDAO
|
||||||
|
.deleteRoom(room.id)
|
||||||
|
.getOrThrow();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeBreakOutRoom(
|
private void closeBreakOutRoom(
|
||||||
|
@ -396,7 +457,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
this.sendReconfigurationInstructions(
|
this.sendReconfigurationInstructions(
|
||||||
examId,
|
examId,
|
||||||
remoteProctoringRoom.breakOutConnections,
|
remoteProctoringRoom.breakOutConnections,
|
||||||
examProctoringService.getDefaultInstructionAttributes());
|
examProctoringService.getDefaultReconfigInstructionAttributes());
|
||||||
|
|
||||||
// Dispose the proctoring room on service side
|
// Dispose the proctoring room on service side
|
||||||
examProctoringService
|
examProctoringService
|
||||||
|
@ -458,7 +519,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
sendReconfigurationInstructions(
|
sendReconfigurationInstructions(
|
||||||
examId,
|
examId,
|
||||||
connectionTokens,
|
connectionTokens,
|
||||||
examProctoringService.getInstructionAttributes(attributes));
|
examProctoringService.mapReconfigInstructionAttributes(attributes));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,7 +584,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
.getClientRoomConnection(
|
.getClientRoomConnection(
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
connectionToken,
|
connectionToken,
|
||||||
examProctoringService.verifyRoomName(roomName, connectionToken),
|
roomName,
|
||||||
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
(StringUtils.isNotBlank(subject)) ? subject : roomName)
|
||||||
.onError(error -> log.error(
|
.onError(error -> log.error(
|
||||||
"Failed to get client room connection data for {} cause: {}",
|
"Failed to get client room connection data for {} cause: {}",
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Base64.Encoder;
|
import java.util.Base64.Encoder;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -50,8 +51,10 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
@ -207,12 +210,12 @@ public class JitsiProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getDefaultInstructionAttributes() {
|
public Map<String, String> getDefaultReconfigInstructionAttributes() {
|
||||||
return SEB_INSTRUCTION_DEFAULTS;
|
return SEB_INSTRUCTION_DEFAULTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getInstructionAttributes(final Map<String, String> attributes) {
|
public Map<String, String> mapReconfigInstructionAttributes(final Map<String, String> attributes) {
|
||||||
final Map<String, String> result = attributes
|
final Map<String, String> result = attributes
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
|
@ -310,6 +313,28 @@ public class JitsiProctoringService implements ExamProctoringService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> notifyBreakOutRoomOpened(
|
||||||
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
final RemoteProctoringRoom room) {
|
||||||
|
|
||||||
|
// Does nothing since the join instructions for break-out rooms has been sent by the overal service
|
||||||
|
|
||||||
|
return Result.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> notifyCollectingRoomOpened(
|
||||||
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
final RemoteProctoringRoom room,
|
||||||
|
final Collection<ClientConnection> clientConnections) {
|
||||||
|
|
||||||
|
// Does nothing at the moment
|
||||||
|
// TODO check if we need something similar for Jitsi as it is implemented for Zoom
|
||||||
|
// --> send join instructions to all involved client connections except them in one to one room.
|
||||||
|
return Result.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
protected Result<ProctoringRoomConnection> createProctoringConnection(
|
protected Result<ProctoringRoomConnection> createProctoringConnection(
|
||||||
final String connectionToken,
|
final String connectionToken,
|
||||||
final String url,
|
final String url,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Base64.Encoder;
|
import java.util.Base64.Encoder;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -36,6 +37,7 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestClientResponseException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
@ -56,8 +58,10 @@ import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||||
|
@ -68,6 +72,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.Authorization
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateMeetingRequest;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateMeetingRequest;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateUserRequest;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateUserRequest;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.MeetingResponse;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.MeetingResponse;
|
||||||
|
@ -99,7 +104,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ALLOW_CHAT))
|
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ALLOW_CHAT))
|
||||||
.stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)));
|
.stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)));
|
||||||
|
|
||||||
private static final Map<String, String> SEB_INSTRUCTION_DEFAULTS = Utils.immutableMapOf(Arrays.asList(
|
private static final Map<String, String> SEB_RECONFIG_INSTRUCTION_DEFAULTS = Utils.immutableMapOf(Arrays.asList(
|
||||||
new Tuple<>(
|
new Tuple<>(
|
||||||
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_RECEIVE_AUDIO,
|
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_RECEIVE_AUDIO,
|
||||||
Constants.FALSE_STRING),
|
Constants.FALSE_STRING),
|
||||||
|
@ -119,6 +124,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
private final ZoomRestTemplate zoomRestTemplate;
|
private final ZoomRestTemplate zoomRestTemplate;
|
||||||
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
||||||
private final AuthorizationService authorizationService;
|
private final AuthorizationService authorizationService;
|
||||||
|
private final SEBClientInstructionService sebInstructionService;
|
||||||
|
|
||||||
public ZoomProctoringService(
|
public ZoomProctoringService(
|
||||||
final ExamSessionService examSessionService,
|
final ExamSessionService examSessionService,
|
||||||
|
@ -127,7 +133,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
final AsyncService asyncService,
|
final AsyncService asyncService,
|
||||||
final JSONMapper jsonMapper,
|
final JSONMapper jsonMapper,
|
||||||
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
|
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
|
||||||
final AuthorizationService authorizationService) {
|
final AuthorizationService authorizationService,
|
||||||
|
final SEBClientInstructionService sebInstructionService) {
|
||||||
|
|
||||||
this.examSessionService = examSessionService;
|
this.examSessionService = examSessionService;
|
||||||
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
|
||||||
|
@ -137,6 +144,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
this.zoomRestTemplate = new ZoomRestTemplate(this);
|
this.zoomRestTemplate = new ZoomRestTemplate(this);
|
||||||
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
|
this.sebInstructionService = sebInstructionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -390,7 +398,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
this.deleteAdHocMeeting(
|
this.deleteAdHocMeeting(
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
credentials,
|
credentials,
|
||||||
roomName,
|
additionalZoomRoomData.meeting_id,
|
||||||
additionalZoomRoomData.user_id)
|
additionalZoomRoomData.user_id)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
|
@ -404,12 +412,12 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getDefaultInstructionAttributes() {
|
public Map<String, String> getDefaultReconfigInstructionAttributes() {
|
||||||
return SEB_INSTRUCTION_DEFAULTS;
|
return SEB_RECONFIG_INSTRUCTION_DEFAULTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getInstructionAttributes(final Map<String, String> attributes) {
|
public Map<String, String> mapReconfigInstructionAttributes(final Map<String, String> attributes) {
|
||||||
return attributes.entrySet().stream()
|
return attributes.entrySet().stream()
|
||||||
.map(entry -> new Tuple<>(
|
.map(entry -> new Tuple<>(
|
||||||
SEB_API_NAME_INSTRUCTION_NAME_MAPPING.getOrDefault(entry.getKey(), entry.getKey()),
|
SEB_API_NAME_INSTRUCTION_NAME_MAPPING.getOrDefault(entry.getKey(), entry.getKey()),
|
||||||
|
@ -417,6 +425,62 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
.collect(Collectors.toMap(Tuple::get_1, Tuple::get_2));
|
.collect(Collectors.toMap(Tuple::get_1, Tuple::get_2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> notifyBreakOutRoomOpened(
|
||||||
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
final RemoteProctoringRoom room) {
|
||||||
|
|
||||||
|
// Not needed for Zoom integration so far
|
||||||
|
|
||||||
|
return Result.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Void> notifyCollectingRoomOpened(
|
||||||
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
|
final RemoteProctoringRoom room,
|
||||||
|
final Collection<ClientConnection> clientConnections) {
|
||||||
|
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
if (this.remoteProctoringRoomDAO.isTownhallRoomActive(proctoringSettings.examId)) {
|
||||||
|
// do nothing is the town-hall of this exam is open. The clients will automatically join
|
||||||
|
// the meeting once the town-hall has been closed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ProctoringRoomConnection proctoringRoomConnection = this.getProctorRoomConnection(
|
||||||
|
proctoringSettings,
|
||||||
|
room.name,
|
||||||
|
room.subject)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
clientConnections.stream()
|
||||||
|
.forEach(cc -> sendJoinInstruction(
|
||||||
|
proctoringSettings.examId,
|
||||||
|
cc.connectionToken,
|
||||||
|
proctoringRoomConnection));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendJoinInstruction(
|
||||||
|
final Long examId,
|
||||||
|
final String connectionToken,
|
||||||
|
final ProctoringRoomConnection proctoringConnection) {
|
||||||
|
|
||||||
|
final Map<String, String> attributes = this
|
||||||
|
.createJoinInstructionAttributes(proctoringConnection);
|
||||||
|
|
||||||
|
this.sebInstructionService
|
||||||
|
.registerInstruction(
|
||||||
|
examId,
|
||||||
|
InstructionType.SEB_PROCTORING,
|
||||||
|
attributes,
|
||||||
|
connectionToken,
|
||||||
|
true)
|
||||||
|
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
|
||||||
|
}
|
||||||
|
|
||||||
private Result<NewRoom> createAdHocMeeting(
|
private Result<NewRoom> createAdHocMeeting(
|
||||||
final String roomName,
|
final String roomName,
|
||||||
final String subject,
|
final String subject,
|
||||||
|
@ -450,8 +514,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
createMeeting.getBody(),
|
createMeeting.getBody(),
|
||||||
MeetingResponse.class);
|
MeetingResponse.class);
|
||||||
|
|
||||||
// TODO start the meeting automatically ???
|
|
||||||
|
|
||||||
// Create NewRoom data with all needed information to store persistent
|
// Create NewRoom data with all needed information to store persistent
|
||||||
final AdditionalZoomRoomData additionalZoomRoomData = new AdditionalZoomRoomData(
|
final AdditionalZoomRoomData additionalZoomRoomData = new AdditionalZoomRoomData(
|
||||||
meetingResponse.id,
|
meetingResponse.id,
|
||||||
|
@ -472,7 +534,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
private Result<Void> deleteAdHocMeeting(
|
private Result<Void> deleteAdHocMeeting(
|
||||||
final ProctoringServiceSettings proctoringSettings,
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
final ClientCredentials credentials,
|
final ClientCredentials credentials,
|
||||||
final String meetingId,
|
final Long meetingId,
|
||||||
final String userId) {
|
final String userId) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
@ -565,6 +627,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
|
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
|
||||||
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
|
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
|
||||||
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
|
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
|
||||||
|
private static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
|
||||||
|
|
||||||
private final ZoomProctoringService zoomProctoringService;
|
private final ZoomProctoringService zoomProctoringService;
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
@ -671,7 +734,38 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
public ResponseEntity<String> deleteMeeting(
|
public ResponseEntity<String> deleteMeeting(
|
||||||
final String zoomServerUrl,
|
final String zoomServerUrl,
|
||||||
final ClientCredentials credentials,
|
final ClientCredentials credentials,
|
||||||
final String meetingId) {
|
final Long meetingId) {
|
||||||
|
|
||||||
|
// try to set set meeting status to ended first
|
||||||
|
try {
|
||||||
|
|
||||||
|
final String url = UriComponentsBuilder
|
||||||
|
.fromUriString(zoomServerUrl)
|
||||||
|
.path(API_END_MEETING_ENDPOINT)
|
||||||
|
.buildAndExpand(meetingId)
|
||||||
|
.toUriString();
|
||||||
|
|
||||||
|
final HttpHeaders headers = getHeaders(credentials);
|
||||||
|
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
|
||||||
|
final ResponseEntity<String> exchange = exchange(
|
||||||
|
url,
|
||||||
|
HttpMethod.PUT,
|
||||||
|
"{\"action\": \"end\"}",
|
||||||
|
headers);
|
||||||
|
|
||||||
|
if (log.isDebugEnabled() && exchange.getStatusCodeValue() != 204) {
|
||||||
|
log.debug("Failed to set meeting to end state. Meeting: {}, http: {}",
|
||||||
|
meetingId,
|
||||||
|
exchange.getStatusCodeValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.warn("Failed to end Zoom ad-hoc meeting: {} cause: {} / {}",
|
||||||
|
meetingId,
|
||||||
|
e.getMessage(),
|
||||||
|
(e.getCause() != null) ? e.getCause().getMessage() : Constants.EMPTY_NOTE);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
@ -684,7 +778,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
return exchange(url, HttpMethod.DELETE, credentials);
|
return exchange(url, HttpMethod.DELETE, credentials);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
|
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
|
||||||
meetingId,
|
meetingId,
|
||||||
e.getMessage(),
|
e.getMessage(),
|
||||||
(e.getCause() != null) ? e.getCause().getMessage() : Constants.EMPTY_NOTE);
|
(e.getCause() != null) ? e.getCause().getMessage() : Constants.EMPTY_NOTE);
|
||||||
|
@ -747,21 +841,27 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
? new HttpEntity<>(body, httpHeaders)
|
? new HttpEntity<>(body, httpHeaders)
|
||||||
: new HttpEntity<>(httpHeaders);
|
: new HttpEntity<>(httpHeaders);
|
||||||
|
|
||||||
final ResponseEntity<String> result = this.restTemplate.exchange(
|
try {
|
||||||
url, //"https://ethz.zoom.us/v2/users?Fstatus=active&page_size=30&page_number=1&data_type=Json",
|
final ResponseEntity<String> result = this.restTemplate.exchange(
|
||||||
method,
|
url,
|
||||||
httpEntity,
|
method,
|
||||||
String.class);
|
httpEntity,
|
||||||
|
String.class);
|
||||||
|
|
||||||
if (result.getStatusCode() != HttpStatus.OK && result.getStatusCode() != HttpStatus.CREATED) {
|
if (result.getStatusCode().value() >= 400) {
|
||||||
log.warn("Error response on Zoom API call to {} response status: {}", url, result.getStatusCode());
|
log.warn("Error response on Zoom API call to {} response status: {}", url,
|
||||||
|
result.getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (final RestClientResponseException rce) {
|
||||||
|
return ResponseEntity
|
||||||
|
.status(rce.getRawStatusCode())
|
||||||
|
.body(rce.getResponseBodyAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
});
|
});
|
||||||
return protectedRunResult.getOrThrow();
|
return protectedRunResult.getOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
|
|
@ -142,6 +142,7 @@ public interface ZoomRoomRequestResponse {
|
||||||
@JsonProperty final int jbh_time = 0;
|
@JsonProperty final int jbh_time = 0;
|
||||||
@JsonProperty final boolean use_pmi = false;
|
@JsonProperty final boolean use_pmi = false;
|
||||||
@JsonProperty final String audio = "voip";
|
@JsonProperty final String audio = "voip";
|
||||||
|
@JsonProperty final boolean waiting_room = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -119,6 +119,26 @@ public class ExamProctoringController {
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequestMapping(
|
||||||
|
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||||
|
+ API.EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT,
|
||||||
|
method = RequestMethod.POST,
|
||||||
|
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||||
|
public void notifyProctoringRoomOpened(
|
||||||
|
@RequestParam(
|
||||||
|
name = API.PARAM_INSTITUTION_ID,
|
||||||
|
required = true,
|
||||||
|
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
||||||
|
@PathVariable(name = API.PARAM_MODEL_ID) final Long examId,
|
||||||
|
@RequestParam(name = ProctoringRoomConnection.ATTR_ROOM_NAME, required = true) final String roomName) {
|
||||||
|
|
||||||
|
checkAccess(institutionId, examId);
|
||||||
|
this.examSessionService.getRunningExam(examId)
|
||||||
|
.flatMap(this.authorizationService::checkRead)
|
||||||
|
.flatMap(exam -> this.examProcotringRoomService.notifyRoomOpened(exam.id, roomName))
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
@RequestMapping(
|
@RequestMapping(
|
||||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||||
+ API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT,
|
+ API.EXAM_PROCTORING_ROOM_CONNECTIONS_PATH_SEGMENT,
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
window.addEventListener('unload', () => {
|
||||||
ZoomMtg.leaveMeeting({});
|
ZoomMtg.endMeeting({});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -166,7 +166,7 @@ public class ZoomWindowScriptResolverTest {
|
||||||
+ " })\r\n"
|
+ " })\r\n"
|
||||||
+ " \r\n"
|
+ " \r\n"
|
||||||
+ " window.addEventListener('unload', () => {\r\n"
|
+ " window.addEventListener('unload', () => {\r\n"
|
||||||
+ " ZoomMtg.leaveMeeting({});\r\n"
|
+ " ZoomMtg.endMeeting({});\r\n"
|
||||||
+ " });\r\n"
|
+ " });\r\n"
|
||||||
+ " </script>\r\n"
|
+ " </script>\r\n"
|
||||||
+ " </body>\r\n"
|
+ " </body>\r\n"
|
||||||
|
|
Loading…
Reference in a new issue