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.

1190 lines
45 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OCA\Talk\Service;
  8. use InvalidArgumentException;
  9. use OCA\Talk\Config;
  10. use OCA\Talk\Events\AParticipantModifiedEvent;
  11. use OCA\Talk\Events\ARoomModifiedEvent;
  12. use OCA\Talk\Events\ARoomSyncedEvent;
  13. use OCA\Talk\Events\BeforeCallEndedEvent;
  14. use OCA\Talk\Events\BeforeCallStartedEvent;
  15. use OCA\Talk\Events\BeforeLobbyModifiedEvent;
  16. use OCA\Talk\Events\BeforeRoomDeletedEvent;
  17. use OCA\Talk\Events\BeforeRoomModifiedEvent;
  18. use OCA\Talk\Events\BeforeRoomSyncedEvent;
  19. use OCA\Talk\Events\CallEndedEvent;
  20. use OCA\Talk\Events\CallStartedEvent;
  21. use OCA\Talk\Events\LobbyModifiedEvent;
  22. use OCA\Talk\Events\RoomDeletedEvent;
  23. use OCA\Talk\Events\RoomModifiedEvent;
  24. use OCA\Talk\Events\RoomPasswordVerifyEvent;
  25. use OCA\Talk\Events\RoomSyncedEvent;
  26. use OCA\Talk\Exceptions\RoomNotFoundException;
  27. use OCA\Talk\Exceptions\RoomProperty\DefaultPermissionsException;
  28. use OCA\Talk\Exceptions\RoomProperty\SipConfigurationException;
  29. use OCA\Talk\Manager;
  30. use OCA\Talk\Model\Attendee;
  31. use OCA\Talk\Model\BreakoutRoom;
  32. use OCA\Talk\Participant;
  33. use OCA\Talk\ResponseDefinitions;
  34. use OCA\Talk\Room;
  35. use OCA\Talk\Webinary;
  36. use OCP\AppFramework\Utility\ITimeFactory;
  37. use OCP\BackgroundJob\IJobList;
  38. use OCP\Comments\IComment;
  39. use OCP\DB\QueryBuilder\IQueryBuilder;
  40. use OCP\EventDispatcher\IEventDispatcher;
  41. use OCP\HintException;
  42. use OCP\IDBConnection;
  43. use OCP\IUser;
  44. use OCP\Log\Audit\CriticalActionPerformedEvent;
  45. use OCP\Security\Events\ValidatePasswordPolicyEvent;
  46. use OCP\Security\IHasher;
  47. use OCP\Share\IManager as IShareManager;
  48. use Psr\Log\LoggerInterface;
  49. /**
  50. * @psalm-import-type TalkRoom from ResponseDefinitions
  51. */
  52. class RoomService {
  53. public function __construct(
  54. protected Manager $manager,
  55. protected ParticipantService $participantService,
  56. protected IDBConnection $db,
  57. protected ITimeFactory $timeFactory,
  58. protected IShareManager $shareManager,
  59. protected Config $config,
  60. protected IHasher $hasher,
  61. protected IEventDispatcher $dispatcher,
  62. protected IJobList $jobList,
  63. protected LoggerInterface $logger,
  64. ) {
  65. }
  66. /**
  67. * @param IUser $actor
  68. * @param IUser $targetUser
  69. * @return Room
  70. * @throws InvalidArgumentException when both users are the same
  71. */
  72. public function createOneToOneConversation(IUser $actor, IUser $targetUser): Room {
  73. if ($actor->getUID() === $targetUser->getUID()) {
  74. throw new InvalidArgumentException('invalid_invitee');
  75. }
  76. try {
  77. // If room exists: Reuse that one, otherwise create a new one.
  78. $room = $this->manager->getOne2OneRoom($actor->getUID(), $targetUser->getUID());
  79. $this->participantService->ensureOneToOneRoomIsFilled($room);
  80. } catch (RoomNotFoundException) {
  81. if (!$this->shareManager->currentUserCanEnumerateTargetUser($actor, $targetUser)) {
  82. throw new RoomNotFoundException();
  83. }
  84. $users = [$actor->getUID(), $targetUser->getUID()];
  85. sort($users);
  86. $room = $this->manager->createRoom(Room::TYPE_ONE_TO_ONE, json_encode($users));
  87. $this->participantService->addUsers($room, [
  88. [
  89. 'actorType' => Attendee::ACTOR_USERS,
  90. 'actorId' => $actor->getUID(),
  91. 'displayName' => $actor->getDisplayName(),
  92. 'participantType' => Participant::OWNER,
  93. ],
  94. [
  95. 'actorType' => Attendee::ACTOR_USERS,
  96. 'actorId' => $targetUser->getUID(),
  97. 'displayName' => $targetUser->getDisplayName(),
  98. 'participantType' => Participant::OWNER,
  99. ],
  100. ], $actor);
  101. }
  102. return $room;
  103. }
  104. /**
  105. * @param int $type
  106. * @param string $name
  107. * @param IUser|null $owner
  108. * @param string $objectType
  109. * @param string $objectId
  110. * @return Room
  111. * @throws InvalidArgumentException on too long or empty names
  112. * @throws InvalidArgumentException unsupported type
  113. * @throws InvalidArgumentException invalid object data
  114. */
  115. public function createConversation(int $type, string $name, ?IUser $owner = null, string $objectType = '', string $objectId = ''): Room {
  116. $name = trim($name);
  117. if ($name === '' || mb_strlen($name) > 255) {
  118. throw new InvalidArgumentException('name');
  119. }
  120. if (!\in_array($type, [
  121. Room::TYPE_GROUP,
  122. Room::TYPE_PUBLIC,
  123. Room::TYPE_CHANGELOG,
  124. Room::TYPE_NOTE_TO_SELF,
  125. ], true)) {
  126. throw new InvalidArgumentException('type');
  127. }
  128. $objectType = trim($objectType);
  129. if (isset($objectType[64])) {
  130. throw new InvalidArgumentException('object_type');
  131. }
  132. $objectId = trim($objectId);
  133. if (isset($objectId[64])) {
  134. throw new InvalidArgumentException('object_id');
  135. }
  136. if (($objectType !== '' && $objectId === '') ||
  137. ($objectType === '' && $objectId !== '')) {
  138. throw new InvalidArgumentException('object');
  139. }
  140. $room = $this->manager->createRoom($type, $name, $objectType, $objectId);
  141. if ($owner instanceof IUser) {
  142. $this->participantService->addUsers($room, [[
  143. 'actorType' => Attendee::ACTOR_USERS,
  144. 'actorId' => $owner->getUID(),
  145. 'displayName' => $owner->getDisplayName(),
  146. 'participantType' => Participant::OWNER,
  147. ]], null);
  148. }
  149. return $room;
  150. }
  151. public function prepareConversationName(string $objectName): string {
  152. return rtrim(mb_substr(ltrim($objectName), 0, 64));
  153. }
  154. /**
  155. * @deprecated
  156. */
  157. public function setPermissions(Room $room, string $level, string $method, int $permissions, bool $resetCustomPermissions): bool {
  158. if ($level === 'default' && $method === 'set') {
  159. try {
  160. $this->setDefaultPermissions($room, $permissions);
  161. return true;
  162. } catch (InvalidArgumentException) {
  163. return false;
  164. }
  165. }
  166. return false;
  167. }
  168. /**
  169. * @throws DefaultPermissionsException
  170. */
  171. public function setDefaultPermissions(Room $room, int $permissions): void {
  172. if ($room->getType() === Room::TYPE_ONE_TO_ONE
  173. || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER
  174. || $room->getType() === Room::TYPE_NOTE_TO_SELF) {
  175. throw new DefaultPermissionsException(DefaultPermissionsException::REASON_TYPE);
  176. }
  177. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  178. // Do not allow manual changing the permissions in breakout rooms
  179. throw new DefaultPermissionsException(DefaultPermissionsException::REASON_BREAKOUT_ROOM);
  180. }
  181. if ($permissions < 0 || $permissions > 255) {
  182. // Do not allow manual changing the permissions in breakout rooms
  183. throw new DefaultPermissionsException(DefaultPermissionsException::REASON_VALUE);
  184. }
  185. $oldPermissions = $room->getDefaultPermissions();
  186. $newPermissions = $permissions;
  187. if ($newPermissions !== Attendee::PERMISSIONS_DEFAULT) {
  188. // Make sure the custom flag is set when not setting to default permissions
  189. $newPermissions |= Attendee::PERMISSIONS_CUSTOM;
  190. }
  191. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS, $newPermissions, $oldPermissions);
  192. $this->dispatcher->dispatchTyped($event);
  193. // Reset custom user permissions to default
  194. $this->participantService->updateAllPermissions($room, Attendee::PERMISSIONS_MODIFY_SET, Attendee::PERMISSIONS_DEFAULT);
  195. $update = $this->db->getQueryBuilder();
  196. $update->update('talk_rooms')
  197. ->set('default_permissions', $update->createNamedParameter($newPermissions, IQueryBuilder::PARAM_INT))
  198. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  199. $update->executeStatement();
  200. $room->setDefaultPermissions($newPermissions);
  201. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS, $newPermissions, $oldPermissions);
  202. $this->dispatcher->dispatchTyped($event);
  203. }
  204. /**
  205. * @throws SipConfigurationException
  206. */
  207. public function setSIPEnabled(Room $room, int $newSipEnabled): void {
  208. $oldSipEnabled = $room->getSIPEnabled();
  209. if ($newSipEnabled === $oldSipEnabled) {
  210. return;
  211. }
  212. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  213. throw new SipConfigurationException(SipConfigurationException::REASON_BREAKOUT_ROOM);
  214. }
  215. if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true)) {
  216. throw new SipConfigurationException(SipConfigurationException::REASON_TYPE);
  217. }
  218. if (!in_array($newSipEnabled, [Webinary::SIP_ENABLED_NO_PIN, Webinary::SIP_ENABLED, Webinary::SIP_DISABLED], true)) {
  219. throw new SipConfigurationException(SipConfigurationException::REASON_VALUE);
  220. }
  221. if (preg_match(Room::SIP_INCOMPATIBLE_REGEX, $room->getToken())) {
  222. throw new SipConfigurationException(SipConfigurationException::REASON_TOKEN);
  223. }
  224. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_SIP_ENABLED, $newSipEnabled, $oldSipEnabled);
  225. $this->dispatcher->dispatchTyped($event);
  226. $update = $this->db->getQueryBuilder();
  227. $update->update('talk_rooms')
  228. ->set('sip_enabled', $update->createNamedParameter($newSipEnabled, IQueryBuilder::PARAM_INT))
  229. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  230. $update->executeStatement();
  231. $room->setSIPEnabled($newSipEnabled);
  232. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_SIP_ENABLED, $newSipEnabled, $oldSipEnabled);
  233. $this->dispatcher->dispatchTyped($event);
  234. }
  235. /**
  236. * @psalm-param RecordingService::CONSENT_REQUIRED_* $recordingConsent
  237. * @throws InvalidArgumentException When the room has an active call or the value is invalid
  238. */
  239. public function setRecordingConsent(Room $room, int $recordingConsent, bool $allowUpdatingBreakoutRooms = false): void {
  240. $oldRecordingConsent = $room->getRecordingConsent();
  241. if ($recordingConsent === $oldRecordingConsent) {
  242. return;
  243. }
  244. if (!in_array($recordingConsent, [RecordingService::CONSENT_REQUIRED_NO, RecordingService::CONSENT_REQUIRED_YES], true)) {
  245. throw new InvalidArgumentException('value');
  246. }
  247. if ($recordingConsent !== RecordingService::CONSENT_REQUIRED_NO && $room->getCallFlag() !== Participant::FLAG_DISCONNECTED) {
  248. throw new InvalidArgumentException('call');
  249. }
  250. if (!$allowUpdatingBreakoutRooms && $room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  251. throw new InvalidArgumentException('breakout-room');
  252. }
  253. if ($room->getBreakoutRoomStatus() !== BreakoutRoom::STATUS_STOPPED) {
  254. throw new InvalidArgumentException('breakout-room');
  255. }
  256. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT, $recordingConsent, $oldRecordingConsent);
  257. $this->dispatcher->dispatchTyped($event);
  258. $now = $this->timeFactory->getDateTime();
  259. $update = $this->db->getQueryBuilder();
  260. $update->update('talk_rooms')
  261. ->set('recording_consent', $update->createNamedParameter($recordingConsent, IQueryBuilder::PARAM_INT))
  262. ->set('last_activity', $update->createNamedParameter($now, IQueryBuilder::PARAM_DATE))
  263. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  264. $update->executeStatement();
  265. $room->setRecordingConsent($recordingConsent);
  266. $room->setLastActivity($now);
  267. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT, $recordingConsent, $oldRecordingConsent);
  268. $this->dispatcher->dispatchTyped($event);
  269. // Update the recording consent for all rooms
  270. if ($room->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
  271. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken());
  272. foreach ($breakoutRooms as $breakoutRoom) {
  273. $this->setRecordingConsent($breakoutRoom, $recordingConsent, true);
  274. }
  275. }
  276. }
  277. /**
  278. * @param string $newName Currently it is only allowed to rename: self::TYPE_GROUP, self::TYPE_PUBLIC
  279. * @return bool True when the change was valid, false otherwise
  280. */
  281. public function setName(Room $room, string $newName, ?string $oldName = null): bool {
  282. $oldName = $oldName !== null ? $oldName : $room->getName();
  283. if ($newName === $oldName) {
  284. return false;
  285. }
  286. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_NAME, $newName, $oldName);
  287. $this->dispatcher->dispatchTyped($event);
  288. $update = $this->db->getQueryBuilder();
  289. $update->update('talk_rooms')
  290. ->set('name', $update->createNamedParameter($newName))
  291. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  292. $update->executeStatement();
  293. $room->setName($newName);
  294. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_NAME, $newName, $oldName);
  295. $this->dispatcher->dispatchTyped($event);
  296. return true;
  297. }
  298. /**
  299. * @param Room $room
  300. * @param int $newState Currently it is only allowed to change between
  301. * `Webinary::LOBBY_NON_MODERATORS` and `Webinary::LOBBY_NONE`
  302. * Also it's not allowed in one-to-one conversations,
  303. * file conversations and password request conversations.
  304. * @param \DateTime|null $dateTime
  305. * @param bool $timerReached
  306. * @param bool $dispatchEvents (Only skip if the room is created in the same PHP request)
  307. * @return bool True when the change was valid, false otherwise
  308. */
  309. public function setLobby(Room $room, int $newState, ?\DateTime $dateTime, bool $timerReached = false, bool $dispatchEvents = true): bool {
  310. $oldState = $room->getLobbyState(false);
  311. if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true)) {
  312. return false;
  313. }
  314. if ($room->getObjectType() !== '' && $room->getObjectType() !== BreakoutRoom::PARENT_OBJECT_TYPE) {
  315. return false;
  316. }
  317. if (!in_array($newState, [Webinary::LOBBY_NON_MODERATORS, Webinary::LOBBY_NONE], true)) {
  318. return false;
  319. }
  320. if ($dispatchEvents) {
  321. $event = new BeforeLobbyModifiedEvent($room, $newState, $oldState, $dateTime, $timerReached);
  322. $this->dispatcher->dispatchTyped($event);
  323. }
  324. $update = $this->db->getQueryBuilder();
  325. $update->update('talk_rooms')
  326. ->set('lobby_state', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT))
  327. ->set('lobby_timer', $update->createNamedParameter($dateTime, IQueryBuilder::PARAM_DATE))
  328. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  329. $update->executeStatement();
  330. $room->setLobbyState($newState);
  331. $room->setLobbyTimer($dateTime);
  332. if ($dispatchEvents) {
  333. $event = new LobbyModifiedEvent($room, $newState, $oldState, $dateTime, $timerReached);
  334. $this->dispatcher->dispatchTyped($event);
  335. }
  336. return true;
  337. }
  338. public function setAvatar(Room $room, string $avatar): bool {
  339. if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  340. return false;
  341. }
  342. $oldAvatar = $room->getAvatar();
  343. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_AVATAR, $avatar, $oldAvatar);
  344. $this->dispatcher->dispatchTyped($event);
  345. $update = $this->db->getQueryBuilder();
  346. $update->update('talk_rooms')
  347. ->set('avatar', $update->createNamedParameter($avatar))
  348. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  349. $update->executeStatement();
  350. $room->setAvatar($avatar);
  351. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_AVATAR, $avatar, $oldAvatar);
  352. $this->dispatcher->dispatchTyped($event);
  353. return true;
  354. }
  355. /**
  356. * @param Room $room
  357. * @param integer $status 0 none|1 video|2 audio
  358. * @param Participant|null $participant the Participant that changed the
  359. * state, null for the current user
  360. * @throws InvalidArgumentException When the status is invalid, not Room::RECORDING_*
  361. * @throws InvalidArgumentException When trying to start
  362. */
  363. public function setCallRecording(Room $room, int $status = Room::RECORDING_NONE, ?Participant $participant = null): void {
  364. $syncFederatedRoom = $room->getRemoteServer() && $room->getRemoteToken();
  365. if (!$syncFederatedRoom && !$this->config->isRecordingEnabled() && $status !== Room::RECORDING_NONE) {
  366. throw new InvalidArgumentException('config');
  367. }
  368. $availableRecordingStatus = [Room::RECORDING_NONE, Room::RECORDING_VIDEO, Room::RECORDING_AUDIO, Room::RECORDING_VIDEO_STARTING, Room::RECORDING_AUDIO_STARTING, Room::RECORDING_FAILED];
  369. if (!in_array($status, $availableRecordingStatus)) {
  370. throw new InvalidArgumentException('status');
  371. }
  372. $oldStatus = $room->getCallRecording();
  373. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_CALL_RECORDING, $status, $oldStatus, $participant);
  374. $this->dispatcher->dispatchTyped($event);
  375. $update = $this->db->getQueryBuilder();
  376. $update->update('talk_rooms')
  377. ->set('call_recording', $update->createNamedParameter($status, IQueryBuilder::PARAM_INT))
  378. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  379. $update->executeStatement();
  380. $room->setCallRecording($status);
  381. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_CALL_RECORDING, $status, $oldStatus, $participant);
  382. $this->dispatcher->dispatchTyped($event);
  383. }
  384. /**
  385. * @param Room $room
  386. * @param int $newType Currently it is only allowed to change between `Room::TYPE_GROUP` and `Room::TYPE_PUBLIC`
  387. * @param bool $allowSwitchingOneToOne Allows additionally to change the type from `Room::TYPE_ONE_TO_ONE` to `Room::TYPE_ONE_TO_ONE_FORMER`
  388. * @return bool True when the change was valid, false otherwise
  389. */
  390. public function setType(Room $room, int $newType, bool $allowSwitchingOneToOne = false): bool {
  391. $oldType = $room->getType();
  392. if ($oldType === $newType) {
  393. return true;
  394. }
  395. if (!$allowSwitchingOneToOne && $oldType === Room::TYPE_ONE_TO_ONE) {
  396. return false;
  397. }
  398. if ($oldType === Room::TYPE_ONE_TO_ONE_FORMER) {
  399. return false;
  400. }
  401. if ($oldType === Room::TYPE_NOTE_TO_SELF) {
  402. return false;
  403. }
  404. if (!in_array($newType, [Room::TYPE_GROUP, Room::TYPE_PUBLIC, Room::TYPE_ONE_TO_ONE_FORMER], true)) {
  405. return false;
  406. }
  407. if ($newType === Room::TYPE_ONE_TO_ONE_FORMER && $oldType !== Room::TYPE_ONE_TO_ONE) {
  408. return false;
  409. }
  410. if ($room->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
  411. return false;
  412. }
  413. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  414. return false;
  415. }
  416. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, $newType, $oldType);
  417. $this->dispatcher->dispatchTyped($event);
  418. $update = $this->db->getQueryBuilder();
  419. $update->update('talk_rooms')
  420. ->set('type', $update->createNamedParameter($newType, IQueryBuilder::PARAM_INT))
  421. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  422. $update->executeStatement();
  423. $room->setType($newType);
  424. if ($oldType === Room::TYPE_PUBLIC) {
  425. // Kick all guests and users that were not invited
  426. $delete = $this->db->getQueryBuilder();
  427. $delete->delete('talk_attendees')
  428. ->where($delete->expr()->eq('room_id', $delete->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
  429. ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY)));
  430. $delete->executeStatement();
  431. }
  432. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_TYPE, $newType, $oldType);
  433. $this->dispatcher->dispatchTyped($event);
  434. return true;
  435. }
  436. /**
  437. * @param Room $room
  438. * @param int $newState Currently it is only allowed to change between
  439. * `Room::READ_ONLY` and `Room::READ_WRITE`
  440. * Also it's only allowed on rooms of type
  441. * `Room::TYPE_GROUP` and `Room::TYPE_PUBLIC`
  442. * @return bool True when the change was valid, false otherwise
  443. */
  444. public function setReadOnly(Room $room, int $newState): bool {
  445. $oldState = $room->getReadOnly();
  446. if ($newState === $oldState) {
  447. return true;
  448. }
  449. if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC, Room::TYPE_CHANGELOG], true)) {
  450. if ($newState !== Room::READ_ONLY || $room->getType() !== Room::TYPE_ONE_TO_ONE_FORMER) {
  451. // Allowed for the automated conversation of one-to-one chats to read only former
  452. return false;
  453. }
  454. }
  455. if (!in_array($newState, [Room::READ_ONLY, Room::READ_WRITE], true)) {
  456. return false;
  457. }
  458. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_READ_ONLY, $newState, $oldState);
  459. $this->dispatcher->dispatchTyped($event);
  460. $update = $this->db->getQueryBuilder();
  461. $update->update('talk_rooms')
  462. ->set('read_only', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT))
  463. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  464. $update->executeStatement();
  465. $room->setReadOnly($newState);
  466. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_READ_ONLY, $newState, $oldState);
  467. $this->dispatcher->dispatchTyped($event);
  468. return true;
  469. }
  470. /**
  471. * @param Room $room
  472. * @param int $newState New listable scope from self::LISTABLE_*
  473. * Also it's only allowed on rooms of type
  474. * `Room::TYPE_GROUP` and `Room::TYPE_PUBLIC`
  475. * @return bool True when the change was valid, false otherwise
  476. */
  477. public function setListable(Room $room, int $newState): bool {
  478. $oldState = $room->getListable();
  479. if ($newState === $oldState) {
  480. return true;
  481. }
  482. if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true)) {
  483. return false;
  484. }
  485. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  486. return false;
  487. }
  488. if (!in_array($newState, [
  489. Room::LISTABLE_NONE,
  490. Room::LISTABLE_USERS,
  491. Room::LISTABLE_ALL,
  492. ], true)) {
  493. return false;
  494. }
  495. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_LISTABLE, $newState, $oldState);
  496. $this->dispatcher->dispatchTyped($event);
  497. $update = $this->db->getQueryBuilder();
  498. $update->update('talk_rooms')
  499. ->set('listable', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT))
  500. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  501. $update->executeStatement();
  502. $room->setListable($newState);
  503. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_LISTABLE, $newState, $oldState);
  504. $this->dispatcher->dispatchTyped($event);
  505. return true;
  506. }
  507. /**
  508. * @param Room $room
  509. * @param int $newState New mention permissions from self::MENTION_PERMISSIONS_*
  510. * @throws \InvalidArgumentException When the room type, state or breakout rooms where invalid
  511. */
  512. public function setMentionPermissions(Room $room, int $newState): void {
  513. $oldState = $room->getMentionPermissions();
  514. if ($newState === $oldState) {
  515. return;
  516. }
  517. if (!in_array($room->getType(), [Room::TYPE_GROUP, Room::TYPE_PUBLIC], true)) {
  518. throw new \InvalidArgumentException('type');
  519. }
  520. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  521. throw new \InvalidArgumentException('breakout-room');
  522. }
  523. if (!in_array($newState, [Room::MENTION_PERMISSIONS_EVERYONE, Room::MENTION_PERMISSIONS_MODERATORS], true)) {
  524. throw new \InvalidArgumentException('state');
  525. }
  526. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS, $newState, $oldState);
  527. $this->dispatcher->dispatchTyped($event);
  528. $update = $this->db->getQueryBuilder();
  529. $update->update('talk_rooms')
  530. ->set('mention_permissions', $update->createNamedParameter($newState, IQueryBuilder::PARAM_INT))
  531. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  532. $update->executeStatement();
  533. $room->setMentionPermissions($newState);
  534. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS, $newState, $oldState);
  535. $this->dispatcher->dispatchTyped($event);
  536. }
  537. public function setAssignedSignalingServer(Room $room, ?int $signalingServer): bool {
  538. $update = $this->db->getQueryBuilder();
  539. $update->update('talk_rooms')
  540. ->set('assigned_hpb', $update->createNamedParameter($signalingServer))
  541. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  542. if ($signalingServer !== null) {
  543. $update->andWhere($update->expr()->isNull('assigned_hpb'));
  544. }
  545. $updated = (bool)$update->executeStatement();
  546. if ($updated) {
  547. $room->setAssignedSignalingServer($signalingServer);
  548. }
  549. return $updated;
  550. }
  551. /**
  552. * @return bool True when the change was valid, false otherwise
  553. * @throws \LengthException when the given description is too long
  554. */
  555. public function setDescription(Room $room, string $description): bool {
  556. $description = trim($description);
  557. if (mb_strlen($description) > Room::DESCRIPTION_MAXIMUM_LENGTH) {
  558. throw new \LengthException('Conversation description is limited to ' . Room::DESCRIPTION_MAXIMUM_LENGTH . ' characters');
  559. }
  560. $oldDescription = $room->getDescription();
  561. if ($description === $oldDescription) {
  562. return false;
  563. }
  564. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_DESCRIPTION, $description, $oldDescription);
  565. $this->dispatcher->dispatchTyped($event);
  566. $update = $this->db->getQueryBuilder();
  567. $update->update('talk_rooms')
  568. ->set('description', $update->createNamedParameter($description))
  569. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  570. $update->executeStatement();
  571. $room->setDescription($description);
  572. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_DESCRIPTION, $description, $oldDescription);
  573. $this->dispatcher->dispatchTyped($event);
  574. return true;
  575. }
  576. /**
  577. * @param string $password Currently it is only allowed to have a password for Room::TYPE_PUBLIC
  578. * @return bool True when the change was valid, false otherwise
  579. * @throws HintException
  580. */
  581. public function setPassword(Room $room, string $password): bool {
  582. if ($room->getType() !== Room::TYPE_PUBLIC) {
  583. return false;
  584. }
  585. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  586. return false;
  587. }
  588. if ($password !== '') {
  589. $event = new ValidatePasswordPolicyEvent($password);
  590. $this->dispatcher->dispatchTyped($event);
  591. }
  592. $hash = $password !== '' ? $this->hasher->hash($password) : '';
  593. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password);
  594. $this->dispatcher->dispatchTyped($event);
  595. $update = $this->db->getQueryBuilder();
  596. $update->update('talk_rooms')
  597. ->set('password', $update->createNamedParameter($hash))
  598. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  599. $update->executeStatement();
  600. $room->setPassword($hash);
  601. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_PASSWORD, $password);
  602. $this->dispatcher->dispatchTyped($event);
  603. return true;
  604. }
  605. /**
  606. * @return array{result: ?bool, url: string}
  607. */
  608. public function verifyPassword(Room $room, string $password): array {
  609. $event = new RoomPasswordVerifyEvent($room, $password);
  610. $this->dispatcher->dispatchTyped($event);
  611. if ($event->isPasswordValid() !== null) {
  612. return [
  613. 'result' => $event->isPasswordValid(),
  614. 'url' => $event->getRedirectUrl(),
  615. ];
  616. }
  617. return [
  618. 'result' => !$room->hasPassword() || $this->hasher->verify($password, $room->getPassword()),
  619. 'url' => '',
  620. ];
  621. }
  622. /**
  623. * @throws InvalidArgumentException When the room is a breakout room or the room is a former one-to-one conversation
  624. */
  625. public function setMessageExpiration(Room $room, int $seconds): void {
  626. if ($room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  627. throw new InvalidArgumentException('room');
  628. }
  629. $oldExpiration = $room->getMessageExpiration();
  630. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION, $seconds, $oldExpiration);
  631. $this->dispatcher->dispatchTyped($event);
  632. $update = $this->db->getQueryBuilder();
  633. $update->update('talk_rooms')
  634. ->set('message_expiration', $update->createNamedParameter($seconds, IQueryBuilder::PARAM_INT))
  635. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  636. $update->executeStatement();
  637. $room->setMessageExpiration($seconds);
  638. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION, $seconds, $oldExpiration);
  639. $this->dispatcher->dispatchTyped($event);
  640. }
  641. public function setBreakoutRoomMode(Room $room, int $mode): bool {
  642. if (!in_array($mode, [
  643. BreakoutRoom::MODE_NOT_CONFIGURED,
  644. BreakoutRoom::MODE_AUTOMATIC,
  645. BreakoutRoom::MODE_MANUAL,
  646. BreakoutRoom::MODE_FREE
  647. ], true)) {
  648. return false;
  649. }
  650. $oldMode = $room->getBreakoutRoomMode();
  651. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_MODE, $mode, $oldMode);
  652. $this->dispatcher->dispatchTyped($event);
  653. $update = $this->db->getQueryBuilder();
  654. $update->update('talk_rooms')
  655. ->set('breakout_room_mode', $update->createNamedParameter($mode, IQueryBuilder::PARAM_INT))
  656. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  657. $update->executeStatement();
  658. $room->setBreakoutRoomMode($mode);
  659. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_MODE, $mode, $oldMode);
  660. $this->dispatcher->dispatchTyped($event);
  661. return true;
  662. }
  663. public function setBreakoutRoomStatus(Room $room, int $status): bool {
  664. if (!in_array($status, [
  665. BreakoutRoom::STATUS_STOPPED,
  666. BreakoutRoom::STATUS_STARTED,
  667. BreakoutRoom::STATUS_ASSISTANCE_RESET,
  668. BreakoutRoom::STATUS_ASSISTANCE_REQUESTED,
  669. ], true)) {
  670. return false;
  671. }
  672. $oldStatus = $room->getBreakoutRoomStatus();
  673. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_STATUS, $status, $oldStatus);
  674. $this->dispatcher->dispatchTyped($event);
  675. $update = $this->db->getQueryBuilder();
  676. $update->update('talk_rooms')
  677. ->set('breakout_room_status', $update->createNamedParameter($status, IQueryBuilder::PARAM_INT))
  678. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  679. $update->executeStatement();
  680. $room->setBreakoutRoomStatus($status);
  681. $oldStatus = $room->getBreakoutRoomStatus();
  682. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_BREAKOUT_ROOM_STATUS, $status, $oldStatus);
  683. $this->dispatcher->dispatchTyped($event);
  684. return true;
  685. }
  686. /**
  687. * @internal Warning! Use with care, this is only used to make sure we win the race condition for posting the final messages
  688. * when "End call for everyone" is used where we print the chat messages before testing the race condition,
  689. * so that no other participant leaving would trigger a call summary
  690. */
  691. public function resetActiveSinceInDatabaseOnly(Room $room): bool {
  692. $update = $this->db->getQueryBuilder();
  693. $update->update('talk_rooms')
  694. ->set('active_since', $update->createNamedParameter(null, IQueryBuilder::PARAM_DATE))
  695. ->set('call_flag', $update->createNamedParameter(0, IQueryBuilder::PARAM_INT))
  696. ->set('call_permissions', $update->createNamedParameter(Attendee::PERMISSIONS_DEFAULT, IQueryBuilder::PARAM_INT))
  697. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
  698. ->andWhere($update->expr()->isNotNull('active_since'));
  699. return (bool)$update->executeStatement();
  700. }
  701. /**
  702. * @internal Warning! Must only be used after {@see preResetActiveSinceInDatabaseOnly()}
  703. * was called and returned `true`
  704. */
  705. public function resetActiveSinceInModelOnly(Room $room): void {
  706. $room->resetActiveSince();
  707. }
  708. public function resetActiveSince(Room $room, ?Participant $participant): void {
  709. $oldActiveSince = $room->getActiveSince();
  710. $oldCallFlag = $room->getCallFlag();
  711. if ($oldActiveSince === null && $oldCallFlag === Participant::FLAG_DISCONNECTED) {
  712. return;
  713. }
  714. $event = new BeforeCallEndedEvent($room, $participant, $oldActiveSince);
  715. $this->dispatcher->dispatchTyped($event);
  716. $result = $this->resetActiveSinceInDatabaseOnly($room);
  717. $this->resetActiveSinceInModelOnly($room);
  718. if (!$result) {
  719. // Lost the race, someone else updated the database
  720. return;
  721. }
  722. $event = new CallEndedEvent($room, $participant, $oldActiveSince);
  723. $this->dispatcher->dispatchTyped($event);
  724. }
  725. public function setActiveSince(Room $room, ?Participant $participant, \DateTime $since, int $callFlag, bool $silent): bool {
  726. $oldCallFlag = $room->getCallFlag();
  727. $callFlag |= $oldCallFlag; // Merge the callFlags, so events and response are with the best values
  728. if ($room->getActiveSince() instanceof \DateTime && $oldCallFlag === $callFlag) {
  729. // Call flags of the conversation are unchanged and it's already marked active
  730. return false;
  731. }
  732. $details = [];
  733. if ($room->getActiveSince() instanceof \DateTime) {
  734. // Call is already active, just someone upgrading the call flags
  735. $event = new BeforeRoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag, $participant);
  736. $this->dispatcher->dispatchTyped($event);
  737. } else {
  738. if ($silent) {
  739. $details[AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT] = true;
  740. }
  741. $event = new BeforeCallStartedEvent($room, $since, $callFlag, $details, $participant);
  742. $this->dispatcher->dispatchTyped($event);
  743. }
  744. $update = $this->db->getQueryBuilder();
  745. $update->update('talk_rooms')
  746. ->set(
  747. 'call_flag',
  748. $update->expr()->bitwiseOr('call_flag', $callFlag)
  749. )
  750. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  751. $update->executeStatement();
  752. if ($room->getActiveSince() instanceof \DateTime) {
  753. // Call is already active, just someone upgrading the call flags
  754. $room->setActiveSince($room->getActiveSince(), $callFlag);
  755. $event = new RoomModifiedEvent($room, ARoomModifiedEvent::PROPERTY_IN_CALL, $callFlag, $oldCallFlag);
  756. $this->dispatcher->dispatchTyped($event);
  757. return false;
  758. }
  759. $update = $this->db->getQueryBuilder();
  760. $update->update('talk_rooms')
  761. ->set('active_since', $update->createNamedParameter($since, IQueryBuilder::PARAM_DATE))
  762. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)))
  763. ->andWhere($update->expr()->isNull('active_since'));
  764. $result = (bool)$update->executeStatement();
  765. $room->setActiveSince($since, $callFlag);
  766. if (!$result) {
  767. // Lost the race, someone else updated the database
  768. return false;
  769. }
  770. $event = new CallStartedEvent($room, $since, $callFlag, $details, $participant);
  771. $this->dispatcher->dispatchTyped($event);
  772. return true;
  773. }
  774. public function setLastMessage(Room $room, IComment $message): void {
  775. $update = $this->db->getQueryBuilder();
  776. $update->update('talk_rooms')
  777. ->set('last_message', $update->createNamedParameter((int)$message->getId()))
  778. ->set('last_activity', $update->createNamedParameter($message->getCreationDateTime(), 'datetime'))
  779. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  780. $update->executeStatement();
  781. $room->setLastMessage($message);
  782. $room->setLastActivity($message->getCreationDateTime());
  783. }
  784. public function setLastMessageInfo(Room $room, int $messageId, \DateTime $dateTime): void {
  785. $update = $this->db->getQueryBuilder();
  786. $update->update('talk_rooms')
  787. ->set('last_message', $update->createNamedParameter($messageId))
  788. ->set('last_activity', $update->createNamedParameter($dateTime, 'datetime'))
  789. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  790. $update->executeStatement();
  791. $room->setLastMessageId($messageId);
  792. $room->setLastActivity($dateTime);
  793. }
  794. /**
  795. * @psalm-param int-mask-of<Room::HAS_FEDERATION_*> $hasFederation
  796. */
  797. public function setHasFederation(Room $room, int $hasFederation): void {
  798. $update = $this->db->getQueryBuilder();
  799. $update->update('talk_rooms')
  800. ->set('has_federation', $update->createNamedParameter($hasFederation, IQueryBuilder::PARAM_INT))
  801. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  802. $update->executeStatement();
  803. $room->setFederatedParticipants($hasFederation);
  804. }
  805. public function setLastActivity(Room $room, \DateTime $now): void {
  806. $update = $this->db->getQueryBuilder();
  807. $update->update('talk_rooms')
  808. ->set('last_activity', $update->createNamedParameter($now, 'datetime'))
  809. ->where($update->expr()->eq('id', $update->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  810. $update->executeStatement();
  811. $room->setLastActivity($now);
  812. }
  813. /**
  814. * @psalm-param TalkRoom $host
  815. */
  816. public function syncPropertiesFromHostRoom(Room $local, array $host): void {
  817. $event = new BeforeRoomSyncedEvent($local);
  818. $this->dispatcher->dispatchTyped($event);
  819. /** @var array<array-key, ARoomModifiedEvent::PROPERTY_*> $changed */
  820. $changed = [];
  821. if (isset($host['type']) && $host['type'] !== $local->getType()) {
  822. $success = $this->setType($local, $host['type']);
  823. if (!$success) {
  824. $this->logger->error('An error occurred while trying to sync type of ' . $local->getId() . ' to ' . $host['type']);
  825. } else {
  826. $changed[] = ARoomModifiedEvent::PROPERTY_TYPE;
  827. }
  828. }
  829. if (isset($host['name']) && $host['name'] !== $local->getName()) {
  830. $success = $this->setName($local, $host['name'], $local->getName());
  831. if (!$success) {
  832. $this->logger->error('An error occurred while trying to sync name of ' . $local->getId() . ' to ' . $host['name']);
  833. } else {
  834. $changed[] = ARoomModifiedEvent::PROPERTY_NAME;
  835. }
  836. }
  837. if (isset($host['description']) && $host['description'] !== $local->getDescription()) {
  838. try {
  839. $success = $this->setDescription($local, $host['description']);
  840. if (!$success) {
  841. $this->logger->error('An error occurred while trying to sync description of ' . $local->getId() . ' to ' . $host['description']);
  842. } else {
  843. $changed[] = ARoomModifiedEvent::PROPERTY_DESCRIPTION;
  844. }
  845. } catch (\LengthException $e) {
  846. $this->logger->error('A \LengthException occurred while trying to sync description of ' . $local->getId() . ' to ' . $host['description'], ['exception' => $e]);
  847. }
  848. }
  849. if (isset($host['callRecording']) && $host['callRecording'] !== $local->getCallRecording()) {
  850. try {
  851. $this->setCallRecording($local, $host['callRecording']);
  852. $changed[] = ARoomModifiedEvent::PROPERTY_CALL_RECORDING;
  853. } catch (\InvalidArgumentException $e) {
  854. $this->logger->error('An error (' . $e->getMessage() . ') occurred while trying to sync callRecording of ' . $local->getId() . ' to ' . $host['callRecording'], ['exception' => $e]);
  855. }
  856. }
  857. if (isset($host['defaultPermissions']) && $host['defaultPermissions'] !== $local->getDefaultPermissions()) {
  858. try {
  859. $this->setDefaultPermissions($local, $host['defaultPermissions']);
  860. $changed[] = ARoomModifiedEvent::PROPERTY_DEFAULT_PERMISSIONS;
  861. } catch (DefaultPermissionsException $e) {
  862. $this->logger->error('An error (' . $e->getReason() . ') occurred while trying to sync defaultPermissions of ' . $local->getId() . ' to ' . $host['defaultPermissions'], ['exception' => $e]);
  863. }
  864. }
  865. if (isset($host['avatarVersion']) && $host['avatarVersion'] !== $local->getAvatar()) {
  866. $hostAvatar = $host['avatarVersion'];
  867. if ($hostAvatar) {
  868. // Add a fake suffix as we explode by the dot in the AvatarService, but the version doesn't have one.
  869. $hostAvatar .= '.fed';
  870. }
  871. $success = $this->setAvatar($local, $hostAvatar);
  872. if (!$success) {
  873. $this->logger->error('An error occurred while trying to sync avatarVersion of ' . $local->getId() . ' to ' . $host['avatarVersion']);
  874. } else {
  875. $changed[] = ARoomModifiedEvent::PROPERTY_AVATAR;
  876. }
  877. }
  878. if (isset($host['lastActivity']) && $host['lastActivity'] !== 0 && $host['lastActivity'] !== ((int)$local->getLastActivity()?->getTimestamp())) {
  879. $lastActivity = $this->timeFactory->getDateTime('@' . $host['lastActivity']);
  880. $this->setLastActivity($local, $lastActivity);
  881. $changed[] = ARoomSyncedEvent::PROPERTY_LAST_ACTIVITY;
  882. }
  883. if (isset($host['lobbyState'], $host['lobbyTimer']) && ($host['lobbyState'] !== $local->getLobbyState(false) || $host['lobbyTimer'] !== ((int)$local->getLobbyTimer(false)?->getTimestamp()))) {
  884. $hostTimer = $host['lobbyTimer'] === 0 ? null : $this->timeFactory->getDateTime('@' . $host['lobbyTimer']);
  885. $success = $this->setLobby($local, $host['lobbyState'], $hostTimer);
  886. if (!$success) {
  887. $this->logger->error('An error occurred while trying to sync lobby of ' . $local->getId() . ' to ' . $host['lobbyState'] . ' with timer to ' . $host['lobbyTimer']);
  888. } else {
  889. $changed[] = ARoomModifiedEvent::PROPERTY_LOBBY;
  890. }
  891. }
  892. if (isset($host['callStartTime'], $host['callFlag'])) {
  893. $localCallStartTime = (int)$local->getActiveSince()?->getTimestamp();
  894. if ($host['callStartTime'] === 0 && ($host['callStartTime'] !== $localCallStartTime || $host['callFlag'] !== $local->getCallFlag())) {
  895. $this->resetActiveSince($local, null);
  896. $changed[] = ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE;
  897. $changed[] = ARoomModifiedEvent::PROPERTY_IN_CALL;
  898. } elseif ($host['callStartTime'] !== 0 && ($host['callStartTime'] !== $localCallStartTime || $host['callFlag'] !== $local->getCallFlag())) {
  899. $startDateTime = $this->timeFactory->getDateTime('@' . $host['callStartTime']);
  900. $this->setActiveSince($local, null, $startDateTime, $host['callFlag'], true);
  901. $changed[] = ARoomModifiedEvent::PROPERTY_ACTIVE_SINCE;
  902. $changed[] = ARoomModifiedEvent::PROPERTY_IN_CALL;
  903. }
  904. }
  905. if (isset($host['mentionPermissions']) && $host['mentionPermissions'] !== $local->getMentionPermissions()) {
  906. try {
  907. $this->setMentionPermissions($local, $host['mentionPermissions']);
  908. $changed[] = ARoomModifiedEvent::PROPERTY_MENTION_PERMISSIONS;
  909. } catch (\InvalidArgumentException $e) {
  910. $this->logger->error('An error (' . $e->getMessage() . ') occurred while trying to sync mentionPermissions of ' . $local->getId() . ' to ' . $host['mentionPermissions'], ['exception' => $e]);
  911. }
  912. }
  913. if (isset($host['messageExpiration']) && $host['messageExpiration'] !== $local->getMessageExpiration()) {
  914. try {
  915. $this->setMessageExpiration($local, $host['messageExpiration']);
  916. $changed[] = ARoomModifiedEvent::PROPERTY_MESSAGE_EXPIRATION;
  917. } catch (\InvalidArgumentException $e) {
  918. $this->logger->error('An error (' . $e->getMessage() . ') occurred while trying to sync messageExpiration of ' . $local->getId() . ' to ' . $host['messageExpiration'], ['exception' => $e]);
  919. }
  920. }
  921. if (isset($host['readOnly']) && $host['readOnly'] !== $local->getReadOnly()) {
  922. $success = $this->setReadOnly($local, $host['readOnly']);
  923. if (!$success) {
  924. $this->logger->error('An error occurred while trying to sync readOnly of ' . $local->getId() . ' to ' . $host['readOnly']);
  925. } else {
  926. $changed[] = ARoomModifiedEvent::PROPERTY_READ_ONLY;
  927. }
  928. }
  929. if (isset($host['recordingConsent']) && $host['recordingConsent'] !== $local->getRecordingConsent()) {
  930. try {
  931. $this->setRecordingConsent($local, $host['recordingConsent'], true);
  932. $changed[] = ARoomModifiedEvent::PROPERTY_RECORDING_CONSENT;
  933. } catch (\InvalidArgumentException $e) {
  934. $this->logger->error('An error (' . $e->getMessage() . ') occurred while trying to sync recordingConsent of ' . $local->getId() . ' to ' . $host['recordingConsent'], ['exception' => $e]);
  935. }
  936. }
  937. if (isset($host['sipEnabled']) && $host['sipEnabled'] !== $local->getSIPEnabled()) {
  938. try {
  939. $this->setSIPEnabled($local, $host['sipEnabled']);
  940. $changed[] = ARoomModifiedEvent::PROPERTY_SIP_ENABLED;
  941. } catch (SipConfigurationException $e) {
  942. $this->logger->error('An error (' . $e->getReason() . ') occurred while trying to sync sipEnabled of ' . $local->getId() . ' to ' . $host['sipEnabled'], ['exception' => $e]);
  943. }
  944. }
  945. // Ignore for now, so the conversation is not found by other users on this federated participants server
  946. // if (isset($host['listable']) && $host['listable'] !== $local->getListable()) {
  947. // $success = $this->setListable($local, $host['listable']);
  948. // if (!$success) {
  949. // $this->logger->error('An error occurred while trying to sync listable of ' . $local->getId() . ' to ' . $host['listable']);
  950. // } else {
  951. // $changed[] = ARoomModifiedEvent::PROPERTY_LISTABLE;
  952. // }
  953. // }
  954. $event = new RoomSyncedEvent($local, $changed);
  955. $this->dispatcher->dispatchTyped($event);
  956. }
  957. public function deleteRoom(Room $room): void {
  958. $event = new BeforeRoomDeletedEvent($room);
  959. $this->dispatcher->dispatchTyped($event);
  960. // Delete all breakout rooms when deleting a parent room
  961. if ($room->getBreakoutRoomMode() !== BreakoutRoom::MODE_NOT_CONFIGURED) {
  962. $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken());
  963. foreach ($breakoutRooms as $breakoutRoom) {
  964. $this->deleteRoom($breakoutRoom);
  965. }
  966. }
  967. if ($room->isFederatedConversation()) {
  968. // Delete PCM messages
  969. $delete = $this->db->getQueryBuilder();
  970. $delete->delete('talk_proxy_messages')
  971. ->where($delete->expr()->eq('local_token', $delete->createNamedParameter($room->getToken())));
  972. $delete->executeStatement();
  973. }
  974. // Delete attendees
  975. $delete = $this->db->getQueryBuilder();
  976. $delete->delete('talk_attendees')
  977. ->where($delete->expr()->eq('room_id', $delete->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  978. $delete->executeStatement();
  979. // Delete room
  980. $delete = $this->db->getQueryBuilder();
  981. $delete->delete('talk_rooms')
  982. ->where($delete->expr()->eq('id', $delete->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT)));
  983. $delete->executeStatement();
  984. $event = new RoomDeletedEvent($room);
  985. $this->dispatcher->dispatchTyped($event);
  986. if (class_exists(CriticalActionPerformedEvent::class)) {
  987. $this->dispatcher->dispatchTyped(new CriticalActionPerformedEvent(
  988. 'Conversation "%s" deleted',
  989. ['name' => $room->getName()],
  990. ));
  991. }
  992. }
  993. }