Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
['name' => 'Local#probeCircles', 'url' => '/probecircles', 'verb' => 'GET'],
['name' => 'Local#create', 'url' => '/circles', 'verb' => 'POST'],
['name' => 'Local#destroy', 'url' => '/circles/{circleId}', 'verb' => 'DELETE'],
['name' => 'Local#leaveParentCircles', 'url' => '/circles/{circleId}/leave-parent-circles', 'verb' => 'PUT'],
['name' => 'Local#search', 'url' => '/search', 'verb' => 'GET'],
['name' => 'Local#circleDetails', 'url' => '/circles/{circleId}', 'verb' => 'GET'],
['name' => 'Local#members', 'url' => '/circles/{circleId}/members', 'verb' => 'GET'],
Expand Down
20 changes: 20 additions & 0 deletions lib/Activity/ProviderSubjectCircle.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,24 @@ public function parseSubjectCircleDelete(IEvent $event, array $params): void {

throw new FakeException();
}

/**
* @param IEvent $event
* @param array $params
*
* @throws FakeException
*/
public function parseSubjectCircleLeavingParentCircles(IEvent $event, array $params): void {
if ($event->getSubject() !== 'circle_leaving_parent_circles') {
return;
}

$this->parseCircleEvent(
$event, $params,
$this->l10n->t('You removed {circle} from all teams it belonged to'),
$this->l10n->t('{author} removed {circle} from all teams it belonged to')
);

throw new FakeException();
}
}
22 changes: 22 additions & 0 deletions lib/Controller/LocalController.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,28 @@ public function destroy(string $circleId): DataResponse {
}


/**
* @NoAdminRequired
*
* @param string $circleId
*
* @return DataResponse
* @throws OCSException
*/
public function leaveParentCircles(string $circleId): DataResponse {
try {
$this->setCurrentFederatedUser();

$circle = $this->circleService->leaveParentCircles($circleId);

return new DataResponse($this->serializeArray($circle));
} catch (Exception $e) {
$this->e($e, ['circleId' => $circleId]);
throw new OCSException($e->getMessage(), (int)$e->getCode());
}
}


/**
* @NoAdminRequired
*
Expand Down
41 changes: 41 additions & 0 deletions lib/Events/LeavingParentCirclesEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);


/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/


namespace OCA\Circles\Events;

use OCA\Circles\Model\Federated\FederatedEvent;

/**
* Class LeavingParentCirclesEvent
*
* This event is called when a Circle is removed from all parent Circles.
* This event is called on every instance of Nextcloud related to the Circle.
*
* The circle member entries have already been removed from parent circles in the members table.
* The circle membership entries have already been removed from parent circles in the membership table.
*
* This is a good place if anything needs to be executed when a Circle has been removed from its parent Circles.
*
* If anything needs to be managed on the master instance of the Circle (ie. LeftParentCirclesEvent), please use:
* $event->getFederatedEvent()->setResultEntry(string $key, array $data);
*
* @package OCA\Circles\Events
*/
class LeavingParentCirclesEvent extends CircleGenericEvent {
/**
* LeavingParentCirclesEvent constructor.
*
* @param FederatedEvent $federatedEvent
*/
public function __construct(FederatedEvent $federatedEvent) {
parent::__construct($federatedEvent);
}
}
39 changes: 39 additions & 0 deletions lib/Events/LeftParentCirclesEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);


/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/


namespace OCA\Circles\Events;

use OCA\Circles\Model\Federated\FederatedEvent;

