<NME> ParticipantService.php
<BEF> <?php

declare(strict_types=1);
/**
 * @copyright Copyright (c) 2020 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 OCA\Talk\Manager;
use OCA\Circles\CirclesManager;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Member;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Config;
use OCA\Talk\Events\AddParticipantsEvent;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Events\AttendeesRemovedEvent;
use OCA\Talk\Events\ChatEvent;
use OCA\Talk\Events\DuplicatedParticipantEvent;
use OCA\Talk\Events\EndCallForEveryoneEvent;
use OCA\Talk\Events\JoinRoomGuestEvent;
use OCA\Talk\Events\JoinRoomUserEvent;
use OCA\Talk\Events\ModifyEveryoneEvent;
use OCA\Talk\Events\ModifyParticipantEvent;
use OCA\Talk\Events\ParticipantEvent;
use OCA\Talk\Events\RemoveParticipantEvent;
use OCA\Talk\Events\RemoveUserEvent;
use OCA\Talk\Events\RoomEvent;
use OCA\Talk\Events\SendCallNotificationEvent;
use OCA\Talk\Events\SilentModifyParticipantEvent;
use OCA\Talk\Exceptions\ForbiddenException;
use OCA\Talk\Exceptions\InvalidPasswordException;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\UnauthorizedException;
use OCA\Talk\Federation\FederationManager;
use OCA\Talk\Federation\Notifications;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper;
use OCA\Talk\Model\SelectHelper;
use OCA\Talk\Model\Session;
use OCA\Talk\Model\SessionMapper;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Webinary;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use OCP\Server;

class ParticipantService {
	protected IConfig $serverConfig;
	protected Config $talkConfig;
	protected AttendeeMapper $attendeeMapper;
	protected SessionMapper $sessionMapper;
	protected SessionService $sessionService;
	private ISecureRandom $secureRandom;
	protected IDBConnection $connection;
	private IEventDispatcher $dispatcher;
	private IUserManager $userManager;
	private IGroupManager $groupManager;
	private MembershipService $membershipService;
	private Notifications $notifications;
	private ITimeFactory $timeFactory;
	private ICacheFactory $cacheFactory;

	protected array $userCache;
	protected array $sessionCache;

	public function __construct(IConfig $serverConfig,
								Config $talkConfig,
								AttendeeMapper $attendeeMapper,
								SessionMapper $sessionMapper,
								SessionService $sessionService,
								ISecureRandom $secureRandom,
								IDBConnection $connection,
								IEventDispatcher $dispatcher,
								IUserManager $userManager,
								IGroupManager $groupManager,
								MembershipService $membershipService,
								Notifications $notifications,
								ITimeFactory $timeFactory,
								ICacheFactory $cacheFactory) {
		$this->serverConfig = $serverConfig;
		$this->talkConfig = $talkConfig;
		$this->attendeeMapper = $attendeeMapper;
		$this->sessionMapper = $sessionMapper;
		$this->sessionService = $sessionService;
		$this->secureRandom = $secureRandom;
		$this->connection = $connection;
		$this->dispatcher = $dispatcher;
		$this->userManager = $userManager;
		$this->groupManager = $groupManager;
		$this->membershipService = $membershipService;
		$this->timeFactory = $timeFactory;
		$this->cacheFactory = $cacheFactory;
		$this->notifications = $notifications;
	}

	public function updateParticipantType(Room $room, Participant $participant, int $participantType): void {
		$attendee = $participant->getAttendee();

		if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) {
			// Can not promote/demote groups
			return;
		}

		$oldType = $attendee->getParticipantType();

		$event = new ModifyParticipantEvent($room, $participant, 'type', $participantType, $oldType);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_TYPE_SET, $event);

		$attendee->setParticipantType($participantType);
		$this->attendeeMapper->update($attendee);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event);
	}

	/**
	 * @throws Exception
	 * @throws ForbiddenException
	 */
	public function updatePermissions(Room $room, Participant $participant, string $method, int $newPermissions): bool {
		if ($room->getType() === Room::TYPE_ONE_TO_ONE) {
			return false;
		}

		if ($participant->hasModeratorPermissions()) {
			throw new ForbiddenException();
		}

		$attendee = $participant->getAttendee();

		if ($attendee->getActorType() === Attendee::ACTOR_GROUPS || $attendee->getActorType() === Attendee::ACTOR_CIRCLES) {
			// Can not set publishing permissions for those actor types
			return false;
		}

		$oldPermissions = $participant->getPermissions();
		if ($method === Attendee::PERMISSIONS_MODIFY_SET) {
			if ($newPermissions !== Attendee::PERMISSIONS_DEFAULT) {
				// Make sure the custom flag is set when not setting to default permissions
				$newPermissions |= Attendee::PERMISSIONS_CUSTOM;
			}
		} elseif ($method === Attendee::PERMISSIONS_MODIFY_ADD) {
			$newPermissions = $oldPermissions | $newPermissions;
		} elseif ($method === Attendee::PERMISSIONS_MODIFY_REMOVE) {
			$newPermissions = $oldPermissions & ~$newPermissions;
		} else {
			return false;
		}

		$event = new ModifyParticipantEvent($room, $participant, 'permissions', $newPermissions, $oldPermissions);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_PERMISSIONS_SET, $event);

		$attendee->setPermissions($newPermissions);
		$this->attendeeMapper->update($attendee);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, $event);

		return true;
	}

	public function updateAllPermissions(Room $room, string $method, int $newState): void {
		$this->attendeeMapper->modifyPermissions($room->getId(), $method, $newState);
	}

	public function updateLastReadMessage(Participant $participant, int $lastReadMessage): void {
		$attendee = $participant->getAttendee();
		$attendee->setLastReadMessage($lastReadMessage);
		$this->attendeeMapper->update($attendee);
	}

	public function updateFavoriteStatus(Participant $participant, bool $isFavorite): void {
		$attendee = $participant->getAttendee();
		$attendee->setFavorite($isFavorite);
		$this->attendeeMapper->update($attendee);
	}

	/**
	 * @param Participant $participant
	 * @param int $level
	 * @throws \InvalidArgumentException When the notification level is invalid
	 */
	public function updateNotificationLevel(Participant $participant, int $level): void {
		if (!\in_array($level, [
			Participant::NOTIFY_ALWAYS,
			Participant::NOTIFY_MENTION,
			Participant::NOTIFY_NEVER
		], true)) {
			throw new \InvalidArgumentException('Invalid notification level');
		}

		$attendee = $participant->getAttendee();
		$attendee->setNotificationLevel($level);
		$this->attendeeMapper->update($attendee);
	}

	/**
	 * @param Participant $participant
	 * @param int $level
	 */
	public function updateNotificationCalls(Participant $participant, int $level): void {
		if (!\in_array($level, [
			Participant::NOTIFY_CALLS_OFF,
			Participant::NOTIFY_CALLS_ON,
		], true)) {
			throw new \InvalidArgumentException('Invalid notification level');
		}

		$attendee = $participant->getAttendee();
		$attendee->setNotificationCalls($level);
		$this->attendeeMapper->update($attendee);
	}

	/**
	 * @param RoomService $roomService
	 * @param Room $room
	 * @param IUser $user
	 * @param string $password
	 * @param bool $passedPasswordProtection
	 * @return Participant
	 * @throws InvalidPasswordException
	 * @throws UnauthorizedException
	 */
	public function joinRoom(RoomService $roomService, Room $room, IUser $user, string $password, bool $passedPasswordProtection = false): Participant {
		$event = new JoinRoomUserEvent($room, $user, $password, $passedPasswordProtection);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_CONNECT, $event);

		if ($event->getCancelJoin() === true) {
			$this->removeUser($room, $user, Room::PARTICIPANT_LEFT);
			throw new UnauthorizedException('Participant is not allowed to join');
		}

		try {
			$attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID());
		} catch (DoesNotExistException $e) {
			// queried here to avoid loop deps
			$manager = Server::get(Manager::class);
			$isListableByUser = $manager->isRoomListableByUser($room, $user->getUID());

			if (!$isListableByUser && !$event->getPassedPasswordProtection() && !$roomService->verifyPassword($room, $password)['result']) {
				throw new InvalidPasswordException('Provided password is invalid');
			}

			// User joining a group or public call through listing
			if (($room->getType() === Room::TYPE_GROUP || $room->getType() === Room::TYPE_PUBLIC) && $isListableByUser) {
				$this->addUsers($room, [[
					'actorType' => Attendee::ACTOR_USERS,
					'actorId' => $user->getUID(),
					'displayName' => $user->getDisplayName(),
					// need to use "USER" here, because "USER_SELF_JOINED" only works for public calls
					'participantType' => Participant::USER,
				]], $user);
			} elseif ($room->getType() === Room::TYPE_PUBLIC) {
				// User joining a public room, without being invited
				$this->addUsers($room, [[
					'actorType' => Attendee::ACTOR_USERS,
					'actorId' => $user->getUID(),
					'displayName' => $user->getDisplayName(),
					'participantType' => Participant::USER_SELF_JOINED,
				]], $user);
			} else {
				// shouldn't happen unless some code called joinRoom without previous checks
				throw new UnauthorizedException('Participant is not allowed to join');
			}

			$attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID());
		}

		$session = $this->sessionService->createSessionForAttendee($attendee);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_CONNECT, $event);

		return new Participant($room, $attendee, $session);
	}

	/**
	 * @param RoomService $roomService
	 * @param Room $room
	 * @param string $password
	 * @param bool $passedPasswordProtection
	 * @param ?Participant $previousParticipant
	 * @return Participant
	 * @throws InvalidPasswordException
	 * @throws UnauthorizedException
	 */
	public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $password, bool $passedPasswordProtection = false, ?Participant $previousParticipant = null): Participant {
		$event = new JoinRoomGuestEvent($room, $password, $passedPasswordProtection);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_GUEST_CONNECT, $event);

		if ($event->getCancelJoin()) {
			throw new UnauthorizedException('Participant is not allowed to join');
		}

		if (!$event->getPassedPasswordProtection() && !$roomService->verifyPassword($room, $password)['result']) {
			throw new InvalidPasswordException();
		}

		$lastMessage = 0;
		if ($room->getLastMessage() instanceof IComment) {
			$lastMessage = (int) $room->getLastMessage()->getId();
		}

		if ($previousParticipant instanceof Participant) {
			$attendee = $previousParticipant->getAttendee();
		} else {
			$randomActorId = $this->secureRandom->generate(255);

			$attendee = new Attendee();
			$attendee->setRoomId($room->getId());
			$attendee->setActorType(Attendee::ACTOR_GUESTS);
			$attendee->setActorId($randomActorId);
			$attendee->setParticipantType(Participant::GUEST);
			$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);
			$attendee->setLastReadMessage($lastMessage);
			$this->attendeeMapper->insert($attendee);

			$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);
			$this->dispatcher->dispatchTyped($attendeeEvent);
		}

		$session = $this->sessionService->createSessionForAttendee($attendee);

		if (!$previousParticipant instanceof Participant) {
			// Update the random guest id
			$attendee->setActorId(sha1($session->getSessionId()));
			$this->attendeeMapper->update($attendee);
		}

		$this->dispatcher->dispatch(Room::EVENT_AFTER_GUEST_CONNECT, $event);

		return new Participant($room, $attendee, $session);
	}

	/**
	 * @param Room $room
	 * @param array $participants
	 * @param IUser|null $addedBy User that is attempting to add these users (must be set for federated users to be added)
	 * @throws \Exception thrown if $addedBy is not set when adding a federated user
	 */
	public function addUsers(Room $room, array $participants, ?IUser $addedBy = null): void {
		if (empty($participants)) {
			return;
		}
		$event = new AddParticipantsEvent($room, $participants, true);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_USERS_ADD, $event);

		$lastMessage = 0;
		if ($room->getLastMessage() instanceof IComment) {
			$lastMessage = (int) $room->getLastMessage()->getId();
		}

		$attendees = [];
		foreach ($participants as $participant) {
			$readPrivacy = Participant::PRIVACY_PUBLIC;
			if ($participant['actorType'] === Attendee::ACTOR_USERS) {
				$readPrivacy = $this->talkConfig->getUserReadPrivacy($participant['actorId']);
			} elseif ($participant['actorType'] === Attendee::ACTOR_FEDERATED_USERS) {
				if ($addedBy === null) {
					throw new \Exception('$addedBy must be set to add a federated user');
				}
				$participant['accessToken'] = $this->secureRandom->generate(
					FederationManager::TOKEN_LENGTH,
					ISecureRandom::CHAR_HUMAN_READABLE
				);
			}

			$attendee = new Attendee();
			$attendee->setRoomId($room->getId());
			$attendee->setActorType($participant['actorType']);
			$attendee->setActorId($participant['actorId']);
			if (isset($participant['displayName'])) {
				$attendee->setDisplayName($participant['displayName']);
			}
			if (isset($participant['accessToken'])) {
				$attendee->setAccessToken($participant['accessToken']);
			}
			if (isset($participant['remoteId'])) {
				$attendee->setRemoteId($participant['remoteId']);
			}
			$attendee->setParticipantType($participant['participantType'] ?? Participant::USER);
			$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);
			$attendee->setLastReadMessage($lastMessage);
			$attendee->setReadPrivacy($readPrivacy);
			try {
				$entity = $this->attendeeMapper->insert($attendee);
				$attendees[] = $attendee;

				if ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
					$this->notifications->sendRemoteShare((string) $entity->getId(), $participant['accessToken'], $participant['actorId'], $addedBy->getDisplayName(), $addedBy->getCloudId(), 'user', $room, $this->getHighestPermissionAttendee($room));
				}
			} catch (Exception $e) {
				if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
					throw $e;
				}
			}
		}

		if (!empty($attendees)) {
			$attendeeEvent = new AttendeesAddedEvent($room, $attendees);
			$this->dispatcher->dispatchTyped($attendeeEvent);

			$this->dispatcher->dispatch(Room::EVENT_AFTER_USERS_ADD, $event);

			$lastMessage = $event->getLastMessage();
			if ($lastMessage instanceof IComment) {
				$this->updateRoomLastMessage($room, $lastMessage);
			}
		}
	}

	protected function updateRoomLastMessage(Room $room, IComment $message): void {
		/** @var RoomService $roomService */
		$roomService = Server::get(RoomService::class);
		$roomService->setLastMessage($room, $message);

		$lastMessageCache = $this->cacheFactory->createDistributed('talk/lastmsgid');
		$lastMessageCache->remove($room->getToken());
		$unreadCountCache = $this->cacheFactory->createDistributed('talk/unreadcount');
		$unreadCountCache->clear($room->getId() . '-');

		$event = new ChatEvent($room, $message);
		$this->dispatcher->dispatch(ChatManager::EVENT_AFTER_MULTIPLE_SYSTEM_MESSAGE_SEND, $event);
	}

	public function getHighestPermissionAttendee(Room $room): ?Attendee {
		try {
			$roomOwners = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::OWNER]);

			if (!empty($roomOwners)) {
				foreach ($roomOwners as $owner) {
					if ($owner->getActorType() === Attendee::ACTOR_USERS) {
						return $owner;
					}
				}
			}
			$roomModerators = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::MODERATOR]);
			if (!empty($roomOwners)) {
				foreach ($roomModerators as $moderator) {
					if ($moderator->getActorType() === Attendee::ACTOR_USERS) {
						return $moderator;
					}
				}
			}
		} catch (Exception $e) {
		}
		return null;
	}

	/**
	 * @param Room $room
	 * @param IGroup $group
	 * @param Participant[] $existingParticipants
	 */
	public function addGroup(Room $room, IGroup $group, array $existingParticipants = []): void {
		$usersInGroup = $group->getUsers();

		if (empty($existingParticipants)) {
			$existingParticipants = $this->getParticipantsForRoom($room);
		}

		$participantsByUserId = [];
		foreach ($existingParticipants as $participant) {
			if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
				$participantsByUserId[$participant->getAttendee()->getActorId()] = $participant;
			}
		}

		$newParticipants = [];
		foreach ($usersInGroup as $user) {
			$existingParticipant = $participantsByUserId[$user->getUID()] ?? null;
			if ($existingParticipant instanceof Participant) {
				if ($existingParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {
					$this->updateParticipantType($room, $existingParticipant, Participant::USER);
				}

				// Participant is already in the conversation, so skip them.
				continue;
			}

			$newParticipants[] = [
				'actorType' => Attendee::ACTOR_USERS,
				'actorId' => $user->getUID(),
				'displayName' => $user->getDisplayName(),
			];
		}

		try {
			$this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_GROUPS, $group->getGID());
		} catch (DoesNotExistException $e) {
			$attendee = new Attendee();
			$attendee->setRoomId($room->getId());
			$attendee->setActorType(Attendee::ACTOR_GROUPS);
			$attendee->setActorId($group->getGID());
			$attendee->setDisplayName($group->getDisplayName());
			$attendee->setParticipantType(Participant::USER);
			$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);
			$attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE);
			$this->attendeeMapper->insert($attendee);

			$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);
			$this->dispatcher->dispatchTyped($attendeeEvent);
		}

		$this->addUsers($room, $newParticipants);
	}

	/**
	 * @param string $circleId
	 * @param string $userId
	 * @return Circle
	 * @throws ParticipantNotFoundException
	 */
	public function getCircle(string $circleId, string $userId): Circle {
		try {
			$circlesManager = Server::get(CirclesManager::class);
			$federatedUser = $circlesManager->getFederatedUser($userId, Member::TYPE_USER);
			$federatedUser->getLink($circleId);
		} catch (\Exception $e) {
			throw new ParticipantNotFoundException('Circle not found or not a member');
		}

		$circlesManager->startSession($federatedUser);
		try {
			$circle = $circlesManager->getCircle($circleId);
			$circlesManager->stopSession();
			return $circle;
		} catch (\Exception $e) {
		}

		$circlesManager->stopSession();
		throw new ParticipantNotFoundException('Circle not found or not a member');
	}

	/**
	 * @param Room $room
	 * @param Circle $circle
	 * @param Participant[] $existingParticipants
	 */
	public function addCircle(Room $room, Circle $circle, array $existingParticipants = []): void {
		$membersInCircle = $circle->getInheritedMembers();

		if (empty($existingParticipants)) {
			$existingParticipants = $this->getParticipantsForRoom($room);
		}

		$participantsByUserId = [];
		foreach ($existingParticipants as $participant) {
			if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
				$participantsByUserId[$participant->getAttendee()->getActorId()] = $participant;
			}
		}

		$newParticipants = [];
		foreach ($membersInCircle as $member) {
			/** @var Member $member */
			if ($member->getUserType() !== Member::TYPE_USER || $member->getUserId() === '') {
				// Not a user?
				continue;
			}

			if ($member->getStatus() !== Member::STATUS_INVITED && $member->getStatus() !== Member::STATUS_MEMBER) {
				// Only allow invited and regular members
				continue;
			}

			$user = $this->userManager->get($member->getUserId());
			if (!$user instanceof IUser) {
				continue;
			}

			$existingParticipant = $participantsByUserId[$user->getUID()] ?? null;
			if ($existingParticipant instanceof Participant) {
				if ($existingParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {
					$this->updateParticipantType($room, $existingParticipant, Participant::USER);
				}

				// Participant is already in the conversation, so skip them.
				continue;
			}

			$newParticipants[] = [
				'actorType' => Attendee::ACTOR_USERS,
				'actorId' => $user->getUID(),
				'displayName' => $user->getDisplayName(),
			];
		}

		try {
			$this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_CIRCLES, $circle->getSingleId());
		} catch (DoesNotExistException $e) {
			$attendee = new Attendee();
			$attendee->setRoomId($room->getId());
			$attendee->setActorType(Attendee::ACTOR_CIRCLES);
			$attendee->setActorId($circle->getSingleId());
			$attendee->setDisplayName($circle->getDisplayName());
			$attendee->setParticipantType(Participant::USER);
			$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);
			$attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE);
			$this->attendeeMapper->insert($attendee);

			$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);
			$this->dispatcher->dispatchTyped($attendeeEvent);
		}

		$this->addUsers($room, $newParticipants);
	}

	/**
	 * @param Room $room
	 * @param string $email
	 * @return Participant
	 */
	public function inviteEmailAddress(Room $room, string $email): Participant {
		$lastMessage = 0;
		if ($room->getLastMessage() instanceof IComment) {
			$lastMessage = (int) $room->getLastMessage()->getId();
		}

		$attendee = new Attendee();
		$attendee->setRoomId($room->getId());
		$attendee->setActorType(Attendee::ACTOR_EMAILS);
		$attendee->setActorId($email);

		if ($room->getSIPEnabled() !== Webinary::SIP_DISABLED
			&& $this->talkConfig->isSIPConfigured()) {
			$attendee->setPin($this->generatePin());
		}

		$attendee->setParticipantType(Participant::GUEST);
		$attendee->setLastReadMessage($lastMessage);
		$this->attendeeMapper->insert($attendee);
		// FIXME handle duplicate invites gracefully

		$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);
		$this->dispatcher->dispatchTyped($attendeeEvent);

		return new Participant($room, $attendee, null);
	}

	public function generatePinForParticipant(Room $room, Participant $participant): void {
		$attendee = $participant->getAttendee();
		if ($room->getSIPEnabled() !== Webinary::SIP_DISABLED
			&& $this->talkConfig->isSIPConfigured()
			&& ($attendee->getActorType() === Attendee::ACTOR_USERS || $attendee->getActorType() === Attendee::ACTOR_EMAILS)
			&& !$attendee->getPin()) {
			$attendee->setPin($this->generatePin());
			$this->attendeeMapper->update($attendee);
		}
	}

	public function ensureOneToOneRoomIsFilled(Room $room): void {
		if ($room->getType() !== Room::TYPE_ONE_TO_ONE) {
			return;
		}

		$users = json_decode($room->getName(), true);
		$participants = $this->getParticipantUserIds($room);
		$missingUsers = array_diff($users, $participants);

		foreach ($missingUsers as $userId) {
			$userDisplayName = $this->userManager->getDisplayName($userId);
			if ($userDisplayName !== null) {
				$this->addUsers($room, [[
					'actorType' => Attendee::ACTOR_USERS,
					'actorId' => $userId,
					'displayName' => $userDisplayName,
					'participantType' => Participant::OWNER,
				]]);
			}
		}
	}

	public function leaveRoomAsSession(Room $room, Participant $participant, bool $duplicatedParticipant = false): void {
		if ($duplicatedParticipant) {
			$event = new DuplicatedParticipantEvent($room, $participant);
		} else {
			$event = new ParticipantEvent($room, $participant);
		}
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_DISCONNECT, $event);

		$session = $participant->getSession();
		if ($session instanceof Session) {
			$isInCall = $session->getInCall() !== Participant::FLAG_DISCONNECTED;
			if ($isInCall) {
				$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED);
			}

			$this->sessionMapper->delete($session);
		} else {
			$this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId());
		}

		if ($participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {
			$this->attendeeMapper->delete($participant->getAttendee());

			$attendeeEvent = new AttendeesRemovedEvent($room, [$participant->getAttendee()]);
			$this->dispatcher->dispatchTyped($attendeeEvent);
		}

		$this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_DISCONNECT, $event);
	}

	public function removeAttendee(Room $room, Participant $participant, string $reason, bool $attendeeEventIsTriggeredAlready = false): void {
		$isUser = $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS;

		$sessions = $this->sessionService->getAllSessionsForAttendee($participant->getAttendee());

		if ($isUser) {
			$user = $this->userManager->get($participant->getAttendee()->getActorId());
			$event = new RemoveUserEvent($room, $participant, $user, $reason, $sessions);
			$this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event);
		} else {
			$event = new RemoveParticipantEvent($room, $participant, $reason, $sessions);
			$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $event);
		}

		$this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId());
		$this->attendeeMapper->delete($participant->getAttendee());

		if (!$attendeeEventIsTriggeredAlready) {
			$attendeeEvent = new AttendeesRemovedEvent($room, [$participant->getAttendee()]);
			$this->dispatcher->dispatchTyped($attendeeEvent);
		}

		if ($isUser) {
			$this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event);
		} else {
			$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event);
		}

		if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GROUPS) {
			$this->removeGroupMembers($room, $participant, $reason);
		} elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_CIRCLES) {
			$this->removeCircleMembers($room, $participant, $reason);
		}
	}

	public function getActorsByType(Room $room, string $actorType): array {
		return $this->attendeeMapper->getActorsByType($room->getId(), $actorType);
	}

	public function removeGroupMembers(Room $room, Participant $removedGroupParticipant, string $reason): void {
		$removedGroup = $this->groupManager->get($removedGroupParticipant->getAttendee()->getActorId());
		if (!$removedGroup instanceof IGroup) {
			return;
		}

		$users = $this->membershipService->getUsersWithoutOtherMemberships($room, $removedGroup->getUsers());
		$attendees = [];
		foreach ($users as $user) {
			try {
				$participant = $this->getParticipant($room, $user->getUID());
				$participantType = $participant->getAttendee()->getParticipantType();

				$attendees[] = $participant->getAttendee();
				if ($participantType === Participant::USER) {
					// Only remove normal users, not moderators/admins
					$this->removeAttendee($room, $participant, $reason, true);
				}
			} catch (ParticipantNotFoundException $e) {
			}
		}

		$attendeeEvent = new AttendeesRemovedEvent($room, $attendees);
		$this->dispatcher->dispatchTyped($attendeeEvent);
	}

	public function removeCircleMembers(Room $room, Participant $removedCircleParticipant, string $reason): void {
		try {
			$circlesManager = Server::get(CirclesManager::class);
			$circlesManager->startSuperSession();
			$circle = $circlesManager->getCircle($removedCircleParticipant->getAttendee()->getActorId());
			$circlesManager->stopSession();
		} catch (\Exception $e) {
			// Circles not enabled
			return;
		}

		$circlesManager->startSuperSession();
		try {
			$circle = $circlesManager->getCircle($removedCircleParticipant->getAttendee()->getActorId());
			$circlesManager->stopSession();
		} catch (\Exception $e) {
			$circlesManager->stopSession();
			return;
		}

		$membersInCircle = $circle->getInheritedMembers();
		$users = [];
		foreach ($membersInCircle as $member) {
			/** @var Member $member */
			if ($member->getUserType() !== Member::TYPE_USER || $member->getUserId() === '') {
				// Not a user?
				continue;
			}

			if ($member->getStatus() !== Member::STATUS_INVITED && $member->getStatus() !== Member::STATUS_MEMBER) {
				// Only allow invited and regular members
				continue;
			}

			$users[] = $this->userManager->get($member->getUserId());
		}

		$users = array_filter($users);

		if (empty($users)) {
			return;
		}

		$users = $this->membershipService->getUsersWithoutOtherMemberships($room, $users);
		$attendees = [];
		foreach ($users as $user) {
			try {
				$participant = $this->getParticipant($room, $user->getUID());
				$participantType = $participant->getAttendee()->getParticipantType();

				$attendees[] = $participant->getAttendee();
				if ($participantType === Participant::USER) {
					// Only remove normal users, not moderators/admins
					$this->removeAttendee($room, $participant, $reason, true);
				}
			} catch (ParticipantNotFoundException $e) {
			}
		}

		$attendeeEvent = new AttendeesRemovedEvent($room, $attendees);
		$this->dispatcher->dispatchTyped($attendeeEvent);
	}

	public function removeUser(Room $room, IUser $user, string $reason): void {
		try {
			$participant = $this->getParticipant($room, $user->getUID(), false);
		} catch (ParticipantNotFoundException $e) {
			return;
		}

		$attendee = $participant->getAttendee();
		$sessions = $this->sessionService->getAllSessionsForAttendee($attendee);

		$event = new RemoveUserEvent($room, $participant, $user, $reason, $sessions);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event);

		foreach ($sessions as $session) {
			$this->sessionMapper->delete($session);
		}

		$this->attendeeMapper->delete($attendee);

		$attendeeEvent = new AttendeesRemovedEvent($room, [$attendee]);
		$this->dispatcher->dispatchTyped($attendeeEvent);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event);
	}

	public function cleanGuestParticipants(Room $room): void {
		$event = new RoomEvent($room);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_GUESTS_CLEAN, $event);

		$query = $this->connection->getQueryBuilder();
		$query->selectAlias('s.id', 's_id')
			->from('talk_sessions', 's')
			->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('s.attendee_id', 'a.id'))
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS)))
			->andWhere($query->expr()->lte('s.last_ping', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT_KILL, IQueryBuilder::PARAM_INT)));

		$sessionTableIds = [];
		$result = $query->executeQuery();
		while ($row = $result->fetch()) {
			$sessionTableIds[] = (int) $row['s_id'];
		}
		$result->closeCursor();

		$this->sessionService->deleteSessionsById($sessionTableIds);

		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('s.attendee_id', 'a.id'))
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS)))
			->andWhere($query->expr()->isNull('s.id'));

		$attendeeIds = [];
		$attendees = [];
		$result = $query->executeQuery();
		while ($row = $result->fetch()) {
			$attendeeIds[] = (int) $row['a_id'];
			$attendees[] = $this->attendeeMapper->createAttendeeFromRow($row);
		}
		$result->closeCursor();

			$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);
		}

		$event->setSessionIds($changedSessionIds);
		$event->setUserIds($changedUserIds);

	public function endCallForEveryone(Room $room, Participant $moderator): void {
		$event = new EndCallForEveryoneEvent($room, $moderator);
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_END_CALL_FOR_EVERYONE, $event);

		$participants = $this->getParticipantsInCall($room);
		$changedSessionIds = [];
		$changedUserIds = [];

		// kick out all participants out of the call
		foreach ($participants as $participant) {
			$changedSessionIds[] = $participant->getSession()->getSessionId();
			if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
				$changedUserIds[] = $participant->getAttendee()->getActorId();
			}
			$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);
		}

		$this->sessionMapper->resetInCallByIds($changedSessionIds);

		$event->setSessionIds($changedSessionIds);
		$event->setUserIds($changedUserIds);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, $event);
	}

	public function changeInCall(Room $room, Participant $participant, int $flags, bool $endCallForEveryone = false, bool $silent = false): void {
		$session = $participant->getSession();
		}

		$session->setInCall($flags);
		$this->sessionMapper->update($session);

		if ($flags !== Participant::FLAG_DISCONNECTED) {
			$attendee = $participant->getAttendee();
			$flags &= ~Participant::FLAG_WITH_VIDEO;
		}

		if ($flags !== Participant::FLAG_DISCONNECTED) {
			if ($silent) {
				$event = new SilentModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall(), $silent);
			} else {
				$event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall(), $silent);
			}
			$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_JOIN_CALL, $event);
		} else {
			if ($endCallForEveryone) {
				$event = new ModifyEveryoneEvent($room, $participant, 'inCall', $flags, $session->getInCall());
			} else {
				$event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall());
			}
			$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_LEAVE_CALL, $event);
		}

		$session->setInCall($flags);
		if (!$endCallForEveryone) {
			$this->sessionMapper->update($session);
		}

		if ($flags !== Participant::FLAG_DISCONNECTED) {
			$attendee = $participant->getAttendee();
			$attendee->setLastJoinedCall($this->timeFactory->getTime());
			$this->attendeeMapper->update($attendee);
		}

		if ($flags !== Participant::FLAG_DISCONNECTED) {
			$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_JOIN_CALL, $event);
		} else {
			$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $event);
		}
	}

	public function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): bool {
		$attendee = $this->attendeeMapper->getById($targetAttendeeId);
		if ($attendee->getActorType() !== Attendee::ACTOR_USERS) {
			return false;
		}

		$sessions = $this->sessionMapper->findByAttendeeId($targetAttendeeId);
		foreach ($sessions as $session) {
			if ($session->getInCall() !== Participant::FLAG_DISCONNECTED) {
				return false;
			}
		}

		$this->dispatcher->dispatchTyped(new SendCallNotificationEvent(
			$room,
			$currentParticipant,
			new Participant($room, $attendee, null)
		));

		return true;
	}

	public function updateCallFlags(Room $room, Participant $participant, int $flags): void {
		$session = $participant->getSession();
		if (!$session instanceof Session) {
			return;
		}

		if (!($session->getInCall() & Participant::FLAG_IN_CALL)) {
			throw new \Exception('Participant not in call');
		}

		if (!($flags & Participant::FLAG_IN_CALL)) {
			throw new \InvalidArgumentException('Invalid flags');
		}

		$permissions = $participant->getPermissions();
		if (!($permissions & Attendee::PERMISSIONS_PUBLISH_AUDIO)) {
			$flags &= ~Participant::FLAG_WITH_AUDIO;
		}
		if (!($permissions & Attendee::PERMISSIONS_PUBLISH_VIDEO)) {
			$flags &= ~Participant::FLAG_WITH_VIDEO;
		}

		$event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall());
		$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS, $event);

		$session->setInCall($flags);
		$this->sessionMapper->update($session);

		$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $event);
	}

	/**
	 * @param Room $room
	 * @param string[] $userIds
	 * @param int $messageId
	 * @param string[] $usersDirectlyMentioned
	 */
	public function markUsersAsMentioned(Room $room, array $userIds, int $messageId, array $usersDirectlyMentioned): void {
		$update = $this->connection->getQueryBuilder();
		$update->update('talk_attendees')
			->set('last_mention_message', $update->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))
			->where($update->expr()->eq('room_id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($update->expr()->eq('actor_type', $update->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($update->expr()->in('actor_id', $update->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
		$update->executeStatement();

		if (!empty($usersDirectlyMentioned)) {
			$update = $this->connection->getQueryBuilder();
			$update->update('talk_attendees')
				->set('last_mention_direct', $update->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))
				->where($update->expr()->eq('room_id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
				->andWhere($update->expr()->eq('actor_type', $update->createNamedParameter(Attendee::ACTOR_USERS)))
				->andWhere($update->expr()->in('actor_id', $update->createNamedParameter($usersDirectlyMentioned, IQueryBuilder::PARAM_STR_ARRAY)));
			$update->executeStatement();
		}
	}

	public function resetChatDetails(Room $room): void {
		$update = $this->connection->getQueryBuilder();
		$update->update('talk_attendees')
			->set('last_read_message', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
			->set('last_mention_message', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
			->set('last_mention_direct', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
			->where($update->expr()->eq('room_id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
		$update->executeStatement();
	}

	public function updateReadPrivacyForActor(string $actorType, string $actorId, int $readPrivacy): void {
		$update = $this->connection->getQueryBuilder();
		$update->update('talk_attendees')
			->set('read_privacy', $update->createNamedParameter($readPrivacy, IQueryBuilder::PARAM_INT))
			->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType)))
			->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId)));
		$update->executeStatement();
	}

	public function updateDisplayNameForActor(string $actorType, string $actorId, string $displayName): void {
		$update = $this->connection->getQueryBuilder();
		$update->update('talk_attendees')
			->set('display_name', $update->createNamedParameter($displayName))
			->where($update->expr()->eq('actor_type', $update->createNamedParameter($actorType)))
			->andWhere($update->expr()->eq('actor_id', $update->createNamedParameter($actorId)));
		$update->executeStatement();
	}

	public function getLastCommonReadChatMessage(Room $room): int {
		$query = $this->connection->getQueryBuilder();
		$query->selectAlias($query->func()->min('last_read_message'), 'last_common_read_message')
			->from('talk_attendees')
			->where($query->expr()->eq('actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($query->expr()->eq('room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('read_privacy', $query->createNamedParameter(Participant::PRIVACY_PUBLIC, IQueryBuilder::PARAM_INT)));

		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		return (int) ($row['last_common_read_message'] ?? 0);
	}

	/**
	 * @param int[] $roomIds
	 * @return array A map of roomId => "last common read message id"
	 * @psalm-return  array<int, int>
	 */
	public function getLastCommonReadChatMessageForMultipleRooms(array $roomIds): array {
		$query = $this->connection->getQueryBuilder();
		$query->select('room_id')
			->selectAlias($query->func()->min('last_read_message'), 'last_common_read_message')
			->from('talk_attendees')
			->where($query->expr()->eq('actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($query->expr()->in('room_id', $query->createNamedParameter($roomIds, IQueryBuilder::PARAM_INT_ARRAY)))
			->andWhere($query->expr()->eq('read_privacy', $query->createNamedParameter(Participant::PRIVACY_PUBLIC, IQueryBuilder::PARAM_INT)))
			->groupBy('room_id');

		$commonReads = array_fill_keys($roomIds, 0);
		$result = $query->executeQuery();
		while ($row = $result->fetch()) {
			$commonReads[(int) $row['room_id']] = (int) $row['last_common_read_message'];
		}
		$result->closeCursor();

		return $commonReads;
	}

	/**
	 * @param Room $room
	 * @return Participant[]
	 */
	public function getParticipantsForRoom(Room $room): array {
		$query = $this->connection->getQueryBuilder();

		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));

		return $this->getParticipantsFromQuery($query, $room);
	}

	/**
	 * Get all sessions and attendees without a session for the room
	 *
	 * This will return multiple items for the same attendee if the attendee
	 * has multiple sessions in the room.
	 *
	 * @param Room $room
	 * @return Participant[]
	 */
	public function getSessionsAndParticipantsForRoom(Room $room): array {
		$query = $this->connection->getQueryBuilder();

		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_attendees', 'a')
			->leftJoin(
				'a', 'talk_sessions', 's',
				$query->expr()->eq('s.attendee_id', 'a.id')
			)
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));

		return $this->getParticipantsFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param int $maxAge
	 * @return Participant[]
	 */
	public function getParticipantsForAllSessions(Room $room, int $maxAge = 0): array {
		$query = $this->connection->getQueryBuilder();

		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_sessions', 's')
			->leftJoin(
				's', 'talk_attendees', 'a',
				$query->expr()->eq('s.attendee_id', 'a.id')
			)
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->isNotNull('a.id'));

		if ($maxAge > 0) {
			$query->andWhere($query->expr()->gt('s.last_ping', $query->createNamedParameter($maxAge, IQueryBuilder::PARAM_INT)));
		}

		return $this->getParticipantsFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param int $maxAge
	 * @return Participant[]
	 */
	public function getParticipantsInCall(Room $room, int $maxAge = 0): array {
		$query = $this->connection->getQueryBuilder();

		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_sessions', 's')
			->leftJoin(
				's', 'talk_attendees', 'a',
				$query->expr()->eq('s.attendee_id', 'a.id')
			)
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->neq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)));

		if ($maxAge > 0) {
			$query->andWhere($query->expr()->gte('s.last_ping', $query->createNamedParameter($maxAge, IQueryBuilder::PARAM_INT)));
		}

		return $this->getParticipantsFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param int $notificationLevel
	 * @return Participant[]
	 */
	public function getParticipantsByNotificationLevel(Room $room, int $notificationLevel): array {
		$query = $this->connection->getQueryBuilder();

		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_attendees', 'a')
			// Currently we only care if the user has a session at all, so we can select any: #ThisIsFine
			->leftJoin(
				'a', 'talk_sessions', 's',
				$query->expr()->eq('s.attendee_id', 'a.id')
			)
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.notification_level', $query->createNamedParameter($notificationLevel, IQueryBuilder::PARAM_INT)));

		$participants = $this->getParticipantsFromQuery($query, $room);

		$uniqueAttendees = [];
		foreach ($participants as $participant) {
			$uniqueAttendees[$participant->getAttendee()->getId()] = $participant;
		}

		return array_values($uniqueAttendees);
	}

	/**
	 * @param IQueryBuilder $query
	 * @return Participant[]
	 */
	protected function getParticipantsFromQuery(IQueryBuilder $query, Room $room): array {
		$participants = [];
		$result = $query->executeQuery();
		while ($row = $result->fetch()) {
			$attendee = $this->attendeeMapper->createAttendeeFromRow($row);
			if (isset($row['s_id'])) {
				$session = $this->sessionMapper->createSessionFromRow($row);
			} else {
				$session = null;
			}

			$participants[] = new Participant($room, $attendee, $session);
		}
		$result->closeCursor();

		return $participants;
	}

	/**
	 * @param IQueryBuilder $query
	 * @return Participant
	 * @throws ParticipantNotFoundException
	 */
	protected function getParticipantFromQuery(IQueryBuilder $query, Room $room): Participant {
		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		if ($row === false) {
			throw new ParticipantNotFoundException('User is not a participant');
		}

		$attendee = $this->attendeeMapper->createAttendeeFromRow($row);
		if (isset($row['s_id'])) {
			$session = $this->sessionMapper->createSessionFromRow($row);
		} else {
			$session = null;
		}

		return new Participant($room, $attendee, $session);
	}

	/**
	 * @param Room $room
	 * @param \DateTime|null $maxLastJoined
	 * @return string[]
	 */
	public function getParticipantUserIds(Room $room, \DateTime $maxLastJoined = null): array {
		$maxLastJoinedTimestamp = null;
		if ($maxLastJoined !== null) {
			$maxLastJoinedTimestamp = $maxLastJoined->getTimestamp();
		}
		$attendees = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_USERS, $maxLastJoinedTimestamp);

		return array_map(static function (Attendee $attendee) {
			return $attendee->getActorId();
		}, $attendees);
	}

	/**
	 * @param Room $room
	 * @param \DateTime|null $maxLastJoined
	 * @return int
	 */
	public function getGuestCount(Room $room, \DateTime $maxLastJoined = null): int {
		$maxLastJoinedTimestamp = null;
		if ($maxLastJoined !== null) {
			$maxLastJoinedTimestamp = $maxLastJoined->getTimestamp();
		}

		return $this->attendeeMapper->getActorsCountByType($room->getId(), Attendee::ACTOR_GUESTS, $maxLastJoinedTimestamp);
	}

	/**
	 * @param Room $room
	 * @return string[]
	 */
	public function getParticipantUserIdsForCallNotifications(Room $room): array {
		$query = $this->connection->getQueryBuilder();

		$query->select('a.actor_id')
			->from('talk_attendees', 'a')
			->leftJoin(
				'a', 'talk_sessions', 's',
				$query->expr()->andX(
					$query->expr()->eq('s.attendee_id', 'a.id'),
					$query->expr()->neq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)),
					$query->expr()->gte('s.last_ping', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT, IQueryBuilder::PARAM_INT)),
				)
			)
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($query->expr()->eq('a.notification_calls', $query->createNamedParameter(Participant::NOTIFY_CALLS_ON)))
			->andWhere($query->expr()->isNull('s.in_call'));

		if ($room->getLobbyState() !== Webinary::LOBBY_NONE) {
			// Filter out non-moderators and users without lobby permissions
			$query->andWhere(
				$query->expr()->orX(
					$query->expr()->in('a.participant_type', $query->createNamedParameter(
						[Participant::MODERATOR, Participant::OWNER],
						IQueryBuilder::PARAM_INT_ARRAY
					)),
					$query->expr()->eq(
						$query->expr()->castColumn(
							$query->expr()->bitwiseAnd(
								'permissions',
								Attendee::PERMISSIONS_LOBBY_IGNORE
							),
							IQueryBuilder::PARAM_INT
						),
						$query->createNamedParameter(Attendee::PERMISSIONS_LOBBY_IGNORE, IQueryBuilder::PARAM_INT)
					)
				)
			);
		}

		$userIds = [];
		$result = $query->executeQuery();
		while ($row = $result->fetch()) {
			$userIds[] = $row['actor_id'];
		}
		$result->closeCursor();

		return $userIds;
	}

	/**
	 * @param Room $room
	 * @return int
	 */
	public function getNumberOfUsers(Room $room): int {
		return $this->attendeeMapper->countActorsByParticipantType($room->getId(), [
			Participant::USER,
			Participant::MODERATOR,
			Participant::OWNER,
		]);
	}

	/**
	 * @param Room $room
	 * @param bool $ignoreGuestModerators
	 * @return int
	 */
	public function getNumberOfModerators(Room $room, bool $ignoreGuestModerators = true): int {
		$participantTypes = [
			Participant::MODERATOR,
			Participant::OWNER,
		];
		if (!$ignoreGuestModerators) {
			$participantTypes[] = Participant::GUEST_MODERATOR;
		}
		return $this->attendeeMapper->countActorsByParticipantType($room->getId(), $participantTypes);
	}

	/**
	 * @param Room $room
	 * @return int
	 */
	public function getNumberOfActors(Room $room): int {
		return $this->attendeeMapper->countActorsByParticipantType($room->getId(), []);
	}

	/**
	 * @param Room $room
	 * @return bool
	 */
	public function hasActiveSessions(Room $room): bool {
		$query = $this->connection->getQueryBuilder();
		$query->select('a.room_id')
			->from('talk_attendees', 'a')
			->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq(
				'a.id', 's.attendee_id'
			))
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->isNotNull('s.id'))
			->setMaxResults(1);
		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		return (bool) $row;
	}

	/**
	 * @param Room $room
	 * @return bool
	 */
	public function hasActiveSessionsInCall(Room $room): bool {
		$query = $this->connection->getQueryBuilder();
		$query->select('a.room_id')
			->from('talk_attendees', 'a')
			->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq(
				'a.id', 's.attendee_id'
			))
			->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->isNotNull('s.in_call'))
			->andWhere($query->expr()->neq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)))
			->andWhere($query->expr()->gte('s.last_ping', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT, IQueryBuilder::PARAM_INT)))
			->setMaxResults(1);
		$result = $query->executeQuery();
		$row = $result->fetch();
		$result->closeCursor();

		return (bool) $row;
	}

	protected function generatePin(int $entropy = 7): string {
		$pin = '';
		// Do not allow to start with a '0' as that is a special mode on the phone server
		// Also there are issues with some providers when you enter the same number twice
		// consecutive too fast, so we avoid this as well.
		$lastDigit = '0';
		for ($i = 0; $i < $entropy; $i++) {
			$lastDigit = $this->secureRandom->generate(1,
				str_replace($lastDigit, '', ISecureRandom::CHAR_DIGITS)
			);
			$pin .= $lastDigit;
		}

		return $pin;
	}

	/**
	 * @param Room $room
	 * @param string|null $userId
	 * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources),
	 *                                     string to try loading a specific session
	 *                                     null to try loading "any"
	 * @return Participant
	 * @throws ParticipantNotFoundException When the user is not a participant
	 */
	public function getParticipant(Room $room, ?string $userId, $sessionId = null): Participant {
		if (!is_string($userId) || $userId === '') {
			throw new ParticipantNotFoundException('Not a user');
		}

		if (isset($this->userCache[$room->getId()][$userId])) {
			$participant = $this->userCache[$room->getId()][$userId];
			if (!$sessionId
				|| ($participant->getSession() instanceof Session
					&& $participant->getSession()->getSessionId() === $sessionId)) {
				return $participant;
			}
		}

		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
			->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId())))
			->setMaxResults(1);

		if ($sessionId !== false) {
			if ($sessionId !== null) {
				$helper->selectSessionsTable($query);
				$query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX(
					$query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)),
					$query->expr()->eq('a.id', 's.attendee_id')
				));
			} else {
				$helper->selectSessionsTable($query); // FIXME PROBLEM
				$query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id'));
			}
		}

		$participant = $this->getParticipantFromQuery($query, $room);

		$this->userCache ??= [];
		$this->userCache[$room->getId()] ??= [];
		$this->userCache[$room->getId()][$userId] = $participant;
		if ($participant->getSession()) {
			$participantSessionId = $participant->getSession()->getSessionId();
			$this->sessionCache ??= [];
			$this->sessionCache[$room->getId()] ??= [];
			$this->sessionCache[$room->getId()][$participantSessionId] = $participant;
		}

		return $participant;
	}

	/**
	 * @param Room $room
	 * @param string|null $sessionId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the user is not a participant
	 */
	public function getParticipantBySession(Room $room, ?string $sessionId): Participant {
		if (!is_string($sessionId) || $sessionId === '' || $sessionId === '0') {
			throw new ParticipantNotFoundException('Not a user');
		}

		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$helper->selectSessionsTable($query);
		$query->from('talk_sessions', 's')
			->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id'))
			->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId())))
			->setMaxResults(1);

		return $this->getParticipantFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param string $pin
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByPin(Room $room, string $pin): Participant {
		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.pin', $query->createNamedParameter($pin)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId())))
			->setMaxResults(1);

		return $this->getParticipantFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param int $attendeeId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByAttendeeId(Room $room, int $attendeeId): Participant {
		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->where($query->expr()->eq('a.id', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId())))
			->setMaxResults(1);

		return $this->getParticipantFromQuery($query, $room);
	}

	/**
	 * @param Room $room
	 * @param string $actorType
	 * @param string $actorId
	 * @return Participant
	 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)
	 */
	public function getParticipantByActor(Room $room, string $actorType, string $actorId): Participant {
		if ($actorType === Attendee::ACTOR_USERS) {
			return $this->getParticipant($room, $actorId, false);
		}

		$query = $this->connection->getQueryBuilder();
		$helper = new SelectHelper();
		$helper->selectAttendeesTable($query);
		$query->from('talk_attendees', 'a')
			->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)))
			->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)))
			->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId())))
			->setMaxResults(1);

		return $this->getParticipantFromQuery($query, $room);
	}
}

