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

View file

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

View file

@ -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<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(
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<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,
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.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<String> 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<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(
final String zoomServerUrl,
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)
static class UserResponse {
final String id;

View file

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

View file

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