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 @@