SEBSERV-204 fixed

This commit is contained in:
anhefti 2021-06-30 13:18:34 +02:00
parent 2ae1b928f9
commit 0638bcafd6
19 changed files with 417 additions and 66 deletions

View file

@ -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";

View file

@ -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(

View file

@ -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(

View file

@ -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<>();

View file

@ -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);
}
}

View file

@ -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;
} }

View file

@ -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()));

View file

@ -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);
} }

View file

@ -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();

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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 {

View file

@ -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: {}",

View file

@ -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,

View file

@ -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)

View file

@ -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;
} }
} }

View file

@ -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,

View file

@ -99,7 +99,7 @@
}) })
window.addEventListener('unload', () => { window.addEventListener('unload', () => {
ZoomMtg.leaveMeeting({}); ZoomMtg.endMeeting({});
}); });
</script> </script>
</body> </body>

View file

@ -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"