SEBSERV-148 added HTML and script for zoom proctoring

This commit is contained in:
anhefti 2021-03-18 14:15:18 +01:00
parent 67d1e3fba1
commit 3350e4eece
20 changed files with 510 additions and 127 deletions

View file

@ -25,6 +25,8 @@ public class ProctoringRoomConnection {
public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL";
public static final String ATTR_USER_NAME = "userName";
public static final String ATTR_ROOM_KEY = "roomKey";
public static final String ATTR_API_KEY = "apiKey";
@JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE)
public final ProctoringServerType proctoringServerType;
@ -45,7 +47,13 @@ public class ProctoringRoomConnection {
public final String subject;
@JsonProperty(ATTR_ACCESS_TOKEN)
public final String accessToken;
public final CharSequence accessToken;
@JsonProperty(ATTR_ROOM_KEY)
public final CharSequence roomKey;
@JsonProperty(ATTR_API_KEY)
public final CharSequence apiKey;
@JsonProperty(ATTR_USER_NAME)
public final String userName;
@ -58,7 +66,9 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_SUBJECT) final String subject,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken,
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
@JsonProperty(ATTR_USER_NAME) final String userName) {
this.proctoringServerType = proctoringServerType;
@ -68,6 +78,8 @@ public class ProctoringRoomConnection {
this.roomName = roomName;
this.subject = subject;
this.accessToken = accessToken;
this.roomKey = roomKey;
this.apiKey = apiKey;
this.userName = userName;
}
@ -83,10 +95,22 @@ public class ProctoringRoomConnection {
return this.serverHost;
}
public String getAccessToken() {
public CharSequence getAccessToken() {
return this.accessToken;
}
public CharSequence getRoomKey() {
return this.roomKey;
}
public CharSequence getApiKey() {
return this.apiKey;
}
public String getUserName() {
return this.userName;
}
public String getServerURL() {
return this.serverURL;
}

View file

@ -46,16 +46,18 @@ public final class ClientInstruction {
public static final String JITSI_TOKEN = "jitsiMeetToken";
public static final String JITSI_RECEIVE_AUDIO = "jitsiMeetReceiveAudio";
public static final String JITSI_RECEIVE_VIDEO = "jitsiMeetReceiveVideo";
public static final String JITSI_ALLOW_CHAT = "jitsiMeetFeatureFlagChat";
public static final String JITSI_ALLOW_CHAT = "jitsiFeatureFlagChat";
public static final String ZOOM_URL = "zoomMeetServerURL";
public static final String ZOOM_ROOM = "zoomMeetRoom";
public static final String ZOOM_ROOM_SUBJECT = "zoomMeetSubject";
public static final String ZOOM_URL = "zoomServerURL";
public static final String ZOOM_ROOM = "zoomRoom";
public static final String ZOOM_ROOM_SUBJECT = "zoomSubject";
public static final String ZOOM_USER_NAME = "zoomUserName";
public static final String ZOOM_TOKEN = "zoomMeetToken";
public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo";
public static final String ZOOM_ALLOW_CHAT = "zoomMeetFeatureFlagChat";
public static final String ZOOM_API_KEY = "zoomAPIKey";
public static final String ZOOM_TOKEN = "zoomToken";
public static final String ZOOM_MEETING_KEY = "zoomMeetingKey";
public static final String ZOOM_RECEIVE_AUDIO = "zoomReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo";
public static final String ZOOM_ALLOW_CHAT = "zoomFeatureFlagChat";
}
public interface SEB_RECONFIGURE_SETTINGS {
@ -63,9 +65,9 @@ public final class ClientInstruction {
public static final String JITSI_RECEIVE_VIDEO = "jitsiMeetReceiveVideo";
public static final String JITSI_ALLOW_CHAT = "jitsiMeetFeatureFlagChat";
public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo";
public static final String ZOOM_ALLOW_CHAT = "zoomMeetFeatureFlagChat";
public static final String ZOOM_RECEIVE_AUDIO = "zoomReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomReceiveVideo";
public static final String ZOOM_ALLOW_CHAT = "zoomFeatureFlagChat";
}
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.gui;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@ -17,6 +18,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
@ -25,62 +28,22 @@ import org.springframework.web.context.support.WebApplicationContextUtils;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringWindowScriptResolver;
@Component
@GuiProfile
public class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
private static final Logger log = LoggerFactory.getLogger(ProctoringServlet.class);
// @formatter:off
private static final String JITSI_WINDOW_HTML =
"<!DOCTYPE html>" +
"<html>" +
"<head>" +
" <title></title>" +
" <script src='https://%s/external_api.js'></script>" +
"</head>" +
"" +
"<body>" +
"<div id=\"proctoring\"></div> " +
"</body>" +
"<script>" +
" const options = {\n" +
" parentNode: document.querySelector('#proctoring'),\n" +
" roomName: '%s',\n" +
// " width: window.innerWidth,\n" +
" height: window.innerHeight - 4,\n" +
" jwt: '%s',\n" +
" configOverwrite: { startAudioOnly: false, startWithAudioMuted: true, startWithVideoMuted: false, disable1On1Mode: true },\n" +
" interfaceConfigOverwrite: { " +
"TOOLBAR_BUTTONS: [\r\n" +
" 'microphone', 'camera',\r\n" +
" 'fodeviceselection', 'profile', 'chat', 'recording',\r\n" +
" 'livestreaming', 'settings',\r\n" +
" 'videoquality', 'filmstrip', 'feedback',\r\n" +
" 'tileview', 'help', 'mute-everyone', 'security'\r\n" +
" ],"
+ "SHOW_WATERMARK_FOR_GUESTS: false, "
+ "RECENT_LIST_ENABLED: false, "
+ "HIDE_INVITE_MORE_HEADER: true, "
+ "DISABLE_RINGING: true, "
+ "DISABLE_PRESENCE_STATUS: true, "
+ "DISABLE_JOIN_LEAVE_NOTIFICATIONS: true, "
+ "GENERATE_ROOMNAMES_ON_WELCOME_PAGE: false, "
+ "MOBILE_APP_PROMO: false, "
+ "SHOW_JITSI_WATERMARK: false, "
+ "DISABLE_PRESENCE_STATUS: true, "
+ "DISABLE_RINGING: true, "
+ "DISABLE_VIDEO_BACKGROUND: false, "
+ "filmStripOnly: false }\n" +
" }\n" +
" const meetAPI = new JitsiMeetExternalAPI(\"%s\", options);\n" +
" meetAPI.executeCommand('subject', '%s');\n" +
"</script>" +
"</html>";
// @formatter:on
private final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver;
public ProctoringServlet(final Collection<ProctoringWindowScriptResolver> proctoringWindowScriptResolver) {
this.proctoringWindowScriptResolver = proctoringWindowScriptResolver;
}
@Override
protected void doGet(
@ -103,21 +66,17 @@ public class ProctoringServlet extends HttpServlet {
(ProctoringWindowData) httpSession
.getAttribute(ProctoringGUIService.SESSION_ATTR_PROCTORING_DATA);
switch (proctoringData.connectionData.proctoringServerType) {
case JITSI_MEET: {
final String script = String.format(
JITSI_WINDOW_HTML,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.roomName,
proctoringData.connectionData.accessToken,
proctoringData.connectionData.serverHost,
proctoringData.connectionData.subject);
resp.getOutputStream().println(script);
break;
}
default:
throw new RuntimeException(
"Unsupported proctoring server type: " + proctoringData.connectionData.proctoringServerType);
final String script = this.proctoringWindowScriptResolver.stream()
.filter(resolver -> resolver.applies(proctoringData))
.findFirst()
.map(resolver -> resolver.getProctoringWindowScript(proctoringData))
.orElse(null);
if (script == null) {
log.error("Failed to get proctoring window script for data: {}", proctoringData);
resp.getOutputStream().println("Failed to get proctoring window script");
} else {
resp.getOutputStream().println(script);
}
}

View file

@ -67,7 +67,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProcto
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition;
import ch.ethz.seb.sebserver.gui.table.ColumnDefinition.TableFilterAttribute;
import ch.ethz.seb.sebserver.gui.table.EntityTable;

View file

@ -81,7 +81,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetTownha
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionTable;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.widget.Message;
@Lazy

View file

@ -33,8 +33,8 @@ import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.IllegalUserSessionStateException;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.widget.Message;
@Lazy

View file

@ -33,8 +33,8 @@ import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringReconfigurationAttributes;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Component

View file

@ -90,8 +90,9 @@ public class ServerPushService {
log.warn(
"Failed to stop Server Push Session on: {}. "
+ "It seems that the UISession is not available anymore. "
+ "This may source from a connection interruption",
Thread.currentThread().getName(), e);
+ "This may source from a connection interruption. Cause: {}",
Thread.currentThread().getName(),
e.getMessage());
}
});

View file

@ -35,7 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
@Component
@GuiProfile

View file

@ -0,0 +1,106 @@
/*
* 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.session.proctoring;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.text.StringSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
@Lazy
@Service
@GuiProfile
public class JitsiWindowScriptResolver implements ProctoringWindowScriptResolver {
private static final Logger log = LoggerFactory.getLogger(JitsiWindowScriptResolver.class);
private static final String ATTR_SUBJECT = "_subject_";
private static final String ATTR_ACCESS_TOKEN = "_accessToken_";
private static final String ATTR_ROOM_NAME = "_roomName_";
private static final String ATTR_HOST = "_host_";
// @formatter:off
private static final String JITSI_WINDOW_HTML =
"<!DOCTYPE html>" +
"<html>" +
"<head>" +
" <title></title>" +
" <script src='https://%%_" + ATTR_HOST + "_%%/external_api.js'></script>" +
"</head>" +
"" +
"<body>" +
"<div id=\"proctoring\"></div> " +
"</body>" +
"<script>" +
" const options = {\n" +
" parentNode: document.querySelector('#proctoring'),\n" +
" roomName: '%%_" + ATTR_ROOM_NAME + "_%%',\n" +
// " width: window.innerWidth,\n" +
" height: window.innerHeight - 4,\n" +
" jwt: '%%_" + ATTR_ACCESS_TOKEN + "_%%',\n" +
" configOverwrite: { startAudioOnly: false, startWithAudioMuted: true, startWithVideoMuted: false, disable1On1Mode: true },\n" +
" interfaceConfigOverwrite: { " +
"TOOLBAR_BUTTONS: [\r\n" +
" 'microphone', 'camera',\r\n" +
" 'fodeviceselection', 'profile', 'chat', 'recording',\r\n" +
" 'livestreaming', 'settings',\r\n" +
" 'videoquality', 'filmstrip', 'feedback',\r\n" +
" 'tileview', 'help', 'mute-everyone', 'security'\r\n" +
" ],"
+ "SHOW_WATERMARK_FOR_GUESTS: false, "
+ "RECENT_LIST_ENABLED: false, "
+ "HIDE_INVITE_MORE_HEADER: true, "
+ "DISABLE_RINGING: true, "
+ "DISABLE_PRESENCE_STATUS: true, "
+ "DISABLE_JOIN_LEAVE_NOTIFICATIONS: true, "
+ "GENERATE_ROOMNAMES_ON_WELCOME_PAGE: false, "
+ "MOBILE_APP_PROMO: false, "
+ "SHOW_JITSI_WATERMARK: false, "
+ "DISABLE_PRESENCE_STATUS: true, "
+ "DISABLE_RINGING: true, "
+ "DISABLE_VIDEO_BACKGROUND: false, "
+ "filmStripOnly: false }\n" +
" }\n" +
" const meetAPI = new JitsiMeetExternalAPI(\"%%_" + ATTR_HOST + "_%%\", options);\n" +
" meetAPI.executeCommand('subject', '%%_" + ATTR_SUBJECT + "_%%');\n" +
"</script>" +
"</html>";
// @formatter:on
@Override
public boolean applies(final ProctoringWindowData data) {
try {
return data.connectionData.proctoringServerType == ProctoringServerType.JITSI_MEET;
} catch (final Exception e) {
log.error("Failed to verify responsibility. Cause: {}", e.getMessage());
return false;
}
}
@Override
public String getProctoringWindowScript(final ProctoringWindowData data) {
final Map<String, String> args = new HashMap<>();
args.put(ATTR_HOST, data.connectionData.serverHost);
args.put(ATTR_ROOM_NAME, data.connectionData.roomName);
args.put(ATTR_ACCESS_TOKEN, String.valueOf(data.connectionData.accessToken));
args.put(ATTR_SUBJECT, data.connectionData.subject);
return new StringSubstitutor(args, "%%_", "_%%")
.replace(JITSI_WINDOW_HTML);
}
}

View file

@ -6,7 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.session;
package ch.ethz.seb.sebserver.gui.service.session.proctoring;
import java.util.Collection;
import java.util.HashMap;

View file

@ -0,0 +1,19 @@
/*
* 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.session.proctoring;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
public interface ProctoringWindowScriptResolver {
boolean applies(ProctoringWindowData data);
String getProctoringWindowScript(ProctoringWindowData data);
}

View file

@ -0,0 +1,173 @@
/*
* 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.session.proctoring;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
@Lazy
@Service
@GuiProfile
public class ZoomWindowScriptResolver implements ProctoringWindowScriptResolver {
private static final Logger log = LoggerFactory.getLogger(ZoomWindowScriptResolver.class);
private static final String ATTR_SUBJECT = "_subject_";
private static final String ATTR_API_KEY = "_apiKey_";
private static final String ATTR_ACCESS_TOKEN = "_accessToken_";
private static final String ATTR_ROOM_KEY = "_roomKey_";
private static final String ATTR_ROOM_NAME = "_roomName_";
private static final String ATTR_HOST = "_host_";
private static final String ATTR_USER_NAME = "_username_";
// @formatter:off
private static final String ZOOM_WINDOW_HTML =
"<html>\n"
+ " <head>\n"
+ " <meta charset=\"utf-8\" />\n"
+ " <link type=\"text/css\" rel=\"stylesheet\" href=\"https://source.zoom.us/1.8.1/css/bootstrap.css\" />\n"
+ " <link type=\"text/css\" rel=\"stylesheet\" href=\"https://source.zoom.us/1.8.1/css/react-select.css\" />\n"
+ " </head>\n"
+ " <body>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/react.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/react-dom.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/redux.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/redux-thunk.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/jquery.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/1.8.1/lib/vendor/lodash.min.js\"></script>\n"
+ " <script src=\"https://source.zoom.us/zoom-meeting-1.8.1.min.js\"></script>\n"
+ " <script src=\"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js\"></script>\n"
+ " <script type=\"text/javascript\">\n"
+ "\n"
+ " console.log(\"Checking system requirements...\");\n"
+ " console.log(JSON.stringify(ZoomMtg.checkSystemRequirements()));\n"
+ "\n"
+ " console.log(\"Initializing Zoom...\");\n"
+ " ZoomMtg.setZoomJSLib('https://source.zoom.us/1.8.1/lib', '/av');\n"
+ " ZoomMtg.preLoadWasm();\n"
+ " ZoomMtg.prepareJssdk();\n"
+ "\n"
+ " const API_KEY = \"%%_\" + ATTR_API_KEY + \"_%%\";\n"
+ " const config = {\n"
+ " meetingNumber: %%_\" + ATTR_ROOM_NAME + \"_%%,\n"
+ " leaveUrl: '%%_\" + ATTR_HOST + \"_%%',\n"
+ " userName: '%%_\" + ATTR_USER_NAME + \"_%%',\n"
+ " passWord: '%%_\" + ATTR_ROOM_KEY + \"_%%',\n"
+ " role: 0 // 1 for host; 0 for attendee\n"
+ " };\n"
+ "\n"
+ " const signature = '%%_\" + ATTR_ACCESS_TOKEN + \"_%%';\n"
+ "\n"
+ " console.log(\"Initializing meeting...\");\n"
+ "\n"
+ " // See documentation: https://zoom.github.io/sample-app-web/ZoomMtg.html#init\n"
+ " ZoomMtg.init({\n"
+ " debug: true, //optional\n"
+ " leaveUrl: config.leaveUrl, //required\n"
+ " // webEndpoint: 'PSO web domain', // PSO option\n"
+ " showMeetingHeader: true, //option\n"
+ " disableInvite: false, //optional\n"
+ " disableCallOut: false, //optional\n"
+ " disableRecord: false, //optional\n"
+ " disableJoinAudio: false, //optional\n"
+ " audioPanelAlwaysOpen: true, //optional\n"
+ " showPureSharingContent: false, //optional\n"
+ " isSupportAV: true, //optional,\n"
+ " isSupportChat: false, //optional,\n"
+ " isSupportQA: true, //optional,\n"
+ " isSupportCC: true, //optional,\n"
+ " screenShare: true, //optional,\n"
+ " rwcBackup: '', //optional,\n"
+ " videoDrag: true, //optional,\n"
+ " sharingMode: 'both', //optional,\n"
+ " videoHeader: true, //optional,\n"
+ " isLockBottom: true, // optional,\n"
+ " isSupportNonverbal: true, // optional,\n"
+ " isShowJoiningErrorDialog: true, // optional,\n"
+ " inviteUrlFormat: '', // optional\n"
+ " loginWindow: { // optional,\n"
+ " width: 400,\n"
+ " height: 380\n"
+ " },\n"
+ " // meetingInfo: [ // optional\n"
+ " // 'topic',\n"
+ " // 'host',\n"
+ " // 'mn',\n"
+ " // 'pwd',\n"
+ " // 'telPwd',\n"
+ " // 'invite',\n"
+ " // 'participant',\n"
+ " // 'dc'\n"
+ " // ],\n"
+ " disableVoIP: false, // optional\n"
+ " disableReport: false, // optional\n"
+ " error: function (res) {\n"
+ " console.warn(\"INIT ERROR\")\n"
+ " console.log(res)\n"
+ " },\n"
+ " success: function () {\n"
+ " ZoomMtg.join({\n"
+ " signature: signature,\n"
+ " apiKey: API_KEY,\n"
+ " meetingNumber: config.meetingNumber,\n"
+ " userName: config.userName,\n"
+ " /* passWord: meetConfig.passWord, */\n"
+ " error(res) {\n"
+ " console.warn(\"JOIN ERROR\")\n"
+ " console.log(res)\n"
+ " }\n"
+ " })\n"
+ " }\n"
+ " })\n"
+ " </script>\n"
+ " </body>\n"
+ "</html>";
// @formatter:on
@Override
public boolean applies(final ProctoringWindowData data) {
try {
return data.connectionData.proctoringServerType == ProctoringServerType.ZOOM;
} catch (final Exception e) {
log.error("Failed to verify responsibility. Cause: {}", e.getMessage());
return false;
}
}
@Override
public String getProctoringWindowScript(final ProctoringWindowData data) {
final Map<String, String> args = new HashMap<>();
args.put(ATTR_HOST, data.connectionData.serverHost);
args.put(ATTR_ROOM_NAME, data.connectionData.roomName);
args.put(ATTR_ACCESS_TOKEN, String.valueOf(data.connectionData.accessToken));
args.put(ATTR_API_KEY, String.valueOf(data.connectionData.apiKey));
if (StringUtils.isNotBlank(data.connectionData.roomKey)) {
args.put(ATTR_ROOM_KEY, String.valueOf(data.connectionData.roomKey));
} else {
args.put(ATTR_ROOM_KEY, "");
}
args.put(ATTR_SUBJECT, data.connectionData.subject);
args.put(ATTR_USER_NAME, data.connectionData.userName);
return new StringSubstitutor(args, "%%_", "_%%")
.replace(ZOOM_WINDOW_HTML);
}
}

View file

@ -16,35 +16,99 @@ import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.NewRoom;
/** Data access for RemoteProctoringRoom domain objects. */
public interface RemoteProctoringRoomDAO {
Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(Long examId);
/** Get all collecting room records that exists for a given exam.
*
* @param examId the exam identifier
* @return Result refer to the collection of all collecting room records for the given exam or to an error when
* happened */
Result<Collection<RemoteProctoringRoom>> getCollectingRooms(Long examId);
//Result<Collection<RemoteProctoringRoom>> getRoomsOfExam(Long examId);
/** Get all room records that exists for a given exam.
*
* @param examId the exam identifier
* @return Result refer to the collection of all room records for the given exam or to an error when
* happened */
Result<Collection<RemoteProctoringRoom>> getRooms(Long examId);
/** The the room record with given identifier (PK).
*
* @param roomId the room record identifier
* @return Result refer to the room record or to an error when happened */
Result<RemoteProctoringRoom> getRoom(Long roomId);
/** Get the room record with specified name for a given exam.
*
* @param examId the exam identifier
* @param roomName the name of the room
* @return Result refer to the room record or to an error when happened */
Result<RemoteProctoringRoom> getRoom(Long examId, String roomName);
/** Get the room name for the room with given identifier.
*
* @param roomId the room record identifier (PK)
* @return Result refer to the rooms name or to an error when happened */
Result<String> getRoomName(Long roomId);
/** Create the town hall room for a given exam. Uses the given room data to create the record
*
* @param examId the exam identifier
* @param room the room data to save to record
* @return Result refer to the created room record or to an error when happened */
Result<RemoteProctoringRoom> createTownhallRoom(Long examId, NewRoom room);
/** Get the town hall room record for a given exam if existing.
*
* @param examId the exam identifier
* @return Result refer to the town-hall room record or to an error when happened. */
Result<RemoteProctoringRoom> getTownhallRoom(Long examId);
/** Delete the town-hall room record for a given exam.
*
* @param examId the exam identifier
* @return Result refer to the entity key of the former town-hall room record or to an error when happened */
Result<EntityKey> deleteTownhallRoom(Long examId);
/** Create a break-out room for a given exam. Uses the given room data to create the record
*
* @param examId the exam identifier
* @param room the room data to save to record
* @param connectionTokens comma separated list of SEB client connection tokens that joins the new break-out room
* @return Result refer to the created break-out room record or to an error when happened */
Result<RemoteProctoringRoom> createBreakOutRoom(Long examId, NewRoom room, String connectionTokens);
/** Delete the room record with given id.
*
* @param roomId the room identifier (PK)
* @return Result refer to the entity key of the former room record or to an error when happened */
Result<EntityKey> deleteRoom(Long roomId);
/** Delete all room records for a given exam.
*
* @param examId the exam identifier
* @return Result refer to a collection of entity keys for all delete room records or to an error when happened */
Result<Collection<EntityKey>> deleteRooms(Long examId);
/** This reserves a place in a collecting room on a given exam.
* Creates a new collecting room record depending on the roomMaxSize and the how many connection
* already have been collected within the actual collecting room.
*
* @param examId the exam identifier
* @param roomMaxSize the maximum size of connection collected in one collecting room
* @param newRoomFunction Function to create data for a new collecting room if needed.
* @return Result refer to the collecting room record of place or to an error when happened */
Result<RemoteProctoringRoom> reservePlaceInCollectingRoom(
Long examId,
int roomMaxSize,
Function<Long, Result<NewRoom>> newRoomFunction);
/** Releases a place in the actual collecting room for a given exam.
*
* @param examId the exam identifier
* @param roomId the room record identifier (PK)
* @return Result refer to the actual collecting room record or to an error when happened */
Result<RemoteProctoringRoom> releasePlaceInCollectingRoom(final Long examId, Long roomId);
}

View file

@ -54,7 +54,7 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
@Override
@Transactional(readOnly = true)
public Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(final Long examId) {
public Result<Collection<RemoteProctoringRoom>> getCollectingRooms(final Long examId) {
return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper.selectByExample()
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
.and(RemoteProctoringRoomRecordDynamicSqlSupport.townhallRoom, isEqualTo(0))
@ -66,6 +66,18 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<Collection<RemoteProctoringRoom>> getRooms(final Long examId) {
return Result.tryCatch(() -> this.remoteProctoringRoomRecordMapper.selectByExample()
.where(RemoteProctoringRoomRecordDynamicSqlSupport.examId, isEqualTo(examId))
.build()
.execute()
.stream()
.map(this::toDomainModel)
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<RemoteProctoringRoom> getRoom(final Long roomId) {
@ -75,6 +87,7 @@ public class RemoteProctoringRoomDAOImpl implements RemoteProctoringRoomDAO {
}
@Override
@Transactional(readOnly = true)
public Result<RemoteProctoringRoom> getRoom(final Long examId, final String roomName) {
return Result.tryCatch(() -> {
return this.remoteProctoringRoomRecordMapper.selectByExample()

View file

@ -81,7 +81,9 @@ public class LmsAPIServiceImpl implements LmsAPIService {
return;
}
log.debug("LmsSetup changed. Update cache by removing eventually used references");
if (log.isDebugEnabled()) {
log.debug("LmsSetup changed. Update cache by removing eventually used references");
}
this.cache.remove(new CacheKey(lmsSetup.getModelId(), 0));
}
@ -105,11 +107,6 @@ public class LmsAPIServiceImpl implements LmsAPIService {
@Override
public Result<LmsAPITemplate> getLmsAPITemplate(final String lmsSetupId) {
if (log.isDebugEnabled()) {
log.debug("Get LmsAPITemplate for id: {}", lmsSetupId);
}
return Result.tryCatch(() -> this.lmsSetupDAO
.byModelId(lmsSetupId)
.getOrThrow())
@ -214,7 +211,6 @@ public class LmsAPIServiceImpl implements LmsAPIService {
.forEach(this.cache::remove);
// get from cache
return this.cache.get(new CacheKey(lmsSetup.getModelId(), 0));
}
private LmsAPITemplate createLmsSetupTemplate(final LmsSetup lmsSetup) {

View file

@ -30,7 +30,7 @@ public interface ExamProctoringService {
*
* @param proctoringSettings the settings to test
* @return Result refer to true if the settings are correct and the proctoring server can be accessed. */
Result<Boolean> testExamProctoring(final ProctoringServiceSettings proctoringSettings);
Result<Boolean> testExamProctoring(ProctoringServiceSettings proctoringSettings);
/** Gets the room connection data for a certain room for the proctor.
*
@ -45,21 +45,14 @@ public interface ExamProctoringService {
String subject);
Result<ProctoringRoomConnection> getClientRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject);
ProctoringServiceSettings proctoringSettings,
String connectionToken,
String roomName,
String subject);
// Result<ProctoringRoomConnection> getClientCollectingRoomConnection(
// final ProctoringServiceSettings proctoringSettings,
// final String connectionToken,
// final String roomName,
// final String subject);
Map<String, String> createJoinInstructionAttributes(ProctoringRoomConnection proctoringConnection);
Map<String, String> createJoinInstructionAttributes(
final ProctoringRoomConnection proctoringConnection);
Result<Void> disposeServiceRoomsForExam(Exam exam);
Result<Void> disposeServiceRoomsForExam(ProctoringServiceSettings proctoringSettings, Exam exam);
default String verifyRoomName(final String requestedRoomName, final String connectionToken) {
if (StringUtils.isNotBlank(requestedRoomName)) {

View file

@ -70,7 +70,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
@Override
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) {
return this.remoteProctoringRoomDAO.getCollectingRoomsForExam(examId);
return this.remoteProctoringRoomDAO.getCollectingRooms(examId);
}
@Override
@ -115,9 +115,14 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examSessionService
.getRunningExam(exam.id)
.flatMap(this.examAdminService::getProctoringServiceSettings)
.getOrThrow();
this.examAdminService
.getExamProctoringService(exam)
.flatMap(service -> service.disposeServiceRoomsForExam(exam))
.flatMap(service -> service.disposeServiceRoomsForExam(settings, exam))
.onError(error -> log.error("Failed to dispose proctoring service rooms for exam: {} / {}",
exam.name,
exam.externalId,

View file

@ -164,7 +164,10 @@ public class JitsiProctoringService implements ExamProctoringService {
}
@Override
public Result<Void> disposeServiceRoomsForExam(final Exam exam) {
public Result<Void> disposeServiceRoomsForExam(
final ProctoringServiceSettings proctoringSettings,
final Exam exam) {
// NOTE: Since Jitsi rooms are generated and disposed automatically we don't need to do anything here
return Result.EMPTY;
}
@ -234,7 +237,7 @@ public class JitsiProctoringService implements ExamProctoringService {
}
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN,
proctoringConnection.accessToken);
String.valueOf(proctoringConnection.accessToken));
return attributes;
}
@ -324,6 +327,8 @@ public class JitsiProctoringService implements ExamProctoringService {
roomName,
subject,
token,
null,
null,
clientName);
});
}

View file

@ -204,17 +204,27 @@ public class ZoomProctoringService implements ExamProctoringService {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ROOM,
proctoringConnection.roomName);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
String.valueOf(proctoringConnection.accessToken));
if (StringUtils.isNotBlank(proctoringConnection.apiKey)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_API_KEY,
String.valueOf(proctoringConnection.apiKey));
}
if (StringUtils.isNotBlank(proctoringConnection.roomKey)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_MEETING_KEY,
String.valueOf(proctoringConnection.roomKey));
}
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_USER_NAME,
proctoringConnection.userName);
if (StringUtils.isNotBlank(proctoringConnection.subject)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ROOM_SUBJECT,
proctoringConnection.subject);
}
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
proctoringConnection.accessToken);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_USER_NAME,
proctoringConnection.userName);
return attributes;
}
@ -250,6 +260,8 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
credentials.accessToken,
credentials.clientId,
this.authorizationService.getUserService().getCurrentUser().getUsername());
});
}
@ -290,18 +302,29 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
credentials.accessToken,
credentials.clientId,
clientConnection.clientConnection.userSessionId);
});
}
@Override
public Result<Void> disposeServiceRoomsForExam(final Exam exam) {
public Result<Void> disposeServiceRoomsForExam(
final ProctoringServiceSettings proctoringSettings,
final Exam exam) {
return Result.tryCatch(() -> {
//this.remoteProctoringRoomDAO.getRoomsOfExam(exam.id);
this.remoteProctoringRoomDAO
.getRooms(exam.id)
.getOrThrow()
.stream()
.forEach(room -> {
disposeBreakOutRoom(proctoringSettings, room.name)
.onError(error -> log.warn("Failed to dispose proctoring room record for: {} cause: {}",
room,
error.getMessage()));
});
});
// Get all rooms of the exam
}
@Override
@ -492,7 +515,7 @@ public class ZoomProctoringService implements ExamProctoringService {
final String jwtHeaderPart = urlEncoder.encodeToString(
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
ZOOM_MEETING_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
credentials.clientIdAsString(),
iat,
exp,