diff --git a/appinfo/info.xml b/appinfo/info.xml index 792402e113e..2108f0004de 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m ]]> - 11.0.0-dev.6 + 11.0.0-dev.7 agpl Daniel Calviño Sánchez diff --git a/appinfo/routes.php b/appinfo/routes.php index a97fc9fb1e6..130359c62c5 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -109,7 +109,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -118,7 +118,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], @@ -127,7 +127,7 @@ 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'DELETE', 'requirements' => [ - 'apiVersion' => 'v1', + 'apiVersion' => 'v(1|2|3)', 'token' => '^[a-z0-9]{4,30}$', ], ], diff --git a/img/phone.png b/img/phone.png new file mode 100644 index 00000000000..c385083f17f Binary files /dev/null and b/img/phone.png differ diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index cc1c69ce5f0..47449606775 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -27,6 +27,7 @@ use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\ModifyParticipantEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCP\Activity\IManager; @@ -129,7 +130,7 @@ public function generateCallActivity(Room $room): bool { } $actorId = $userIds[0] ?? 'guests-only'; - $actorType = $actorId !== 'guests-only' ? 'users' : 'guests'; + $actorType = $actorId !== 'guests-only' ? Attendee::ACTOR_USERS : Attendee::ACTOR_GUESTS; $this->chatManager->addSystemMessage($room, $actorType, $actorId, json_encode([ 'message' => 'call_ended', 'parameters' => [ @@ -205,7 +206,7 @@ public function generateInvitationActivity(Room $room, array $participants): voi } foreach ($participants as $participant) { - if ($participant['actorType'] !== 'users') { + if ($participant['actorType'] !== Attendee::ACTOR_USERS) { // No user => no activity continue; } diff --git a/lib/Chat/AutoComplete/SearchPlugin.php b/lib/Chat/AutoComplete/SearchPlugin.php index 3469339e5c4..770c963634b 100644 --- a/lib/Chat/AutoComplete/SearchPlugin.php +++ b/lib/Chat/AutoComplete/SearchPlugin.php @@ -25,6 +25,7 @@ use OCA\Talk\Files\Util; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; @@ -102,9 +103,9 @@ public function search($search, $limit, $offset, ISearchResult $searchResult) { $participants = $this->participantService->getParticipantsForRoom($this->room); foreach ($participants as $participant) { $attendee = $participant->getAttendee(); - if ($attendee->getActorType() === 'guests') { + if ($attendee->getActorType() === Attendee::ACTOR_GUESTS) { $guestSessionHashes[] = $attendee->getActorId(); - } elseif ($attendee->getActorType() === 'users') { + } elseif ($attendee->getActorType() === Attendee::ACTOR_USERS) { $userIds[] = $attendee->getActorId(); } } diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 3a3d615910c..6b43981db30 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -28,6 +28,7 @@ use OC\Memcache\NullCache; use OCA\Talk\Events\ChatEvent; use OCA\Talk\Events\ChatParticipantEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; @@ -144,7 +145,7 @@ public function addSystemMessage(Room $chat, string $actorType, string $actorId, * @return IComment */ public function addChangelogMessage(Room $chat, string $message): IComment { - $comment = $this->commentsManager->create('guests', 'changelog', 'chat', (string) $chat->getId()); + $comment = $this->commentsManager->create(Attendee::ACTOR_GUESTS, 'changelog', 'chat', (string) $chat->getId()); $comment->setMessage($message, self::MAX_CHAT_LENGTH); $comment->setCreationDateTime($this->timeFactory->getDateTime()); diff --git a/lib/Chat/Command/Executor.php b/lib/Chat/Command/Executor.php index 21a01fce954..0862936c4bb 100644 --- a/lib/Chat/Command/Executor.php +++ b/lib/Chat/Command/Executor.php @@ -25,6 +25,7 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Events\CommandEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Command; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -90,7 +91,7 @@ public function exec(Room $room, IComment $message, Command $command, string $ar try { $command = $this->commandService->resolveAlias($command); } catch (DoesNotExistException $e) { - $user = $message->getActorType() === 'users' ? $message->getActorId() : ''; + $user = $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : ''; $message->setMessage(json_encode([ 'user' => $user, 'visibility' => $command->getResponse(), @@ -109,7 +110,7 @@ public function exec(Room $room, IComment $message, Command $command, string $ar $output = $this->execShell($room, $message, $command, $arguments); } - $user = $message->getActorType() === 'users' ? $message->getActorId() : ''; + $user = $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : ''; $message->setMessage(json_encode([ 'user' => $user, 'visibility' => $command->getResponse(), @@ -205,7 +206,7 @@ public function execShell(Room $room, IComment $message, Command $command, strin $command->getScript(), $arguments, $room->getToken(), - $message->getActorType() === 'users' ? $message->getActorId() : '' + $message->getActorType() === Attendee::ACTOR_USERS ? $message->getActorId() : '' ); } catch (\InvalidArgumentException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 176be751e68..ebea4d20bea 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -27,6 +27,7 @@ use OCA\Talk\Events\ChatMessageEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -79,10 +80,10 @@ protected function setActor(Message $message): void { $comment = $message->getComment(); $displayName = ''; - if ($comment->getActorType() === 'users') { + if ($comment->getActorType() === Attendee::ACTOR_USERS) { $user = $this->userManager->get($comment->getActorId()); $displayName = $user instanceof IUser ? $user->getDisplayName() : $comment->getActorId(); - } elseif ($comment->getActorType() === 'guests') { + } elseif ($comment->getActorType() === Attendee::ACTOR_GUESTS) { if (isset($guestNames[$comment->getActorId()])) { $displayName = $this->guestNames[$comment->getActorId()]; } else { diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index 13fb7d02797..0b5db45eb3e 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -28,6 +28,7 @@ use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Files\Util; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -135,7 +136,7 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea * @return string[] Users that were mentioned */ public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo): array { - if ($replyTo->getActorType() !== 'users') { + if ($replyTo->getActorType() !== Attendee::ACTOR_USERS) { // No reply notification when the replyTo-author was not a user return []; } @@ -308,7 +309,7 @@ protected function getDefaultGroupNotification(): int { * @return bool */ protected function shouldMentionedUserBeNotified(string $userId, IComment $comment): bool { - if ($comment->getActorType() === 'users' && $userId === $comment->getActorId()) { + if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) { // Do not notify the user if they mentioned themselves return false; } @@ -341,7 +342,7 @@ protected function shouldMentionedUserBeNotified(string $userId, IComment $comme // the notification can be parsed and links to an existing room, // where they are a participant of. $this->participantService->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $userId, ]]); return true; @@ -364,12 +365,12 @@ protected function shouldMentionedUserBeNotified(string $userId, IComment $comme * @return bool */ protected function shouldParticipantBeNotified(Participant $participant, IComment $comment, array $alreadyNotifiedUsers): bool { - if ($participant->getAttendee()->getActorType() !== 'users') { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS) { return false; } $userId = $participant->getAttendee()->getActorId(); - if ($comment->getActorType() === 'users' && $userId === $comment->getActorId()) { + if ($comment->getActorType() === Attendee::ACTOR_USERS && $userId === $comment->getActorId()) { // Do not notify the author return false; } diff --git a/lib/Chat/Parser/Changelog.php b/lib/Chat/Parser/Changelog.php index 2f6621848d2..d1c8ce12927 100644 --- a/lib/Chat/Parser/Changelog.php +++ b/lib/Chat/Parser/Changelog.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; class Changelog { @@ -32,7 +33,7 @@ class Changelog { * @throws \OutOfBoundsException */ public function parseMessage(Message $chatMessage): void { - if ($chatMessage->getActorType() !== 'guests' || + if ($chatMessage->getActorType() !== Attendee::ACTOR_GUESTS || $chatMessage->getActorId() !== 'changelog') { throw new \OutOfBoundsException('Not a changelog'); } diff --git a/lib/Chat/Parser/Command.php b/lib/Chat/Parser/Command.php index 1e321d8d8d4..6a69f2c8223 100644 --- a/lib/Chat/Parser/Command.php +++ b/lib/Chat/Parser/Command.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; class Command { @@ -44,7 +45,7 @@ public function parseMessage(Message $message): void { $participant = $message->getParticipant(); if ($data['visibility'] !== \OCA\Talk\Model\Command::RESPONSE_ALL && - ($participant->getAttendee()->getActorType() !== 'users' + ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS || $data['user'] !== $participant->getAttendee()->getActorId())) { $message->setVisibility(false); return; diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index b593c3f2b87..cde11bcc5b0 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -25,6 +25,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Participant; use OCA\Talk\Share\RoomShareProvider; @@ -94,7 +95,7 @@ public function parseMessage(Message $chatMessage): void { if (!$participant->isGuest()) { $currentActorId = $participant->getAttendee()->getActorId(); $currentUserIsActor = $parsedParameters['actor']['type'] === 'user' && - $participant->getAttendee()->getActorType() === 'users' && + $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS && $currentActorId === $parsedParameters['actor']['id']; } else { $currentActorId = $participant->getAttendee()->getActorId(); @@ -385,7 +386,7 @@ protected function getFileFromShare(Participant $participant, string $shareId): } protected function getActor(IComment $comment): array { - if ($comment->getActorType() === 'guests') { + if ($comment->getActorType() === Attendee::ACTOR_GUESTS) { return $this->getGuest($comment->getActorId()); } diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index 1d0f33099f4..1095f3ec4b9 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -26,6 +26,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Room; use OCP\Comments\ICommentsManager; @@ -116,7 +117,7 @@ public function parseMessage(Message $chatMessage): void { if ($mention['type'] === 'call') { $userId = ''; - if ($chatMessage->getParticipant()->getAttendee()->getActorType() === 'users') { + if ($chatMessage->getParticipant()->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { $userId = $chatMessage->getParticipant()->getAttendee()->getActorId(); } diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 88c5a644dba..e45ab3a5a55 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -31,6 +31,7 @@ use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -80,16 +81,16 @@ public static function register(IEventDispatcher $dispatcher): void { $participantService = \OC::$server->query(ParticipantService::class); if ($participantService->hasActiveSessionsInCall($room)) { - $listener->sendSystemMessage($room, 'call_joined'); + $listener->sendSystemMessage($room, 'call_joined', [], $event->getParticipant()); } else { - $listener->sendSystemMessage($room, 'call_started'); + $listener->sendSystemMessage($room, 'call_started', [], $event->getParticipant()); } }); $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, static function (ModifyParticipantEvent $event) { $room = $event->getRoom(); $session = $event->getParticipant()->getSession(); - if (!$session instanceof Session || $session->getInCall() === Participant::FLAG_DISCONNECTED) { + if (!$session instanceof Session) { // This happens in case the user was kicked/lobbied return; } @@ -221,7 +222,7 @@ public static function register(IEventDispatcher $dispatcher): void { $room = $event->getRoom(); $attendee = $event->getParticipant()->getAttendee(); - if ($attendee->getActorType() !== 'users' && $attendee->getActorType() !== 'guests') { + if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { return; } @@ -277,10 +278,10 @@ protected function sendSystemMessage(Room $room, string $message, array $paramet $actorType = 'users'; $actorId = $user->getUID(); } elseif (\OC::$CLI) { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $actorId = 'cli'; } else { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $sessionId = $this->talkSession->getSessionForRoom($room->getToken()); $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session'; } diff --git a/lib/Command/Room/TRoomCommand.php b/lib/Command/Room/TRoomCommand.php index 2c59205e26e..b69c4e5e321 100644 --- a/lib/Command/Room/TRoomCommand.php +++ b/lib/Command/Room/TRoomCommand.php @@ -29,6 +29,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; @@ -249,7 +250,7 @@ protected function addRoomParticipants(Room $room, array $userIds): void { } $participants[] = [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $user->getUID(), ]; } @@ -377,7 +378,7 @@ protected function completeParticipantValues(CompletionContext $context): array $users = []; $participants = $this->participantService->getParticipantsForRoom($room); foreach ($participants as $participant) { - if ($participant->getAttendee()->getActorType() === 'users' + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS && stripos($participant->getAttendee()->getActorId(), $context->getCurrentWord()) !== false) { $users[] = $participant->getAttendee()->getActorId(); } diff --git a/lib/Config.php b/lib/Config.php index 6347700bb3f..16255dbfb17 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -43,6 +43,9 @@ class Config { /** @var ISecureRandom */ private $secureRandom; + /** @var array */ + protected $canEnableSIP = []; + public function __construct(IConfig $config, ISecureRandom $secureRandom, IGroupManager $groupManager, @@ -84,6 +87,24 @@ public function getSIPSharedSecret(): string { return $this->config->getAppValue('spreed', 'sip_bridge_shared_secret'); } + public function canUserEnableSIP(IUser $user): bool { + if (isset($this->canEnableSIP[$user->getUID()])) { + return $this->canEnableSIP[$user->getUID()]; + } + + $this->canEnableSIP[$user->getUID()] = false; + + $allowedGroups = $this->getSIPGroups(); + if (empty($allowedGroups)) { + $this->canEnableSIP[$user->getUID()] = true; + } else { + $userGroups = $this->groupManager->getUserGroupIds($user); + $this->canEnableSIP[$user->getUID()] = !empty(array_intersect($allowedGroups, $userGroups)); + } + + return $this->canEnableSIP[$user->getUID()]; + } + public function isDisabledForUser(IUser $user): bool { $allowedGroups = $this->getAllowedTalkGroupIds(); if (empty($allowedGroups)) { diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 6e1c66a083d..85c85e834f6 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -27,6 +27,7 @@ namespace OCA\Talk\Controller; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Service\ParticipantService; @@ -71,17 +72,29 @@ public function getPeersForCall(): DataResponse { continue; } - $userId = ''; - if ($participant->getAttendee()->getActorType() === 'users') { - $userId = $participant->getAttendee()->getActorId(); + if ($this->getAPIVersion() >= 3) { + $result[] = [ + 'actorType' => $participant->getAttendee()->getActorType(), + 'actorId' => $participant->getAttendee()->getActorId(), + // FIXME 'displayName' => $participant->getAttendee()->getDisplayName(), + 'displayName' => $participant->getAttendee()->getActorId(), + 'token' => $this->room->getToken(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]; + } else { + $userId = ''; + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $userId = $participant->getAttendee()->getActorId(); + } + + $result[] = [ + 'userId' => $userId, + 'token' => $this->room->getToken(), + 'lastPing' => $session->getLastPing(), + 'sessionId' => $session->getSessionId(), + ]; } - - $result[] = [ - 'userId' => $userId, - 'token' => $this->room->getToken(), - 'lastPing' => $session->getLastPing(), - 'sessionId' => $session->getSessionId(), - ]; } return new DataResponse($result); diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 41da215e198..6e97a747bf0 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -29,6 +29,7 @@ use OCA\Talk\Chat\ChatManager; use OCA\Talk\Chat\MessageParser; use OCA\Talk\GuestManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; use OCA\Talk\Model\Session; use OCA\Talk\Participant; @@ -165,7 +166,7 @@ public function __construct(string $appName, */ public function sendMessage(string $message, string $actorDisplayName = '', string $referenceId = '', int $replyTo = 0): DataResponse { if ($this->userId === null) { - $actorType = 'guests'; + $actorType = Attendee::ACTOR_GUESTS; $sessionId = $this->session->getSessionForRoom($this->room->getToken()); // The character limit for actorId is 64, but the spreed-session is // 256 characters long, so it has to be hashed to get an ID that @@ -178,7 +179,7 @@ public function sendMessage(string $message, string $actorDisplayName = '', stri $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName); } } else { - $actorType = 'users'; + $actorType = Attendee::ACTOR_USERS; $actorId = $this->userId; } @@ -293,7 +294,7 @@ public function receiveMessages(int $lookIntoFuture, int $limit = 100, int $last if ($lookIntoFuture) { $attendee = $this->participant->getAttendee(); - if ($attendee->getActorType() === 'users') { + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { // Bump the user status again $event = new UserLiveStatusEvent( $this->userManager->get($attendee->getActorId()), @@ -485,7 +486,7 @@ public function mentions(string $search, int $limit = 20, bool $includeStatus = $results = $this->prepareResultArray($results, $statuses); $attendee = $this->participant->getAttendee(); - $userId = $attendee->getActorType() === 'users' ? $attendee->getActorId() : ''; + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; $roomDisplayName = $this->room->getDisplayName($userId); if (($search === '' || strpos('all', $search) !== false || stripos($roomDisplayName, $search) !== false) && $this->room->getType() !== Room::ONE_TO_ONE_CALL) { if ($search === '' || @@ -524,7 +525,7 @@ protected function prepareResultArray(array $results, array $statuses): array { 'source' => $type, ]; - if ($type === 'users' && isset($statuses[$data['id']])) { + if ($type === Attendee::ACTOR_USERS && isset($statuses[$data['id']])) { $data['status'] = $statuses[$data['id']]->getStatus(); $data['statusIcon'] = $statuses[$data['id']]->getIcon(); $data['statusMessage'] = $statuses[$data['id']]->getMessage(); diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 6a9ef18e217..7afc446ec34 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -40,6 +40,7 @@ use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\GuestManager; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -158,6 +159,9 @@ protected function getTalkHashHeader(): array { $this->config->getAppValue('spreed', 'allowed_groups', '') . '#' . $this->config->getAppValue('spreed', 'start_conversations', '') . '#' . $this->config->getAppValue('spreed', 'has_reference_id', '') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_groups', '[]') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_dialin_info') . '#' . + $this->config->getAppValue('spreed', 'sip_bridge_shared_secret') . '#' . $this->config->getAppValue('theming', 'cachebuster', '1') )]; } @@ -191,6 +195,7 @@ public function getRooms(int $noStatusUpdate = 0): DataResponse { } } + $rooms = $this->manager->getRoomsForUser($this->userId, true); $return = []; @@ -218,9 +223,11 @@ public function getSingleRoom(string $token): DataResponse { return new DataResponse([], Http::STATUS_UNAUTHORIZED); } - if ($isSIPBridgeRequest && $this->getAPIVersion() === 1) { + if ($isSIPBridgeRequest && $this->getAPIVersion() < 3) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } + + // The SIP bridge only needs room details (public, sip enabled, lobby state, etc) $includeLastMessage = !$isSIPBridgeRequest; try { @@ -252,21 +259,21 @@ public function getSingleRoom(string $token): DataResponse { * configuration. * * @param string $data - * @return bool - * @throws UnauthorizedException when the request tried to sign as SIP bridge but failed + * @return bool True if the request is from the SIP bridge and valid, false if not from SIP bridge + * @throws UnauthorizedException when the request tried to sign as SIP bridge but is not valid */ private function validateSIPBridgeRequest(string $data): bool { - if (!isset($_SERVER['HTTP_TALK_SIPBRIDGE_RANDOM']) - && !isset($_SERVER['HTTP_TALK_SIPBRIDGE_CHECKSUM'])) { + $random = $this->request->getHeader('TALK_SIPBRIDGE_RANDOM'); + $checksum = $this->request->getHeader('TALK_SIPBRIDGE_CHECKSUM'); + + if ($random === '' && $checksum === '') { return false; } - $random = $_SERVER['HTTP_TALK_SIPBRIDGE_RANDOM'] ?? ''; if (strlen($random) < 32) { throw new UnauthorizedException('Invalid random provided'); } - $checksum = $_SERVER['HTTP_TALK_SIPBRIDGE_CHECKSUM'] ?? ''; if (empty($checksum)) { throw new UnauthorizedException('Invalid checksum provided'); } @@ -292,7 +299,7 @@ private function validateSIPBridgeRequest(string $data): bool { * @throws RoomNotFoundException */ protected function formatRoom(Room $room, ?Participant $currentParticipant, bool $isSIPBridgeRequest = false): array { - if ($this->getAPIVersion() !== 1) { + if ($this->getAPIVersion() >= 2) { return $this->formatRoomV2andV3($room, $currentParticipant, $isSIPBridgeRequest); } @@ -344,7 +351,7 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a } $attendee = $currentParticipant->getAttendee(); - $userId = $attendee->getActorType() === 'users' ? $attendee->getActorId() : ''; + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; $lastActivity = $room->getLastActivity(); if ($lastActivity instanceof \DateTimeInterface) { @@ -451,7 +458,7 @@ protected function formatRoomV1(Room $room, ?Participant $currentParticipant): a $numActiveGuests++; } } - } elseif ($participant->getAttendee()->getActorType() === 'users') { + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { $attendee = $participant->getAttendee(); $session = $participant->getSession(); $user = $this->userManager->get($attendee->getActorId()); @@ -528,17 +535,19 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan 'notificationLevel' => Participant::NOTIFY_NEVER, 'lobbyState' => Webinary::LOBBY_NONE, 'lobbyTimer' => 0, - 'sipEnabled' => Webinary::SIP_DISABLED, 'lastPing' => 0, 'sessionId' => '0', 'guestList' => '', 'lastMessage' => [], ]; - if ($this->getAPIVersion() === 3) { + if ($this->getAPIVersion() >= 3) { $roomData = array_merge($roomData, [ + 'sipEnabled' => Webinary::SIP_DISABLED, 'actorType' => '', 'actorId' => '', 'attendeeId' => 0, + 'canEnableSIP' => false, + 'attendeePin' => '', ]); } @@ -576,7 +585,7 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan } $attendee = $currentParticipant->getAttendee(); - $userId = $attendee->getActorType() === 'users' ? $attendee->getActorId() : ''; + $userId = $attendee->getActorType() === Attendee::ACTOR_USERS ? $attendee->getActorId() : ''; $roomData = array_merge($roomData, [ 'name' => $room->getName(), @@ -591,9 +600,13 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan 'notificationLevel' => $attendee->getNotificationLevel(), 'lobbyState' => $room->getLobbyState(), 'lobbyTimer' => $lobbyTimer, - 'sipEnabled' => $room->getSIPEnabled(), ]); - if ($this->getAPIVersion() === 3) { + if ($this->getAPIVersion() >= 3) { + if ($this->talkConfig->isSIPConfigured()) { + $roomData['sipEnabled'] = $room->getSIPEnabled(); + $roomData['attendeePin'] = $attendee->getPin(); + } + $roomData = array_merge($roomData, [ 'actorType' => $attendee->getActorType(), 'actorId' => $attendee->getActorId(), @@ -633,7 +646,7 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan $roomData['canStartCall'] = $currentParticipant->canStartCall($this->config); - if ($attendee->getActorType() === 'users') { + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { $currentUser = $this->userManager->get($attendee->getActorId()); if ($currentUser instanceof IUser) { $lastReadMessage = $attendee->getLastReadMessage(); @@ -661,6 +674,14 @@ protected function formatRoomV2andV3(Room $room, ?Participant $currentParticipan $roomData['canDeleteConversation'] = $room->getType() !== Room::ONE_TO_ONE_CALL && $currentParticipant->hasModeratorPermissions(false); $roomData['canLeaveConversation'] = true; + if ($this->getAPIVersion() >= 3) { + $roomData['canEnableSIP'] = + $this->talkConfig->isSIPConfigured() + && !preg_match(Room::SIP_INCOMPATIBLE_REGEX, $room->getToken()) + && ($room->getType() === Room::GROUP_CALL || $room->getType() === Room::PUBLIC_CALL) + && $currentParticipant->hasModeratorPermissions(false) + && $this->talkConfig->canUserEnableSIP($currentUser); + } } } @@ -817,7 +838,7 @@ protected function createGroupRoom(string $targetGroupName): DataResponse { } $participants[] = [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $user->getUID(), ]; } @@ -875,7 +896,7 @@ protected function createCircleRoom(string $targetCircleId): DataResponse { } $participants[] = [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $member->getUserId(), ]; } @@ -1009,7 +1030,7 @@ public function getParticipants(bool $includeStatus = false): DataResponse { && count($participants) < 100 && $this->appManager->isEnabledForUser('user_status')) { $userIds = array_filter(array_map(static function (Participant $participant) { - if ($participant->getAttendee()->getActorType() === 'users') { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { return $participant->getAttendee()->getActorId(); } return null; @@ -1022,7 +1043,7 @@ public function getParticipants(bool $includeStatus = false): DataResponse { $guestSessions = array_filter(array_map(static function (Participant $participant) { $session = $participant->getSession(); - if (!$session || $participant->getAttendee()->getActorType() !== 'guests') { + if (!$session || $participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { return null; } @@ -1040,10 +1061,15 @@ public function getParticipants(bool $includeStatus = false): DataResponse { 'sessionId' => '0', // FIXME empty string or null? 'participantType' => $participant->getAttendee()->getParticipantType(), ]; - if ($this->getAPIVersion() === 3) { + if ($this->getAPIVersion() >= 3) { $result['attendeeId'] = $participant->getAttendee()->getId(); $result['actorId'] = $participant->getAttendee()->getActorId(); $result['actorType'] = $participant->getAttendee()->getActorType(); + if ($this->talkConfig->isSIPConfigured() + && ($this->participant->hasModeratorPermissions(false) + || $this->participant->getAttendee()->getId() === $participant->getAttendee()->getId())) { + $result['attendeePin'] = (string) $participant->getAttendee()->getPin(); + } } if ($participant->getSession() instanceof Session) { $result['inCall'] = $participant->getSession()->getInCall(); @@ -1051,7 +1077,7 @@ public function getParticipants(bool $includeStatus = false): DataResponse { $result['sessionId'] = $participant->getSession()->getSessionId(); } - if ($participant->getAttendee()->getActorType() === 'users') { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { $userId = $participant->getAttendee()->getActorId(); $user = $this->userManager->get($userId); if (!$user instanceof IUser) { @@ -1062,7 +1088,7 @@ public function getParticipants(bool $includeStatus = false): DataResponse { $this->participantService->leaveRoomAsSession($this->room, $participant); } - if ($this->getAPIVersion() !== 3) { + if ($this->getAPIVersion() < 3) { $result['userId'] = $participant->getAttendee()->getActorId(); } $result['displayName'] = (string) $user->getDisplayName(); @@ -1073,18 +1099,18 @@ public function getParticipants(bool $includeStatus = false): DataResponse { $result['statusMessage'] = $statuses[$userId]->getMessage(); $result['statusClearAt'] = $statuses[$userId]->getClearAt(); } - } elseif ($participant->getAttendee()->getActorType() === 'guests') { + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) { if ($result['lastPing'] <= $maxPingAge) { $cleanGuests = true; continue; } - if ($this->getAPIVersion() !== 3) { + if ($this->getAPIVersion() < 3) { $result['userId'] = ''; } $result['displayName'] = $guestNames[$participant->getAttendee()->getActorId()] ?? ''; - } elseif ($this->getAPIVersion() === 3) { - // Other types are only reported on V3 + } elseif ($this->getAPIVersion() >= 3) { + // Other types are only reported on v3 or later $result['displayName'] = $participant->getAttendee()->getActorId(); } else { // Skip unknown actor types @@ -1128,7 +1154,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $this->participantService->addUsers($this->room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $newUser->getUID(), ]]); } elseif ($source === 'groups') { @@ -1144,7 +1170,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $participantsToAdd[] = [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $user->getUID(), ]; } @@ -1183,7 +1209,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $participantsToAdd[] = [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $member->getUserId(), ]; } @@ -1220,7 +1246,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u */ public function removeParticipantFromRoom(string $participant): DataResponse { $attendee = $this->participant->getAttendee(); - if ($attendee->getActorType() === 'users' && $attendee->getActorId() === $participant) { + if ($attendee->getActorType() === Attendee::ACTOR_USERS && $attendee->getActorId() === $participant) { // Removing self, abusing moderator power return $this->removeSelfFromRoomLogic($this->room, $this->participant); } @@ -1465,6 +1491,7 @@ public function joinRoom(string $token, string $password = '', bool $force = tru $result = $room->verifyPassword((string) $this->session->getPasswordForRoom($token)); if ($user instanceof IUser) { $participant = $this->participantService->joinRoom($room, $user, $password, $result['result']); + $this->participantService->generatePinForParticipant($room, $participant); } else { $participant = $this->participantService->joinRoomAsNewGuest($room, $password, $result['result']); } @@ -1541,7 +1568,7 @@ public function leaveRoom(string $token): DataResponse { * @return DataResponse */ public function promoteModerator(?int $attendeeId, ?string $participant, ?string $sessionId): DataResponse { - return $this->toggleParticipantType($attendeeId, $participant, $sessionId); + return $this->changeParticipantType($attendeeId, $participant, $sessionId, true); } /** @@ -1554,7 +1581,7 @@ public function promoteModerator(?int $attendeeId, ?string $participant, ?string * @return DataResponse */ public function demoteModerator(?int $attendeeId, ?string $participant, ?string $sessionId): DataResponse { - return $this->toggleParticipantType($attendeeId, $participant, $sessionId); + return $this->changeParticipantType($attendeeId, $participant, $sessionId, false); } /** @@ -1564,9 +1591,10 @@ public function demoteModerator(?int $attendeeId, ?string $participant, ?string * @param int|null $attendeeId * @param string|null $userId * @param string|null $sessionId + * @param bool $promote Shall the attendee be promoted or demoted * @return DataResponse */ - protected function toggleParticipantType(?int $attendeeId, ?string $userId, ?string $sessionId): DataResponse { + protected function changeParticipantType(?int $attendeeId, ?string $userId, ?string $sessionId, bool $promote): DataResponse { try { if ($attendeeId !== null) { $targetParticipant = $this->room->getParticipantByAttendeeId($attendeeId); @@ -1582,11 +1610,11 @@ protected function toggleParticipantType(?int $attendeeId, ?string $userId, ?str $attendee = $targetParticipant->getAttendee(); // Prevent users/moderators modifying themselves - if ($attendee->getActorType() === 'users') { + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { if ($attendee->getActorId() === $this->userId) { return new DataResponse([], Http::STATUS_FORBIDDEN); } - } elseif ($attendee->getActorType() === 'guests') { + } elseif ($attendee->getActorType() === Attendee::ACTOR_GUESTS) { $session = $targetParticipant->getSession(); $currentSessionId = $this->session->getSessionForRoom($this->room->getToken()); @@ -1595,6 +1623,11 @@ protected function toggleParticipantType(?int $attendeeId, ?string $userId, ?str } } + if ($promote === $targetParticipant->hasModeratorPermissions()) { + // Prevent concurrent changes + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + if ($attendee->getParticipantType() === Participant::USER) { $newType = Participant::MODERATOR; } elseif ($attendee->getParticipantType() === Participant::GUEST) { diff --git a/lib/Controller/SignalingController.php b/lib/Controller/SignalingController.php index 1acb977133e..9902e27deec 100644 --- a/lib/Controller/SignalingController.php +++ b/lib/Controller/SignalingController.php @@ -25,11 +25,13 @@ namespace OCA\Talk\Controller; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use OCA\Talk\Config; use OCA\Talk\Events\SignalingEvent; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -377,7 +379,7 @@ protected function getUsersInRoom(Room $room, int $pingTimestamp): array { } $userId = ''; - if ($participant->getAttendee()->getActorType() === 'users') { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { $userId = $participant->getAttendee()->getActorId(); } @@ -519,36 +521,64 @@ private function backendAuth(array $auth): DataResponse { } private function backendRoom(array $roomRequest): DataResponse { - $roomId = $roomRequest['roomid']; + $token = $roomRequest['roomid']; // It's actually the room token $userId = $roomRequest['userid']; $sessionId = $roomRequest['sessionid']; $action = !empty($roomRequest['action']) ? $roomRequest['action'] : 'join'; - - try { - $room = $this->manager->getRoomByToken($roomId, $userId); - } catch (RoomNotFoundException $e) { - return new DataResponse([ - 'type' => 'error', - 'error' => [ - 'code' => 'no_such_room', - 'message' => 'The user is not invited to this room.', - ], - ]); - } + $actorId = $roomRequest['actorid'] ?? null; + $actorType = $roomRequest['actortype'] ?? null; + $inCall = $roomRequest['incall'] ?? null; $participant = null; - if ($sessionId) { + if ($actorId !== null && $actorType !== null) { try { - $participant = $room->getParticipantBySession($sessionId); - } catch (ParticipantNotFoundException $e) { + $room = $this->manager->getRoomByActor($token, $actorType, $actorId); + } catch (RoomNotFoundException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); } - } - if (!empty($userId)) { - // User trying to join room. + if ($sessionId) { + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + } + } else { + try { + $participant = $room->getParticipantByActor($actorType, $actorId); + } catch (ParticipantNotFoundException $e) { + } + } + } else { try { - $participant = $room->getParticipant($userId); - } catch (ParticipantNotFoundException $e) { + // FIXME Don't preload with the user as that misses the session, kinda meh. + $room = $this->manager->getRoomByToken($token); + } catch (RoomNotFoundException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'no_such_room', + 'message' => 'The user is not invited to this room.', + ], + ]); + } + + if ($sessionId) { + try { + $participant = $room->getParticipantBySession($sessionId); + } catch (ParticipantNotFoundException $e) { + } + } elseif (!empty($userId)) { + // User trying to join room. + try { + $participant = $room->getParticipant($userId); + } catch (ParticipantNotFoundException $e) { + } } } @@ -564,16 +594,36 @@ private function backendRoom(array $roomRequest): DataResponse { } if ($action === 'join') { + if ($sessionId && !$participant->getSession() instanceof Session) { + try { + $session = $this->sessionService->createSessionForAttendee($participant->getAttendee(), $sessionId); + } catch (UniqueConstraintViolationException $e) { + return new DataResponse([ + 'type' => 'error', + 'error' => [ + 'code' => 'duplicate_session', + 'message' => 'The given session is already in use.', + ], + ]); + } + $participant->setSession($session); + } + if ($participant->getSession() instanceof Session) { + if ($inCall !== null) { + $this->participantService->changeInCall($room, $participant, $inCall); + } $this->sessionService->updateLastPing($participant->getSession(), $this->timeFactory->getTime()); } } elseif ($action === 'leave') { - if ($participant instanceof Participant) { - if (!empty($userId)) { - $this->participantService->leaveRoomAsSession($room, $participant); - } else { - $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_LEFT); - } + // Guests are removed completely as they don't reuse attendees, + // but this is only true for guests that joined directly. + // Emails are retained as their PIN needs to remain and stay + // valid. + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) { + $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_LEFT); + } else { + $this->participantService->leaveRoomAsSession($room, $participant); } } diff --git a/lib/Controller/WebinarController.php b/lib/Controller/WebinarController.php index c6fa35ec5c2..b04b4d48761 100644 --- a/lib/Controller/WebinarController.php +++ b/lib/Controller/WebinarController.php @@ -33,6 +33,8 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; class WebinarController extends AEnvironmentAwareController { @@ -42,16 +44,24 @@ class WebinarController extends AEnvironmentAwareController { protected $participantService; /** @var Config */ protected $talkConfig; + /** @var IUserManager */ + protected $userManager; + /** @var string|null */ + protected $userId; public function __construct(string $appName, IRequest $request, ITimeFactory $timeFactory, ParticipantService $participantService, - Config $talkConfig) { + Config $talkConfig, + IUserManager $userManager, + ?string $userId) { parent::__construct($appName, $request); $this->timeFactory = $timeFactory; $this->participantService = $participantService; $this->talkConfig = $talkConfig; + $this->userManager = $userManager; + $this->userId = $userId; } /** @@ -99,11 +109,19 @@ public function setLobby(int $state, ?int $timer = null): DataResponse { * @return DataResponse */ public function setSIPEnabled(int $state): DataResponse { + $user = $this->userManager->get($this->userId); + if (!$user instanceof IUser) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + + if (!$this->talkConfig->canUserEnableSIP($user)) { + return new DataResponse([], Http::STATUS_UNAUTHORIZED); + } + if (!$this->talkConfig->isSIPConfigured()) { return new DataResponse([], Http::STATUS_PRECONDITION_FAILED); } - // TODO Check if user is in "SIP groups" if (!$this->room->setSIPEnabled($state)) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Files/Listener.php b/lib/Files/Listener.php index 64b2a82f233..f6f2f0e9454 100644 --- a/lib/Files/Listener.php +++ b/lib/Files/Listener.php @@ -28,6 +28,7 @@ use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; @@ -154,7 +155,7 @@ public function addUserAsPersistentParticipant(Room $room, string $userId): void $room->getParticipant($userId); } catch (ParticipantNotFoundException $e) { $this->participantService->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $userId, ]]); } diff --git a/lib/GuestManager.php b/lib/GuestManager.php index 41bef5ff4eb..067ffac2730 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -45,6 +45,9 @@ class GuestManager { /** @var IDBConnection */ protected $connection; + /** @var Config */ + protected $talkConfig; + /** @var IMailer */ protected $mailer; @@ -64,6 +67,7 @@ class GuestManager { protected $dispatcher; public function __construct(IDBConnection $connection, + Config $talkConfig, IMailer $mailer, Defaults $defaults, IUserSession $userSession, @@ -71,6 +75,7 @@ public function __construct(IDBConnection $connection, IL10N $l, IEventDispatcher $dispatcher) { $this->connection = $connection; + $this->talkConfig = $talkConfig; $this->mailer = $mailer; $this->defaults = $defaults; $this->userSession = $userSession; @@ -205,16 +210,33 @@ public function sendEmailInvitation(Room $room, Participant $participant): void $subject ); -// if ($pin) { -// // FIXME wrap in text -// $template->addBodyText($pin); -// } - $template->addBodyButton( $this->l->t('Join »%s«', [$room->getDisplayName('')]), $link ); + if ($pin) { + $template->addBodyText($this->l->t('You can also dial-in via phone with the following details')); + + $template->addBodyListItem( + $this->talkConfig->getDialInInfo(), + $this->l->t('Dial-in information'), + $this->url->getAbsoluteURL($this->url->imagePath('spreed', 'phone.png')) + ); + + $template->addBodyListItem( + $room->getToken(), + $this->l->t('Meeting ID'), + $this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar-dark.png')) + ); + + $template->addBodyListItem( + $pin, + $this->l->t('Your PIN'), + $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/password.png')) + ); + } + $template->addFooter(); $message->setTo([$email]); diff --git a/lib/Manager.php b/lib/Manager.php index 6e02ae666a0..84f6faad0b2 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -27,6 +27,7 @@ use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\SessionMapper; use OCA\Talk\Service\ParticipantService; @@ -110,6 +111,7 @@ public function __construct(IDBConnection $db, public function forAllRooms(callable $callback): void { $query = $this->db->getQueryBuilder(); $query->select('*') + ->selectAlias('id', 'r_id') ->from('talk_rooms'); $result = $query->execute(); @@ -289,14 +291,19 @@ public function getRoomsForUser(string $userId, bool $includeLastMessage = false $query = $this->db->getQueryBuilder(); $query->select('r.*') ->addSelect('a.*') + ->addSelect('s.*') ->selectAlias('r.id', 'r_id') ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') ->from('talk_rooms', 'r') ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), - $query->expr()->eq('a.actor_type', $query->createNamedParameter('users')), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), $query->expr()->eq('a.room_id', 'r.id') )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') + )) ->where($query->expr()->isNotNull('a.id')); if ($includeLastMessage) { @@ -340,12 +347,17 @@ public function getRoomForUser(int $roomId, ?string $userId): Room { if ($userId !== null) { // Non guest user $query->addSelect('a.*') + ->addSelect('s.*') ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), - $query->expr()->eq('a.actor_type', $query->createNamedParameter('users')), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), $query->expr()->eq('a.room_id', 'r.id') )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') + )) ->andWhere($query->expr()->isNotNull('a.id')); } @@ -395,11 +407,16 @@ public function getRoomForUserByToken(string $token, ?string $userId, bool $incl if ($userId !== null) { // Non guest user $query->addSelect('a.*') - ->selectAlias('a.id', 'a_id'); - $query->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + ->addSelect('s.*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), - $query->expr()->eq('a.actor_type', $query->createNamedParameter('users')), + $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') )); } @@ -466,29 +483,30 @@ public function getRoomById(int $roomId): Room { /** * @param string $token - * @param string|null $preloadUserId Load this participants information if possible + * @param string $actorType + * @param string $actorId * @return Room * @throws RoomNotFoundException */ - public function getRoomByToken(string $token, ?string $preloadUserId = null): Room { - $preloadUserId = $preloadUserId === '' ? null : $preloadUserId; - + public function getRoomByActor(string $token, string $actorType, string $actorId): Room { $query = $this->db->getQueryBuilder(); $query->select('r.*') + ->addSelect('a.*') + ->addSelect('s.*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') ->selectAlias('r.id', 'r_id') ->from('talk_rooms', 'r') + ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( + $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), + $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), + $query->expr()->eq('a.room_id', 'r.id') + )) + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( + $query->expr()->eq('a.id', 's.attendee_id') + )) ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); - if ($preloadUserId !== null) { - $query->addSelect('a.*') - ->selectAlias('a.id', 'a_id'); - $query->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( - $query->expr()->eq('a.actor_id', $query->createNamedParameter($preloadUserId)), - $query->expr()->eq('a.actor_type', $query->createNamedParameter('users')), - $query->expr()->eq('a.room_id', 'r.id') - )); - } - $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); @@ -503,13 +521,47 @@ public function getRoomByToken(string $token, ?string $preloadUserId = null): Ro } $room = $this->createRoomObject($row); - if ($preloadUserId !== null && isset($row['actor_id'])) { + if ($actorType === Attendee::ACTOR_USERS && isset($row['actor_id'])) { $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } return $room; } + /** + * @param string $token + * @param string|null $preloadUserId Load this participants information if possible + * @return Room + * @throws RoomNotFoundException + */ + public function getRoomByToken(string $token, ?string $preloadUserId = null): Room { + $preloadUserId = $preloadUserId === '' ? null : $preloadUserId; + if ($preloadUserId !== null) { + return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId); + } + + $query = $this->db->getQueryBuilder(); + $query->select('r.*') + ->selectAlias('r.id', 'r_id') + ->from('talk_rooms', 'r') + ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); + + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new RoomNotFoundException(); + } + + if ($row['token'] === null) { + // FIXME Temporary solution for the Talk6 release + throw new RoomNotFoundException(); + } + + return $this->createRoomObject($row); + } + /** * @param string $objectType * @param string $objectId @@ -571,11 +623,11 @@ public function getRoomForSession(?string $userId, ?string $sessionId): Room { } if ($userId !== null) { - if ($row['actor_type'] !== 'users' || $userId !== $row['actor_id']) { + if ($row['actor_type'] !== Attendee::ACTOR_USERS || $userId !== $row['actor_id']) { throw new RoomNotFoundException(); } } else { - if ($row['actor_type'] !== 'guests') { + if ($row['actor_type'] !== Attendee::ACTOR_GUESTS) { throw new RoomNotFoundException(); } } @@ -653,7 +705,7 @@ public function getChangelogRoom(string $userId): Room { $room->setReadOnly(Room::READ_ONLY); $this->participantService->addUsers($room,[[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $userId, ]]); return $room; @@ -665,7 +717,7 @@ public function getChangelogRoom(string $userId): Room { $room->getParticipant($userId); } catch (ParticipantNotFoundException $e) { $this->participantService->addUsers($room,[[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $userId, ]]); } diff --git a/lib/MatterbridgeManager.php b/lib/MatterbridgeManager.php index 00511a16961..c279a4c7ff5 100644 --- a/lib/MatterbridgeManager.php +++ b/lib/MatterbridgeManager.php @@ -24,6 +24,7 @@ namespace OCA\Talk; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Model\Attendee; use OCA\Talk\Service\ParticipantService; use OCP\IConfig; use OCP\IDBConnection; @@ -303,7 +304,7 @@ private function checkBotUser(Room $room, bool $create): array { $participant = $room->getParticipant($botUserId); } catch (ParticipantNotFoundException $e) { $this->participantService->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $botUserId, 'participantType' => Participant::USER, ]]); @@ -654,7 +655,7 @@ private function compareBridgeParts(array $part1, array $part2): bool { private function sendSystemMessage(Room $room, string $userId, string $message): void { $this->chatManager->addSystemMessage( $room, - 'users', + Attendee::ACTOR_USERS, $userId, json_encode(['message' => $message, 'parameters' => []]), $this->timeFactory->getDateTime(), diff --git a/lib/Migration/Version10000Date20201015134000.php b/lib/Migration/Version10000Date20201015134000.php index b050619ff09..aa7e5bb5e00 100644 --- a/lib/Migration/Version10000Date20201015134000.php +++ b/lib/Migration/Version10000Date20201015134000.php @@ -27,6 +27,7 @@ use Closure; use Doctrine\DBAL\Types\Type; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\ISchemaWrapper; @@ -149,7 +150,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt // Unique key to avoid duplication issues $table->addColumn('session_id', Type::STRING, [ 'notnull' => true, - 'length' => 255, + 'length' => 512, ]); $table->addColumn('in_call', Type::INTEGER, [ @@ -209,7 +210,7 @@ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $insert ->setParameter('room_id', (int) $row['room_id'], IQueryBuilder::PARAM_INT) - ->setParameter('actor_type', 'users') + ->setParameter('actor_type', Attendee::ACTOR_USERS) ->setParameter('actor_id', $row['user_id']) ->setParameter('participant_type', (int) $row['participant_type'], IQueryBuilder::PARAM_INT) ->setParameter('favorite', (bool) $row['favorite'], IQueryBuilder::PARAM_BOOL) diff --git a/lib/Migration/Version10000Date20201015150000.php b/lib/Migration/Version10000Date20201015150000.php new file mode 100644 index 00000000000..1180257d519 --- /dev/null +++ b/lib/Migration/Version10000Date20201015150000.php @@ -0,0 +1,62 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * The HPB is generating sessions longer than 255 chars. So we update the length + * But the install migration was fixed, so this only does something on update. + */ +class Version10000Date20201015150000 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if ($schema->hasTable('talk_sessions')) { + $table = $schema->getTable('talk_sessions'); + + $column = $table->getColumn('session_id'); + + if ($column->getLength() !== 512) { + $column->setLength(512); + return $schema; + } + } + + return null; + } +} diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 121139d158e..3189ede37f5 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -50,6 +50,9 @@ * @method int getLastMentionMessage() */ class Attendee extends Entity { + public const ACTOR_USERS = 'users'; + public const ACTOR_GUESTS = 'guests'; + public const ACTOR_EMAILS = 'emails'; /** @var int */ protected $roomId; diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 6d54f04c546..97d578c184e 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -158,7 +158,7 @@ public function getActorDisplayName(): string { public function isReplyable(): bool { return $this->getMessageType() !== 'system' && $this->getMessageType() !== 'command' && - \in_array($this->getActorType(), ['users', 'guests']); + \in_array($this->getActorType(), [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS]); } public function toArray(): array { diff --git a/lib/Model/Session.php b/lib/Model/Session.php index 172e39ef9a1..0acabf59383 100644 --- a/lib/Model/Session.php +++ b/lib/Model/Session.php @@ -26,6 +26,14 @@ use OCP\AppFramework\Db\Entity; /** + * A session is the "I'm online in this conversation" state of Talk, you get one + * when opening the conversation while the inCall flag tells if you are just + * online (chatting), or in a call (with audio, camera or even sip). + * Currently it's limited to 1 per attendee, but the plan is to remove this + * restriction in the future, so e.g. in the future you can join with your phone + * on the SIP bridge, have your video/screenshare on the laptop and chat in the + * mobile app. + * * @method void setAttendeeId(int $attendeeId) * @method string getAttendeeId() * @method void setSessionId(string $sessionId) diff --git a/lib/Notification/Listener.php b/lib/Notification/Listener.php index a56d4798028..1ba06641f49 100644 --- a/lib/Notification/Listener.php +++ b/lib/Notification/Listener.php @@ -26,6 +26,7 @@ use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\JoinRoomUserEvent; use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; @@ -142,7 +143,7 @@ public function generateInvitation(Room $room, array $participants): void { } foreach ($participants as $participant) { - if ($participant['actorType'] !== 'users') { + if ($participant['actorType'] !== Attendee::ACTOR_USERS) { // No user => no activity continue; } diff --git a/lib/Participant.php b/lib/Participant.php index 9044c8736e7..b0a6bfaa6fc 100644 --- a/lib/Participant.php +++ b/lib/Participant.php @@ -71,6 +71,10 @@ public function getSession(): ?Session { return $this->session; } + public function setSession(Session $session): void { + $this->session = $session; + } + public function isGuest(): bool { $participantType = $this->attendee->getParticipantType(); return \in_array($participantType, [self::GUEST, self::GUEST_MODERATOR], true); diff --git a/lib/Room.php b/lib/Room.php index e7e13c93fbf..ee655156f0e 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -33,6 +33,7 @@ use OCA\Talk\Events\SignalingRoomPropertiesEvent; use OCA\Talk\Events\VerifyRoomPasswordEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Model\Attendee; use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; @@ -43,6 +44,15 @@ use OCP\Security\ISecureRandom; class Room { + + /** + * Regex that matches SIP incompatible rooms: + * 1. duplicate digit: …11… + * 2. leading zero: 0… + * 3. non-digit: …a… + */ + public const SIP_INCOMPATIBLE_REGEX = '/((\d)(?=\2+)|^0|\D)/'; + public const UNKNOWN_CALL = -1; public const ONE_TO_ONE_CALL = 1; public const GROUP_CALL = 2; @@ -352,7 +362,7 @@ public function getParticipant(?string $userId): Participant { ->selectAlias('s.id', 's_id') ->from('talk_attendees', 'a') ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) - ->where($query->expr()->eq('a.actor_type', $query->createNamedParameter('users'))) + ->where($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($userId))) ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) ->setMaxResults(1); @@ -454,6 +464,38 @@ public function getParticipantByAttendeeId(int $attendeeId): Participant { return $this->manager->createParticipantObject($this, $row); } + /** + * @param string $actorType + * @param string $actorId + * @return Participant + * @throws ParticipantNotFoundException When the pin is not valid (has no participant assigned) + */ + public function getParticipantByActor(string $actorType, string $actorId): Participant { + if ($actorType === Attendee::ACTOR_USERS) { + return $this->getParticipant($actorId); + } + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->selectAlias('a.id', 'a_id') + ->selectAlias('s.id', 's_id') + ->from('talk_attendees', 'a') + ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('a.id', 's.attendee_id')) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType))) + ->andWhere($query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId))) + ->andWhere($query->expr()->eq('a.room_id', $query->createNamedParameter($this->getId()))) + ->setMaxResults(1); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row === false) { + throw new ParticipantNotFoundException('User is not a participant'); + } + + return $this->manager->createParticipantObject($this, $row); + } + public function deleteRoom(): void { $event = new RoomEvent($this); $this->dispatcher->dispatch(self::EVENT_BEFORE_ROOM_DELETE, $event); @@ -743,12 +785,10 @@ public function setSIPEnabled(int $newSipEnabled): bool { return false; } - if (!preg_match('/^\d+$/', $this->token)) { + if (preg_match(self::SIP_INCOMPATIBLE_REGEX, $this->token)) { return false; } - // FIXME check if SIP is enabled/configured - $event = new ModifyRoomEvent($this, 'sipEnabled', $newSipEnabled, $oldSipEnabled); $this->dispatcher->dispatch(self::EVENT_BEFORE_SIP_ENABLED_SET, $event); diff --git a/lib/Search/MessageSearch.php b/lib/Search/MessageSearch.php index ada804a2b00..84b727a9762 100644 --- a/lib/Search/MessageSearch.php +++ b/lib/Search/MessageSearch.php @@ -29,6 +29,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\Manager as RoomManager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Room; use OCP\Comments\IComment; use OCP\IL10N; @@ -193,7 +194,7 @@ protected function commentToSearchResultEntry(Room $room, IUser $user, IComment } $iconUrl = ''; - if ($message->getActorType() === 'users') { + if ($message->getActorType() === Attendee::ACTOR_USERS) { $iconUrl = $this->url->linkToRouteAbsolute('core.avatar.getAvatar', [ 'userId' => $message->getActorId(), 'size' => 64, diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 1b0458b3f8a..8707cf3ca9e 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -23,6 +23,7 @@ namespace OCA\Talk\Service; +use OCA\Talk\Config; use OCA\Talk\Events\AddParticipantsEvent; use OCA\Talk\Events\JoinRoomGuestEvent; use OCA\Talk\Events\JoinRoomUserEvent; @@ -40,6 +41,7 @@ use OCA\Talk\Model\SessionMapper; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Webinary; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; @@ -51,6 +53,8 @@ use OCP\Security\ISecureRandom; class ParticipantService { + /** @var Config */ + protected $talkConfig; /** @var AttendeeMapper */ protected $attendeeMapper; /** @var SessionMapper */ @@ -68,7 +72,8 @@ class ParticipantService { /** @var ITimeFactory */ private $timeFactory; - public function __construct(AttendeeMapper $attendeeMapper, + public function __construct(Config $talkConfig, + AttendeeMapper $attendeeMapper, SessionMapper $sessionMapper, SessionService $sessionService, ISecureRandom $secureRandom, @@ -76,6 +81,7 @@ public function __construct(AttendeeMapper $attendeeMapper, IEventDispatcher $dispatcher, IUserManager $userManager, ITimeFactory $timeFactory) { + $this->talkConfig = $talkConfig; $this->attendeeMapper = $attendeeMapper; $this->sessionMapper = $sessionMapper; $this->sessionService = $sessionService; @@ -149,7 +155,7 @@ public function joinRoom(Room $room, IUser $user, string $password, bool $passed } try { - $attendee = $this->attendeeMapper->findByActor($room->getId(), 'users', $user->getUID()); + $attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID()); } catch (DoesNotExistException $e) { if (!$event->getPassedPasswordProtection() && !$room->verifyPassword($password)['result']) { throw new InvalidPasswordException('Provided password is invalid'); @@ -157,12 +163,12 @@ public function joinRoom(Room $room, IUser $user, string $password, bool $passed // User joining a public room, without being invited $this->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $user->getUID(), 'participantType' => Participant::USER_SELF_JOINED, ]]); - $attendee = $this->attendeeMapper->findByActor($room->getId(), 'users', $user->getUID()); + $attendee = $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_USERS, $user->getUID()); } $session = $this->sessionService->createSessionForAttendee($attendee); @@ -201,7 +207,7 @@ public function joinRoomAsNewGuest(Room $room, string $password, bool $passedPas $attendee = new Attendee(); $attendee->setRoomId($room->getId()); - $attendee->setActorType('guests'); + $attendee->setActorType(Attendee::ACTOR_GUESTS); $attendee->setActorId($randomActorId); $attendee->setParticipantType(Participant::GUEST); $attendee->setLastReadMessage($lastMessage); @@ -257,11 +263,13 @@ public function inviteEmailAddress(Room $room, string $email): Participant { $attendee = new Attendee(); $attendee->setRoomId($room->getId()); - $attendee->setActorType('emails'); + $attendee->setActorType(Attendee::ACTOR_EMAILS); $attendee->setActorId($email); - // FIXME Only do this when SIP is enabled? - $attendee->setPin($this->generatePin()); + if ($room->getSIPEnabled() === Webinary::SIP_ENABLED + && $this->talkConfig->isSIPConfigured()) { + $attendee->setPin($this->generatePin()); + } $attendee->setParticipantType(Participant::GUEST); $attendee->setLastReadMessage($lastMessage); @@ -271,6 +279,16 @@ public function inviteEmailAddress(Room $room, string $email): Participant { return new Participant($room, $attendee, null); } + public function generatePinForParticipant(Room $room, Participant $participant): void { + $attendee = $participant->getAttendee(); + if ($room->getSIPEnabled() === Webinary::SIP_ENABLED + && $this->talkConfig->isSIPConfigured() + && !$attendee->getPin()) { + $attendee->setPin($this->generatePin()); + $this->attendeeMapper->update($attendee); + } + } + public function ensureOneToOneRoomIsFilled(Room $room): void { if ($room->getType() !== Room::ONE_TO_ONE_CALL) { return; @@ -283,7 +301,7 @@ public function ensureOneToOneRoomIsFilled(Room $room): void { foreach ($missingUsers as $userId) { if ($this->userManager->userExists($userId)) { $this->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $userId, 'participantType' => Participant::OWNER, ]]); @@ -292,7 +310,7 @@ public function ensureOneToOneRoomIsFilled(Room $room): void { } public function leaveRoomAsSession(Room $room, Participant $participant): void { - if (!$participant->isGuest()) { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { $event = new ParticipantEvent($room, $participant); $this->dispatcher->dispatch(Room::EVENT_BEFORE_ROOM_DISCONNECT, $event); } else { @@ -302,17 +320,27 @@ public function leaveRoomAsSession(Room $room, Participant $participant): void { $session = $participant->getSession(); if ($session instanceof Session) { + $dispatchLeaveCallEvents = $session->getInCall() !== Participant::FLAG_DISCONNECTED; + if ($dispatchLeaveCallEvents) { + $event = new ModifyParticipantEvent($room, $participant, 'inCall', Participant::FLAG_DISCONNECTED, $session->getInCall()); + $this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_LEAVE_CALL, $event); + } + $this->sessionMapper->delete($session); + + if ($dispatchLeaveCallEvents) { + $this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $event); + } } else { $this->sessionMapper->deleteByAttendeeId($participant->getAttendee()->getId()); } - if ($participant->isGuest() + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS || $participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { $this->attendeeMapper->delete($participant->getAttendee()); } - if (!$participant->isGuest()) { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_GUESTS) { $this->dispatcher->dispatch(Room::EVENT_AFTER_ROOM_DISCONNECT, $event); } else { $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event); @@ -320,7 +348,7 @@ public function leaveRoomAsSession(Room $room, Participant $participant): void { } public function removeAttendee(Room $room, Participant $participant, string $reason): void { - $isUser = $participant->getAttendee()->getActorType() === 'users'; + $isUser = $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS; if ($isUser) { $user = $this->userManager->get($participant->getAttendee()->getActorId()); @@ -371,7 +399,7 @@ public function cleanGuestParticipants(Room $room): void { ->from('talk_sessions', 's') ->leftJoin('s', 'talk_attendees', 'a', $query->expr()->eq('s.attendee_id', 'a.id')) ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter('guests'))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS))) ->andWhere($query->expr()->lte('s.last_ping', $query->createNamedParameter($this->timeFactory->getTime() - 100, IQueryBuilder::PARAM_INT))); $sessionTableIds = []; @@ -388,7 +416,7 @@ public function cleanGuestParticipants(Room $room): void { ->from('talk_attendees', 'a') ->leftJoin('a', 'talk_sessions', 's', $query->expr()->eq('s.attendee_id', 'a.id')) ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter('guests'))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_GUESTS))) ->andWhere($query->expr()->isNull('s.id')); $attendeeIds = []; @@ -437,7 +465,7 @@ public function markUsersAsMentioned(Room $room, array $userIds, int $messageId) $query->update('talk_attendees') ->set('last_mention_message', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)) ->where($query->expr()->eq('room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter('users'))) + ->andWhere($query->expr()->eq('actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->in('actor_id', $query->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); $query->execute(); } @@ -562,7 +590,7 @@ public function getParticipantUserIds(Room $room, \DateTime $maxLastJoined = nul if ($maxLastJoined !== null) { $maxLastJoinedTimestamp = $maxLastJoined->getTimestamp(); } - $attendees = $this->attendeeMapper->getActorsByType($room->getId(), 'users', $maxLastJoinedTimestamp); + $attendees = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_USERS, $maxLastJoinedTimestamp); return array_map(static function (Attendee $attendee) { return $attendee->getActorId(); @@ -583,7 +611,7 @@ public function getParticipantUserIdsNotInCall(Room $room): array { $query->expr()->eq('s.attendee_id', 'a.id') ) ->where($query->expr()->eq('a.room_id', $query->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter('users'))) + ->andWhere($query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS))) ->andWhere($query->expr()->orX( $query->expr()->eq('s.in_call', $query->createNamedParameter(Participant::FLAG_DISCONNECTED)), $query->expr()->isNull('s.in_call') diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 69ade564dac..afc87851f8c 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -26,6 +26,7 @@ use InvalidArgumentException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; use OCA\Talk\Participant; use OCA\Talk\Room; use OCP\IUser; @@ -65,12 +66,12 @@ public function createOneToOneConversation(IUser $actor, IUser $targetUser): Roo $this->participantService->addUsers($room, [ [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $actor->getUID(), 'participantType' => Participant::OWNER, ], [ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $targetUser->getUID(), 'participantType' => Participant::OWNER, ], @@ -124,7 +125,7 @@ public function createConversation(int $type, string $name, ?IUser $owner = null if ($owner instanceof IUser) { $this->participantService->addUsers($room, [[ - 'actorType' => 'users', + 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $owner->getUID(), 'participantType' => Participant::OWNER, ]]); diff --git a/lib/Service/SessionService.php b/lib/Service/SessionService.php index 37b8d6af46a..7d66b8d9813 100644 --- a/lib/Service/SessionService.php +++ b/lib/Service/SessionService.php @@ -78,21 +78,37 @@ public function deleteSessionsById(array $ids): void { $this->sessionMapper->deleteByIds($ids); } - public function createSessionForAttendee(Attendee $attendee): Session { + /** + * @param Attendee $attendee + * @param string $forceSessionId + * @return Session + * @throws UniqueConstraintViolationException + */ + public function createSessionForAttendee(Attendee $attendee, string $forceSessionId = ''): Session { // Currently a participant can only join once $this->sessionMapper->deleteByAttendeeId($attendee->getId()); $session = new Session(); $session->setAttendeeId($attendee->getId()); - while (true) { - $sessionId = $this->secureRandom->generate(255); - $session->setSessionId($sessionId); + if ($forceSessionId !== '') { + $session->setSessionId($forceSessionId); try { $this->sessionMapper->insert($session); - break; } catch (UniqueConstraintViolationException $e) { - // 255 chars are not unique? Try again... + // The HPB told us to use a session which exists already… + throw $e; + } + } else { + while (true) { + $sessionId = $this->secureRandom->generate(255); + $session->setSessionId($sessionId); + try { + $this->sessionMapper->insert($session); + break; + } catch (UniqueConstraintViolationException $e) { + // 255 chars are not unique? Try again... + } } } diff --git a/lib/Settings/Admin/AdminSettings.php b/lib/Settings/Admin/AdminSettings.php index 4623d914d1c..66f6525ff3c 100644 --- a/lib/Settings/Admin/AdminSettings.php +++ b/lib/Settings/Admin/AdminSettings.php @@ -99,7 +99,7 @@ public function getForm(): TemplateResponse { $this->initTurnServers(); $this->initSignalingServers(); $this->initRequestSignalingServerTrial(); -// $this->initSIPBridge(); + $this->initSIPBridge(); return new TemplateResponse('spreed', 'settings/admin-settings', [], ''); } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 1ada571c703..e1429f9e90f 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -26,6 +26,7 @@ namespace OCA\Talk\Signaling; use OCA\Talk\Config; +use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -147,7 +148,7 @@ public function roomInvited(Room $room, array $users): void { $this->logger->info('Now invited to ' . $room->getToken() . ': ' . print_r($users, true)); $userIds = []; foreach ($users as $user) { - if ($user['actorType'] === 'users') { + if ($user['actorType'] === Attendee::ACTOR_USERS) { $userIds[] = $user['actorId']; } } @@ -253,8 +254,8 @@ public function participantsModified(Room $room, array $sessionIds): void { $participants = $this->participantService->getParticipantsForRoom($room); foreach ($participants as $participant) { $attendee = $participant->getAttendee(); - if ($attendee->getActorType() !== 'users' - && $attendee->getActorType() !== 'guests') { + if ($attendee->getActorType() !== Attendee::ACTOR_USERS + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { continue; } @@ -264,7 +265,7 @@ public function participantsModified(Room $room, array $sessionIds): void { 'sessionId' => '0', 'participantType' => $attendee->getParticipantType(), ]; - if ($attendee->getActorType() === 'users') { + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { $data['userId'] = $attendee->getActorId(); } @@ -313,8 +314,8 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds): vo $participants = $this->participantService->getParticipantsForRoom($room); foreach ($participants as $participant) { $attendee = $participant->getAttendee(); - if ($attendee->getActorType() !== 'users' - && $attendee->getActorType() !== 'guests') { + if ($attendee->getActorType() !== Attendee::ACTOR_USERS + && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { continue; } @@ -324,7 +325,7 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds): vo 'sessionId' => '0', 'participantType' => $attendee->getParticipantType(), ]; - if ($attendee->getActorType() === 'users') { + if ($attendee->getActorType() === Attendee::ACTOR_USERS) { $data['userId'] = $attendee->getActorId(); } diff --git a/lib/TInitialState.php b/lib/TInitialState.php index c918befe3fa..5b771d50f65 100644 --- a/lib/TInitialState.php +++ b/lib/TInitialState.php @@ -67,6 +67,11 @@ protected function publishInitialStateShared(): void { 'talk', 'signaling_mode', $this->talkConfig->getSignalingMode() ); + + $this->initialStateService->provideInitialState( + 'talk', 'sip_dialin_info', + $this->talkConfig->getDialInInfo() + ); } protected function publishInitialStateForUser(IUser $user, IRootFolder $rootFolder, IAppManager $appManager): void { diff --git a/psalm.xml b/psalm.xml index 050ae81010c..4b701c084c1 100644 --- a/psalm.xml +++ b/psalm.xml @@ -20,19 +20,25 @@ - - + + + + + + + + diff --git a/src/components/AdminSettings/SIPBridge.vue b/src/components/AdminSettings/SIPBridge.vue index e626261512f..6559c4b0a36 100644 --- a/src/components/AdminSettings/SIPBridge.vue +++ b/src/components/AdminSettings/SIPBridge.vue @@ -1,5 +1,5 @@ + + + + diff --git a/src/components/TopBar/TopBar.vue b/src/components/TopBar/TopBar.vue index 4832354305f..1955b70b846 100644 --- a/src/components/TopBar/TopBar.vue +++ b/src/components/TopBar/TopBar.vue @@ -141,6 +141,7 @@ {{ t('spreed', 'Start time (optional)') }} {{ t('spreed', 'Enable SIP dial-in') }} @@ -296,6 +297,9 @@ export default { showModerationOptions() { return !this.isOneToOneConversation && this.canModerate }, + canUserEnableSIP() { + return this.conversation.canEnableSIP + }, token() { return this.$store.getters.getToken() }, @@ -311,6 +315,7 @@ export default { displayName: '', isFavorite: false, hasPassword: false, + canEnableSIP: false, type: CONVERSATION.TYPE.PUBLIC, lobbyState: WEBINAR.LOBBY.NONE, lobbyTimer: 0, diff --git a/src/constants.js b/src/constants.js index e5a5cfb09c5..a3d3c0a9855 100644 --- a/src/constants.js +++ b/src/constants.js @@ -49,6 +49,12 @@ export const PARTICIPANT = { WITH_VIDEO: 4, WITH_PHONE: 8, }, + SIP_FLAG: { + MUTE_MICROPHONE: 1, + MUTE_SPEAKER: 2, + SPEAKING: 4, + RAISE_HAND: 8, + }, NOTIFY: { DEFAULT: 0, ALWAYS: 1, diff --git a/src/services/callsService.js b/src/services/callsService.js index 3298522c0ea..86b39cb2498 100644 --- a/src/services/callsService.js +++ b/src/services/callsService.js @@ -53,17 +53,9 @@ const leaveCall = async function(token) { } } -/** - * Fetches all peers for a call - * @param {string} token The token of the call to be fetched. - */ -const fetchPeers = async function(token) { - try { - const response = await axios.get(generateOcsUrl('apps/spreed/api/v1', 2) + `call/${token}`) - return response - } catch (error) { - console.debug('Error while fetching the peers: ', error) - } +const fetchPeers = async function(token, options) { + const response = await axios.get(generateOcsUrl('apps/spreed/api/v3', 2) + `call/${token}`, options) + return response } export { diff --git a/src/store/participantsStore.js b/src/store/participantsStore.js index 1da71d610ec..b191a8a45a8 100644 --- a/src/store/participantsStore.js +++ b/src/store/participantsStore.js @@ -34,6 +34,8 @@ import { PARTICIPANT } from '../constants' const state = { participants: { }, + peers: { + }, } const getters = { @@ -71,6 +73,17 @@ const getters = { return index }, + getPeer: (state) => (token, sessionId) => { + if (!state.peers[token]) { + return {} + } + + if (state.peers[token].hasOwnProperty(sessionId)) { + return state.peers[token][sessionId] + } + + return {} + }, } const mutations = { @@ -110,6 +123,18 @@ const mutations = { Vue.delete(state.participants, token) } }, + + addPeer(state, { token, peer }) { + if (!state.peers[token]) { + Vue.set(state.peers, token, []) + } + Vue.set(state.peers[token], peer.sessionId, peer) + }, + purgePeersStore(state, token) { + if (state.peers[token]) { + Vue.delete(state.peers, token) + } + }, } const actions = { @@ -189,6 +214,13 @@ const actions = { commit('purgeParticipantsStore', token) }, + addPeer({ commit }, { token, peer }) { + commit('addPeer', { token, peer }) + }, + purgePeersStore({ commit }, token) { + commit('purgePeersStore', token) + }, + updateSessionId({ commit, getters }, { token, participantIdentifier, sessionId }) { const index = getters.getParticipantIndex(token, participantIdentifier) if (index === -1) { diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 1b0af2b75f8..6042e98de0c 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1147,6 +1147,9 @@ Signaling.Standalone.prototype.processRoomParticipantsEvent = function(data) { this._trigger('usersChanged', [data.event.update.users || []]) this._trigger('participantListChanged') break + case 'flags': + this._trigger('participantFlagsChanged', [data.event.flags || []]) + break default: console.error('Unknown room participant event', data) break diff --git a/src/utils/webrtc/models/CallParticipantModel.js b/src/utils/webrtc/models/CallParticipantModel.js index 88b7e0f6226..185d85a8df2 100644 --- a/src/utils/webrtc/models/CallParticipantModel.js +++ b/src/utils/webrtc/models/CallParticipantModel.js @@ -43,6 +43,7 @@ export default function CallParticipantModel(options) { // are used for known but negative/empty values. userId: undefined, name: undefined, + internal: undefined, connectionState: ConnectionState.NEW, stream: null, // The audio element is part of the model to ensure that it can be diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index 0535e15002d..be9ee4e993b 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -237,6 +237,9 @@ function usersChanged(signaling, newUsers, disconnectedSessionIds) { }) } callParticipantModel.setUserId(userId) + if (user.internal) { + callParticipantModel.set('internal', true) + } // When the MCU is used and the other participant has no streams or // when no MCU is used and neither the local participant nor the @@ -394,6 +397,20 @@ export default function initWebRTC(signaling, _callParticipantCollection, _local }) usersInCallChanged(signaling, usersInCallMapping) }) + signaling.on('participantFlagsChanged', function(event) { + /** + * event { + * roomid: "1609407087", + * sessionid: "…", + * flags: 1 + * } + */ + const callParticipantModel = callParticipantCollection.get(event.sessionid) + if (callParticipantModel) { + callParticipantModel.set('speaking', (event.flags & PARTICIPANT.SIP_FLAG.SPEAKING) > 0) + callParticipantModel.set('audioAvailable', (event.flags & PARTICIPANT.SIP_FLAG.MUTE_MICROPHONE) === 0) + } + }) signaling.on('usersInRoom', function(users) { usersInCallMapping = {} users.forEach(function(user) { diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index 69cc90c4f14..095c7903b92 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -30,7 +30,7 @@ - + @@ -41,7 +41,7 @@ import GeneralSettings from '../components/AdminSettings/GeneralSettings' import HostedSignalingServer from '../components/AdminSettings/HostedSignalingServer' import MatterbridgeIntegration from '../components/AdminSettings/MatterbridgeIntegration' import SignalingServers from '../components/AdminSettings/SignalingServers' -// import SIPBridge from '../components/AdminSettings/SIPBridge' +import SIPBridge from '../components/AdminSettings/SIPBridge' import StunServers from '../components/AdminSettings/StunServers' import TurnServers from '../components/AdminSettings/TurnServers' @@ -55,7 +55,7 @@ export default { HostedSignalingServer, MatterbridgeIntegration, SignalingServers, - // SIPBridge, + SIPBridge, StunServers, TurnServers, }, diff --git a/tests/integration/features/command/active-calls.feature b/tests/integration/features/command/active-calls.feature index f73145c774b..e7ab23b5c44 100644 --- a/tests/integration/features/command/active-calls.feature +++ b/tests/integration/features/command/active-calls.feature @@ -30,6 +30,7 @@ Feature: create And user "participant1" joins call "room" with 200 Given invoking occ with "talk:active-calls" + # It didn't really fail, it just has an exit code that is not 0 Then the command failed with exit code 1 And the command output contains the text "There are currently 1 calls in progress with 1 participants" diff --git a/tests/php/Controller/SignalingControllerTest.php b/tests/php/Controller/SignalingControllerTest.php index e9da6f7f2f7..b99e10ef191 100644 --- a/tests/php/Controller/SignalingControllerTest.php +++ b/tests/php/Controller/SignalingControllerTest.php @@ -591,10 +591,6 @@ public function testBackendRoomInvitedPublic() { ->willReturn($room); $participant = $this->createMock(Participant::class); - $room->expects($this->once()) - ->method('getParticipantBySession') - ->with($sessionId) - ->willThrowException(new ParticipantNotFoundException()); $room->expects($this->once()) ->method('getParticipantBySession') ->with($sessionId) diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 62636e07546..e3320df41f7 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,5 +1,5 @@ - + BeforeTemplateRenderedEvent @@ -38,6 +38,12 @@ getById + + + $listener + $listener + + Base @@ -172,16 +178,6 @@ DBALException - - - $success - - - - - $success - - IRootFolder @@ -217,10 +213,6 @@ - - 'Internal signaling disabled.' - 'Internal signaling disabled.' - \GuzzleHttp\Exception\ConnectException @@ -249,13 +241,16 @@ GroupFolderStorage + + + \Doctrine\DBAL\DBALException - + @@ -341,6 +336,9 @@ + + [self::class, 'listenPreShare'] + $event->getView() $event->getView() @@ -350,16 +348,6 @@ $view Filesystem - - - - - $listener - $listener - - - - @@ -387,12 +375,6 @@ Cache - - - IURLGenerator - IUrlGenerator - - IRootFolder