You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1525 lines
46 KiB

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