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

<?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
}
}