<MSG> Merge pull request #6941 from nextcloud/bugfix/noid/reset-in_call-flag-in-one-query

Reset all in_call flags in one go
<DFF> @@ -924,6 +924,8 @@ class ParticipantService {
 			$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);
 		}
 
+		$this->sessionMapper->resetInCallByIds($changedSessionIds);
+
 		$event->setSessionIds($changedSessionIds);
 		$event->setUserIds($changedUserIds);
 
@@ -957,7 +959,9 @@ class ParticipantService {
 		}
 
 		$session->setInCall($flags);
-		$this->sessionMapper->update($session);
+		if (!$endCallForEveryone) {
+			$this->sessionMapper->update($session);
+		}
 
 		if ($flags !== Participant::FLAG_DISCONNECTED) {
 			$attendee = $participant->getAttendee();

{'commit': "diff --git a/lib/Model/SessionMapper.php b/lib/Model/SessionMapper.php\nindex 1b747eace..79798b7bc 100644\n--- a/lib/Model/SessionMapper.php\n+++ b/lib/Model/SessionMapper.php\n@@ -23,6 +23,7 @@ declare(strict_types=1);\n \n namespace OCA\\Talk\\Model;\n \n+use OCA\\Talk\\Participant;\n use OCP\\AppFramework\\Db\\QBMapper;\n use OCP\\DB\\QueryBuilder\\IQueryBuilder;\n use OCP\\IDBConnection;\n@@ -90,6 +91,17 @@ class SessionMapper extends QBMapper {\n \t\treturn $delete->executeStatement();\n \t}\n \n+\t/**\n+\t * @param string[] $sessionIds\n+\t */\n+\tpublic function resetInCallByIds(array $sessionIds): void {\n+\t\t$update = $this->db->getQueryBuilder();\n+\t\t$update->update($this->getTableName())\n+\t\t\t->set('in_call', $update->createNamedParameter(Participant::FLAG_DISCONNECTED, IQueryBuilder::PARAM_INT))\n+\t\t\t->where($update->expr()->in('session_id', $update->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY)));\n+\t\t$update->executeStatement();\n+\t}\n+\n \tpublic function createSessionFromRow(array $row): Session {\n \t\treturn $this->mapRowToEntity([\n \t\t\t'id' => $row['s_id'],\ndiff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php\nindex 24f8e4a3a..960330cee 100644\n--- a/lib/Service/ParticipantService.php\n+++ b/lib/Service/ParticipantService.php\n@@ -924,6 +924,8 @@ class ParticipantService {\n \t\t\t$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);\n \t\t}\n \n+\t\t$this->sessionMapper->resetInCallByIds($changedSessionIds);\n+\n \t\t$event->setSessionIds($changedSessionIds);\n \t\t$event->setUserIds($changedUserIds);\n \n@@ -957,7 +959,9 @@ class ParticipantService {\n \t\t}\n \n \t\t$session->setInCall($flags);\n-\t\t$this->sessionMapper->update($session);\n+\t\tif (!$endCallForEveryone) {\n+\t\t\t$this->sessionMapper->update($session);\n+\t\t}\n \n \t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n \t\t\t$attendee = $participant->getAttendee();\n", 'commit_subject': 'Merge pull request #6941 from nextcloud/bugfix/noid/reset-in_call-flag-in-one-query', 'repo_name': 'nextcloud/spreed', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1646246416', 'tz_offset': '60', 'date': {'seconds': '1646246416'}}, 'commit_tree': 'c288d8b9d173c1776026ba4c62da00f877397daa', 'commit_message': 'Merge pull request #6941 from nextcloud/bugfix/noid/reset-in_call-flag-in-one-query\n\nReset all in_call flags in one go', 'language_name': 'JavaScript', 'license': 'agpl-3.0', 'hunks': '@@ -924,6 +924,8 @@ class ParticipantService {\n \t\t\t$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);\n \t\t}\n \n+\t\t$this->sessionMapper->resetInCallByIds($changedSessionIds);\n+\n \t\t$event->setSessionIds($changedSessionIds);\n \t\t$event->setUserIds($changedUserIds);\n \n@@ -957,7 +959,9 @@ class ParticipantService {\n \t\t}\n \n \t\t$session->setInCall($flags);\n-\t\t$this->sessionMapper->update($session);\n+\t\tif (!$endCallForEveryone) {\n+\t\t\t$this->sessionMapper->update($session);\n+\t\t}\n \n \t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n \t\t\t$attendee = $participant->getAttendee();\n', 'addition_count': 5, 'deletion_count': 1, 'src_file': 'a/lib/Service/ParticipantService.php', 'tgt_file': 'b/lib/Service/ParticipantService.php', 'file_extension': '.php', 'before_file': '<?php\n\ndeclare(strict_types=1);\n/**\n * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>\n *\n * @license GNU AGPL version 3 or any later version\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program.  If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\nnamespace OCA\\Talk\\Service;\n\nuse OCA\\Talk\\Manager;\nuse OCA\\Circles\\CirclesManager;\nuse OCA\\Circles\\Model\\Circle;\nuse OCA\\Circles\\Model\\Member;\nuse OCA\\Talk\\Chat\\ChatManager;\nuse OCA\\Talk\\Config;\nuse OCA\\Talk\\Events\\AddParticipantsEvent;\nuse OCA\\Talk\\Events\\AttendeesAddedEvent;\nuse OCA\\Talk\\Events\\AttendeesRemovedEvent;\nuse OCA\\Talk\\Events\\ChatEvent;\nuse OCA\\Talk\\Events\\DuplicatedParticipantEvent;\nuse OCA\\Talk\\Events\\EndCallForEveryoneEvent;\nuse OCA\\Talk\\Events\\JoinRoomGuestEvent;\nuse OCA\\Talk\\Events\\JoinRoomUserEvent;\nuse OCA\\Talk\\Events\\ModifyEveryoneEvent;\nuse OCA\\Talk\\Events\\ModifyParticipantEvent;\nuse OCA\\Talk\\Events\\ParticipantEvent;\nuse OCA\\Talk\\Events\\RemoveParticipantEvent;\nuse OCA\\Talk\\Events\\RemoveUserEvent;\nuse OCA\\Talk\\Events\\RoomEvent;\nuse OCA\\Talk\\Events\\SendCallNotificationEvent;\nuse OCA\\Talk\\Events\\SilentModifyParticipantEvent;\nuse OCA\\Talk\\Exceptions\\ForbiddenException;\nuse OCA\\Talk\\Exceptions\\InvalidPasswordException;\nuse OCA\\Talk\\Exceptions\\ParticipantNotFoundException;\nuse OCA\\Talk\\Exceptions\\UnauthorizedException;\nuse OCA\\Talk\\Federation\\FederationManager;\nuse OCA\\Talk\\Federation\\Notifications;\nuse OCA\\Talk\\Model\\Attendee;\nuse OCA\\Talk\\Model\\AttendeeMapper;\nuse OCA\\Talk\\Model\\SelectHelper;\nuse OCA\\Talk\\Model\\Session;\nuse OCA\\Talk\\Model\\SessionMapper;\nuse OCA\\Talk\\Participant;\nuse OCA\\Talk\\Room;\nuse OCA\\Talk\\Webinary;\nuse OCP\\AppFramework\\Db\\DoesNotExistException;\nuse OCP\\AppFramework\\Utility\\ITimeFactory;\nuse OCP\\Comments\\IComment;\nuse OCP\\DB\\Exception;\nuse OCP\\DB\\QueryBuilder\\IQueryBuilder;\nuse OCP\\EventDispatcher\\IEventDispatcher;\nuse OCP\\ICacheFactory;\nuse OCP\\IConfig;\nuse OCP\\IDBConnection;\nuse OCP\\IGroup;\nuse OCP\\IGroupManager;\nuse OCP\\IUser;\nuse OCP\\IUserManager;\nuse OCP\\Security\\ISecureRandom;\nuse OCP\\Server;\n\nclass ParticipantService {\n\tprotected IConfig $serverConfig;\n\tprotected Config $talkConfig;\n\tprotected AttendeeMapper $attendeeMapper;\n\tprotected SessionMapper $sessionMapper;\n\tprotected SessionService $sessionService;\n\tprivate ISecureRandom $secureRandom;\n\tprotected IDBConnection $connection;\n\tprivate IEventDispatcher $dispatcher;\n\tprivate IUserManager $userManager;\n\tprivate IGroupManager $groupManager;\n\tprivate MembershipService $membershipService;\n\tprivate Notifications $notifications;\n\tprivate ITimeFactory $timeFactory;\n\tprivate ICacheFactory $cacheFactory;\n\n\tprotected array $userCache;\n\tprotected array $sessionCache;\n\n\tpublic function __construct(IConfig $serverConfig,\n\t\t\t\t\t\t\t\tConfig $talkConfig,\n\t\t\t\t\t\t\t\tAttendeeMapper $attendeeMapper,\n\t\t\t\t\t\t\t\tSessionMapper $sessionMapper,\n\t\t\t\t\t\t\t\tSessionService $sessionService,\n\t\t\t\t\t\t\t\tISecureRandom $secureRandom,\n\t\t\t\t\t\t\t\tIDBConnection $connection,\n\t\t\t\t\t\t\t\tIEventDispatcher $dispatcher,\n\t\t\t\t\t\t\t\tIUserManager $userManager,\n\t\t\t\t\t\t\t\tIGroupManager $groupManager,\n\t\t\t\t\t\t\t\tMembershipService $membershipService,\n\t\t\t\t\t\t\t\tNotifications $notifications,\n\t\t\t\t\t\t\t\tITimeFactory $timeFactory,\n\t\t\t\t\t\t\t\tICacheFactory $cacheFactory) {\n\t\t$this->serverConfig = $serverConfig;\n\t\t$this->talkConfig = $talkConfig;\n\t\t$this->attendeeMapper = $attendeeMapper;\n\t\t$this->sessionMapper = $sessionMapper;\n\t\t$this->sessionService = $sessionService;\n\t\t$this->secureRandom = $secureRandom;\n\t\t$this->connection = $connection;\n\t\t$this->dispatcher = $dispatcher;\n\t\t$this->userManager = $userManager;\n\t\t$this->groupManager = $groupManager;\n\t\t$this->membershipService = $membershipService;\n\t\t$this->timeFactory = $timeFactory;\n\t\t$this->cacheFactory = $cacheFactory;\n\t\t$this->notifications = $notifications;\n\t}\n\n\tpublic function updateParticipantType(Room $room, Participant $participant, int $participantType): void {\n\t\t$attendee = $participant->getAttendee();\n\n\t\tif ($attendee->getActorType() === Attendee::ACTOR_GROUPS) {\n\t\t\t// Can not promote/demote groups\n\t\t\treturn;\n\t\t}\n\n\t\t$oldType = $attendee->getParticipantType();\n\n\t\t$event = new ModifyParticipantEvent($room, $participant, \'type\', $participantType, $oldType);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_TYPE_SET, $event);\n\n\t\t$attendee->setParticipantType($participantType);\n\t\t$this->attendeeMapper->update($attendee);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, $event);\n\t}\n\n\t/**\n\t * @throws Exception\n\t * @throws ForbiddenException\n\t */\n\tpublic function updatePermissions(Room $room, Participant $participant, string $method, int $newPermissions): bool {\n\t\tif ($room->getType() === Room::TYPE_ONE_TO_ONE) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif ($participant->hasModeratorPermissions()) {\n\t\t\tthrow new ForbiddenException();\n\t\t}\n\n\t\t$attendee = $participant->getAttendee();\n\n\t\tif ($attendee->getActorType() === Attendee::ACTOR_GROUPS || $attendee->getActorType() === Attendee::ACTOR_CIRCLES) {\n\t\t\t// Can not set publishing permissions for those actor types\n\t\t\treturn false;\n\t\t}\n\n\t\t$oldPermissions = $participant->getPermissions();\n\t\tif ($method === Attendee::PERMISSIONS_MODIFY_SET) {\n\t\t\tif ($newPermissions !== Attendee::PERMISSIONS_DEFAULT) {\n\t\t\t\t// Make sure the custom flag is set when not setting to default permissions\n\t\t\t\t$newPermissions |= Attendee::PERMISSIONS_CUSTOM;\n\t\t\t}\n\t\t} elseif ($method === Attendee::PERMISSIONS_MODIFY_ADD) {\n\t\t\t$newPermissions = $oldPermissions | $newPermissions;\n\t\t} elseif ($method === Attendee::PERMISSIONS_MODIFY_REMOVE) {\n\t\t\t$newPermissions = $oldPermissions & ~$newPermissions;\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\n\t\t$event = new ModifyParticipantEvent($room, $participant, \'permissions\', $newPermissions, $oldPermissions);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_PERMISSIONS_SET, $event);\n\n\t\t$attendee->setPermissions($newPermissions);\n\t\t$this->attendeeMapper->update($attendee);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET, $event);\n\n\t\treturn true;\n\t}\n\n\tpublic function updateAllPermissions(Room $room, string $method, int $newState): void {\n\t\t$this->attendeeMapper->modifyPermissions($room->getId(), $method, $newState);\n\t}\n\n\tpublic function updateLastReadMessage(Participant $participant, int $lastReadMessage): void {\n\t\t$attendee = $participant->getAttendee();\n\t\t$attendee->setLastReadMessage($lastReadMessage);\n\t\t$this->attendeeMapper->update($attendee);\n\t}\n\n\tpublic function updateFavoriteStatus(Participant $participant, bool $isFavorite): void {\n\t\t$attendee = $participant->getAttendee();\n\t\t$attendee->setFavorite($isFavorite);\n\t\t$this->attendeeMapper->update($attendee);\n\t}\n\n\t/**\n\t * @param Participant $participant\n\t * @param int $level\n\t * @throws \\InvalidArgumentException When the notification level is invalid\n\t */\n\tpublic function updateNotificationLevel(Participant $participant, int $level): void {\n\t\tif (!\\in_array($level, [\n\t\t\tParticipant::NOTIFY_ALWAYS,\n\t\t\tParticipant::NOTIFY_MENTION,\n\t\t\tParticipant::NOTIFY_NEVER\n\t\t], true)) {\n\t\t\tthrow new \\InvalidArgumentException(\'Invalid notification level\');\n\t\t}\n\n\t\t$attendee = $participant->getAttendee();\n\t\t$attendee->setNotificationLevel($level);\n\t\t$this->attendeeMapper->update($attendee);\n\t}\n\n\t/**\n\t * @param Participant $participant\n\t * @param int $level\n\t */\n\tpublic function updateNotificationCalls(Participant $participant, int $level): void {\n\t\tif (!\\in_array($level, [\n\t\t\tParticipant::NOTIFY_CALLS_OFF,\n\t\t\tParticipant::NOTIFY_CALLS_ON,\n\t\t], true)) {\n\t\t\tthrow new \\InvalidArgumentException(\'Invalid notification level\');\n\t\t}\n\n\t\t$attendee = $participant->getAttendee();\n\t\t$attendee->setNotificationCalls($level);\n\t\t$this->attendeeMapper->update($attendee);\n\t}\n\n\t/**\n\t * @param RoomService $roomService\n\t * @param Room $room\n\t * @param IUser $user\n\t * @param string $password\n\t * @param bool $passedPasswordProtection\n\t * @return Participant\n\t * @throws InvalidPasswordException\n\t * @throws UnauthorizedException\n\t */\n\tpublic function joinRoom(RoomService $roomService, Room $room, IUser $user, string $password, bool $passedPasswordProtection = false): Participant {\n\t\t$event = new JoinRoomUserEvent($room, $user, $password, $passedPasswordProtection);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_CONNECT, $event);\n\n\t\tif ($event->getCancelJoin() === true) {\n\t\t\t$this->removeUser($room, $user, Room::PARTICIPANT_LEFT);\n\t\t\tthrow new UnauthorizedException(\'Participant is not allowed to join\');\n\t\t}\n\n\t\ttry {\n\t\t\t$attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID());\n\t\t} catch (DoesNotExistException $e) {\n\t\t\t// queried here to avoid loop deps\n\t\t\t$manager = Server::get(Manager::class);\n\t\t\t$isListableByUser = $manager->isRoomListableByUser($room, $user->getUID());\n\n\t\t\tif (!$isListableByUser && !$event->getPassedPasswordProtection() && !$roomService->verifyPassword($room, $password)[\'result\']) {\n\t\t\t\tthrow new InvalidPasswordException(\'Provided password is invalid\');\n\t\t\t}\n\n\t\t\t// User joining a group or public call through listing\n\t\t\tif (($room->getType() === Room::TYPE_GROUP || $room->getType() === Room::TYPE_PUBLIC) && $isListableByUser) {\n\t\t\t\t$this->addUsers($room, [[\n\t\t\t\t\t\'actorType\' => Attendee::ACTOR_USERS,\n\t\t\t\t\t\'actorId\' => $user->getUID(),\n\t\t\t\t\t\'displayName\' => $user->getDisplayName(),\n\t\t\t\t\t// need to use "USER" here, because "USER_SELF_JOINED" only works for public calls\n\t\t\t\t\t\'participantType\' => Participant::USER,\n\t\t\t\t]], $user);\n\t\t\t} elseif ($room->getType() === Room::TYPE_PUBLIC) {\n\t\t\t\t// User joining a public room, without being invited\n\t\t\t\t$this->addUsers($room, [[\n\t\t\t\t\t\'actorType\' => Attendee::ACTOR_USERS,\n\t\t\t\t\t\'actorId\' => $user->getUID(),\n\t\t\t\t\t\'displayName\' => $user->getDisplayName(),\n\t\t\t\t\t\'participantType\' => Participant::USER_SELF_JOINED,\n\t\t\t\t]], $user);\n\t\t\t} else {\n\t\t\t\t// shouldn\'t happen unless some code called joinRoom without previous checks\n\t\t\t\tthrow new UnauthorizedException(\'Participant is not allowed to join\');\n\t\t\t}\n\n\t\t\t$attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID());\n\t\t}\n\n\t\t$session = $this->sessionService->createSessionForAttendee($attendee);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_CONNECT, $event);\n\n\t\treturn new Participant($room, $attendee, $session);\n\t}\n\n\t/**\n\t * @param RoomService $roomService\n\t * @param Room $room\n\t * @param string $password\n\t * @param bool $passedPasswordProtection\n\t * @param ?Participant $previousParticipant\n\t * @return Participant\n\t * @throws InvalidPasswordException\n\t * @throws UnauthorizedException\n\t */\n\tpublic function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $password, bool $passedPasswordProtection = false, ?Participant $previousParticipant = null): Participant {\n\t\t$event = new JoinRoomGuestEvent($room, $password, $passedPasswordProtection);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_GUEST_CONNECT, $event);\n\n\t\tif ($event->getCancelJoin()) {\n\t\t\tthrow new UnauthorizedException(\'Participant is not allowed to join\');\n\t\t}\n\n\t\tif (!$event->getPassedPasswordProtection() && !$roomService->verifyPassword($room, $password)[\'result\']) {\n\t\t\tthrow new InvalidPasswordException();\n\t\t}\n\n\t\t$lastMessage = 0;\n\t\tif ($room->getLastMessage() instanceof IComment) {\n\t\t\t$lastMessage = (int) $room->getLastMessage()->getId();\n\t\t}\n\n\t\tif ($previousParticipant instanceof Participant) {\n\t\t\t$attendee = $previousParticipant->getAttendee();\n\t\t} else {\n\t\t\t$randomActorId = $this->secureRandom->generate(255);\n\n\t\t\t$attendee = new Attendee();\n\t\t\t$attendee->setRoomId($room->getId());\n\t\t\t$attendee->setActorType(Attendee::ACTOR_GUESTS);\n\t\t\t$attendee->setActorId($randomActorId);\n\t\t\t$attendee->setParticipantType(Participant::GUEST);\n\t\t\t$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);\n\t\t\t$attendee->setLastReadMessage($lastMessage);\n\t\t\t$this->attendeeMapper->insert($attendee);\n\n\t\t\t$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t\t}\n\n\t\t$session = $this->sessionService->createSessionForAttendee($attendee);\n\n\t\tif (!$previousParticipant instanceof Participant) {\n\t\t\t// Update the random guest id\n\t\t\t$attendee->setActorId(sha1($session->getSessionId()));\n\t\t\t$this->attendeeMapper->update($attendee);\n\t\t}\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_GUEST_CONNECT, $event);\n\n\t\treturn new Participant($room, $attendee, $session);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param array $participants\n\t * @param IUser|null $addedBy User that is attempting to add these users (must be set for federated users to be added)\n\t * @throws \\Exception thrown if $addedBy is not set when adding a federated user\n\t */\n\tpublic function addUsers(Room $room, array $participants, ?IUser $addedBy = null): void {\n\t\tif (empty($participants)) {\n\t\t\treturn;\n\t\t}\n\t\t$event = new AddParticipantsEvent($room, $participants, true);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_USERS_ADD, $event);\n\n\t\t$lastMessage = 0;\n\t\tif ($room->getLastMessage() instanceof IComment) {\n\t\t\t$lastMessage = (int) $room->getLastMessage()->getId();\n\t\t}\n\n\t\t$attendees = [];\n\t\tforeach ($participants as $participant) {\n\t\t\t$readPrivacy = Participant::PRIVACY_PUBLIC;\n\t\t\tif ($participant[\'actorType\'] === Attendee::ACTOR_USERS) {\n\t\t\t\t$readPrivacy = $this->talkConfig->getUserReadPrivacy($participant[\'actorId\']);\n\t\t\t} elseif ($participant[\'actorType\'] === Attendee::ACTOR_FEDERATED_USERS) {\n\t\t\t\tif ($addedBy === null) {\n\t\t\t\t\tthrow new \\Exception(\'$addedBy must be set to add a federated user\');\n\t\t\t\t}\n\t\t\t\t$participant[\'accessToken\'] = $this->secureRandom->generate(\n\t\t\t\t\tFederationManager::TOKEN_LENGTH,\n\t\t\t\t\tISecureRandom::CHAR_HUMAN_READABLE\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t$attendee = new Attendee();\n\t\t\t$attendee->setRoomId($room->getId());\n\t\t\t$attendee->setActorType($participant[\'actorType\']);\n\t\t\t$attendee->setActorId($participant[\'actorId\']);\n\t\t\tif (isset($participant[\'displayName\'])) {\n\t\t\t\t$attendee->setDisplayName($participant[\'displayName\']);\n\t\t\t}\n\t\t\tif (isset($participant[\'accessToken\'])) {\n\t\t\t\t$attendee->setAccessToken($participant[\'accessToken\']);\n\t\t\t}\n\t\t\tif (isset($participant[\'remoteId\'])) {\n\t\t\t\t$attendee->setRemoteId($participant[\'remoteId\']);\n\t\t\t}\n\t\t\t$attendee->setParticipantType($participant[\'participantType\'] ?? Participant::USER);\n\t\t\t$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);\n\t\t\t$attendee->setLastReadMessage($lastMessage);\n\t\t\t$attendee->setReadPrivacy($readPrivacy);\n\t\t\ttry {\n\t\t\t\t$entity = $this->attendeeMapper->insert($attendee);\n\t\t\t\t$attendees[] = $attendee;\n\n\t\t\t\tif ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {\n\t\t\t\t\t$this->notifications->sendRemoteShare((string) $entity->getId(), $participant[\'accessToken\'], $participant[\'actorId\'], $addedBy->getDisplayName(), $addedBy->getCloudId(), \'user\', $room, $this->getHighestPermissionAttendee($room));\n\t\t\t\t}\n\t\t\t} catch (Exception $e) {\n\t\t\t\tif ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {\n\t\t\t\t\tthrow $e;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!empty($attendees)) {\n\t\t\t$attendeeEvent = new AttendeesAddedEvent($room, $attendees);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_USERS_ADD, $event);\n\n\t\t\t$lastMessage = $event->getLastMessage();\n\t\t\tif ($lastMessage instanceof IComment) {\n\t\t\t\t$this->updateRoomLastMessage($room, $lastMessage);\n\t\t\t}\n\t\t}\n\t}\n\n\tprotected function updateRoomLastMessage(Room $room, IComment $message): void {\n\t\t/** @var RoomService $roomService */\n\t\t$roomService = Server::get(RoomService::class);\n\t\t$roomService->setLastMessage($room, $message);\n\n\t\t$lastMessageCache = $this->cacheFactory->createDistributed(\'talk/lastmsgid\');\n\t\t$lastMessageCache->remove($room->getToken());\n\t\t$unreadCountCache = $this->cacheFactory->createDistributed(\'talk/unreadcount\');\n\t\t$unreadCountCache->clear($room->getId() . \'-\');\n\n\t\t$event = new ChatEvent($room, $message);\n\t\t$this->dispatcher->dispatch(ChatManager::EVENT_AFTER_MULTIPLE_SYSTEM_MESSAGE_SEND, $event);\n\t}\n\n\tpublic function getHighestPermissionAttendee(Room $room): ?Attendee {\n\t\ttry {\n\t\t\t$roomOwners = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::OWNER]);\n\n\t\t\tif (!empty($roomOwners)) {\n\t\t\t\tforeach ($roomOwners as $owner) {\n\t\t\t\t\tif ($owner->getActorType() === Attendee::ACTOR_USERS) {\n\t\t\t\t\t\treturn $owner;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t$roomModerators = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::MODERATOR]);\n\t\t\tif (!empty($roomOwners)) {\n\t\t\t\tforeach ($roomModerators as $moderator) {\n\t\t\t\t\tif ($moderator->getActorType() === Attendee::ACTOR_USERS) {\n\t\t\t\t\t\treturn $moderator;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (Exception $e) {\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param IGroup $group\n\t * @param Participant[] $existingParticipants\n\t */\n\tpublic function addGroup(Room $room, IGroup $group, array $existingParticipants = []): void {\n\t\t$usersInGroup = $group->getUsers();\n\n\t\tif (empty($existingParticipants)) {\n\t\t\t$existingParticipants = $this->getParticipantsForRoom($room);\n\t\t}\n\n\t\t$participantsByUserId = [];\n\t\tforeach ($existingParticipants as $participant) {\n\t\t\tif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {\n\t\t\t\t$participantsByUserId[$participant->getAttendee()->getActorId()] = $participant;\n\t\t\t}\n\t\t}\n\n\t\t$newParticipants = [];\n\t\tforeach ($usersInGroup as $user) {\n\t\t\t$existingParticipant = $participantsByUserId[$user->getUID()] ?? null;\n\t\t\tif ($existingParticipant instanceof Participant) {\n\t\t\t\tif ($existingParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {\n\t\t\t\t\t$this->updateParticipantType($room, $existingParticipant, Participant::USER);\n\t\t\t\t}\n\n\t\t\t\t// Participant is already in the conversation, so skip them.\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$newParticipants[] = [\n\t\t\t\t\'actorType\' => Attendee::ACTOR_USERS,\n\t\t\t\t\'actorId\' => $user->getUID(),\n\t\t\t\t\'displayName\' => $user->getDisplayName(),\n\t\t\t];\n\t\t}\n\n\t\ttry {\n\t\t\t$this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_GROUPS, $group->getGID());\n\t\t} catch (DoesNotExistException $e) {\n\t\t\t$attendee = new Attendee();\n\t\t\t$attendee->setRoomId($room->getId());\n\t\t\t$attendee->setActorType(Attendee::ACTOR_GROUPS);\n\t\t\t$attendee->setActorId($group->getGID());\n\t\t\t$attendee->setDisplayName($group->getDisplayName());\n\t\t\t$attendee->setParticipantType(Participant::USER);\n\t\t\t$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);\n\t\t\t$attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE);\n\t\t\t$this->attendeeMapper->insert($attendee);\n\n\t\t\t$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t\t}\n\n\t\t$this->addUsers($room, $newParticipants);\n\t}\n\n\t/**\n\t * @param string $circleId\n\t * @param string $userId\n\t * @return Circle\n\t * @throws ParticipantNotFoundException\n\t */\n\tpublic function getCircle(string $circleId, string $userId): Circle {\n\t\ttry {\n\t\t\t$circlesManager = Server::get(CirclesManager::class);\n\t\t\t$federatedUser = $circlesManager->getFederatedUser($userId, Member::TYPE_USER);\n\t\t\t$federatedUser->getLink($circleId);\n\t\t} catch (\\Exception $e) {\n\t\t\tthrow new ParticipantNotFoundException(\'Circle not found or not a member\');\n\t\t}\n\n\t\t$circlesManager->startSession($federatedUser);\n\t\ttry {\n\t\t\t$circle = $circlesManager->getCircle($circleId);\n\t\t\t$circlesManager->stopSession();\n\t\t\treturn $circle;\n\t\t} catch (\\Exception $e) {\n\t\t}\n\n\t\t$circlesManager->stopSession();\n\t\tthrow new ParticipantNotFoundException(\'Circle not found or not a member\');\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param Circle $circle\n\t * @param Participant[] $existingParticipants\n\t */\n\tpublic function addCircle(Room $room, Circle $circle, array $existingParticipants = []): void {\n\t\t$membersInCircle = $circle->getInheritedMembers();\n\n\t\tif (empty($existingParticipants)) {\n\t\t\t$existingParticipants = $this->getParticipantsForRoom($room);\n\t\t}\n\n\t\t$participantsByUserId = [];\n\t\tforeach ($existingParticipants as $participant) {\n\t\t\tif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {\n\t\t\t\t$participantsByUserId[$participant->getAttendee()->getActorId()] = $participant;\n\t\t\t}\n\t\t}\n\n\t\t$newParticipants = [];\n\t\tforeach ($membersInCircle as $member) {\n\t\t\t/** @var Member $member */\n\t\t\tif ($member->getUserType() !== Member::TYPE_USER || $member->getUserId() === \'\') {\n\t\t\t\t// Not a user?\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ($member->getStatus() !== Member::STATUS_INVITED && $member->getStatus() !== Member::STATUS_MEMBER) {\n\t\t\t\t// Only allow invited and regular members\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$user = $this->userManager->get($member->getUserId());\n\t\t\tif (!$user instanceof IUser) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$existingParticipant = $participantsByUserId[$user->getUID()] ?? null;\n\t\t\tif ($existingParticipant instanceof Participant) {\n\t\t\t\tif ($existingParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {\n\t\t\t\t\t$this->updateParticipantType($room, $existingParticipant, Participant::USER);\n\t\t\t\t}\n\n\t\t\t\t// Participant is already in the conversation, so skip them.\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$newParticipants[] = [\n\t\t\t\t\'actorType\' => Attendee::ACTOR_USERS,\n\t\t\t\t\'actorId\' => $user->getUID(),\n\t\t\t\t\'displayName\' => $user->getDisplayName(),\n\t\t\t];\n\t\t}\n\n\t\ttry {\n\t\t\t$this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_CIRCLES, $circle->getSingleId());\n\t\t} catch (DoesNotExistException $e) {\n\t\t\t$attendee = new Attendee();\n\t\t\t$attendee->setRoomId($room->getId());\n\t\t\t$attendee->setActorType(Attendee::ACTOR_CIRCLES);\n\t\t\t$attendee->setActorId($circle->getSingleId());\n\t\t\t$attendee->setDisplayName($circle->getDisplayName());\n\t\t\t$attendee->setParticipantType(Participant::USER);\n\t\t\t$attendee->setPermissions(Attendee::PERMISSIONS_DEFAULT);\n\t\t\t$attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE);\n\t\t\t$this->attendeeMapper->insert($attendee);\n\n\t\t\t$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t\t}\n\n\t\t$this->addUsers($room, $newParticipants);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string $email\n\t * @return Participant\n\t */\n\tpublic function inviteEmailAddress(Room $room, string $email): Participant {\n\t\t$lastMessage = 0;\n\t\tif ($room->getLastMessage() instanceof IComment) {\n\t\t\t$lastMessage = (int) $room->getLastMessage()->getId();\n\t\t}\n\n\t\t$attendee = new Attendee();\n\t\t$attendee->setRoomId($room->getId());\n\t\t$attendee->setActorType(Attendee::ACTOR_EMAILS);\n\t\t$attendee->setActorId($email);\n\n\t\tif ($room->getSIPEnabled() !== Webinary::SIP_DISABLED\n\t\t\t&& $this->talkConfig->isSIPConfigured()) {\n\t\t\t$attendee->setPin($this->generatePin());\n\t\t}\n\n\t\t$attendee->setParticipantType(Participant::GUEST);\n\t\t$attendee->setLastReadMessage($lastMessage);\n\t\t$this->attendeeMapper->insert($attendee);\n\t\t// FIXME handle duplicate invites gracefully\n\n\t\t$attendeeEvent = new AttendeesAddedEvent($room, [$attendee]);\n\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\n\t\treturn new Participant($room, $attendee, null);\n\t}\n\n\tpublic function generatePinForParticipant(Room $room, Participant $participant): void {\n\t\t$attendee = $participant->getAttendee();\n\t\tif ($room->getSIPEnabled() !== Webinary::SIP_DISABLED\n\t\t\t&& $this->talkConfig->isSIPConfigured()\n\t\t\t&& ($attendee->getActorType() === Attendee::ACTOR_USERS || $attendee->getActorType() === Attendee::ACTOR_EMAILS)\n\t\t\t&& !$attendee->getPin()) {\n\t\t\t$attendee->setPin($this->generatePin());\n\t\t\t$this->attendeeMapper->update($attendee);\n\t\t}\n\t}\n\n\tpublic function ensureOneToOneRoomIsFilled(Room $room): void {\n\t\tif ($room->getType() !== Room::TYPE_ONE_TO_ONE) {\n\t\t\treturn;\n\t\t}\n\n\t\t$users = json_decode($room->getName(), true);\n\t\t$participants = $this->getParticipantUserIds($room);\n\t\t$missingUsers = array_diff($users, $participants);\n\n\t\tforeach ($missingUsers as $userId) {\n\t\t\t$userDisplayName = $this->userManager->getDisplayName($userId);\n\t\t\tif ($userDisplayName !== null) {\n\t\t\t\t$this->addUsers($room, [[\n\t\t\t\t\t\'actorType\' => Attendee::ACTOR_USERS,\n\t\t\t\t\t\'actorId\' => $userId,\n\t\t\t\t\t\'displayName\' => $userDisplayName,\n\t\t\t\t\t\'participantType\' => Participant::OWNER,\n\t\t\t\t]]);\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic function leaveRoomAsSession(Room $room, Participant $participant, bool $duplicatedParticipant = false): void {\n\t\tif ($duplicatedParticipant) {\n\t\t\t$event = new DuplicatedParticipantEvent($room, $participant);\n\t\t} else {\n\t\t\t$event = new ParticipantEvent($room, $participant);\n\t\t}\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_DISCONNECT, $event);\n\n\t\t$session = $participant->getSession();\n\t\tif ($session instanceof Session) {\n\t\t\t$isInCall = $session->getInCall() !== Participant::FLAG_DISCONNECTED;\n\t\t\tif ($isInCall) {\n\t\t\t\t$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED);\n\t\t\t}\n\n\t\t\t$this->sessionMapper->delete($session);\n\t\t} else {\n\t\t\t$this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId());\n\t\t}\n\n\t\tif ($participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {\n\t\t\t$this->attendeeMapper->delete($participant->getAttendee());\n\n\t\t\t$attendeeEvent = new AttendeesRemovedEvent($room, [$participant->getAttendee()]);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t\t}\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_DISCONNECT, $event);\n\t}\n\n\tpublic function removeAttendee(Room $room, Participant $participant, string $reason, bool $attendeeEventIsTriggeredAlready = false): void {\n\t\t$isUser = $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS;\n\n\t\t$sessions = $this->sessionService->getAllSessionsForAttendee($participant->getAttendee());\n\n\t\tif ($isUser) {\n\t\t\t$user = $this->userManager->get($participant->getAttendee()->getActorId());\n\t\t\t$event = new RemoveUserEvent($room, $participant, $user, $reason, $sessions);\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event);\n\t\t} else {\n\t\t\t$event = new RemoveParticipantEvent($room, $participant, $reason, $sessions);\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_REMOVE, $event);\n\t\t}\n\n\t\t$this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId());\n\t\t$this->attendeeMapper->delete($participant->getAttendee());\n\n\t\tif (!$attendeeEventIsTriggeredAlready) {\n\t\t\t$attendeeEvent = new AttendeesRemovedEvent($room, [$participant->getAttendee()]);\n\t\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t\t}\n\n\t\tif ($isUser) {\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event);\n\t\t} else {\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event);\n\t\t}\n\n\t\tif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GROUPS) {\n\t\t\t$this->removeGroupMembers($room, $participant, $reason);\n\t\t} elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_CIRCLES) {\n\t\t\t$this->removeCircleMembers($room, $participant, $reason);\n\t\t}\n\t}\n\n\tpublic function getActorsByType(Room $room, string $actorType): array {\n\t\treturn $this->attendeeMapper->getActorsByType($room->getId(), $actorType);\n\t}\n\n\tpublic function removeGroupMembers(Room $room, Participant $removedGroupParticipant, string $reason): void {\n\t\t$removedGroup = $this->groupManager->get($removedGroupParticipant->getAttendee()->getActorId());\n\t\tif (!$removedGroup instanceof IGroup) {\n\t\t\treturn;\n\t\t}\n\n\t\t$users = $this->membershipService->getUsersWithoutOtherMemberships($room, $removedGroup->getUsers());\n\t\t$attendees = [];\n\t\tforeach ($users as $user) {\n\t\t\ttry {\n\t\t\t\t$participant = $this->getParticipant($room, $user->getUID());\n\t\t\t\t$participantType = $participant->getAttendee()->getParticipantType();\n\n\t\t\t\t$attendees[] = $participant->getAttendee();\n\t\t\t\tif ($participantType === Participant::USER) {\n\t\t\t\t\t// Only remove normal users, not moderators/admins\n\t\t\t\t\t$this->removeAttendee($room, $participant, $reason, true);\n\t\t\t\t}\n\t\t\t} catch (ParticipantNotFoundException $e) {\n\t\t\t}\n\t\t}\n\n\t\t$attendeeEvent = new AttendeesRemovedEvent($room, $attendees);\n\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t}\n\n\tpublic function removeCircleMembers(Room $room, Participant $removedCircleParticipant, string $reason): void {\n\t\ttry {\n\t\t\t$circlesManager = Server::get(CirclesManager::class);\n\t\t\t$circlesManager->startSuperSession();\n\t\t\t$circle = $circlesManager->getCircle($removedCircleParticipant->getAttendee()->getActorId());\n\t\t\t$circlesManager->stopSession();\n\t\t} catch (\\Exception $e) {\n\t\t\t// Circles not enabled\n\t\t\treturn;\n\t\t}\n\n\t\t$circlesManager->startSuperSession();\n\t\ttry {\n\t\t\t$circle = $circlesManager->getCircle($removedCircleParticipant->getAttendee()->getActorId());\n\t\t\t$circlesManager->stopSession();\n\t\t} catch (\\Exception $e) {\n\t\t\t$circlesManager->stopSession();\n\t\t\treturn;\n\t\t}\n\n\t\t$membersInCircle = $circle->getInheritedMembers();\n\t\t$users = [];\n\t\tforeach ($membersInCircle as $member) {\n\t\t\t/** @var Member $member */\n\t\t\tif ($member->getUserType() !== Member::TYPE_USER || $member->getUserId() === \'\') {\n\t\t\t\t// Not a user?\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ($member->getStatus() !== Member::STATUS_INVITED && $member->getStatus() !== Member::STATUS_MEMBER) {\n\t\t\t\t// Only allow invited and regular members\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t$users[] = $this->userManager->get($member->getUserId());\n\t\t}\n\n\t\t$users = array_filter($users);\n\n\t\tif (empty($users)) {\n\t\t\treturn;\n\t\t}\n\n\t\t$users = $this->membershipService->getUsersWithoutOtherMemberships($room, $users);\n\t\t$attendees = [];\n\t\tforeach ($users as $user) {\n\t\t\ttry {\n\t\t\t\t$participant = $this->getParticipant($room, $user->getUID());\n\t\t\t\t$participantType = $participant->getAttendee()->getParticipantType();\n\n\t\t\t\t$attendees[] = $participant->getAttendee();\n\t\t\t\tif ($participantType === Participant::USER) {\n\t\t\t\t\t// Only remove normal users, not moderators/admins\n\t\t\t\t\t$this->removeAttendee($room, $participant, $reason, true);\n\t\t\t\t}\n\t\t\t} catch (ParticipantNotFoundException $e) {\n\t\t\t}\n\t\t}\n\n\t\t$attendeeEvent = new AttendeesRemovedEvent($room, $attendees);\n\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\t}\n\n\tpublic function removeUser(Room $room, IUser $user, string $reason): void {\n\t\ttry {\n\t\t\t$participant = $this->getParticipant($room, $user->getUID(), false);\n\t\t} catch (ParticipantNotFoundException $e) {\n\t\t\treturn;\n\t\t}\n\n\t\t$attendee = $participant->getAttendee();\n\t\t$sessions = $this->sessionService->getAllSessionsForAttendee($attendee);\n\n\t\t$event = new RemoveUserEvent($room, $participant, $user, $reason, $sessions);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_USER_REMOVE, $event);\n\n\t\tforeach ($sessions as $session) {\n\t\t\t$this->sessionMapper->delete($session);\n\t\t}\n\n\t\t$this->attendeeMapper->delete($attendee);\n\n\t\t$attendeeEvent = new AttendeesRemovedEvent($room, [$attendee]);\n\t\t$this->dispatcher->dispatchTyped($attendeeEvent);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_USER_REMOVE, $event);\n\t}\n\n\tpublic function cleanGuestParticipants(Room $room): void {\n\t\t$event = new RoomEvent($room);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_GUESTS_CLEAN, $event);\n\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$query->selectAlias(\'s.id\', \'s_id\')\n\t\t\t->from(\'talk_sessions\', \'s\')\n\t\t\t->leftJoin(\'s\', \'talk_attendees\', \'a\', $query->expr()->eq(\'s.attendee_id\', \'a.id\'))\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_type\', $query->createNamedParameter(Attendee::ACTOR_GUESTS)))\n\t\t\t->andWhere($query->expr()->lte(\'s.last_ping\', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT_KILL, IQueryBuilder::PARAM_INT)));\n\n\t\t$sessionTableIds = [];\n\t\t$result = $query->executeQuery();\n\t\twhile ($row = $result->fetch()) {\n\t\t\t$sessionTableIds[] = (int) $row[\'s_id\'];\n\t\t}\n\t\t$result->closeCursor();\n\n\t\t$this->sessionService->deleteSessionsById($sessionTableIds);\n\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->leftJoin(\'a\', \'talk_sessions\', \'s\', $query->expr()->eq(\'s.attendee_id\', \'a.id\'))\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_type\', $query->createNamedParameter(Attendee::ACTOR_GUESTS)))\n\t\t\t->andWhere($query->expr()->isNull(\'s.id\'));\n\n\t\t$attendeeIds = [];\n\t\t$attendees = [];\n\t\t$result = $query->executeQuery();\n\t\twhile ($row = $result->fetch()) {\n\t\t\t$attendeeIds[] = (int) $row[\'a_id\'];\n\t\t\t$attendees[] = $this->attendeeMapper->createAttendeeFromRow($row);\n\t\t}\n\t\t$result->closeCursor();\n\n\t\t\t$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);\n\t\t}\n\n\t\t$event->setSessionIds($changedSessionIds);\n\t\t$event->setUserIds($changedUserIds);\n\n\tpublic function endCallForEveryone(Room $room, Participant $moderator): void {\n\t\t$event = new EndCallForEveryoneEvent($room, $moderator);\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_END_CALL_FOR_EVERYONE, $event);\n\n\t\t$participants = $this->getParticipantsInCall($room);\n\t\t$changedSessionIds = [];\n\t\t$changedUserIds = [];\n\n\t\t// kick out all participants out of the call\n\t\tforeach ($participants as $participant) {\n\t\t\t$changedSessionIds[] = $participant->getSession()->getSessionId();\n\t\t\tif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {\n\t\t\t\t$changedUserIds[] = $participant->getAttendee()->getActorId();\n\t\t\t}\n\t\t\t$this->changeInCall($room, $participant, Participant::FLAG_DISCONNECTED, true);\n\t\t}\n\n\t\t$this->sessionMapper->resetInCallByIds($changedSessionIds);\n\n\t\t$event->setSessionIds($changedSessionIds);\n\t\t$event->setUserIds($changedUserIds);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, $event);\n\t}\n\n\tpublic function changeInCall(Room $room, Participant $participant, int $flags, bool $endCallForEveryone = false, bool $silent = false): void {\n\t\t$session = $participant->getSession();\n\t\t}\n\n\t\t$session->setInCall($flags);\n\t\t$this->sessionMapper->update($session);\n\n\t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n\t\t\t$attendee = $participant->getAttendee();\n\t\t\t$flags &= ~Participant::FLAG_WITH_VIDEO;\n\t\t}\n\n\t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n\t\t\tif ($silent) {\n\t\t\t\t$event = new SilentModifyParticipantEvent($room, $participant, \'inCall\', $flags, $session->getInCall(), $silent);\n\t\t\t} else {\n\t\t\t\t$event = new ModifyParticipantEvent($room, $participant, \'inCall\', $flags, $session->getInCall(), $silent);\n\t\t\t}\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_JOIN_CALL, $event);\n\t\t} else {\n\t\t\tif ($endCallForEveryone) {\n\t\t\t\t$event = new ModifyEveryoneEvent($room, $participant, \'inCall\', $flags, $session->getInCall());\n\t\t\t} else {\n\t\t\t\t$event = new ModifyParticipantEvent($room, $participant, \'inCall\', $flags, $session->getInCall());\n\t\t\t}\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_LEAVE_CALL, $event);\n\t\t}\n\n\t\t$session->setInCall($flags);\n\t\tif (!$endCallForEveryone) {\n\t\t\t$this->sessionMapper->update($session);\n\t\t}\n\n\t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n\t\t\t$attendee = $participant->getAttendee();\n\t\t\t$attendee->setLastJoinedCall($this->timeFactory->getTime());\n\t\t\t$this->attendeeMapper->update($attendee);\n\t\t}\n\n\t\tif ($flags !== Participant::FLAG_DISCONNECTED) {\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_JOIN_CALL, $event);\n\t\t} else {\n\t\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $event);\n\t\t}\n\t}\n\n\tpublic function sendCallNotificationForAttendee(Room $room, Participant $currentParticipant, int $targetAttendeeId): bool {\n\t\t$attendee = $this->attendeeMapper->getById($targetAttendeeId);\n\t\tif ($attendee->getActorType() !== Attendee::ACTOR_USERS) {\n\t\t\treturn false;\n\t\t}\n\n\t\t$sessions = $this->sessionMapper->findByAttendeeId($targetAttendeeId);\n\t\tforeach ($sessions as $session) {\n\t\t\tif ($session->getInCall() !== Participant::FLAG_DISCONNECTED) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\n\t\t$this->dispatcher->dispatchTyped(new SendCallNotificationEvent(\n\t\t\t$room,\n\t\t\t$currentParticipant,\n\t\t\tnew Participant($room, $attendee, null)\n\t\t));\n\n\t\treturn true;\n\t}\n\n\tpublic function updateCallFlags(Room $room, Participant $participant, int $flags): void {\n\t\t$session = $participant->getSession();\n\t\tif (!$session instanceof Session) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!($session->getInCall() & Participant::FLAG_IN_CALL)) {\n\t\t\tthrow new \\Exception(\'Participant not in call\');\n\t\t}\n\n\t\tif (!($flags & Participant::FLAG_IN_CALL)) {\n\t\t\tthrow new \\InvalidArgumentException(\'Invalid flags\');\n\t\t}\n\n\t\t$permissions = $participant->getPermissions();\n\t\tif (!($permissions & Attendee::PERMISSIONS_PUBLISH_AUDIO)) {\n\t\t\t$flags &= ~Participant::FLAG_WITH_AUDIO;\n\t\t}\n\t\tif (!($permissions & Attendee::PERMISSIONS_PUBLISH_VIDEO)) {\n\t\t\t$flags &= ~Participant::FLAG_WITH_VIDEO;\n\t\t}\n\n\t\t$event = new ModifyParticipantEvent($room, $participant, \'inCall\', $flags, $session->getInCall());\n\t\t$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS, $event);\n\n\t\t$session->setInCall($flags);\n\t\t$this->sessionMapper->update($session);\n\n\t\t$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $event);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string[] $userIds\n\t * @param int $messageId\n\t * @param string[] $usersDirectlyMentioned\n\t */\n\tpublic function markUsersAsMentioned(Room $room, array $userIds, int $messageId, array $usersDirectlyMentioned): void {\n\t\t$update = $this->connection->getQueryBuilder();\n\t\t$update->update(\'talk_attendees\')\n\t\t\t->set(\'last_mention_message\', $update->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))\n\t\t\t->where($update->expr()->eq(\'room_id\', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($update->expr()->eq(\'actor_type\', $update->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t->andWhere($update->expr()->in(\'actor_id\', $update->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));\n\t\t$update->executeStatement();\n\n\t\tif (!empty($usersDirectlyMentioned)) {\n\t\t\t$update = $this->connection->getQueryBuilder();\n\t\t\t$update->update(\'talk_attendees\')\n\t\t\t\t->set(\'last_mention_direct\', $update->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))\n\t\t\t\t->where($update->expr()->eq(\'room_id\', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t\t->andWhere($update->expr()->eq(\'actor_type\', $update->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t\t->andWhere($update->expr()->in(\'actor_id\', $update->createNamedParameter($usersDirectlyMentioned, IQueryBuilder::PARAM_STR_ARRAY)));\n\t\t\t$update->executeStatement();\n\t\t}\n\t}\n\n\tpublic function resetChatDetails(Room $room): void {\n\t\t$update = $this->connection->getQueryBuilder();\n\t\t$update->update(\'talk_attendees\')\n\t\t\t->set(\'last_read_message\', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))\n\t\t\t->set(\'last_mention_message\', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))\n\t\t\t->set(\'last_mention_direct\', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))\n\t\t\t->where($update->expr()->eq(\'room_id\', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));\n\t\t$update->executeStatement();\n\t}\n\n\tpublic function updateReadPrivacyForActor(string $actorType, string $actorId, int $readPrivacy): void {\n\t\t$update = $this->connection->getQueryBuilder();\n\t\t$update->update(\'talk_attendees\')\n\t\t\t->set(\'read_privacy\', $update->createNamedParameter($readPrivacy, IQueryBuilder::PARAM_INT))\n\t\t\t->where($update->expr()->eq(\'actor_type\', $update->createNamedParameter($actorType)))\n\t\t\t->andWhere($update->expr()->eq(\'actor_id\', $update->createNamedParameter($actorId)));\n\t\t$update->executeStatement();\n\t}\n\n\tpublic function updateDisplayNameForActor(string $actorType, string $actorId, string $displayName): void {\n\t\t$update = $this->connection->getQueryBuilder();\n\t\t$update->update(\'talk_attendees\')\n\t\t\t->set(\'display_name\', $update->createNamedParameter($displayName))\n\t\t\t->where($update->expr()->eq(\'actor_type\', $update->createNamedParameter($actorType)))\n\t\t\t->andWhere($update->expr()->eq(\'actor_id\', $update->createNamedParameter($actorId)));\n\t\t$update->executeStatement();\n\t}\n\n\tpublic function getLastCommonReadChatMessage(Room $room): int {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$query->selectAlias($query->func()->min(\'last_read_message\'), \'last_common_read_message\')\n\t\t\t->from(\'talk_attendees\')\n\t\t\t->where($query->expr()->eq(\'actor_type\', $query->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t->andWhere($query->expr()->eq(\'room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'read_privacy\', $query->createNamedParameter(Participant::PRIVACY_PUBLIC, IQueryBuilder::PARAM_INT)));\n\n\t\t$result = $query->executeQuery();\n\t\t$row = $result->fetch();\n\t\t$result->closeCursor();\n\n\t\treturn (int) ($row[\'last_common_read_message\'] ?? 0);\n\t}\n\n\t/**\n\t * @param int[] $roomIds\n\t * @return array A map of roomId => "last common read message id"\n\t * @psalm-return  array<int, int>\n\t */\n\tpublic function getLastCommonReadChatMessageForMultipleRooms(array $roomIds): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$query->select(\'room_id\')\n\t\t\t->selectAlias($query->func()->min(\'last_read_message\'), \'last_common_read_message\')\n\t\t\t->from(\'talk_attendees\')\n\t\t\t->where($query->expr()->eq(\'actor_type\', $query->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t->andWhere($query->expr()->in(\'room_id\', $query->createNamedParameter($roomIds, IQueryBuilder::PARAM_INT_ARRAY)))\n\t\t\t->andWhere($query->expr()->eq(\'read_privacy\', $query->createNamedParameter(Participant::PRIVACY_PUBLIC, IQueryBuilder::PARAM_INT)))\n\t\t\t->groupBy(\'room_id\');\n\n\t\t$commonReads = array_fill_keys($roomIds, 0);\n\t\t$result = $query->executeQuery();\n\t\twhile ($row = $result->fetch()) {\n\t\t\t$commonReads[(int) $row[\'room_id\']] = (int) $row[\'last_common_read_message\'];\n\t\t}\n\t\t$result->closeCursor();\n\n\t\treturn $commonReads;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return Participant[]\n\t */\n\tpublic function getParticipantsForRoom(Room $room): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));\n\n\t\treturn $this->getParticipantsFromQuery($query, $room);\n\t}\n\n\t/**\n\t * Get all sessions and attendees without a session for the room\n\t *\n\t * This will return multiple items for the same attendee if the attendee\n\t * has multiple sessions in the room.\n\t *\n\t * @param Room $room\n\t * @return Participant[]\n\t */\n\tpublic function getSessionsAndParticipantsForRoom(Room $room): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$helper->selectSessionsTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->leftJoin(\n\t\t\t\t\'a\', \'talk_sessions\', \'s\',\n\t\t\t\t$query->expr()->eq(\'s.attendee_id\', \'a.id\')\n\t\t\t)\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));\n\n\t\treturn $this->getParticipantsFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param int $maxAge\n\t * @return Participant[]\n\t */\n\tpublic function getParticipantsForAllSessions(Room $room, int $maxAge = 0): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$helper->selectSessionsTable($query);\n\t\t$query->from(\'talk_sessions\', \'s\')\n\t\t\t->leftJoin(\n\t\t\t\t\'s\', \'talk_attendees\', \'a\',\n\t\t\t\t$query->expr()->eq(\'s.attendee_id\', \'a.id\')\n\t\t\t)\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->isNotNull(\'a.id\'));\n\n\t\tif ($maxAge > 0) {\n\t\t\t$query->andWhere($query->expr()->gt(\'s.last_ping\', $query->createNamedParameter($maxAge, IQueryBuilder::PARAM_INT)));\n\t\t}\n\n\t\treturn $this->getParticipantsFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param int $maxAge\n\t * @return Participant[]\n\t */\n\tpublic function getParticipantsInCall(Room $room, int $maxAge = 0): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$helper->selectSessionsTable($query);\n\t\t$query->from(\'talk_sessions\', \'s\')\n\t\t\t->leftJoin(\n\t\t\t\t\'s\', \'talk_attendees\', \'a\',\n\t\t\t\t$query->expr()->eq(\'s.attendee_id\', \'a.id\')\n\t\t\t)\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->neq(\'s.in_call\', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)));\n\n\t\tif ($maxAge > 0) {\n\t\t\t$query->andWhere($query->expr()->gte(\'s.last_ping\', $query->createNamedParameter($maxAge, IQueryBuilder::PARAM_INT)));\n\t\t}\n\n\t\treturn $this->getParticipantsFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param int $notificationLevel\n\t * @return Participant[]\n\t */\n\tpublic function getParticipantsByNotificationLevel(Room $room, int $notificationLevel): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$helper->selectSessionsTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t// Currently we only care if the user has a session at all, so we can select any: #ThisIsFine\n\t\t\t->leftJoin(\n\t\t\t\t\'a\', \'talk_sessions\', \'s\',\n\t\t\t\t$query->expr()->eq(\'s.attendee_id\', \'a.id\')\n\t\t\t)\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'a.notification_level\', $query->createNamedParameter($notificationLevel, IQueryBuilder::PARAM_INT)));\n\n\t\t$participants = $this->getParticipantsFromQuery($query, $room);\n\n\t\t$uniqueAttendees = [];\n\t\tforeach ($participants as $participant) {\n\t\t\t$uniqueAttendees[$participant->getAttendee()->getId()] = $participant;\n\t\t}\n\n\t\treturn array_values($uniqueAttendees);\n\t}\n\n\t/**\n\t * @param IQueryBuilder $query\n\t * @return Participant[]\n\t */\n\tprotected function getParticipantsFromQuery(IQueryBuilder $query, Room $room): array {\n\t\t$participants = [];\n\t\t$result = $query->executeQuery();\n\t\twhile ($row = $result->fetch()) {\n\t\t\t$attendee = $this->attendeeMapper->createAttendeeFromRow($row);\n\t\t\tif (isset($row[\'s_id\'])) {\n\t\t\t\t$session = $this->sessionMapper->createSessionFromRow($row);\n\t\t\t} else {\n\t\t\t\t$session = null;\n\t\t\t}\n\n\t\t\t$participants[] = new Participant($room, $attendee, $session);\n\t\t}\n\t\t$result->closeCursor();\n\n\t\treturn $participants;\n\t}\n\n\t/**\n\t * @param IQueryBuilder $query\n\t * @return Participant\n\t * @throws ParticipantNotFoundException\n\t */\n\tprotected function getParticipantFromQuery(IQueryBuilder $query, Room $room): Participant {\n\t\t$result = $query->executeQuery();\n\t\t$row = $result->fetch();\n\t\t$result->closeCursor();\n\n\t\tif ($row === false) {\n\t\t\tthrow new ParticipantNotFoundException(\'User is not a participant\');\n\t\t}\n\n\t\t$attendee = $this->attendeeMapper->createAttendeeFromRow($row);\n\t\tif (isset($row[\'s_id\'])) {\n\t\t\t$session = $this->sessionMapper->createSessionFromRow($row);\n\t\t} else {\n\t\t\t$session = null;\n\t\t}\n\n\t\treturn new Participant($room, $attendee, $session);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param \\DateTime|null $maxLastJoined\n\t * @return string[]\n\t */\n\tpublic function getParticipantUserIds(Room $room, \\DateTime $maxLastJoined = null): array {\n\t\t$maxLastJoinedTimestamp = null;\n\t\tif ($maxLastJoined !== null) {\n\t\t\t$maxLastJoinedTimestamp = $maxLastJoined->getTimestamp();\n\t\t}\n\t\t$attendees = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_USERS, $maxLastJoinedTimestamp);\n\n\t\treturn array_map(static function (Attendee $attendee) {\n\t\t\treturn $attendee->getActorId();\n\t\t}, $attendees);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param \\DateTime|null $maxLastJoined\n\t * @return int\n\t */\n\tpublic function getGuestCount(Room $room, \\DateTime $maxLastJoined = null): int {\n\t\t$maxLastJoinedTimestamp = null;\n\t\tif ($maxLastJoined !== null) {\n\t\t\t$maxLastJoinedTimestamp = $maxLastJoined->getTimestamp();\n\t\t}\n\n\t\treturn $this->attendeeMapper->getActorsCountByType($room->getId(), Attendee::ACTOR_GUESTS, $maxLastJoinedTimestamp);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return string[]\n\t */\n\tpublic function getParticipantUserIdsForCallNotifications(Room $room): array {\n\t\t$query = $this->connection->getQueryBuilder();\n\n\t\t$query->select(\'a.actor_id\')\n\t\t\t->from(\'talk_attendees\', \'a\')\n\t\t\t->leftJoin(\n\t\t\t\t\'a\', \'talk_sessions\', \'s\',\n\t\t\t\t$query->expr()->andX(\n\t\t\t\t\t$query->expr()->eq(\'s.attendee_id\', \'a.id\'),\n\t\t\t\t\t$query->expr()->neq(\'s.in_call\', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)),\n\t\t\t\t\t$query->expr()->gte(\'s.last_ping\', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT, IQueryBuilder::PARAM_INT)),\n\t\t\t\t)\n\t\t\t)\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_type\', $query->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t->andWhere($query->expr()->eq(\'a.notification_calls\', $query->createNamedParameter(Participant::NOTIFY_CALLS_ON)))\n\t\t\t->andWhere($query->expr()->isNull(\'s.in_call\'));\n\n\t\tif ($room->getLobbyState() !== Webinary::LOBBY_NONE) {\n\t\t\t// Filter out non-moderators and users without lobby permissions\n\t\t\t$query->andWhere(\n\t\t\t\t$query->expr()->orX(\n\t\t\t\t\t$query->expr()->in(\'a.participant_type\', $query->createNamedParameter(\n\t\t\t\t\t\t[Participant::MODERATOR, Participant::OWNER],\n\t\t\t\t\t\tIQueryBuilder::PARAM_INT_ARRAY\n\t\t\t\t\t)),\n\t\t\t\t\t$query->expr()->eq(\n\t\t\t\t\t\t$query->expr()->castColumn(\n\t\t\t\t\t\t\t$query->expr()->bitwiseAnd(\n\t\t\t\t\t\t\t\t\'permissions\',\n\t\t\t\t\t\t\t\tAttendee::PERMISSIONS_LOBBY_IGNORE\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tIQueryBuilder::PARAM_INT\n\t\t\t\t\t\t),\n\t\t\t\t\t\t$query->createNamedParameter(Attendee::PERMISSIONS_LOBBY_IGNORE, IQueryBuilder::PARAM_INT)\n\t\t\t\t\t)\n\t\t\t\t)\n\t\t\t);\n\t\t}\n\n\t\t$userIds = [];\n\t\t$result = $query->executeQuery();\n\t\twhile ($row = $result->fetch()) {\n\t\t\t$userIds[] = $row[\'actor_id\'];\n\t\t}\n\t\t$result->closeCursor();\n\n\t\treturn $userIds;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return int\n\t */\n\tpublic function getNumberOfUsers(Room $room): int {\n\t\treturn $this->attendeeMapper->countActorsByParticipantType($room->getId(), [\n\t\t\tParticipant::USER,\n\t\t\tParticipant::MODERATOR,\n\t\t\tParticipant::OWNER,\n\t\t]);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param bool $ignoreGuestModerators\n\t * @return int\n\t */\n\tpublic function getNumberOfModerators(Room $room, bool $ignoreGuestModerators = true): int {\n\t\t$participantTypes = [\n\t\t\tParticipant::MODERATOR,\n\t\t\tParticipant::OWNER,\n\t\t];\n\t\tif (!$ignoreGuestModerators) {\n\t\t\t$participantTypes[] = Participant::GUEST_MODERATOR;\n\t\t}\n\t\treturn $this->attendeeMapper->countActorsByParticipantType($room->getId(), $participantTypes);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return int\n\t */\n\tpublic function getNumberOfActors(Room $room): int {\n\t\treturn $this->attendeeMapper->countActorsByParticipantType($room->getId(), []);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return bool\n\t */\n\tpublic function hasActiveSessions(Room $room): bool {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$query->select(\'a.room_id\')\n\t\t\t->from(\'talk_attendees\', \'a\')\n\t\t\t->leftJoin(\'a\', \'talk_sessions\', \'s\', $query->expr()->eq(\n\t\t\t\t\'a.id\', \'s.attendee_id\'\n\t\t\t))\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->isNotNull(\'s.id\'))\n\t\t\t->setMaxResults(1);\n\t\t$result = $query->executeQuery();\n\t\t$row = $result->fetch();\n\t\t$result->closeCursor();\n\n\t\treturn (bool) $row;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @return bool\n\t */\n\tpublic function hasActiveSessionsInCall(Room $room): bool {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$query->select(\'a.room_id\')\n\t\t\t->from(\'talk_attendees\', \'a\')\n\t\t\t->leftJoin(\'a\', \'talk_sessions\', \'s\', $query->expr()->eq(\n\t\t\t\t\'a.id\', \'s.attendee_id\'\n\t\t\t))\n\t\t\t->where($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->isNotNull(\'s.in_call\'))\n\t\t\t->andWhere($query->expr()->neq(\'s.in_call\', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)))\n\t\t\t->andWhere($query->expr()->gte(\'s.last_ping\', $query->createNamedParameter($this->timeFactory->getTime() - Session::SESSION_TIMEOUT, IQueryBuilder::PARAM_INT)))\n\t\t\t->setMaxResults(1);\n\t\t$result = $query->executeQuery();\n\t\t$row = $result->fetch();\n\t\t$result->closeCursor();\n\n\t\treturn (bool) $row;\n\t}\n\n\tprotected function generatePin(int $entropy = 7): string {\n\t\t$pin = \'\';\n\t\t// Do not allow to start with a \'0\' as that is a special mode on the phone server\n\t\t// Also there are issues with some providers when you enter the same number twice\n\t\t// consecutive too fast, so we avoid this as well.\n\t\t$lastDigit = \'0\';\n\t\tfor ($i = 0; $i < $entropy; $i++) {\n\t\t\t$lastDigit = $this->secureRandom->generate(1,\n\t\t\t\tstr_replace($lastDigit, \'\', ISecureRandom::CHAR_DIGITS)\n\t\t\t);\n\t\t\t$pin .= $lastDigit;\n\t\t}\n\n\t\treturn $pin;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string|null $userId\n\t * @param string|null|false $sessionId Set to false if you don\'t want to load a session (and save resources),\n\t *                                     string to try loading a specific session\n\t *                                     null to try loading "any"\n\t * @return Participant\n\t * @throws ParticipantNotFoundException When the user is not a participant\n\t */\n\tpublic function getParticipant(Room $room, ?string $userId, $sessionId = null): Participant {\n\t\tif (!is_string($userId) || $userId === \'\') {\n\t\t\tthrow new ParticipantNotFoundException(\'Not a user\');\n\t\t}\n\n\t\tif (isset($this->userCache[$room->getId()][$userId])) {\n\t\t\t$participant = $this->userCache[$room->getId()][$userId];\n\t\t\tif (!$sessionId\n\t\t\t\t|| ($participant->getSession() instanceof Session\n\t\t\t\t\t&& $participant->getSession()->getSessionId() === $sessionId)) {\n\t\t\t\treturn $participant;\n\t\t\t}\n\t\t}\n\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->where($query->expr()->eq(\'a.actor_type\', $query->createNamedParameter(Attendee::ACTOR_USERS)))\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_id\', $query->createNamedParameter($userId)))\n\t\t\t->andWhere($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId())))\n\t\t\t->setMaxResults(1);\n\n\t\tif ($sessionId !== false) {\n\t\t\tif ($sessionId !== null) {\n\t\t\t\t$helper->selectSessionsTable($query);\n\t\t\t\t$query->leftJoin(\'a\', \'talk_sessions\', \'s\', $query->expr()->andX(\n\t\t\t\t\t$query->expr()->eq(\'s.session_id\', $query->createNamedParameter($sessionId)),\n\t\t\t\t\t$query->expr()->eq(\'a.id\', \'s.attendee_id\')\n\t\t\t\t));\n\t\t\t} else {\n\t\t\t\t$helper->selectSessionsTable($query); // FIXME PROBLEM\n\t\t\t\t$query->leftJoin(\'a\', \'talk_sessions\', \'s\', $query->expr()->eq(\'a.id\', \'s.attendee_id\'));\n\t\t\t}\n\t\t}\n\n\t\t$participant = $this->getParticipantFromQuery($query, $room);\n\n\t\t$this->userCache ??= [];\n\t\t$this->userCache[$room->getId()] ??= [];\n\t\t$this->userCache[$room->getId()][$userId] = $participant;\n\t\tif ($participant->getSession()) {\n\t\t\t$participantSessionId = $participant->getSession()->getSessionId();\n\t\t\t$this->sessionCache ??= [];\n\t\t\t$this->sessionCache[$room->getId()] ??= [];\n\t\t\t$this->sessionCache[$room->getId()][$participantSessionId] = $participant;\n\t\t}\n\n\t\treturn $participant;\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string|null $sessionId\n\t * @return Participant\n\t * @throws ParticipantNotFoundException When the user is not a participant\n\t */\n\tpublic function getParticipantBySession(Room $room, ?string $sessionId): Participant {\n\t\tif (!is_string($sessionId) || $sessionId === \'\' || $sessionId === \'0\') {\n\t\t\tthrow new ParticipantNotFoundException(\'Not a user\');\n\t\t}\n\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$helper->selectSessionsTable($query);\n\t\t$query->from(\'talk_sessions\', \'s\')\n\t\t\t->leftJoin(\'s\', \'talk_attendees\', \'a\', $query->expr()->eq(\'a.id\', \'s.attendee_id\'))\n\t\t\t->where($query->expr()->eq(\'s.session_id\', $query->createNamedParameter($sessionId)))\n\t\t\t->andWhere($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId())))\n\t\t\t->setMaxResults(1);\n\n\t\treturn $this->getParticipantFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string $pin\n\t * @return Participant\n\t * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)\n\t */\n\tpublic function getParticipantByPin(Room $room, string $pin): Participant {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->where($query->expr()->eq(\'a.pin\', $query->createNamedParameter($pin)))\n\t\t\t->andWhere($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId())))\n\t\t\t->setMaxResults(1);\n\n\t\treturn $this->getParticipantFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param int $attendeeId\n\t * @return Participant\n\t * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)\n\t */\n\tpublic function getParticipantByAttendeeId(Room $room, int $attendeeId): Participant {\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->where($query->expr()->eq(\'a.id\', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT)))\n\t\t\t->andWhere($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId())))\n\t\t\t->setMaxResults(1);\n\n\t\treturn $this->getParticipantFromQuery($query, $room);\n\t}\n\n\t/**\n\t * @param Room $room\n\t * @param string $actorType\n\t * @param string $actorId\n\t * @return Participant\n\t * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned)\n\t */\n\tpublic function getParticipantByActor(Room $room, string $actorType, string $actorId): Participant {\n\t\tif ($actorType === Attendee::ACTOR_USERS) {\n\t\t\treturn $this->getParticipant($room, $actorId, false);\n\t\t}\n\n\t\t$query = $this->connection->getQueryBuilder();\n\t\t$helper = new SelectHelper();\n\t\t$helper->selectAttendeesTable($query);\n\t\t$query->from(\'talk_attendees\', \'a\')\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_type\', $query->createNamedParameter($actorType)))\n\t\t\t->andWhere($query->expr()->eq(\'a.actor_id\', $query->createNamedParameter($actorId)))\n\t\t\t->andWhere($query->expr()->eq(\'a.room_id\', $query->createNamedParameter($room->getId())))\n\t\t\t->setMaxResults(1);\n\n\t\treturn $this->getParticipantFromQuery($query, $room);\n\t}\n}\n', 'message': 'Merge pull request #6941 from nextcloud/bugfix/noid/reset-in_call-flag-in-one-query\n\nReset all in_call flags in one go', 'author_name': 'GitHub', 'lang': 'php'}