diff --git a/appinfo/info.xml b/appinfo/info.xml index d57a365a2db..683d4c2f63e 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -105,6 +105,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m OCA\Talk\Command\Room\Demote OCA\Talk\Command\Room\Update OCA\Talk\Command\User\Remove + OCA\Talk\Command\User\TransferOwnership diff --git a/lib/Command/User/TransferOwnership.php b/lib/Command/User/TransferOwnership.php new file mode 100644 index 00000000000..b26f1f85243 --- /dev/null +++ b/lib/Command/User/TransferOwnership.php @@ -0,0 +1,175 @@ + + * + * @author Joas Schilling + * + * @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\Command\User; + +use OC\Core\Command\Base; +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 OCA\Talk\Service\RoomService; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class TransferOwnership extends Base { + private RoomService $roomService; + private ParticipantService $participantService; + private Manager $manager; + private IUserManager $userManager; + + public function __construct(ParticipantService $participantService, + Manager $manager, + IUserManager $userManager) { + parent::__construct(); + $this->participantService = $participantService; + $this->manager = $manager; + $this->userManager = $userManager; + } + + protected function configure(): void { + $this + ->setName('talk:user:transfer-ownership') + ->setDescription('Adds the destination-user with the same participant type to all (not one-to-one) conversations of source-user') + ->addArgument( + 'source-user', + InputArgument::REQUIRED, + 'Owner of conversations which shall be moved' + ) + ->addArgument( + 'destination-user', + InputArgument::REQUIRED, + 'User who will be the new owner of the conversations' + ) + ->addOption( + 'include-non-moderator', + null, + InputOption::VALUE_NONE, + 'Also include conversations where the source-user is a normal user' + ) + ->addOption( + 'remove-source-user', + null, + InputOption::VALUE_NONE, + 'Remove the source-user from the conversations' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $sourceUID = $input->getArgument('source-user'); + $destinationUID = $input->getArgument('destination-user'); + + $destinationUser = $this->userManager->get($destinationUID); + if ($destinationUser === null) { + $output->writeln('Destination user could not be found.'); + return 1; + } + + $includeNonModeratorRooms = $input->getOption('include-non-moderator'); + $removeSourceUser = $input->getOption('remove-source-user'); + + $modified = 0; + $rooms = $this->manager->getRoomsForActor(Attendee::ACTOR_USERS, $sourceUID); + foreach ($rooms as $room) { + if ($room->getType() !== Room::TYPE_GROUP && $room->getType() !== Room::TYPE_PUBLIC) { + // Skip one-to-one, changelog and any other room types + continue; + } + + $sourceParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_USERS, $sourceUID); + + if ($sourceParticipant->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { + continue; + } + + if (!$includeNonModeratorRooms && !$sourceParticipant->hasModeratorPermissions()) { + continue; + } + + try { + $destinationParticipant = $this->participantService->getParticipantByActor($room, Attendee::ACTOR_USERS, $destinationUser->getUID()); + + $targetType = $this->shouldUpdateParticipantType($sourceParticipant->getAttendee()->getParticipantType(), $destinationParticipant->getAttendee()->getParticipantType()); + + if ($targetType !== null) { + $this->participantService->updateParticipantType( + $room, + $destinationParticipant, + $sourceParticipant->getAttendee()->getParticipantType() + ); + $modified++; + } + } catch (ParticipantNotFoundException $e) { + $this->participantService->addUsers($room, [ + [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $destinationUser->getUID(), + 'displayName' => $destinationUser->getDisplayName(), + 'participantType' => $sourceParticipant->getAttendee()->getParticipantType(), + ] + ]); + $modified++; + } + + if ($removeSourceUser) { + $this->participantService->removeAttendee($room, $sourceParticipant, Room::PARTICIPANT_REMOVED); + } + } + + $output->writeln('Added or promoted user ' . $destinationUser->getUID() . ' in ' . $modified . ' rooms.'); + return 0; + } + + protected function shouldUpdateParticipantType(int $sourceParticipantType, int $destinationParticipantType): ?int { + if ($sourceParticipantType === Participant::OWNER) { + if ($destinationParticipantType === Participant::OWNER) { + return null; + } + return $sourceParticipantType; + } + + if ($sourceParticipantType === Participant::MODERATOR) { + if ($destinationParticipantType === Participant::OWNER || $destinationParticipantType === Participant::MODERATOR) { + return null; + } + return $sourceParticipantType; + } + + if ($sourceParticipantType === Participant::USER) { + if ($destinationParticipantType !== Participant::USER_SELF_JOINED) { + return null; + } + return $sourceParticipantType; + } + + return null; + } +} diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 3a0733afe8c..d48e448cf02 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -134,6 +134,9 @@ public function updateParticipantType(Room $room, Participant $participant, int } $oldType = $attendee->getParticipantType(); + if ($oldType === $participantType) { + return; + } $event = new ModifyParticipantEvent($room, $participant, 'type', $participantType, $oldType); $this->dispatcher->dispatch(Room::EVENT_BEFORE_PARTICIPANT_TYPE_SET, $event); diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index c0bab7b1424..2778f961a28 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -40,6 +40,8 @@ class FeatureContext implements Context, SnippetAcceptingContext { /** @var string[] */ protected static $identifierToToken; /** @var string[] */ + protected static $identifierToId; + /** @var string[] */ protected static $tokenToIdentifier; /** @var array[] */ protected static $identifierToAvatar; @@ -146,6 +148,7 @@ public function __construct() { */ public function setUp() { self::$identifierToToken = []; + self::$identifierToId = []; self::$tokenToIdentifier = []; self::$sessionIdToUser = []; self::$userToSessionId = []; @@ -325,7 +328,15 @@ private function assertRooms($rooms, TableNode $formData, bool $shouldOrder = fa $expected = $formData->getHash(); if ($shouldOrder) { $sorter = static function (array $roomA, array $roomB): int { - return $roomA['id'] < $roomB['id'] ? -1 : 1; + $idA = $roomA['id']; + $idB = $roomB['id']; + if (isset(self::$identifierToId[$idA])) { + $idA = self::$identifierToId[$idA]; + } + if (isset(self::$identifierToId[$idB])) { + $idB = self::$identifierToId[$idB]; + } + return $idA < $idB ? -1 : 1; }; usort($expected, $sorter); @@ -786,6 +797,7 @@ public function userCreatesRoomWith(string $user, string $identifier, int $statu if ($statusCode === 201) { self::$identifierToToken[$identifier] = $response['token']; + self::$identifierToId[$identifier] = $response['id']; self::$tokenToIdentifier[$response['token']] = $identifier; } } diff --git a/tests/integration/features/command/user-transfer-ownership.feature b/tests/integration/features/command/user-transfer-ownership.feature new file mode 100644 index 00000000000..0572f2c207b --- /dev/null +++ b/tests/integration/features/command/user-transfer-ownership.feature @@ -0,0 +1,165 @@ +Feature: command/user-remove + + Background: + Given user "participant1" exists + Given user "participant2" exists + Given user "participant3" exists + + Scenario: Only transfer when moderator permissions + Given user "participant1" creates room "one-to-one" (v4) + | roomType | 1 | + | invite | participant2 | + Given user "participant1" creates room "user" (v4) + | roomType | 3 | + | roomName | user | + And user "participant1" adds user "participant2" to room "user" with 200 (v4) + Given user "participant1" creates room "moderator" (v4) + | roomType | 2 | + | roomName | moderator | + And user "participant1" adds user "participant2" to room "moderator" with 200 (v4) + And user "participant1" promotes "participant2" in room "moderator" with 200 (v4) + Given user "participant2" creates room "owner" (v4) + | roomType | 2 | + | roomName | owner | + Given user "participant1" creates room "self-joined" (v4) + | roomType | 3 | + | roomName | self-joined | + And user "participant2" joins room "self-joined" with 200 (v4) + And invoking occ with "talk:user:transfer-ownership participant2 participant3" + And the command output contains the text "Added or promoted user participant3 in 2 rooms." + Then the command was successful + And user "participant2" is participant of the following rooms (v4) + | id | name | type | participantType | + | one-to-one | participant1 | 1 | 1 | + | user | user | 3 | 3 | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + | self-joined | self-joined | 3 | 5 | + And user "participant3" is participant of the following rooms (v4) + | id | name | type | participantType | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + + Scenario: Also transfer without moderator permissions + Given user "participant1" creates room "one-to-one" (v4) + | roomType | 1 | + | invite | participant2 | + Given user "participant1" creates room "user" (v4) + | roomType | 3 | + | roomName | user | + And user "participant1" adds user "participant2" to room "user" with 200 (v4) + Given user "participant1" creates room "moderator" (v4) + | roomType | 2 | + | roomName | moderator | + And user "participant1" adds user "participant2" to room "moderator" with 200 (v4) + And user "participant1" promotes "participant2" in room "moderator" with 200 (v4) + Given user "participant2" creates room "owner" (v4) + | roomType | 2 | + | roomName | owner | + Given user "participant1" creates room "self-joined" (v4) + | roomType | 3 | + | roomName | self-joined | + And user "participant2" joins room "self-joined" with 200 (v4) + And invoking occ with "talk:user:transfer-ownership --include-non-moderator participant2 participant3" + And the command output contains the text "Added or promoted user participant3 in 3 rooms." + Then the command was successful + And user "participant2" is participant of the following rooms (v4) + | id | name | type | participantType | + | one-to-one | participant1 | 1 | 1 | + | user | user | 3 | 3 | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + | self-joined | self-joined | 3 | 5 | + And user "participant3" is participant of the following rooms (v4) + | id | name | type | participantType | + | user | user | 3 | 3 | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + + Scenario: Remove source user on successful transfer + Given user "participant1" creates room "one-to-one" (v4) + | roomType | 1 | + | invite | participant2 | + Given user "participant1" creates room "user" (v4) + | roomType | 3 | + | roomName | user | + And user "participant1" adds user "participant2" to room "user" with 200 (v4) + Given user "participant1" creates room "moderator" (v4) + | roomType | 2 | + | roomName | moderator | + And user "participant1" adds user "participant2" to room "moderator" with 200 (v4) + And user "participant1" promotes "participant2" in room "moderator" with 200 (v4) + Given user "participant2" creates room "owner" (v4) + | roomType | 2 | + | roomName | owner | + Given user "participant1" creates room "self-joined" (v4) + | roomType | 3 | + | roomName | self-joined | + And user "participant2" joins room "self-joined" with 200 (v4) + And invoking occ with "talk:user:transfer-ownership --remove-source-user participant2 participant3" + And the command output contains the text "Added or promoted user participant3 in 2 rooms." + Then the command was successful + And user "participant2" is participant of the following rooms (v4) + | id | name | type | participantType | + | one-to-one | participant1 | 1 | 1 | + | user | user | 3 | 3 | + | self-joined | self-joined | 3 | 5 | + And user "participant3" is participant of the following rooms (v4) + | id | name | type | participantType | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + + Scenario: Promote user if source had privileges + Given user "participant1" creates room "moderator" (v4) + | roomType | 2 | + | roomName | moderator | + And user "participant1" adds user "participant2" to room "moderator" with 200 (v4) + And user "participant1" promotes "participant2" in room "moderator" with 200 (v4) + Given user "participant2" creates room "owner" (v4) + | roomType | 2 | + | roomName | owner | + Given user "participant2" creates room "moderator to owner" (v4) + | roomType | 2 | + | roomName | moderator to owner | + And user "participant2" adds user "participant3" to room "moderator to owner" with 200 (v4) + And user "participant2" promotes "participant3" in room "moderator to owner" with 200 (v4) + Given user "participant1" creates room "from self-joined to moderator" (v4) + | roomType | 3 | + | roomName | from self-joined to moderator | + And user "participant1" adds user "participant2" to room "from self-joined to moderator" with 200 (v4) + And user "participant1" promotes "participant2" in room "from self-joined to moderator" with 200 (v4) + Given user "participant1" creates room "from self-joined to user" (v4) + | roomType | 3 | + | roomName | from self-joined to user | + And user "participant1" adds user "participant2" to room "from self-joined to user" with 200 (v4) + And user "participant3" is participant of the following rooms (v4) + | id | name | type | participantType | + | moderator to owner | moderator to owner | 2 | 2 | + And user "participant3" joins room "from self-joined to user" with 200 (v4) + And user "participant3" is participant of the following unordered rooms (v4) + | id | name | type | participantType | + | moderator to owner | moderator to owner | 2 | 2 | + | from self-joined to user | from self-joined to user | 3 | 5 | + And user "participant3" joins room "from self-joined to moderator" with 200 (v4) + And user "participant3" is participant of the following unordered rooms (v4) + | id | name | type | participantType | + | moderator to owner | moderator to owner | 2 | 2 | + | from self-joined to user | from self-joined to user | 3 | 5 | + | from self-joined to moderator | from self-joined to moderator | 3 | 5 | + And invoking occ with "talk:user:transfer-ownership --include-non-moderator participant2 participant3" + And the command output contains the text "Added or promoted user participant3 in 5 rooms." + Then the command was successful + And user "participant2" is participant of the following rooms (v4) + | id | name | type | participantType | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + | moderator to owner | moderator to owner | 2 | 1 | + | from self-joined to moderator | from self-joined to moderator | 3 | 2 | + | from self-joined to user | from self-joined to user | 3 | 3 | + And user "participant3" is participant of the following unordered rooms (v4) + | id | name | type | participantType | + | moderator | moderator | 2 | 2 | + | owner | owner | 2 | 1 | + | moderator to owner | moderator to owner | 2 | 1 | + | from self-joined to moderator | from self-joined to moderator | 3 | 2 | + | from self-joined to user | from self-joined to user | 3 | 3 | diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index abdde0391dc..2412a5e9699 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,5 +1,5 @@ - + BeforeTemplateRenderedEvent @@ -154,6 +154,11 @@ Base + + + Base + + null