diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 56a25854ab9..a02cde54ca4 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -45,6 +45,7 @@ use OCA\Talk\Listener\BeforeUserLoggedOutListener; use OCA\Talk\Listener\CSPListener; use OCA\Talk\Listener\FeaturePolicyListener; +use OCA\Talk\Listener\GroupMembershipListener; use OCA\Talk\Listener\RestrictStartingCalls as RestrictStartingCallsListener; use OCA\Talk\Listener\UserDeletedListener; use OCA\Talk\Listener\UserDisplayNameListener; @@ -71,6 +72,8 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Collaboration\Resources\IProviderManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; use OCP\IServerContainer; use OCP\IUser; use OCP\Security\CSP\AddContentSecurityPolicyEvent; @@ -95,6 +98,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class); $context->registerEventListener(AddFeaturePolicyEvent::class, FeaturePolicyListener::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); + $context->registerEventListener(UserAddedEvent::class, GroupMembershipListener::class); + $context->registerEventListener(UserRemovedEvent::class, GroupMembershipListener::class); $context->registerEventListener(BeforeUserLoggedOutEvent::class, BeforeUserLoggedOutListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareTemplateLoader::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, PublicShareAuthTemplateLoader::class); diff --git a/lib/Collaboration/Collaborators/Listener.php b/lib/Collaboration/Collaborators/Listener.php index b77c4df4b7b..a7da8da38ec 100644 --- a/lib/Collaboration/Collaborators/Listener.php +++ b/lib/Collaboration/Collaborators/Listener.php @@ -27,6 +27,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 OCP\Collaboration\AutoComplete\AutoCompleteEvent; @@ -61,7 +62,7 @@ public function __construct(Manager $manager, public static function register(IEventDispatcher $dispatcher): void { $dispatcher->addListener(IManager::class . '::filterResults', static function (AutoCompleteEvent $event) { /** @var self $listener */ - $listener = \OC::$server->query(self::class); + $listener = \OC::$server->get(self::class); if ($event->getItemType() !== 'call') { return; @@ -82,28 +83,28 @@ protected function filterUsersAndGroupsWithoutTalk(array $results): array { } if (!empty($results['groups'])) { - $results['groups'] = array_filter($results['groups'], [$this, 'filterGroupResult']); + $results['groups'] = array_filter($results['groups'], [$this, 'filterBlockedGroupResult']); } if (!empty($results['exact']['groups'])) { - $results['exact']['groups'] = array_filter($results['exact']['groups'], [$this, 'filterGroupResult']); + $results['exact']['groups'] = array_filter($results['exact']['groups'], [$this, 'filterBlockedGroupResult']); } if (!empty($results['users'])) { - $results['users'] = array_filter($results['users'], [$this, 'filterUserResult']); + $results['users'] = array_filter($results['users'], [$this, 'filterBlockedUserResult']); } if (!empty($results['exact']['users'])) { - $results['exact']['users'] = array_filter($results['exact']['users'], [$this, 'filterUserResult']); + $results['exact']['users'] = array_filter($results['exact']['users'], [$this, 'filterBlockedUserResult']); } return $results; } - protected function filterUserResult(array $result): bool { + protected function filterBlockedUserResult(array $result): bool { $user = $this->userManager->get($result['value']['shareWith']); return $user instanceof IUser && !$this->config->isDisabledForUser($user); } - protected function filterGroupResult(array $result): bool { + protected function filterBlockedGroupResult(array $result): bool { return \in_array($result['value']['shareWith'], $this->allowedGroupIds, true); } @@ -114,17 +115,24 @@ protected function filterExistingParticipants(string $token, array $results): ar return $results; } + if (!empty($results['groups'])) { + $results['groups'] = array_filter($results['groups'], [$this, 'filterParticipantGroupResult']); + } + if (!empty($results['exact']['groups'])) { + $results['exact']['groups'] = array_filter($results['exact']['groups'], [$this, 'filterParticipantGroupResult']); + } + if (!empty($results['users'])) { - $results['users'] = array_filter($results['users'], [$this, 'filterParticipantResult']); + $results['users'] = array_filter($results['users'], [$this, 'filterParticipantUserResult']); } if (!empty($results['exact']['users'])) { - $results['exact']['users'] = array_filter($results['exact']['users'], [$this, 'filterParticipantResult']); + $results['exact']['users'] = array_filter($results['exact']['users'], [$this, 'filterParticipantUserResult']); } return $results; } - protected function filterParticipantResult(array $result): bool { + protected function filterParticipantUserResult(array $result): bool { $userId = $result['value']['shareWith']; try { @@ -138,4 +146,15 @@ protected function filterParticipantResult(array $result): bool { return true; } } + + protected function filterParticipantGroupResult(array $result): bool { + $groupId = $result['value']['shareWith']; + + try { + $this->room->getParticipantByActor(Attendee::ACTOR_GROUPS, $groupId); + return false; + } catch (ParticipantNotFoundException $e) { + return true; + } + } } diff --git a/lib/Command/Room/TRoomCommand.php b/lib/Command/Room/TRoomCommand.php index 06061662bf2..94a2c7869ef 100644 --- a/lib/Command/Room/TRoomCommand.php +++ b/lib/Command/Room/TRoomCommand.php @@ -230,21 +230,14 @@ protected function addRoomParticipantsByGroup(Room $room, array $groupIds): void return; } - $users = []; foreach ($groupIds as $groupId) { $group = $this->groupManager->get($groupId); if ($group === null) { throw new InvalidArgumentException(sprintf("Group '%s' not found.", $groupId)); } - $groupUsers = array_map(function (IUser $user) { - return $user->getUID(); - }, $group->getUsers()); - - $users = array_merge($users, array_values($groupUsers)); + $this->participantService->addGroup($room, $group); } - - $this->addRoomParticipants($room, array_unique($users)); } /** diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index a0bd1908ffc..4cd3e007821 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -673,23 +673,7 @@ protected function createGroupRoom(string $targetGroupName): DataResponse { // Create the room $name = $this->roomService->prepareConversationName($targetGroup->getDisplayName()); $room = $this->roomService->createConversation(Room::GROUP_CALL, $name, $currentUser); - - $usersInGroup = $targetGroup->getUsers(); - $participants = []; - foreach ($usersInGroup as $user) { - if ($currentUser->getUID() === $user->getUID()) { - // Owner is already added. - continue; - } - - $participants[] = [ - 'actorType' => Attendee::ACTOR_USERS, - 'actorId' => $user->getUID(), - 'displayName' => $user->getDisplayName(), - ]; - } - - $this->participantService->addUsers($room, $participants); + $this->participantService->addGroup($room, $targetGroup); return new DataResponse($this->formatRoom($room, $room->getParticipant($currentUser->getUID(), false)), Http::STATUS_CREATED); } @@ -995,6 +979,8 @@ public function getParticipants(bool $includeStatus = false): DataResponse { continue; } + $result['displayName'] = $participant->getAttendee()->getDisplayName(); + } elseif ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GROUPS) { $result['displayName'] = $participant->getAttendee()->getDisplayName(); } @@ -1050,14 +1036,7 @@ public function addParticipantToRoom(string $newParticipant, string $source = 'u return new DataResponse([], Http::STATUS_NOT_FOUND); } - $usersInGroup = $group->getUsers(); - foreach ($usersInGroup as $user) { - $participantsToAdd[] = [ - 'actorType' => Attendee::ACTOR_USERS, - 'actorId' => $user->getUID(), - 'displayName' => $user->getDisplayName(), - ]; - } + $this->participantService->addGroup($this->room, $group, $participants); } elseif ($source === 'circles') { if (!$this->appManager->isEnabledForUser('circles')) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -1458,6 +1437,9 @@ protected function changeParticipantType(int $attendeeId, bool $promote): DataR if ($session instanceof Session && $currentSessionId === $session->getSessionId()) { return new DataResponse([], Http::STATUS_FORBIDDEN); } + } elseif ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { + // Can not promote/demote groups + return new DataResponse([], Http::STATUS_BAD_REQUEST); } if ($promote === $targetParticipant->hasModeratorPermissions()) { diff --git a/lib/Listener/GroupMembershipListener.php b/lib/Listener/GroupMembershipListener.php new file mode 100644 index 00000000000..d67e04d5c44 --- /dev/null +++ b/lib/Listener/GroupMembershipListener.php @@ -0,0 +1,117 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Talk\Listener; + +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; + +class GroupMembershipListener implements IEventListener { + + /** @var IGroupManager */ + private $groupManager; + /** @var Manager */ + private $manager; + /** @var ParticipantService */ + private $participantService; + + public function __construct(IGroupManager $groupManager, + Manager $manager, + ParticipantService $participantService) { + $this->groupManager = $groupManager; + $this->manager = $manager; + $this->participantService = $participantService; + } + + public function handle(Event $event): void { + if ($event instanceof UserAddedEvent) { + $this->addNewMemberToRooms($event->getGroup(), $event->getUser()); + } + if ($event instanceof UserRemovedEvent) { + $this->removeFormerMemberFromRooms($event->getGroup(), $event->getUser()); + } + } + + protected function addNewMemberToRooms(IGroup $group, IUser $user): void { + $rooms = $this->manager->getRoomsForActor(Attendee::ACTOR_GROUPS, $group->getGID()); + + foreach ($rooms as $room) { + try { + $participant = $room->getParticipant($user->getUID()); + if ($participant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { + $this->participantService->updateParticipantType($room, $participant, Participant::USER); + } + } catch (ParticipantNotFoundException $e) { + $this->participantService->addUsers($room, [[ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), + 'displayName' => $user->getDisplayName(), + ]]); + } + } + } + + protected function removeFormerMemberFromRooms(IGroup $group, IUser $user): void { + $rooms = $this->manager->getRoomsForActor(Attendee::ACTOR_GROUPS, $group->getGID()); + if (empty($rooms)) { + return; + } + + $userGroupIds = $this->groupManager->getUserGroupIds($user); + + $furtherMemberships = []; + foreach ($userGroupIds as $groupId) { + $groupRooms = $this->manager->getRoomsForActor(Attendee::ACTOR_GROUPS, $groupId); + foreach ($groupRooms as $room) { + $furtherMemberships[$room->getId()] = true; + } + } + + $rooms = array_filter($rooms, static function (Room $room) use ($furtherMemberships) { + // Only delete from rooms where the user is not member via another group + return !isset($furtherMemberships[$room->getId()]); + }); + + foreach ($rooms as $room) { + try { + $participant = $room->getParticipant($user->getUID()); + $participantType = $participant->getAttendee()->getParticipantType(); + if ($participantType === Participant::USER) { + $this->participantService->removeUser($room, $user, Room::PARTICIPANT_REMOVED); + } + } catch (ParticipantNotFoundException $e) { + } + } + } +} diff --git a/lib/Manager.php b/lib/Manager.php index 30861c33836..dc7b034850c 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -304,14 +304,25 @@ public function searchRoomsByToken(string $searchToken = '', int $limit = null, * @return Room[] */ public function getRoomsForUser(string $userId, array $sessionIds = [], bool $includeLastMessage = false): array { + return $this->getRoomsForActor(Attendee::ACTOR_USERS, $userId, $sessionIds, $includeLastMessage); + } + + /** + * @param string $actorType + * @param string $actorId + * @param array $sessionIds A list of talk sessions to consider for loading (otherwise no session is loaded) + * @param bool $includeLastMessage + * @return Room[] + */ + public function getRoomsForActor(string $actorType, string $actorId, array $sessionIds = [], bool $includeLastMessage = false): array { $query = $this->db->getQueryBuilder(); $helper = new SelectHelper(); $helper->selectRoomsTable($query); $helper->selectAttendeesTable($query); $query->from('talk_rooms', 'r') ->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX( - $query->expr()->eq('a.actor_id', $query->createNamedParameter($userId)), - $query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)), + $query->expr()->eq('a.actor_id', $query->createNamedParameter($actorId)), + $query->expr()->eq('a.actor_type', $query->createNamedParameter($actorType)), $query->expr()->eq('a.room_id', 'r.id') )) ->where($query->expr()->isNotNull('a.id')); @@ -337,7 +348,7 @@ public function getRoomsForUser(string $userId, array $sessionIds = [], bool $in } $room = $this->createRoomObject($row); - if ($userId !== null && isset($row['actor_id'])) { + if ($actorType === Attendee::ACTOR_USERS && isset($row['actor_id'])) { $room->setParticipant($row['actor_id'], $this->createParticipantObject($room, $row)); } $rooms[] = $room; diff --git a/lib/Model/Attendee.php b/lib/Model/Attendee.php index 40d4663db23..d840a9fd446 100644 --- a/lib/Model/Attendee.php +++ b/lib/Model/Attendee.php @@ -52,6 +52,7 @@ */ class Attendee extends Entity { public const ACTOR_USERS = 'users'; + public const ACTOR_GROUPS = 'groups'; public const ACTOR_GUESTS = 'guests'; public const ACTOR_EMAILS = 'emails'; diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 867a664af43..f369f865e72 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -50,6 +50,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IGroup; use OCP\IUser; use OCP\IUserManager; use OCP\IGroupManager; @@ -105,6 +106,12 @@ public function __construct(IConfig $serverConfig, public function updateParticipantType(Room $room, Participant $participant, int $participantType): void { $attendee = $participant->getAttendee(); + + if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { + // Can not promote/demote groups + return; + } + $oldType = $attendee->getParticipantType(); $event = new ModifyParticipantEvent($room, $participant, 'type', $participantType, $oldType); @@ -300,6 +307,60 @@ public function addUsers(Room $room, array $participants): void { $this->dispatcher->dispatch(Room::EVENT_AFTER_USERS_ADD, $event); } + /** + * @param Room $room + * @param IGroup $group + * @param Participant[] $existingParticipants + */ + public function addGroup(Room $room, IGroup $group, array $existingParticipants = []): void { + $usersInGroup = $group->getUsers(); + + if (empty($existingParticipants)) { + $existingParticipants = $this->getParticipantsForRoom($room); + } + + $participantsByUserId = []; + foreach ($existingParticipants as $participant) { + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { + $participantsByUserId[$participant->getAttendee()->getActorId()] = $participant; + } + } + + $newParticipants = []; + foreach ($usersInGroup as $user) { + $existingParticipant = $participantsByUserId[$user->getUID()] ?? null; + if ($existingParticipant instanceof Participant) { + if ($existingParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { + $this->updateParticipantType($room, $existingParticipant, Participant::USER); + } + + // Participant is already in the conversation, so skip them. + continue; + } + + $newParticipants[] = [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), + 'displayName' => $user->getDisplayName(), + ]; + } + + try { + $this->attendeeMapper->findByActor($room->getId(), Attendee::ACTOR_GROUPS, $group->getGID()); + } catch (DoesNotExistException $e) { + $attendee = new Attendee(); + $attendee->setRoomId($room->getId()); + $attendee->setActorType(Attendee::ACTOR_GROUPS); + $attendee->setActorId($group->getGID()); + $attendee->setDisplayName($group->getDisplayName()); + $attendee->setParticipantType(Participant::USER); + $attendee->setReadPrivacy(Participant::PRIVACY_PRIVATE); + $this->attendeeMapper->insert($attendee); + } + + $this->addUsers($room, $newParticipants); + } + /** * @param Room $room * @param string $email @@ -412,6 +473,43 @@ public function removeAttendee(Room $room, Participant $participant, string $rea } else { $this->dispatcher->dispatch(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $event); } + + if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_GROUPS) { + $this->removeGroupMembers($room, $participant, $reason); + } + } + + public function removeGroupMembers(Room $room, Participant $removedGroupParticipant, string $reason): void { + $removedGroup = $this->groupManager->get($removedGroupParticipant->getAttendee()->getActorId()); + if (!$removedGroup instanceof IGroup) { + return; + } + + $attendeeGroups = $this->attendeeMapper->getActorsByType($room->getId(), Attendee::ACTOR_GROUPS); + + $groupsInRoom = []; + foreach ($attendeeGroups as $attendee) { + $groupsInRoom[] = $attendee->getActorId(); + } + + foreach ($removedGroup->getUsers() as $user) { + try { + $participant = $room->getParticipant($user->getUID()); + } catch (ParticipantNotFoundException $e) { + continue; + } + + $userGroups = $this->groupManager->getUserGroupIds($user); + $stillHasGroup = array_intersect($userGroups, $groupsInRoom); + if (!empty($stillHasGroup)) { + continue; + } + + if ($participant->getAttendee()->getParticipantType() === Participant::USER) { + // Only remove normal users, not moderators/admins + $this->removeAttendee($room, $participant, $reason); + } + } } public function removeUser(Room $room, IUser $user, string $reason): void { diff --git a/src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue b/src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue index 97797426a0c..b2e91f7cd8f 100644 --- a/src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue +++ b/src/components/RightSidebar/Participants/CurrentParticipants/CurrentParticipants.vue @@ -32,7 +32,7 @@