You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

328 lines
10 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
  5. *
  6. * @author Joas Schilling <coding@schilljs.com>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OCA\Talk\Service;
  25. use InvalidArgumentException;
  26. use OCA\Talk\Chat\ChatManager;
  27. use OCA\Talk\Config;
  28. use OCA\Talk\Manager;
  29. use OCA\Talk\Model\Attendee;
  30. use OCA\Talk\Model\BreakoutRoom;
  31. use OCA\Talk\Participant;
  32. use OCA\Talk\Room;
  33. use OCA\Talk\Webinary;
  34. use OCP\EventDispatcher\IEventDispatcher;
  35. use OCP\Notification\IManager as INotificationManager;
  36. use OCP\IL10N;
  37. class BreakoutRoomService {
  38. protected Config $config;
  39. protected Manager $manager;
  40. protected RoomService $roomService;
  41. protected ParticipantService $participantService;
  42. protected ChatManager $chatManager;
  43. protected INotificationManager $notificationManager;
  44. protected IEventDispatcher $dispatcher;
  45. protected IL10N $l;
  46. public function __construct(Config $config,
  47. Manager $manager,
  48. RoomService $roomService,
  49. ParticipantService $participantService,
  50. ChatManager $chatManager,
  51. INotificationManager $notificationManager,
  52. IEventDispatcher $dispatcher,
  53. IL10N $l) {
  54. $this->config = $config;
  55. $this->manager = $manager;
  56. $this->roomService = $roomService;
  57. $this->participantService = $participantService;
  58. $this->chatManager = $chatManager;
  59. $this->notificationManager = $notificationManager;
  60. $this->dispatcher = $dispatcher;
  61. $this->l = $l;
  62. }
  63. /**
  64. * @param Room $parent
  65. * @param int $mode
  66. * @psalm-param 0|1|2|3 $mode
  67. * @param int $amount
  68. * @param string $attendeeMap
  69. * @return Room[]
  70. * @throws InvalidArgumentException When the breakout rooms are configured already
  71. */
  72. public function setupBreakoutRooms(Room $parent, int $mode, int $amount, string $attendeeMap): array {
  73. if (!$this->config->isBreakoutRoomsEnabled()) {
  74. throw new InvalidArgumentException('config');
  75. }
  76. if ($parent->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
  77. throw new InvalidArgumentException('room');
  78. }
  79. if ($parent->getType() !== Room::TYPE_GROUP
  80. && $parent->getType() !== Room::TYPE_PUBLIC) {
  81. // Can only do breakout rooms in group and public rooms
  82. throw new InvalidArgumentException('room');
  83. }
  84. if ($parent->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  85. // Can not nest breakout rooms
  86. throw new InvalidArgumentException('room');
  87. }
  88. if (!$this->roomService->setBreakoutRoomMode($parent, $mode)) {
  89. throw new InvalidArgumentException('mode');
  90. }
  91. if ($amount < BreakoutRoom::MINIMUM_ROOM_AMOUNT) {
  92. throw new InvalidArgumentException('amount');
  93. }
  94. if ($amount > BreakoutRoom::MAXIMUM_ROOM_AMOUNT) {
  95. throw new InvalidArgumentException('amount');
  96. }
  97. if ($mode === BreakoutRoom::MODE_MANUAL) {
  98. try {
  99. $attendeeMap = json_decode($attendeeMap, true, 2, JSON_THROW_ON_ERROR);
  100. } catch (\JsonException $e) {
  101. throw new InvalidArgumentException('attendeeMap');
  102. }
  103. if (!empty($attendeeMap)) {
  104. if (max($attendeeMap) >= $amount) {
  105. throw new InvalidArgumentException('attendeeMap');
  106. }
  107. if (min($attendeeMap) < 0) {
  108. throw new InvalidArgumentException('attendeeMap');
  109. }
  110. }
  111. }
  112. $breakoutRooms = $this->createBreakoutRooms($parent, $amount);
  113. $participants = $this->participantService->getParticipantsForRoom($parent);
  114. // TODO Removing any non-users here as breakout rooms only support logged in users in version 1
  115. $participants = array_filter($participants, static fn (Participant $participant) => $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS);
  116. $moderators = array_filter($participants, static fn (Participant $participant) => $participant->hasModeratorPermissions());
  117. $this->addModeratorsToBreakoutRooms($breakoutRooms, $moderators);
  118. $others = array_filter($participants, static fn (Participant $participant) => !$participant->hasModeratorPermissions());
  119. if ($mode === BreakoutRoom::MODE_AUTOMATIC) {
  120. // Shuffle the attendees, so they are not always distributed in the same way
  121. shuffle($others);
  122. $map = [];
  123. foreach ($others as $index => $participant) {
  124. $map[$index % $amount] ??= [];
  125. $map[$index % $amount][] = $participant;
  126. }
  127. $this->addOthersToBreakoutRooms($breakoutRooms, $map);
  128. } elseif ($mode === BreakoutRoom::MODE_MANUAL) {
  129. $map = [];
  130. foreach ($others as $participant) {
  131. $roomNumber = $attendeeMap[$participant->getAttendee()->getId()] ?? null;
  132. if ($roomNumber === null) {
  133. continue;
  134. }
  135. $roomNumber = (int) $roomNumber;
  136. $map[$roomNumber] ??= [];
  137. $map[$roomNumber][] = $participant;
  138. }
  139. $this->addOthersToBreakoutRooms($breakoutRooms, $map);
  140. }
  141. return $breakoutRooms;
  142. }
  143. /**
  144. * @param Room[] $rooms
  145. * @param Participant[] $moderators
  146. */
  147. protected function addModeratorsToBreakoutRooms(array $rooms, array $moderators): void {
  148. $moderatorsToAdd = [];
  149. foreach ($moderators as $moderator) {
  150. $attendee = $moderator->getAttendee();
  151. $moderatorsToAdd[] = [
  152. 'actorType' => $attendee->getActorType(),
  153. 'actorId' => $attendee->getActorId(),
  154. 'displayName' => $attendee->getDisplayName(),
  155. 'participantType' => $attendee->getParticipantType(),
  156. ];
  157. }
  158. foreach ($rooms as $room) {
  159. $this->participantService->addUsers($room, $moderatorsToAdd);
  160. }
  161. }
  162. /**
  163. * @param array $rooms
  164. * @param Participant[][] $participantsMap
  165. */
  166. protected function addOthersToBreakoutRooms(array $rooms, array $participantsMap): void {
  167. foreach ($rooms as $roomNumber => $room) {
  168. $toAdd = [];
  169. $participants = $participantsMap[$roomNumber] ?? [];
  170. foreach ($participants as $participant) {
  171. $attendee = $participant->getAttendee();
  172. $toAdd[] = [
  173. 'actorType' => $attendee->getActorType(),
  174. 'actorId' => $attendee->getActorId(),
  175. 'displayName' => $attendee->getDisplayName(),
  176. 'participantType' => $attendee->getParticipantType(),
  177. ];
  178. }
  179. if (empty($toAdd)) {
  180. continue;
  181. }
  182. $this->participantService->addUsers($room, $toAdd);
  183. }
  184. }
  185. protected function createBreakoutRooms(Room $parent, int $amount): array {
  186. // Safety caution cleaning up potential orphan rooms
  187. $this->deleteBreakoutRooms($parent);
  188. // TRANSLATORS Label for the breakout rooms, this is not a plural! The result will be "Room 1", "Room 2", "Room 3", ...
  189. $label = $this->l->t('Room {number}');
  190. $rooms = [];
  191. for ($i = 1; $i <= $amount; $i++) {
  192. $breakoutRoom = $this->roomService->createConversation(
  193. $parent->getType(),
  194. str_replace('{number}', (string) $i, $label),
  195. null,
  196. BreakoutRoom::PARENT_OBJECT_TYPE,
  197. $parent->getToken()
  198. );
  199. $this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NON_MODERATORS, null, false, false);
  200. $rooms[] = $breakoutRoom;
  201. }
  202. return $rooms;
  203. }
  204. public function removeBreakoutRooms(Room $parent): void {
  205. $this->deleteBreakoutRooms($parent);
  206. $this->roomService->setBreakoutRoomMode($parent, BreakoutRoom::MODE_NOT_CONFIGURED);
  207. }
  208. protected function deleteBreakoutRooms(Room $parent): void {
  209. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
  210. foreach ($breakoutRooms as $breakoutRoom) {
  211. $this->roomService->deleteRoom($breakoutRoom);
  212. }
  213. }
  214. public function broadcastChatMessage(Room $parent, Participant $participant, string $message): void {
  215. if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
  216. throw new \InvalidArgumentException('mode');
  217. }
  218. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
  219. $attendeeType = $participant->getAttendee()->getActorType();
  220. $attendeeId = $participant->getAttendee()->getActorId();
  221. $creationDateTime = new \DateTime();
  222. $shouldFlush = $this->notificationManager->defer();
  223. try {
  224. foreach ($breakoutRooms as $breakoutRoom) {
  225. $breakoutParticipant = $this->participantService->getParticipantByActor($breakoutRoom, $attendeeType, $attendeeId);
  226. $this->chatManager->sendMessage($breakoutRoom, $breakoutParticipant, $attendeeType, $attendeeId, $message, $creationDateTime, null, '', false);
  227. }
  228. } finally {
  229. if ($shouldFlush) {
  230. $this->notificationManager->flush();
  231. }
  232. }
  233. }
  234. public function setBreakoutRoomAssistanceRequest(Room $breakoutRoom, int $status): void {
  235. if ($breakoutRoom->getObjectType() !== BreakoutRoom::PARENT_OBJECT_TYPE) {
  236. throw new \InvalidArgumentException('room');
  237. }
  238. if ($breakoutRoom->getLobbyState() !== Webinary::LOBBY_NONE) {
  239. throw new \InvalidArgumentException('room');
  240. }
  241. if (!in_array($status, [
  242. BreakoutRoom::STATUS_ASSISTANCE_RESET,
  243. BreakoutRoom::STATUS_ASSISTANCE_REQUESTED,
  244. ], true)) {
  245. throw new \InvalidArgumentException('status');
  246. }
  247. $this->roomService->setBreakoutRoomStatus($breakoutRoom, $status);
  248. }
  249. public function startBreakoutRooms(Room $parent): void {
  250. if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
  251. throw new \InvalidArgumentException('mode');
  252. }
  253. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
  254. foreach ($breakoutRooms as $breakoutRoom) {
  255. $this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NONE, null);
  256. }
  257. $this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STARTED);
  258. // FIXME missing to send the signaling messages so participants are moved
  259. }
  260. public function stopBreakoutRooms(Room $parent): void {
  261. if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
  262. throw new \InvalidArgumentException('mode');
  263. }
  264. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
  265. foreach ($breakoutRooms as $breakoutRoom) {
  266. $this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NON_MODERATORS, null);
  267. }
  268. $this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STOPPED);
  269. // FIXME missing to send the signaling messages so participants are moved back
  270. }
  271. }