/**
* Class LeftParentCirclesEvent
*
* This Event is called when it has been confirmed that the Circle has been removed from all parent Circles
* on all instances related to the Circle.
*
* Meaning that the event won't be triggered until each instances have been once available during the
* retry-on-fail initiated in a background job.
*
* WARNING: Unlike LeavingParentCirclesEvent, this Event is only called on the master instance of the Circle.
*
* @package OCA\Circles\Events
*/
class LeftParentCirclesEvent extends CircleResultGenericEvent {
/**
* LeftParentCirclesEvent constructor.
*
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function __construct(FederatedEvent $federatedEvent, array $results) {
parent::__construct($federatedEvent, $results);
}
}
126 changes: 126 additions & 0 deletions lib/FederatedItems/CircleLeaveParentCircles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Circles\FederatedItems;

use OCA\Circles\AppInfo\Application;
use OCA\Circles\Db\CircleRequest;
use OCA\Circles\Db\MemberRequest;
use OCA\Circles\Exceptions\MemberHelperException;
use OCA\Circles\Exceptions\MemberLevelException;
use OCA\Circles\Exceptions\MemberNotFoundException;
use OCA\Circles\Exceptions\RequestBuilderException;
use OCA\Circles\IFederatedItem;
use OCA\Circles\IFederatedItemAsyncProcess;
use OCA\Circles\IFederatedItemHighSeverity;
use OCA\Circles\Model\Federated\FederatedEvent;
use OCA\Circles\Model\Helpers\MemberHelper;
use OCA\Circles\Service\ConfigService;
use OCA\Circles\Service\EventService;
use OCA\Circles\Service\MembershipService;
use OCA\Circles\Tools\Traits\TDeserialize;
use OCA\Circles\Tools\Traits\TNCLogger;

/**
* Class CircleLeaveParentCircles
*
* @package OCA\Circles\FederatedItems
*/
class CircleLeaveParentCircles implements
IFederatedItem,
IFederatedItemHighSeverity,
IFederatedItemAsyncProcess {
use TDeserialize;
use TNCLogger;

/** @var MemberRequest */
private $memberRequest;

/** @var CircleRequest */
private $circleRequest;

/** @var MembershipService */
private $membershipService;

/** @var EventService */
private $eventService;

/** @var ConfigService */
private $configService;

/**
* CircleLeaveParentCircles constructor.
*
* @param MemberRequest $memberRequest
* @param CircleRequest $circleRequest
* @param MembershipService $membershipService
* @param EventService $eventService
* @param ConfigService $configService
*/
public function __construct(
MemberRequest $memberRequest,
CircleRequest $circleRequest,
MembershipService $membershipService,
EventService $eventService,
ConfigService $configService,
) {
$this->memberRequest = $memberRequest;
$this->circleRequest = $circleRequest;
$this->membershipService = $membershipService;
$this->eventService = $eventService;
$this->configService = $configService;

$this->setup('app', Application::APP_ID);
}

/**
* @param FederatedEvent $event
*
* @throws RequestBuilderException
* @throws MemberHelperException
* @throws MemberLevelException
*/
public function verify(FederatedEvent $event): void {
$circle = $event->getCircle();
$initiator = $circle->getInitiator();
$initiatorHelper = new MemberHelper($initiator);
$initiatorHelper->mustBeOwner();

$event->setOutcome($this->serialize($circle));
}

/**
* @param FederatedEvent $event
*
* @throws RequestBuilderException
* @throws MemberNotFoundException
*/
public function manage(FederatedEvent $event): void {
$circle = $event->getCircle();
$parentCircles = $this->membershipService->getParentCircles($circle);
foreach ($parentCircles as $parentCircle) {
$member = $this->memberRequest->getMember(
$parentCircle->getSingleId(),
$circle->getSingleId()
);
$this->memberRequest->delete($member);
$this->membershipService->onUpdate($member->getSingleId());
$this->membershipService->updatePopulation($parentCircle);
}
$this->eventService->circleLeavingParentCircles($event);
}

/**
* @param FederatedEvent $event
* @param array $results
*/
public function result(FederatedEvent $event, array $results): void {
$this->eventService->circleLeftParentCircles($event, $results);
}
}
24 changes: 24 additions & 0 deletions lib/Service/ActivityService.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ public function onCircleDestruction(Circle $circle): void {
);
}

