1<?php 2 3declare(strict_types=1); 4/** 5 * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> 6 * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> 7 * 8 * @author Lukas Reschke <lukas@statuscode.ch> 9 * @author Joas Schilling <coding@schilljs.com> 10 * 11 * @license GNU AGPL version 3 or any later version 12 * 13 * This program is free software: you can redistribute it and/or modify 14 * it under the terms of the GNU Affero General Public License as 15 * published by the Free Software Foundation, either version 3 of the 16 * License, or (at your option) any later version. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU Affero General Public License for more details. 22 * 23 * You should have received a copy of the GNU Affero General Public License 24 * along with this program. If not, see <http://www.gnu.org/licenses/>. 25 * 26 */ 27 28namespace OCA\Talk; 29 30use OCA\Talk\Events\ModifyLobbyEvent; 31use OCA\Talk\Events\ModifyRoomEvent; 32use OCA\Talk\Events\RoomEvent; 33use OCA\Talk\Events\SignalingRoomPropertiesEvent; 34use OCA\Talk\Events\VerifyRoomPasswordEvent; 35use OCA\Talk\Exceptions\ParticipantNotFoundException; 36use OCA\Talk\Model\Attendee; 37use OCA\Talk\Model\SelectHelper; 38use OCA\Talk\Model\Session; 39use OCA\Talk\Service\ParticipantService; 40use OCP\AppFramework\Utility\ITimeFactory; 41use OCP\Comments\IComment; 42use OCP\DB\QueryBuilder\IQueryBuilder; 43use OCP\EventDispatcher\IEventDispatcher; 44use OCP\IDBConnection; 45use OCP\Log\Audit\CriticalActionPerformedEvent; 46use OCP\Security\IHasher; 47 48class Room { 49 50 /** 51 * Regex that matches SIP incompatible rooms: 52 * 1. duplicate digit: …11… 53 * 2. leading zero: 0… 54 * 3. non-digit: …a… 55 */ 56 public const SIP_INCOMPATIBLE_REGEX = '/((\d)(?=\2+)|^0|\D)/'; 57 58 public const TYPE_UNKNOWN = -1; 59 public const TYPE_ONE_TO_ONE = 1; 60 public const TYPE_GROUP = 2; 61 public const TYPE_PUBLIC = 3; 62 public const TYPE_CHANGELOG = 4; 63 64 /** @deprecated Use self::TYPE_UNKNOWN */ 65 public const UNKNOWN_CALL = self::TYPE_UNKNOWN; 66 /** @deprecated Use self::TYPE_ONE_TO_ONE */ 67 public const ONE_TO_ONE_CALL = self::TYPE_ONE_TO_ONE; 68 /** @deprecated Use self::TYPE_GROUP */ 69 public const GROUP_CALL = self::TYPE_GROUP; 70 /** @deprecated Use self::TYPE_PUBLIC */ 71 public const PUBLIC_CALL = self::TYPE_PUBLIC; 72 /** @deprecated Use self::TYPE_CHANGELOG */ 73 public const CHANGELOG_CONVERSATION = self::TYPE_CHANGELOG; 74 75 public const READ_WRITE = 0; 76 public const READ_ONLY = 1; 77 78 /** 79 * Only visible when joined 80 */ 81 public const LISTABLE_NONE = 0; 82 83 /** 84 * Searchable by all regular users and moderators, even when not joined, excluding users from the guest app 85 */ 86 public const LISTABLE_USERS = 1; 87 88 /** 89 * Searchable by everyone, which includes guest users (from guest app), even when not joined 90 */ 91 public const LISTABLE_ALL = 2; 92 93 public const START_CALL_EVERYONE = 0; 94 public const START_CALL_USERS = 1; 95 public const START_CALL_MODERATORS = 2; 96 97 public const PARTICIPANT_REMOVED = 'remove'; 98 public const PARTICIPANT_LEFT = 'leave'; 99 100 public const EVENT_AFTER_ROOM_CREATE = self::class . '::createdRoom'; 101 public const EVENT_BEFORE_ROOM_DELETE = self::class . '::preDeleteRoom'; 102 public const EVENT_AFTER_ROOM_DELETE = self::class . '::postDeleteRoom'; 103 public const EVENT_BEFORE_NAME_SET = self::class . '::preSetName'; 104 public const EVENT_AFTER_NAME_SET = self::class . '::postSetName'; 105 public const EVENT_BEFORE_DESCRIPTION_SET = self::class . '::preSetDescription'; 106 public const EVENT_AFTER_DESCRIPTION_SET = self::class . '::postSetDescription'; 107 public const EVENT_BEFORE_PASSWORD_SET = self::class . '::preSetPassword'; 108 public const EVENT_AFTER_PASSWORD_SET = self::class . '::postSetPassword'; 109 public const EVENT_BEFORE_TYPE_SET = self::class . '::preSetType'; 110 public const EVENT_AFTER_TYPE_SET = self::class . '::postSetType'; 111 public const EVENT_BEFORE_READONLY_SET = self::class . '::preSetReadOnly'; 112 public const EVENT_AFTER_READONLY_SET = self::class . '::postSetReadOnly'; 113 public const EVENT_BEFORE_LISTABLE_SET = self::class . '::preSetListable'; 114 public const EVENT_AFTER_LISTABLE_SET = self::class . '::postSetListable'; 115 public const EVENT_BEFORE_LOBBY_STATE_SET = self::class . '::preSetLobbyState'; 116 public const EVENT_AFTER_LOBBY_STATE_SET = self::class . '::postSetLobbyState'; 117 public const EVENT_BEFORE_END_CALL_FOR_EVERYONE = self::class . '::preEndCallForEveryone'; 118 public const EVENT_AFTER_END_CALL_FOR_EVERYONE = self::class . '::postEndCallForEveryone'; 119 public const EVENT_BEFORE_SIP_ENABLED_SET = self::class . '::preSetSIPEnabled'; 120 public const EVENT_AFTER_SIP_ENABLED_SET = self::class . '::postSetSIPEnabled'; 121 public const EVENT_BEFORE_PERMISSIONS_SET = self::class . '::preSetPermissions'; 122 public const EVENT_AFTER_PERMISSIONS_SET = self::class . '::postSetPermissions'; 123 public const EVENT_BEFORE_USERS_ADD = self::class . '::preAddUsers'; 124 public const EVENT_AFTER_USERS_ADD = self::class . '::postAddUsers'; 125 public const EVENT_BEFORE_PARTICIPANT_TYPE_SET = self::class . '::preSetParticipantType'; 126 public const EVENT_AFTER_PARTICIPANT_TYPE_SET = self::class . '::postSetParticipantType'; 127 public const EVENT_BEFORE_PARTICIPANT_PERMISSIONS_SET = self::class . '::preSetParticipantPermissions'; 128 public const EVENT_AFTER_PARTICIPANT_PERMISSIONS_SET = self::class . '::postSetParticipantPermissions'; 129 public const EVENT_BEFORE_USER_REMOVE = self::class . '::preRemoveUser'; 130 public const EVENT_AFTER_USER_REMOVE = self::class . '::postRemoveUser'; 131 public const EVENT_BEFORE_PARTICIPANT_REMOVE = self::class . '::preRemoveBySession'; 132 public const EVENT_AFTER_PARTICIPANT_REMOVE = self::class . '::postRemoveBySession'; 133 public const EVENT_BEFORE_ROOM_CONNECT = self::class . '::preJoinRoom'; 134 public const EVENT_AFTER_ROOM_CONNECT = self::class . '::postJoinRoom'; 135 public const EVENT_BEFORE_ROOM_DISCONNECT = self::class . '::preUserDisconnectRoom'; 136 public const EVENT_AFTER_ROOM_DISCONNECT = self::class . '::postUserDisconnectRoom'; 137 public const EVENT_BEFORE_GUEST_CONNECT = self::class . '::preJoinRoomGuest'; 138 public const EVENT_AFTER_GUEST_CONNECT = self::class . '::postJoinRoomGuest'; 139 public const EVENT_PASSWORD_VERIFY = self::class . '::verifyPassword'; 140 public const EVENT_BEFORE_GUESTS_CLEAN = self::class . '::preCleanGuests'; 141 public const EVENT_AFTER_GUESTS_CLEAN = self::class . '::postCleanGuests'; 142 public const EVENT_BEFORE_SESSION_JOIN_CALL = self::class . '::preSessionJoinCall'; 143 public const EVENT_AFTER_SESSION_JOIN_CALL = self::class . '::postSessionJoinCall'; 144 public const EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS = self::class . '::preSessionUpdateCallFlags'; 145 public const EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS = self::class . '::postSessionUpdateCallFlags'; 146 public const EVENT_BEFORE_SESSION_LEAVE_CALL = self::class . '::preSessionLeaveCall'; 147 public const EVENT_AFTER_SESSION_LEAVE_CALL = self::class . '::postSessionLeaveCall'; 148 public const EVENT_BEFORE_SIGNALING_PROPERTIES = self::class . '::beforeSignalingProperties'; 149 150 public const DESCRIPTION_MAXIMUM_LENGTH = 500; 151 152 /** @var Manager */ 153 private $manager; 154 /** @var IDBConnection */ 155 private $db; 156 /** @var IEventDispatcher */ 157 private $dispatcher; 158 /** @var ITimeFactory */ 159 private $timeFactory; 160 /** @var IHasher */ 161 private $hasher; 162 163 /** @var int */ 164 private $id; 165 /** @var int */ 166 private $type; 167 /** @var int */ 168 private $readOnly; 169 /** @var int */ 170 private $listable; 171 /** @var int */ 172 private $lobbyState; 173 /** @var int */ 174 private $sipEnabled; 175 /** @var int|null */ 176 private $assignedSignalingServer; 177 /** @var \DateTime|null */ 178 private $lobbyTimer; 179 /** @var string */ 180 private $token; 181 /** @var string */ 182 private $name; 183 /** @var string */ 184 private $description; 185 /** @var string */ 186 private $password; 187 /** @var string */ 188 private $serverUrl; 189 /** @var int */ 190 private $activeGuests; 191 /** @var int */ 192 private $defaultPermissions; 193 /** @var int */ 194 private $callPermissions; 195 /** @var int */ 196 private $callFlag; 197 /** @var \DateTime|null */ 198 private $activeSince; 199 /** @var \DateTime|null */ 200 private $lastActivity; 201 /** @var int */ 202 private $lastMessageId; 203 /** @var IComment|null */ 204 private $lastMessage; 205 /** @var string */ 206 private $objectType; 207 /** @var string */ 208 private $objectId; 209 210 /** @var string */ 211 protected $currentUser; 212 /** @var Participant|null */ 213 protected $participant; 214 215 public function __construct(Manager $manager, 216 IDBConnection $db, 217 IEventDispatcher $dispatcher, 218 ITimeFactory $timeFactory, 219 IHasher $hasher, 220 int $id, 221 int $type, 222 int $readOnly, 223 int $listable, 224 int $lobbyState, 225 int $sipEnabled, 226 ?int $assignedSignalingServer, 227 string $token, 228 string $name, 229 string $description, 230 string $password, 231 string $serverUrl, 232 int $activeGuests, 233 int $defaultPermissions, 234 int $callPermissions, 235 int $callFlag, 236 ?\DateTime $activeSince, 237 ?\DateTime $lastActivity, 238 int $lastMessageId, 239 ?IComment $lastMessage, 240 ?\DateTime $lobbyTimer, 241 string $objectType, 242 string $objectId) { 243 $this->manager = $manager; 244 $this->db = $db; 245 $this->dispatcher = $dispatcher; 246 $this->timeFactory = $timeFactory; 247 $this->hasher = $hasher; 248 $this->id = $id; 249 $this->type = $type; 250 $this->readOnly = $readOnly; 251 $this->listable = $listable; 252 $this->lobbyState = $lobbyState; 253 $this->sipEnabled = $sipEnabled; 254 $this->assignedSignalingServer = $assignedSignalingServer; 255 $this->token = $token; 256 $this->name = $name; 257 $this->description = $description; 258 $this->password = $password; 259 $this->serverUrl = $serverUrl; 260 $this->activeGuests = $activeGuests; 261 $this->defaultPermissions = $defaultPermissions; 262 $this->callPermissions = $callPermissions; 263 $this->callFlag = $callFlag; 264 $this->activeSince = $activeSince; 265 $this->lastActivity = $lastActivity; 266 $this->lastMessageId = $lastMessageId; 267 $this->lastMessage = $lastMessage; 268 $this->lobbyTimer = $lobbyTimer; 269 $this->objectType = $objectType; 270 $this->objectId = $objectId; 271 } 272 273 public function getId(): int { 274 return $this->id; 275 } 276 277 public function getType(): int { 278 return $this->type; 279 } 280 281 public function getReadOnly(): int { 282 return $this->readOnly; 283 } 284 285 public function getListable(): int { 286 return $this->listable; 287 } 288 289 public function getLobbyState(): int { 290 $this->validateTimer(); 291 return $this->lobbyState; 292 } 293 294 public function getSIPEnabled(): int { 295 return $this->sipEnabled; 296 } 297 298 public function getLobbyTimer(): ?\DateTime { 299 $this->validateTimer(); 300 return $this->lobbyTimer; 301 } 302 303 protected function validateTimer(): void { 304 if ($this->lobbyTimer !== null && $this->lobbyTimer < $this->timeFactory->getDateTime()) { 305 $this->setLobby(Webinary::LOBBY_NONE, null, true); 306 } 307 } 308 309 public function getAssignedSignalingServer(): ?int { 310 return $this->assignedSignalingServer; 311 } 312 313 public function getToken(): string { 314 return $this->token; 315 } 316 317 public function getName(): string { 318 if ($this->type === self::TYPE_ONE_TO_ONE) { 319 if ($this->name === '') { 320 // TODO use DI 321 $participantService = \OC::$server->get(ParticipantService::class); 322 // Fill the room name with the participants for 1-to-1 conversations 323 $users = $participantService->getParticipantUserIds($this); 324 sort($users); 325 $this->setName(json_encode($users), ''); 326 } elseif (strpos($this->name, '["') !== 0) { 327 // TODO use DI 328 $participantService = \OC::$server->get(ParticipantService::class); 329 // Not the json array, but the old fallback when someone left 330 $users = $participantService->getParticipantUserIds($this); 331 if (count($users) !== 2) { 332 $users[] = $this->name; 333 } 334 sort($users); 335 $this->setName(json_encode($users), ''); 336 } 337 } 338 return $this->name; 339 } 340 341 public function getDisplayName(string $userId): string { 342 return $this->manager->resolveRoomDisplayName($this, $userId); 343 } 344 345 public function getDescription(): string { 346 return $this->description; 347 } 348 349 /** 350 * @deprecated Use ParticipantService::getGuestCount() instead 351 * @return int 352 */ 353 public function getActiveGuests(): int { 354 return $this->activeGuests; 355 } 356 357 public function getDefaultPermissions(): int { 358 return $this->defaultPermissions; 359 } 360 361 public function getCallPermissions(): int { 362 return $this->callPermissions; 363 } 364 365 public function getCallFlag(): int { 366 return $this->callFlag; 367 } 368 369 public function getActiveSince(): ?\DateTime { 370 return $this->activeSince; 371 } 372 373 public function getLastActivity(): ?\DateTime { 374 return $this->lastActivity; 375 } 376 377 public function getLastMessage(): ?IComment { 378 if ($this->lastMessageId && $this->lastMessage === null) { 379 $this->lastMessage = $this->manager->loadLastCommentInfo($this->lastMessageId); 380 if ($this->lastMessage === null) { 381 $this->lastMessageId = 0; 382 } 383 } 384 385 return $this->lastMessage; 386 } 387 388 public function getObjectType(): string { 389 return $this->objectType; 390 } 391 392 public function getObjectId(): string { 393 return $this->objectId; 394 } 395 396 public function hasPassword(): bool { 397 return $this->password !== ''; 398 } 399 400 public function getPassword(): string { 401 return $this->password; 402 } 403 404 public function getServerUrl(): string { 405 return $this->serverUrl; 406 } 407 408 public function isFederatedRemoteRoom(): bool { 409 return $this->serverUrl !== ''; 410 } 411 412 public function setParticipant(?string $userId, Participant $participant): void { 413 $this->currentUser = $userId; 414 $this->participant = $participant; 415 } 416 417 /** 418 * Return the room properties to send to the signaling server. 419 * 420 * @param string $userId 421 * @param bool $roomModified 422 * @return array 423 */ 424 public function getPropertiesForSignaling(string $userId, bool $roomModified = true): array { 425 $properties = [ 426 'name' => $this->getDisplayName($userId), 427 'type' => $this->getType(), 428 'lobby-state' => $this->getLobbyState(), 429 'lobby-timer' => $this->getLobbyTimer(), 430 'read-only' => $this->getReadOnly(), 431 'listable' => $this->getListable(), 432 'active-since' => $this->getActiveSince(), 433 'sip-enabled' => $this->getSIPEnabled(), 434 ]; 435 436 if ($roomModified) { 437 $properties = array_merge($properties, [ 438 'description' => $this->getDescription(), 439 ]); 440 } 441 442 $event = new SignalingRoomPropertiesEvent($this, $userId, $properties); 443 $this->dispatcher->dispatch(self::EVENT_BEFORE_SIGNALING_PROPERTIES, $event); 444 return $event->getProperties(); 445 } 446 447 /** 448 * @param string|null $userId 449 * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources), 450 * string to try loading a specific session 451 * null to try loading "any" 452 * @return Participant 453 * @throws ParticipantNotFoundException When the user is not a participant 454 */ 455 public function getParticipant(?string $userId, $sessionId = null): Participant { 456 if (!is_string($userId) || $userId === '') { 457 throw new ParticipantNotFoundException('Not a user'); 458 } 459 460 if ($this->currentUser === $userId && $this->participant instanceof Participant) { 461 if (!$sessionId 462 || ($this->participant->getSession() instanceof Session 463 && $this->participant->getSession()->getSessionId() === $sessionId)) { 464 return $this->participant; 465 } 466 } 467 468 $query = $this->db->getQueryBuilder(); 469 $helper = new SelectHelper(); 470 $helper->selectAttendeesTable($query); 471 $query->from('talk_attendees', 'a') 472 ->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) 473 ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId))) 474 ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) 475 ->setMaxResults(1); 476 477 if ($sessionId !== false) { 478 if ($sessionId !== null) { 479 $helper->selectSessionsTable($query); 480 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 481 $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), 482 $query->expr()->eq('a.id', 's.attendee_id') 483 )); 484 } else { 485 $helper->selectSessionsTable($query); // FIXME PROBLEM 486 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')); 487 } 488 } 489 490 $result = $query->executeQuery(); 491 $row = $result->fetch(); 492 $result->closeCursor(); 493 494 if ($row === false) { 495 throw new ParticipantNotFoundException('User is not a participant'); 496 } 497 498 if ($this->currentUser === $userId) { 499 $this->participant = $this->manager->createParticipantObject($this, $row); 500 return $this->participant; 501 } 502 503 return $this->manager->createParticipantObject($this, $row); 504 } 505 506 /** 507 * @param string|null $sessionId 508 * @return Participant 509 * @throws ParticipantNotFoundException When the user is not a participant 510 */ 511 public function getParticipantBySession(?string $sessionId): Participant { 512 if (!is_string($sessionId) || $sessionId === '' || $sessionId === '0') { 513 throw new ParticipantNotFoundException('Not a user'); 514 } 515 516 $query = $this->db->getQueryBuilder(); 517 $helper = new SelectHelper(); 518 $helper->selectAttendeesTable($query); 519 $helper->selectSessionsTable($query); 520 $query->from('talk_sessions', 's') 521 ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id')) 522 ->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId))) 523 ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) 524 ->setMaxResults(1); 525 $result = $query->executeQuery(); 526 $row = $result->fetch(); 527 $result->closeCursor(); 528 529 if ($row === false) { 530 throw new ParticipantNotFoundException('User is not a participant'); 531 } 532 533 return $this->manager->createParticipantObject($this, $row); 534 } 535 536 /** 537 * @param string $pin 538 * @return Participant 539 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) 540 */ 541 public function getParticipantByPin(string $pin): Participant { 542 $query = $this->db->getQueryBuilder(); 543 $helper = new SelectHelper(); 544 $helper->selectAttendeesTable($query); 545 $query->from('talk_attendees', 'a') 546 ->where($query->expr()->eq('a.pin', $query->createNamedParameter($pin))) 547 ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) 548 ->setMaxResults(1); 549 $result = $query->executeQuery(); 550 $row = $result->fetch(); 551 $result->closeCursor(); 552 553 if ($row === false) { 554 throw new ParticipantNotFoundException('User is not a participant'); 555 } 556 557 return $this->manager->createParticipantObject($this, $row); 558 } 559 560 /** 561 * @param int $attendeeId 562 * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources), 563 * string to try loading a specific session 564 * null to try loading "any" 565 * @return Participant 566 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) 567 */ 568 public function getParticipantByAttendeeId(int $attendeeId, $sessionId = null): Participant { 569 $query = $this->db->getQueryBuilder(); 570 $helper = new SelectHelper(); 571 $helper->selectAttendeesTable($query); 572 $query->from('talk_attendees', 'a') 573 ->where($query->expr()->eq('a.id', $query->createNamedParameter($attendeeId, IQueryBuilder::PARAM_INT))) 574 ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) 575 ->setMaxResults(1); 576 577 if ($sessionId !== false) { 578 if ($sessionId !== null) { 579 $helper->selectSessionsTable($query); 580 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 581 $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), 582 $query->expr()->eq('a.id', 's.attendee_id') 583 )); 584 } else { 585 $helper->selectSessionsTableMax($query); 586 $query->groupBy('a.id'); 587 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')); 588 } 589 } 590 591 $result = $query->executeQuery(); 592 $row = $result->fetch(); 593 $result->closeCursor(); 594 595 if ($row === false) { 596 throw new ParticipantNotFoundException('User is not a participant'); 597 } 598 599 return $this->manager->createParticipantObject($this, $row); 600 } 601 602 /** 603 * @param string $actorType 604 * @param string $actorId 605 * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources), 606 * string to try loading a specific session 607 * null to try loading "any" 608 * @return Participant 609 * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) 610 */ 611 public function getParticipantByActor(string $actorType, string $actorId, $sessionId = null): Participant { 612 if ($actorType === Attendee::ACTOR_USERS) { 613 return $this->getParticipant($actorId, $sessionId); 614 } 615 616 $query = $this->db->getQueryBuilder(); 617 $helper = new SelectHelper(); 618 $helper->selectAttendeesTable($query); 619 $query->from('talk_attendees', 'a') 620 ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType))) 621 ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId))) 622 ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) 623 ->setMaxResults(1); 624 625 if ($sessionId !== false) { 626 if ($sessionId !== null) { 627 $helper->selectSessionsTable($query); 628 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 629 $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), 630 $query->expr()->eq('a.id', 's.attendee_id') 631 )); 632 } else { 633 $helper->selectSessionsTableMax($query); 634 $query->groupBy('a.id'); 635 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')); 636 } 637 } 638 639 $result = $query->executeQuery(); 640 $row = $result->fetch(); 641 $result->closeCursor(); 642 643 if ($row === false) { 644 throw new ParticipantNotFoundException('User is not a participant'); 645 } 646 647 return $this->manager->createParticipantObject($this, $row); 648 } 649 650 public function deleteRoom(): void { 651 $event = new RoomEvent($this); 652 $this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_DELETE, $event); 653 $delete = $this->db->getQueryBuilder(); 654 655 // Delete attendees 656 $delete->delete('talk_attendees') 657 ->where($delete->expr()->eq('room_id', $delete->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 658 $delete->executeStatement(); 659 660 // Delete room 661 $delete->delete('talk_rooms') 662 ->where($delete->expr()->eq('id', $delete->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 663 $delete->executeStatement(); 664 665 $this->dispatcher->dispatch(self::EVENT_AFTER_ROOM_DELETE, $event); 666 if (class_exists(CriticalActionPerformedEvent::class)) { 667 $this->dispatcher->dispatchTyped(new CriticalActionPerformedEvent( 668 'Conversation "%s" deleted', 669 ['name' => $this->getName()], 670 )); 671 } 672 } 673 674 /** 675 * @param string $newName Currently it is only allowed to rename: self::TYPE_GROUP, self::TYPE_PUBLIC 676 * @param string|null $oldName 677 * @return bool True when the change was valid, false otherwise 678 */ 679 public function setName(string $newName, ?string $oldName = null): bool { 680 $oldName = $oldName !== null ? $oldName : $this->getName(); 681 if ($newName === $oldName) { 682 return false; 683 } 684 685 $event = new ModifyRoomEvent($this, 'name', $newName, $oldName); 686 $this->dispatcher->dispatch(self::EVENT_BEFORE_NAME_SET, $event); 687 688 $update = $this->db->getQueryBuilder(); 689 $update->update('talk_rooms') 690 ->set('name', $update->createNamedParameter($newName)) 691 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 692 $update->executeStatement(); 693 $this->name = $newName; 694 695 $this->dispatcher->dispatch(self::EVENT_AFTER_NAME_SET, $event); 696 697 return true; 698 } 699 700 /** 701 * @param string $description 702 * @return bool True when the change was valid, false otherwise 703 * @throws \LengthException when the given description is too long 704 */ 705 public function setDescription(string $description): bool { 706 $description = trim($description); 707 708 if (mb_strlen($description) > self::DESCRIPTION_MAXIMUM_LENGTH) { 709 throw new \LengthException('Conversation description is limited to ' . self::DESCRIPTION_MAXIMUM_LENGTH . ' characters'); 710 } 711 712 $oldDescription = $this->getDescription(); 713 if ($description === $oldDescription) { 714 return false; 715 } 716 717 $event = new ModifyRoomEvent($this, 'description', $description, $oldDescription); 718 $this->dispatcher->dispatch(self::EVENT_BEFORE_DESCRIPTION_SET, $event); 719 720 $update = $this->db->getQueryBuilder(); 721 $update->update('talk_rooms') 722 ->set('description', $update->createNamedParameter($description)) 723 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 724 $update->executeStatement(); 725 $this->description = $description; 726 727 $this->dispatcher->dispatch(self::EVENT_AFTER_DESCRIPTION_SET, $event); 728 729 return true; 730 } 731 732 /** 733 * @param string $password Currently it is only allowed to have a password for Room::TYPE_PUBLIC 734 * @return bool True when the change was valid, false otherwise 735 */ 736 public function setPassword(string $password): bool { 737 if ($this->getType() !== self::TYPE_PUBLIC) { 738 return false; 739 } 740 741 $hash = $password !== '' ? $this->hasher->hash($password) : ''; 742 743 $event = new ModifyRoomEvent($this, 'password', $password); 744 $this->dispatcher->dispatch(self::EVENT_BEFORE_PASSWORD_SET, $event); 745 746 $update = $this->db->getQueryBuilder(); 747 $update->update('talk_rooms') 748 ->set('password', $update->createNamedParameter($hash)) 749 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 750 $update->executeStatement(); 751 $this->password = $hash; 752 753 $this->dispatcher->dispatch(self::EVENT_AFTER_PASSWORD_SET, $event); 754 755 return true; 756 } 757 758 /** 759 * @param \DateTime $now 760 * @return bool 761 */ 762 public function setLastActivity(\DateTime $now): bool { 763 $update = $this->db->getQueryBuilder(); 764 $update->update('talk_rooms') 765 ->set('last_activity', $update->createNamedParameter($now, 'datetime')) 766 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 767 $update->executeStatement(); 768 769 $this->lastActivity = $now; 770 771 return true; 772 } 773 774 /** 775 * @param \DateTime $since 776 * @param int $callFlag 777 * @param bool $isGuest 778 * @return bool 779 */ 780 public function setActiveSince(\DateTime $since, int $callFlag, bool $isGuest): bool { 781 if ($isGuest && $this->getType() === self::TYPE_PUBLIC) { 782 $update = $this->db->getQueryBuilder(); 783 $update->update('talk_rooms') 784 ->set('active_guests', $update->createFunction($update->getColumnName('active_guests') . ' + 1')) 785 ->set( 786 'call_flag', 787 $update->expr()->bitwiseOr('call_flag', $callFlag) 788 ) 789 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 790 $update->executeStatement(); 791 792 $this->activeGuests++; 793 } elseif (!$isGuest) { 794 $update = $this->db->getQueryBuilder(); 795 $update->update('talk_rooms') 796 ->set( 797 'call_flag', 798 $update->expr()->bitwiseOr('call_flag', $callFlag) 799 ) 800 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 801 $update->executeStatement(); 802 } 803 804 if ($this->activeSince instanceof \DateTime) { 805 return false; 806 } 807 808 $update = $this->db->getQueryBuilder(); 809 $update->update('talk_rooms') 810 ->set('active_since', $update->createNamedParameter($since, IQueryBuilder::PARAM_DATE)) 811 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) 812 ->andWhere($update->expr()->isNull('active_since')); 813 $update->executeStatement(); 814 815 $this->activeSince = $since; 816 817 return true; 818 } 819 820 public function setLastMessage(IComment $message): void { 821 $update = $this->db->getQueryBuilder(); 822 $update->update('talk_rooms') 823 ->set('last_message', $update->createNamedParameter((int) $message->getId())) 824 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 825 $update->executeStatement(); 826 827 $this->lastMessage = $message; 828 $this->lastMessageId = (int) $message->getId(); 829 } 830 831 public function resetActiveSince(): bool { 832 $update = $this->db->getQueryBuilder(); 833 $update->update('talk_rooms') 834 ->set('active_guests', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) 835 ->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE)) 836 ->set('call_flag', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT)) 837 ->set('call_permissions', $update->createNamedParameter(Attendee::PERMISSIONS_DEFAULT, IQueryBuilder::PARAM_INT)) 838 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) 839 ->andWhere($update->expr()->isNotNull('active_since')); 840 841 $this->activeGuests = 0; 842 $this->activeSince = null; 843 844 return (bool) $update->executeStatement(); 845 } 846 847 public function setAssignedSignalingServer(?int $signalingServer): bool { 848 $update = $this->db->getQueryBuilder(); 849 $update->update('talk_rooms') 850 ->set('assigned_hpb', $update->createNamedParameter($signalingServer)) 851 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 852 853 if ($signalingServer !== null) { 854 $update->andWhere($update->expr()->isNull('assigned_hpb')); 855 } 856 857 return (bool) $update->executeStatement(); 858 } 859 860 /** 861 * @param int $newType Currently it is only allowed to change between `self::TYPE_GROUP` and `self::TYPE_PUBLIC` 862 * @return bool True when the change was valid, false otherwise 863 */ 864 public function setType(int $newType, bool $allowSwitchingOneToOne = false): bool { 865 if ($newType === $this->getType()) { 866 return true; 867 } 868 869 if (!$allowSwitchingOneToOne && $this->getType() === self::TYPE_ONE_TO_ONE) { 870 return false; 871 } 872 873 if (!in_array($newType, [self::TYPE_GROUP, self::TYPE_PUBLIC], true)) { 874 return false; 875 } 876 877 $oldType = $this->getType(); 878 879 $event = new ModifyRoomEvent($this, 'type', $newType, $oldType); 880 $this->dispatcher->dispatch(self::EVENT_BEFORE_TYPE_SET, $event); 881 882 $update = $this->db->getQueryBuilder(); 883 $update->update('talk_rooms') 884 ->set('type', $update->createNamedParameter($newType, IQueryBuilder::PARAM_INT)) 885 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 886 $update->executeStatement(); 887 888 $this->type = $newType; 889 890 if ($oldType === self::TYPE_PUBLIC) { 891 // Kick all guests and users that were not invited 892 $delete = $this->db->getQueryBuilder(); 893 $delete->delete('talk_attendees') 894 ->where($delete->expr()->eq('room_id', $delete->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))) 895 ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY))); 896 $delete->executeStatement(); 897 } 898 899 $this->dispatcher->dispatch(self::EVENT_AFTER_TYPE_SET, $event); 900 901 return true; 902 } 903 904 /** 905 * @param int $newState Currently it is only allowed to change between 906 * `self::READ_ONLY` and `self::READ_WRITE` 907 * Also it's only allowed on rooms of type 908 * `self::TYPE_GROUP` and `self::TYPE_PUBLIC` 909 * @return bool True when the change was valid, false otherwise 910 */ 911 public function setReadOnly(int $newState): bool { 912 $oldState = $this->getReadOnly(); 913 if ($newState === $oldState) { 914 return true; 915 } 916 917 if (!in_array($this->getType(), [self::TYPE_GROUP, self::TYPE_PUBLIC, self::TYPE_CHANGELOG], true)) { 918 return false; 919 } 920 921 if (!in_array($newState, [self::READ_ONLY, self::READ_WRITE], true)) { 922 return false; 923 } 924 925 $event = new ModifyRoomEvent($this, 'readOnly', $newState, $oldState); 926 $this->dispatcher->dispatch(self::EVENT_BEFORE_READONLY_SET, $event); 927 928 $update = $this->db->getQueryBuilder(); 929 $update->update('talk_rooms') 930 ->set('read_only', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT)) 931 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 932 $update->executeStatement(); 933 934 $this->readOnly = $newState; 935 936 $this->dispatcher->dispatch(self::EVENT_AFTER_READONLY_SET, $event); 937 938 return true; 939 } 940 941 /** 942 * @param int $newState New listable scope from self::LISTABLE_* 943 * Also it's only allowed on rooms of type 944 * `self::TYPE_GROUP` and `self::TYPE_PUBLIC` 945 * @return bool True when the change was valid, false otherwise 946 */ 947 public function setListable(int $newState): bool { 948 $oldState = $this->getListable(); 949 if ($newState === $oldState) { 950 return true; 951 } 952 953 if (!in_array($this->getType(), [self::TYPE_GROUP, self::TYPE_PUBLIC], true)) { 954 return false; 955 } 956 957 if (!in_array($newState, [ 958 Room::LISTABLE_NONE, 959 Room::LISTABLE_USERS, 960 Room::LISTABLE_ALL, 961 ], true)) { 962 return false; 963 } 964 965 $event = new ModifyRoomEvent($this, 'listable', $newState, $oldState); 966 $this->dispatcher->dispatch(self::EVENT_BEFORE_LISTABLE_SET, $event); 967 968 $update = $this->db->getQueryBuilder(); 969 $update->update('talk_rooms') 970 ->set('listable', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT)) 971 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 972 $update->executeStatement(); 973 974 $this->listable = $newState; 975 976 $this->dispatcher->dispatch(self::EVENT_AFTER_LISTABLE_SET, $event); 977 978 return true; 979 } 980 981 /** 982 * @param int $newState Currently it is only allowed to change between 983 * `Webinary::LOBBY_NON_MODERATORS` and `Webinary::LOBBY_NONE` 984 * Also it's not allowed in one-to-one conversations, 985 * file conversations and password request conversations. 986 * @param \DateTime|null $dateTime 987 * @param bool $timerReached 988 * @return bool True when the change was valid, false otherwise 989 */ 990 public function setLobby(int $newState, ?\DateTime $dateTime, bool $timerReached = false): bool { 991 $oldState = $this->lobbyState; 992 993 if (!in_array($this->getType(), [self::TYPE_GROUP, self::TYPE_PUBLIC], true)) { 994 return false; 995 } 996 997 if ($this->getObjectType() !== '') { 998 return false; 999 } 1000 1001 if (!in_array($newState, [Webinary::LOBBY_NON_MODERATORS, Webinary::LOBBY_NONE], true)) { 1002 return false; 1003 } 1004 1005 $event = new ModifyLobbyEvent($this, 'lobby', $newState, $oldState, $dateTime, $timerReached); 1006 $this->dispatcher->dispatch(self::EVENT_BEFORE_LOBBY_STATE_SET, $event); 1007 1008 $update = $this->db->getQueryBuilder(); 1009 $update->update('talk_rooms') 1010 ->set('lobby_state', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT)) 1011 ->set('lobby_timer', $update->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE)) 1012 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 1013 $update->executeStatement(); 1014 1015 $this->lobbyState = $newState; 1016 1017 $this->dispatcher->dispatch(self::EVENT_AFTER_LOBBY_STATE_SET, $event); 1018 1019 return true; 1020 } 1021 1022 public function setSIPEnabled(int $newSipEnabled): bool { 1023 $oldSipEnabled = $this->sipEnabled; 1024 1025 if ($newSipEnabled === $oldSipEnabled) { 1026 return false; 1027 } 1028 1029 if (!in_array($this->getType(), [self::TYPE_GROUP, self::TYPE_PUBLIC], true)) { 1030 return false; 1031 } 1032 1033 if (!in_array($newSipEnabled, [Webinary::SIP_ENABLED, Webinary::SIP_DISABLED], true)) { 1034 return false; 1035 } 1036 1037 if (preg_match(self::SIP_INCOMPATIBLE_REGEX, $this->token)) { 1038 return false; 1039 } 1040 1041 $event = new ModifyRoomEvent($this, 'sipEnabled', $newSipEnabled, $oldSipEnabled); 1042 $this->dispatcher->dispatch(self::EVENT_BEFORE_SIP_ENABLED_SET, $event); 1043 1044 $update = $this->db->getQueryBuilder(); 1045 $update->update('talk_rooms') 1046 ->set('sip_enabled', $update->createNamedParameter($newSipEnabled, IQueryBuilder::PARAM_INT)) 1047 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 1048 $update->executeStatement(); 1049 1050 $this->sipEnabled = $newSipEnabled; 1051 1052 $this->dispatcher->dispatch(self::EVENT_AFTER_SIP_ENABLED_SET, $event); 1053 1054 return true; 1055 } 1056 1057 public function setPermissions(string $level, int $newPermissions): bool { 1058 if ($level !== 'default' && $level !== 'call') { 1059 return false; 1060 } 1061 1062 $update = $this->db->getQueryBuilder(); 1063 $update->update('talk_rooms') 1064 ->set($level . '_permissions', $update->createNamedParameter($newPermissions, IQueryBuilder::PARAM_INT)) 1065 ->where($update->expr()->eq('id', $update->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); 1066 $update->executeStatement(); 1067 1068 if ($level === 'default') { 1069 $this->defaultPermissions = $newPermissions; 1070 } else { 1071 $this->callPermissions = $newPermissions; 1072 } 1073 1074 return true; 1075 } 1076 1077 /** 1078 * @param string $password 1079 * @return array 1080 */ 1081 public function verifyPassword(string $password): array { 1082 $event = new VerifyRoomPasswordEvent($this, $password); 1083 $this->dispatcher->dispatch(self::EVENT_PASSWORD_VERIFY, $event); 1084 1085 if ($event->isPasswordValid() !== null) { 1086 return [ 1087 'result' => $event->isPasswordValid(), 1088 'url' => $event->getRedirectUrl(), 1089 ]; 1090 } 1091 1092 return [ 1093 'result' => !$this->hasPassword() || $this->hasher->verify($password, $this->password), 1094 'url' => '', 1095 ]; 1096 } 1097} 1098