Merge branch 'SEBSERV-262' into development

This commit is contained in:
anhefti 2022-01-27 11:42:50 +01:00
commit 45322deea0
8 changed files with 129 additions and 30 deletions

View file

@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -66,6 +67,9 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_USER_NAME) @JsonProperty(ATTR_USER_NAME)
public final String userName; public final String userName;
@JsonProperty(Domain.REMOTE_PROCTORING_ROOM.ATTR_ROOM_DATA)
public final String additionalRoomData;
@JsonCreator @JsonCreator
public ProctoringRoomConnection( public ProctoringRoomConnection(
@JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType, @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType,
@ -79,7 +83,8 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey, @JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey, @JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
@JsonProperty(ATTR_MEETING_ID) final String meetingId, @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.proctoringServerType = proctoringServerType;
this.connectionToken = connectionToken; this.connectionToken = connectionToken;
@ -93,6 +98,7 @@ public class ProctoringRoomConnection {
this.apiKey = apiKey; this.apiKey = apiKey;
this.meetingId = meetingId; this.meetingId = meetingId;
this.userName = userName; this.userName = userName;
this.additionalRoomData = additionalRoomData;
} }
public ProctoringServerType getProctoringServerType() { public ProctoringServerType getProctoringServerType() {
@ -143,6 +149,10 @@ public class ProctoringRoomConnection {
return this.meetingId; return this.meetingId;
} }
public String getAdditionalRoomData() {
return this.additionalRoomData;
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder builder = new StringBuilder(); final StringBuilder builder = new StringBuilder();

View file

@ -282,7 +282,9 @@ public class MonitoringRunningExam implements TemplateComposer {
return CONFIRM_CLOSE_TOWNHALL; return CONFIRM_CLOSE_TOWNHALL;
} }
}) })
.withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(proctoringGUIService, .withExec(action -> this.monitoringProctoringService.toggleTownhallRoom(
proctoringGUIService,
proctoringSettings,
action)) action))
.noEventPropagation() .noEventPropagation()
.publish(); .publish();

View file

@ -119,6 +119,7 @@ public class MonitoringProctoringService {
public PageAction toggleTownhallRoom( public PageAction toggleTownhallRoom(
final ProctoringGUIService proctoringGUIService, final ProctoringGUIService proctoringGUIService,
final ProctoringServiceSettings proctoringSettings,
final PageAction action) { final PageAction action) {
if (isTownhallRoomActive(action.getEntityKey().modelId)) { if (isTownhallRoomActive(action.getEntityKey().modelId)) {
@ -135,7 +136,7 @@ public class MonitoringProctoringService {
} }
} else { } else {
if (openTownhallRoom(proctoringGUIService, action)) { if (openTownhallRoom(proctoringGUIService, proctoringSettings, action)) {
this.pageService.firePageEvent( this.pageService.firePageEvent(
new ActionActivationEvent( new ActionActivationEvent(
true, true,
@ -245,7 +246,7 @@ public class MonitoringProctoringService {
}); });
joinURL = roomData.get("start_url"); joinURL = roomData.get("start_url");
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to get proctoring start URL: ", e);
} }
final PageContext pc = pageContext.copy() final PageContext pc = pageContext.copy()
.clearAttributes() .clearAttributes()
@ -282,17 +283,17 @@ public class MonitoringProctoringService {
String.valueOf(proctoringSettings.examId), String.valueOf(proctoringSettings.examId),
proctoringConnectionData); proctoringConnectionData);
final String zoomStartLink = extractZoomStartLink(room.additionalRoomData);
if (proctoringSettings.useZoomAppClientForCollectingRoom && if (proctoringSettings.useZoomAppClientForCollectingRoom &&
StringUtils.isNotBlank(extractZoomStartLink(room))) { StringUtils.isNotBlank(zoomStartLink)) {
final String startLink = extractZoomStartLink(room);
final String script = String.format( final String script = String.format(
getOpenRoomScriptTemplate(), getOpenRoomScriptTemplate(),
room.name, room.name,
800, 800,
1200, 1200,
room.name, room.name,
startLink, zoomStartLink,
""); "");
RWT.getClient() RWT.getClient()
@ -326,24 +327,11 @@ public class MonitoringProctoringService {
.call() .call()
.onError(error -> log.error("Failed to notify proctoring room opened: ", error)); .onError(error -> log.error("Failed to notify proctoring room opened: ", error));
} }
} }
return action; return action;
} }
private String extractZoomStartLink(final RemoteProctoringRoom room) {
try {
final Map<String, String> data =
this.jsonMapper.readValue(room.additionalRoomData, new TypeReference<Map<String, String>>() {
});
return data.get("start_url");
} catch (final Exception e) {
log.error("Failed to extract Zoom start link: ", e);
return null;
}
}
public PageAction openOneToOneRoom( public PageAction openOneToOneRoom(
final PageAction action, final PageAction action,
final ClientConnectionData connectionData, final ClientConnectionData connectionData,
@ -394,11 +382,14 @@ public class MonitoringProctoringService {
private boolean openTownhallRoom( private boolean openTownhallRoom(
final ProctoringGUIService proctoringGUIService, final ProctoringGUIService proctoringGUIService,
final ProctoringServiceSettings proctoringSettings,
final PageAction action) { final PageAction action) {
try { try {
final EntityKey examId = action.getEntityKey(); final EntityKey examId = action.getEntityKey();
final String endpoint = this.remoteProctoringEndpoint;
final String joinURL = this.guiServiceInfo.getExternalServerURIBuilder().toUriString();
if (proctoringGUIService.getTownhallWindowName(examId.modelId) == null) { if (proctoringGUIService.getTownhallWindowName(examId.modelId) == null) {
final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService final ProctoringRoomConnection proctoringConnectionData = proctoringGUIService
.openTownhallRoom( .openTownhallRoom(
@ -422,8 +413,8 @@ public class MonitoringProctoringService {
800, 800,
1200, 1200,
"Town-Hall", "Town-Hall",
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(), joinURL,
this.remoteProctoringEndpoint); endpoint);
javaScriptExecutor.execute(script); javaScriptExecutor.execute(script);
} catch (final Exception e) { } catch (final Exception e) {
@ -520,4 +511,18 @@ public class MonitoringProctoringService {
} }
} }
private String extractZoomStartLink(final String additioalRoomAttributesJson) {
try {
final Map<String, String> data =
this.jsonMapper.readValue(
additioalRoomAttributesJson,
new TypeReference<Map<String, String>>() {
});
return data.get("start_url");
} catch (final Exception e) {
log.error("Failed to extract Zoom start link: ", e);
return null;
}
}
} }

View file

@ -379,7 +379,8 @@ public class JitsiProctoringService implements ExamProctoringService {
null, null,
null, null,
null, null,
clientName); clientName,
null);
}); });
} }

