1<?php 2 3declare(strict_types=1); 4/** 5 * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com> 6 * 7 * @license GNU AGPL version 3 or any later version 8 * 9 * This program is free software: you can redistribute it and/or modify 10 * it under the terms of the GNU Affero General Public License as 11 * published by the Free Software Foundation, either version 3 of the 12 * License, or (at your option) any later version. 13 * 14 * This program is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 * GNU Affero General Public License for more details. 18 * 19 * You should have received a copy of the GNU Affero General Public License 20 * along with this program. If not, see <http://www.gnu.org/licenses/>. 21 * 22 */ 23 24namespace OCA\Talk; 25 26use OCA\Talk\Chat\CommentsManager; 27use OCA\Talk\Events\RoomEvent; 28use OCA\Talk\Exceptions\ParticipantNotFoundException; 29use OCA\Talk\Exceptions\RoomNotFoundException; 30use OCA\Talk\Model\Attendee; 31use OCA\Talk\Model\AttendeeMapper; 32use OCA\Talk\Model\SelectHelper; 33use OCA\Talk\Model\SessionMapper; 34use OCA\Talk\Service\ParticipantService; 35use OCP\App\IAppManager; 36use OCP\AppFramework\Utility\ITimeFactory; 37use OCP\Comments\IComment; 38use OCP\Comments\ICommentsManager; 39use OCP\Comments\NotFoundException; 40use OCP\DB\Exception as DBException; 41use OCP\DB\QueryBuilder\IQueryBuilder; 42use OCP\EventDispatcher\IEventDispatcher; 43use OCP\ICache; 44use OCP\IConfig; 45use OCP\IDBConnection; 46use OCP\IGroupManager; 47use OCP\IL10N; 48use OCP\IUser; 49use OCP\IUserManager; 50use OCP\Security\IHasher; 51use OCP\Security\ISecureRandom; 52 53class Manager { 54 public const EVENT_TOKEN_GENERATE = self::class . '::generateNewToken'; 55 56 /** @var IDBConnection */ 57 private $db; 58 /** @var IConfig */ 59 private $config; 60 /** @var Config */ 61 private $talkConfig; 62 /** @var IAppManager */ 63 private $appManager; 64 /** @var AttendeeMapper */ 65 private $attendeeMapper; 66 /** @var SessionMapper */ 67 private $sessionMapper; 68 /** @var ParticipantService */ 69 private $participantService; 70 /** @var ISecureRandom */ 71 private $secureRandom; 72 /** @var IUserManager */ 73 private $userManager; 74 /** @var IGroupManager */ 75 private $groupManager; 76 /** @var ICommentsManager */ 77 private $commentsManager; 78 /** @var TalkSession */ 79 private $talkSession; 80 /** @var IEventDispatcher */ 81 private $dispatcher; 82 /** @var ITimeFactory */ 83 protected $timeFactory; 84 /** @var IHasher */ 85 private $hasher; 86 /** @var IL10N */ 87 private $l; 88 89 public function __construct(IDBConnection $db, 90 IConfig $config, 91 Config $talkConfig, 92 IAppManager $appManager, 93 AttendeeMapper $attendeeMapper, 94 SessionMapper $sessionMapper, 95 ParticipantService $participantService, 96 ISecureRandom $secureRandom, 97 IUserManager $userManager, 98 IGroupManager $groupManager, 99 CommentsManager $commentsManager, 100 TalkSession $talkSession, 101 IEventDispatcher $dispatcher, 102 ITimeFactory $timeFactory, 103 IHasher $hasher, 104 IL10N $l) { 105 $this->db = $db; 106 $this->config = $config; 107 $this->talkConfig = $talkConfig; 108 $this->appManager = $appManager; 109 $this->attendeeMapper = $attendeeMapper; 110 $this->sessionMapper = $sessionMapper; 111 $this->participantService = $participantService; 112 $this->secureRandom = $secureRandom; 113 $this->userManager = $userManager; 114 $this->groupManager = $groupManager; 115 $this->commentsManager = $commentsManager; 116 $this->talkSession = $talkSession; 117 $this->dispatcher = $dispatcher; 118 $this->timeFactory = $timeFactory; 119 $this->hasher = $hasher; 120 $this->l = $l; 121 } 122 123 public function forAllRooms(callable $callback): void { 124 $query = $this->db->getQueryBuilder(); 125 $helper = new SelectHelper(); 126 $helper->selectRoomsTable($query); 127 $query->from('talk_rooms', 'r'); 128 129 $result = $query->executeQuery(); 130 while ($row = $result->fetch()) { 131 if ($row['token'] === null) { 132 // FIXME Temporary solution for the Talk6 release 133 continue; 134 } 135 136 $room = $this->createRoomObject($row); 137 $callback($room); 138 } 139 $result->closeCursor(); 140 } 141 142 /** 143 * @param array $row 144 * @return Room 145 */ 146 public function createRoomObject(array $row): Room { 147 $activeSince = null; 148 if (!empty($row['active_since'])) { 149 $activeSince = $this->timeFactory->getDateTime($row['active_since']); 150 } 151 152 $lastActivity = null; 153 if (!empty($row['last_activity'])) { 154 $lastActivity = $this->timeFactory->getDateTime($row['last_activity']); 155 } 156 157 $lobbyTimer = null; 158 if (!empty($row['lobby_timer'])) { 159 $lobbyTimer = $this->timeFactory->getDateTime($row['lobby_timer']); 160 } 161 162 $lastMessage = null; 163 if (!empty($row['comment_id'])) { 164 $lastMessage = $this->createCommentObject($row); 165 } 166 167 $assignedSignalingServer = $row['assigned_hpb']; 168 if ($assignedSignalingServer !== null) { 169 $assignedSignalingServer = (int) $assignedSignalingServer; 170 } 171 172 return new Room( 173 $this, 174 $this->db, 175 $this->dispatcher, 176 $this->timeFactory, 177 $this->hasher, 178 (int) $row['r_id'], 179 (int) $row['type'], 180 (int) $row['read_only'], 181 (int) $row['listable'], 182 (int) $row['lobby_state'], 183 (int) $row['sip_enabled'], 184 $assignedSignalingServer, 185 (string) $row['token'], 186 (string) $row['name'], 187 (string) $row['description'], 188 (string) $row['password'], 189 (string) $row['server_url'], 190 (int) $row['active_guests'], 191 (int) $row['default_permissions'], 192 (int) $row['call_permissions'], 193 (int) $row['call_flag'], 194 $activeSince, 195 $lastActivity, 196 (int) $row['last_message'], 197 $lastMessage, 198 $lobbyTimer, 199 (string) $row['object_type'], 200 (string) $row['object_id'] 201 ); 202 } 203 204 /** 205 * @param Room $room 206 * @param array $row 207 * @return Participant 208 */ 209 public function createParticipantObject(Room $room, array $row): Participant { 210 $attendee = $this->attendeeMapper->createAttendeeFromRow($row); 211 $session = null; 212 if (!empty($row['s_id'])) { 213 $session = $this->sessionMapper->createSessionFromRow($row); 214 } 215 216 return new Participant($room, $attendee, $session); 217 } 218 219 public function createCommentObject(array $row): ?IComment { 220 /** @psalm-suppress UndefinedInterfaceMethod */ 221 return $this->commentsManager->getCommentFromData([ 222 'id' => $row['comment_id'], 223 'parent_id' => $row['comment_parent_id'], 224 'topmost_parent_id' => $row['comment_topmost_parent_id'], 225 'children_count' => $row['comment_children_count'], 226 'message' => $row['comment_message'], 227 'verb' => $row['comment_verb'], 228 'actor_type' => $row['comment_actor_type'], 229 'actor_id' => $row['comment_actor_id'], 230 'object_type' => $row['comment_object_type'], 231 'object_id' => $row['comment_object_id'], 232 // Reference id column might not be there, so we need to fallback to null 233 'reference_id' => $row['comment_reference_id'] ?? null, 234 'creation_timestamp' => $row['comment_creation_timestamp'], 235 'latest_child_timestamp' => $row['comment_latest_child_timestamp'], 236 ]); 237 } 238 239 public function loadLastCommentInfo(int $id): ?IComment { 240 try { 241 return $this->commentsManager->get((string)$id); 242 } catch (NotFoundException $e) { 243 return null; 244 } 245 } 246 247 public function resetAssignedSignalingServers(ICache $cache): void { 248 $query = $this->db->getQueryBuilder(); 249 $helper = new SelectHelper(); 250 $helper->selectRoomsTable($query); 251 $query->from('talk_rooms', 'r') 252 ->where($query->expr()->isNotNull('r.assigned_hpb')); 253 254 $result = $query->executeQuery(); 255 while ($row = $result->fetch()) { 256 $room = $this->createRoomObject($row); 257 if (!$this->participantService->hasActiveSessions($room)) { 258 $room->setAssignedSignalingServer(null); 259 $cache->remove($room->getToken()); 260 } 261 } 262 $result->closeCursor(); 263 } 264 265 /** 266 * @param string $searchToken 267 * @param int|null $limit 268 * @param int|null $offset 269 * @return Room[] 270 */ 271 public function searchRoomsByToken(string $searchToken = '', int $limit = null, int $offset = null): array { 272 $query = $this->db->getQueryBuilder(); 273 $helper = new SelectHelper(); 274 $helper->selectRoomsTable($query); 275 $query->from('talk_rooms', 'r') 276 ->setMaxResults(1); 277 278 if ($searchToken !== '') { 279 $query->where($query->expr()->iLike('r.token', $query->createNamedParameter( 280 '%' . $this->db->escapeLikeParameter($searchToken) . '%' 281 ))); 282 } 283 284 $query->setMaxResults($limit) 285 ->setFirstResult($offset) 286 ->orderBy('r.token', 'ASC'); 287 $result = $query->executeQuery(); 288 289 $rooms = []; 290 while ($row = $result->fetch()) { 291 if ($row['token'] === null) { 292 // FIXME Temporary solution for the Talk6 release 293 continue; 294 } 295 296 $rooms[] = $this->createRoomObject($row); 297 } 298 $result->closeCursor(); 299 300 return $rooms; 301 } 302 303 /** 304 * @param string $userId 305 * @param array $sessionIds A list of talk sessions to consider for loading (otherwise no session is loaded) 306 * @param bool $includeLastMessage 307 * @return Room[] 308 */ 309 public function getRoomsForUser(string $userId, array $sessionIds = [], bool $includeLastMessage = false): array { 310 return $this->getRoomsForActor(Attendee::ACTOR_USERS, $userId, $sessionIds, $includeLastMessage); 311 } 312 313 /** 314 * @param string $actorType 315 * @param string $actorId 316 * @param array $sessionIds A list of talk sessions to consider for loading (otherwise no session is loaded) 317 * @param bool $includeLastMessage 318 * @return Room[] 319 */ 320 public function getRoomsForActor(string $actorType, string $actorId, array $sessionIds = [], bool $includeLastMessage = false): array { 321 $query = $this->db->getQueryBuilder(); 322 $helper = new SelectHelper(); 323 $helper->selectRoomsTable($query); 324 $helper->selectAttendeesTable($query); 325 $query->from('talk_rooms', 'r') 326 ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( 327 $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), 328 $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), 329 $query->expr()->eq('a.room_id', 'r.id') 330 )) 331 ->where($query->expr()->isNotNull('a.id')); 332 333 if (!empty($sessionIds)) { 334 $helper->selectSessionsTable($query); 335 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 336 $query->expr()->eq('a.id', 's.attendee_id'), 337 $query->expr()->in('s.session_id', $query->createNamedParameter($sessionIds, IQueryBuilder::PARAM_STR_ARRAY)) 338 )); 339 } 340 341 if ($includeLastMessage) { 342 $this->loadLastMessageInfo($query); 343 } 344 345 $result = $query->executeQuery(); 346 $rooms = []; 347 while ($row = $result->fetch()) { 348 if ($row['token'] === null) { 349 // FIXME Temporary solution for the Talk6 release 350 continue; 351 } 352 353 $room = $this->createRoomObject($row); 354 if ($actorType === Attendee::ACTOR_USERS && isset($row['actor_id'])) { 355 $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); 356 } 357 $rooms[] = $room; 358 } 359 $result->closeCursor(); 360 361 return $rooms; 362 } 363 364 /** 365 * @param string $userId 366 * @return Room[] 367 */ 368 public function getLeftOneToOneRoomsForUser(string $userId): array { 369 $query = $this->db->getQueryBuilder(); 370 $helper = new SelectHelper(); 371 $helper->selectRoomsTable($query); 372 $query->from('talk_rooms', 'r') 373 ->where($query->expr()->eq('r.type', $query->createNamedParameter(Room::TYPE_ONE_TO_ONE))) 374 ->andWhere($query->expr()->like('r.name', $query->createNamedParameter('%' . $this->db->escapeLikeParameter(json_encode($userId)) . '%'))); 375 376 $result = $query->executeQuery(); 377 $rooms = []; 378 while ($row = $result->fetch()) { 379 if ($row['token'] === null) { 380 // FIXME Temporary solution for the Talk6 release 381 continue; 382 } 383 384 $room = $this->createRoomObject($row); 385 $rooms[] = $room; 386 } 387 $result->closeCursor(); 388 389 return $rooms; 390 } 391 392 /** 393 * @param string $userId 394 * @return string[] 395 */ 396 public function getRoomTokensForUser(string $userId): array { 397 $query = $this->db->getQueryBuilder(); 398 $query->select('r.token') 399 ->from('talk_attendees', 'a') 400 ->leftJoin('a', 'talk_rooms', 'r', $query->expr()->eq('a.room_id', 'r.id')) 401 ->where($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId))) 402 ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))); 403 404 $result = $query->executeQuery(); 405 $roomTokens = []; 406 while ($row = $result->fetch()) { 407 if ($row['token'] === null) { 408 // FIXME Temporary solution for the Talk6 release 409 continue; 410 } 411 412 $roomTokens[] = $row['token']; 413 } 414 $result->closeCursor(); 415 416 return $roomTokens; 417 } 418 419 /** 420 * Returns rooms that are listable where the current user is not a participant. 421 * 422 * @param string $userId user id 423 * @param string $term search term 424 * @return Room[] 425 */ 426 public function getListedRoomsForUser(string $userId, string $term = ''): array { 427 $allowedRoomTypes = [Room::TYPE_GROUP, Room::TYPE_PUBLIC]; 428 $allowedListedTypes = [Room::LISTABLE_ALL]; 429 if (!$this->isGuestUser($userId)) { 430 $allowedListedTypes[] = Room::LISTABLE_USERS; 431 } 432 $query = $this->db->getQueryBuilder(); 433 $helper = new SelectHelper(); 434 $helper->selectRoomsTable($query); 435 $query->from('talk_rooms', 'r') 436 ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( 437 $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), 438 $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), 439 $query->expr()->eq('a.room_id', 'r.id') 440 )) 441 ->where($query->expr()->isNull('a.id')) 442 ->andWhere($query->expr()->in('r.type', $query->createNamedParameter($allowedRoomTypes, IQueryBuilder::PARAM_INT_ARRAY))) 443 ->andWhere($query->expr()->in('r.listable', $query->createNamedParameter($allowedListedTypes, IQueryBuilder::PARAM_INT_ARRAY))); 444 445 if ($term !== '') { 446 $query->andWhere( 447 $query->expr()->iLike('name', $query->createNamedParameter( 448 '%' . $this->db->escapeLikeParameter($term). '%' 449 )) 450 ); 451 } 452 453 $result = $query->executeQuery(); 454 $rooms = []; 455 while ($row = $result->fetch()) { 456 $room = $this->createRoomObject($row); 457 $rooms[] = $room; 458 } 459 $result->closeCursor(); 460 461 return $rooms; 462 } 463 464 /** 465 * Does *not* return public rooms for participants that have not been invited 466 * 467 * @param int $roomId 468 * @param string|null $userId 469 * @return Room 470 * @throws RoomNotFoundException 471 */ 472 public function getRoomForUser(int $roomId, ?string $userId): Room { 473 $query = $this->db->getQueryBuilder(); 474 $helper = new SelectHelper(); 475 $helper->selectRoomsTable($query); 476 $query->from('talk_rooms', 'r') 477 ->where($query->expr()->eq('r.id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); 478 479 if ($userId !== null) { 480 // Non guest user 481 $helper->selectAttendeesTable($query); 482 $query->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( 483 $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), 484 $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), 485 $query->expr()->eq('a.room_id', 'r.id') 486 )) 487 ->andWhere($query->expr()->isNotNull('a.id')); 488 } 489 490 $result = $query->executeQuery(); 491 $row = $result->fetch(); 492 $result->closeCursor(); 493 494 if ($row === false) { 495 throw new RoomNotFoundException(); 496 } 497 498 if ($row['token'] === null) { 499 // FIXME Temporary solution for the Talk6 release 500 throw new RoomNotFoundException(); 501 } 502 503 $room = $this->createRoomObject($row); 504 if ($userId !== null && isset($row['actor_id'])) { 505 $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); 506 } 507 508 if ($userId === null && $room->getType() !== Room::TYPE_PUBLIC) { 509 throw new RoomNotFoundException(); 510 } 511 512 return $room; 513 } 514 515 /** 516 * Returns room object for a user by token. 517 * 518 * Also returns: 519 * - public rooms for participants that have not been invited 520 * - listable rooms for participants that have not been invited 521 * 522 * This is useful so they can join. 523 * 524 * @param string $token 525 * @param string|null $userId 526 * @param string|null $sessionId 527 * @param bool $includeLastMessage 528 * @param bool $isSIPBridgeRequest 529 * @return Room 530 * @throws RoomNotFoundException 531 */ 532 public function getRoomForUserByToken(string $token, ?string $userId, ?string $sessionId = null, bool $includeLastMessage = false, bool $isSIPBridgeRequest = false): Room { 533 $query = $this->db->getQueryBuilder(); 534 $helper = new SelectHelper(); 535 $helper->selectRoomsTable($query); 536 $query->from('talk_rooms', 'r') 537 ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))) 538 ->setMaxResults(1); 539 540 if ($userId !== null) { 541 // Non guest user 542 $helper->selectAttendeesTable($query); 543 $query->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( 544 $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), 545 $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), 546 $query->expr()->eq('a.room_id', 'r.id') 547 )); 548 if ($sessionId !== null) { 549 $helper->selectSessionsTable($query); 550 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 551 $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), 552 $query->expr()->eq('a.id', 's.attendee_id') 553 )); 554 } 555 } 556 557 if ($includeLastMessage) { 558 $this->loadLastMessageInfo($query); 559 } 560 561 $result = $query->executeQuery(); 562 $row = $result->fetch(); 563 $result->closeCursor(); 564 565 if ($row === false) { 566 throw new RoomNotFoundException(); 567 } 568 569 if ($row['token'] === null) { 570 // FIXME Temporary solution for the Talk6 release 571 throw new RoomNotFoundException(); 572 } 573 574 $room = $this->createRoomObject($row); 575 if ($userId !== null && isset($row['actor_id'])) { 576 $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); 577 } 578 579 if ($isSIPBridgeRequest || $room->getType() === Room::TYPE_PUBLIC) { 580 return $room; 581 } 582 583 if ($userId !== null) { 584 // user already joined that room before 585 if ($row['actor_id'] === $userId) { 586 return $room; 587 } 588 589 // never joined before but found in listing 590 $listable = (int)$row['listable']; 591 if ($this->isRoomListableByUser($room, $userId)) { 592 return $room; 593 } 594 } 595 596 throw new RoomNotFoundException(); 597 } 598 599 /** 600 * @param int $roomId 601 * @return Room 602 * @throws RoomNotFoundException 603 */ 604 public function getRoomById(int $roomId): Room { 605 $query = $this->db->getQueryBuilder(); 606 $helper = new SelectHelper(); 607 $helper->selectRoomsTable($query); 608 $query->from('talk_rooms', 'r') 609 ->where($query->expr()->eq('r.id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); 610 611 $result = $query->executeQuery(); 612 $row = $result->fetch(); 613 $result->closeCursor(); 614 615 if ($row === false) { 616 throw new RoomNotFoundException(); 617 } 618 619 if ($row['token'] === null) { 620 // FIXME Temporary solution for the Talk6 release 621 throw new RoomNotFoundException(); 622 } 623 624 return $this->createRoomObject($row); 625 } 626 627 /** 628 * @param string $token 629 * @param string $actorType 630 * @param string $actorId 631 * @param string|null $sessionId 632 * @return Room 633 * @throws RoomNotFoundException 634 */ 635 public function getRoomByActor(string $token, string $actorType, string $actorId, ?string $sessionId = null, ?string $serverUrl = null): Room { 636 $query = $this->db->getQueryBuilder(); 637 $helper = new SelectHelper(); 638 $helper->selectRoomsTable($query); 639 $helper->selectAttendeesTable($query); 640 $query->from('talk_rooms', 'r') 641 ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( 642 $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), 643 $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), 644 $query->expr()->eq('a.room_id', 'r.id') 645 )) 646 ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); 647 648 if ($serverUrl === null) { 649 $query->andWhere($query->expr()->isNull('r.server_url')); 650 } else { 651 $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl))); 652 } 653 654 if ($sessionId !== null) { 655 $helper->selectSessionsTable($query); 656 $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( 657 $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)), 658 $query->expr()->eq('a.id', 's.attendee_id') 659 )); 660 } 661 662 $result = $query->executeQuery(); 663 $row = $result->fetch(); 664 $result->closeCursor(); 665 666 if ($row === false) { 667 throw new RoomNotFoundException(); 668 } 669 670 if ($row['token'] === null) { 671 // FIXME Temporary solution for the Talk6 release 672 throw new RoomNotFoundException(); 673 } 674 675 $room = $this->createRoomObject($row); 676 if ($actorType === Attendee::ACTOR_USERS && isset($row['actor_id'])) { 677 $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); 678 } 679 680 return $room; 681 } 682 683 /** 684 * @param string $token 685 * @param string|null $preloadUserId Load this participants information if possible 686 * @return Room 687 * @throws RoomNotFoundException 688 */ 689 public function getRoomByToken(string $token, ?string $preloadUserId = null, ?string $serverUrl = null): Room { 690 $preloadUserId = $preloadUserId === '' ? null : $preloadUserId; 691 if ($preloadUserId !== null) { 692 return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId, null, $serverUrl); 693 } 694 695 $query = $this->db->getQueryBuilder(); 696 $helper = new SelectHelper(); 697 $helper->selectRoomsTable($query); 698 $query->from('talk_rooms', 'r') 699 ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); 700 701 if ($serverUrl === null) { 702 $query->andWhere($query->expr()->isNull('r.server_url')); 703 } else { 704 $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl))); 705 } 706 707 708 $result = $query->executeQuery(); 709 $row = $result->fetch(); 710 $result->closeCursor(); 711 712 if ($row === false) { 713 throw new RoomNotFoundException(); 714 } 715 716 if ($row['token'] === null) { 717 // FIXME Temporary solution for the Talk6 release 718 throw new RoomNotFoundException(); 719 } 720 721 return $this->createRoomObject($row); 722 } 723 724 /** 725 * @param string $objectType 726 * @param string $objectId 727 * @return Room 728 * @throws RoomNotFoundException 729 */ 730 public function getRoomByObject(string $objectType, string $objectId): Room { 731 $query = $this->db->getQueryBuilder(); 732 $helper = new SelectHelper(); 733 $helper->selectRoomsTable($query); 734 $query->from('talk_rooms', 'r') 735 ->where($query->expr()->eq('r.object_type', $query->createNamedParameter($objectType))) 736 ->andWhere($query->expr()->eq('r.object_id', $query->createNamedParameter($objectId))); 737 738 $result = $query->executeQuery(); 739 $row = $result->fetch(); 740 $result->closeCursor(); 741 742 if ($row === false) { 743 throw new RoomNotFoundException(); 744 } 745 746 if ($row['token'] === null) { 747 // FIXME Temporary solution for the Talk6 release 748 throw new RoomNotFoundException(); 749 } 750 751 return $this->createRoomObject($row); 752 } 753 754 /** 755 * @param string|null $userId 756 * @param string|null $sessionId 757 * @return Room 758 * @throws RoomNotFoundException 759 */ 760 public function getRoomForSession(?string $userId, ?string $sessionId): Room { 761 if ($sessionId === '' || $sessionId === '0') { 762 throw new RoomNotFoundException(); 763 } 764 765 $query = $this->db->getQueryBuilder(); 766 $helper = new SelectHelper(); 767 $helper->selectRoomsTable($query); 768 $helper->selectAttendeesTable($query); 769 $helper->selectSessionsTable($query); 770 $query->from('talk_sessions', 's') 771 ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('a.id', 's.attendee_id')) 772 ->leftJoin('a', 'talk_rooms', 'r', $query->expr()->eq('a.room_id', 'r.id')) 773 ->where($query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId))) 774 ->setMaxResults(1); 775 776 $result = $query->executeQuery(); 777 $row = $result->fetch(); 778 $result->closeCursor(); 779 780 if ($row === false || !$row['r_id']) { 781 throw new RoomNotFoundException(); 782 } 783 784 if ($userId !== null) { 785 if ($row['actor_type'] !== Attendee::ACTOR_USERS || $userId !== $row['actor_id']) { 786 throw new RoomNotFoundException(); 787 } 788 } else { 789 if ($row['actor_type'] !== Attendee::ACTOR_GUESTS) { 790 throw new RoomNotFoundException(); 791 } 792 } 793 794 if ($row['token'] === null) { 795 // FIXME Temporary solution for the Talk6 release 796 throw new RoomNotFoundException(); 797 } 798 799 $room = $this->createRoomObject($row); 800 $participant = $this->createParticipantObject($room, $row); 801 $room->setParticipant($row['actor_id'], $participant); 802 803 if ($room->getType() === Room::TYPE_PUBLIC || !in_array($participant->getAttendee()->getParticipantType(), [Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], true)) { 804 return $room; 805 } 806 807 throw new RoomNotFoundException(); 808 } 809 810 /** 811 * @param string $participant1 812 * @param string $participant2 813 * @return Room 814 * @throws RoomNotFoundException 815 */ 816 public function getOne2OneRoom(string $participant1, string $participant2): Room { 817 $users = [$participant1, $participant2]; 818 sort($users); 819 $name = json_encode($users); 820 821 $query = $this->db->getQueryBuilder(); 822 $helper = new SelectHelper(); 823 $helper->selectRoomsTable($query); 824 $query->from('talk_rooms', 'r') 825 ->where($query->expr()->eq('r.type', $query->createNamedParameter(Room::TYPE_ONE_TO_ONE, IQueryBuilder::PARAM_INT))) 826 ->andWhere($query->expr()->eq('r.name', $query->createNamedParameter($name))); 827 828 $result = $query->executeQuery(); 829 $row = $result->fetch(); 830 $result->closeCursor(); 831 832 if ($row === false) { 833 throw new RoomNotFoundException(); 834 } 835 836 if ($row['token'] === null) { 837 // FIXME Temporary solution for the Talk6 release 838 throw new RoomNotFoundException(); 839 } 840 841 return $this->createRoomObject($row); 842 } 843 844 /** 845 * Makes sure the user is part of a changelog room and returns it 846 * 847 * @param string $userId 848 * @return Room 849 */ 850 public function getChangelogRoom(string $userId): Room { 851 $query = $this->db->getQueryBuilder(); 852 $helper = new SelectHelper(); 853 $helper->selectRoomsTable($query); 854 $query->from('talk_rooms', 'r') 855 ->where($query->expr()->eq('r.type', $query->createNamedParameter(Room::TYPE_CHANGELOG, IQueryBuilder::PARAM_INT))) 856 ->andWhere($query->expr()->eq('r.name', $query->createNamedParameter($userId))); 857 858 $result = $query->executeQuery(); 859 $row = $result->fetch(); 860 $result->closeCursor(); 861 862 if ($row === false) { 863 $room = $this->createRoom(Room::TYPE_CHANGELOG, $userId); 864 $room->setReadOnly(Room::READ_ONLY); 865 $room->setListable(Room::LISTABLE_NONE); 866 867 $user = $this->userManager->get($userId); 868 $this->participantService->addUsers($room,[[ 869 'actorType' => Attendee::ACTOR_USERS, 870 'actorId' => $userId, 871 'displayName' => $user ? $user->getDisplayName() : $userId, 872 ]]); 873 return $room; 874 } 875 876 $room = $this->createRoomObject($row); 877 878 try { 879 $room->getParticipant($userId, false); 880 } catch (ParticipantNotFoundException $e) { 881 $user = $this->userManager->get($userId); 882 $this->participantService->addUsers($room,[[ 883 'actorType' => Attendee::ACTOR_USERS, 884 'actorId' => $userId, 885 'displayName' => $user ? $user->getDisplayName() : $userId, 886 ]]); 887 } 888 889 return $room; 890 } 891 892 /** 893 * @param int $type 894 * @param string $name 895 * @param string $objectType 896 * @param string $objectId 897 * @return Room 898 */ 899 public function createRoom(int $type, string $name = '', string $objectType = '', string $objectId = ''): Room { 900 $token = $this->getNewToken(); 901 902 $insert = $this->db->getQueryBuilder(); 903 $insert->insert('talk_rooms') 904 ->values( 905 [ 906 'name' => $insert->createNamedParameter($name), 907 'type' => $insert->createNamedParameter($type, IQueryBuilder::PARAM_INT), 908 'token' => $insert->createNamedParameter($token), 909 ] 910 ); 911 912 if (!empty($objectType) && !empty($objectId)) { 913 $insert->setValue('object_type', $insert->createNamedParameter($objectType)) 914 ->setValue('object_id', $insert->createNamedParameter($objectId)); 915 } 916 917 $insert->executeStatement(); 918 $roomId = $insert->getLastInsertId(); 919 920 $room = $this->getRoomById($roomId); 921 922 $event = new RoomEvent($room); 923 $this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_CREATE, $event); 924 925 return $room; 926 } 927 928 /** 929 * @param int $type 930 * @param string $name 931 * @return Room 932 * @throws DBException 933 */ 934 public function createRemoteRoom(int $type, string $name, string $token, string $serverUrl): Room { 935 $qb = $this->db->getQueryBuilder(); 936 937 $qb->insert('talk_rooms') 938 ->values([ 939 'name' => $qb->createNamedParameter($name), 940 'type' => $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), 941 'token' => $qb->createNamedParameter($token), 942 'server_url' => $qb->createNamedParameter($serverUrl), 943 ]); 944 945 $qb->executeStatement(); 946 $roomId = $qb->getLastInsertId(); 947 948 return $this->getRoomById($roomId); 949 } 950 951 public function resolveRoomDisplayName(Room $room, string $userId): string { 952 if ($room->getObjectType() === 'share:password') { 953 return $this->l->t('Password request: %s', [$room->getName()]); 954 } 955 if ($room->getType() === Room::TYPE_CHANGELOG) { 956 return $this->l->t('Talk updates ✅'); 957 } 958 if ($userId === '' && $room->getType() !== Room::TYPE_PUBLIC) { 959 return $this->l->t('Private conversation'); 960 } 961 962 963 if ($room->getType() !== Room::TYPE_ONE_TO_ONE && $room->getName() === '') { 964 $room->setName($this->getRoomNameByParticipants($room)); 965 } 966 967 // Set the room name to the other participant for one-to-one rooms 968 if ($room->getType() === Room::TYPE_ONE_TO_ONE) { 969 if ($userId === '') { 970 return $this->l->t('Private conversation'); 971 } 972 973 $users = json_decode($room->getName(), true); 974 $otherParticipant = ''; 975 $userIsParticipant = false; 976 977 foreach ($users as $participantId) { 978 if ($participantId !== $userId) { 979 $user = $this->userManager->get($participantId); 980 $otherParticipant = $user instanceof IUser ? $user->getDisplayName() : $participantId; 981 } else { 982 $userIsParticipant = true; 983 } 984 } 985 986 if (!$userIsParticipant) { 987 // Do not leak the name of rooms the user is not a part of 988 return $this->l->t('Private conversation'); 989 } 990 991 if ($otherParticipant === '' && $room->getName() !== '') { 992 $user = $this->userManager->get($room->getName()); 993 $otherParticipant = $user instanceof IUser ? $user->getDisplayName() : $this->l->t('Deleted user (%s)', $room->getName()); 994 } 995 996 return $otherParticipant; 997 } 998 999 if (!$this->isRoomListableByUser($room, $userId)) { 1000 try { 1001 if ($userId === '') { 1002 $sessionId = $this->talkSession->getSessionForRoom($room->getToken()); 1003 $room->getParticipantBySession($sessionId); 1004 } else { 1005 $room->getParticipant($userId, false); 1006 } 1007 } catch (ParticipantNotFoundException $e) { 1008 // Do not leak the name of rooms the user is not a part of 1009 return $this->l->t('Private conversation'); 1010 } 1011 } 1012 1013 return $room->getName(); 1014 } 1015 1016 /** 1017 * Returns whether the given room is listable for the given user. 1018 * 1019 * @param Room $room room 1020 * @param string|null $userId user id 1021 */ 1022 public function isRoomListableByUser(Room $room, ?string $userId): bool { 1023 if ($userId === null) { 1024 // not listable for guest users with no account 1025 return false; 1026 } 1027 1028 if ($room->getListable() === Room::LISTABLE_ALL) { 1029 return true; 1030 } 1031 1032 if ($room->getListable() === Room::LISTABLE_USERS && !$this->isGuestUser($userId)) { 1033 return true; 1034 } 1035 1036 return false; 1037 } 1038 1039 protected function getRoomNameByParticipants(Room $room): string { 1040 $users = $this->participantService->getParticipantUserIds($room); 1041 $displayNames = []; 1042 1043 foreach ($users as $participantId) { 1044 $user = $this->userManager->get($participantId); 1045 $displayNames[] = $user instanceof IUser ? $user->getDisplayName() : $participantId; 1046 } 1047 1048 $roomName = implode(', ', $displayNames); 1049 if (mb_strlen($roomName) > 64) { 1050 $roomName = mb_substr($roomName, 0, 60) . '…'; 1051 } 1052 return $roomName; 1053 } 1054 1055 /** 1056 * @return string 1057 */ 1058 protected function getNewToken(): string { 1059 $entropy = (int) $this->config->getAppValue('spreed', 'token_entropy', 8); 1060 $entropy = max(8, $entropy); // For update cases 1061 $digitsOnly = $this->talkConfig->isSIPConfigured(); 1062 if ($digitsOnly) { 1063 // Increase default token length as we only use numbers 1064 $entropy = max(10, $entropy); 1065 } 1066 1067 $query = $this->db->getQueryBuilder(); 1068 $query->select('r.id') 1069 ->from('talk_rooms', 'r') 1070 ->where($query->expr()->eq('r.token', $query->createParameter('token'))); 1071 1072 $i = 0; 1073 while ($i < 1000) { 1074 try { 1075 $token = $this->generateNewToken($query, $entropy, $digitsOnly); 1076 if (\in_array($token, ['settings', 'backend'], true)) { 1077 throw new \OutOfBoundsException('Reserved word'); 1078 } 1079 return $token; 1080 } catch (\OutOfBoundsException $e) { 1081 $i++; 1082 if ($entropy >= 30 || $i >= 999) { 1083 // Max entropy of 30 1084 $i = 0; 1085 } 1086 } 1087 } 1088 1089 $entropy++; 1090 $this->config->setAppValue('spreed', 'token_entropy', $entropy); 1091 return $this->generateNewToken($query, $entropy, $digitsOnly); 1092 } 1093 1094 /** 1095 * @param IQueryBuilder $query 1096 * @param int $entropy 1097 * @param bool $digitsOnly 1098 * @return string 1099 * @throws \OutOfBoundsException 1100 */ 1101 protected function generateNewToken(IQueryBuilder $query, int $entropy, bool $digitsOnly): string { 1102 if (!$digitsOnly) { 1103 $chars = str_replace(['l', '0', '1'], '', ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS); 1104 $token = $this->secureRandom->generate($entropy, $chars); 1105 } else { 1106 $chars = ISecureRandom::CHAR_DIGITS; 1107 $token = ''; 1108 // Do not allow to start with a '0' as that is a special mode on the phone server 1109 // Also there are issues with some providers when you enter the same number twice 1110 // consecutive too fast, so we avoid this as well. 1111 $lastDigit = '0'; 1112 for ($i = 0; $i < $entropy; $i++) { 1113 $lastDigit = $this->secureRandom->generate(1, 1114 str_replace($lastDigit, '', $chars) 1115 ); 1116 $token .= $lastDigit; 1117 } 1118 } 1119 1120 $query->setParameter('token', $token); 1121 $result = $query->executeQuery(); 1122 $row = $result->fetch(); 1123 $result->closeCursor(); 1124 1125 if (is_array($row)) { 1126 // Token already in use 1127 throw new \OutOfBoundsException(); 1128 } 1129 return $token; 1130 } 1131 1132 public function isValidParticipant(string $userId): bool { 1133 return $this->userManager->userExists($userId); 1134 } 1135 1136 /** 1137 * Returns whether the given user id is a guest user from 1138 * the guest app 1139 * 1140 * @param string $userId user id to check 1141 * @return bool true if the user is a guest, false otherwise 1142 */ 1143 public function isGuestUser(string $userId): bool { 1144 if (!$this->appManager->isEnabledForUser('guests')) { 1145 return false; 1146 } 1147 // TODO: retrieve guest group name from app once exposed 1148 return $this->groupManager->isInGroup($userId, 'guest_app'); 1149 } 1150 1151 protected function loadLastMessageInfo(IQueryBuilder $query): void { 1152 $query->leftJoin('r','comments', 'c', $query->expr()->eq('r.last_message', 'c.id')); 1153 $query->selectAlias('c.id', 'comment_id'); 1154 $query->selectAlias('c.parent_id', 'comment_parent_id'); 1155 $query->selectAlias('c.topmost_parent_id', 'comment_topmost_parent_id'); 1156 $query->selectAlias('c.children_count', 'comment_children_count'); 1157 $query->selectAlias('c.message', 'comment_message'); 1158 $query->selectAlias('c.verb', 'comment_verb'); 1159 $query->selectAlias('c.actor_type', 'comment_actor_type'); 1160 $query->selectAlias('c.actor_id', 'comment_actor_id'); 1161 $query->selectAlias('c.object_type', 'comment_object_type'); 1162 $query->selectAlias('c.object_id', 'comment_object_id'); 1163 if ($this->config->getAppValue('spreed', 'has_reference_id', 'no') === 'yes') { 1164 // Only try to load the reference_id column when it should be there 1165 $query->selectAlias('c.reference_id', 'comment_reference_id'); 1166 } 1167 $query->selectAlias('c.creation_timestamp', 'comment_creation_timestamp'); 1168 $query->selectAlias('c.latest_child_timestamp', 'comment_latest_child_timestamp'); 1169 } 1170} 1171