diff --git a/css/icons.css b/css/icons.css index f1c9527f11d..35b28cdffd7 100644 --- a/css/icons.css +++ b/css/icons.css @@ -98,10 +98,13 @@ * not accept several classes. */ .user-bubble__avatar .icon-group-forced-white.avatar-class-icon, .user-bubble__avatar .icon-user-forced-white.avatar-class-icon, +.user-bubble__avatar .icon-mail-forced-white.avatar-class-icon, .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--, .autocomplete-result .icon-user-forced-white.autocomplete-result__icon--, +.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--, .mention-bubble .icon-group-forced-white.mention-bubble__icon--, -.mention-bubble .icon-user-forced-white.mention-bubble__icon-- { +.mention-bubble .icon-user-forced-white.mention-bubble__icon--, +.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- { background-color: #6B6B6B; } @@ -109,10 +112,13 @@ @media (prefers-color-scheme: dark) { body[data-theme-default] .user-bubble__avatar .icon-group-forced-white.avatar-class-icon, body[data-theme-default] .user-bubble__avatar .icon-user-forced-white.avatar-class-icon, + body[data-theme-default] .user-bubble__avatar .icon-mail-forced-white.avatar-class-icon, body[data-theme-default] .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--, body[data-theme-default] .autocomplete-result .icon-user-forced-white.autocomplete-result__icon--, + body[data-theme-default] .autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--, body[data-theme-default] .mention-bubble .icon-group-forced-white.mention-bubble__icon--, - body[data-theme-default] .mention-bubble .icon-user-forced-white.mention-bubble__icon-- { + body[data-theme-default] .mention-bubble .icon-user-forced-white.mention-bubble__icon--, + body[data-theme-default] .mention-bubble .icon-mail-forced-white.mention-bubble__icon-- { background-color: #3B3B3B; } } @@ -120,22 +126,28 @@ /* Manually set dark theme */ body[data-theme-dark] .user-bubble__avatar .icon-group-forced-white.avatar-class-icon, body[data-theme-dark] .user-bubble__avatar .icon-user-forced-white.avatar-class-icon, +body[data-theme-dark] .user-bubble__avatar .icon-mail-forced-white.avatar-class-icon, body[data-theme-dark] .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--, body[data-theme-dark] .autocomplete-result .icon-user-forced-white.autocomplete-result__icon--, +body[data-theme-dark] .autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--, body[data-theme-dark] .mention-bubble .icon-group-forced-white.mention-bubble__icon--, -body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__icon-- { +body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__icon--, +body[data-theme-dark] .mention-bubble .icon-mail-forced-white.mention-bubble__icon-- { background-color: #3B3B3B; } .user-bubble__avatar .icon-group-forced-white.avatar-class-icon, .user-bubble__avatar .icon-user-forced-white.avatar-class-icon, +.user-bubble__avatar .icon-mail-forced-white.avatar-class-icon, .mention-bubble .icon-group-forced-white.mention-bubble__icon--, -.mention-bubble .icon-user-forced-white.mention-bubble__icon-- { +.mention-bubble .icon-user-forced-white.mention-bubble__icon--, +.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- { background-size: 75%; } .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--, -.autocomplete-result .icon-user-forced-white.autocomplete-result__icon-- { +.autocomplete-result .icon-user-forced-white.autocomplete-result__icon--, +.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon-- { background-size: 50% !important; } @@ -145,6 +157,12 @@ body[data-theme-dark] .mention-bubble .icon-user-forced-white.mention-bubble__ic background-image: url(../img/icon-user-white.svg); } +.user-bubble__avatar .icon-mail-forced-white, +.autocomplete-result .icon-mail-forced-white.autocomplete-result__icon--, +.mention-bubble .icon-mail-forced-white.mention-bubble__icon-- { + background-image: url(../img/icon-mail-white.svg); +} + .user-bubble__avatar .icon-group-forced-white, .autocomplete-result .icon-group-forced-white.autocomplete-result__icon--, .mention-bubble .icon-group-forced-white.mention-bubble__icon-- { diff --git a/docs/bots.md b/docs/bots.md index 417faa4b878..1c3de113b83 100644 --- a/docs/bots.md +++ b/docs/bots.md @@ -74,7 +74,7 @@ The content format follows the [Activity Streams 2.0 Vocabulary](https://www.w3. | Path | Description | |------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| actor.id | One of the known [attendee types](constants.md#attendee-types) followed by the `/` slash character and a unique identifier within the given type. For users it is the Nextcloud user ID, for guests a sha1 value. | +| actor.id | One of the known [attendee types](constants.md#attendee-types) followed by the `/` slash character and a unique identifier within the given type. For users it is the Nextcloud user ID, for guests and email invited guests a random hash value. | | actor.name | The display name of the attendee sending the message. | | object.id | The message ID of the given message on the origin server. It can be used to react or reply to the given message. | | object.name | For normal written messages `message`, otherwise one of the known [system message identifiers](chat.md#system-messages). | diff --git a/lib/Activity/Listener.php b/lib/Activity/Listener.php index ada80a15ff8..d7e5a8c0d84 100644 --- a/lib/Activity/Listener.php +++ b/lib/Activity/Listener.php @@ -69,7 +69,8 @@ protected function generateCallActivity(ACallEndedEvent $event): void { $duration = $this->timeFactory->getTime() - $activeSince->getTimestamp(); $userIds = $this->participantService->getParticipantUserIds($room, $activeSince); $cloudIds = $this->participantService->getParticipantActorIdsByActorType($room, [Attendee::ACTOR_FEDERATED_USERS], $activeSince); - $numGuests = $this->participantService->getGuestCount($room, $activeSince); + $numGuests = $this->participantService->getActorsCountByType($room, Attendee::ACTOR_GUESTS, $activeSince->getTimestamp()); + $numGuests += $this->participantService->getActorsCountByType($room, Attendee::ACTOR_EMAILS, $activeSince->getTimestamp()); $message = 'call_ended'; if ($event instanceof CallEndedForEveryoneEvent) { diff --git a/lib/Chat/AutoComplete/SearchPlugin.php b/lib/Chat/AutoComplete/SearchPlugin.php index 26c5401f661..32e5eb6f3e6 100644 --- a/lib/Chat/AutoComplete/SearchPlugin.php +++ b/lib/Chat/AutoComplete/SearchPlugin.php @@ -67,6 +67,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $groupIds = []; /** @var array $cloudIds */ $cloudIds = []; + /** @var array $emailAttendees */ + $emailAttendees = []; /** @var list $guestAttendees */ $guestAttendees = []; @@ -82,6 +84,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $attendee = $participant->getAttendee(); if ($attendee->getActorType() === Attendee::ACTOR_GUESTS) { $guestAttendees[] = $attendee; + } elseif ($attendee->getActorType() === Attendee::ACTOR_EMAILS) { + $emailAttendees[$attendee->getActorId()] = $attendee->getDisplayName(); } elseif ($attendee->getActorType() === Attendee::ACTOR_USERS) { $userIds[$attendee->getActorId()] = $attendee->getDisplayName(); } elseif ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { @@ -95,6 +99,7 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $this->searchUsers($search, $userIds, $searchResult); $this->searchGroups($search, $groupIds, $searchResult); $this->searchGuests($search, $guestAttendees, $searchResult); + $this->searchEmails($search, $emailAttendees, $searchResult); $this->searchFederatedUsers($search, $cloudIds, $searchResult); return false; @@ -300,6 +305,53 @@ protected function searchGuests(string $search, array $attendees, ISearchResult $searchResult->addResultSet($type, $matches, $exactMatches); } + /** + * @param string $search + * @param array $attendees + * @param ISearchResult $searchResult + */ + protected function searchEmails(string $search, array $attendees, ISearchResult $searchResult): void { + if (empty($attendees)) { + $type = new SearchResultType('emails'); + $searchResult->addResultSet($type, [], []); + return; + } + + $search = strtolower($search); + $currentSessionHash = null; + if (!$this->userId) { + // Best effort: Might not work on guests that reloaded but not worth too much performance impact atm. + $currentSessionHash = false; // FIXME sha1($this->talkSession->getSessionForRoom($this->room->getToken())); + } + + $matches = $exactMatches = []; + foreach ($attendees as $actorId => $displayName) { + if ($currentSessionHash === $actorId) { + // Do not suggest the current guest + continue; + } + + $displayName = $displayName ?: $this->l->t('Guest'); + if ($search === '') { + $matches[] = $this->createEmailResult($actorId, $displayName); + continue; + } + + if (strtolower($displayName) === $search) { + $exactMatches[] = $this->createEmailResult($actorId, $displayName); + continue; + } + + if (stripos($displayName, $search) !== false) { + $matches[] = $this->createEmailResult($actorId, $displayName); + continue; + } + } + + $type = new SearchResultType('emails'); + $searchResult->addResultSet($type, $matches, $exactMatches); + } + protected function createResult(string $type, string $uid, string $name): array { if ($type === 'user' && $name === '') { $name = $this->userManager->getDisplayName($uid) ?? $uid; @@ -333,4 +385,14 @@ protected function createGuestResult(string $actorId, string $name): array { ], ]; } + + protected function createEmailResult(string $actorId, string $name): array { + return [ + 'label' => $name, + 'value' => [ + 'shareType' => 'email', + 'shareWith' => 'email/' . $actorId, + ], + ]; + } } diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 422fcf15195..3aa74f255f7 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -136,17 +136,18 @@ protected function getActorInformation(Message $message, string $actorType, stri } elseif ($actorType === Attendee::ACTOR_BRIDGED) { $displayName = $actorId; $actorId = MatterbridgeManager::BRIDGE_BOT_USERID; - } elseif ($actorType === Attendee::ACTOR_GUESTS + } elseif (($actorType === Attendee::ACTOR_GUESTS || $actorType === Attendee::ACTOR_EMAILS) && !in_array($actorId, [Attendee::ACTOR_ID_CLI, Attendee::ACTOR_ID_CHANGELOG], true)) { - if (isset($this->guestNames[$actorId])) { - $displayName = $this->guestNames[$actorId]; + $cacheKey = $actorType . '/' . $actorId; + if (isset($this->guestNames[$cacheKey])) { + $displayName = $this->guestNames[$cacheKey]; } else { try { - $participant = $this->participantService->getParticipantByActor($message->getRoom(), Attendee::ACTOR_GUESTS, $actorId); + $participant = $this->participantService->getParticipantByActor($message->getRoom(), $actorType, $actorId); $displayName = $participant->getAttendee()->getDisplayName(); } catch (ParticipantNotFoundException) { } - $this->guestNames[$actorId] = $displayName; + $this->guestNames[$cacheKey] = $displayName; } } elseif ($actorType === Attendee::ACTOR_BOTS) { $displayName = $actorId . '-bot'; diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index 4d46806f1a9..aebaebf5eb3 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -148,6 +148,12 @@ protected function parseMessage(Message $chatMessage): void { $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS && $currentActorId === $parsedParameters['actor']['id'] && empty($parsedParameters['actor']['server']); + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS) { + $currentActorType = $participant->getAttendee()->getActorType(); + $currentActorId = $participant->getAttendee()->getActorId(); + $currentUserIsActor = $parsedParameters['actor']['type'] === 'email' && + $participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS && + $participant->getAttendee()->getActorId() === $parsedParameters['actor']['id']; } else { $currentActorType = $participant->getAttendee()->getActorType(); $currentActorId = $participant->getAttendee()->getActorId(); @@ -457,7 +463,12 @@ protected function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('An administrator demoted {user} from moderator'); } } elseif ($message === 'guest_moderator_promoted') { - $parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']); + if (isset($parameters['type'], $parameters['id'])) { + $parsedParameters['user'] = $this->getGuest($room, $parameters['type'], $parameters['id']); + } else { + // Before Nextcloud 30 + $parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']); + } $parsedMessage = $this->l->t('{actor} promoted {user} to moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You promoted {user} to moderator'); @@ -470,7 +481,12 @@ protected function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('An administrator promoted {user} to moderator'); } } elseif ($message === 'guest_moderator_demoted') { - $parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']); + if (isset($parameters['type'], $parameters['id'])) { + $parsedParameters['user'] = $this->getGuest($room, $parameters['type'], $parameters['id']); + } else { + // Before Nextcloud 30 + $parsedParameters['user'] = $this->getGuest($room, Attendee::ACTOR_GUESTS, $parameters['session']); + } $parsedMessage = $this->l->t('{actor} demoted {user} from moderator'); if ($currentUserIsActor) { $parsedMessage = $this->l->t('You demoted {user} from moderator'); @@ -847,6 +863,9 @@ protected function isCurrentParticipantChangedUser(?string $currentActorType, ?s if ($currentActorType === Attendee::ACTOR_GUESTS) { return $parameter['type'] === 'guest' && $currentActorId === $parameter['id']; } + if ($currentActorType === Attendee::ACTOR_EMAILS) { + return $parameter['type'] === 'guest' && 'email/' . $currentActorId === $parameter['id']; + } if (isset($parameter['server']) && $currentActorType === Attendee::ACTOR_FEDERATED_USERS @@ -1019,7 +1038,7 @@ protected function getGuest(Room $room, string $actorType, string $actorId): arr return [ 'type' => 'guest', - 'id' => 'guest/' . $actorId, + 'id' => ($actorType === Attendee::ACTOR_GUESTS ? 'guest/' : 'email/') . $actorId, 'name' => $this->guestNames[$key], ]; } diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index 8b181d57315..5244c4faaf3 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -114,6 +114,7 @@ protected function parseMessage(Message $chatMessage): void { $search = $mention['id']; if ( + $mention['type'] === 'email' || $mention['type'] === 'group' || // $mention['type'] === 'federated_group' || // $mention['type'] === 'team' || @@ -131,6 +132,7 @@ protected function parseMessage(Message $chatMessage): void { $message = str_replace('@"' . $search . '"', '{' . $mentionParameterId . '}', $message); if (!str_contains($search, ' ') && !str_starts_with($search, 'guest/') + && !str_starts_with($search, 'email/') && !str_starts_with($search, 'group/') // && !str_starts_with($search, 'federated_group/') // && !str_starts_with($search, 'team/') @@ -160,6 +162,19 @@ protected function parseMessage(Message $chatMessage): void { $displayName = $this->l->t('Guest'); } + $messageParameters[$mentionParameterId] = [ + 'type' => $mention['type'], + 'id' => $mention['id'], + 'name' => $displayName, + ]; + } elseif ($mention['type'] === 'email') { + try { + $participant = $this->participantService->getParticipantByActor($chatMessage->getRoom(), Attendee::ACTOR_EMAILS, $mention['id']); + $displayName = $participant->getAttendee()->getDisplayName() ?: $this->l->t('Guest'); + } catch (ParticipantNotFoundException) { + $displayName = $this->l->t('Guest'); + } + $messageParameters[$mentionParameterId] = [ 'type' => $mention['type'], 'id' => $mention['id'], diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 702ab950ac5..5e1d71c3ab0 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -311,7 +311,11 @@ public function sendSystemMessageAboutPromoteOrDemoteModerator(ParticipantModifi $room = $event->getRoom(); $attendee = $event->getParticipant()->getAttendee(); - if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { + if (!in_array($attendee->getActorType(), [ + Attendee::ACTOR_USERS, + Attendee::ACTOR_EMAILS, + Attendee::ACTOR_GUESTS, + ], true)) { return; } @@ -324,9 +328,9 @@ public function sendSystemMessageAboutPromoteOrDemoteModerator(ParticipantModifi $this->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]); } } elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) { - $this->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]); + $this->sendSystemMessage($room, 'guest_moderator_promoted', ['type' => $attendee->getActorType(), 'id' => $attendee->getActorId()]); } elseif ($event->getNewValue() === Participant::GUEST) { - $this->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]); + $this->sendSystemMessage($room, 'guest_moderator_demoted', ['type' => $attendee->getActorType(), 'id' => $attendee->getActorId()]); } } diff --git a/lib/Collaboration/Reference/TalkReferenceProvider.php b/lib/Collaboration/Reference/TalkReferenceProvider.php index d1710720214..2ddc7b2574b 100644 --- a/lib/Collaboration/Reference/TalkReferenceProvider.php +++ b/lib/Collaboration/Reference/TalkReferenceProvider.php @@ -201,7 +201,7 @@ protected function fetchReference(Reference $reference): void { } $displayName = $message->getActorDisplayName(); - if ($message->getActorType() === Attendee::ACTOR_GUESTS) { + if (in_array($message->getActorType(), [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { if ($displayName === '') { $displayName = $this->l->t('Guest'); } else { diff --git a/lib/Controller/BanController.php b/lib/Controller/BanController.php index 8725e8b64d0..6520cb68522 100644 --- a/lib/Controller/BanController.php +++ b/lib/Controller/BanController.php @@ -37,7 +37,7 @@ public function __construct( * * Required capability: `ban-v1` * - * @param 'users'|'guests'|'ip' $actorType Type of actor to ban, or `ip` when banning a clients remote address + * @param 'users'|'guests'|'emails'|'ip' $actorType Type of actor to ban, or `ip` when banning a clients remote address * @param string $actorId Actor ID or the IP address or range in case of type `ip` * @param string $internalNote Optional internal note (max. 4000 characters) * @return DataResponse|DataResponse diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 25489c9b648..e598b0e9e26 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -134,7 +134,9 @@ protected function getActorInfo(string $actorDisplayName = ''): array { if ($actorDisplayName) { $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName); } - return [Attendee::ACTOR_GUESTS, $this->participant->getAttendee()->getActorId()]; + /** @var Attendee::ACTOR_GUESTS|Attendee::ACTOR_EMAILS $actorType */ + $actorType = $this->participant->getAttendee()->getActorType(); + return [$actorType, $this->participant->getAttendee()->getActorId()]; } if ($this->userId === MatterbridgeManager::BRIDGE_BOT_USERID && $actorDisplayName) { diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index cf6b64b9664..0d3e6b5143d 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -13,6 +13,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; @@ -50,6 +51,7 @@ use OCP\Notification\IManager as INotificationManager; use OCP\Security\Bruteforce\IThrottler; use Psr\Log\LoggerInterface; +use SensitiveParameter; #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] class PageController extends Controller { @@ -96,9 +98,9 @@ public function __construct( #[PublicPage] #[UseSession] #[BruteForceProtection(action: 'talkRoomToken')] - public function showCall(string $token): Response { + public function showCall(string $token, string $email = '', string $access = ''): Response { // This is the entry point from the `/call/{token}` URL which is hardcoded in the server. - return $this->index($token); + return $this->pageHandler($token, email: $email, accessToken: $access); } /** @@ -113,7 +115,7 @@ public function showCall(string $token): Response { #[BruteForceProtection(action: 'talkRoomPassword')] public function authenticatePassword(string $token, string $password = ''): Response { // This is the entry point from the `/call/{token}` URL which is hardcoded in the server. - return $this->pageHandler($token, '', $password); + return $this->pageHandler($token, password: $password); } #[NoCSRFRequired] @@ -152,11 +154,18 @@ public function index(string $token = '', string $callUser = ''): Response { * @return TemplateResponse|RedirectResponse * @throws HintException */ - protected function pageHandler(string $token = '', string $callUser = '', string $password = ''): Response { + protected function pageHandler( + string $token = '', + string $callUser = '', + string $password = '', + string $email = '', + #[SensitiveParameter] + string $accessToken = '', + ): Response { $bruteForceToken = $token; $user = $this->userSession->getUser(); if (!$user instanceof IUser) { - return $this->guestEnterRoom($token, $password); + return $this->guestEnterRoom($token, $password, $email, $accessToken); } $throttle = false; @@ -332,12 +341,23 @@ public function recording(string $token): Response { } /** - * @param string $token - * @param string $password * @return TemplateResponse|RedirectResponse * @throws HintException */ - protected function guestEnterRoom(string $token, string $password): Response { + protected function guestEnterRoom( + string $token, + string $password, + string $email, + #[SensitiveParameter] + string $accessToken, + ): Response { + if ($email && $accessToken) { + return $this->invitedEmail( + $token, + $email, + $accessToken, + ); + } try { $room = $this->manager->getRoomByToken($token); if ($room->getType() !== Room::TYPE_PUBLIC) { @@ -405,6 +425,63 @@ protected function guestEnterRoom(string $token, string $password): Response { return $response; } + /** + * @return TemplateResponse|RedirectResponse + * @throws HintException + */ + protected function invitedEmail( + string $token, + string $email, + #[SensitiveParameter] + string $accessToken, + ): Response { + try { + $actorId = hash('sha256', $email); + $this->manager->getRoomByAccessToken( + $token, + Attendee::ACTOR_EMAILS, + $actorId, + $accessToken, + ); + $this->talkSession->renewSessionId(); + $this->talkSession->setAuthedEmailActorIdForRoom($token, $actorId); + } catch (RoomNotFoundException) { + $redirectUrl = $this->url->linkToRoute('spreed.Page.index'); + if ($token) { + $redirectUrl = $this->url->linkToRoute('spreed.Page.showCall', ['token' => $token]); + } + $response = new RedirectResponse($this->url->linkToRoute('core.login.showLoginForm', [ + 'redirect_url' => $redirectUrl, + ])); + $response->throttle(['token' => $token, 'action' => 'talkRoomToken']); + return $response; + } + + $this->publishInitialStateForGuest(); + $this->eventDispatcher->dispatchTyped(new RenderReferenceEvent()); + + $response = new PublicTemplateResponse($this->appName, 'index', [ + 'id-app-content' => '#content-vue', + 'id-app-navigation' => null, + ]); + + $response->setFooterVisible(false); + $csp = new ContentSecurityPolicy(); + $csp->addAllowedConnectDomain('*'); + $csp->addAllowedMediaDomain('blob:'); + $csp->addAllowedWorkerSrcDomain('blob:'); + $csp->addAllowedWorkerSrcDomain("'self'"); + $csp->addAllowedChildSrcDomain('blob:'); + $csp->addAllowedChildSrcDomain("'self'"); + $csp->addAllowedScriptDomain('blob:'); + $csp->addAllowedScriptDomain("'self'"); + $csp->addAllowedConnectDomain('blob:'); + $csp->addAllowedConnectDomain("'self'"); + $csp->addAllowedImageDomain('https://*.tile.openstreetmap.org'); + $response->setContentSecurityPolicy($csp); + return $response; + } + /** * @param string $token * @return RedirectResponse diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 08ff87eff08..17821e88409 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -984,6 +984,7 @@ protected function formatParticipantList(array $participants, bool $includeStatu if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GUESTS) { $cleanGuests = true; } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS + || $participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS || $participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { $this->participantService->leaveRoomAsSession($this->room, $participant); } @@ -1076,6 +1077,14 @@ protected function formatParticipantList(array $participants, bool $includeStatu $result['displayName'] = $participant->getAttendee()->getDisplayName(); } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_CIRCLES) { $result['displayName'] = $participant->getAttendee()->getDisplayName(); + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS) { + if ($participant->getSession() instanceof Session && $participant->getSession()->getLastPing() <= $maxPingAge) { + $this->participantService->leaveRoomAsSession($this->room, $participant); + } + $result['displayName'] = $participant->getAttendee()->getDisplayName(); + if ($this->participant->hasModeratorPermissions() || $this->participant->getAttendee()->getId() === $participant->getAttendee()->getId()) { + $result['invitedActorId'] = $participant->getAttendee()->getInvitedCloudId(); + } } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { if ($participant->getSession() instanceof Session && $participant->getSession()->getLastPing() <= $maxPingAge) { $this->participantService->leaveRoomAsSession($this->room, $participant); @@ -1195,10 +1204,12 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } catch (TypeException) { } + $email = $newParticipant; + $actorId = hash('sha256', $email); try { - $this->participantService->getParticipantByActor($this->room, Attendee::ACTOR_EMAILS, $newParticipant); + $this->participantService->getParticipantByActor($this->room, Attendee::ACTOR_EMAILS, $actorId); } catch (ParticipantNotFoundException) { - $participant = $this->participantService->inviteEmailAddress($this->room, $newParticipant); + $participant = $this->participantService->inviteEmailAddress($this->room, $actorId, $email); $this->guestManager->sendEmailInvitation($this->room, $participant); } @@ -1643,8 +1654,10 @@ public function joinRoom(string $token, string $password = '', bool $force = tru } } + $authenticatedEmailGuest = $this->session->getAuthedEmailActorIdForRoom($token); + $headers = []; - if ($room->isFederatedConversation()) { + if ($authenticatedEmailGuest !== null || $room->isFederatedConversation()) { // Skip password checking $result = [ 'result' => true, @@ -1659,6 +1672,12 @@ public function joinRoom(string $token, string $password = '', bool $force = tru $participant = $this->participantService->joinRoom($this->roomService, $room, $user, $password, $result['result']); $this->participantService->generatePinForParticipant($room, $participant); } else { + if ($authenticatedEmailGuest !== null && $previousParticipant === null) { + try { + $previousParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_EMAILS, $authenticatedEmailGuest); + } catch (ParticipantNotFoundException $e) { + } + } $participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, $password, $result['result'], $previousParticipant); $this->session->setGuestActorIdForRoom($room->getToken(), $participant->getAttendee()->getActorId()); } diff --git a/lib/GuestManager.php b/lib/GuestManager.php index a807c596596..31f8ae6680a 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -73,13 +73,13 @@ public function sendEmailInvitation(Room $room, Participant $participant): void if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_EMAILS) { throw new \InvalidArgumentException('Cannot send email for non-email participant actor type'); } - $email = $participant->getAttendee()->getActorId(); + $email = $participant->getAttendee()->getInvitedCloudId(); $pin = $participant->getAttendee()->getPin(); $event = new BeforeEmailInvitationSentEvent($room, $participant->getAttendee()); $this->dispatcher->dispatchTyped($event); - $link = $this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()]); + $link = $this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken(), 'email' => $email, 'access' => $participant->getAttendee()->getAccessToken()]); $message = $this->mailer->createMessage(); diff --git a/lib/Manager.php b/lib/Manager.php index 0aea79bb11a..a0b4c7fe111 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -37,6 +37,7 @@ use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use OCP\Server; +use SensitiveParameter; class Manager { @@ -746,7 +747,34 @@ public function getRoomByActor(string $token, string $actorType, string $actorId * @return Room * @throws RoomNotFoundException */ - public function getRoomByRemoteAccess(string $token, string $actorType, string $actorId, string $remoteAccess, ?string $sessionId = null): Room { + public function getRoomByRemoteAccess( + string $token, + string $actorType, + string $actorId, + #[SensitiveParameter] + string $remoteAccess, + ?string $sessionId = null, + ): Room { + return $this->getRoomByAccessToken($token, $actorType, $actorId, $remoteAccess, $sessionId); + } + + /** + * @param string $token + * @param string $actorType + * @param string $actorId + * @param string $remoteAccess + * @param ?string $sessionId + * @return Room + * @throws RoomNotFoundException + */ + public function getRoomByAccessToken( + string $token, + string $actorType, + string $actorId, + #[SensitiveParameter] + string $accessToken, + ?string $sessionId = null, + ): Room { $query = $this->db->getQueryBuilder(); $helper = new SelectHelper(); $helper->selectRoomsTable($query); @@ -755,7 +783,7 @@ public function getRoomByRemoteAccess(string $token, string $actorType, string $ ->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.access_token', $query->createNamedParameter($remoteAccess)), + $query->expr()->eq('a.access_token', $query->createNamedParameter($accessToken)), $query->expr()->eq('a.room_id', 'r.id') )) ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); @@ -946,7 +974,7 @@ public function getRoomForSession(?string $userId, ?string $sessionId): Room { throw new RoomNotFoundException(); } } else { - if ($row['actor_type'] !== Attendee::ACTOR_GUESTS) { + if ($row['actor_type'] !== Attendee::ACTOR_GUESTS && $row['actor_type'] !== Attendee::ACTOR_EMAILS) { throw new RoomNotFoundException(); } } diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 9743f86e552..895c07ed93f 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -174,6 +174,7 @@ public function isReplyable(): bool { Attendee::ACTOR_USERS, Attendee::ACTOR_FEDERATED_USERS, Attendee::ACTOR_GUESTS, + Attendee::ACTOR_EMAILS, Attendee::ACTOR_BOTS, ], true); } diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index 309dcbb8ad4..f6ff48d2cf9 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -635,7 +635,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('Reminder: Deleted user in {call}') . "\n{message}"; } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); // TRANSLATORS Reminder for a message from a guest in conversation {call} $subject = $l->t('Reminder: {guest} (guest) in {call}') . "\n{message}"; } catch (ParticipantNotFoundException $e) { @@ -658,7 +658,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('Deleted user reacted with {reaction} in {call}') . "\n{message}"; } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('{guest} (guest) reacted with {reaction} in {call}') . "\n{message}"; } catch (ParticipantNotFoundException $e) { $subject = $l->t('Guest reacted with {reaction} in {call}') . "\n{message}"; @@ -673,7 +673,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('Deleted user in {call}') . "\n{message}"; } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('{guest} (guest) in {call}') . "\n{message}"; } catch (ParticipantNotFoundException $e) { $subject = $l->t('Guest in {call}') . "\n{message}"; @@ -689,7 +689,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('A deleted user sent a message in conversation {call}'); } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('{guest} (guest) sent a message in conversation {call}'); } catch (ParticipantNotFoundException $e) { $subject = $l->t('A guest sent a message in conversation {call}'); @@ -704,7 +704,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('A deleted user replied to your message in conversation {call}'); } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('{guest} (guest) replied to your message in conversation {call}'); } catch (ParticipantNotFoundException $e) { $subject = $l->t('A guest replied to your message in conversation {call}'); @@ -729,7 +729,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('Reminder: A deleted user in conversation {call}'); } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('Reminder: {guest} (guest) in conversation {call}'); } catch (ParticipantNotFoundException) { $subject = $l->t('Reminder: A guest in conversation {call}'); @@ -750,7 +750,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $subject = $l->t('A deleted user reacted with {reaction} to your message in conversation {call}'); } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); $subject = $l->t('{guest} (guest) reacted with {reaction} to your message in conversation {call}'); } catch (ParticipantNotFoundException $e) { $subject = $l->t('A guest reacted with {reaction} to your message in conversation {call}'); @@ -790,7 +790,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par } } else { try { - $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorId()); + $richSubjectParameters['guest'] = $this->getGuestParameter($room, $message->getActorType(), $message->getActorId()); if ($notification->getSubject() === 'mention_group') { $groupName = $this->groupManager->getDisplayName($subjectParameters['sourceId']) ?? $subjectParameters['sourceId']; $richSubjectParameters['group'] = [ @@ -864,12 +864,17 @@ protected function parseChatMessage(INotification $notification, Room $room, Par /** * @param Room $room + * @param Attendee::ACTOR_* $actorType * @param string $actorId * @return array * @throws ParticipantNotFoundException */ - protected function getGuestParameter(Room $room, string $actorId): array { - $participant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_GUESTS, $actorId); + protected function getGuestParameter(Room $room, string $actorType, string $actorId): array { + if (!in_array($actorType, [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { + throw new ParticipantNotFoundException('Not a guest actor type'); + } + + $participant = $this->participantService->getParticipantByActor($room, $actorType, $actorId); $name = $participant->getAttendee()->getDisplayName(); if (trim($name) === '') { throw new ParticipantNotFoundException('Empty name'); diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 3b9e5a90451..9722340e49d 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -173,6 +173,7 @@ * * @psalm-type TalkParticipant = array{ * actorId: string, + * invitedActorId?: string, * actorType: string, * attendeeId: int, * attendeePermissions: int, @@ -227,6 +228,7 @@ * * @psalm-type TalkRoom = array{ * actorId: string, + * invitedActorId?: string, * actorType: string, * attendeeId: int, * attendeePermissions: int, diff --git a/lib/Search/MessageSearch.php b/lib/Search/MessageSearch.php index 00e1ed5c6a9..748653709f2 100644 --- a/lib/Search/MessageSearch.php +++ b/lib/Search/MessageSearch.php @@ -270,7 +270,7 @@ protected function commentToSearchResultEntry(Room $room, IUser $user, IComment } $displayName = $message->getActorDisplayName(); - if ($message->getActorType() === Attendee::ACTOR_GUESTS) { + if (in_array($message->getActorType(), [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { if ($displayName === '') { $displayName = $this->l->t('Guest'); } else { diff --git a/lib/Service/BanService.php b/lib/Service/BanService.php index 6857df786a2..f7b7c7d53b9 100644 --- a/lib/Service/BanService.php +++ b/lib/Service/BanService.php @@ -49,7 +49,7 @@ public function createBan(Room $room, string $moderatorActorType, string $modera throw new \InvalidArgumentException('room'); } - if (!in_array($bannedActorType, [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS, 'ip'], true)) { + if (!in_array($bannedActorType, [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS, 'ip'], true)) { throw new \InvalidArgumentException('bannedActor'); } @@ -81,7 +81,7 @@ public function createBan(Room $room, string $moderatorActorType, string $modera /** @var ?string $displayname */ $displayname = null; - if (in_array($bannedActorType, [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS], true)) { + if (in_array($bannedActorType, [Attendee::ACTOR_USERS, Attendee::ACTOR_EMAILS, Attendee::ACTOR_GUESTS], true)) { try { $bannedParticipant = $this->participantService->getParticipantByActor($room, $bannedActorType, $bannedActorId); $displayname = $bannedParticipant->getAttendee()->getDisplayName(); @@ -120,7 +120,7 @@ public function createBan(Room $room, string $moderatorActorType, string $modera // No failure if the banned actor is not in the room yet/anymore } } - + return $this->banMapper->insert($ban); } @@ -156,14 +156,19 @@ public function throwIfActorIsBanned(Room $room, ?string $userId): void { $actorType = Attendee::ACTOR_USERS; $actorId = $userId; } else { - $actorType = Attendee::ACTOR_GUESTS; - $actorId = $this->talkSession->getGuestActorIdForRoom($room->getToken()); + $actorId = $this->talkSession->getAuthedEmailActorIdForRoom($room->getToken()); + if ($actorId !== null) { + $actorType = Attendee::ACTOR_EMAILS; + } else { + $actorId = $this->talkSession->getGuestActorIdForRoom($room->getToken()); + $actorType = Attendee::ACTOR_GUESTS; + } } if ($actorId !== null) { try { $ban = $this->banMapper->findForBannedActorAndRoom($actorType, $actorId, $room->getId()); - if ($actorType === Attendee::ACTOR_GUESTS) { + if (in_array($actorType, [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { $this->copyBanForRemoteAddress($ban, $this->request->getRemoteAddress()); } throw new ForbiddenException('actor'); @@ -175,7 +180,6 @@ public function throwIfActorIsBanned(Room $room, ?string $userId): void { return; } - $ipBans = $this->banMapper->findByRoomId($room->getId(), 'ip'); if (empty($ipBans)) { diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 68a4919c0f9..4535031b049 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -806,12 +806,7 @@ public function addCircle(Room $room, Circle $circle, array $existingParticipant $this->addUsers($room, $newParticipants, bansAlreadyChecked: true); } - /** - * @param Room $room - * @param string $email - * @return Participant - */ - public function inviteEmailAddress(Room $room, string $email): Participant { + public function inviteEmailAddress(Room $room, string $actorId, string $email): Participant { $lastMessage = 0; if ($room->getLastMessage() instanceof IComment) { $lastMessage = (int)$room->getLastMessage()->getId(); @@ -820,7 +815,12 @@ public function inviteEmailAddress(Room $room, string $email): Participant { $attendee = new Attendee(); $attendee->setRoomId($room->getId()); $attendee->setActorType(Attendee::ACTOR_EMAILS); - $attendee->setActorId($email); + $attendee->setActorId($actorId); + $attendee->setInvitedCloudId($email); + $attendee->setAccessToken($this->secureRandom->generate( + FederationManager::TOKEN_LENGTH, + ISecureRandom::CHAR_HUMAN_READABLE + )); if ($room->getSIPEnabled() !== Webinary::SIP_DISABLED && $this->talkConfig->isSIPConfigured()) { @@ -1724,13 +1724,8 @@ public function getParticipantActorIdsByActorType(Room $room, array $actorTypes, }, $attendees); } - public function getGuestCount(Room $room, ?\DateTime $maxLastJoined = null): int { - $maxLastJoinedTimestamp = null; - if ($maxLastJoined !== null) { - $maxLastJoinedTimestamp = $maxLastJoined->getTimestamp(); - } - - return $this->attendeeMapper->getActorsCountByType($room->getId(), Attendee::ACTOR_GUESTS, $maxLastJoinedTimestamp); + public function getActorsCountByType(Room $room, string $actorType, int $maxLastJoined): int { + return $this->attendeeMapper->getActorsCountByType($room->getId(), $actorType, $maxLastJoined); } /** diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index eeab3e1ab8b..86ee4104268 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -336,6 +336,9 @@ public function formatRoomV4( $roomData['unreadMention'] = $lastMention !== 0 && $lastReadMessage < $lastMention; $roomData['unreadMentionDirect'] = $lastMentionDirect !== 0 && $lastReadMessage < $lastMentionDirect; } else { + if ($attendee->getActorType() === Attendee::ACTOR_EMAILS) { + $roomData['invitedActorId'] = $attendee->getInvitedCloudId(); + } $roomData['lastReadMessage'] = $attendee->getLastReadMessage(); } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 22928bffe10..1f23fe4db3b 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -325,6 +325,7 @@ public function participantsModified(Room $room, array $sessionIds): void { $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_EMAILS && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } @@ -418,6 +419,7 @@ public function roomInCallChanged(Room $room, int $flags, array $sessionIds, boo $attendee = $participant->getAttendee(); if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS + && $attendee->getActorType() !== Attendee::ACTOR_EMAILS && $attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { continue; } diff --git a/lib/TalkSession.php b/lib/TalkSession.php index e85430d2cba..a049ed50e3b 100644 --- a/lib/TalkSession.php +++ b/lib/TalkSession.php @@ -49,6 +49,14 @@ public function removeGuestActorIdForRoom(string $token): void { $this->removeValue('spreed-guest-id', $token); } + public function getAuthedEmailActorIdForRoom(string $token): ?string { + return $this->getValue('spreed-authed-email', $token); + } + + public function setAuthedEmailActorIdForRoom(string $token, string $actorId): void { + $this->setValue('spreed-authed-email', $token, $actorId); + } + public function getFileShareTokenForRoom(string $roomToken): ?string { return $this->getValue('spreed-file-share-token', $roomToken); } diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index 80c9a4edf82..5df0b9eeb46 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -570,6 +570,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, diff --git a/openapi-federation.json b/openapi-federation.json index 4e5813a5fbf..1b2274d22c7 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -624,6 +624,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, diff --git a/openapi-full.json b/openapi-full.json index 6a8fb410e30..ee2f367ec0e 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -792,6 +792,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, @@ -1198,6 +1201,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, @@ -2141,6 +2147,7 @@ "enum": [ "users", "guests", + "emails", "ip" ], "description": "Type of actor to ban, or `ip` when banning a clients remote address" diff --git a/openapi.json b/openapi.json index 0b7ab1e1ad6..cdfc0dadfab 100644 --- a/openapi.json +++ b/openapi.json @@ -679,6 +679,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, @@ -1085,6 +1088,9 @@ "actorId": { "type": "string" }, + "invitedActorId": { + "type": "string" + }, "actorType": { "type": "string" }, @@ -2028,6 +2034,7 @@ "enum": [ "users", "guests", + "emails", "ip" ], "description": "Type of actor to ban, or `ip` when banning a clients remote address" diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Mention.vue b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Mention.vue index 3774dbfe627..2aeede5b5f1 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Mention.vue +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Mention.vue @@ -74,6 +74,9 @@ export default { isGroupMention() { return this.type === 'user-group' || this.type === 'group' }, + isEmailGuest() { + return this.type === 'guest' && this.id.startsWith('email/') + }, isMentionToGuest() { return this.type === 'guest' }, @@ -114,6 +117,8 @@ export default { : 'icon-user-forced-white' } else if (this.isGroupMention) { return 'icon-group-forced-white' + } else if (this.isEmailGuest) { + return 'icon-mail-forced-white' } else if (this.isMentionToGuest) { return 'icon-user-forced-white' } else if (!this.isMentionToAll) { diff --git a/src/types/openapi/openapi-backend-sipbridge.ts b/src/types/openapi/openapi-backend-sipbridge.ts index b195c2e9726..84e84856e36 100644 --- a/src/types/openapi/openapi-backend-sipbridge.ts +++ b/src/types/openapi/openapi-backend-sipbridge.ts @@ -249,6 +249,7 @@ export type components = { }; Room: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index b1a9507b482..b498f99f0f7 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -296,6 +296,7 @@ export type components = { }; Room: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index e81bedfc73f..d097abeb12c 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -2052,6 +2052,7 @@ export type components = { }; Participant: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; @@ -2160,6 +2161,7 @@ export type components = { }; Room: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; @@ -2545,7 +2547,7 @@ export interface operations { * @description Type of actor to ban, or `ip` when banning a clients remote address * @enum {string} */ - actorType: "users" | "guests" | "ip"; + actorType: "users" | "guests" | "emails" | "ip"; /** @description Actor ID or the IP address or range in case of type `ip` */ actorId: string; /** diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index f8b74f59bce..303985fa07d 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1533,6 +1533,7 @@ export type components = { }; Participant: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; @@ -1641,6 +1642,7 @@ export type components = { }; Room: { actorId: string; + invitedActorId?: string; actorType: string; /** Format: int64 */ attendeeId: number; @@ -2026,7 +2028,7 @@ export interface operations { * @description Type of actor to ban, or `ip` when banning a clients remote address * @enum {string} */ - actorType: "users" | "guests" | "ip"; + actorType: "users" | "guests" | "emails" | "ip"; /** @description Actor ID or the IP address or range in case of type `ip` */ actorId: string; /** diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 03c70f04797..c416f08d543 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -847,6 +847,9 @@ protected function assertAttendeeList(string $identifier, ?TableNode $formData, if (isset($expectedKeys['callId'])) { $data['callId'] = (string)$attendee['callId']; } + if (isset($expectedKeys['invitedActorId'], $attendee['invitedActorId'])) { + $data['invitedActorId'] = (string)$attendee['invitedActorId']; + } if (isset($expectedKeys['status'], $attendee['status'])) { $data['status'] = (string)$attendee['status']; } @@ -891,6 +894,9 @@ protected function assertAttendeeList(string $identifier, ?TableNode $formData, if (isset($attendee['actorId']) && str_ends_with($attendee['actorId'], '@{$REMOTE_URL}')) { $attendee['actorId'] = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $attendee['actorId']); } + if (preg_match('/^SHA256\(([a-z0-9@.+\-]+)\)$/', $attendee['actorId'], $match)) { + $attendee['actorId'] = hash('sha256', $match[1]); + } if (isset($attendee['actorId'], $attendee['actorType']) && $attendee['actorType'] === 'federated_users' && !str_contains($attendee['actorId'], '@')) { $attendee['actorId'] .= '@' . rtrim($this->localRemoteServerUrl, '/'); @@ -928,6 +934,10 @@ protected function assertAttendeeList(string $identifier, ?TableNode $formData, $attendee['participantType'] = (string)$this->mapParticipantTypeTestInput($attendee['participantType']); } + if (isset($attendee['invitedActorId']) && $attendee['invitedActorId'] === 'ABSENT') { + unset($attendee['invitedActorId']); + } + if (isset($attendee['status']) && $attendee['status'] === 'ABSENT') { unset($attendee['status']); } @@ -1382,9 +1392,10 @@ public function userResendsInvite(string $user, string $identifier, int $statusC $body = null; if ($formData instanceof TableNode) { $attendee = $formData?->getRowsHash()['attendeeId'] ?? ''; - if (isset(self::$userToAttendeeId[$identifier]['emails'][$attendee])) { + $actorId = hash('sha256', $attendee); + if (isset(self::$userToAttendeeId[$identifier]['emails'][$actorId])) { $body = [ - 'attendeeId' => self::$userToAttendeeId[$identifier]['emails'][$attendee], + 'attendeeId' => self::$userToAttendeeId[$identifier]['emails'][$actorId], ]; } elseif (str_starts_with($attendee, 'not-found')) { $body = [ diff --git a/tests/integration/features/conversation-3/invite-email.feature b/tests/integration/features/conversation-3/invite-email.feature index acaf529d3dd..edf3935e166 100644 --- a/tests/integration/features/conversation-3/invite-email.feature +++ b/tests/integration/features/conversation-3/invite-email.feature @@ -11,9 +11,9 @@ Feature: conversation/invite-email # Ref https://github.com/nextcloud/calendar/pull/5380 When user "participant1" adds email "test@example.tld" to room "room" with 200 (v4) Then user "participant1" sees the following attendees in room "room" with 200 (v4) - | participantType | inCall | actorType | actorId | - | 4 | 0 | emails | test@example.tld | - | 1 | 0 | users | participant1 | + | participantType | inCall | actorType | actorId | invitedActorId | + | 4 | 0 | emails | SHA256(test@example.tld) | test@example.tld | + | 1 | 0 | users | participant1 | ABSENT | # Reinvite all emails When user "participant1" resends invite for room "room" with 200 (v4) # Reinvite only one diff --git a/tests/integration/features/conversation-5/sip-dialin.feature b/tests/integration/features/conversation-5/sip-dialin.feature index 1d70da42113..64a8e614a68 100644 --- a/tests/integration/features/conversation-5/sip-dialin.feature +++ b/tests/integration/features/conversation-5/sip-dialin.feature @@ -27,36 +27,36 @@ Feature: conversation-2/sip-dialin # Guests don't get a PIN as they can not be recognized and are deleted on leave When user "guest" joins room "room" with 200 (v4) Then user "participant1" sees the following attendees in room "room" with 200 (v4) - | participantType | inCall | actorType | actorId | attendeePin | - | 4 | 0 | emails | test@example.tld | **PIN** | - | 4 | 0 | guests | "guest" | | - | 1 | 0 | users | participant1 | **PIN** | - | 3 | 0 | users | participant2 | **PIN** | - | 3 | 0 | users | participant3 | **PIN** | + | participantType | inCall | actorType | actorId | attendeePin | + | 4 | 0 | emails | SHA256(test@example.tld) | **PIN** | + | 4 | 0 | guests | "guest" | | + | 1 | 0 | users | participant1 | **PIN** | + | 3 | 0 | users | participant2 | **PIN** | + | 3 | 0 | users | participant3 | **PIN** | When user "participant2" sets SIP state for room "room" to "disabled" with 403 (v4) Then user "participant1" sees the following attendees in room "room" with 200 (v4) - | participantType | inCall | actorType | actorId | attendeePin | - | 4 | 0 | emails | test@example.tld | **PIN** | - | 4 | 0 | guests | "guest" | | - | 1 | 0 | users | participant1 | **PIN** | - | 3 | 0 | users | participant2 | **PIN** | - | 3 | 0 | users | participant3 | **PIN** | + | participantType | inCall | actorType | actorId | attendeePin | + | 4 | 0 | emails | SHA256(test@example.tld) | **PIN** | + | 4 | 0 | guests | "guest" | | + | 1 | 0 | users | participant1 | **PIN** | + | 3 | 0 | users | participant2 | **PIN** | + | 3 | 0 | users | participant3 | **PIN** | When user "participant1" sets SIP state for room "room" to "disabled" with 200 (v4) Then user "participant1" sees the following attendees in room "room" with 200 (v4) - | participantType | inCall | actorType | actorId | attendeePin | - | 4 | 0 | emails | test@example.tld | | - | 4 | 0 | guests | "guest" | | - | 1 | 0 | users | participant1 | | - | 3 | 0 | users | participant2 | | - | 3 | 0 | users | participant3 | | + | participantType | inCall | actorType | actorId | attendeePin | + | 4 | 0 | emails | SHA256(test@example.tld) | | + | 4 | 0 | guests | "guest" | | + | 1 | 0 | users | participant1 | | + | 3 | 0 | users | participant2 | | + | 3 | 0 | users | participant3 | | When user "participant1" sets SIP state for room "room" to "no pin" with 200 (v4) Then user "participant1" sees the following attendees in room "room" with 200 (v4) - | participantType | inCall | actorType | actorId | attendeePin | - | 4 | 0 | emails | test@example.tld | **PIN** | - | 4 | 0 | guests | "guest" | | - | 1 | 0 | users | participant1 | **PIN** | - | 3 | 0 | users | participant2 | **PIN** | - | 3 | 0 | users | participant3 | **PIN** | + | participantType | inCall | actorType | actorId | attendeePin | + | 4 | 0 | emails | SHA256(test@example.tld) | **PIN** | + | 4 | 0 | guests | "guest" | | + | 1 | 0 | users | participant1 | **PIN** | + | 3 | 0 | users | participant2 | **PIN** | + | 3 | 0 | users | participant3 | **PIN** | Scenario: Non-SIP admin tries to enable SIP Given the following "spreed" app config is set diff --git a/tests/php/Chat/Parser/SystemMessageTest.php b/tests/php/Chat/Parser/SystemMessageTest.php index ea5ef646ffa..a1609886049 100644 --- a/tests/php/Chat/Parser/SystemMessageTest.php +++ b/tests/php/Chat/Parser/SystemMessageTest.php @@ -1217,15 +1217,15 @@ public function testGetDisplayNameGroup(string $gid, bool $validGroup, string $n public static function dataGetGuest(): array { return [ - [Attendee::ACTOR_GUESTS, sha1('name')], - [Attendee::ACTOR_EMAILS, 'test@test.tld'], + [Attendee::ACTOR_GUESTS, sha1('name'), 'guest/' . sha1('name')], + [Attendee::ACTOR_EMAILS, hash('sha256', 'test@test.tld'), 'email/' . hash('sha256', 'test@test.tld')], ]; } /** * @dataProvider dataGetGuest */ - public function testGetGuest(string $attendeeType, string $actorId): void { + public function testGetGuest(string $attendeeType, string $actorId, string $expected): void { /** @var Room&MockObject $room */ $room = $this->createMock(Room::class); @@ -1237,14 +1237,14 @@ public function testGetGuest(string $attendeeType, string $actorId): void { $this->assertSame([ 'type' => 'guest', - 'id' => 'guest/' . $actorId, + 'id' => $expected, 'name' => 'name', ], self::invokePrivate($parser, 'getGuest', [$room, $attendeeType, $actorId])); // Cached call: no call to getGuestName() again $this->assertSame([ 'type' => 'guest', - 'id' => 'guest/' . $actorId, + 'id' => $expected, 'name' => 'name', ], self::invokePrivate($parser, 'getGuest', [$room, $attendeeType, $actorId])); } diff --git a/tests/php/Chat/SystemMessage/ListenerTest.php b/tests/php/Chat/SystemMessage/ListenerTest.php index acc1fddee2f..77cbccbcf19 100644 --- a/tests/php/Chat/SystemMessage/ListenerTest.php +++ b/tests/php/Chat/SystemMessage/ListenerTest.php @@ -259,7 +259,7 @@ public function testAfterUsersAdd(int $roomType, string $objectType, array $part public static function dataParticipantTypeChange(): array { return [ [ - Attendee::ACTOR_EMAILS, + Attendee::ACTOR_GROUPS, Participant::USER, Participant::MODERATOR, [], @@ -280,13 +280,25 @@ public static function dataParticipantTypeChange(): array { Attendee::ACTOR_GUESTS, Participant::GUEST, Participant::GUEST_MODERATOR, - [['message' => 'guest_moderator_promoted', 'parameters' => ['session' => 'bob_participant']]], + [['message' => 'guest_moderator_promoted', 'parameters' => ['type' => 'guests', 'id' => 'bob_participant']]], ], [ Attendee::ACTOR_GUESTS, Participant::GUEST_MODERATOR, Participant::GUEST, - [['message' => 'guest_moderator_demoted', 'parameters' => ['session' => 'bob_participant']]], + [['message' => 'guest_moderator_demoted', 'parameters' => ['type' => 'guests', 'id' => 'bob_participant']]], + ], + [ + Attendee::ACTOR_EMAILS, + Participant::GUEST, + Participant::GUEST_MODERATOR, + [['message' => 'guest_moderator_promoted', 'parameters' => ['type' => 'emails', 'id' => 'bob_participant']]], + ], + [ + Attendee::ACTOR_EMAILS, + Participant::GUEST_MODERATOR, + Participant::GUEST, + [['message' => 'guest_moderator_demoted', 'parameters' => ['type' => 'emails', 'id' => 'bob_participant']]], ], [ Attendee::ACTOR_USERS, diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php index 1594106daf9..f5d3d8e1217 100644 --- a/tests/php/Notification/NotifierTest.php +++ b/tests/php/Notification/NotifierTest.php @@ -921,22 +921,26 @@ public function testPrepareChatMessage(string $subject, int $roomType, array $su $comment->expects($this->any()) ->method('getActorId') ->willReturn('random-hash'); + $comment->expects($this->any()) + ->method('getActorType') + ->willReturn(Attendee::ACTOR_GUESTS); $this->commentsManager->expects($this->once()) ->method('get') ->with('23') ->willReturn($comment); if (is_string($guestName)) { + $participant2 = $this->createMock(Participant::class); $this->participantService->method('getParticipantByActor') ->with($room, Attendee::ACTOR_GUESTS, 'random-hash') - ->willReturn($participant); + ->willReturn($participant2); $attendee = Attendee::fromRow([ 'actor_type' => 'guests', 'actor_id' => 'random-hash', 'display_name' => $guestName, ]); - $participant->method('getAttendee') + $participant2->method('getAttendee') ->willReturn($attendee); } else { $this->participantService->method('getParticipantByActor') @@ -968,6 +972,9 @@ public function testPrepareChatMessage(string $subject, int $roomType, array $su $chatMessage->expects($this->any()) ->method('getActorId') ->willReturn('random-hash'); + $chatMessage->expects($this->any()) + ->method('getActorType') + ->willReturn(Attendee::ACTOR_GUESTS); $this->messageParser->expects($this->once()) ->method('createMessage')