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