From 5e276f5dc2a2e02794e3a852b93e00b19d22b390 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 9 Oct 2024 06:34:06 +0200 Subject: [PATCH 01/15] feat(email): Recognize guests invited via email Signed-off-by: Joas Schilling --- lib/Chat/MessageParser.php | 2 +- lib/Controller/PageController.php | 92 +++++++++++++++++++++++++++--- lib/Controller/RoomController.php | 12 +++- lib/GuestManager.php | 2 +- lib/Manager.php | 34 ++++++++++- lib/Service/ParticipantService.php | 4 ++ lib/TalkSession.php | 8 +++ 7 files changed, 140 insertions(+), 14 deletions(-) diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 422fcf15195..cc724752126 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -142,7 +142,7 @@ protected function getActorInformation(Message $message, string $actorType, stri $displayName = $this->guestNames[$actorId]; } else { try { - $participant = $this->participantService->getParticipantByActor($message->getRoom(), Attendee::ACTOR_GUESTS, $actorId); + $participant = $this->participantService->getParticipantByActor($message->getRoom(), str_contains($actorId, '@') ? Attendee::ACTOR_EMAILS : Attendee::ACTOR_GUESTS, $actorId); $displayName = $participant->getAttendee()->getDisplayName(); } catch (ParticipantNotFoundException) { } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index cf6b64b9664..015c3bd430e 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,62 @@ 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 { + $this->manager->getRoomByAccessToken( + $token, + Attendee::ACTOR_EMAILS, + $email, + $accessToken, + ); + $this->talkSession->renewSessionId(); + $this->talkSession->setAuthedEmailActorIdForRoom($token, $email); + } 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..e945e02a370 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1076,6 +1076,8 @@ 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) { + $result['displayName'] = $participant->getAttendee()->getDisplayName(); } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { if ($participant->getSession() instanceof Session && $participant->getSession()->getLastPing() <= $maxPingAge) { $this->participantService->leaveRoomAsSession($this->room, $participant); @@ -1643,8 +1645,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 +1663,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..02c9c2c116c 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -79,7 +79,7 @@ public function sendEmailInvitation(Room $room, Participant $participant): void $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/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 68a4919c0f9..35ef9e8c4a9 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -821,6 +821,10 @@ public function inviteEmailAddress(Room $room, string $email): Participant { $attendee->setRoomId($room->getId()); $attendee->setActorType(Attendee::ACTOR_EMAILS); $attendee->setActorId($email); + $attendee->setAccessToken($this->secureRandom->generate( + FederationManager::TOKEN_LENGTH, + ISecureRandom::CHAR_HUMAN_READABLE + )); if ($room->getSIPEnabled() !== Webinary::SIP_DISABLED && $this->talkConfig->isSIPConfigured()) { 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); } From 70562c21f7af8d7603316002952fe02fb5b94a8a Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 15 Oct 2024 14:44:53 +0200 Subject: [PATCH 02/15] fix(email): Use a hash for the actor ID for better GDPR compliance Signed-off-by: Joas Schilling --- lib/Controller/PageController.php | 5 +++-- lib/Controller/RoomController.php | 12 ++++++++++-- lib/GuestManager.php | 2 +- lib/Service/ParticipantService.php | 10 +++------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 015c3bd430e..0d3e6b5143d 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -436,14 +436,15 @@ protected function invitedEmail( string $accessToken, ): Response { try { + $actorId = hash('sha256', $email); $this->manager->getRoomByAccessToken( $token, Attendee::ACTOR_EMAILS, - $email, + $actorId, $accessToken, ); $this->talkSession->renewSessionId(); - $this->talkSession->setAuthedEmailActorIdForRoom($token, $email); + $this->talkSession->setAuthedEmailActorIdForRoom($token, $actorId); } catch (RoomNotFoundException) { $redirectUrl = $this->url->linkToRoute('spreed.Page.index'); if ($token) { diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index e945e02a370..aedaf5be506 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1078,6 +1078,12 @@ protected function formatParticipantList(array $participants, bool $includeStatu $result['displayName'] = $participant->getAttendee()->getDisplayName(); } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_EMAILS) { $result['displayName'] = $participant->getAttendee()->getDisplayName(); + if ($this->participant->hasModeratorPermissions()) { + $result['status'] = IUserStatus::OFFLINE; + $result['statusIcon'] = null; + $result['statusMessage'] = $participant->getAttendee()->getInvitedCloudId(); + $result['statusClearAt'] = null; + } } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { if ($participant->getSession() instanceof Session && $participant->getSession()->getLastPing() <= $maxPingAge) { $this->participantService->leaveRoomAsSession($this->room, $participant); @@ -1197,10 +1203,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); } diff --git a/lib/GuestManager.php b/lib/GuestManager.php index 02c9c2c116c..31f8ae6680a 100644 --- a/lib/GuestManager.php +++ b/lib/GuestManager.php @@ -73,7 +73,7 @@ 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()); diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 35ef9e8c4a9..c59fee666e7 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,8 @@ 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 From 6af6d5c9f7fa3b71caeaf2960765e0eff8609b42 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 15 Oct 2024 14:54:44 +0200 Subject: [PATCH 03/15] fix(email): Treat email guests like normal guests in some cases Signed-off-by: Joas Schilling --- lib/Chat/MessageParser.php | 11 ++++---- lib/Chat/SystemMessage/Listener.php | 6 ++++- .../Reference/TalkReferenceProvider.php | 2 +- lib/Controller/ChatController.php | 4 ++- lib/Model/Message.php | 1 + lib/Notification/Notifier.php | 25 +++++++++++-------- lib/Search/MessageSearch.php | 2 +- lib/Signaling/BackendNotifier.php | 2 ++ tests/php/Chat/SystemMessage/ListenerTest.php | 2 +- tests/php/Notification/NotifierTest.php | 11 ++++++-- 10 files changed, 44 insertions(+), 22 deletions(-) diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index cc724752126..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(), str_contains($actorId, '@') ? Attendee::ACTOR_EMAILS : 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/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 702ab950ac5..3110a6a6173 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; } 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/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/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/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/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/tests/php/Chat/SystemMessage/ListenerTest.php b/tests/php/Chat/SystemMessage/ListenerTest.php index 12ef958bc4d..d01f6b469d1 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, [], 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') From 63adde2a5910bc0211573ce6dee8b3d84731755c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 17 Oct 2024 13:58:18 +0200 Subject: [PATCH 04/15] fix(email): Allow mentioning email guests Signed-off-by: Joas Schilling --- lib/Chat/AutoComplete/SearchPlugin.php | 62 ++++++++++++++++++++++++++ lib/Chat/Parser/UserMention.php | 15 +++++++ 2 files changed, 77 insertions(+) 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/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'], From 3e0d4c037409e525a308f7b084f0c76884b42efa Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 17 Oct 2024 16:05:25 +0200 Subject: [PATCH 05/15] fix(email): Fix handling of email guests in system messages Signed-off-by: Joas Schilling --- lib/Chat/Parser/SystemMessage.php | 25 ++++++++++++++++--- lib/Chat/SystemMessage/Listener.php | 4 +-- tests/php/Chat/Parser/SystemMessageTest.php | 10 ++++---- tests/php/Chat/SystemMessage/ListenerTest.php | 16 ++++++++++-- 4 files changed, 43 insertions(+), 12 deletions(-) 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/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 3110a6a6173..5e1d71c3ab0 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -328,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/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 d01f6b469d1..ed833a8588d 100644 --- a/tests/php/Chat/SystemMessage/ListenerTest.php +++ b/tests/php/Chat/SystemMessage/ListenerTest.php @@ -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, From 3ba5adfc4aca16dc7009a40a1fb24f64a851aa45 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 17 Oct 2024 16:23:02 +0200 Subject: [PATCH 06/15] feat(email): Handle mentioned email guests Signed-off-by: Joas Schilling --- css/icons.css | 28 +++++++++++++++---- .../Message/MessagePart/Mention.vue | 5 ++++ 2 files changed, 28 insertions(+), 5 deletions(-) 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/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) { From de5200a83698ccdbb034fc7f3c50c1fdb5b0cd5b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 10:56:16 +0200 Subject: [PATCH 07/15] feat(emails): Allow banning email guests Signed-off-by: Joas Schilling --- lib/Controller/BanController.php | 2 +- lib/Service/BanService.php | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) 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/Service/BanService.php b/lib/Service/BanService.php index 43e75cc437d..3b98784f6b4 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'); @@ -171,11 +176,10 @@ public function throwIfActorIsBanned(Room $room, ?string $userId): void { } } - if ($actorType !== Attendee::ACTOR_GUESTS) { + if (!in_array($actorType, [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { return; } - $ipBans = $this->banMapper->findByRoomId($room->getId(), 'ip'); if (empty($ipBans)) { From b9c3a2b7defb96cbfe2c4637c95a0199d5ee5704 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 11:05:32 +0200 Subject: [PATCH 08/15] fix(emails): Expire the session but not the attendees for email guests Signed-off-by: Joas Schilling --- lib/Controller/RoomController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index aedaf5be506..3689328f2f8 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); } @@ -1077,6 +1078,9 @@ protected function formatParticipantList(array $participants, bool $includeStatu } 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()) { $result['status'] = IUserStatus::OFFLINE; From 88e7cfc56435abc662916bd84ed5e15c6d7633ff Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 11:11:22 +0200 Subject: [PATCH 09/15] fix(calls): Count email guests as guests in the call summary Signed-off-by: Joas Schilling --- lib/Activity/Listener.php | 3 ++- lib/Service/ParticipantService.php | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) 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/Service/ParticipantService.php b/lib/Service/ParticipantService.php index c59fee666e7..4535031b049 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -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); } /** From b8c76b0584c80825c67000f2e0609ac86ebc19e2 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 11:47:05 +0200 Subject: [PATCH 10/15] docs(bots): Clarify the random guest actor id Signed-off-by: Joas Schilling --- docs/bots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). | From 4cb26cc724758a02ec4c355d9684942aefbcfa5c Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 15:40:48 +0200 Subject: [PATCH 11/15] fix(emails): Use dedicated key in API response Signed-off-by: Joas Schilling --- lib/Controller/RoomController.php | 7 ++----- lib/ResponseDefinitions.php | 2 ++ lib/Service/RoomFormatter.php | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 3689328f2f8..17821e88409 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1082,11 +1082,8 @@ protected function formatParticipantList(array $participants, bool $includeStatu $this->participantService->leaveRoomAsSession($this->room, $participant); } $result['displayName'] = $participant->getAttendee()->getDisplayName(); - if ($this->participant->hasModeratorPermissions()) { - $result['status'] = IUserStatus::OFFLINE; - $result['statusIcon'] = null; - $result['statusMessage'] = $participant->getAttendee()->getInvitedCloudId(); - $result['statusClearAt'] = null; + 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) { 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/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(); } From 4874ab52852d75d9b2a496676af014f651737db3 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 16:04:03 +0200 Subject: [PATCH 12/15] fix(emails): Fix integration tests assuming the email is the actorId Signed-off-by: Joas Schilling --- .../features/bootstrap/FeatureContext.php | 15 +++++- .../conversation-3/invite-email.feature | 6 +-- .../conversation-5/sip-dialin.feature | 48 +++++++++---------- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 84c4cc85314..965e85e2e10 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 From 13f8f7101fa1b1fcbdc97a27ea3e5b82b5ae68d0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 18 Oct 2024 16:29:03 +0200 Subject: [PATCH 13/15] chore(openapi): Recompile openapi Signed-off-by: Joas Schilling --- openapi-backend-sipbridge.json | 3 +++ openapi-federation.json | 3 +++ openapi-full.json | 7 +++++++ openapi.json | 7 +++++++ src/types/openapi/openapi-backend-sipbridge.ts | 1 + src/types/openapi/openapi-federation.ts | 1 + src/types/openapi/openapi-full.ts | 4 +++- src/types/openapi/openapi.ts | 4 +++- 8 files changed, 28 insertions(+), 2 deletions(-) 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 279744b5ed8..60054b6c5fa 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 1ac6ced0651..c14042aad27 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/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 883a78aab87..3cddddd4be9 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 2b877740f83..ee0fa97d878 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; /** From 644c3fa946a26986fa6bf97486c522c2bfeffc08 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 23 Oct 2024 06:23:48 +0200 Subject: [PATCH 14/15] fix(ban): Allow email guests to bypass bans on their IP Signed-off-by: Joas Schilling --- lib/Service/BanService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/BanService.php b/lib/Service/BanService.php index 3b98784f6b4..883f1629dea 100644 --- a/lib/Service/BanService.php +++ b/lib/Service/BanService.php @@ -176,7 +176,7 @@ public function throwIfActorIsBanned(Room $room, ?string $userId): void { } } - if (!in_array($actorType, [Attendee::ACTOR_GUESTS, Attendee::ACTOR_EMAILS], true)) { + if ($actorType !== Attendee::ACTOR_GUESTS) { return; } From eae90ddce4e4c8d7093e5ffb7702a4250a228f58 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 23 Oct 2024 06:57:23 +0200 Subject: [PATCH 15/15] feat(email): Allow inviting email guests to private conversations Signed-off-by: Joas Schilling --- lib/Controller/RoomController.php | 9 +-------- lib/Service/RoomService.php | 6 ++++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 17821e88409..e3af9a7a85d 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1197,13 +1197,6 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u $this->participantService->addCircle($this->room, $circle, $participants); } elseif ($source === 'emails') { - $data = []; - try { - $this->roomService->setType($this->room, Room::TYPE_PUBLIC); - $data = ['type' => $this->room->getType()]; - } catch (TypeException) { - } - $email = $newParticipant; $actorId = hash('sha256', $email); try { @@ -1213,7 +1206,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u $this->guestManager->sendEmailInvitation($this->room, $participant); } - return new DataResponse($data); + return new DataResponse([]); } elseif ($source === 'federated_users') { if (!$this->talkConfig->isFederationEnabled()) { return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED); diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index a813e906596..2d01b0b6bae 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -531,11 +531,13 @@ public function setType(Room $room, int $newType, bool $allowSwitchingOneToOne = $room->setType($newType); if ($oldType === Room::TYPE_PUBLIC) { - // Kick all guests and users that were not invited + // Kick all guests that are not email invited + // and all users that joined the public link $delete = $this->db->getQueryBuilder(); $delete->delete('talk_attendees') ->where($delete->expr()->eq('room_id', $delete->createNamedParameter($room->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY))); + ->andWhere($delete->expr()->in('participant_type', $delete->createNamedParameter([Participant::GUEST, Participant::GUEST_MODERATOR, Participant::USER_SELF_JOINED], IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($delete->expr()->neq('actor_type', $delete->createNamedParameter(Attendee::ACTOR_EMAILS, IQueryBuilder::PARAM_INT))); $delete->executeStatement(); }