diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java index 8a8ac806..24bfc16f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringRoomConnection.java @@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; @JsonIgnoreProperties(ignoreUnknown = true) @@ -66,6 +67,9 @@ public class ProctoringRoomConnection { @JsonProperty(ATTR_USER_NAME) public final String userName; + @JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA) + public final String additionalRoomData; + @JsonCreator public ProctoringRoomConnection( @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType, @@ -79,7 +83,8 @@ public class ProctoringRoomConnection { @JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey, @JsonProperty(ATTR_API_KEY) final CharSequence apiKey, @JsonProperty(ATTR_MEETING_ID) final String meetingId, - @JsonProperty(ATTR_USER_NAME) final String userName) { + @JsonProperty(ATTR_USER_NAME) final String userName, + @JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA) final String additionalRoomData) { this.proctoringServerType = proctoringServerType; this.connectionToken = connectionToken; @@ -93,6 +98,7 @@ public class ProctoringRoomConnection { this.apiKey = apiKey; this.meetingId = meetingId; this.userName = userName; + this.additionalRoomData = additionalRoomData; } public ProctoringServerType getProctoringServerType() { @@ -143,6 +149,10 @@ public class ProctoringRoomConnection { return this.meetingId; } + public String getAdditionalRoomData() { + return this.additionalRoomData; + } + @Override public String toString() { final StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java index 269a911d..aa61abd4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java @@ -282,7 +282,9 @@ public class MonitoringRunningExam implements TemplateComposer { return CONFIRM_CLOSE_TOWNHALL; } }) - .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService, + .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom( + proctoringGUIService, + proctoringSettings, action)) .noEventPropagation() .publish(); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java index 93f80704..26f5d832 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -119,6 +119,7 @@ public class MonitoringProctoringService { public PageAction toggleTownhallRoom( final ProctoringGUIService proctoringGUIService, + final ProctoringServiceSettings proctoringSettings, final PageAction action) { if (isTownhallRoomActive(action.getEntityKey().modelId)) { @@ -135,7 +136,7 @@ public class MonitoringProctoringService { } } else { - if (openTownhallRoom(proctoringGUIService, action)) { + if (openTownhallRoom(proctoringGUIService, proctoringSettings, action)) { this.pageService.firePageEvent( new ActionActivationEvent( true, @@ -245,7 +246,7 @@ public class MonitoringProctoringService { }); joinURL = roomData.get("start_url"); } catch (final Exception e) { - + log.error("Failed to get proctoring start URL: ", e); } final PageContext pc = pageContext.copy() .clearAttributes() @@ -282,17 +283,17 @@ public class MonitoringProctoringService { String.valueOf(proctoringSettings.examId), proctoringConnectionData); + final String zoomStartLink = extractZoomStartLink(room.additionalRoomData); if (proctoringSettings.useZoomAppClientForCollectingRoom && - StringUtils.isNotBlank(extractZoomStartLink(room))) { + StringUtils.isNotBlank(zoomStartLink)) { - final String startLink = extractZoomStartLink(room); final String script = String.format( getOpenRoomScriptTemplate(), room.name, 800, 1200, room.name, - startLink, + zoomStartLink, ""); RWT.getClient() @@ -326,24 +327,11 @@ public class MonitoringProctoringService { .call() .onError(error -> log.error("Failed to notify proctoring room opened: ", error)); } - } return action; } - private String extractZoomStartLink(final RemoteProctoringRoom room) { - try { - final Map data = - this.jsonMapper.readValue(room.additionalRoomData, new TypeReference>() { - }); - return data.get("start_url"); - } catch (final Exception e) { - log.error("Failed to extract Zoom start link: ", e); - return null; - } - } - public PageAction openOneToOneRoom( final PageAction action, final ClientConnectionData connectionData, @@ -394,11 +382,14 @@ public class MonitoringProctoringService { private boolean openTownhallRoom( final ProctoringGUIService proctoringGUIService, + final ProctoringServiceSettings proctoringSettings, final PageAction action) { try { final EntityKey examId = action.getEntityKey(); + final String endpoint = this.remoteProctoringEndpoint; + final String joinURL = this.guiServiceInfo.getExternalServerURIBuilder().toUriString(); if (proctoringGUIService.getTownhallWindowName(examId.modelId) == null) { final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService .openTownhallRoom( @@ -422,8 +413,8 @@ public class MonitoringProctoringService { 800, 1200, "Town-Hall", - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); + joinURL, + endpoint); javaScriptExecutor.execute(script); } catch (final Exception e) { @@ -520,4 +511,18 @@ public class MonitoringProctoringService { } } + private String extractZoomStartLink(final String additioalRoomAttributesJson) { + try { + final Map data = + this.jsonMapper.readValue( + additioalRoomAttributesJson, + new TypeReference>() { + }); + return data.get("start_url"); + } catch (final Exception e) { + log.error("Failed to extract Zoom start link: ", e); + return null; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java index 59330685..293c039e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/JitsiProctoringService.java @@ -379,7 +379,8 @@ public class JitsiProctoringService implements ExamProctoringService { null, null, null, - clientName); + clientName, + null); }); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index 0490056b..690f25d2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -75,6 +75,7 @@ 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.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.ApplyUserSettingsRequest; 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.MeetingResponse; @@ -293,7 +294,8 @@ public class ZoomProctoringService implements ExamProctoringService { credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), - this.authorizationService.getUserService().getCurrentUser().getUsername()); + this.authorizationService.getUserService().getCurrentUser().getUsername(), + remoteProctoringRoom.additionalRoomData); }); } @@ -353,7 +355,8 @@ public class ZoomProctoringService implements ExamProctoringService { credentials.accessToken, credentials.clientId, String.valueOf(additionalZoomRoomData.meeting_id), - clientConnection.clientConnection.userSessionId); + clientConnection.clientConnection.userSessionId, + remoteProctoringRoom.additionalRoomData); }); } @@ -553,6 +556,11 @@ public class ZoomProctoringService implements ExamProctoringService { createUser.getBody(), UserResponse.class); + this.zoomRestTemplate.applyUserSettings( + proctoringSettings.serverURL, + credentials, + userResponse.id); + // Then create new meeting with the ad-hoc user/host final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9); final ResponseEntity createMeeting = this.zoomRestTemplate.createMeeting( @@ -745,7 +753,6 @@ public class ZoomProctoringService implements ExamProctoringService { private long forExam(final ProctoringServiceSettings examProctoring) { - // TODO // NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT final long nowInSeconds = Utils.getSecondsNow(); final long nowPlus30MinInSeconds = nowInSeconds + Utils.toSeconds(30 * Constants.MINUTE_IN_MILLIS); @@ -779,6 +786,7 @@ public class ZoomProctoringService implements ExamProctoringService { private static final int LIZENSED_USER = 2; private static final String API_TEST_ENDPOINT = "v2/users"; private static final String API_CREATE_USER_ENDPOINT = "v2/users"; + private static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings"; private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete"; private static final String API_USER_CUST_CREATE = "custCreate"; private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser"; @@ -856,6 +864,31 @@ public class ZoomProctoringService implements ExamProctoringService { } } + public ResponseEntity applyUserSettings( + final String zoomServerUrl, + final ClientCredentials credentials, + final String userId) { + try { + final String url = UriComponentsBuilder + .fromUriString(zoomServerUrl) + .path(API_APPLY_USER_SETTINGS_ENDPOINT) + .buildAndExpand(userId) + .normalize() + .toUriString(); + + final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest(); + final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(applySettingsRequest); + final HttpHeaders headers = getHeaders(credentials); + + headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + final ResponseEntity exchange = exchange(url, HttpMethod.PATCH, body, headers); + return exchange; + } catch (final Exception e) { + log.error("Failed to apply user settings for Zoom user: {}", userId, e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + public ResponseEntity createMeeting( final String zoomServerUrl, final ClientCredentials credentials, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java index d822da64..d1a2378d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomRoomRequestResponse.java @@ -87,6 +87,50 @@ public interface ZoomRoomRequestResponse { } } + @JsonIgnoreProperties(ignoreUnknown = true) + static class ApplyUserSettingsRequest { + @JsonProperty final InMeetingSettings in_meeting; + + // NOTE: This seems to need a special Zoom Account Plan to work + //@JsonProperty final FeaturesSettings feature; + + public ApplyUserSettingsRequest() { + this.in_meeting = new InMeetingSettings(true, 3, 3); + //this.feature = new FeaturesSettings("Basic"); + } + + public ApplyUserSettingsRequest( + final InMeetingSettings in_meeting, + final FeaturesSettings feature) { + this.in_meeting = in_meeting; + //this.feature = feature; + } + + static class InMeetingSettings { + + @JsonProperty final boolean auto_saving_chat; + @JsonProperty final int allow_users_save_chats; + @JsonProperty final int allow_participants_chat_with; + + public InMeetingSettings( + final boolean auto_saving_chat, + final int allow_users_save_chats, + final int allow_participants_chat_with) { + + this.auto_saving_chat = auto_saving_chat; + this.allow_users_save_chats = allow_users_save_chats; + this.allow_participants_chat_with = allow_participants_chat_with; + } + } + + static class FeaturesSettings { + @JsonProperty final String concurrent_meeting; + public FeaturesSettings(final String concurrent_meeting) { + this.concurrent_meeting = concurrent_meeting; + } + } + } + @JsonIgnoreProperties(ignoreUnknown = true) static class UserResponse { final String id; diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java index 749c24a4..1fd9539c 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/JitsiWindowScriptResolverTest.java @@ -41,7 +41,8 @@ public class JitsiWindowScriptResolverTest { "API_KEY", "ROOM_KEY", "MEETING_ID", - "USER_NAME")); + "USER_NAME", + null)); final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData( "0", @@ -58,7 +59,8 @@ public class JitsiWindowScriptResolverTest { "API_KEY", "ROOM_KEY", "MEETING_ID", - "USER_NAME")); + "USER_NAME", + null)); assertFalse(jitsiWindowScriptResolver.applies(proctoringWindowDataOther)); assertTrue(jitsiWindowScriptResolver.applies(proctoringWindowData)); diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java index 6c2d625d..72d66925 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ZoomWindowScriptResolverTest.java @@ -41,7 +41,8 @@ public class ZoomWindowScriptResolverTest { "API_KEY", "ROOM_KEY", "MEETING_ID", - "USER_NAME")); + "USER_NAME", + null)); final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData( "0", @@ -58,7 +59,8 @@ public class ZoomWindowScriptResolverTest { "API_KEY", "ROOM_KEY", "MEETING_ID", - "USER_NAME")); + "USER_NAME", + null)); assertFalse(zoomWindowScriptResolver.applies(proctoringWindowDataOther)); assertTrue(zoomWindowScriptResolver.applies(proctoringWindowDataZoom));