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
328 lines
10 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
|
|
*
|
|
* @author Joas Schilling <coding@schilljs.com>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Talk\Service;
|
|
|
|
use InvalidArgumentException;
|
|
use OCA\Talk\Chat\ChatManager;
|
|
use OCA\Talk\Config;
|
|
use OCA\Talk\Manager;
|
|
use OCA\Talk\Model\Attendee;
|
|
use OCA\Talk\Model\BreakoutRoom;
|
|
use OCA\Talk\Participant;
|
|
use OCA\Talk\Room;
|
|
use OCA\Talk\Webinary;
|
|
use OCP\EventDispatcher\IEventDispatcher;
|
|
use OCP\Notification\IManager as INotificationManager;
|
|
use OCP\IL10N;
|
|
|
|
class BreakoutRoomService {
|
|
protected Config $config;
|
|
protected Manager $manager;
|
|
protected RoomService $roomService;
|
|
protected ParticipantService $participantService;
|
|
protected ChatManager $chatManager;
|
|
protected INotificationManager $notificationManager;
|
|
protected IEventDispatcher $dispatcher;
|
|
protected IL10N $l;
|
|
|
|
public function __construct(Config $config,
|
|
Manager $manager,
|
|
RoomService $roomService,
|
|
ParticipantService $participantService,
|
|
ChatManager $chatManager,
|
|
INotificationManager $notificationManager,
|
|
IEventDispatcher $dispatcher,
|
|
IL10N $l) {
|
|
$this->config = $config;
|
|
$this->manager = $manager;
|
|
$this->roomService = $roomService;
|
|
$this->participantService = $participantService;
|
|
$this->chatManager = $chatManager;
|
|
$this->notificationManager = $notificationManager;
|
|
$this->dispatcher = $dispatcher;
|
|
$this->l = $l;
|
|
}
|
|
|
|
/**
|
|
* @param Room $parent
|
|
* @param int $mode
|
|
* @psalm-param 0|1|2|3 $mode
|
|
* @param int $amount
|
|
* @param string $attendeeMap
|
|
* @return Room[]
|
|
* @throws InvalidArgumentException When the breakout rooms are configured already
|
|
*/
|
|
public function setupBreakoutRooms(Room $parent, int $mode, int $amount, string $attendeeMap): array {
|
|
if (!$this->config->isBreakoutRoomsEnabled()) {
|
|
throw new InvalidArgumentException('config');
|
|
}
|
|
|
|
if ($parent->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
|
|
throw new InvalidArgumentException('room');
|
|
}
|
|
|
|
if ($parent->getType() !== Room::TYPE_GROUP
|
|
&& $parent->getType() !== Room::TYPE_PUBLIC) {
|
|
// Can only do breakout rooms in group and public rooms
|
|
throw new InvalidArgumentException('room');
|
|
}
|
|
|
|
if ($parent->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
|
|
// Can not nest breakout rooms
|
|
throw new InvalidArgumentException('room');
|
|
}
|
|
|
|
if (!$this->roomService->setBreakoutRoomMode($parent, $mode)) {
|
|
throw new InvalidArgumentException('mode');
|
|
}
|
|
|
|
if ($amount < BreakoutRoom::MINIMUM_ROOM_AMOUNT) {
|
|
throw new InvalidArgumentException('amount');
|
|
}
|
|
|
|
if ($amount > BreakoutRoom::MAXIMUM_ROOM_AMOUNT) {
|
|
throw new InvalidArgumentException('amount');
|
|
}
|
|
|
|
if ($mode === BreakoutRoom::MODE_MANUAL) {
|
|
try {
|
|
$attendeeMap = json_decode($attendeeMap, true, 2, JSON_THROW_ON_ERROR);
|
|
} catch (\JsonException $e) {
|
|
throw new InvalidArgumentException('attendeeMap');
|
|
}
|
|
|
|
if (!empty($attendeeMap)) {
|
|
if (max($attendeeMap) >= $amount) {
|
|
throw new InvalidArgumentException('attendeeMap');
|
|
}
|
|
|
|
if (min($attendeeMap) < 0) {
|
|
throw new InvalidArgumentException('attendeeMap');
|
|
}
|
|
}
|
|
}
|
|
|
|
$breakoutRooms = $this->createBreakoutRooms($parent, $amount);
|
|
|
|
$participants = $this->participantService->getParticipantsForRoom($parent);
|
|
// TODO Removing any non-users here as breakout rooms only support logged in users in version 1
|
|
$participants = array_filter($participants, static fn (Participant $participant) => $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS);
|
|
|
|
$moderators = array_filter($participants, static fn (Participant $participant) => $participant->hasModeratorPermissions());
|
|
$this->addModeratorsToBreakoutRooms($breakoutRooms, $moderators);
|
|
|
|
$others = array_filter($participants, static fn (Participant $participant) => !$participant->hasModeratorPermissions());
|
|
if ($mode === BreakoutRoom::MODE_AUTOMATIC) {
|
|
// Shuffle the attendees, so they are not always distributed in the same way
|
|
shuffle($others);
|
|
|
|
$map = [];
|
|
foreach ($others as $index => $participant) {
|
|
$map[$index % $amount] ??= [];
|
|
$map[$index % $amount][] = $participant;
|
|
}
|
|
|
|
$this->addOthersToBreakoutRooms($breakoutRooms, $map);
|
|
} elseif ($mode === BreakoutRoom::MODE_MANUAL) {
|
|
$map = [];
|
|
foreach ($others as $participant) {
|
|
$roomNumber = $attendeeMap[$participant->getAttendee()->getId()] ?? null;
|
|
if ($roomNumber === null) {
|
|
continue;
|
|
}
|
|
|
|
$roomNumber = (int) $roomNumber;
|
|
|
|
$map[$roomNumber] ??= [];
|
|
$map[$roomNumber][] = $participant;
|
|
}
|
|
|
|
$this->addOthersToBreakoutRooms($breakoutRooms, $map);
|
|
}
|
|
|
|
|
|
return $breakoutRooms;
|
|
}
|
|
|
|
/**
|
|
* @param Room[] $rooms
|
|
* @param Participant[] $moderators
|
|
*/
|
|
protected function addModeratorsToBreakoutRooms(array $rooms, array $moderators): void {
|
|
$moderatorsToAdd = [];
|
|
foreach ($moderators as $moderator) {
|
|
$attendee = $moderator->getAttendee();
|
|
|
|
$moderatorsToAdd[] = [
|
|
'actorType' => $attendee->getActorType(),
|
|
'actorId' => $attendee->getActorId(),
|
|
'displayName' => $attendee->getDisplayName(),
|
|
'participantType' => $attendee->getParticipantType(),
|
|
];
|
|
}
|
|
|
|
foreach ($rooms as $room) {
|
|
$this->participantService->addUsers($room, $moderatorsToAdd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array $rooms
|
|
* @param Participant[][] $participantsMap
|
|
*/
|
|
protected function addOthersToBreakoutRooms(array $rooms, array $participantsMap): void {
|
|
foreach ($rooms as $roomNumber => $room) {
|
|
$toAdd = [];
|
|
|
|
$participants = $participantsMap[$roomNumber] ?? [];
|
|
foreach ($participants as $participant) {
|
|
$attendee = $participant->getAttendee();
|
|
|
|
$toAdd[] = [
|
|
'actorType' => $attendee->getActorType(),
|
|
'actorId' => $attendee->getActorId(),
|
|
'displayName' => $attendee->getDisplayName(),
|
|
'participantType' => $attendee->getParticipantType(),
|
|
];
|
|
}
|
|
|
|
if (empty($toAdd)) {
|
|
continue;
|
|
}
|
|
|
|
$this->participantService->addUsers($room, $toAdd);
|
|
}
|
|
}
|
|
|
|
protected function createBreakoutRooms(Room $parent, int $amount): array {
|
|
// Safety caution cleaning up potential orphan rooms
|
|
$this->deleteBreakoutRooms($parent);
|
|
|
|
// TRANSLATORS Label for the breakout rooms, this is not a plural! The result will be "Room 1", "Room 2", "Room 3", ...
|
|
$label = $this->l->t('Room {number}');
|
|
|
|
$rooms = [];
|
|
for ($i = 1; $i <= $amount; $i++) {
|
|
$breakoutRoom = $this->roomService->createConversation(
|
|
$parent->getType(),
|
|
str_replace('{number}', (string) $i, $label),
|
|
null,
|
|
BreakoutRoom::PARENT_OBJECT_TYPE,
|
|
$parent->getToken()
|
|
);
|
|
|
|
$this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NON_MODERATORS, null, false, false);
|
|
|
|
$rooms[] = $breakoutRoom;
|
|
}
|
|
|
|
return $rooms;
|
|
}
|
|
|
|
public function removeBreakoutRooms(Room $parent): void {
|
|
$this->deleteBreakoutRooms($parent);
|
|
$this->roomService->setBreakoutRoomMode($parent, BreakoutRoom::MODE_NOT_CONFIGURED);
|
|
}
|
|
|
|
protected function deleteBreakoutRooms(Room $parent): void {
|
|
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
|
|
foreach ($breakoutRooms as $breakoutRoom) {
|
|
$this->roomService->deleteRoom($breakoutRoom);
|
|
}
|
|
}
|
|
|
|
public function broadcastChatMessage(Room $parent, Participant $participant, string $message): void {
|
|
if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
|
|
throw new \InvalidArgumentException('mode');
|
|
}
|
|
|
|
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
|
|
$attendeeType = $participant->getAttendee()->getActorType();
|
|
$attendeeId = $participant->getAttendee()->getActorId();
|
|
$creationDateTime = new \DateTime();
|
|
|
|
$shouldFlush = $this->notificationManager->defer();
|
|
try {
|
|
foreach ($breakoutRooms as $breakoutRoom) {
|
|
$breakoutParticipant = $this->participantService->getParticipantByActor($breakoutRoom, $attendeeType, $attendeeId);
|
|
$this->chatManager->sendMessage($breakoutRoom, $breakoutParticipant, $attendeeType, $attendeeId, $message, $creationDateTime, null, '', false);
|
|
}
|
|
} finally {
|
|
if ($shouldFlush) {
|
|
$this->notificationManager->flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
public function setBreakoutRoomAssistanceRequest(Room $breakoutRoom, int $status): void {
|
|
if ($breakoutRoom->getObjectType() !== BreakoutRoom::PARENT_OBJECT_TYPE) {
|
|
throw new \InvalidArgumentException('room');
|
|
}
|
|
|
|
if ($breakoutRoom->getLobbyState() !== Webinary::LOBBY_NONE) {
|
|
throw new \InvalidArgumentException('room');
|
|
}
|
|
|
|
if (!in_array($status, [
|
|
BreakoutRoom::STATUS_ASSISTANCE_RESET,
|
|
BreakoutRoom::STATUS_ASSISTANCE_REQUESTED,
|
|
], true)) {
|
|
throw new \InvalidArgumentException('status');
|
|
}
|
|
|
|
$this->roomService->setBreakoutRoomStatus($breakoutRoom, $status);
|
|
}
|
|
|
|
public function startBreakoutRooms(Room $parent): void {
|
|
if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
|
|
throw new \InvalidArgumentException('mode');
|
|
}
|
|
|
|
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
|
|
foreach ($breakoutRooms as $breakoutRoom) {
|
|
$this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NONE, null);
|
|
}
|
|
|
|
$this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STARTED);
|
|
|
|
// FIXME missing to send the signaling messages so participants are moved
|
|
}
|
|
|
|
public function stopBreakoutRooms(Room $parent): void {
|
|
if ($parent->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
|
|
throw new \InvalidArgumentException('mode');
|
|
}
|
|
|
|
$breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken());
|
|
foreach ($breakoutRooms as $breakoutRoom) {
|
|
$this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NON_MODERATORS, null);
|
|
}
|
|
|
|
$this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STOPPED);
|
|
|
|
// FIXME missing to send the signaling messages so participants are moved back
|
|
}
|
|
}
|