/**
* @param Circle $circle
*/
public function onCircleLeavingParentCircles(Circle $circle): void {
if ($circle->isConfig(Circle::CFG_PERSONAL)) {
return;
}

$event = $this->generateEvent('circles_as_member');
$event->setSubject(
'circle_leaving_parent_circles',
[
'ver' => 2,
'circle' => $this->shortenCircleData($circle),
'initiator' => ($circle->hasInitiator() ? $this->shortenMemberData($circle->getInitiator()) : null),
]
);

$this->publishEvent(
$event,
$this->memberRequest->getInheritedMembers($circle->getSingleId(), false, Member::LEVEL_MODERATOR)
);
}

/**
* @param Circle $circle
* @param Member $member
Expand Down
32 changes: 32 additions & 0 deletions lib/Service/CircleService.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use OCA\Circles\FederatedItems\CircleEdit;
use OCA\Circles\FederatedItems\CircleJoin;
use OCA\Circles\FederatedItems\CircleLeave;
use OCA\Circles\FederatedItems\CircleLeaveParentCircles;
use OCA\Circles\FederatedItems\CircleSetting;
use OCA\Circles\IEntity;
use OCA\Circles\IFederatedUser;
Expand Down Expand Up @@ -253,6 +254,37 @@ public function destroy(string $circleId, bool $forceSync = false): array {
}


/**
* @param string $circleId
* @param bool $forceSync
*
* @return array
* @throws CircleNotFoundException
* @throws FederatedEventException
* @throws FederatedItemException
* @throws InitiatorNotConfirmedException
* @throws InitiatorNotFoundException
* @throws OwnerNotFoundException
* @throws RemoteInstanceException
* @throws RemoteNotFoundException
* @throws RemoteResourceNotFoundException
* @throws RequestBuilderException
* @throws UnknownRemoteException
*/
public function leaveParentCircles(string $circleId, bool $forceSync = false): array {
$this->federatedUserService->mustHaveCurrentUser();

$circle = $this->getCircle($circleId);

$event = new FederatedEvent(CircleLeaveParentCircles::class);
$event->setCircle($circle);
$event->forceSync($forceSync);
$this->federatedEventService->newEvent($event);

return $event->getOutcome();
}


/**
* @param string $circleId
* @param int $config
Expand Down
19 changes: 19 additions & 0 deletions lib/Service/EventService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
use OCA\Circles\Events\Files\CreatingFileShareEvent;
use OCA\Circles\Events\Files\FileShareCreatedEvent;
use OCA\Circles\Events\Files\PreparingFileShareEvent;
use OCA\Circles\Events\LeavingParentCirclesEvent;
use OCA\Circles\Events\LeftParentCirclesEvent;
use OCA\Circles\Events\MembershipsCreatedEvent;
use OCA\Circles\Events\MembershipsRemovedEvent;
use OCA\Circles\Events\PreparingCircleMemberEvent;
Expand Down Expand Up @@ -100,6 +102,23 @@ public function circleDestroyed(FederatedEvent $federatedEvent, array $results):
$this->eventDispatcher->dispatchTyped($event);
}

/**
* @param FederatedEvent $federatedEvent
*/
public function circleLeavingParentCircles(FederatedEvent $federatedEvent): void {
$event = new LeavingParentCirclesEvent($federatedEvent);
$this->eventDispatcher->dispatchTyped($event);
$this->activityService->onCircleLeavingParentCircles($event->getCircle());
}

/**
* @param FederatedEvent $federatedEvent
* @param array $results
*/
public function circleLeftParentCircles(FederatedEvent $federatedEvent, array $results): void {
$event = new LeftParentCirclesEvent($federatedEvent, $results);
$this->eventDispatcher->dispatchTyped($event);
}

/**
* @param FederatedEvent $federatedEvent
Expand Down
Loading