From 0f7ef455e8acdb8bc6cf3be968e6f0460bdc4c97 Mon Sep 17 00:00:00 2001 From: anhefti Date: Tue, 7 Sep 2021 11:35:20 +0200 Subject: [PATCH] Added Zoom Client App integration for collecting rooms --- .../model/exam/ProctoringServiceSettings.java | 13 ++- .../gui/content/ExamProctoringSettings.java | 42 +++++--- .../MonitoringProctoringService.java | 98 ++++++++++++++----- .../exam/impl/ExamAdminServiceImpl.java | 17 +++- src/main/resources/messages.properties | 10 +- .../admin/ExamProctoringRoomServiceTest.java | 2 +- 6 files changed, 133 insertions(+), 49 deletions(-) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java index 92c094fe..b1832f08 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/ProctoringServiceSettings.java @@ -49,6 +49,7 @@ public class ProctoringServiceSettings implements Entity { public static final String ATTR_ENABLED_FEATURES = "enabledFeatures"; public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName"; public static final String ATTR_SERVICE_IN_USE = "serviceInUse"; + public static final String ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM = "useZoomAppClientForCollectingRoom"; @JsonProperty(Domain.EXAM.ATTR_ID) public final Long examId; @@ -84,6 +85,9 @@ public class ProctoringServiceSettings implements Entity { @JsonProperty(ATTR_SERVICE_IN_USE) public final Boolean serviceInUse; + @JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) + public final Boolean useZoomAppClientForCollectingRoom; + @JsonCreator public ProctoringServiceSettings( @JsonProperty(Domain.EXAM.ATTR_ID) final Long examId, @@ -96,7 +100,8 @@ public class ProctoringServiceSettings implements Entity { @JsonProperty(ATTR_APP_KEY) final String appKey, @JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret, @JsonProperty(ATTR_SDK_KEY) final String sdkKey, - @JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret) { + @JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret, + @JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) { this.examId = examId; this.enableProctoring = BooleanUtils.isTrue(enableProctoring); @@ -109,7 +114,7 @@ public class ProctoringServiceSettings implements Entity { this.appSecret = appSecret; this.sdkKey = sdkKey; this.sdkSecret = sdkSecret; - + this.useZoomAppClientForCollectingRoom = BooleanUtils.toBoolean(useZoomAppClientForCollectingRoom); } @Override @@ -171,6 +176,10 @@ public class ProctoringServiceSettings implements Entity { return this.serviceInUse; } + public Boolean getUseZoomAppClientForCollectingRoom() { + return this.useZoomAppClientForCollectingRoom; + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java index c2a07821..737d5dac 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/ExamProctoringSettings.java @@ -76,6 +76,8 @@ public class ExamProctoringSettings { new LocTextKey("sebserver.exam.proctoring.form.sdkkey"); private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET = new LocTextKey("sebserver.exam.proctoring.form.sdksecret"); + private final static LocTextKey SEB_PROCTORING_FORM_USE_ZOOM_APP_CLIENT = + new LocTextKey("sebserver.exam.proctoring.form.useZoomAppClient"); private final static LocTextKey SEB_PROCTORING_FORM_FEATURES = new LocTextKey("sebserver.exam.proctoring.form.features"); @@ -162,7 +164,9 @@ public class ExamProctoringSettings { form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY), form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET), form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_KEY), - form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET)); + form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET), + BooleanUtils.toBoolean(form.getFieldValue( + ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM))); } catch (final Exception e) { log.error("Unexpected error while trying to get settings from form: ", e); @@ -232,7 +236,7 @@ public class ExamProctoringSettings { .copyOf(content) .clearEntityKeys(); - final boolean isZoom = proctoringSettings.serverType == ProctoringServerType.ZOOM; + //final boolean isZoom = proctoringSettings.serverType == ProctoringServerType.ZOOM; final FormHandle formHandle = this.pageService.formBuilder( formContext) @@ -258,8 +262,7 @@ public class ExamProctoringSettings { ProctoringServiceSettings.ATTR_SERVER_TYPE, SEB_PROCTORING_FORM_TYPE, proctoringSettings.serverType.name(), - resourceService::examProctoringTypeResources) - .withSelectionListener(this::serviceSelection)) + resourceService::examProctoringTypeResources)) .addField(FormBuilder.text( ProctoringServiceSettings.ATTR_SERVER_URL, @@ -282,8 +285,7 @@ public class ExamProctoringSettings { .addField(FormBuilder.text( ProctoringServiceSettings.ATTR_SDK_KEY, SEB_PROCTORING_FORM_SDKKEY, - proctoringSettings.sdkKey) - .visibleIf(isZoom)) + proctoringSettings.sdkKey)) .withEmptyCellSeparation(false) .addField(FormBuilder.password( @@ -291,8 +293,7 @@ public class ExamProctoringSettings { SEB_PROCTORING_FORM_SDKSECRET, (proctoringSettings.sdkSecret != null) ? String.valueOf(proctoringSettings.sdkSecret) - : null) - .visibleIf(isZoom)) + : null)) .withDefaultSpanInput(1) .addField(FormBuilder.text( @@ -304,6 +305,14 @@ public class ExamProctoringSettings { .withDefaultSpanEmptyCell(4) .withDefaultSpanInput(5) + .addField(FormBuilder.checkbox( + ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM, + SEB_PROCTORING_FORM_USE_ZOOM_APP_CLIENT, + String.valueOf(proctoringSettings.useZoomAppClientForCollectingRoom))) + .withDefaultSpanInput(5) + .withEmptyCellSeparation(true) + .withDefaultSpanEmptyCell(1) + .addField(FormBuilder.multiCheckboxSelection( ProctoringServiceSettings.ATTR_ENABLED_FEATURES, SEB_PROCTORING_FORM_FEATURES, @@ -317,16 +326,19 @@ public class ExamProctoringSettings { formHandle.getForm().getFieldInput(ProctoringServiceSettings.ATTR_SERVER_URL).setEnabled(false); } + //serviceSelection(formHandle.getForm()); + return () -> formHandle; } - private void serviceSelection(final Form form) { - final boolean isZoom = ProctoringServerType.ZOOM.name() - .equals(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE)); - - form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_KEY); - form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_SECRET); - } +// private void serviceSelection(final Form form) { +// final boolean isZoom = ProctoringServerType.ZOOM.name() +// .equals(form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE)); +// +// form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_KEY); +// form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_SDK_SECRET); +// form.setFieldVisible(isZoom, ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM); +// } } } 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 42eaf11c..3d056de1 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 @@ -10,8 +10,10 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring; import java.util.Arrays; import java.util.Collections; +import java.util.Map; import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; import org.eclipse.swt.graphics.Color; @@ -24,9 +26,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import com.fasterxml.jackson.core.type.TypeReference; + import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.EntityType; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; @@ -79,14 +84,19 @@ public class MonitoringProctoringService { static final String OPEN_ROOM_SCRIPT = "try {\n" + "var existingWin = window.open('', '%s', 'height=%s,width=%s,location=no,scrollbars=yes,status=no,menubar=0,toolbar=no,titlebar=no,dialog=no');\n" + - "existingWin.document.title = '%s';\n" + + "try {\n" + "if(existingWin.location.href === 'about:blank'){\n" + + " existingWin.document.title = '%s';\n" + " existingWin.location.href = '%s%s';\n" + " existingWin.focus();\n" + "} else {\n" + " existingWin.focus();\n" + "}" + - "}\n" + + "} catch(secErr) {\n" + + " alert(\"Unexpected Javascript Error happened: \" + secErr);\n"+ + " existingWin.focus();\n" + + "}" + + "}" + "catch(err) {\n" + " alert(\"Unexpected Javascript Error happened: \" + err);\n"+ "}"; @@ -95,17 +105,20 @@ public class MonitoringProctoringService { private final PageService pageService; private final GuiServiceInfo guiServiceInfo; private final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup; + private final JSONMapper jsonMapper; private final String remoteProctoringEndpoint; public MonitoringProctoringService( final PageService pageService, final GuiServiceInfo guiServiceInfo, final ProctorRoomConnectionsPopup proctorRoomConnectionsPopup, + final JSONMapper jsonMapper, @Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint) { this.pageService = pageService; this.guiServiceInfo = guiServiceInfo; this.proctorRoomConnectionsPopup = proctorRoomConnectionsPopup; + this.jsonMapper = jsonMapper; this.remoteProctoringEndpoint = remoteProctoringEndpoint; } @@ -259,35 +272,68 @@ public class MonitoringProctoringService { String.valueOf(proctoringSettings.examId), proctoringConnectionData); - final String script = String.format( - OPEN_ROOM_SCRIPT, - room.name, - 800, - 1200, - room.name, - this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), - this.remoteProctoringEndpoint); + if (proctoringSettings.useZoomAppClientForCollectingRoom && + StringUtils.isNotBlank(extractZoomStartLink(room))) { - RWT.getClient() - .getService(JavaScriptExecutor.class) - .execute(script); + final String startLink = extractZoomStartLink(room); + final String script = String.format( + OPEN_ROOM_SCRIPT, + room.name, + 800, + 1200, + room.name, + startLink, + ""); - final boolean newWindow = this.pageService.getCurrentUser() - .getProctoringGUIService() - .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name); + RWT.getClient() + .getService(JavaScriptExecutor.class) + .execute(script); + + } else { + + final String script = String.format( + OPEN_ROOM_SCRIPT, + room.name, + 800, + 1200, + room.name, + this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), + this.remoteProctoringEndpoint); + + RWT.getClient() + .getService(JavaScriptExecutor.class) + .execute(script); + + final boolean newWindow = this.pageService.getCurrentUser() + .getProctoringGUIService() + .registerProctoringWindow(String.valueOf(room.examId), room.name, room.name); + + if (newWindow) { + 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)); + } - if (newWindow) { - 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; } + 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, @@ -320,9 +366,9 @@ public class MonitoringProctoringService { final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class); final String script = String.format( MonitoringProctoringService.OPEN_ROOM_SCRIPT, - connectionToken, - 420, - 640, + connectionData.clientConnection.userSessionId, + 800, + 1200, connectionData.clientConnection.userSessionId, this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), this.remoteProctoringEndpoint); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 4e50ce09..7c27aab8 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -215,7 +215,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY), getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET), getString(mapping, ProctoringServiceSettings.ATTR_SDK_KEY), - getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET)); + getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET), + getBoolean(mapping, ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)); }); } @@ -287,6 +288,12 @@ public class ExamAdminServiceImpl implements ExamAdminService { ProctoringServiceSettings.ATTR_ENABLED_FEATURES, StringUtils.join(proctoringServiceSettings.enabledFeatures, Constants.LIST_SEPARATOR)); + this.additionalAttributesDAO.saveAdditionalAttribute( + EntityType.EXAM, + examId, + ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM, + String.valueOf(proctoringServiceSettings.useZoomAppClientForCollectingRoom)); + return proctoringServiceSettings; }); } @@ -334,6 +341,14 @@ public class ExamAdminServiceImpl implements ExamAdminService { } } + private Boolean getBoolean(final Map mapping, final String name) { + if (mapping.containsKey(name)) { + return BooleanUtils.toBooleanObject(mapping.get(name).getValue()); + } else { + return false; + } + } + private Integer getCollectingRoomSize(final Map mapping) { if (mapping.containsKey(ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE)) { return Integer.valueOf(mapping.get(ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE).getValue()); diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 1b11ebaa..7ba3332b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -662,10 +662,10 @@ sebserver.exam.proctoring.form.appkey=App Key sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server sebserver.exam.proctoring.form.secret=App Secret sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service -sebserver.exam.proctoring.form.sdkkey=SDK Key (MacOS/iOS) -sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS -sebserver.exam.proctoring.form.sdksecret=SDK Secret (MacOS/iOS) -sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS +sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom - MacOS/iOS) +sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS
This is only relevant for proctoring with Zoom service. +sebserver.exam.proctoring.form.sdksecret=SDK Secret (Zoom - MacOS/iOS) +sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS
This is only relevant for proctoring with Zoom service. sebserver.exam.proctoring.form.features=Enabled Features sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room @@ -674,6 +674,8 @@ sebserver.exam.proctoring.form.features.ENABLE_CHAT=Chat Feature sebserver.exam.proctoring.form.features.WAITING_ROOM=Enable waiting room for collecting rooms sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin for collecting rooms sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave +sebserver.exam.proctoring.form.useZoomAppClient=Use Zoom App-Client +sebserver.exam.proctoring.form.useZoomAppClient.tooltip=If this is set SEB Server opens a start link for the meeting instead of a new popup-window with the Zoom Web Client.
A Zoom App Client must already be installed on the proctor's device or can be installed by following the instructions shown in the browser window. sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java index 282352c5..b13bc549 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/admin/ExamProctoringRoomServiceTest.java @@ -63,7 +63,7 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT 2L, new ProctoringServiceSettings( 2L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch", 1, null, false, - "app-key", "app.secret", "sdk-key", "sdk.secret")); + "app-key", "app.secret", "sdk-key", "sdk.secret", false)); assertTrue(this.examAdminService.isProctoringEnabled(2L).get()); }