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.

1619 lines
53 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
  5. * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
  6. *
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. *
  10. * @license GNU AGPL version 3 or any later version
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU Affero General Public License as
  14. * published by the Free Software Foundation, either version 3 of the
  15. * License, or (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  24. *
  25. */
  26. namespace OCA\Talk\Controller;
  27. use InvalidArgumentException;
  28. use OCA\Talk\Config;
  29. use OCA\Talk\Events\UserEvent;
  30. use OCA\Talk\Exceptions\ForbiddenException;
  31. use OCA\Talk\Exceptions\InvalidPasswordException;
  32. use OCA\Talk\Exceptions\ParticipantNotFoundException;
  33. use OCA\Talk\Exceptions\RoomNotFoundException;
  34. use OCA\Talk\Exceptions\UnauthorizedException;
  35. use OCA\Talk\GuestManager;
  36. use OCA\Talk\Manager;
  37. use OCA\Talk\MatterbridgeManager;
  38. use OCA\Talk\Model\Attendee;
  39. use OCA\Talk\Model\BreakoutRoom;
  40. use OCA\Talk\Model\Session;
  41. use OCA\Talk\Participant;
  42. use OCA\Talk\Room;
  43. use OCA\Talk\Service\BreakoutRoomService;
  44. use OCA\Talk\Service\ParticipantService;
  45. use OCA\Talk\Service\RoomFormatter;
  46. use OCA\Talk\Service\RoomService;
  47. use OCA\Talk\Service\SessionService;
  48. use OCA\Talk\Service\SIPBridgeService;
  49. use OCA\Talk\TalkSession;
  50. use OCA\Talk\Webinary;
  51. use OCP\App\IAppManager;
  52. use OCP\AppFramework\Http;
  53. use OCP\AppFramework\Http\DataResponse;
  54. use OCP\AppFramework\Utility\ITimeFactory;
  55. use OCP\EventDispatcher\IEventDispatcher;
  56. use OCP\Federation\ICloudIdManager;
  57. use OCP\HintException;
  58. use OCP\IConfig;
  59. use OCP\IGroup;
  60. use OCP\IGroupManager;
  61. use OCP\IRequest;
  62. use OCP\IUser;
  63. use OCP\IUserManager;
  64. use OCP\Security\Bruteforce\IThrottler;
  65. use OCP\User\Events\UserLiveStatusEvent;
  66. use OCP\UserStatus\IManager as IUserStatusManager;
  67. use OCP\UserStatus\IUserStatus;
  68. use Psr\Log\LoggerInterface;
  69. class RoomController extends AEnvironmentAwareController {
  70. public const EVENT_BEFORE_ROOMS_GET = self::class . '::preGetRooms';
  71. protected ?string $userId;
  72. protected IAppManager $appManager;
  73. protected TalkSession $session;
  74. protected IUserManager $userManager;
  75. protected IGroupManager $groupManager;
  76. protected Manager $manager;
  77. protected ICloudIdManager $cloudIdManager;
  78. protected RoomService $roomService;
  79. protected BreakoutRoomService $breakoutRoomService;
  80. protected ParticipantService $participantService;
  81. protected SessionService $sessionService;
  82. protected GuestManager $guestManager;
  83. protected IUserStatusManager $statusManager;
  84. protected IEventDispatcher $dispatcher;
  85. protected ITimeFactory $timeFactory;
  86. protected SIPBridgeService $SIPBridgeService;
  87. protected RoomFormatter $roomFormatter;
  88. protected IConfig $config;
  89. protected Config $talkConfig;
  90. protected IThrottler $throttler;
  91. protected LoggerInterface $logger;
  92. protected array $commonReadMessages = [];
  93. public function __construct(string $appName,
  94. ?string $UserId,
  95. IRequest $request,
  96. IAppManager $appManager,
  97. TalkSession $session,
  98. IUserManager $userManager,
  99. IGroupManager $groupManager,
  100. Manager $manager,
  101. RoomService $roomService,
  102. BreakoutRoomService $breakoutRoomService,
  103. ParticipantService $participantService,
  104. SessionService $sessionService,
  105. GuestManager $guestManager,
  106. IUserStatusManager $statusManager,
  107. IEventDispatcher $dispatcher,
  108. ITimeFactory $timeFactory,
  109. SIPBridgeService $SIPBridgeService,
  110. RoomFormatter $roomFormatter,
  111. IConfig $config,
  112. Config $talkConfig,
  113. ICloudIdManager $cloudIdManager,
  114. IThrottler $throttler,
  115. LoggerInterface $logger) {
  116. parent::__construct($appName, $request);
  117. $this->session = $session;
  118. $this->appManager = $appManager;
  119. $this->userId = $UserId;
  120. $this->userManager = $userManager;
  121. $this->groupManager = $groupManager;
  122. $this->manager = $manager;
  123. $this->roomService = $roomService;
  124. $this->breakoutRoomService = $breakoutRoomService;
  125. $this->participantService = $participantService;
  126. $this->sessionService = $sessionService;
  127. $this->guestManager = $guestManager;
  128. $this->statusManager = $statusManager;
  129. $this->dispatcher = $dispatcher;
  130. $this->timeFactory = $timeFactory;
  131. $this->SIPBridgeService = $SIPBridgeService;
  132. $this->config = $config;
  133. $this->talkConfig = $talkConfig;
  134. $this->cloudIdManager = $cloudIdManager;
  135. $this->throttler = $throttler;
  136. $this->logger = $logger;
  137. $this->roomFormatter = $roomFormatter;
  138. }
  139. protected function getTalkHashHeader(): array {
  140. return [
  141. 'X-Nextcloud-Talk-Hash' => sha1(
  142. $this->config->getSystemValueString('version') . '#' .
  143. $this->config->getAppValue('spreed', 'installed_version', '') . '#' .
  144. $this->config->getAppValue('spreed', 'stun_servers', '') . '#' .
  145. $this->config->getAppValue('spreed', 'turn_servers', '') . '#' .
  146. $this->config->getAppValue('spreed', 'signaling_servers', '') . '#' .
  147. $this->config->getAppValue('spreed', 'signaling_mode', '') . '#' .
  148. $this->config->getAppValue('spreed', 'allowed_groups', '') . '#' .
  149. $this->config->getAppValue('spreed', 'start_calls', '') . '#' .
  150. $this->config->getAppValue('spreed', 'start_conversations', '') . '#' .
  151. $this->config->getAppValue('spreed', 'has_reference_id', '') . '#' .
  152. $this->config->getAppValue('spreed', 'sip_bridge_groups', '[]') . '#' .
  153. $this->config->getAppValue('spreed', 'sip_bridge_dialin_info') . '#' .
  154. $this->config->getAppValue('spreed', 'sip_bridge_shared_secret') . '#' .
  155. $this->config->getAppValue('theming', 'cachebuster', '1')
  156. )];
  157. }
  158. /**
  159. * Get all currently existent rooms which the user has joined
  160. *
  161. * @NoAdminRequired
  162. *
  163. * @param int $noStatusUpdate When the user status should not be automatically set to online set to 1 (default 0)
  164. * @param bool $includeStatus
  165. * @return DataResponse
  166. */
  167. public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, int $modifiedSince = 0): DataResponse {
  168. $nextModifiedSince = $this->timeFactory->getTime();
  169. $event = new UserEvent($this->userId);
  170. $this->dispatcher->dispatch(self::EVENT_BEFORE_ROOMS_GET, $event);
  171. if ($noStatusUpdate === 0) {
  172. $isMobileApp = $this->request->isUserAgent([
  173. IRequest::USER_AGENT_TALK_ANDROID,
  174. IRequest::USER_AGENT_TALK_IOS,
  175. ]);
  176. if ($isMobileApp) {
  177. // Bump the user status again
  178. $event = new UserLiveStatusEvent(
  179. $this->userManager->get($this->userId),
  180. IUserStatus::ONLINE,
  181. $this->timeFactory->getTime()
  182. );
  183. $this->dispatcher->dispatchTyped($event);
  184. }
  185. }
  186. $sessionIds = $this->session->getAllActiveSessions();
  187. $rooms = $this->manager->getRoomsForUser($this->userId, $sessionIds, true);
  188. if ($modifiedSince !== 0) {
  189. $rooms = array_filter($rooms, static function (Room $room) use ($includeStatus, $modifiedSince): bool {
  190. return ($includeStatus && $room->getType() === Room::TYPE_ONE_TO_ONE)
  191. || ($room->getLastActivity() && $room->getLastActivity()->getTimestamp() >= $modifiedSince);
  192. });
  193. }
  194. $readPrivacy = $this->talkConfig->getUserReadPrivacy($this->userId);
  195. if ($readPrivacy === Participant::PRIVACY_PUBLIC) {
  196. $roomIds = array_map(static function (Room $room) {
  197. return $room->getId();
  198. }, $rooms);
  199. $this->commonReadMessages = $this->participantService->getLastCommonReadChatMessageForMultipleRooms($roomIds);
  200. }
  201. $statuses = [];
  202. if ($this->userId !== null
  203. && $includeStatus
  204. && $this->appManager->isEnabledForUser('user_status')) {
  205. $userIds = array_filter(array_map(function (Room $room) {
  206. if ($room->getType() === Room::TYPE_ONE_TO_ONE) {
  207. $participants = json_decode($room->getName(), true);
  208. foreach ($participants as $participant) {
  209. if ($participant !== $this->userId) {
  210. return $participant;
  211. }
  212. }
  213. }
  214. return null;
  215. }, $rooms));
  216. $statuses = $this->statusManager->getUserStatuses($userIds);
  217. }
  218. $return = [];
  219. foreach ($rooms as $room) {
  220. try {
  221. $return[] = $this->formatRoom($room, $this->participantService->getParticipant($room, $this->userId), $statuses);
  222. } catch (ParticipantNotFoundException $e) {
  223. // for example in case the room was deleted concurrently,
  224. // the user is not a participant anymore
  225. }
  226. }
  227. $response = new DataResponse($return, Http::STATUS_OK, $this->getTalkHashHeader());
  228. $response->addHeader('X-Nextcloud-Talk-Modified-Before', (string) $nextModifiedSince);
  229. return $response;
  230. }
  231. /**
  232. * Get listed rooms with optional search term
  233. *
  234. * @NoAdminRequired
  235. *
  236. * @param string $searchTerm search term
  237. * @return DataResponse
  238. */
  239. public function getListedRooms(string $searchTerm = ''): DataResponse {
  240. $rooms = $this->manager->getListedRoomsForUser($this->userId, $searchTerm);
  241. $return = [];
  242. foreach ($rooms as $room) {
  243. $return[] = $this->formatRoom($room, null);
  244. }
  245. return new DataResponse($return, Http::STATUS_OK);
  246. }
  247. /**
  248. * Get all (for moderators and in case of "free selection) or the assigned breakout room
  249. *
  250. * @NoAdminRequired
  251. * @RequireLoggedInParticipant
  252. * @BruteForceProtection(action=talkRoomToken)
  253. *
  254. * @return DataResponse
  255. */
  256. public function getBreakoutRooms(): DataResponse {
  257. try {
  258. $rooms = $this->breakoutRoomService->getBreakoutRooms($this->room, $this->participant);
  259. } catch (InvalidArgumentException $e) {
  260. return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
  261. }
  262. $return = [];
  263. foreach ($rooms as $room) {
  264. try {
  265. $participant = $this->participantService->getParticipant($room, $this->userId);
  266. } catch (ParticipantNotFoundException $e) {
  267. $participant = null;
  268. }
  269. $return[] = $this->formatRoom($room, $participant, null, false, true);
  270. }
  271. return new DataResponse($return);
  272. }
  273. /**
  274. * @PublicPage
  275. * @BruteForceProtection(action=talkRoomToken)
  276. *
  277. * @param string $token
  278. * @return DataResponse
  279. */
  280. public function getSingleRoom(string $token): DataResponse {
  281. try {
  282. $isSIPBridgeRequest = $this->validateSIPBridgeRequest($token);
  283. } catch (UnauthorizedException $e) {
  284. $ip = $this->request->getRemoteAddress();
  285. $action = 'talkSipBridgeSecret';
  286. $this->throttler->sleepDelay($ip, $action);
  287. $this->throttler->registerAttempt($action, $ip);
  288. return new DataResponse([], Http::STATUS_UNAUTHORIZED);
  289. }
  290. // The SIP bridge only needs room details (public, sip enabled, lobby state, etc)
  291. $includeLastMessage = !$isSIPBridgeRequest;
  292. try {
  293. $sessionId = $this->session->getSessionForRoom($token);
  294. $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId, $includeLastMessage, $isSIPBridgeRequest);
  295. $participant = null;
  296. try {
  297. $participant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
  298. } catch (ParticipantNotFoundException $e) {
  299. try {
  300. $participant = $this->participantService->getParticipantBySession($room, $sessionId);
  301. } catch (ParticipantNotFoundException $e) {
  302. }
  303. }
  304. $statuses = [];
  305. if ($this->userId !== null
  306. && $this->appManager->isEnabledForUser('user_status')) {
  307. $userIds = array_filter(array_map(function (Room $room) {
  308. if ($room->getType() === Room::TYPE_ONE_TO_ONE) {
  309. $participants = json_decode($room->getName(), true);
  310. foreach ($participants as $participant) {
  311. if ($participant !== $this->userId) {
  312. return $participant;
  313. }
  314. }
  315. }
  316. return null;
  317. }, [$room]));
  318. $statuses = $this->statusManager->getUserStatuses($userIds);
  319. }
  320. return new DataResponse($this->formatRoom($room, $participant, $statuses, $isSIPBridgeRequest), Http::STATUS_OK, $this->getTalkHashHeader());
  321. } catch (RoomNotFoundException $e) {
  322. $response = new DataResponse([], Http::STATUS_NOT_FOUND);
  323. $response->throttle(['token' => $token]);
  324. return $response;
  325. }
  326. }
  327. /**
  328. * Check if the current request is coming from an allowed backend.
  329. *
  330. * The SIP bridge is sending the custom header "Talk-SIPBridge-Random"
  331. * containing at least 32 bytes random data, and the header
  332. * "Talk-SIPBridge-Checksum", which is the SHA256-HMAC of the random data
  333. * and the body of the request, calculated with the shared secret from the
  334. * configuration.
  335. *
  336. * @param string $token
  337. * @return bool True if the request is from the SIP bridge and valid, false if not from SIP bridge
  338. * @throws UnauthorizedException when the request tried to sign as SIP bridge but is not valid
  339. */
  340. private function validateSIPBridgeRequest(string $token): bool {
  341. $random = $this->request->getHeader('TALK_SIPBRIDGE_RANDOM');
  342. $checksum = $this->request->getHeader('TALK_SIPBRIDGE_CHECKSUM');
  343. $secret = $this->talkConfig->getSIPSharedSecret();
  344. return $this->SIPBridgeService->validateSIPBridgeRequest($random, $checksum, $secret, $token);
  345. }
  346. protected function formatRoom(Room $room, ?Participant $currentParticipant, ?array $statuses = null, bool $isSIPBridgeRequest = false, bool $isListingBreakoutRooms = false): array {
  347. return $this->roomFormatter->formatRoom(
  348. $this->getResponseFormat(),
  349. $this->commonReadMessages,
  350. $room,
  351. $currentParticipant,
  352. $statuses,
  353. $isSIPBridgeRequest,
  354. $isListingBreakoutRooms,
  355. );
  356. }
  357. /**
  358. * Initiates a one-to-one video call from the current user to the recipient
  359. *
  360. * @NoAdminRequired
  361. *
  362. * @param int $roomType
  363. * @param string $invite
  364. * @param string $roomName
  365. * @param string $source
  366. * @return DataResponse
  367. */
  368. public function createRoom(int $roomType, string $invite = '', string $roomName = '', string $source = '', string $objectType = '', string $objectId = ''): DataResponse {
  369. if ($roomType !== Room::TYPE_ONE_TO_ONE) {
  370. /** @var IUser $user */
  371. $user = $this->userManager->get($this->userId);
  372. if ($this->talkConfig->isNotAllowedToCreateConversations($user)) {
  373. return new DataResponse([], Http::STATUS_FORBIDDEN);
  374. }
  375. }
  376. switch ($roomType) {
  377. case Room::TYPE_ONE_TO_ONE:
  378. return $this->createOneToOneRoom($invite);
  379. case Room::TYPE_GROUP:
  380. if ($invite === '') {
  381. return $this->createEmptyRoom($roomName, false, $objectType, $objectId);
  382. }
  383. if ($source === 'circles') {
  384. return $this->createCircleRoom($invite);
  385. }
  386. return $this->createGroupRoom($invite);
  387. case Room::TYPE_PUBLIC:
  388. return $this->createEmptyRoom($roomName);
  389. }
  390. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  391. }
  392. /**
  393. * Initiates a one-to-one video call from the current user to the recipient
  394. *
  395. * @NoAdminRequired
  396. *
  397. * @param string $targetUserId
  398. * @return DataResponse
  399. */
  400. protected function createOneToOneRoom(string $targetUserId): DataResponse {
  401. $currentUser = $this->userManager->get($this->userId);
  402. if (!$currentUser instanceof IUser) {
  403. return new DataResponse([], Http::STATUS_NOT_FOUND);
  404. }
  405. if ($targetUserId === MatterbridgeManager::BRIDGE_BOT_USERID) {
  406. return new DataResponse([], Http::STATUS_NOT_FOUND);
  407. }
  408. $targetUser = $this->userManager->get($targetUserId);
  409. if (!$targetUser instanceof IUser) {
  410. return new DataResponse([], Http::STATUS_NOT_FOUND);
  411. }
  412. try {
  413. // We are only doing this manually here to be able to return different status codes
  414. // Actually createOneToOneConversation also checks it.
  415. $room = $this->manager->getOne2OneRoom($currentUser->getUID(), $targetUser->getUID());
  416. $this->participantService->ensureOneToOneRoomIsFilled($room);
  417. return new DataResponse(
  418. $this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)),
  419. Http::STATUS_OK
  420. );
  421. } catch (RoomNotFoundException $e) {
  422. }
  423. try {
  424. $room = $this->roomService->createOneToOneConversation($currentUser, $targetUser);
  425. return new DataResponse(
  426. $this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)),
  427. Http::STATUS_CREATED
  428. );
  429. } catch (InvalidArgumentException $e) {
  430. // Same current and target user
  431. return new DataResponse([], Http::STATUS_FORBIDDEN);
  432. } catch (RoomNotFoundException $e) {
  433. return new DataResponse([], Http::STATUS_FORBIDDEN);
  434. }
  435. }
  436. /**
  437. * Initiates a group video call from the selected group
  438. *
  439. * @NoAdminRequired
  440. *
  441. * @param string $targetGroupName
  442. * @return DataResponse
  443. */
  444. protected function createGroupRoom(string $targetGroupName): DataResponse {
  445. $currentUser = $this->userManager->get($this->userId);
  446. if (!$currentUser instanceof IUser) {
  447. return new DataResponse([], Http::STATUS_NOT_FOUND);
  448. }
  449. $targetGroup = $this->groupManager->get($targetGroupName);
  450. if (!$targetGroup instanceof IGroup) {
  451. return new DataResponse([], Http::STATUS_NOT_FOUND);
  452. }
  453. // Create the room
  454. $name = $this->roomService->prepareConversationName($targetGroup->getDisplayName());
  455. $room = $this->roomService->createConversation(Room::TYPE_GROUP, $name, $currentUser);
  456. $this->participantService->addGroup($room, $targetGroup);
  457. return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)), Http::STATUS_CREATED);
  458. }
  459. /**
  460. * Initiates a group video call from the selected circle
  461. *
  462. * @NoAdminRequired
  463. *
  464. * @param string $targetCircleId
  465. * @return DataResponse
  466. */
  467. protected function createCircleRoom(string $targetCircleId): DataResponse {
  468. if (!$this->appManager->isEnabledForUser('circles')) {
  469. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  470. }
  471. $currentUser = $this->userManager->get($this->userId);
  472. if (!$currentUser instanceof IUser) {
  473. return new DataResponse([], Http::STATUS_NOT_FOUND);
  474. }
  475. try {
  476. $circle = $this->participantService->getCircle($targetCircleId, $this->userId);
  477. } catch (\Exception $e) {
  478. return new DataResponse([], Http::STATUS_NOT_FOUND);
  479. }
  480. // Create the room
  481. $name = $this->roomService->prepareConversationName($circle->getName());
  482. $room = $this->roomService->createConversation(Room::TYPE_GROUP, $name, $currentUser);
  483. $this->participantService->addCircle($room, $circle);
  484. return new DataResponse($this->formatRoom($room, $this->participantService->getParticipant($room, $currentUser->getUID(), false)), Http::STATUS_CREATED);
  485. }
  486. /**
  487. * @NoAdminRequired
  488. */
  489. protected function createEmptyRoom(string $roomName, bool $public = true, string $objectType = '', string $objectId = ''): DataResponse {
  490. $currentUser = $this->userManager->get($this->userId);
  491. if (!$currentUser instanceof IUser) {
  492. return new DataResponse([], Http::STATUS_NOT_FOUND);
  493. }
  494. $roomType = $public ? Room::TYPE_PUBLIC : Room::TYPE_GROUP;
  495. /** @var Room|null $parentRoom */
  496. $parentRoom = null;
  497. if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
  498. try {
  499. $parentRoom = $this->manager->getRoomForUserByToken($objectId, $this->userId);
  500. $parentRoomParticipant = $this->participantService->getParticipant($parentRoom, $this->userId);
  501. if (!$parentRoomParticipant->hasModeratorPermissions()) {
  502. return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
  503. }
  504. if ($parentRoom->getBreakoutRoomMode() === BreakoutRoom::MODE_NOT_CONFIGURED) {
  505. return new DataResponse(['error' => 'mode'], Http::STATUS_BAD_REQUEST);
  506. }
  507. // Overwriting the type with the parent type.
  508. $roomType = $parentRoom->getType();
  509. } catch (RoomNotFoundException $e) {
  510. return new DataResponse(['error' => 'room'], Http::STATUS_BAD_REQUEST);
  511. } catch (ParticipantNotFoundException $e) {
  512. return new DataResponse(['error' => 'permissions'], Http::STATUS_BAD_REQUEST);
  513. }
  514. } elseif ($objectType !== '') {
  515. return new DataResponse(['error' => 'object'], Http::STATUS_BAD_REQUEST);
  516. }
  517. // Create the room
  518. try {
  519. $room = $this->roomService->createConversation($roomType, $roomName, $currentUser, $objectType, $objectId);
  520. } catch (InvalidArgumentException $e) {
  521. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  522. }
  523. $currentParticipant = $this->participantService->getParticipant($room, $currentUser->getUID(), false);
  524. if ($objectType === BreakoutRoom::PARENT_OBJECT_TYPE) {
  525. // Enforce the lobby state when breakout rooms are disabled
  526. if ($parentRoom instanceof Room && $parentRoom->getBreakoutRoomStatus() === BreakoutRoom::STATUS_STOPPED) {
  527. $this->roomService->setLobby($room, Webinary::LOBBY_NON_MODERATORS, null, false, false);
  528. }
  529. $participants = $this->participantService->getParticipantsForRoom($parentRoom);
  530. $moderators = array_filter($participants, static function (Participant $participant) use ($currentParticipant) {
  531. return $participant->hasModeratorPermissions()
  532. && $participant->getAttendee()->getId() !== $currentParticipant->getAttendee()->getId();
  533. });
  534. if (!empty($moderators)) {
  535. $this->breakoutRoomService->addModeratorsToBreakoutRooms([$room], $moderators);
  536. }
  537. }
  538. return new DataResponse($this->formatRoom($room, $currentParticipant), Http::STATUS_CREATED);
  539. }
  540. /**
  541. * @NoAdminRequired
  542. * @RequireLoggedInParticipant
  543. *
  544. * @return DataResponse
  545. */
  546. public function addToFavorites(): DataResponse {
  547. $this->participantService->updateFavoriteStatus($this->participant, true);
  548. return new DataResponse([]);
  549. }
  550. /**
  551. * @NoAdminRequired
  552. * @RequireLoggedInParticipant
  553. *
  554. * @return DataResponse
  555. */
  556. public function removeFromFavorites(): DataResponse {
  557. $this->participantService->updateFavoriteStatus($this->participant, false);
  558. return new DataResponse([]);
  559. }
  560. /**
  561. * @NoAdminRequired
  562. * @RequireLoggedInParticipant
  563. *
  564. * @param int $level
  565. * @return DataResponse
  566. */
  567. public function setNotificationLevel(int $level): DataResponse {
  568. try {
  569. $this->participantService->updateNotificationLevel($this->participant, $level);
  570. } catch (\InvalidArgumentException $e) {
  571. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  572. }
  573. return new DataResponse();
  574. }
  575. /**
  576. * @NoAdminRequired
  577. * @RequireLoggedInParticipant
  578. *
  579. * @param int $level
  580. * @return DataResponse
  581. */
  582. public function setNotificationCalls(int $level): DataResponse {
  583. try {
  584. $this->participantService->updateNotificationCalls($this->participant, $level);
  585. } catch (\InvalidArgumentException $e) {
  586. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  587. }
  588. return new DataResponse();
  589. }
  590. /**
  591. * @PublicPage
  592. * @RequireModeratorParticipant
  593. *
  594. * @param string $roomName
  595. * @return DataResponse
  596. */
  597. public function renameRoom(string $roomName): DataResponse {
  598. if ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  599. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  600. }
  601. $roomName = trim($roomName);
  602. if ($roomName === '' || mb_strlen($roomName) > 255) {
  603. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  604. }
  605. $this->roomService->setName($this->room, $roomName);
  606. return new DataResponse();
  607. }
  608. /**
  609. * @PublicPage
  610. * @RequireModeratorParticipant
  611. *
  612. * @param string $description
  613. * @return DataResponse
  614. */
  615. public function setDescription(string $description): DataResponse {
  616. if ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  617. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  618. }
  619. try {
  620. $this->roomService->setDescription($this->room, $description);
  621. } catch (\LengthException $exception) {
  622. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  623. }
  624. return new DataResponse();
  625. }
  626. /**
  627. * @PublicPage
  628. * @RequireModeratorParticipant
  629. *
  630. * @return DataResponse
  631. */
  632. public function deleteRoom(): DataResponse {
  633. if ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  634. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  635. }
  636. $this->roomService->deleteRoom($this->room);
  637. return new DataResponse([]);
  638. }
  639. /**
  640. * @PublicPage
  641. * @RequireParticipant
  642. * @RequireModeratorOrNoLobby
  643. *
  644. * @param bool $includeStatus
  645. * @return DataResponse
  646. */
  647. public function getParticipants(bool $includeStatus = false): DataResponse {
  648. if ($this->participant->getAttendee()->getParticipantType() === Participant::GUEST) {
  649. return new DataResponse([], Http::STATUS_FORBIDDEN);
  650. }
  651. $participants = $this->participantService->getSessionsAndParticipantsForRoom($this->room);
  652. return $this->formatParticipantList($participants, $includeStatus);
  653. }
  654. /**
  655. * @PublicPage
  656. * @RequireParticipant
  657. * @RequireModeratorOrNoLobby
  658. *
  659. * @param bool $includeStatus
  660. * @return DataResponse
  661. */
  662. public function getBreakoutRoomParticipants(bool $includeStatus = false): DataResponse {
  663. if ($this->participant->getAttendee()->getParticipantType() === Participant::GUEST) {
  664. return new DataResponse([], Http::STATUS_FORBIDDEN);
  665. }
  666. try {
  667. $breakoutRooms = $this->breakoutRoomService->getBreakoutRooms($this->room, $this->participant);
  668. } catch (InvalidArgumentException $e) {
  669. return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
  670. }
  671. $breakoutRooms[] = $this->room;
  672. $participants = $this->participantService->getSessionsAndParticipantsForRooms($breakoutRooms);
  673. return $this->formatParticipantList($participants, $includeStatus);
  674. }
  675. /**
  676. * @param Participant[] $participants
  677. * @param bool $includeStatus
  678. * @return DataResponse
  679. */
  680. protected function formatParticipantList(array $participants, bool $includeStatus): DataResponse {
  681. $results = $headers = $statuses = [];
  682. $maxPingAge = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT_KILL;
  683. if ($this->userId !== null
  684. && $includeStatus
  685. && count($participants) < 100
  686. && $this->appManager->isEnabledForUser('user_status')) {
  687. $userIds = array_filter(array_map(static function (Participant $participant) {
  688. if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
  689. return $participant->getAttendee()->getActorId();
  690. }
  691. return null;
  692. }, $participants));
  693. $statuses = $this->statusManager->getUserStatuses($userIds);
  694. $headers['X-Nextcloud-Has-User-Statuses'] = true;
  695. }
  696. $cleanGuests = false;
  697. foreach ($participants as $participant) {
  698. $attendeeId = $participant->getAttendee()->getId();
  699. if (isset($results[$attendeeId])) {
  700. $session = $participant->getSession();
  701. if (!$session instanceof Session) {
  702. // If the user has an entry already and this has no session we don't need it anymore.
  703. continue;
  704. }
  705. if ($session->getLastPing() <= $maxPingAge) {
  706. if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) {
  707. $cleanGuests = true;
  708. } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
  709. $this->participantService->leaveRoomAsSession($this->room, $participant);
  710. }
  711. // Session expired, ignore
  712. continue;
  713. }
  714. // Combine the session values: All inCall bit flags, newest lastPing and any sessionId (for online checking)
  715. $results[$attendeeId]['inCall'] |= $session->getInCall();
  716. $results[$attendeeId]['lastPing'] = max($results[$attendeeId]['lastPing'], $session->getLastPing());
  717. $results[$attendeeId]['sessionIds'][] = $session->getSessionId();
  718. continue;
  719. }
  720. $result = [
  721. 'roomToken' => $participant->getRoom()->getToken(),
  722. 'inCall' => Participant::FLAG_DISCONNECTED,
  723. 'lastPing' => 0,
  724. 'sessionIds' => [],
  725. 'participantType' => $participant->getAttendee()->getParticipantType(),
  726. 'attendeeId' => $attendeeId,
  727. 'actorId' => $participant->getAttendee()->getActorId(),
  728. 'actorType' => $participant->getAttendee()->getActorType(),
  729. 'displayName' => $participant->getAttendee()->getActorId(),
  730. 'permissions' => $participant->getPermissions(),
  731. 'attendeePermissions' => $participant->getAttendee()->getPermissions(),
  732. 'attendeePin' => '',
  733. ];
  734. if ($this->talkConfig->isSIPConfigured()
  735. && $this->room->getSIPEnabled() !== Webinary::SIP_DISABLED
  736. && ($this->participant->hasModeratorPermissions(false)
  737. || $this->participant->getAttendee()->getId() === $participant->getAttendee()->getId())) {
  738. // Generate a PIN if the attendee is a user and doesn't have one.
  739. $this->participantService->generatePinForParticipant($this->room, $participant);
  740. $result['attendeePin'] = (string) $participant->getAttendee()->getPin();
  741. }
  742. if ($participant->getSession() instanceof Session) {
  743. $result['inCall'] = $participant->getSession()->getInCall();
  744. $result['lastPing'] = $participant->getSession()->getLastPing();
  745. $result['sessionIds'] = [$participant->getSession()->getSessionId()];
  746. }
  747. if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
  748. $userId = $participant->getAttendee()->getActorId();
  749. if ($result['lastPing'] > 0 && $result['lastPing'] <= $maxPingAge) {
  750. $this->participantService->leaveRoomAsSession($this->room, $participant);
  751. }
  752. $result['displayName'] = $participant->getAttendee()->getDisplayName();
  753. if (!$result['displayName']) {
  754. $userDisplayName = $this->userManager->getDisplayName($userId);
  755. if ($userDisplayName === null) {
  756. continue;
  757. }
  758. $result['displayName'] = $userDisplayName;
  759. }
  760. if (isset($statuses[$userId])) {
  761. $result['status'] = $statuses[$userId]->getStatus();
  762. $result['statusIcon'] = $statuses[$userId]->getIcon();
  763. $result['statusMessage'] = $statuses[$userId]->getMessage();
  764. $result['statusClearAt'] = $statuses[$userId]->getClearAt();
  765. } elseif (isset($headers['X-Nextcloud-Has-User-Statuses'])) {
  766. $result['status'] = IUserStatus::OFFLINE;
  767. $result['statusIcon'] = null;
  768. $result['statusMessage'] = null;
  769. $result['statusClearAt'] = null;
  770. }
  771. } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) {
  772. if ($result['lastPing'] <= $maxPingAge) {
  773. $cleanGuests = true;
  774. continue;
  775. }
  776. $result['displayName'] = $participant->getAttendee()->getDisplayName();
  777. } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GROUPS) {
  778. $result['displayName'] = $participant->getAttendee()->getDisplayName();
  779. } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_CIRCLES) {
  780. $result['displayName'] = $participant->getAttendee()->getDisplayName();
  781. }
  782. $results[$attendeeId] = $result;
  783. }
  784. if ($cleanGuests) {
  785. $this->participantService->cleanGuestParticipants($this->room);
  786. }
  787. return new DataResponse(array_values($results), Http::STATUS_OK, $headers);
  788. }
  789. /**
  790. * @NoAdminRequired
  791. * @RequireLoggedInModeratorParticipant
  792. *
  793. * @param string $newParticipant
  794. * @param string $source
  795. * @return DataResponse
  796. */
  797. public function addParticipantToRoom(string $newParticipant, string $source = 'users'): DataResponse {
  798. if ($this->room->getType() === Room::TYPE_ONE_TO_ONE
  799. || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER
  800. || $this->room->getObjectType() === 'share:password') {
  801. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  802. }
  803. if ($source !== 'users' && $this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  804. // Can only add users to breakout rooms
  805. return new DataResponse(['error' => 'source'], Http::STATUS_BAD_REQUEST);
  806. }
  807. $participants = $this->participantService->getParticipantsForRoom($this->room);
  808. $participantsByUserId = [];
  809. $remoteParticipantsByFederatedId = [];
  810. foreach ($participants as $participant) {
  811. if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
  812. $participantsByUserId[$participant->getAttendee()->getActorId()] = $participant;
  813. } elseif ($participant->getAttendee()->getAccessToken() === Attendee::ACTOR_FEDERATED_USERS) {
  814. $remoteParticipantsByFederatedId[$participant->getAttendee()->getActorId()] = $participant;
  815. }
  816. }
  817. // list of participants to attempt adding,
  818. // existing ones will be filtered later below
  819. $participantsToAdd = [];
  820. if ($source === 'users') {
  821. if ($newParticipant === MatterbridgeManager::BRIDGE_BOT_USERID) {
  822. return new DataResponse([], Http::STATUS_NOT_FOUND);
  823. }
  824. $newUser = $this->userManager->get($newParticipant);
  825. if (!$newUser instanceof IUser) {
  826. return new DataResponse([], Http::STATUS_NOT_FOUND);
  827. }
  828. $participantsToAdd[] = [
  829. 'actorType' => Attendee::ACTOR_USERS,
  830. 'actorId' => $newUser->getUID(),
  831. 'displayName' => $newUser->getDisplayName(),
  832. ];
  833. } elseif ($source === 'groups') {
  834. $group = $this->groupManager->get($newParticipant);
  835. if (!$group instanceof IGroup) {
  836. return new DataResponse([], Http::STATUS_NOT_FOUND);
  837. }
  838. $this->participantService->addGroup($this->room, $group, $participants);
  839. } elseif ($source === 'circles') {
  840. if (!$this->appManager->isEnabledForUser('circles')) {
  841. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  842. }
  843. try {
  844. $circle = $this->participantService->getCircle($newParticipant, $this->userId);
  845. } catch (\Exception $e) {
  846. return new DataResponse([], Http::STATUS_NOT_FOUND);
  847. }
  848. $this->participantService->addCircle($this->room, $circle, $participants);
  849. } elseif ($source === 'emails') {
  850. $data = [];
  851. if ($this->roomService->setType($this->room, Room::TYPE_PUBLIC)) {
  852. $data = ['type' => $this->room->getType()];
  853. }
  854. $participant = $this->participantService->inviteEmailAddress($this->room, $newParticipant);
  855. $this->guestManager->sendEmailInvitation($this->room, $participant);
  856. return new DataResponse($data);
  857. } elseif ($source === 'remotes') {
  858. if (!$this->talkConfig->isFederationEnabled()) {
  859. return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
  860. }
  861. try {
  862. $newUser = $this->cloudIdManager->resolveCloudId($newParticipant);
  863. } catch (\InvalidArgumentException $e) {
  864. $this->logger->error($e->getMessage(), [
  865. 'exception' => $e,
  866. ]);
  867. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  868. }
  869. $participantsToAdd[] = [
  870. 'actorType' => Attendee::ACTOR_FEDERATED_USERS,
  871. 'actorId' => $newUser->getId(),
  872. 'displayName' => $newUser->getDisplayId(),
  873. ];
  874. } else {
  875. $this->logger->error('Trying to add participant from unsupported source ' . $source);
  876. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  877. }
  878. // attempt adding the listed users to the room
  879. // existing users with USER_SELF_JOINED will get converted to regular USER participants
  880. foreach ($participantsToAdd as $index => $participantToAdd) {
  881. $existingParticipant = $participantsByUserId[$participantToAdd['actorId']] ?? null;
  882. if ($participantToAdd['actorType'] === Attendee::ACTOR_FEDERATED_USERS) {
  883. $existingParticipant = $remoteParticipantsByFederatedId[$participantToAdd['actorId']] ?? null;
  884. }
  885. if ($existingParticipant !== null) {
  886. unset($participantsToAdd[$index]);
  887. if ($existingParticipant->getAttendee()->getParticipantType() !== Participant::USER_SELF_JOINED) {
  888. // user is already a regular participant, skip
  889. continue;
  890. }
  891. $this->participantService->updateParticipantType($this->room, $existingParticipant, Participant::USER);
  892. }
  893. }
  894. $addedBy = $this->userManager->get($this->userId);
  895. if ($source === 'users' && $this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  896. $parentRoom = $this->manager->getRoomByToken($this->room->getObjectId());
  897. // Also add to parent room in case the user is missing
  898. try {
  899. $this->participantService->getParticipantByActor(
  900. $parentRoom,
  901. Attendee::ACTOR_USERS,
  902. $newParticipant
  903. );
  904. } catch (ParticipantNotFoundException $e) {
  905. $this->participantService->addUsers($parentRoom, $participantsToAdd, $addedBy);
  906. }
  907. // Remove from previous breakout room in case the user is moved
  908. try {
  909. $this->breakoutRoomService->removeAttendeeFromBreakoutRoom($parentRoom, Attendee::ACTOR_USERS, $newParticipant);
  910. } catch (\InvalidArgumentException $e) {
  911. return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
  912. }
  913. }
  914. // add the remaining users in batch
  915. $this->participantService->addUsers($this->room, $participantsToAdd, $addedBy);
  916. return new DataResponse([]);
  917. }
  918. /**
  919. * @NoAdminRequired
  920. * @RequireLoggedInParticipant
  921. *
  922. * @return DataResponse
  923. */
  924. public function removeSelfFromRoom(): DataResponse {
  925. return $this->removeSelfFromRoomLogic($this->room, $this->participant);
  926. }
  927. protected function removeSelfFromRoomLogic(Room $room, Participant $participant): DataResponse {
  928. if ($room->getType() !== Room::TYPE_ONE_TO_ONE && $room->getType() !== Room::TYPE_ONE_TO_ONE_FORMER) {
  929. if ($participant->hasModeratorPermissions(false)
  930. && $this->participantService->getNumberOfUsers($room) > 1
  931. && $this->participantService->getNumberOfModerators($room) === 1) {
  932. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  933. }
  934. }
  935. if ($room->getType() !== Room::TYPE_CHANGELOG &&
  936. $room->getObjectType() !== 'file' &&
  937. $this->participantService->getNumberOfUsers($room) === 1 &&
  938. \in_array($participant->getAttendee()->getParticipantType(), [
  939. Participant::USER,
  940. Participant::MODERATOR,
  941. Participant::OWNER,
  942. ], true)) {
  943. $this->roomService->deleteRoom($room);
  944. return new DataResponse();
  945. }
  946. $currentUser = $this->userManager->get($this->userId);
  947. if (!$currentUser instanceof IUser) {
  948. return new DataResponse([], Http::STATUS_NOT_FOUND);
  949. }
  950. $this->participantService->removeUser($room, $currentUser, Room::PARTICIPANT_LEFT);
  951. return new DataResponse();
  952. }
  953. /**
  954. * @PublicPage
  955. * @RequireModeratorParticipant
  956. *
  957. * @param int $attendeeId
  958. * @return DataResponse
  959. */
  960. public function removeAttendeeFromRoom(int $attendeeId): DataResponse {
  961. try {
  962. $targetParticipant = $this->participantService->getParticipantByAttendeeId($this->room, $attendeeId);
  963. } catch (ParticipantNotFoundException $e) {
  964. return new DataResponse([], Http::STATUS_NOT_FOUND);
  965. }
  966. if ($targetParticipant->getAttendee()->getActorType() === Attendee::ACTOR_USERS
  967. && $targetParticipant->getAttendee()->getActorId() === MatterbridgeManager::BRIDGE_BOT_USERID) {
  968. return new DataResponse([], Http::STATUS_NOT_FOUND);
  969. }
  970. if ($this->room->getType() === Room::TYPE_ONE_TO_ONE || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
  971. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  972. }
  973. if ($this->participant->getAttendee()->getId() === $targetParticipant->getAttendee()->getId()) {
  974. return $this->removeSelfFromRoomLogic($this->room, $targetParticipant);
  975. }
  976. if ($targetParticipant->getAttendee()->getParticipantType() === Participant::OWNER) {
  977. return new DataResponse([], Http::STATUS_FORBIDDEN);
  978. }
  979. $this->participantService->removeAttendee($this->room, $targetParticipant, Room::PARTICIPANT_REMOVED);
  980. return new DataResponse([]);
  981. }
  982. /**
  983. * @NoAdminRequired
  984. * @RequireLoggedInModeratorParticipant
  985. *
  986. * @return DataResponse
  987. */
  988. public function makePublic(): DataResponse {
  989. if (!$this->roomService->setType($this->room, Room::TYPE_PUBLIC)) {
  990. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  991. }
  992. return new DataResponse();
  993. }
  994. /**
  995. * @NoAdminRequired
  996. * @RequireLoggedInModeratorParticipant
  997. *
  998. * @return DataResponse
  999. */
  1000. public function makePrivate(): DataResponse {
  1001. if (!$this->roomService->setType($this->room, Room::TYPE_GROUP)) {
  1002. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1003. }
  1004. return new DataResponse();
  1005. }
  1006. /**
  1007. * @NoAdminRequired
  1008. * @RequireModeratorParticipant
  1009. *
  1010. * @param int $state
  1011. * @return DataResponse
  1012. */
  1013. public function setReadOnly(int $state): DataResponse {
  1014. if (!$this->roomService->setReadOnly($this->room, $state)) {
  1015. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1016. }
  1017. if ($state === Room::READ_ONLY) {
  1018. $participants = $this->participantService->getParticipantsInCall($this->room);
  1019. // kick out all participants out of the call
  1020. foreach ($participants as $participant) {
  1021. $this->participantService->changeInCall($this->room, $participant, Participant::FLAG_DISCONNECTED);
  1022. }
  1023. }
  1024. return new DataResponse();
  1025. }
  1026. /**
  1027. * @NoAdminRequired
  1028. * @RequireModeratorParticipant
  1029. *
  1030. * @param int $scope
  1031. * @return DataResponse
  1032. */
  1033. public function setListable(int $scope): DataResponse {
  1034. if (!$this->roomService->setListable($this->room, $scope)) {
  1035. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1036. }
  1037. return new DataResponse();
  1038. }
  1039. /**
  1040. * @PublicPage
  1041. * @RequireModeratorParticipant
  1042. *
  1043. * @param string $password
  1044. * @return DataResponse
  1045. */
  1046. public function setPassword(string $password): DataResponse {
  1047. if ($this->room->getType() !== Room::TYPE_PUBLIC) {
  1048. return new DataResponse([], Http::STATUS_FORBIDDEN);
  1049. }
  1050. try {
  1051. $this->roomService->setPassword($this->room, $password);
  1052. } catch (HintException $e) {
  1053. return new DataResponse([
  1054. 'message' => $e->getHint(),
  1055. ], Http::STATUS_BAD_REQUEST);
  1056. }
  1057. return new DataResponse();
  1058. }
  1059. /**
  1060. * @PublicPage
  1061. * @BruteForceProtection(action=talkRoomPassword)
  1062. *
  1063. * @param string $token
  1064. * @param string $password
  1065. * @param bool $force
  1066. * @return DataResponse
  1067. */
  1068. public function joinRoom(string $token, string $password = '', bool $force = true): DataResponse {
  1069. $sessionId = $this->session->getSessionForRoom($token);
  1070. try {
  1071. // The participant is just joining, so enforce to not load any session
  1072. $room = $this->manager->getRoomForUserByToken($token, $this->userId, null);
  1073. } catch (RoomNotFoundException $e) {
  1074. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1075. }
  1076. /** @var Participant|null $previousSession */
  1077. $previousParticipant = null;
  1078. /** @var Session|null $previousSession */
  1079. $previousSession = null;
  1080. if ($sessionId !== null) {
  1081. try {
  1082. if ($this->userId !== null) {
  1083. $previousParticipant = $this->participantService->getParticipant($room, $this->userId, $sessionId);
  1084. } else {
  1085. $previousParticipant = $this->participantService->getParticipantBySession($room, $sessionId);
  1086. }
  1087. $previousSession = $previousParticipant->getSession();
  1088. } catch (ParticipantNotFoundException $e) {
  1089. }
  1090. if ($previousSession instanceof Session && $previousSession->getSessionId() === $sessionId) {
  1091. if ($force === false && $previousSession->getInCall() !== Participant::FLAG_DISCONNECTED) {
  1092. // Previous session is/was active in the call, show a warning
  1093. return new DataResponse([
  1094. 'sessionId' => $previousSession->getSessionId(),
  1095. 'inCall' => $previousSession->getInCall(),
  1096. 'lastPing' => $previousSession->getLastPing(),
  1097. ], Http::STATUS_CONFLICT);
  1098. }
  1099. if ($previousSession->getInCall() !== Participant::FLAG_DISCONNECTED) {
  1100. $this->participantService->changeInCall($room, $previousParticipant, Participant::FLAG_DISCONNECTED);
  1101. }
  1102. $this->participantService->leaveRoomAsSession($room, $previousParticipant, true);
  1103. }
  1104. }
  1105. $user = $this->userManager->get($this->userId);
  1106. try {
  1107. $result = $this->roomService->verifyPassword($room, (string) $this->session->getPasswordForRoom($token));
  1108. if ($user instanceof IUser) {
  1109. $participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']);
  1110. $this->participantService->generatePinForParticipant($room, $participant);
  1111. } else {
  1112. $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant);
  1113. }
  1114. $this->throttler->resetDelay($this->request->getRemoteAddress(), 'talkRoomToken', ['token' => $token]);
  1115. } catch (InvalidPasswordException $e) {
  1116. $response = new DataResponse([], Http::STATUS_FORBIDDEN);
  1117. $response->throttle(['token' => $token]);
  1118. return $response;
  1119. } catch (UnauthorizedException $e) {
  1120. $response = new DataResponse([], Http::STATUS_NOT_FOUND);
  1121. $response->throttle(['token' => $token]);
  1122. return $response;
  1123. }
  1124. $this->session->removePasswordForRoom($token);
  1125. $session = $participant->getSession();
  1126. if ($session instanceof Session) {
  1127. $this->session->setSessionForRoom($token, $session->getSessionId());
  1128. $this->sessionService->updateLastPing($session, $this->timeFactory->getTime());
  1129. }
  1130. return new DataResponse($this->formatRoom($room, $participant));
  1131. }
  1132. /**
  1133. * @PublicPage
  1134. * @RequireRoom
  1135. * @BruteForceProtection(action=talkSipBridgeSecret)
  1136. *
  1137. * @param string $pin
  1138. * @return DataResponse
  1139. */
  1140. public function getParticipantByDialInPin(string $pin): DataResponse {
  1141. try {
  1142. if (!$this->validateSIPBridgeRequest($this->room->getToken())) {
  1143. $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
  1144. $response->throttle();
  1145. return $response;
  1146. }
  1147. } catch (UnauthorizedException $e) {
  1148. $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
  1149. $response->throttle();
  1150. return $response;
  1151. }
  1152. try {
  1153. $participant = $this->participantService->getParticipantByPin($this->room, $pin);
  1154. } catch (ParticipantNotFoundException $e) {
  1155. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1156. }
  1157. return new DataResponse($this->formatRoom($this->room, $participant));
  1158. }
  1159. /**
  1160. * @PublicPage
  1161. * @RequireRoom
  1162. * @BruteForceProtection(action=talkSipBridgeSecret)
  1163. *
  1164. * @return DataResponse
  1165. */
  1166. public function createGuestByDialIn(): DataResponse {
  1167. try {
  1168. if (!$this->validateSIPBridgeRequest($this->room->getToken())) {
  1169. $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
  1170. $response->throttle();
  1171. return $response;
  1172. }
  1173. } catch (UnauthorizedException $e) {
  1174. $response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
  1175. $response->throttle();
  1176. return $response;
  1177. }
  1178. if ($this->room->getSIPEnabled() !== Webinary::SIP_ENABLED_NO_PIN) {
  1179. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1180. }
  1181. $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $this->room, '', true);
  1182. return new DataResponse($this->formatRoom($this->room, $participant));
  1183. }
  1184. /**
  1185. * @PublicPage
  1186. *
  1187. * @param string $token
  1188. * @return DataResponse
  1189. */
  1190. public function leaveRoom(string $token): DataResponse {
  1191. $sessionId = $this->session->getSessionForRoom($token);
  1192. $this->session->removeSessionForRoom($token);
  1193. try {
  1194. $room = $this->manager->getRoomForUserByToken($token, $this->userId, $sessionId);
  1195. $participant = $this->participantService->getParticipantBySession($room, $sessionId);
  1196. $this->participantService->leaveRoomAsSession($room, $participant);
  1197. } catch (RoomNotFoundException $e) {
  1198. } catch (ParticipantNotFoundException $e) {
  1199. }
  1200. return new DataResponse();
  1201. }
  1202. /**
  1203. * @PublicPage
  1204. * @RequireModeratorParticipant
  1205. *
  1206. * @param int $attendeeId
  1207. * @return DataResponse
  1208. */
  1209. public function promoteModerator(int $attendeeId): DataResponse {
  1210. return $this->changeParticipantType($attendeeId, true);
  1211. }
  1212. /**
  1213. * @PublicPage
  1214. * @RequireModeratorParticipant
  1215. *
  1216. * @param int $attendeeId
  1217. * @return DataResponse
  1218. */
  1219. public function demoteModerator(int $attendeeId): DataResponse {
  1220. return $this->changeParticipantType($attendeeId, false);
  1221. }
  1222. /**
  1223. * Toggle a user/guest to moderator/guest-moderator or vice-versa based on
  1224. * attendeeId
  1225. *
  1226. * @param int $attendeeId
  1227. * @param bool $promote Shall the attendee be promoted or demoted
  1228. * @return DataResponse
  1229. */
  1230. protected function changeParticipantType(int $attendeeId, bool $promote): DataResponse {
  1231. try {
  1232. $targetParticipant = $this->participantService->getParticipantByAttendeeId($this->room, $attendeeId);
  1233. } catch (ParticipantNotFoundException $e) {
  1234. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1235. }
  1236. $attendee = $targetParticipant->getAttendee();
  1237. if ($attendee->getActorType() === Attendee::ACTOR_USERS
  1238. && $attendee->getActorId() === MatterbridgeManager::BRIDGE_BOT_USERID) {
  1239. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1240. }
  1241. // Prevent users/moderators modifying themselves
  1242. if ($attendee->getActorType() === $this->participant->getAttendee()->getActorType()) {
  1243. if ($attendee->getActorId() === $this->participant->getAttendee()->getActorId()) {
  1244. return new DataResponse([], Http::STATUS_FORBIDDEN);
  1245. }
  1246. } elseif ($attendee->getActorType() === Attendee::ACTOR_GROUPS) {
  1247. // Can not promote/demote groups
  1248. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1249. }
  1250. if ($promote === $targetParticipant->hasModeratorPermissions()) {
  1251. // Prevent concurrent changes
  1252. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1253. }
  1254. if ($attendee->getParticipantType() === Participant::USER) {
  1255. $newType = Participant::MODERATOR;
  1256. } elseif ($attendee->getParticipantType() === Participant::GUEST) {
  1257. $newType = Participant::GUEST_MODERATOR;
  1258. } elseif ($attendee->getParticipantType() === Participant::MODERATOR) {
  1259. $newType = Participant::USER;
  1260. } elseif ($attendee->getParticipantType() === Participant::GUEST_MODERATOR) {
  1261. $newType = Participant::GUEST;
  1262. } else {
  1263. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1264. }
  1265. $this->participantService->updateParticipantType($this->room, $targetParticipant, $newType);
  1266. return new DataResponse();
  1267. }
  1268. /**
  1269. * @PublicPage
  1270. * @RequireModeratorParticipant
  1271. *
  1272. * @param int $permissions
  1273. * @return DataResponse
  1274. */
  1275. public function setPermissions(string $mode, int $permissions): DataResponse {
  1276. if (!$this->roomService->setPermissions($this->room, $mode, Attendee::PERMISSIONS_MODIFY_SET, $permissions, true)) {
  1277. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1278. }
  1279. return new DataResponse($this->formatRoom($this->room, $this->participant));
  1280. }
  1281. /**
  1282. * @PublicPage
  1283. * @RequireModeratorParticipant
  1284. *
  1285. * @param int $attendeeId
  1286. * @param string $method
  1287. * @param int $permissions
  1288. * @return DataResponse
  1289. */
  1290. public function setAttendeePermissions(int $attendeeId, string $method, int $permissions): DataResponse {
  1291. try {
  1292. $targetParticipant = $this->participantService->getParticipantByAttendeeId($this->room, $attendeeId);
  1293. } catch (ParticipantNotFoundException $e) {
  1294. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1295. }
  1296. try {
  1297. $result = $this->participantService->updatePermissions($this->room, $targetParticipant, $method, $permissions);
  1298. } catch (ForbiddenException $e) {
  1299. return new DataResponse([], Http::STATUS_FORBIDDEN);
  1300. }
  1301. if (!$result) {
  1302. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1303. }
  1304. return new DataResponse();
  1305. }
  1306. /**
  1307. * @PublicPage
  1308. * @RequireModeratorParticipant
  1309. *
  1310. * @param string $method
  1311. * @param int $permissions
  1312. * @return DataResponse
  1313. */
  1314. public function setAllAttendeesPermissions(string $method, int $permissions): DataResponse {
  1315. if (!$this->roomService->setPermissions($this->room, 'call', $method, $permissions, false)) {
  1316. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1317. }
  1318. return new DataResponse($this->formatRoom($this->room, $this->participant));
  1319. }
  1320. /**
  1321. * @NoAdminRequired
  1322. * @RequireModeratorParticipant
  1323. *
  1324. * @param int $state
  1325. * @param int|null $timer
  1326. * @return DataResponse
  1327. */
  1328. public function setLobby(int $state, ?int $timer = null): DataResponse {
  1329. $timerDateTime = null;
  1330. if ($timer !== null && $timer > 0) {
  1331. try {
  1332. $timerDateTime = $this->timeFactory->getDateTime('@' . $timer);
  1333. $timerDateTime->setTimezone(new \DateTimeZone('UTC'));
  1334. } catch (\Exception $e) {
  1335. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1336. }
  1337. }
  1338. if ($this->room->getObjectType() === BreakoutRoom::PARENT_OBJECT_TYPE) {
  1339. // Do not allow manual changing the lobby in breakout rooms
  1340. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1341. }
  1342. if (!$this->roomService->setLobby($this->room, $state, $timerDateTime)) {
  1343. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1344. }
  1345. if ($state === Webinary::LOBBY_NON_MODERATORS) {
  1346. $participants = $this->participantService->getParticipantsInCall($this->room);
  1347. foreach ($participants as $participant) {
  1348. if ($participant->hasModeratorPermissions()) {
  1349. continue;
  1350. }
  1351. $this->participantService->changeInCall($this->room, $participant, Participant::FLAG_DISCONNECTED);
  1352. }
  1353. }
  1354. return new DataResponse($this->formatRoom($this->room, $this->participant));
  1355. }
  1356. /**
  1357. * @NoAdminRequired
  1358. * @RequireModeratorParticipant
  1359. *
  1360. * @param int $state
  1361. * @return DataResponse
  1362. */
  1363. public function setSIPEnabled(int $state): DataResponse {
  1364. $user = $this->userManager->get($this->userId);
  1365. if (!$user instanceof IUser) {
  1366. return new DataResponse([], Http::STATUS_UNAUTHORIZED);
  1367. }
  1368. if (!$this->talkConfig->canUserEnableSIP($user)) {
  1369. return new DataResponse([], Http::STATUS_FORBIDDEN);
  1370. }
  1371. if (!$this->talkConfig->isSIPConfigured()) {
  1372. return new DataResponse([], Http::STATUS_PRECONDITION_FAILED);
  1373. }
  1374. if (!$this->roomService->setSIPEnabled($this->room, $state)) {
  1375. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1376. }
  1377. return new DataResponse($this->formatRoom($this->room, $this->participant));
  1378. }
  1379. /**
  1380. * @NoAdminRequired
  1381. * @RequireModeratorParticipant
  1382. *
  1383. * @param int|null $attendeeId attendee id
  1384. * @return DataResponse
  1385. */
  1386. public function resendInvitations(?int $attendeeId): DataResponse {
  1387. $participants = [];
  1388. // targeting specific participant
  1389. if ($attendeeId !== null) {
  1390. try {
  1391. $participants[] = $this->participantService->getParticipantByAttendeeId($this->room, $attendeeId);
  1392. } catch (ParticipantNotFoundException $e) {
  1393. return new DataResponse([], Http::STATUS_NOT_FOUND);
  1394. }
  1395. } else {
  1396. $participants = $this->participantService->getActorsByType($this->room, Attendee::ACTOR_EMAILS);
  1397. }
  1398. foreach ($participants as $participant) {
  1399. if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS) {
  1400. // generate PIN if applicable
  1401. $this->participantService->generatePinForParticipant($this->room, $participant);
  1402. $this->guestManager->sendEmailInvitation($this->room, $participant);
  1403. }
  1404. }
  1405. return new DataResponse();
  1406. }
  1407. /**
  1408. * @PublicPage
  1409. * @RequireModeratorParticipant
  1410. */
  1411. public function setMessageExpiration(int $seconds): DataResponse {
  1412. if ($seconds < 0) {
  1413. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  1414. }
  1415. $this->roomService->setMessageExpiration($this->room, $seconds);
  1416. return new DataResponse();
  1417. }
  1418. }