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