View file

@ -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.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.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.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;
@ -293,7 +294,8 @@ public class ZoomProctoringService implements ExamProctoringService {
credentials.accessToken, credentials.accessToken,
credentials.clientId, credentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id), 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.accessToken,
credentials.clientId, credentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id), String.valueOf(additionalZoomRoomData.meeting_id),
clientConnection.clientConnection.userSessionId); clientConnection.clientConnection.userSessionId,
remoteProctoringRoom.additionalRoomData);
}); });
} }
@ -553,6 +556,11 @@ public class ZoomProctoringService implements ExamProctoringService {
createUser.getBody(), createUser.getBody(),
UserResponse.class); UserResponse.class);
this.zoomRestTemplate.applyUserSettings(
proctoringSettings.serverURL,
credentials,
userResponse.id);
// Then create new meeting with the ad-hoc user/host // Then create new meeting with the ad-hoc user/host
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9); final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting( final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
@ -745,7 +753,6 @@ public class ZoomProctoringService implements ExamProctoringService {
private long forExam(final ProctoringServiceSettings examProctoring) { 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 // 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 nowInSeconds = Utils.getSecondsNow();
final long nowPlus30MinInSeconds = nowInSeconds + Utils.toSeconds(30 * Constants.MINUTE_IN_MILLIS); 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 int LIZENSED_USER = 2;
private static final String API_TEST_ENDPOINT = "v2/users"; private static final String API_TEST_ENDPOINT = "v2/users";
private static final String API_CREATE_USER_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_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
private static final String API_USER_CUST_CREATE = "custCreate"; private static final String API_USER_CUST_CREATE = "custCreate";
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser"; private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
@ -856,6 +864,31 @@ public class ZoomProctoringService implements ExamProctoringService {
} }
} }
public ResponseEntity<String> 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<String> 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<String> createMeeting( public ResponseEntity<String> createMeeting(
final String zoomServerUrl, final String zoomServerUrl,
final ClientCredentials credentials, final ClientCredentials credentials,

View file

@ -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) @JsonIgnoreProperties(ignoreUnknown = true)
static class UserResponse { static class UserResponse {
final String id; final String id;

View file

@ -41,7 +41,8 @@ public class JitsiWindowScriptResolverTest {
"API_KEY", "API_KEY",
"ROOM_KEY", "ROOM_KEY",
"MEETING_ID", "MEETING_ID",
"USER_NAME")); "USER_NAME",
null));
final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData( final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData(
"0", "0",
@ -58,7 +59,8 @@ public class JitsiWindowScriptResolverTest {
"API_KEY", "API_KEY",
"ROOM_KEY", "ROOM_KEY",
"MEETING_ID", "MEETING_ID",
"USER_NAME")); "USER_NAME",
null));
assertFalse(jitsiWindowScriptResolver.applies(proctoringWindowDataOther)); assertFalse(jitsiWindowScriptResolver.applies(proctoringWindowDataOther));
assertTrue(jitsiWindowScriptResolver.applies(proctoringWindowData)); assertTrue(jitsiWindowScriptResolver.applies(proctoringWindowData));

View file

@ -41,7 +41,8 @@ public class ZoomWindowScriptResolverTest {
"API_KEY", "API_KEY",
"ROOM_KEY", "ROOM_KEY",
"MEETING_ID", "MEETING_ID",
"USER_NAME")); "USER_NAME",
null));
final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData( final ProctoringWindowData proctoringWindowDataOther = new ProctoringWindowData(
"0", "0",
@ -58,7 +59,8 @@ public class ZoomWindowScriptResolverTest {
"API_KEY", "API_KEY",
"ROOM_KEY", "ROOM_KEY",
"MEETING_ID", "MEETING_ID",
"USER_NAME")); "USER_NAME",
null));
assertFalse(zoomWindowScriptResolver.applies(proctoringWindowDataOther)); assertFalse(zoomWindowScriptResolver.applies(proctoringWindowDataOther));
assertTrue(zoomWindowScriptResolver.applies(proctoringWindowDataZoom)); assertTrue(zoomWindowScriptResolver.applies(proctoringWindowDataZoom));