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.

577 lines
15 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\Events\BeforeSignalingRoomPropertiesSentEvent;
  9. use OCA\Talk\Exceptions\ParticipantNotFoundException;
  10. use OCA\Talk\Model\Attendee;
  11. use OCA\Talk\Model\SelectHelper;
  12. use OCA\Talk\Model\Session;
  13. use OCA\Talk\Service\ParticipantService;
  14. use OCA\Talk\Service\RecordingService;
  15. use OCA\Talk\Service\RoomService;
  16. use OCP\AppFramework\Utility\ITimeFactory;
  17. use OCP\Comments\IComment;
  18. use OCP\EventDispatcher\IEventDispatcher;
  19. use OCP\IDBConnection;
  20. use OCP\Server;
  21. class Room {
  22. /**
  23. * Regex that matches SIP incompatible rooms:
  24. * 1. duplicate digit: …11…
  25. * 2. leading zero: 0
  26. * 3. non-digit: …a…
  27. */
  28. public const SIP_INCOMPATIBLE_REGEX = '/((\d)(?=\2+)|^0|\D)/';
  29. public const TYPE_UNKNOWN = -1;
  30. public const TYPE_ONE_TO_ONE = 1;
  31. public const TYPE_GROUP = 2;
  32. public const TYPE_PUBLIC = 3;
  33. public const TYPE_CHANGELOG = 4;
  34. public const TYPE_ONE_TO_ONE_FORMER = 5;
  35. public const TYPE_NOTE_TO_SELF = 6;
  36. public const OBJECT_TYPE_EMAIL = 'emails';
  37. public const OBJECT_TYPE_FILE = 'file';
  38. public const OBJECT_TYPE_PHONE = 'phone';
  39. public const OBJECT_TYPE_VIDEO_VERIFICATION = 'share:password';
  40. public const OBJECT_TYPE_SAMPLE = 'sample';
  41. public const RECORDING_NONE = 0;
  42. public const RECORDING_VIDEO = 1;
  43. public const RECORDING_AUDIO = 2;
  44. public const RECORDING_VIDEO_STARTING = 3;
  45. public const RECORDING_AUDIO_STARTING = 4;
  46. public const RECORDING_FAILED = 5;
  47. public const READ_WRITE = 0;
  48. public const READ_ONLY = 1;
  49. /**
  50. * Only visible when joined
  51. */
  52. public const LISTABLE_NONE = 0;
  53. /**
  54. * Searchable by all regular users and moderators, even when not joined, excluding users created with the Guests app
  55. */
  56. public const LISTABLE_USERS = 1;
  57. /**
  58. * Searchable by everyone, which includes users created with the Guests app, even when not joined
  59. */
  60. public const LISTABLE_ALL = 2;
  61. public const START_CALL_EVERYONE = 0;
  62. public const START_CALL_USERS = 1;
  63. public const START_CALL_MODERATORS = 2;
  64. public const START_CALL_NOONE = 3;
  65. public const DESCRIPTION_MAXIMUM_LENGTH = 500;
  66. public const HAS_FEDERATION_NONE = 0;
  67. public const HAS_FEDERATION_TALKv1 = 1;
  68. public const MENTION_PERMISSIONS_EVERYONE = 0;
  69. public const MENTION_PERMISSIONS_MODERATORS = 1;
  70. protected ?string $currentUser = null;
  71. protected ?Participant $participant = null;
  72. /**
  73. * @psalm-param Room::TYPE_* $type
  74. * @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
  75. * @psalm-param int-mask-of<self::HAS_FEDERATION_*> $hasFederation
  76. */
  77. public function __construct(
  78. private Manager $manager,
  79. private IDBConnection $db,
  80. private IEventDispatcher $dispatcher,
  81. private ITimeFactory $timeFactory,
  82. private int $id,
  83. private int $type,
  84. private int $readOnly,
  85. private int $listable,
  86. private int $messageExpiration,
  87. private int $lobbyState,
  88. private int $sipEnabled,
  89. private ?int $assignedSignalingServer,
  90. private string $token,
  91. private string $name,
  92. private string $description,
  93. private string $password,
  94. private string $avatar,
  95. private string $remoteServer,
  96. private string $remoteToken,
  97. private int $defaultPermissions,
  98. private int $callPermissions,
  99. private int $callFlag,
  100. private ?\DateTime $activeSince,
  101. private ?\DateTime $lastActivity,
  102. private int $lastMessageId,
  103. private ?IComment $lastMessage,
  104. private ?\DateTime $lobbyTimer,
  105. private string $objectType,
  106. private string $objectId,
  107. private int $breakoutRoomMode,
  108. private int $breakoutRoomStatus,
  109. private int $callRecording,
  110. private int $recordingConsent,
  111. private int $hasFederation,
  112. private int $mentionPermissions,
  113. ) {
  114. }
  115. public function getId(): int {
  116. return $this->id;
  117. }
  118. /**
  119. * @return int
  120. * @psalm-return Room::TYPE_*
  121. */
  122. public function getType(): int {
  123. return $this->type;
  124. }
  125. /**
  126. * @param int $type
  127. * @psalm-param Room::TYPE_* $type
  128. */
  129. public function setType(int $type): void {
  130. $this->type = $type;
  131. }
  132. public function getReadOnly(): int {
  133. return $this->readOnly;
  134. }
  135. /**
  136. * @param int $readOnly Currently it is only allowed to change between
  137. * `self::READ_ONLY` and `self::READ_WRITE`
  138. * Also it's only allowed on rooms of type
  139. * `self::TYPE_GROUP` and `self::TYPE_PUBLIC`
  140. */
  141. public function setReadOnly(int $readOnly): void {
  142. $this->readOnly = $readOnly;
  143. }
  144. public function getListable(): int {
  145. return $this->listable;
  146. }
  147. /**
  148. * @param int $newState New listable scope from self::LISTABLE_*
  149. * Also it's only allowed on rooms of type
  150. * `self::TYPE_GROUP` and `self::TYPE_PUBLIC`
  151. */
  152. public function setListable(int $newState): void {
  153. $this->listable = $newState;
  154. }
  155. public function getMessageExpiration(): int {
  156. return $this->messageExpiration;
  157. }
  158. public function setMessageExpiration(int $messageExpiration): void {
  159. $this->messageExpiration = $messageExpiration;
  160. }
  161. public function getLobbyState(bool $validateTime = true): int {
  162. if ($validateTime) {
  163. $this->validateTimer();
  164. }
  165. return $this->lobbyState;
  166. }
  167. public function setLobbyState(int $lobbyState): void {
  168. $this->lobbyState = $lobbyState;
  169. }
  170. public function getLobbyTimer(bool $validateTime = true): ?\DateTime {
  171. if ($validateTime) {
  172. $this->validateTimer();
  173. }
  174. return $this->lobbyTimer;
  175. }
  176. public function setLobbyTimer(?\DateTime $lobbyTimer): void {
  177. $this->lobbyTimer = $lobbyTimer;
  178. }
  179. protected function validateTimer(): void {
  180. if ($this->lobbyTimer !== null && $this->lobbyTimer < $this->timeFactory->getDateTime()) {
  181. /** @var RoomService $roomService */
  182. $roomService = Server::get(RoomService::class);
  183. $roomService->setLobby($this, Webinary::LOBBY_NONE, null, true);
  184. }
  185. }
  186. public function getSIPEnabled(): int {
  187. return $this->sipEnabled;
  188. }
  189. public function setSIPEnabled(int $sipEnabled): void {
  190. $this->sipEnabled = $sipEnabled;
  191. }
  192. public function getAssignedSignalingServer(): ?int {
  193. return $this->assignedSignalingServer;
  194. }
  195. public function setAssignedSignalingServer(?int $assignedSignalingServer): void {
  196. $this->assignedSignalingServer = $assignedSignalingServer;
  197. }
  198. public function getToken(): string {
  199. return $this->token;
  200. }
  201. public function getName(): string {
  202. if ($this->type === self::TYPE_ONE_TO_ONE) {
  203. if ($this->name === '') {
  204. // TODO use DI
  205. $participantService = Server::get(ParticipantService::class);
  206. // Fill the room name with the participants for 1-to-1 conversations
  207. $users = $participantService->getParticipantUserIds($this);
  208. sort($users);
  209. /** @var RoomService $roomService */
  210. $roomService = Server::get(RoomService::class);
  211. $roomService->setName($this, json_encode($users), '');
  212. } elseif (!str_starts_with($this->name, '["')) {
  213. // TODO use DI
  214. $participantService = Server::get(ParticipantService::class);
  215. // Not the json array, but the old fallback when someone left
  216. $users = $participantService->getParticipantUserIds($this);
  217. if (count($users) !== 2) {
  218. $users[] = $this->name;
  219. }
  220. sort($users);
  221. /** @var RoomService $roomService */
  222. $roomService = Server::get(RoomService::class);
  223. $roomService->setName($this, json_encode($users), '');
  224. }
  225. }
  226. return $this->name;
  227. }
  228. public function setName(string $name): void {
  229. $this->name = $name;
  230. }
  231. public function getSecondParticipant(string $userId): string {
  232. if ($this->getType() !== self::TYPE_ONE_TO_ONE) {
  233. throw new \InvalidArgumentException('Not a one-to-one room');
  234. }
  235. $participants = json_decode($this->getName(), true);
  236. foreach ($participants as $uid) {
  237. if ($uid !== $userId) {
  238. return $uid;
  239. }
  240. }
  241. return $this->getName();
  242. }
  243. public function getDisplayName(string $userId, bool $forceName = false): string {
  244. return $this->manager->resolveRoomDisplayName($this, $userId, $forceName);
  245. }
  246. public function getDescription(): string {
  247. return $this->description;
  248. }
  249. public function setDescription(string $description): void {
  250. $this->description = $description;
  251. }
  252. public function resetActiveSince(): void {
  253. $this->activeSince = null;
  254. $this->callFlag = Participant::FLAG_DISCONNECTED;
  255. }
  256. public function getDefaultPermissions(): int {
  257. return $this->defaultPermissions;
  258. }
  259. public function setDefaultPermissions(int $defaultPermissions): void {
  260. $this->defaultPermissions = $defaultPermissions;
  261. }
  262. /**
  263. * @deprecated
  264. */
  265. public function getCallPermissions(): int {
  266. return Attendee::PERMISSIONS_DEFAULT;
  267. }
  268. /**
  269. * @deprecated
  270. */
  271. public function setCallPermissions(int $callPermissions): void {
  272. $this->callPermissions = $callPermissions;
  273. }
  274. public function getCallFlag(): int {
  275. return $this->callFlag;
  276. }
  277. public function getActiveSince(): ?\DateTime {
  278. return $this->activeSince;
  279. }
  280. public function getLastActivity(): ?\DateTime {
  281. return $this->lastActivity;
  282. }
  283. public function setLastActivity(\DateTime $now): void {
  284. $this->lastActivity = $now;
  285. }
  286. public function getLastMessageId(): int {
  287. return $this->lastMessageId;
  288. }
  289. public function setLastMessageId(int $lastMessageId): void {
  290. $this->lastMessageId = $lastMessageId;
  291. }
  292. public function getLastMessage(): ?IComment {
  293. if ($this->isFederatedConversation()) {
  294. return null;
  295. }
  296. if ($this->lastMessageId && $this->lastMessage === null) {
  297. $this->lastMessage = $this->manager->loadLastCommentInfo($this->lastMessageId);
  298. if ($this->lastMessage === null) {
  299. $this->lastMessageId = 0;
  300. }
  301. }
  302. return $this->lastMessage;
  303. }
  304. public function setLastMessage(IComment $message): void {
  305. $this->lastMessage = $message;
  306. $this->lastMessageId = (int)$message->getId();
  307. }
  308. public function getObjectType(): string {
  309. return $this->objectType;
  310. }
  311. public function getObjectId(): string {
  312. return $this->objectId;
  313. }
  314. public function hasPassword(): bool {
  315. return $this->password !== '';
  316. }
  317. public function getPassword(): string {
  318. return $this->password;
  319. }
  320. public function setPassword(string $password): void {
  321. $this->password = $password;
  322. }
  323. public function setAvatar(string $avatar): void {
  324. $this->avatar = $avatar;
  325. }
  326. public function getAvatar(): string {
  327. return $this->avatar;
  328. }
  329. public function getRemoteServer(): string {
  330. return $this->remoteServer;
  331. }
  332. /**
  333. * Whether the conversation is a "proxy conversation" or the original hosted conversation
  334. * @return bool
  335. */
  336. public function isFederatedConversation(): bool {
  337. return $this->remoteServer !== '';
  338. }
  339. public function getRemoteToken(): string {
  340. return $this->remoteToken;
  341. }
  342. public function setParticipant(?string $userId, Participant $participant): void {
  343. // FIXME Also used with cloudId, need actorType checking?
  344. $this->currentUser = $userId;
  345. $this->participant = $participant;
  346. }
  347. /**
  348. * Return the room properties to send to the signaling server.
  349. *
  350. * @param string $userId
  351. * @param bool $roomModified
  352. * @return array
  353. */
  354. public function getPropertiesForSignaling(string $userId, bool $roomModified = true): array {
  355. $properties = [
  356. 'name' => $this->getDisplayName($userId),
  357. 'type' => $this->getType(),
  358. 'lobby-state' => $this->getLobbyState(),
  359. 'lobby-timer' => $this->getLobbyTimer(),
  360. 'read-only' => $this->getReadOnly(),
  361. 'listable' => $this->getListable(),
  362. 'active-since' => $this->getActiveSince(),
  363. 'sip-enabled' => $this->getSIPEnabled(),
  364. ];
  365. if ($roomModified) {
  366. $properties['description'] = $this->getDescription();
  367. } else {
  368. $properties['participant-list'] = 'refresh';
  369. }
  370. $event = new BeforeSignalingRoomPropertiesSentEvent($this, $userId, $properties);
  371. $this->dispatcher->dispatchTyped($event);
  372. return $event->getProperties();
  373. }
  374. /**
  375. * @param string|null $userId
  376. * @param string|null|false $sessionId Set to false if you don't want to load a session (and save resources),
  377. * string to try loading a specific session
  378. * null to try loading "any"
  379. * @return Participant
  380. * @throws ParticipantNotFoundException When the user is not a participant
  381. * @deprecated
  382. */
  383. public function getParticipant(?string $userId, $sessionId = null): Participant {
  384. if (!is_string($userId) || $userId === '') {
  385. throw new ParticipantNotFoundException('Not a user');
  386. }
  387. if ($this->currentUser === $userId && $this->participant instanceof Participant) {
  388. if (!$sessionId
  389. || ($this->participant->getSession() instanceof Session
  390. && $this->participant->getSession()->getSessionId() === $sessionId)) {
  391. return $this->participant;
  392. }
  393. }
  394. $query = $this->db->getQueryBuilder();
  395. $helper = new SelectHelper();
  396. $helper->selectAttendeesTable($query);
  397. $query->from('talk_attendees', 'a')
  398. ->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)))
  399. ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)))
  400. ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId())))
  401. ->setMaxResults(1);
  402. if ($sessionId !== false) {
  403. if ($sessionId !== null) {
  404. $helper->selectSessionsTable($query);
  405. $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX(
  406. $query->expr()->eq('s.session_id', $query->createNamedParameter($sessionId)),
  407. $query->expr()->eq('a.id', 's.attendee_id')
  408. ));
  409. } else {
  410. $helper->selectSessionsTable($query); // FIXME PROBLEM
  411. $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id'));
  412. }
  413. }
  414. $result = $query->executeQuery();
  415. $row = $result->fetch();
  416. $result->closeCursor();
  417. if ($row === false) {
  418. throw new ParticipantNotFoundException('User is not a participant');
  419. }
  420. if ($this->currentUser === $userId) {
  421. $this->participant = $this->manager->createParticipantObject($this, $row);
  422. return $this->participant;
  423. }
  424. return $this->manager->createParticipantObject($this, $row);
  425. }
  426. public function setActiveSince(\DateTime $since, int $callFlag): void {
  427. if (!$this->activeSince) {
  428. $this->activeSince = $since;
  429. }
  430. $this->callFlag |= $callFlag;
  431. }
  432. public function getBreakoutRoomMode(): int {
  433. return $this->breakoutRoomMode;
  434. }
  435. public function setBreakoutRoomMode(int $mode): void {
  436. $this->breakoutRoomMode = $mode;
  437. }
  438. public function getBreakoutRoomStatus(): int {
  439. return $this->breakoutRoomStatus;
  440. }
  441. public function setBreakoutRoomStatus(int $status): void {
  442. $this->breakoutRoomStatus = $status;
  443. }
  444. public function getCallRecording(): int {
  445. return $this->callRecording;
  446. }
  447. public function setCallRecording(int $callRecording): void {
  448. $this->callRecording = $callRecording;
  449. }
  450. /**
  451. * @return RecordingService::CONSENT_REQUIRED_*
  452. */
  453. public function getRecordingConsent(): int {
  454. return $this->recordingConsent;
  455. }
  456. /**
  457. * @param int $recordingConsent
  458. * @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
  459. */
  460. public function setRecordingConsent(int $recordingConsent): void {
  461. $this->recordingConsent = $recordingConsent;
  462. }
  463. /**
  464. * @psalm-return int-mask-of<self::HAS_FEDERATION_*>
  465. */
  466. public function hasFederatedParticipants(): int {
  467. return $this->hasFederation;
  468. }
  469. /**
  470. * @param int $hasFederation
  471. * @psalm-param int-mask-of<self::HAS_FEDERATION_*> $hasFederation (bit map)
  472. */
  473. public function setFederatedParticipants(int $hasFederation): void {
  474. $this->hasFederation = $hasFederation;
  475. }
  476. public function getMentionPermissions(): int {
  477. return $this->mentionPermissions;
  478. }
  479. public function setMentionPermissions(int $mentionPermissions): void {
  480. $this->mentionPermissions = $mentionPermissions;
  481. }
  482. }