From 4fc2f20c6d0145f39ce258a5100d9ab78a5af398 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Tue, 21 Jan 2025 18:17:36 +0100 Subject: [PATCH 1/2] feat(mentions): allow teams to be mentioned Signed-off-by: Anna Larch --- lib/Chat/AutoComplete/SearchPlugin.php | 67 ++++++++++++++++ lib/Chat/Notifier.php | 52 +++++++++++++ lib/Chat/Parser/UserMention.php | 57 +++++++++++++- lib/Controller/RoomController.php | 4 +- lib/Notification/Notifier.php | 65 +++++++++++++++- lib/Service/ParticipantService.php | 35 ++++++++- openapi-full.json | 3 +- openapi.json | 3 +- src/constants.ts | 1 + src/types/openapi/openapi-full.ts | 2 +- src/types/openapi/openapi.ts | 2 +- .../features/bootstrap/FeatureContext.php | 42 ++++++++-- .../features/chat-1/notifications.feature | 76 +++++++++++++++++++ .../features/chat-2/mentions.feature | 14 +++- tests/php/Chat/Parser/UserMentionTest.php | 4 + tests/php/Notification/NotifierTest.php | 4 + 16 files changed, 410 insertions(+), 21 deletions(-) diff --git a/lib/Chat/AutoComplete/SearchPlugin.php b/lib/Chat/AutoComplete/SearchPlugin.php index 59863819732..134d8163474 100644 --- a/lib/Chat/AutoComplete/SearchPlugin.php +++ b/lib/Chat/AutoComplete/SearchPlugin.php @@ -71,6 +71,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $emailAttendees = []; /** @var list $guestAttendees */ $guestAttendees = []; + /** @var array $teamIds */ + $teamIds = []; if ($this->room->getType() === Room::TYPE_ONE_TO_ONE) { // Add potential leavers of one-to-one rooms again. @@ -92,6 +94,8 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $cloudIds[$attendee->getActorId()] = $attendee->getDisplayName(); } elseif ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { $groupIds[$attendee->getActorId()] = $attendee->getDisplayName(); + } elseif ($attendee->getActorType() === Attendee::ACTOR_CIRCLES) { + $teamIds[$attendee->getActorId()] = $attendee->getDisplayName(); } } } @@ -101,6 +105,7 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $this->searchGuests($search, $guestAttendees, $searchResult); $this->searchEmails($search, $emailAttendees, $searchResult); $this->searchFederatedUsers($search, $cloudIds, $searchResult); + $this->searchTeams($search, $teamIds, $searchResult); return false; } @@ -352,6 +357,58 @@ protected function searchEmails(string $search, array $attendees, ISearchResult $searchResult->addResultSet($type, $matches, $exactMatches); } + /** + * @param string $search + * @param array $attendees + * @param ISearchResult $searchResult + */ + /** + * @param array $teams + */ + protected function searchTeams(string $search, array $teams, ISearchResult $searchResult): void { + $search = strtolower($search); + + $type = new SearchResultType('teams'); + + $matches = $exactMatches = []; + foreach ($teams as $teamId => $displayName) { + if ($displayName === '') { + continue; + } + + $teamId = (string)$teamId; + if ($searchResult->hasResult($type, $teamId)) { + continue; + } + + if ($search === '') { + $matches[] = $this->createTeamResult($teamId, $displayName); + continue; + } + + if (strtolower($teamId) === $search) { + $exactMatches[] = $this->createTeamResult($teamId, $displayName); + continue; + } + + if (stripos($teamId, $search) !== false) { + $matches[] = $this->createTeamResult($teamId, $displayName); + continue; + } + + if (strtolower($displayName) === $search) { + $exactMatches[] = $this->createTeamResult($teamId, $displayName); + continue; + } + + if (stripos($displayName, $search) !== false) { + $matches[] = $this->createTeamResult($teamId, $displayName); + } + } + + $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; @@ -401,4 +458,14 @@ protected function createEmailResult(string $actorId, string $name, ?string $ema return $data; } + + protected function createTeamResult(string $actorId, string $name): array { + return [ + 'label' => $name, + 'value' => [ + 'shareType' => 'team', + 'shareWith' => 'team/' . $actorId, + ], + ]; + } } diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index 2f8d462678b..16cf8e8aa50 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -112,6 +112,7 @@ public function notifyMentionedUsers(Room $chat, IComment $comment, array $alrea public function getUsersToNotify(Room $chat, IComment $comment, array $alreadyNotifiedUsers, ?Participant $participant = null): array { $usersToNotify = $this->getMentionedUsers($comment); $usersToNotify = $this->getMentionedGroupMembers($chat, $comment, $usersToNotify); + $usersToNotify = $this->getMentionedTeamMembers($chat, $comment, $usersToNotify); $usersToNotify = $this->addMentionAllToList($chat, $usersToNotify, $participant); $usersToNotify = $this->removeAlreadyNotifiedUsers($usersToNotify, $alreadyNotifiedUsers); @@ -532,6 +533,57 @@ private function getMentionedGroupMembers(Room $chat, IComment $comment, array $ return $list; } + /** + * @param Room $chat + * @param IComment $comment + * @param array $list + * @psalm-param array $list + * @return array[] + * @psalm-return array + */ + private function getMentionedTeamMembers(Room $chat, IComment $comment, array $list): array { + $mentions = $comment->getMentions(); + + if (empty($mentions)) { + return []; + } + + $alreadyMentionedUserIds = array_filter( + array_map(static fn (array $entry) => $entry['type'] === Attendee::ACTOR_USERS ? $entry['id'] : null, $list), + static fn ($userId) => $userId !== null + ); + $alreadyMentionedUserIds = array_flip($alreadyMentionedUserIds); + + foreach ($mentions as $mention) { + if ($mention['type'] !== 'team') { + continue; + } + + try { + $this->participantService->getParticipantByActor($chat, Attendee::ACTOR_CIRCLES, $mention['id']); + } catch (ParticipantNotFoundException) { + continue; + } + + $members = $this->participantService->getCircleMembers($mention['id']); + if (empty($members)) { + continue; + } + + foreach ($members as $member) { + $list[] = [ + 'id' => $member->getUserId(), + 'type' => Attendee::ACTOR_USERS, + 'reason' => 'team', + 'sourceId' => $mention['id'], + ]; + $alreadyMentionedUserIds[$member->getUserId()] = true; + } + } + + return $list; + } + /** * Creates a notification for the given chat message comment and mentioned * user ID. diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index 5244c4faaf3..233915f70b1 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -8,6 +8,7 @@ namespace OCA\Talk\Chat\Parser; +use OCA\Circles\CirclesManager; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Events\MessageParseEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; @@ -17,6 +18,7 @@ use OCA\Talk\Room; use OCA\Talk\Service\AvatarService; use OCA\Talk\Service\ParticipantService; +use OCP\App\IAppManager; use OCP\Comments\ICommentsManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -25,14 +27,20 @@ use OCP\IGroupManager; use OCP\IL10N; use OCP\IUserManager; +use OCP\Server; /** * Helper class to get a rich message from a plain text message. * @template-implements IEventListener */ class UserMention implements IEventListener { + /** @var array */ + protected array $circleNames = []; + /** @var array */ + protected array $circleLinks = []; public function __construct( + protected IAppManager $appManager, protected ICommentsManager $commentsManager, protected IUserManager $userManager, protected IGroupManager $groupManager, @@ -117,7 +125,7 @@ protected function parseMessage(Message $chatMessage): void { $mention['type'] === 'email' || $mention['type'] === 'group' || // $mention['type'] === 'federated_group' || - // $mention['type'] === 'team' || + $mention['type'] === 'team' || // $mention['type'] === 'federated_team' || $mention['type'] === 'federated_user') { $search = $mention['type'] . '/' . $mention['id']; @@ -135,7 +143,7 @@ protected function parseMessage(Message $chatMessage): void { && !str_starts_with($search, 'email/') && !str_starts_with($search, 'group/') // && !str_starts_with($search, 'federated_group/') - // && !str_starts_with($search, 'team/') + && !str_starts_with($search, 'team/') // && !str_starts_with($search, 'federated_team/') && !str_starts_with($search, 'federated_user/')) { $message = str_replace('@' . $search, '{' . $mentionParameterId . '}', $message); @@ -213,6 +221,8 @@ protected function parseMessage(Message $chatMessage): void { 'id' => $mention['id'], 'name' => $displayName, ]; + } elseif ($mention['type'] === 'team') { + $messageParameters[$mentionParameterId] = $this->getCircle($mention['id']); } else { try { $displayName = $this->commentsManager->resolveDisplayName($mention['type'], $mention['id']); @@ -256,4 +266,47 @@ protected function getRoomType(Room $room): string { throw new \InvalidArgumentException('Unknown room type'); } } + + protected function getCircle(string $circleId): array { + if (!$this->appManager->isEnabledForUser('circles')) { + return [ + 'type' => 'highlight', + 'id' => $circleId, + 'name' => $circleId, + ]; + } + + if (!isset($this->circleNames[$circleId])) { + $this->loadCircleDetails($circleId); + } + + if (!isset($this->circleNames[$circleId])) { + return [ + 'type' => 'highlight', + 'id' => $circleId, + 'name' => $circleId, + ]; + } + + return [ + 'type' => 'circle', + 'id' => $circleId, + 'name' => $this->circleNames[$circleId], + 'link' => $this->circleLinks[$circleId], + ]; + } + + protected function loadCircleDetails(string $circleId): void { + try { + $circlesManager = Server::get(CirclesManager::class); + $circlesManager->startSuperSession(); + $circle = $circlesManager->getCircle($circleId); + + $this->circleNames[$circleId] = $circle->getDisplayName(); + $this->circleLinks[$circleId] = $circle->getUrl(); + } catch (\Exception) { + } finally { + $circlesManager?->stopSession(); + } + } } diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index 33bf1f8dd5b..1a7ed2f3df8 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -1147,7 +1147,7 @@ protected function formatParticipantList(array $participants, bool $includeStatu * Add a participant to a room * * @param string $newParticipant New participant - * @param 'users'|'groups'|'circles'|'emails'|'federated_users'|'phones' $source Source of the participant + * @param 'users'|'groups'|'circles'|'emails'|'federated_users'|'phones'|'teams' $source Source of the participant * @return DataResponse|DataResponse * * 200: Participant successfully added @@ -1215,7 +1215,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u } $this->participantService->addGroup($this->room, $group, $participants); - } elseif ($source === 'circles') { + } elseif ($source === 'circles' || $source === 'teams') { if (!$this->appManager->isEnabledForUser('circles')) { return new DataResponse(['error' => 'new-participant'], Http::STATUS_BAD_REQUEST); } diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index aff49bd1998..c1de40a9c13 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -8,6 +8,7 @@ namespace OCA\Talk\Notification; +use OCA\Circles\CirclesManager; use OCA\FederatedFileSharing\AddressHandler; use OCA\Talk\AppInfo\Application; use OCA\Talk\Chat\ChatManager; @@ -28,6 +29,7 @@ use OCA\Talk\Service\AvatarService; use OCA\Talk\Service\ParticipantService; use OCA\Talk\Webinary; +use OCP\App\IAppManager; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\ICommentsManager; @@ -47,6 +49,7 @@ use OCP\Notification\INotifier; use OCP\Notification\UnknownNotificationException; use OCP\RichObjectStrings\Definitions; +use OCP\Server; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; @@ -57,12 +60,17 @@ class Notifier implements INotifier { protected array $rooms = []; /** @var Participant[][] */ protected array $participants = []; + /** @var array */ + protected array $circleNames = []; + /** @var array */ + protected array $circleLinks = []; protected ICommentsManager $commentManager; public function __construct( protected IFactory $lFactory, protected IURLGenerator $url, protected Config $config, + protected IAppManager $appManager, protected IUserManager $userManager, protected IGroupManager $groupManager, protected GuestManager $guestManager, @@ -262,7 +270,7 @@ public function prepare(INotification $notification, string $languageCode): INot } return $this->parseCall($notification, $room, $l); } - if ($subject === 'reply' || $subject === 'mention' || $subject === 'mention_direct' || $subject === 'mention_group' || $subject === 'mention_all' || $subject === 'chat' || $subject === 'reaction' || $subject === 'reminder') { + if ($subject === 'reply' || $subject === 'mention' || $subject === 'mention_direct' || $subject === 'mention_group' || $subject === 'mention_team' || $subject === 'mention_all' || $subject === 'chat' || $subject === 'reaction' || $subject === 'reminder') { if ($participant instanceof Participant && $room->getLobbyState() !== Webinary::LOBBY_NONE && !($participant->getPermissions() & Attendee::PERMISSIONS_LOBBY_IGNORE)) { @@ -774,6 +782,9 @@ protected function parseChatMessage(INotification $notification, Room $room, Par ]; $subject = $l->t('{user} mentioned group {group} in conversation {call}'); + } elseif ($notification->getSubject() === 'mention_team') { + $richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']); + $subject = $l->t('{user} mentioned team {team} in conversation {call}'); } elseif ($notification->getSubject() === 'mention_all') { $subject = $l->t('{user} mentioned everyone in conversation {call}'); } else { @@ -789,6 +800,9 @@ protected function parseChatMessage(INotification $notification, Room $room, Par ]; $subject = $l->t('A deleted user mentioned group {group} in conversation {call}'); + } elseif ($notification->getSubject() === 'mention_team') { + $richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']); + $subject = $l->t('A deleted user mentioned team {team} in conversation {call}'); } elseif ($notification->getSubject() === 'mention_all') { $subject = $l->t('A deleted user mentioned everyone in conversation {call}'); } else { @@ -806,6 +820,9 @@ protected function parseChatMessage(INotification $notification, Room $room, Par ]; $subject = $l->t('{guest} (guest) mentioned group {group} in conversation {call}'); + } elseif ($notification->getSubject() === 'mention_team') { + $richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']); + $subject = $l->t('{guest} (guest) mentioned team {team} in conversation {call}'); } elseif ($notification->getSubject() === 'mention_all') { $subject = $l->t('{guest} (guest) mentioned everyone in conversation {call}'); } else { @@ -821,6 +838,9 @@ protected function parseChatMessage(INotification $notification, Room $room, Par ]; $subject = $l->t('A guest mentioned group {group} in conversation {call}'); + } elseif ($notification->getSubject() === 'mention_team') { + $richSubjectParameters['team'] = $this->getCircle($subjectParameters['sourceId']); + $subject = $l->t('A guest mentioned team {team} in conversation {call}'); } elseif ($notification->getSubject() === 'mention_all') { $subject = $l->t('A guest mentioned everyone in conversation {call}'); } else { @@ -1247,4 +1267,47 @@ protected function parseCertificateExpiration(INotification $notification, IL10N return $notification; } + + protected function getCircle(string $circleId): array { + if (!$this->appManager->isEnabledForUser('circles')) { + return [ + 'type' => 'highlight', + 'id' => $circleId, + 'name' => $circleId, + ]; + } + + if (!isset($this->circleNames[$circleId])) { + $this->loadCircleDetails($circleId); + } + + if (!isset($this->circleNames[$circleId])) { + return [ + 'type' => 'highlight', + 'id' => $circleId, + 'name' => $circleId, + ]; + } + + return [ + 'type' => 'circle', + 'id' => $circleId, + 'name' => $this->circleNames[$circleId], + 'link' => $this->circleLinks[$circleId], + ]; + } + + protected function loadCircleDetails(string $circleId): void { + try { + $circlesManager = Server::get(CirclesManager::class); + $circlesManager->startSuperSession(); + $circle = $circlesManager->getCircle($circleId); + + $this->circleNames[$circleId] = $circle->getDisplayName(); + $this->circleLinks[$circleId] = $circle->getUrl(); + } catch (\Exception) { + } finally { + $circlesManager?->stopSession(); + } + } } diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 503e4ac210f..144f5bec1cd 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -719,16 +719,43 @@ public function getCircle(string $circleId, string $userId): Circle { $circlesManager->startSession($federatedUser); try { - $circle = $circlesManager->getCircle($circleId); - $circlesManager->stopSession(); - return $circle; + return $circlesManager->getCircle($circleId); } catch (\Exception $e) { + } finally { + $circlesManager->stopSession(); } - $circlesManager->stopSession(); throw new ParticipantNotFoundException('Circle not found or not a member'); } + /** + * @param string $circleId + * @param string $userId + * @return Member[] + * @throws ParticipantNotFoundException + */ + public function getCircleMembers(string $circleId): array { + try { + $circlesManager = Server::get(CirclesManager::class); + } catch (\Exception) { + throw new ParticipantNotFoundException('Circle not found'); + } + + $circlesManager->startSuperSession(); + try { + $circle = $circlesManager->getCircle($circleId); + } catch (\Exception) { + throw new ParticipantNotFoundException('Circle not found'); + } finally { + $circlesManager->stopSession(); + } + + $members = $circle->getInheritedMembers(); + return array_filter($members, static function (Member $member) { + return $member->getUserType() === Member::TYPE_USER; + }); + } + /** * @param Room $room * @param Circle $circle diff --git a/openapi-full.json b/openapi-full.json index 75e2ec32855..76428dfacc3 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -13791,7 +13791,8 @@ "circles", "emails", "federated_users", - "phones" + "phones", + "teams" ], "description": "Source of the participant" } diff --git a/openapi.json b/openapi.json index f044f5f76d7..fcc98304250 100644 --- a/openapi.json +++ b/openapi.json @@ -13949,7 +13949,8 @@ "circles", "emails", "federated_users", - "phones" + "phones", + "teams" ], "description": "Source of the participant" } diff --git a/src/constants.ts b/src/constants.ts index bf18953c48d..b76df332dda 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -328,6 +328,7 @@ export const MENTION = { // Parsed to another types FEDERATED_USER: 'federated_user', GROUP: 'group', + TEAM: 'team', }, } diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 307cf1d73bf..c18d37bc5ce 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -7227,7 +7227,7 @@ export interface operations { * @default users * @enum {string} */ - source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones"; + source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones" | "teams"; }; }; }; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index cd6ae0b7269..a624393f9a0 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -6811,7 +6811,7 @@ export interface operations { * @default users * @enum {string} */ - source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones"; + source?: "users" | "groups" | "circles" | "emails" | "federated_users" | "phones" | "teams"; }; }; }; diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index b2f1b6ce489..49553e2a45e 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1970,7 +1970,7 @@ public function userChangesListableScopeOfTheRoom(string $user, string $identifi } /** - * @Then /^user "([^"]*)" adds (user|group|email|circle|federated_user|phone) "([^"]*)" to room "([^"]*)" with (\d+) \((v4)\)$/ + * @Then /^user "([^"]*)" adds (user|group|email|circle|federated_user|phone|team) "([^"]*)" to room "([^"]*)" with (\d+) \((v4)\)$/ * * @param string $user * @param string $newType @@ -1990,6 +1990,10 @@ public function userAddAttendeeToRoom(string $user, string $newType, string $new } } + if ($newType === 'circle' || $newType === 'team') { + $newId = self::$createdTeams[$this->currentServer][$newId]; + } + $this->sendRequest( 'POST', '/apps/spreed/api/' . $apiVersion . '/room/' . self::$identifierToToken[$identifier] . '/participants', new TableNode([ @@ -2309,6 +2313,12 @@ public function userSendsMessageToRoom(string $user, string $sendingMode, string $message = str_replace('{$LOCAL_URL}', $this->localServerUrl, $message); $message = str_replace('{$LOCAL_REMOTE_URL}', $this->localRemoteServerUrl, $message); $message = str_replace('{$REMOTE_URL}', $this->remoteServerUrl, $message); + if (str_contains($message, '@"TEAM_ID(')) { + $result = preg_match('/TEAM_ID\(([^)]+)\)/', $message, $matches); + if ($result) { + $message = str_replace($matches[0], 'team/' . self::$createdTeams[$this->currentServer][$matches[1]], $message); + } + } if ($message === '413 Payload Too Large') { $message .= "\n" . str_repeat('1', 32000); @@ -3499,6 +3509,10 @@ public function userGetsTheFollowingCandidateMentionsInRoomFor($user, $identifie if (str_ends_with($row['mentionId'], '@{$REMOTE_URL}')) { $row['mentionId'] = str_replace('{$REMOTE_URL}', rtrim($this->remoteServerUrl, '/'), $row['mentionId']); } + if ($row['source'] === 'teams') { + [, $teamId] = explode('/', $row['mentionId'], 2); + $row['id'] = $row['mentionId'] = 'team/' . self::getTeamIdForLabel($this->currentServer, $teamId); + } if (array_key_exists('avatar', $row)) { Assert::assertMatchesRegularExpression('/' . self::$identifierToToken[$row['avatar']] . '\/avatar/', $mentions[$key]['avatar']); unset($row['avatar']); @@ -3948,10 +3962,23 @@ public function userNotifications(string $user, ?TableNode $body = null): void { private function assertNotifications($notifications, TableNode $formData) { Assert::assertCount(count($formData->getHash()), $notifications, 'Notifications count does not match:' . "\n" . json_encode($notifications, JSON_PRETTY_PRINT)); - Assert::assertEquals($formData->getHash(), array_map(function ($notification, $expectedNotification) { + + $expectedNotifications = array_map(function (array $expectedNotification): array { + if (str_contains($expectedNotification['object_id'], '/')) { + [$roomToken, $message] = explode('/', $expectedNotification['object_id']); + $result = preg_match('/TEAM_ID\(([^)]+)\)/', $message, $matches); + if ($result) { + $message = str_replace($matches[0], 'team/' . self::$createdTeams[$this->currentServer][$matches[1]], $message); + } + $expectedNotification['object_id'] = $roomToken . '/' . $message; + } + return $expectedNotification; + }, $formData->getHash()); + + Assert::assertEquals($expectedNotifications, array_map(function ($notification, $expectedNotification) { $data = []; if (isset($expectedNotification['object_id'])) { - if (strpos($notification['object_id'], '/') !== false) { + if (str_contains($notification['object_id'], '/')) { [$roomToken, $message] = explode('/', $notification['object_id']); $messageText = self::$messageIdToText[$message] ?? 'UNKNOWN_MESSAGE'; @@ -3976,6 +4003,10 @@ private function assertNotifications($notifications, TableNode $formData) { if ($result && isset(self::$identifierToToken[$matches[1]])) { $data['message'] = str_replace(self::$identifierToToken[$matches[1]], $matches[0], $data['message']); } + $result = preg_match('/TEAM_ID\(([^)]+)\)/', $expectedNotification['message'], $matches); + if ($result) { + $data['message'] = str_replace($matches[0], self::$createdTeams[$this->currentServer][$matches[1]], $data['message']); + } } if (isset($expectedNotification['object_type'])) { $data['object_type'] = (string)$notification['object_type']; @@ -3985,7 +4016,7 @@ private function assertNotifications($notifications, TableNode $formData) { } return $data; - }, $notifications, $formData->getHash()), json_encode($notifications, JSON_PRETTY_PRINT)); + }, $notifications, $expectedNotifications), json_encode($notifications, JSON_PRETTY_PRINT)); } /** @@ -5232,7 +5263,4 @@ protected function assertStatusCode(ResponseInterface $response, int $statusCode Assert::assertEquals($statusCode, $response->getStatusCode(), $message); } } - - - } diff --git a/tests/integration/features/chat-1/notifications.feature b/tests/integration/features/chat-1/notifications.feature index e29accc663d..2b1e994ece9 100644 --- a/tests/integration/features/chat-1/notifications.feature +++ b/tests/integration/features/chat-1/notifications.feature @@ -364,6 +364,82 @@ Feature: chat/notifications Then user "participant2" has the following notifications | app | object_type | object_id | subject | + + Scenario: Team-mention when recipient is online in the group room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant2" to team "1234" + When user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds team "1234" to room "room" with 200 (v4) + Given user "participant2" joins room "room" with 200 (v4) + When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | + | spreed | chat | room/Hi @"TEAM_ID(1234)" bye | participant1-displayname mentioned team 1234 in conversation room | + + Scenario: Team-mention when team is not a member of the room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant2" to team "1234" + When user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds user "participant2" to room "room" with 200 (v4) + Given user "participant2" joins room "room" with 200 (v4) + When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | + + Scenario: Team-mention when recipient is offline in the group room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant2" to team "1234" + When user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds team "1234" to room "room" with 200 (v4) + # Join and leave to clear the invite notification + Given user "participant2" joins room "room" with 200 (v4) + Given user "participant2" leaves room "room" with 200 (v4) + When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | + | spreed | chat | room/Hi @"TEAM_ID(1234)" bye | participant1-displayname mentioned team 1234 in conversation room | + + Scenario: Silent team-mention when recipient is offline in the group room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant2" to team "1234" + When user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds team "1234" to room "room" with 200 (v4) + # Join and leave to clear the invite notification + Given user "participant2" joins room "room" with 200 (v4) + Given user "participant2" leaves room "room" with 200 (v4) + When user "participant1" silent sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | + + Scenario: Team-mention when recipient with disabled notifications in the group room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant2" to team "1234" + When user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds team "1234" to room "room" with 200 (v4) + # Join and leave to clear the invite notification + Given user "participant2" joins room "room" with 200 (v4) + Given user "participant2" leaves room "room" with 200 (v4) + And user "participant2" sets notifications to disabled for room "room" (v4) + When user "participant1" sends message 'Hi @"TEAM_ID(1234)" bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | + + Scenario: Replying with all mention types only gives a reply notification When user "participant1" creates room "room" (v4) | roomType | 2 | diff --git a/tests/integration/features/chat-2/mentions.feature b/tests/integration/features/chat-2/mentions.feature index 78d32f8167c..ce1464a8bda 100644 --- a/tests/integration/features/chat-2/mentions.feature +++ b/tests/integration/features/chat-2/mentions.feature @@ -74,11 +74,14 @@ Feature: chat/mentions | participant2 | participant2-displayname | users | participant2 | Scenario: get matched mentions in a group room + Given team "1234" exists + Given add user "participant1" to team "1234" + Given add user "participant3" to team "1234" When user "participant1" creates room "group room" (v4) | roomType | 2 | | roomName | room | And user "participant1" adds user "participant2" to room "group room" with 200 (v4) - And user "participant1" adds user "participant3" to room "group room" with 200 (v4) + And user "participant1" adds team "1234" to room "group room" with 200 (v4) Then user "participant1" gets the following candidate mentions in room "group room" for "part" with 200 | id | label | source | mentionId | | participant2 | participant2-displayname | users | participant2 | @@ -91,6 +94,15 @@ Feature: chat/mentions | id | label | source | mentionId | | participant1 | participant1-displayname | users | participant1 | | participant2 | participant2-displayname | users | participant2 | + And user "participant1" gets the following candidate mentions in room "group room" for "1234" with 200 + | id | label | source | mentionId | + | 1234 | 1234 | teams | team/1234 | + And user "participant2" gets the following candidate mentions in room "group room" for "1234" with 200 + | id | label | source | mentionId | + | 1234 | 1234 | teams | team/1234 | + And user "participant3" gets the following candidate mentions in room "group room" for "1234" with 200 + | id | label | source | mentionId | + | 1234 | 1234 | teams | team/1234 | Scenario: get unmatched mentions in a group room When user "participant1" creates room "group room" (v4) diff --git a/tests/php/Chat/Parser/UserMentionTest.php b/tests/php/Chat/Parser/UserMentionTest.php index 5f9386ce6e0..ddf41310efe 100644 --- a/tests/php/Chat/Parser/UserMentionTest.php +++ b/tests/php/Chat/Parser/UserMentionTest.php @@ -17,6 +17,7 @@ use OCA\Talk\Room; use OCA\Talk\Service\AvatarService; use OCA\Talk\Service\ParticipantService; +use OCP\App\IAppManager; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Federation\ICloudId; @@ -28,6 +29,7 @@ use Test\TestCase; class UserMentionTest extends TestCase { + protected IAppManager&MockObject $appManager; protected ICommentsManager&MockObject $commentsManager; protected IUserManager&MockObject $userManager; protected IGroupManager&MockObject $groupManager; @@ -42,6 +44,7 @@ class UserMentionTest extends TestCase { public function setUp(): void { parent::setUp(); + $this->appManager = $this->createMock(IAppManager::class); $this->commentsManager = $this->createMock(ICommentsManager::class); $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); @@ -52,6 +55,7 @@ public function setUp(): void { $this->l = $this->createMock(IL10N::class); $this->parser = new UserMention( + $this->appManager, $this->commentsManager, $this->userManager, $this->groupManager, diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php index f5d3d8e1217..bf101361c7f 100644 --- a/tests/php/Notification/NotifierTest.php +++ b/tests/php/Notification/NotifierTest.php @@ -26,6 +26,7 @@ use OCA\Talk\Room; use OCA\Talk\Service\AvatarService; use OCA\Talk\Service\ParticipantService; +use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\Federation\ICloudIdManager; @@ -49,6 +50,7 @@ class NotifierTest extends TestCase { protected IFactory&MockObject $lFactory; protected IURLGenerator&MockObject $url; protected Config&MockObject $config; + protected IAppManager&MockObject $appManager; protected IUserManager&MockObject $userManager; protected IGroupManager&MockObject $groupManager; protected GuestManager&MockObject $guestManager; @@ -75,6 +77,7 @@ public function setUp(): void { $this->lFactory = $this->createMock(IFactory::class); $this->url = $this->createMock(IURLGenerator::class); $this->config = $this->createMock(Config::class); + $this->appManager = $this->createMock(IAppManager::class); $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); $this->guestManager = $this->createMock(GuestManager::class); @@ -98,6 +101,7 @@ public function setUp(): void { $this->lFactory, $this->url, $this->config, + $this->appManager, $this->userManager, $this->groupManager, $this->guestManager, From 0c93abb1b04294eb66e72235c6187e97c26ceb21 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 30 Jan 2025 16:39:43 +0100 Subject: [PATCH 2/2] test: Fix federation test Signed-off-by: Joas Schilling --- tests/integration/features/bootstrap/FeatureContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 49553e2a45e..25b57470827 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -3965,7 +3965,7 @@ private function assertNotifications($notifications, TableNode $formData) { $expectedNotifications = array_map(function (array $expectedNotification): array { if (str_contains($expectedNotification['object_id'], '/')) { - [$roomToken, $message] = explode('/', $expectedNotification['object_id']); + [$roomToken, $message] = explode('/', $expectedNotification['object_id'], 2); $result = preg_match('/TEAM_ID\(([^)]+)\)/', $message, $matches); if ($result) { $message = str_replace($matches[0], 'team/' . self::$createdTeams[$this->currentServer][$matches[1]], $message);