zoom integration
This commit is contained in:
		
							parent
							
								
									564b7ec9c9
								
							
						
					
					
						commit
						43383bcebb
					
				
					 13 changed files with 574 additions and 281 deletions
				
			
		|  | @ -53,6 +53,7 @@ public final class Constants { | ||||||
|     public static final Character SLASH = '/'; |     public static final Character SLASH = '/'; | ||||||
|     public static final Character BACKSLASH = '\\'; |     public static final Character BACKSLASH = '\\'; | ||||||
|     public static final Character QUOTE = '\''; |     public static final Character QUOTE = '\''; | ||||||
|  |     public static final Character QUERY = '?'; | ||||||
|     public static final Character DOUBLE_QUOTE = '"'; |     public static final Character DOUBLE_QUOTE = '"'; | ||||||
|     public static final Character COMMA = ','; |     public static final Character COMMA = ','; | ||||||
|     public static final Character PIPE = '|'; |     public static final Character PIPE = '|'; | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ public class ProctoringRoomConnection { | ||||||
|     public static final String ATTR_SUBJECT = "subject"; |     public static final String ATTR_SUBJECT = "subject"; | ||||||
|     public static final String ATTR_ACCESS_TOKEN = "accessToken"; |     public static final String ATTR_ACCESS_TOKEN = "accessToken"; | ||||||
|     public static final String ATTR_CONNECTION_URL = "connectionURL"; |     public static final String ATTR_CONNECTION_URL = "connectionURL"; | ||||||
|  |     public static final String ATTR_USER_NAME = "userName"; | ||||||
| 
 | 
 | ||||||
|     @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) |     @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) | ||||||
|     public final ProctoringServerType proctoringServerType; |     public final ProctoringServerType proctoringServerType; | ||||||
|  | @ -46,6 +47,9 @@ public class ProctoringRoomConnection { | ||||||
|     @JsonProperty(ATTR_ACCESS_TOKEN) |     @JsonProperty(ATTR_ACCESS_TOKEN) | ||||||
|     public final String accessToken; |     public final String accessToken; | ||||||
| 
 | 
 | ||||||
|  |     @JsonProperty(ATTR_USER_NAME) | ||||||
|  |     public final String userName; | ||||||
|  | 
 | ||||||
|     @JsonCreator |     @JsonCreator | ||||||
|     public ProctoringRoomConnection( |     public ProctoringRoomConnection( | ||||||
|             @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType, |             @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType, | ||||||
|  | @ -54,7 +58,8 @@ public class ProctoringRoomConnection { | ||||||
|             @JsonProperty(ATTR_SERVER_URL) final String serverURL, |             @JsonProperty(ATTR_SERVER_URL) final String serverURL, | ||||||
|             @JsonProperty(ATTR_ROOM_NAME) final String roomName, |             @JsonProperty(ATTR_ROOM_NAME) final String roomName, | ||||||
|             @JsonProperty(ATTR_SUBJECT) final String subject, |             @JsonProperty(ATTR_SUBJECT) final String subject, | ||||||
|             @JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken) { |             @JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken, | ||||||
|  |             @JsonProperty(ATTR_USER_NAME) final String userName) { | ||||||
| 
 | 
 | ||||||
|         this.proctoringServerType = proctoringServerType; |         this.proctoringServerType = proctoringServerType; | ||||||
|         this.connectionToken = connectionToken; |         this.connectionToken = connectionToken; | ||||||
|  | @ -63,6 +68,7 @@ public class ProctoringRoomConnection { | ||||||
|         this.roomName = roomName; |         this.roomName = roomName; | ||||||
|         this.subject = subject; |         this.subject = subject; | ||||||
|         this.accessToken = accessToken; |         this.accessToken = accessToken; | ||||||
|  |         this.userName = userName; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ProctoringServerType getProctoringServerType() { |     public ProctoringServerType getProctoringServerType() { | ||||||
|  |  | ||||||
|  | @ -51,6 +51,7 @@ public final class ClientInstruction { | ||||||
|             public static final String ZOOM_URL = "zoomMeetServerURL"; |             public static final String ZOOM_URL = "zoomMeetServerURL"; | ||||||
|             public static final String ZOOM_ROOM = "zoomMeetRoom"; |             public static final String ZOOM_ROOM = "zoomMeetRoom"; | ||||||
|             public static final String ZOOM_ROOM_SUBJECT = "zoomMeetSubject"; |             public static final String ZOOM_ROOM_SUBJECT = "zoomMeetSubject"; | ||||||
|  |             public static final String ZOOM_USER_NAME = "zoomUserName"; | ||||||
|             public static final String ZOOM_TOKEN = "zoomMeetToken"; |             public static final String ZOOM_TOKEN = "zoomMeetToken"; | ||||||
|             public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio"; |             public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio"; | ||||||
|             public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo"; |             public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo"; | ||||||
|  |  | ||||||
|  | @ -686,5 +686,4 @@ public final class Utils { | ||||||
|     public static String valueOrEmptyNote(final String value) { |     public static String valueOrEmptyNote(final String value) { | ||||||
|         return StringUtils.isBlank(value) ? Constants.EMPTY_NOTE : value; |         return StringUtils.isBlank(value) ? Constants.EMPTY_NOTE : value; | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ public interface RemoteProctoringRoomDAO { | ||||||
| 
 | 
 | ||||||
|     Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(Long examId); |     Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(Long examId); | ||||||
| 
 | 
 | ||||||
|  |     //Result<Collection<RemoteProctoringRoom>> getRoomsOfExam(Long examId); | ||||||
|  | 
 | ||||||
|     Result<RemoteProctoringRoom> getRoom(Long roomId); |     Result<RemoteProctoringRoom> getRoom(Long roomId); | ||||||
| 
 | 
 | ||||||
|     Result<RemoteProctoringRoom> getRoom(Long examId, String roomName); |     Result<RemoteProctoringRoom> getRoom(Long examId, String roomName); | ||||||
|  |  | ||||||
|  | @ -44,51 +44,20 @@ public interface ExamProctoringService { | ||||||
|             String roomName, |             String roomName, | ||||||
|             String subject); |             String subject); | ||||||
| 
 | 
 | ||||||
|     Result<ProctoringRoomConnection> getClientBreakOutRoomConnection( |     Result<ProctoringRoomConnection> getClientRoomConnection( | ||||||
|             final ProctoringServiceSettings proctoringSettings, |             final ProctoringServiceSettings proctoringSettings, | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final String roomName, |             final String roomName, | ||||||
|             final String subject); |             final String subject); | ||||||
| 
 | 
 | ||||||
|     Result<ProctoringRoomConnection> getClientCollectingRoomConnection( | //    Result<ProctoringRoomConnection> getClientCollectingRoomConnection( | ||||||
|             final ProctoringServiceSettings proctoringSettings, | //            final ProctoringServiceSettings proctoringSettings, | ||||||
|             final String connectionToken, | //            final String connectionToken, | ||||||
|             final String roomName, | //            final String roomName, | ||||||
|             final String subject); | //            final String subject); | ||||||
| 
 | 
 | ||||||
|     Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection); |     Map<String, String> createJoinInstructionAttributes( | ||||||
| 
 |             final ProctoringRoomConnection proctoringConnection); | ||||||
| //    /** This instructs all sepcified SEB clients to join a defined room by creating a individual room access token |  | ||||||
| //     * and join instruction for each client and put this instruction to the clients instruction queue. |  | ||||||
| //     * |  | ||||||
| //     * @param proctoringSettings The proctoring service settings |  | ||||||
| //     * @param clientConnectionTokens A collection of SEB connection tokens. Only active SEB clients will get the |  | ||||||
| //     *            instructions |  | ||||||
| //     * @param roomName The name of the room to join |  | ||||||
| //     * @param subject the subject of the room to join |  | ||||||
| //     * @return Result refer to the room connection data for the proctor to join to room too or to an error when |  | ||||||
| //     *         happened */ |  | ||||||
| //    Result<ProctoringRoomConnection> sendJoinRoomToClients( |  | ||||||
| //            ProctoringServiceSettings proctoringSettings, |  | ||||||
| //            Collection<String> clientConnectionTokens, |  | ||||||
| //            String roomName, |  | ||||||
| //            String subject); |  | ||||||
| 
 |  | ||||||
| //    /** Sends instructions to join or rejoin the individual assigned collecting rooms of each involved SEB client. |  | ||||||
| //     * Creates an individual join instruction for each involved client and put that to the clients instruction queue. |  | ||||||
| //     * |  | ||||||
| //     * INFO: |  | ||||||
| //     * A collecting room is assigned to each SEB client connection while connecting to the SEB server and |  | ||||||
| //     * each SEB client that has successfully connected to the SEB Server and is participating in an exam |  | ||||||
| //     * with proctoring enabled, is assigned to a collecting room. |  | ||||||
| //     * |  | ||||||
| //     * @param proctoringSettings he proctoring service settings |  | ||||||
| //     * @param clientConnectionTokens A collection of SEB connection tokens. Only active SEB clients will get the |  | ||||||
| //     *            instructions |  | ||||||
| //     * @return Empty Result that refers to an error when happened */ |  | ||||||
| //    Result<Void> sendJoinCollectingRoomToClients( |  | ||||||
| //            ProctoringServiceSettings proctoringSettings, |  | ||||||
| //            Collection<String> clientConnectionTokens); |  | ||||||
| 
 | 
 | ||||||
|     Result<Void> disposeServiceRoomsForExam(Exam exam); |     Result<Void> disposeServiceRoomsForExam(Exam exam); | ||||||
| 
 | 
 | ||||||
|  | @ -100,11 +69,11 @@ public interface ExamProctoringService { | ||||||
|         throw new RuntimeException("Test Why: " + connectionToken); |         throw new RuntimeException("Test Why: " + connectionToken); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Result<NewRoom> newCollectingRoom(Long roomNumber); |     Result<NewRoom> newCollectingRoom(ProctoringServiceSettings proctoringSettings, Long roomNumber); | ||||||
| 
 | 
 | ||||||
|     Result<NewRoom> newBreakOutRoom(String subject); |     Result<NewRoom> newBreakOutRoom(ProctoringServiceSettings proctoringSettings, String subject); | ||||||
| 
 | 
 | ||||||
|     Result<Void> disposeBreakOutRoom(String roomName); |     Result<Void> disposeBreakOutRoom(ProctoringServiceSettings proctoringSettings, String roomName); | ||||||
| 
 | 
 | ||||||
|     Map<String, String> getDefaultInstructionAttributes(); |     Map<String, String> getDefaultInstructionAttributes(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -149,7 +149,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
| 
 | 
 | ||||||
|             // First create and get the town-hall room for specified exam |             // First create and get the town-hall room for specified exam | ||||||
|             final RemoteProctoringRoom townhallRoom = examProctoringService |             final RemoteProctoringRoom townhallRoom = examProctoringService | ||||||
|                     .newBreakOutRoom(subject) |                     .newBreakOutRoom(settings, subject) | ||||||
|                     .flatMap(room -> this.remoteProctoringRoomDAO.createTownhallRoom(examId, room)) |                     .flatMap(room -> this.remoteProctoringRoomDAO.createTownhallRoom(examId, room)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|  | @ -187,7 +187,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             final RemoteProctoringRoom breakOutRoom = examProctoringService |             final RemoteProctoringRoom breakOutRoom = examProctoringService | ||||||
|                     .newBreakOutRoom(subject) |                     .newBreakOutRoom(settings, subject) | ||||||
|                     .flatMap(room -> this.remoteProctoringRoomDAO.createBreakOutRoom(examId, room, connectionTokens)) |                     .flatMap(room -> this.remoteProctoringRoomDAO.createBreakOutRoom(examId, room, connectionTokens)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|  | @ -302,7 +302,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|             return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom( |             return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom( | ||||||
|                     examId, |                     examId, | ||||||
|                     proctoringSettings.collectingRoomSize, |                     proctoringSettings.collectingRoomSize, | ||||||
|                     examProctoringService::newCollectingRoom) |                     roomNumber -> examProctoringService.newCollectingRoom( | ||||||
|  |                             proctoringSettings, | ||||||
|  |                             roomNumber)) | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|         } catch (final Exception e) { |         } catch (final Exception e) { | ||||||
|  | @ -334,7 +336,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|         this.remoteProctoringRoomDAO |         this.remoteProctoringRoomDAO | ||||||
|                 .getTownhallRoom(examId) |                 .getTownhallRoom(examId) | ||||||
|                 .map(RemoteProctoringRoom::getName) |                 .map(RemoteProctoringRoom::getName) | ||||||
|                 .flatMap(examProctoringService::disposeBreakOutRoom) |                 .flatMap(roomName -> examProctoringService.disposeBreakOutRoom( | ||||||
|  |                         proctoringSettings, | ||||||
|  |                         roomName)) | ||||||
|                 .flatMap(service -> this.remoteProctoringRoomDAO.deleteTownhallRoom(examId)) |                 .flatMap(service -> this.remoteProctoringRoomDAO.deleteTownhallRoom(examId)) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|  | @ -382,7 +386,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|         // Dispose the proctoring room on service side |         // Dispose the proctoring room on service side | ||||||
|         examProctoringService.disposeBreakOutRoom(remoteProctoringRoom.name) |         examProctoringService | ||||||
|  |                 .disposeBreakOutRoom(proctoringSettings, remoteProctoringRoom.name) | ||||||
|                 .getOrThrow(); |                 .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|         // Send join collecting rooms to involving clients |         // Send join collecting rooms to involving clients | ||||||
|  | @ -448,7 +453,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|         connectionTokens |         connectionTokens | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .forEach(connectionToken -> { |                 .forEach(connectionToken -> { | ||||||
|                     this.sebInstructionService.registerInstruction( |                     this.sebInstructionService | ||||||
|  |                             .registerInstruction( | ||||||
|                                     examId, |                                     examId, | ||||||
|                                     InstructionType.SEB_RECONFIGURE_SETTINGS, |                                     InstructionType.SEB_RECONFIGURE_SETTINGS, | ||||||
|                                     attributes, |                                     attributes, | ||||||
|  | @ -496,8 +502,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|         clientConnectionTokens |         clientConnectionTokens | ||||||
|                 .stream() |                 .stream() | ||||||
|                 .forEach(connectionToken -> { |                 .forEach(connectionToken -> { | ||||||
|                     final ProctoringRoomConnection proctoringConnection = |                     final ProctoringRoomConnection proctoringConnection = examProctoringService | ||||||
|                             examProctoringService.getClientBreakOutRoomConnection( |                             .getClientRoomConnection( | ||||||
|                                     proctoringSettings, |                                     proctoringSettings, | ||||||
|                                     connectionToken, |                                     connectionToken, | ||||||
|                                     examProctoringService.verifyRoomName(roomName, connectionToken), |                                     examProctoringService.verifyRoomName(roomName, connectionToken), | ||||||
|  | @ -551,7 +557,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             final ProctoringRoomConnection proctoringConnection = examProctoringService |             final ProctoringRoomConnection proctoringConnection = examProctoringService | ||||||
|                     .getClientCollectingRoomConnection( |                     .getClientRoomConnection( | ||||||
|                             proctoringSettings, |                             proctoringSettings, | ||||||
|                             clientConnection.clientConnection.connectionToken, |                             clientConnection.clientConnection.connectionToken, | ||||||
|                             roomName, |                             roomName, | ||||||
|  | @ -577,7 +583,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService | ||||||
|         final Map<String, String> attributes = examProctoringService |         final Map<String, String> attributes = examProctoringService | ||||||
|                 .createJoinInstructionAttributes(proctoringConnection); |                 .createJoinInstructionAttributes(proctoringConnection); | ||||||
| 
 | 
 | ||||||
|         this.sebInstructionService.registerInstruction( |         this.sebInstructionService | ||||||
|  |                 .registerInstruction( | ||||||
|                         examId, |                         examId, | ||||||
|                         InstructionType.SEB_PROCTORING, |                         InstructionType.SEB_PROCTORING, | ||||||
|                         attributes, |                         attributes, | ||||||
|  |  | ||||||
|  | @ -170,21 +170,30 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<NewRoom> newCollectingRoom(final Long roomNumber) { |     public Result<NewRoom> newCollectingRoom( | ||||||
|  |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  |             final Long roomNumber) { | ||||||
|  | 
 | ||||||
|         return Result.of(new NewRoom( |         return Result.of(new NewRoom( | ||||||
|                 UUID.randomUUID().toString(), |                 UUID.randomUUID().toString(), | ||||||
|                 "Room " + (roomNumber + 1))); |                 "Room " + (roomNumber + 1))); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<NewRoom> newBreakOutRoom(final String subject) { |     public Result<NewRoom> newBreakOutRoom( | ||||||
|  |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  |             final String subject) { | ||||||
|  | 
 | ||||||
|         return Result.of(new NewRoom( |         return Result.of(new NewRoom( | ||||||
|                 UUID.randomUUID().toString(), |                 UUID.randomUUID().toString(), | ||||||
|                 subject)); |                 subject)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Void> disposeBreakOutRoom(final String roomName) { |     public Result<Void> disposeBreakOutRoom( | ||||||
|  |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  |             final String roomName) { | ||||||
|  | 
 | ||||||
|         return Result.EMPTY; |         return Result.EMPTY; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -237,7 +246,6 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
| 
 | 
 | ||||||
|         return Result.tryCatch(() -> { |         return Result.tryCatch(() -> { | ||||||
|             return createProctoringConnection( |             return createProctoringConnection( | ||||||
|                     proctoringSettings.serverType, |  | ||||||
|                     null, |                     null, | ||||||
|                     proctoringSettings.serverURL, |                     proctoringSettings.serverURL, | ||||||
|                     proctoringSettings.appKey, |                     proctoringSettings.appKey, | ||||||
|  | @ -253,7 +261,7 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<ProctoringRoomConnection> getClientCollectingRoomConnection( |     public Result<ProctoringRoomConnection> getClientRoomConnection( | ||||||
|             final ProctoringServiceSettings proctoringSettings, |             final ProctoringServiceSettings proctoringSettings, | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final String roomName, |             final String roomName, | ||||||
|  | @ -265,7 +273,6 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
|                     .getOrThrow(); |                     .getOrThrow(); | ||||||
| 
 | 
 | ||||||
|             return createProctoringConnection( |             return createProctoringConnection( | ||||||
|                     proctoringSettings.serverType, |  | ||||||
|                     null, |                     null, | ||||||
|                     proctoringSettings.serverURL, |                     proctoringSettings.serverURL, | ||||||
|                     proctoringSettings.appKey, |                     proctoringSettings.appKey, | ||||||
|  | @ -280,38 +287,7 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     public Result<ProctoringRoomConnection> getClientBreakOutRoomConnection( |  | ||||||
|             final ProctoringServiceSettings proctoringSettings, |  | ||||||
|             final String connectionToken, |  | ||||||
|             final String roomName, |  | ||||||
|             final String subject) { |  | ||||||
| 
 |  | ||||||
|         return Result.tryCatch(() -> { |  | ||||||
|             final long expTime = forExam(proctoringSettings); |  | ||||||
| 
 |  | ||||||
|             final ClientConnectionData connectionData = this.examSessionService |  | ||||||
|                     .getConnectionData(connectionToken) |  | ||||||
|                     .getOrThrow(); |  | ||||||
| 
 |  | ||||||
|             return createProctoringConnection( |  | ||||||
|                     proctoringSettings.serverType, |  | ||||||
|                     connectionToken, |  | ||||||
|                     proctoringSettings.serverURL, |  | ||||||
|                     proctoringSettings.appKey, |  | ||||||
|                     proctoringSettings.getAppSecret(), |  | ||||||
|                     connectionData.clientConnection.userSessionId, |  | ||||||
|                     SEB_CLIENT_KEY, |  | ||||||
|                     roomName, |  | ||||||
|                     subject, |  | ||||||
|                     expTime, |  | ||||||
|                     false) |  | ||||||
|                             .getOrThrow(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     protected Result<ProctoringRoomConnection> createProctoringConnection( |     protected Result<ProctoringRoomConnection> createProctoringConnection( | ||||||
|             final ProctoringServerType proctoringServerType, |  | ||||||
|             final String connectionToken, |             final String connectionToken, | ||||||
|             final String url, |             final String url, | ||||||
|             final String appKey, |             final String appKey, | ||||||
|  | @ -341,13 +317,14 @@ public class JitsiProctoringService implements ExamProctoringService { | ||||||
|                     moderator); |                     moderator); | ||||||
| 
 | 
 | ||||||
|             return new ProctoringRoomConnection( |             return new ProctoringRoomConnection( | ||||||
|                     proctoringServerType, |                     ProctoringServerType.JITSI_MEET, | ||||||
|                     connectionToken, |                     connectionToken, | ||||||
|                     host, |                     host, | ||||||
|                     url, |                     url, | ||||||
|                     roomName, |                     roomName, | ||||||
|                     subject, |                     subject, | ||||||
|                     token); |                     token, | ||||||
|  |                     clientName); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,16 +8,20 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; | package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; | ||||||
| 
 | 
 | ||||||
|  | import java.net.URL; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Base64; | import java.util.Base64; | ||||||
| import java.util.Base64.Encoder; | import java.util.Base64.Encoder; | ||||||
|  | import java.util.HashMap; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  | import java.util.UUID; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import javax.crypto.Mac; | import javax.crypto.Mac; | ||||||
| import javax.crypto.spec.SecretKeySpec; | import javax.crypto.spec.SecretKeySpec; | ||||||
| 
 | 
 | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.springframework.context.annotation.Lazy; | import org.springframework.context.annotation.Lazy; | ||||||
|  | @ -29,8 +33,11 @@ import org.springframework.http.MediaType; | ||||||
| import org.springframework.http.ResponseEntity; | import org.springframework.http.ResponseEntity; | ||||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||||
| import org.springframework.web.client.RestTemplate; | import org.springframework.web.client.RestTemplate; | ||||||
|  | import org.springframework.web.util.UriComponentsBuilder; | ||||||
| 
 | 
 | ||||||
| import com.fasterxml.jackson.core.JsonProcessingException; | import com.fasterxml.jackson.annotation.JsonCreator; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; | import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; | ||||||
| import ch.ethz.seb.sebserver.gbl.Constants; | import ch.ethz.seb.sebserver.gbl.Constants; | ||||||
|  | @ -41,20 +48,27 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; | ||||||
| import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | import ch.ethz.seb.sebserver.gbl.api.JSONMapper; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.AsyncService; | import ch.ethz.seb.sebserver.gbl.async.AsyncService; | ||||||
| import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | import ch.ethz.seb.sebserver.gbl.model.exam.Exam; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; | import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; | import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; | import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; | import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; | ||||||
|  | import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; | ||||||
| import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Cryptor; | import ch.ethz.seb.sebserver.gbl.util.Cryptor; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Result; | import ch.ethz.seb.sebserver.gbl.util.Result; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Tuple; | import ch.ethz.seb.sebserver.gbl.util.Tuple; | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Utils; | import ch.ethz.seb.sebserver.gbl.util.Utils; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; | ||||||
|  | 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.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.UserPageResponse; | import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.MeetingResponse; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.UserResponse; | ||||||
| 
 | 
 | ||||||
| @Lazy | @Lazy | ||||||
| @Service | @Service | ||||||
|  | @ -67,8 +81,10 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
| 
 | 
 | ||||||
|     private static final String ZOOM_ACCESS_TOKEN_HEADER = |     private static final String ZOOM_ACCESS_TOKEN_HEADER = | ||||||
|             "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; |             "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; | ||||||
|     private static final String ZOOM_ACCESS_TOKEN_PAYLOAD = |     private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD = | ||||||
|             "{\"iss\":\"%s\",\"exp\":%s}"; |             "{\"iss\":\"%s\",\"exp\":%s}"; | ||||||
|  |     private static final String ZOOM_MEETING_ACCESS_TOKEN_PAYLOAD = | ||||||
|  |             "{\"app_key\":\"%s\",\"iat\":%s,\"exp\":%s,\"tpc\":\"%s\",\"pwd\":\"%s\"}"; | ||||||
| 
 | 
 | ||||||
|     private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( |     private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( | ||||||
|             new Tuple<>( |             new Tuple<>( | ||||||
|  | @ -100,20 +116,26 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
|     private final AsyncService asyncService; |     private final AsyncService asyncService; | ||||||
|     private final JSONMapper jsonMapper; |     private final JSONMapper jsonMapper; | ||||||
|     private final ZoomRestTemplate zoomRestTemplate; |     private final ZoomRestTemplate zoomRestTemplate; | ||||||
|  |     private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; | ||||||
|  |     private final AuthorizationService authorizationService; | ||||||
| 
 | 
 | ||||||
|     public ZoomProctoringService( |     public ZoomProctoringService( | ||||||
|             final ExamSessionService examSessionService, |             final ExamSessionService examSessionService, | ||||||
|             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, |             final ClientHttpRequestFactoryService clientHttpRequestFactoryService, | ||||||
|             final Cryptor cryptor, |             final Cryptor cryptor, | ||||||
|             final AsyncService asyncService, |             final AsyncService asyncService, | ||||||
|             final JSONMapper jsonMapper) { |             final JSONMapper jsonMapper, | ||||||
|  |             final RemoteProctoringRoomDAO remoteProctoringRoomDAO, | ||||||
|  |             final AuthorizationService authorizationService) { | ||||||
| 
 | 
 | ||||||
|         this.examSessionService = examSessionService; |         this.examSessionService = examSessionService; | ||||||
|         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; |         this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; | ||||||
|         this.cryptor = cryptor; |         this.cryptor = cryptor; | ||||||
|         this.asyncService = asyncService; |         this.asyncService = asyncService; | ||||||
|         this.jsonMapper = jsonMapper; |         this.jsonMapper = jsonMapper; | ||||||
|         this.zoomRestTemplate = new ZoomRestTemplate(); |         this.zoomRestTemplate = new ZoomRestTemplate(this); | ||||||
|  |         this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; | ||||||
|  |         this.authorizationService = authorizationService; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -132,25 +154,32 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
| 
 | 
 | ||||||
|             try { |             try { | ||||||
| 
 | 
 | ||||||
|  |                 final ClientCredentials credentials = new ClientCredentials( | ||||||
|  |                         proctoringSettings.appKey, | ||||||
|  |                         proctoringSettings.appSecret); | ||||||
|  | 
 | ||||||
|                 final ResponseEntity<String> result = this.zoomRestTemplate |                 final ResponseEntity<String> result = this.zoomRestTemplate | ||||||
|                         .testServiceConnection(proctoringSettings); |                         .testServiceConnection( | ||||||
|  |                                 proctoringSettings.serverURL, | ||||||
|  |                                 credentials); | ||||||
| 
 | 
 | ||||||
|                 if (result.getStatusCode() != HttpStatus.OK) { |                 if (result.getStatusCode() != HttpStatus.OK) { | ||||||
|                     throw new APIMessageException( |                     throw new APIMessageException( | ||||||
|                             APIMessage.ErrorMessage.BINDING_ERROR, |                             APIMessage.ErrorMessage.BINDING_ERROR, | ||||||
|                             String.valueOf(result.getStatusCode())); |                             String.valueOf(result.getStatusCode())); | ||||||
|                 } else { |  | ||||||
|                     final UserPageResponse response = this.jsonMapper.readValue( |  | ||||||
|                             result.getBody(), |  | ||||||
|                             UserPageResponse.class); |  | ||||||
| 
 |  | ||||||
|                     System.out.println(response); |  | ||||||
| 
 |  | ||||||
|                     final ResponseEntity<String> createUser = this.zoomRestTemplate |  | ||||||
|                             .createUser(proctoringSettings); |  | ||||||
| 
 |  | ||||||
|                     System.out.println(response); |  | ||||||
|                 } |                 } | ||||||
|  | //                else { | ||||||
|  | //                    final UserPageResponse response = this.jsonMapper.readValue( | ||||||
|  | //                            result.getBody(), | ||||||
|  | //                            UserPageResponse.class); | ||||||
|  | // | ||||||
|  | //                    System.out.println(response); | ||||||
|  | // | ||||||
|  | //                    final ResponseEntity<String> createUser = this.zoomRestTemplate | ||||||
|  | //                            .createUser(credentials, "TestRoom"); | ||||||
|  | // | ||||||
|  | //                    System.out.println(response); | ||||||
|  | //                } | ||||||
|             } catch (final Exception e) { |             } catch (final Exception e) { | ||||||
|                 log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e); |                 log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e); | ||||||
|                 throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage()); |                 throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage()); | ||||||
|  | @ -160,32 +189,34 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |  | ||||||
|     public Result<ProctoringRoomConnection> getClientBreakOutRoomConnection( |  | ||||||
|             final ProctoringServiceSettings proctoringSettings, |  | ||||||
|             final String connectionToken, |  | ||||||
|             final String roomName, |  | ||||||
|             final String subject) { |  | ||||||
| 
 |  | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public Result<ProctoringRoomConnection> getClientCollectingRoomConnection( |  | ||||||
|             final ProctoringServiceSettings proctoringSettings, |  | ||||||
|             final String connectionToken, |  | ||||||
|             final String roomName, |  | ||||||
|             final String subject) { |  | ||||||
| 
 |  | ||||||
|         // TODO Auto-generated method stub |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |     @Override | ||||||
|     public Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection) { |     public Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection) { | ||||||
|         // TODO Auto-generated method stub |         final Map<String, String> attributes = new HashMap<>(); | ||||||
|         return null; |         attributes.put( | ||||||
|  |                 ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, | ||||||
|  |                 ProctoringServiceSettings.ProctoringServerType.ZOOM.name()); | ||||||
|  |         attributes.put( | ||||||
|  |                 ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, | ||||||
|  |                 ClientInstruction.ProctoringInstructionMethod.JOIN.name()); | ||||||
|  |         attributes.put( | ||||||
|  |                 ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_URL, | ||||||
|  |                 proctoringConnection.serverURL); | ||||||
|  |         attributes.put( | ||||||
|  |                 ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ROOM, | ||||||
|  |                 proctoringConnection.roomName); | ||||||
|  |         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; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -194,33 +225,136 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
|             final String roomName, |             final String roomName, | ||||||
|             final String subject) { |             final String subject) { | ||||||
| 
 | 
 | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; | 
 | ||||||
|  |             final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO | ||||||
|  |                     .getRoom(proctoringSettings.examId, roomName) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             final AdditionalZoomRoomData additionalZoomRoomData = this.jsonMapper.readValue( | ||||||
|  |                     remoteProctoringRoom.additionalRoomData, | ||||||
|  |                     AdditionalZoomRoomData.class); | ||||||
|  | 
 | ||||||
|  |             final ClientCredentials credentials = new ClientCredentials( | ||||||
|  |                     proctoringSettings.appKey, | ||||||
|  |                     this.cryptor.decrypt(proctoringSettings.appSecret), | ||||||
|  |                     this.cryptor.decrypt(remoteProctoringRoom.joinKey)); | ||||||
|  | 
 | ||||||
|  |             final String jwt = this.createJWTForMeetingAccess(credentials, subject); | ||||||
|  | 
 | ||||||
|  |             return new ProctoringRoomConnection( | ||||||
|  |                     ProctoringServerType.ZOOM, | ||||||
|  |                     null, | ||||||
|  |                     proctoringSettings.serverURL, | ||||||
|  |                     additionalZoomRoomData.join_url, | ||||||
|  |                     roomName, | ||||||
|  |                     subject, | ||||||
|  |                     jwt, | ||||||
|  |                     this.authorizationService.getUserService().getCurrentUser().getUsername()); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Result<ProctoringRoomConnection> getClientRoomConnection( | ||||||
|  |             final ProctoringServiceSettings proctoringSettings, | ||||||
|  |             final String connectionToken, | ||||||
|  |             final String roomName, | ||||||
|  |             final String subject) { | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  | 
 | ||||||
|  |             final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO | ||||||
|  |                     .getRoom(proctoringSettings.examId, roomName) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             final AdditionalZoomRoomData additionalZoomRoomData = this.jsonMapper.readValue( | ||||||
|  |                     remoteProctoringRoom.additionalRoomData, | ||||||
|  |                     AdditionalZoomRoomData.class); | ||||||
|  | 
 | ||||||
|  |             final ClientCredentials credentials = new ClientCredentials( | ||||||
|  |                     proctoringSettings.appKey, | ||||||
|  |                     this.cryptor.decrypt(proctoringSettings.appSecret), | ||||||
|  |                     this.cryptor.decrypt(remoteProctoringRoom.joinKey)); | ||||||
|  | 
 | ||||||
|  |             final String jwt = this.createJWTForMeetingAccess(credentials, subject); | ||||||
|  | 
 | ||||||
|  |             final ClientConnectionData clientConnection = this.examSessionService | ||||||
|  |                     .getConnectionData(connectionToken) | ||||||
|  |                     .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             return new ProctoringRoomConnection( | ||||||
|  |                     ProctoringServerType.ZOOM, | ||||||
|  |                     connectionToken, | ||||||
|  |                     proctoringSettings.serverURL, | ||||||
|  |                     additionalZoomRoomData.join_url, | ||||||
|  |                     roomName, | ||||||
|  |                     subject, | ||||||
|  |                     jwt, | ||||||
|  |                     clientConnection.clientConnection.userSessionId); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Void> disposeServiceRoomsForExam(final Exam exam) { |     public Result<Void> disposeServiceRoomsForExam(final Exam exam) { | ||||||
|         // TODO get all rooms of the exam | 
 | ||||||
|         // close the rooms on Zoom service |         return Result.tryCatch(() -> { | ||||||
|         return null; |             //this.remoteProctoringRoomDAO.getRoomsOfExam(exam.id); | ||||||
|  |         }); | ||||||
|  |         // Get all rooms of the exam | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<NewRoom> newCollectingRoom(final Long roomNumber) { |     public Result<NewRoom> newCollectingRoom( | ||||||
|         // TODO create new room on zoom side and use the id as room name |             final ProctoringServiceSettings proctoringSettings, | ||||||
|         return null; |             final Long roomNumber) { | ||||||
|  | 
 | ||||||
|  |         return createAdHocMeeting( | ||||||
|  |                 UUID.randomUUID().toString(), | ||||||
|  |                 "Proctoring Room " + (roomNumber + 1), | ||||||
|  |                 proctoringSettings); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<NewRoom> newBreakOutRoom(final String subject) { |     public Result<NewRoom> newBreakOutRoom( | ||||||
|         // TODO create new room on zoom side and use the id as room name |             final ProctoringServiceSettings proctoringSettings, | ||||||
|         return null; |             final String subject) { | ||||||
|  | 
 | ||||||
|  |         return createAdHocMeeting( | ||||||
|  |                 UUID.randomUUID().toString(), | ||||||
|  |                 subject, | ||||||
|  |                 proctoringSettings); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Void> disposeBreakOutRoom(final String roomName) { |     public Result<Void> disposeBreakOutRoom( | ||||||
|         // TODO close the room with specified roomName on zoom side |             final ProctoringServiceSettings proctoringSettings, | ||||||
|         return null; |             final String roomName) { | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final RemoteProctoringRoom roomData = this.remoteProctoringRoomDAO | ||||||
|  |                         .getRoom(proctoringSettings.examId, roomName) | ||||||
|  |                         .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |                 AdditionalZoomRoomData additionalZoomRoomData; | ||||||
|  | 
 | ||||||
|  |                 additionalZoomRoomData = this.jsonMapper.readValue( | ||||||
|  |                         roomData.getAdditionalRoomData(), | ||||||
|  |                         AdditionalZoomRoomData.class); | ||||||
|  | 
 | ||||||
|  |                 this.deleteAdHocMeeting( | ||||||
|  |                         proctoringSettings, | ||||||
|  |                         roomName, | ||||||
|  |                         additionalZoomRoomData.user_id) | ||||||
|  |                         .getOrThrow(); | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 throw new RuntimeException("Unexpected error while trying to dispose ad-hoc room for zoom proctoring"); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|  | @ -237,150 +371,96 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
|                 .collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)); |                 .collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected String createPayload( |     private Result<NewRoom> createAdHocMeeting( | ||||||
|             final String clientKey, |             final String roomName, | ||||||
|             final Long expTime) { |             final String subject, | ||||||
| 
 |  | ||||||
|         return String.format( |  | ||||||
|                 ZOOM_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), |  | ||||||
|                 clientKey, |  | ||||||
|                 expTime); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| //    private long forExam(final ProctoringServiceSettings examProctoring) { |  | ||||||
| //        if (examProctoring.examId == null) { |  | ||||||
| //            throw new IllegalStateException("Missing exam identifier from ExamProctoring data"); |  | ||||||
| //        } |  | ||||||
| // |  | ||||||
| //        long expTime = System.currentTimeMillis() + Constants.DAY_IN_MILLIS; |  | ||||||
| //        if (this.examSessionService.isExamRunning(examProctoring.examId)) { |  | ||||||
| //            final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId) |  | ||||||
| //                    .getOrThrow(); |  | ||||||
| //            if (exam.endTime != null) { |  | ||||||
| //                expTime = exam.endTime.getMillis(); |  | ||||||
| //            } |  | ||||||
| //        } |  | ||||||
| //        return expTime; |  | ||||||
| //    } |  | ||||||
| 
 |  | ||||||
|     private class ZoomRestTemplate { |  | ||||||
| 
 |  | ||||||
|         private static final String API_TEST_ENDPOINT = |  | ||||||
|                 "v2/users?status=active&page_size=30&page_number=1&data_type=Json"; |  | ||||||
| 
 |  | ||||||
|         private final RestTemplate restTemplate; |  | ||||||
|         private final CircuitBreaker<ResponseEntity<String>> circuitBreaker; |  | ||||||
| 
 |  | ||||||
|         public ZoomRestTemplate() { |  | ||||||
| 
 |  | ||||||
|             this.restTemplate = new RestTemplate(ZoomProctoringService.this.clientHttpRequestFactoryService |  | ||||||
|                     .getClientHttpRequestFactory() |  | ||||||
|                     .getOrThrow()); |  | ||||||
| 
 |  | ||||||
|             this.circuitBreaker = ZoomProctoringService.this.asyncService.createCircuitBreaker( |  | ||||||
|                     2, |  | ||||||
|                     10 * Constants.SECOND_IN_MILLIS, |  | ||||||
|                     10 * Constants.SECOND_IN_MILLIS); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public ResponseEntity<String> testServiceConnection(final ProctoringServiceSettings proctoringSettings) { |  | ||||||
|             final String url = proctoringSettings.serverURL.endsWith(Constants.SLASH.toString()) |  | ||||||
|                     ? proctoringSettings.serverURL + API_TEST_ENDPOINT |  | ||||||
|                     : proctoringSettings.serverURL + "/" + API_TEST_ENDPOINT; |  | ||||||
|             return exchange(url, HttpMethod.GET, proctoringSettings); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         public ResponseEntity<String> createUser(final ProctoringServiceSettings proctoringSettings) |  | ||||||
|                 throws JsonProcessingException { |  | ||||||
|             final String url = proctoringSettings.serverURL.endsWith(Constants.SLASH.toString()) |  | ||||||
|                     ? proctoringSettings.serverURL + "v2/users" |  | ||||||
|                     : proctoringSettings.serverURL + "/" + "v2/users"; |  | ||||||
|             final CreateUserRequest createUserRequest = new CreateUserRequest( |  | ||||||
|                     "custCreate", |  | ||||||
|                     new CreateUserRequest.UserInfo( |  | ||||||
|                             "andreas.hefti@let.ethz.ch", |  | ||||||
|                             1, |  | ||||||
|                             "Andreas", |  | ||||||
|                             "Hefti")); |  | ||||||
|             final String body = ZoomProctoringService.this.jsonMapper.writeValueAsString(createUserRequest); |  | ||||||
|             final HttpHeaders headers = getHeaders(proctoringSettings); |  | ||||||
|             headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |  | ||||||
|             return exchange(url, HttpMethod.POST, body, headers, null); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private HttpHeaders getHeaders(final ProctoringServiceSettings proctoringSettings) { |  | ||||||
|             final String jwt = createJWT( |  | ||||||
|                     proctoringSettings.appKey, |  | ||||||
|                     proctoringSettings.appSecret, |  | ||||||
|                     System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS); |  | ||||||
| 
 |  | ||||||
|             final HttpHeaders httpHeaders = new HttpHeaders(); |  | ||||||
|             httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); |  | ||||||
|             httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); |  | ||||||
|             return httpHeaders; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private ResponseEntity<String> exchange( |  | ||||||
|                 final String url, |  | ||||||
|                 final HttpMethod method, |  | ||||||
|             final ProctoringServiceSettings proctoringSettings) { |             final ProctoringServiceSettings proctoringSettings) { | ||||||
| 
 | 
 | ||||||
|             return exchange(url, HttpMethod.GET, null, getHeaders(proctoringSettings), null); |         return Result.tryCatch(() -> { | ||||||
|         } |             final ClientCredentials credentials = new ClientCredentials( | ||||||
|  |                     proctoringSettings.appKey, | ||||||
|  |                     this.cryptor.decrypt(proctoringSettings.appSecret)); | ||||||
| 
 | 
 | ||||||
|         private ResponseEntity<String> exchange( |             // First create a new user/host for the new room | ||||||
|                 final String url, |             final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser( | ||||||
|                 final HttpMethod method, |                     proctoringSettings.serverURL, | ||||||
|                 final Object body, |                     credentials, | ||||||
|                 final HttpHeaders httpHeaders, |                     roomName); | ||||||
|                 final Map<String, ?> uriVariables) { |             final UserResponse userResponse = this.jsonMapper.readValue( | ||||||
|  |                     createUser.getBody(), | ||||||
|  |                     UserResponse.class); | ||||||
| 
 | 
 | ||||||
|             final Result<ResponseEntity<String>> protectedRunResult = this.circuitBreaker.protectedRun(() -> { |             // Then create new meeting with the ad-hoc user/host | ||||||
|                 final HttpEntity<Object> httpEntity = (body != null) |             final CharSequence meetingPwd = UUID.randomUUID().toString(); | ||||||
|                         ? new HttpEntity<>(body, httpHeaders) |             final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting( | ||||||
|                         : new HttpEntity<>(httpHeaders); |                     roomName, | ||||||
|  |                     credentials, | ||||||
|  |                     userResponse.id, | ||||||
|  |                     subject, | ||||||
|  |                     meetingPwd); | ||||||
|  |             final MeetingResponse meetingResponse = this.jsonMapper.readValue( | ||||||
|  |                     createMeeting.getBody(), | ||||||
|  |                     MeetingResponse.class); | ||||||
| 
 | 
 | ||||||
|                 final ResponseEntity<String> result = (uriVariables != null) |             // TODO start the meeting automatically ??? | ||||||
|                         ? this.restTemplate.exchange( |  | ||||||
|                                 url, |  | ||||||
|                                 method, |  | ||||||
|                                 httpEntity, |  | ||||||
|                                 String.class, |  | ||||||
|                                 uriVariables) |  | ||||||
|                         : this.restTemplate.exchange( |  | ||||||
|                                 url, |  | ||||||
|                                 method, |  | ||||||
|                                 httpEntity, |  | ||||||
|                                 String.class); |  | ||||||
| 
 | 
 | ||||||
|                 if (result.getStatusCode() != HttpStatus.OK) { |             // Create NewRoom data with all needed information to store persistent | ||||||
|                     log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode()); |             final AdditionalZoomRoomData additionalZoomRoomData = new AdditionalZoomRoomData( | ||||||
|                 } |                     userResponse.id, | ||||||
|  |                     meetingResponse.start_url, | ||||||
|  |                     meetingResponse.join_url); | ||||||
|  |             final String additionalZoomRoomDataString = this.jsonMapper | ||||||
|  |                     .writeValueAsString(additionalZoomRoomData); | ||||||
| 
 | 
 | ||||||
|                 return result; |             return new NewRoom( | ||||||
|  |                     roomName, | ||||||
|  |                     subject, | ||||||
|  |                     this.cryptor.encrypt(meetingPwd), | ||||||
|  |                     additionalZoomRoomDataString); | ||||||
|         }); |         }); | ||||||
|             return protectedRunResult.getOrThrow(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|         private String createJWT( |     private Result<Void> deleteAdHocMeeting( | ||||||
|                 final String appKey, |             final ProctoringServiceSettings proctoringSettings, | ||||||
|                 final CharSequence appSecret, |             final String meetingId, | ||||||
|  |             final String userId) { | ||||||
|  | 
 | ||||||
|  |         return Result.tryCatch(() -> { | ||||||
|  | 
 | ||||||
|  |             final ClientCredentials credentials = new ClientCredentials( | ||||||
|  |                     proctoringSettings.appKey, | ||||||
|  |                     this.cryptor.decrypt(proctoringSettings.appSecret)); | ||||||
|  | 
 | ||||||
|  |             this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId); | ||||||
|  |             this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId); | ||||||
|  | 
 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private String createJWTForAPIAccess( | ||||||
|  |             final ClientCredentials credentials, | ||||||
|             final Long expTime) { |             final Long expTime) { | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|  | 
 | ||||||
|  |             final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret); | ||||||
|  | 
 | ||||||
|             final StringBuilder builder = new StringBuilder(); |             final StringBuilder builder = new StringBuilder(); | ||||||
|             final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); |             final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); | ||||||
| 
 | 
 | ||||||
|             final String jwtHeaderPart = urlEncoder.encodeToString( |             final String jwtHeaderPart = urlEncoder.encodeToString( | ||||||
|                     ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); |                     ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); | ||||||
|                 final String jwtPayload = createPayload(appKey, expTime); |             final String jwtPayload = String.format( | ||||||
|  |                     ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), | ||||||
|  |                     credentials.clientIdAsString(), | ||||||
|  |                     expTime); | ||||||
|             final String jwtPayloadPart = urlEncoder.encodeToString( |             final String jwtPayloadPart = urlEncoder.encodeToString( | ||||||
|                     jwtPayload.getBytes(StandardCharsets.UTF_8)); |                     jwtPayload.getBytes(StandardCharsets.UTF_8)); | ||||||
|             final String message = jwtHeaderPart + "." + jwtPayloadPart; |             final String message = jwtHeaderPart + "." + jwtPayloadPart; | ||||||
| 
 | 
 | ||||||
|             final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); |             final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); | ||||||
|             final SecretKeySpec secret_key = new SecretKeySpec( |             final SecretKeySpec secret_key = new SecretKeySpec( | ||||||
|                         Utils.toByteArray(appSecret), |                     Utils.toByteArray(decryptedSecret), | ||||||
|                     TOKEN_ENCODE_ALG); |                     TOKEN_ENCODE_ALG); | ||||||
|             sha256_HMAC.init(secret_key); |             sha256_HMAC.init(secret_key); | ||||||
|             final String hash = urlEncoder.encodeToString( |             final String hash = urlEncoder.encodeToString( | ||||||
|  | @ -395,6 +475,259 @@ public class ZoomProctoringService implements ExamProctoringService { | ||||||
|             throw new RuntimeException("Failed to create JWT for Zoom API access: ", e); |             throw new RuntimeException("Failed to create JWT for Zoom API access: ", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private String createJWTForMeetingAccess( | ||||||
|  |             final ClientCredentials credentials, | ||||||
|  |             final String subject) { | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  |             final long iat = Utils.getMillisecondsNow() / 1000; | ||||||
|  |             final long exp = iat + 7200; | ||||||
|  | 
 | ||||||
|  |             final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret); | ||||||
|  |             final StringBuilder builder = new StringBuilder(); | ||||||
|  |             final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); | ||||||
|  | 
 | ||||||
|  |             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", ""), | ||||||
|  |                     credentials.clientIdAsString(), | ||||||
|  |                     iat, | ||||||
|  |                     exp, | ||||||
|  |                     subject, | ||||||
|  |                     credentials.accessTokenAsString()); | ||||||
|  |             final String jwtPayloadPart = urlEncoder.encodeToString( | ||||||
|  |                     jwtPayload.getBytes(StandardCharsets.UTF_8)); | ||||||
|  |             final String message = jwtHeaderPart + "." + jwtPayloadPart; | ||||||
|  | 
 | ||||||
|  |             final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); | ||||||
|  |             final SecretKeySpec secret_key = new SecretKeySpec( | ||||||
|  |                     Utils.toByteArray(decryptedSecret), | ||||||
|  |                     TOKEN_ENCODE_ALG); | ||||||
|  |             sha256_HMAC.init(secret_key); | ||||||
|  |             final String hash = urlEncoder.encodeToString( | ||||||
|  |                     sha256_HMAC.doFinal(Utils.toByteArray(message))); | ||||||
|  | 
 | ||||||
|  |             builder.append(message) | ||||||
|  |                     .append(".") | ||||||
|  |                     .append(hash); | ||||||
|  | 
 | ||||||
|  |             return builder.toString(); | ||||||
|  |         } catch (final Exception e) { | ||||||
|  |             throw new RuntimeException("Failed to create JWT for Zoom meeting access: ", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private final static class ZoomRestTemplate { | ||||||
|  | 
 | ||||||
|  |         private static final String API_TEST_ENDPOINT = | ||||||
|  |                 "v2/users?status=active&page_size=30&page_number=1&data_type=Json"; | ||||||
|  |         private static final String API_CREATE_USER_ENDPOINT = "v2/users"; | ||||||
|  |         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 = "ZoomRoomUser"; | ||||||
|  |         private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings"; | ||||||
|  |         private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}"; | ||||||
|  | 
 | ||||||
|  |         private final ZoomProctoringService zoomProctoringService; | ||||||
|  |         private final RestTemplate restTemplate; | ||||||
|  |         private final CircuitBreaker<ResponseEntity<String>> circuitBreaker; | ||||||
|  | 
 | ||||||
|  |         public ZoomRestTemplate(final ZoomProctoringService zoomProctoringService) { | ||||||
|  | 
 | ||||||
|  |             this.zoomProctoringService = zoomProctoringService; | ||||||
|  |             this.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService | ||||||
|  |                     .getClientHttpRequestFactory() | ||||||
|  |                     .getOrThrow()); | ||||||
|  | 
 | ||||||
|  |             this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker( | ||||||
|  |                     2, | ||||||
|  |                     10 * Constants.SECOND_IN_MILLIS, | ||||||
|  |                     10 * Constants.SECOND_IN_MILLIS); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> testServiceConnection( | ||||||
|  |                 final String zoomServerUrl, | ||||||
|  |                 final ClientCredentials credentials) { | ||||||
|  | 
 | ||||||
|  |             final String url = UriComponentsBuilder | ||||||
|  |                     .fromUriString(zoomServerUrl) | ||||||
|  |                     .path(API_TEST_ENDPOINT) | ||||||
|  |                     .toString(); | ||||||
|  |             return exchange(url, HttpMethod.GET, credentials); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> createUser( | ||||||
|  |                 final String zoomServerUrl, | ||||||
|  |                 final ClientCredentials credentials, | ||||||
|  |                 final String roomName) { | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 final String url = UriComponentsBuilder | ||||||
|  |                         .fromUriString(zoomServerUrl) | ||||||
|  |                         .path(API_CREATE_USER_ENDPOINT) | ||||||
|  |                         .toString(); | ||||||
|  |                 final String host = new URL(zoomServerUrl).getHost(); | ||||||
|  |                 final CreateUserRequest createUserRequest = new CreateUserRequest( | ||||||
|  |                         API_USER_CUST_CREATE, | ||||||
|  |                         new CreateUserRequest.UserInfo( | ||||||
|  |                                 roomName + "@" + host, | ||||||
|  |                                 1, | ||||||
|  |                                 roomName, | ||||||
|  |                                 API_ZOOM_ROOM_USER)); | ||||||
|  | 
 | ||||||
|  |                 final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest); | ||||||
|  |                 final HttpHeaders headers = getHeaders(credentials); | ||||||
|  |                 headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); | ||||||
|  |                 return exchange(url, HttpMethod.POST, body, headers); | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.error("Failed to create Zoom ad-hoc user for room: {}", roomName, e); | ||||||
|  |                 throw new RuntimeException("Failed to create Zoom ad-hoc user", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> createMeeting( | ||||||
|  |                 final String zoomServerUrl, | ||||||
|  |                 final ClientCredentials credentials, | ||||||
|  |                 final String userId, | ||||||
|  |                 final String topic, | ||||||
|  |                 final CharSequence password) { | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final String url = UriComponentsBuilder | ||||||
|  |                         .fromUriString(zoomServerUrl) | ||||||
|  |                         .path(API_CREATE_MEETING_ENDPOINT) | ||||||
|  |                         .buildAndExpand(userId) | ||||||
|  |                         .toString(); | ||||||
|  | 
 | ||||||
|  |                 final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest(topic, password); | ||||||
|  | 
 | ||||||
|  |                 final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest); | ||||||
|  |                 final HttpHeaders headers = getHeaders(credentials); | ||||||
|  |                 headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); | ||||||
|  |                 return exchange(url, HttpMethod.POST, body, headers); | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.error("Failed to create Zoom ad-hoc meeting: {}", topic, e); | ||||||
|  |                 throw new RuntimeException("Failed to create Zoom ad-hoc meeting", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> deleteMeeting( | ||||||
|  |                 final String zoomServerUrl, | ||||||
|  |                 final ClientCredentials credentials, | ||||||
|  |                 final String meetingId) { | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  | 
 | ||||||
|  |                 final String url = UriComponentsBuilder | ||||||
|  |                         .fromUriString(zoomServerUrl) | ||||||
|  |                         .path(API_DELETE_MEETING_ENDPOINT) | ||||||
|  |                         .buildAndExpand(meetingId) | ||||||
|  |                         .toString(); | ||||||
|  | 
 | ||||||
|  |                 return exchange(url, HttpMethod.DELETE, credentials); | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.error("Failed to delete Zoom ad-hoc meeting: {}", meetingId, e); | ||||||
|  |                 throw new RuntimeException("Failed to delete Zoom ad-hoc meeting", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> deleteUser( | ||||||
|  |                 final String zoomServerUrl, | ||||||
|  |                 final ClientCredentials credentials, | ||||||
|  |                 final String userId) { | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 final String url = UriComponentsBuilder | ||||||
|  |                         .fromUriString(zoomServerUrl) | ||||||
|  |                         .path(API_DELETE_USER_ENDPOINT) | ||||||
|  |                         .buildAndExpand(userId) | ||||||
|  |                         .toString(); | ||||||
|  | 
 | ||||||
|  |                 return exchange(url, HttpMethod.DELETE, credentials); | ||||||
|  | 
 | ||||||
|  |             } catch (final Exception e) { | ||||||
|  |                 log.error("Failed to delete Zoom ad-hoc user with id: {}", userId, e); | ||||||
|  |                 throw new RuntimeException("Failed to delete Zoom ad-hoc user", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private HttpHeaders getHeaders(final ClientCredentials credentials) { | ||||||
|  |             final String jwt = this.zoomProctoringService | ||||||
|  |                     .createJWTForAPIAccess( | ||||||
|  |                             credentials, | ||||||
|  |                             System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS); | ||||||
|  | 
 | ||||||
|  |             final HttpHeaders httpHeaders = new HttpHeaders(); | ||||||
|  |             httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); | ||||||
|  |             httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); | ||||||
|  |             return httpHeaders; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private ResponseEntity<String> exchange( | ||||||
|  |                 final String url, | ||||||
|  |                 final HttpMethod method, | ||||||
|  |                 final ClientCredentials credentials) { | ||||||
|  | 
 | ||||||
|  |             return exchange(url, HttpMethod.GET, null, getHeaders(credentials)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private ResponseEntity<String> exchange( | ||||||
|  |                 final String url, | ||||||
|  |                 final HttpMethod method, | ||||||
|  |                 final Object body, | ||||||
|  |                 final HttpHeaders httpHeaders) { | ||||||
|  | 
 | ||||||
|  |             final Result<ResponseEntity<String>> protectedRunResult = this.circuitBreaker.protectedRun(() -> { | ||||||
|  |                 final HttpEntity<Object> httpEntity = (body != null) | ||||||
|  |                         ? new HttpEntity<>(body, httpHeaders) | ||||||
|  |                         : new HttpEntity<>(httpHeaders); | ||||||
|  | 
 | ||||||
|  |                 final ResponseEntity<String> result = this.restTemplate.exchange( | ||||||
|  |                         url, | ||||||
|  |                         method, | ||||||
|  |                         httpEntity, | ||||||
|  |                         String.class); | ||||||
|  | 
 | ||||||
|  |                 if (result.getStatusCode() != HttpStatus.OK) { | ||||||
|  |                     log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode()); | ||||||
|  |                     throw new RuntimeException("Error Response: " + result.getStatusCode()); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return result; | ||||||
|  |             }); | ||||||
|  |             return protectedRunResult.getOrThrow(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|  |     public static final class AdditionalZoomRoomData { | ||||||
|  | 
 | ||||||
|  |         @JsonProperty("user_id") | ||||||
|  |         public final String user_id; | ||||||
|  |         @JsonProperty("start_url") | ||||||
|  |         public final String start_url; | ||||||
|  |         @JsonProperty("join_url") | ||||||
|  |         public final String join_url; | ||||||
|  | 
 | ||||||
|  |         @JsonCreator | ||||||
|  |         public AdditionalZoomRoomData( | ||||||
|  |                 @JsonProperty("user_id") final String user_id, | ||||||
|  |                 @JsonProperty("start_url") final String start_url, | ||||||
|  |                 @JsonProperty("join_url") final String join_url) { | ||||||
|  | 
 | ||||||
|  |             this.user_id = user_id; | ||||||
|  |             this.start_url = start_url; | ||||||
|  |             this.join_url = join_url; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -88,14 +88,14 @@ public interface ZoomRoomRequestResponse { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|     static class CreateUserResponse { |     static class UserResponse { | ||||||
|         final String id; |         final String id; | ||||||
|         final String email; |         final String email; | ||||||
|         final int type; |         final int type; | ||||||
|         final String first_name; |         final String first_name; | ||||||
|         final String lasr_name; |         final String lasr_name; | ||||||
|         @JsonCreator |         @JsonCreator | ||||||
|         public CreateUserResponse( |         public UserResponse( | ||||||
|                 @JsonProperty("id") final String id, |                 @JsonProperty("id") final String id, | ||||||
|                 @JsonProperty("email") final String email, |                 @JsonProperty("email") final String email, | ||||||
|                 @JsonProperty("type") final int type, |                 @JsonProperty("type") final int type, | ||||||
|  | @ -111,34 +111,34 @@ public interface ZoomRoomRequestResponse { | ||||||
| 
 | 
 | ||||||
|     // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate |     // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|     static class NewRoomRequest { |     static class CreateMeetingRequest { | ||||||
|         @JsonProperty final String topic; |         @JsonProperty final String topic; | ||||||
|         @JsonProperty final int type = 1; // Instant meeting |         @JsonProperty final int type = 1; // Instant meeting | ||||||
|         @JsonProperty final String start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd`T`HH:mm:ssZ"); |         @JsonProperty final String start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd`T`HH:mm:ssZ"); | ||||||
|         @JsonProperty final String password; |         @JsonProperty final CharSequence password; | ||||||
|         @JsonProperty final Settings settings; |         @JsonProperty final Settings settings; | ||||||
| 
 | 
 | ||||||
|         public NewRoomRequest(final String topic, final String password, final Settings settings) { |         public CreateMeetingRequest(final String topic, final CharSequence password) { | ||||||
|             this.topic = topic; |             this.topic = topic; | ||||||
|             this.password = password; |             this.password = password; | ||||||
|             this.settings = settings; |             this.settings = new Settings(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @JsonIgnoreProperties(ignoreUnknown = true) |         @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|         static class Settings { |         static class Settings { | ||||||
|             final boolean host_video = false; |             @JsonProperty final boolean host_video = false; | ||||||
|             final boolean participant_video = true; |             @JsonProperty final boolean participant_video = true; | ||||||
|             final boolean join_before_host = true; |             @JsonProperty final boolean join_before_host = true; | ||||||
|             final int jbh_time = 0; |             @JsonProperty final int jbh_time = 0; | ||||||
|             final boolean use_pmi = false; |             @JsonProperty final boolean use_pmi = false; | ||||||
|             final String audio = "voip"; |             @JsonProperty final String audio = "voip"; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate |     // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate | ||||||
|     @JsonIgnoreProperties(ignoreUnknown = true) |     @JsonIgnoreProperties(ignoreUnknown = true) | ||||||
|     static class NewRoomResponse { |     static class MeetingResponse { | ||||||
|         final Integer id; |         final Long id; | ||||||
|         final String join_url; |         final String join_url; | ||||||
|         final String start_url; |         final String start_url; | ||||||
|         final String start_time; |         final String start_time; | ||||||
|  | @ -148,8 +148,8 @@ public interface ZoomRoomRequestResponse { | ||||||
|         final String host_id; |         final String host_id; | ||||||
| 
 | 
 | ||||||
|         @JsonCreator |         @JsonCreator | ||||||
|         public NewRoomResponse( |         public MeetingResponse( | ||||||
|                 @JsonProperty("id") final Integer id, |                 @JsonProperty("id") final Long id, | ||||||
|                 @JsonProperty("join_url") final String join_url, |                 @JsonProperty("join_url") final String join_url, | ||||||
|                 @JsonProperty("start_url") final String start_url, |                 @JsonProperty("start_url") final String start_url, | ||||||
|                 @JsonProperty("start_time") final String start_time, |                 @JsonProperty("start_time") final String start_time, | ||||||
|  |  | ||||||
|  | @ -9,6 +9,6 @@ ADD COLUMN IF NOT EXISTS `vdi_pair_token` VARCHAR(255) NULL AFTER `vdi`; | ||||||
| -- Alter Table `remote_proctoring_room` | -- Alter Table `remote_proctoring_room` | ||||||
| -- ----------------------------------------------------- | -- ----------------------------------------------------- | ||||||
| ALTER TABLE `remote_proctoring_room` | ALTER TABLE `remote_proctoring_room` | ||||||
| ADD COLUMN IF NOT EXISTS `break_out_connections` VARCHAR(10000) NULL, | ADD COLUMN IF NOT EXISTS `break_out_connections` VARCHAR(4000) NULL, | ||||||
| ADD COLUMN IF NOT EXISTS `join_key` VARCHAR(255) NULL, | ADD COLUMN IF NOT EXISTS `join_key` VARCHAR(255) NULL, | ||||||
| ADD COLUMN IF NOT EXISTS `room_data` VARCHAR(10000) NULL; | ADD COLUMN IF NOT EXISTS `room_data` VARCHAR(4000) NULL; | ||||||
|  | @ -18,7 +18,6 @@ import org.junit.Test; | ||||||
| import org.mockito.Mockito; | import org.mockito.Mockito; | ||||||
| 
 | 
 | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; | import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; | ||||||
| import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; |  | ||||||
| import ch.ethz.seb.sebserver.gbl.util.Cryptor; | import ch.ethz.seb.sebserver.gbl.util.Cryptor; | ||||||
| 
 | 
 | ||||||
| public class ExamJITSIProctoringServiceTest { | public class ExamJITSIProctoringServiceTest { | ||||||
|  | @ -64,7 +63,6 @@ public class ExamJITSIProctoringServiceTest { | ||||||
|         final JitsiProctoringService examJITSIProctoringService = |         final JitsiProctoringService examJITSIProctoringService = | ||||||
|                 new JitsiProctoringService(null, null, cryptorMock, null); |                 new JitsiProctoringService(null, null, cryptorMock, null); | ||||||
|         final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection( |         final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection( | ||||||
|                 ProctoringServerType.JITSI_MEET, |  | ||||||
|                 "connectionToken", |                 "connectionToken", | ||||||
|                 "https://seb-jitsi.example.ch", |                 "https://seb-jitsi.example.ch", | ||||||
|                 "test-app", |                 "test-app", | ||||||
|  |  | ||||||
|  | @ -95,9 +95,9 @@ CREATE TABLE IF NOT EXISTS `remote_proctoring_room` ( | ||||||
|   `size` INT NULL, |   `size` INT NULL, | ||||||
|   `subject` VARCHAR(255) NULL, |   `subject` VARCHAR(255) NULL, | ||||||
|   `townhall_room` INT(1) NOT NULL DEFAULT 0, |   `townhall_room` INT(1) NOT NULL DEFAULT 0, | ||||||
|   `break_out_connections` VARCHAR(10000) NULL, |   `break_out_connections` VARCHAR(4000) NULL, | ||||||
|   `join_key` VARCHAR(255) NULL, |   `join_key` VARCHAR(255) NULL, | ||||||
|   `room_data` VARCHAR(10000) NULL, |   `room_data` VARCHAR(4000) NULL, | ||||||
|   PRIMARY KEY (`id`), |   PRIMARY KEY (`id`), | ||||||
|   INDEX `proctor_room_exam_id_idx` (`exam_id` ASC), |   INDEX `proctor_room_exam_id_idx` (`exam_id` ASC), | ||||||
|   CONSTRAINT `proctorRoomExamRef` |   CONSTRAINT `proctorRoomExamRef` | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti