summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--appinfo/routes/routesBreakoutRoomController.php2
-rw-r--r--docs/breakout-rooms.md61
-rw-r--r--docs/conversation.md20
-rw-r--r--docs/participant.md6
-rw-r--r--lib/Controller/BreakoutRoomController.php13
-rw-r--r--lib/Controller/RoomController.php88
-rw-r--r--lib/Manager.php2
-rw-r--r--lib/Room.php1
-rw-r--r--lib/Service/BreakoutRoomService.php168
-rw-r--r--lib/Service/ParticipantService.php24
-rw-r--r--lib/Service/RoomService.php23
-rw-r--r--tests/integration/features/bootstrap/FeatureContext.php48
-rw-r--r--tests/integration/features/conversation/breakout-rooms.feature319
-rw-r--r--tests/php/Service/BreakoutRoomServiceTest.php108
14 files changed, 821 insertions, 62 deletions
diff --git a/appinfo/routes/routesBreakoutRoomController.php b/appinfo/routes/routesBreakoutRoomController.php
index 5afd44bfd..d5661c335 100644
--- a/appinfo/routes/routesBreakoutRoomController.php
+++ b/appinfo/routes/routesBreakoutRoomController.php
@@ -36,6 +36,8 @@ return [
['name' => 'BreakoutRoom#removeBreakoutRooms', 'url' => '/api/{apiVersion}/breakout-rooms/{token}', 'verb' => 'DELETE', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\BreakoutRoomController::broadcastChatMessage() */
['name' => 'BreakoutRoom#broadcastChatMessage', 'url' => '/api/{apiVersion}/breakout-rooms/{token}/broadcast', 'verb' => 'POST', 'requirements' => $requirements],
+ /** @see \OCA\Talk\Controller\BreakoutRoomController::applyAttendeeMap() */
+ ['name' => 'BreakoutRoom#applyAttendeeMap', 'url' => '/api/{apiVersion}/breakout-rooms/{token}/attendees', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\BreakoutRoomController::requestAssistance() */
['name' => 'BreakoutRoom#requestAssistance', 'url' => '/api/{apiVersion}/breakout-rooms/{token}/request-assistance', 'verb' => 'POST', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\BreakoutRoomController::resetRequestForAssistance() */
diff --git a/docs/breakout-rooms.md b/docs/breakout-rooms.md
index a94f5ba48..4e1790e76 100644
--- a/docs/breakout-rooms.md
+++ b/docs/breakout-rooms.md
@@ -17,22 +17,22 @@ Group and public conversations can be used to host breakout rooms.
* Endpoint: `/breakout-rooms/{token}`
* Data:
-| field | type | Description |
-|---------------|--------|------------------------------------------------------------------------------------------------------|
-| `mode` | int | Participant assignment mode (see [constants list](constants.md#breakout-room-modes)) |
-| `amount` | int | Number of breakout rooms to create (Minimum `1`, maximum `20`) |
-| `attendeeMap` | string | A json encoded Map of attendeeId => room number (0 based) (Only considered when the mode is "manual" |
+| field | type | Description |
+|---------------|--------|-------------------------------------------------------------------------------------------------------|
+| `mode` | int | Participant assignment mode (see [constants list](constants.md#breakout-room-modes)) |
+| `amount` | int | Number of breakout rooms to create (Minimum `1`, maximum `20`) |
+| `attendeeMap` | string | A json encoded Map of attendeeId => room number (0 based) (Only considered when the mode is "manual") |
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When breakout rooms are disabled on the server
- + `400 Bad Request` When breakout rooms are already configured
- + `400 Bad Request` When the conversation is not a group conversation
- + `400 Bad Request` When the conversation is a breakout room itself
- + `400 Bad Request` When the mode is invalid
- + `400 Bad Request` When the amount is below the minimum or above the maximum
- + `400 Bad Request` When the attendee map contains an invalid room number
+ + `400 Bad Request` Error `config`: When breakout rooms are disabled on the server
+ + `400 Bad Request` Error `mode`: When breakout rooms are already configured
+ + `400 Bad Request` Error `room`: When the conversation is not a group conversation
+ + `400 Bad Request` Error `room`: When the conversation is a breakout room itself
+ + `400 Bad Request` Error `mode`: When the mode is invalid
+ + `400 Bad Request` Error `amount`: When the amount is below the minimum or above the maximum
+ + `400 Bad Request` Error `attendeeMap`: When the attendee map contains an invalid room number or moderator
+ `403 Forbidden` When the current user is not a moderator/owner
+ `404 Not Found` When the conversation could not be found for the participant
@@ -57,7 +57,7 @@ Group and public conversations can be used to host breakout rooms.
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When breakout rooms are not configured
+ + `400 Bad Request` Error `mode`: When breakout rooms are not configured
+ `403 Forbidden` When the current user is not a moderator/owner
+ `404 Not Found` When the conversation could not be found for the participant
@@ -70,7 +70,7 @@ Group and public conversations can be used to host breakout rooms.
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When breakout rooms are not configured
+ + `400 Bad Request` Error `mode`: When breakout rooms are not configured
+ `403 Forbidden` When the current user is not a moderator/owner
+ `404 Not Found` When the conversation could not be found for the participant
@@ -89,11 +89,31 @@ Group and public conversations can be used to host breakout rooms.
* Response:
- Status code:
+ `201 Created`
- + `400 Bad Request` When the room does not have breakout rooms configured
+ + `400 Bad Request` Error `mode`: When the room does not have breakout rooms configured
+ `403 Forbidden` When the participant is not a moderator
+ `404 Not Found` When the conversation could not be found for the participant
+ `413 Payload Too Large` When the message was longer than the allowed limit of 32000 characters (check the `spreed => config => chat => max-length` capability for the limit)
+## Reorganize attendees
+
+* Required capability: `breakout-rooms-v1`
+* Method: `POST`
+* Endpoint: `/breakout-rooms/{token}/attendees`
+* Data:
+
+| field | type | Description |
+|---------------|--------|-------------------------------------------------------------------------------------------------------|
+| `attendeeMap` | string | A json encoded Map of attendeeId => room number (0 based) (Only considered when the mode is "manual") |
+
+* Response:
+ - Status code:
+ + `200 OK`
+ + `400 Bad Request` Error `config`: When breakout rooms are disabled on the server
+ + `400 Bad Request` Error `mode`: When breakout rooms are not configured
+ + `400 Bad Request` Error `attendeeMap`: When the attendee map contains an invalid room number or moderator
+ + `403 Forbidden` When the current user is not a moderator/owner
+ + `404 Not Found` When the conversation could not be found for the participant
+
## Request assistance
This endpoint allows participants to raise their hand (token is the breakout room) and moderators will see it in any of the breakout rooms as well as the parent room.
@@ -104,7 +124,7 @@ This endpoint allows participants to raise their hand (token is the breakout roo
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When the room is not a breakout room or breakout rooms are not started
+ + `400 Bad Request` Error `room`: When the room is not a breakout room or breakout rooms are not started
+ `404 Not Found` When the conversation could not be found for the participant
## Reset request for assistance
@@ -115,7 +135,7 @@ This endpoint allows participants to raise their hand (token is the breakout roo
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When the room does not have breakout rooms configured
+ + `400 Bad Request` Error `room`: When the room does not have breakout rooms configured
+ `404 Not Found` When the conversation could not be found for the participant
## List all breakout rooms
@@ -139,7 +159,8 @@ This endpoint allows participants to raise their hand (token is the breakout roo
* Response:
- Status code:
+ `200 OK`
- + `400 Bad Request` When the participant is a moderator in the conversation
- + `400 Bad Request` When breakout rooms are not configured in `free` mode
- + `400 Bad Request` When breakout rooms are not started
+ + `400 Bad Request` Error `moderator`: When the participant is a moderator in the conversation
+ + `400 Bad Request` Error `mode`: When breakout rooms are not configured in `free` mode
+ + `400 Bad Request` Error `status`: When breakout rooms are not started
+ + `400 Bad Request` Error `target`: When the target room is not breakout room of the parent
+ `404 Not Found` When the conversation could not be found for the participant
diff --git a/docs/conversation.md b/docs/conversation.md
index bab915cc6..c5362c564 100644
--- a/docs/conversation.md
+++ b/docs/conversation.md
@@ -96,16 +96,20 @@
## Creating a new conversation
+*Note:* Creating a conversation as a child breakout room, will automatically set the lobby when breakout rooms are not started and will always overwrite the room type with the parent room type. Also moderators of the parent conversation will be automatically added as moderators.
+
* Method: `POST`
* Endpoint: `/room`
* Data:
-| field | type | Description |
-|------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `roomType` | int | See [constants list](constants.md#conversation-types) |
-| `invite` | string | user id (`roomType = 1`), group id (`roomType = 2` - optional), circle id (`roomType = 2`, `source = 'circles'`], only available with `circles-support` capability)) |
-| `source` | string | The source for the invite, only supported on `roomType = 2` for `groups` and `circles` (only available with `circles-support` capability) |
-| `roomName` | string | Conversation name up to 255 characters (Not available for `roomType = 1`) |
+| field | type | Description |
+|--------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `roomType` | int | See [constants list](constants.md#conversation-types) |
+| `invite` | string | user id (`roomType = 1`), group id (`roomType = 2` - optional), circle id (`roomType = 2`, `source = 'circles'`], only available with `circles-support` capability)) |
+| `source` | string | The source for the invite, only supported on `roomType = 2` for `groups` and `circles` (only available with `circles-support` capability) |
+| `roomName` | string | Conversation name up to 255 characters (Not available for `roomType = 1`) |
+| `objectType` | string | Type of an object this room references, currently only allowed value is `room` to indicate the parent of a breakout room |
+| `objectId` | string | Id of an object this room references, room token is used for the parent of a breakout room |
* Response:
- Status code:
@@ -157,7 +161,7 @@
## Get breakout rooms
-Get all (for moderators and in case of "free selection) or the assigned breakout room
+Get all (for moderators and in case of "free selection") or the assigned breakout room
* Required capability: `breakout-rooms-v1`
* Method: `GET`
@@ -193,6 +197,8 @@ Get all (for moderators and in case of "free selection) or the assigned breakout
## Delete a conversation
+*Note:* Deleting a conversation that is the parent of breakout rooms, will also delete them.
+
* Method: `DELETE`
* Endpoint: `/room/{token}`
diff --git a/docs/participant.md b/docs/participant.md
index 69bfb0c7c..0a1ffdd08 100644
--- a/docs/participant.md
+++ b/docs/participant.md
@@ -44,6 +44,12 @@
## Add a participant to a conversation
+*Note:* Adding a participant to a breakout room will automatically add them to the parent room as well.
+
+*Note:* Only source users can be added directly to a breakout room.
+
+*Note:* Adding a participant to a breakout room, that is already a participant in another breakout room of the same parent will remove them from there.
+
* Method: `POST`
* Endpoint: `/room/{token}/participants`
* Data:
diff --git a/lib/Controller/BreakoutRoomController.php b/lib/Controller/BreakoutRoomController.php
index 659e0cd08..1e322fb16 100644
--- a/lib/Controller/BreakoutRoomController.php
+++ b/lib/Controller/BreakoutRoomController.php
@@ -88,6 +88,19 @@ class BreakoutRoomController extends AEnvironmentAwareController {
/**
* @NoAdminRequired
+ * @RequireLoggedInModeratorParticipant
+ */
+ public function applyAttendeeMap(string $attendeeMap): DataResponse {
+ try {
+ $this->breakoutRoomService->applyAttendeeMap($this->room, $attendeeMap);
+ } catch (InvalidArgumentException $e) {
+ return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
+ }
+ return new DataResponse([], Http::STATUS_OK);
+ }
+
+ /**
+ * @NoAdminRequired
* @RequireLoggedInParticipant
*
* @return DataResponse
diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php
index e66aff032..4272cde23 100644
--- a/lib/Controller/RoomController.php
+++ b/lib/Controller/RoomController.php
@@ -657,7 +657,7 @@ class RoomController extends AEnvironmentAwareController {
* @param string $source
* @return DataResponse
*/
- public function createRoom(int $roomType, string $invite = '', string $roomName = '', string $source = ''): DataResponse {
+ public function createRoom(int $roomType, string $invite = '', string $roomName = '', string $source = '', string $objectType = '', string $objectId = ''): DataResponse {
if ($roomType !== Room::TYPE_ONE_TO_ONE) {
/** @var IUser $user */
$user = $this->userManager->get($this->userId);
@@ -672,7 +672,7 @@ class RoomController extends AEnvironmentAwareController {
return $this->createOneToOneRoom($invite);
case Room::TYPE_GROUP:
if ($invite === '') {
- return $this->createEmptyRoom($roomName, false);
+ return $this->createEmptyRoom($roomName, false, $objectType, $objectId);
}
if ($source === 'circles') {
return $this->createCircleRoom($invite);
@@ -795,27 +795,65 @@ class RoomController extends AEnvironmentAwareController {
/**
* @NoAdminRequired
- *
- * @param string $roomName
- * @param bool $public
- * @return DataResponse
*/
- protected function createEmptyRoom(string $roomName, bool $public = true): DataResponse {
+ protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = ''): DataResponse {
$currentUser = $this->userManager->get($this->userId);
if (!$currentUser instanceof IUser) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$roomType = $public ? Room::TYPE_PUBLIC : Room::TYPE_GROUP;
+ /** @var Room|null $parentRoom */
+ $parentRoom = null;
+
+ if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
+ try {
+ $parentRoom = $this->manager->getRoomForUserByToken($objectId, $this->userId);
+ $parentRoomParticipant = $this->participantService->getParticipant($parentRoom, $this->userId);
+
+ if (!$parentRoomParticipant->hasModeratorPermissions()) {
+ return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
+ }
+ if ($parentRoom->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
+ return new DataResponse(['error' => 'mode'], Http::STATUS_BAD_REQUEST);
+ }
+
+ // Overwriting the type with the parent type.
+ $roomType = $parentRoom->getType();
+ } catch (RoomNotFoundException $e) {
+ return new DataResponse(['error' => 'room'], Http::STATUS_BAD_REQUEST);
+ } catch (ParticipantNotFoundException $e) {
+ return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
+ }
+ } elseif ($objectType !== '') {
+ return new DataResponse(['error' => 'object'], Http::STATUS_BAD_REQUEST);
+ }
// Create the room
try {
- $room = $this->roomService->createConversation($roomType, $roomName, $currentUser);
+ $room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId);
} catch (InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
- return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)), Http::STATUS_CREATED);
+ $currentParticipant = $this->participantService->getParticipant($room, $currentUser->getUID(), false);
+ if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
+ // Enforce the lobby state when breakout rooms are disabled
+ if ($parentRoom instanceof Room && $parentRoom->getBreakoutRoomStatus() === BreakoutRoom::STATUS_STOPPED) {
+ $this->roomService->setLobby($room, Webinary::LOBBY_NON_MODERATORS, null, false, false);
+ }
+
+ $participants = $this->participantService->getParticipantsForRoom($parentRoom);
+ $moderators = array_filter($participants, static function (Participant $participant) use ($currentParticipant) {
+ return $participant->hasModeratorPermissions()
+ && $participant->getAttendee()->getId() !== $currentParticipant->getAttendee()->getId();
+ });
+ if (!empty($moderators)) {
+ $this->breakoutRoomService->addModeratorsToBreakoutRooms([$room], $moderators);
+ }
+ }
+
+ return new DataResponse($this->formatRoom($room, $currentParticipant), Http::STATUS_CREATED);
}
/**
@@ -1084,6 +1122,11 @@ class RoomController extends AEnvironmentAwareController {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
+ if ($source !== 'users' && $this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
+ // Can only add users to breakout rooms
+ return new DataResponse(['error' => 'source'], Http::STATUS_BAD_REQUEST);
+ }
+
$participants = $this->participantService->getParticipantsForRoom($this->room);
$participantsByUserId = [];
$remoteParticipantsByFederatedId = [];
@@ -1187,6 +1230,28 @@ class RoomController extends AEnvironmentAwareController {
$addedBy = $this->userManager->get($this->userId);
+ if ($source === 'users' && $this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
+ $parentRoom = $this->manager->getRoomByToken($this->room->getObjectId());
+
+ // Also add to parent room in case the user is missing
+ try {
+ $this->participantService->getParticipantByActor(
+ $parentRoom,
+ Attendee::ACTOR_USERS,
+ $newParticipant
+ );
+ } catch (ParticipantNotFoundException $e) {
+ $this->participantService->addUsers($parentRoom, $participantsToAdd, $addedBy);
+ }
+
+ // Remove from previous breakout room in case the user is moved
+ try {
+ $this->breakoutRoomService->removeAttendeeFromBreakoutRoom($parentRoom, Attendee::ACTOR_USERS, $newParticipant);
+ } catch (\InvalidArgumentException $e) {
+ return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
+ }
+ }
+
// add the remaining users in batch
$this->participantService->addUsers($this->room, $participantsToAdd, $addedBy);
@@ -1675,6 +1740,11 @@ class RoomController extends AEnvironmentAwareController {
}
}
+ if ($this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
+ // Do not allow manual changing the lobby in breakout rooms
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
if (!$this->roomService->setLobby($this->room, $state, $timerDateTime)) {
return new DataResponse([]