diff --git a/lib/Command/AccountDelete.php b/lib/Command/AccountDelete.php index aaad6d9e..ccef197b 100644 --- a/lib/Command/AccountDelete.php +++ b/lib/Command/AccountDelete.php @@ -33,12 +33,10 @@ namespace OCA\Social\Command; use Exception; use OC\Core\Command\Base; -use OCA\Social\Interfaces\Actor\PersonInterface; use OCA\Social\Service\AccountService; use OCA\Social\Service\CacheActorService; use OCA\Social\Service\ConfigService; use OCP\IUserManager; -use OCP\Server; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -76,12 +74,7 @@ class AccountDelete extends Base { protected function execute(InputInterface $input, OutputInterface $output): int { $account = $input->getArgument('account'); - // TODO: broadcast to other instance - throw new Exception('not fully available'); - - $actor = $this->cacheActorService->getFromLocalAccount($account); - $personInterface = Server::get(PersonInterface::class); - $personInterface->deleteActor($actor->getId()); + $this->accountService->deleteActor($account); return 0; } diff --git a/lib/Command/CacheRefresh.php b/lib/Command/CacheRefresh.php index 5d39f373..5108ee1a 100644 --- a/lib/Command/CacheRefresh.php +++ b/lib/Command/CacheRefresh.php @@ -68,8 +68,11 @@ class CacheRefresh extends Base { * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { - $result = $this->accountService->blindKeyRotation(); - $output->writeLn($result . ' key pairs refreshed'); +// $result = $this->accountService->blindKeyRotation(); +// $output->writeLn($result . ' key pairs refreshed'); + + $result = $this->accountService->manageDeletedActors(); + $output->writeLn($result . ' local accounts deleted'); $result = $this->accountService->manageCacheLocalActors(); $output->writeLn($result . ' local accounts regenerated'); diff --git a/lib/Cron/Cache.php b/lib/Cron/Cache.php index 27796efa..cf40cdf9 100644 --- a/lib/Cron/Cache.php +++ b/lib/Cron/Cache.php @@ -67,7 +67,12 @@ class Cache extends TimedJob { */ protected function run($argument) { try { - $this->accountService->blindKeyRotation(); +// $this->accountService->blindKeyRotation(); + } catch (Exception $e) { + } + + try { + $this->accountService->manageDeletedActors(); } catch (Exception $e) { } diff --git a/lib/Db/ActorsRequest.php b/lib/Db/ActorsRequest.php index cbf31d0d..915e556c 100644 --- a/lib/Db/ActorsRequest.php +++ b/lib/Db/ActorsRequest.php @@ -40,6 +40,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; class ActorsRequest extends ActorsRequestBuilder { /** * Create a new Person in the database. + * * @throws SocialAppConfigException */ public function create(Person $actor): void { @@ -119,9 +120,9 @@ class ActorsRequest extends ActorsRequestBuilder { */ public function getFromId(string $id): Person { $qb = $this->getActorsSelectSql(); - $this->limitToIdString($qb, $id); + $qb->limitToIdString($id); - $cursor = $qb->executeQuery(); + $cursor = $qb->execute(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -158,6 +159,28 @@ class ActorsRequest extends ActorsRequestBuilder { } + public function setAsDeleted(string $handle): void { + $qb = $this->getActorsUpdateSql(); + $qb->set( + 'deleted', + $qb->createNamedParameter(new DateTime('now'), IQueryBuilder::PARAM_DATE) + ); + $qb->limitToPreferredUsername($handle); + + $qb->execute(); + } + + /** + * @param string $handle + */ + public function delete(string $handle): void { + $qb = $this->getActorsDeleteSql(); + $qb->limitToPreferredUsername($handle); + + $qb->execute(); + } + + /** * @return Person[] * @throws SocialAppConfigException diff --git a/lib/Db/ActorsRequestBuilder.php b/lib/Db/ActorsRequestBuilder.php index 98b9ec82..851dfaf2 100644 --- a/lib/Db/ActorsRequestBuilder.php +++ b/lib/Db/ActorsRequestBuilder.php @@ -75,7 +75,7 @@ class ActorsRequestBuilder extends CoreRequestBuilder { /** @noinspection PhpMethodParametersCountMismatchInspection */ $qb->select( 'a.id', 'a.id_prim', 'a.user_id', 'a.preferred_username', 'a.name', 'a.summary', - 'a.public_key', 'a.avatar_version', 'a.private_key', 'a.creation' + 'a.public_key', 'a.avatar_version', 'a.private_key', 'a.creation', 'a.deleted' ) ->from(self::TABLE_ACTORS, 'a'); diff --git a/lib/Db/CacheActorsRequest.php b/lib/Db/CacheActorsRequest.php index 520a8250..7899f768 100644 --- a/lib/Db/CacheActorsRequest.php +++ b/lib/Db/CacheActorsRequest.php @@ -34,8 +34,8 @@ use DateTime; use Exception; use OCA\Social\Exceptions\CacheActorDoesNotExistException; use OCA\Social\Model\ActivityPub\Actor\Person; -use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Exception as DBException; +use OCP\DB\QueryBuilder\IQueryBuilder; class CacheActorsRequest extends CacheActorsRequestBuilder { public const CACHE_TTL = 60 * 24; // 1d @@ -245,4 +245,22 @@ class CacheActorsRequest extends CacheActorsRequestBuilder { $qb->execute(); } + + + /** + * @return array + */ + public function getSharedInboxes(): array { + $qb = $this->getQueryBuilder(); + $qb->selectDistinct('shared_inbox') + ->from(self::TABLE_CACHE_ACTORS); + $inbox = []; + $cursor = $qb->execute(); + while ($data = $cursor->fetch()) { + $inbox[] = $data['shared_inbox']; + } + $cursor->closeCursor(); + + return $inbox; + } } diff --git a/lib/Db/SocialLimitsQueryBuilder.php b/lib/Db/SocialLimitsQueryBuilder.php index 88bc4369..f3f395b9 100644 --- a/lib/Db/SocialLimitsQueryBuilder.php +++ b/lib/Db/SocialLimitsQueryBuilder.php @@ -336,7 +336,6 @@ class SocialLimitsQueryBuilder extends SocialCrossQueryBuilder { $pf = $this->getDefaultSelectAlias(); if ($options->getSince() > 0) { - $options->setInverted(true); $this->andWhere($expr->gt($pf . '.nid', $this->createNamedParameter($options->getSince()))); } @@ -345,6 +344,7 @@ class SocialLimitsQueryBuilder extends SocialCrossQueryBuilder { } if ($options->getMinId() > 0) { + $options->setInverted(true); $this->andWhere($expr->gt($pf . '.nid', $this->createNamedParameter($options->getMinId()))); } diff --git a/lib/Migration/Version1000Date20221118000001.php b/lib/Migration/Version1000Date20221118000001.php index aa5e33da..e65ac193 100644 --- a/lib/Migration/Version1000Date20221118000001.php +++ b/lib/Migration/Version1000Date20221118000001.php @@ -222,6 +222,12 @@ class Version1000Date20221118000001 extends SimpleMigrationStep { 'notnull' => false, ] ); + $table->addColumn( + 'deleted', Types::DATETIME, + [ + 'notnull' => false, + ] + ); $table->setPrimaryKey(['id_prim']); } diff --git a/lib/Model/ActivityPub/Actor/Person.php b/lib/Model/ActivityPub/Actor/Person.php index a5db5244..a161a773 100644 --- a/lib/Model/ActivityPub/Actor/Person.php +++ b/lib/Model/ActivityPub/Actor/Person.php @@ -31,7 +31,6 @@ declare(strict_types=1); namespace OCA\Social\Model\ActivityPub\Actor; -use OCA\Social\Tools\IQueryRow; use DateTime; use Exception; use JsonSerializable; @@ -42,6 +41,7 @@ use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UrlCloudException; use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Model\ActivityPub\Object\Image; +use OCA\Social\Tools\IQueryRow; use OCA\Social\Traits\TDetails; /** @@ -62,53 +62,30 @@ class Person extends ACore implements IQueryRow, JsonSerializable { private string $userId = ''; - private string $name = ''; - private string $preferredUsername = ''; - private string $displayName = ''; - private string $description = ''; - private string $publicKey = ''; - private string $privateKey = ''; - private int $creation = 0; - + private int $deleted = 0; private string $account = ''; - private string $following = ''; - private string $followers = ''; - private string $inbox = ''; - private string $outbox = ''; - private string $sharedInbox = ''; - private string $featured = ''; - private string $avatar = ''; - private string $header = ''; - private bool $locked = false; - private bool $bot = false; - private bool $discoverable = false; - private string $privacy = 'public'; - private bool $sensitive = false; - private string $language = 'en'; - private int $avatarVersion = -1; - private string $viewerLink = ''; /** @@ -307,6 +284,25 @@ class Person extends ACore implements IQueryRow, JsonSerializable { } + /** + * @return int + */ + public function getDeleted(): int { + return $this->deleted; + } + + /** + * @param int $deleted + * + * @return Person + */ + public function setDeleted(int $deleted): self { + $this->deleted = $deleted; + + return $this; + } + + /** * @return string */ @@ -661,8 +657,21 @@ class Person extends ACore implements IQueryRow, JsonSerializable { ->setDetailsAll($this->getArray('details', $data, [])); try { - $dTime = new DateTime($this->get('creation', $data, 'yesterday')); - $this->setCreation($dTime->getTimestamp()); + $cTime = new DateTime($this->get('creation', $data, 'yesterday')); + $this->setCreation($cTime->getTimestamp()); + } catch (Exception $e) { + } + + try { + $deletedValue = $this->get('deleted', $data); + if ($deletedValue === '') { + return; + } + $dTime = new DateTime(); + $deleted = $dTime->getTimestamp(); + if ($deleted > 0) { + $this->setDeleted($deleted); + } } catch (Exception $e) { } } diff --git a/lib/Model/InstancePath.php b/lib/Model/InstancePath.php index 37c393be..00fe6e65 100644 --- a/lib/Model/InstancePath.php +++ b/lib/Model/InstancePath.php @@ -46,6 +46,7 @@ class InstancePath implements JsonSerializable { public const TYPE_INBOX = 1; public const TYPE_GLOBAL = 2; public const TYPE_FOLLOWERS = 3; + public const TYPE_ALL = 4; public const PRIORITY_NONE = 0; public const PRIORITY_LOW = 1; diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 2b06825d..679becb1 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -32,6 +32,7 @@ namespace OCA\Social\Service; use Exception; use OC\User\NoUserException; +use OCA\Social\AP; use OCA\Social\Db\ActorsRequest; use OCA\Social\Db\FollowsRequest; use OCA\Social\Db\StreamRequest; @@ -43,11 +44,17 @@ use OCA\Social\Exceptions\ItemUnknownException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\StreamNotFoundException; use OCA\Social\Exceptions\UrlCloudException; +use OCA\Social\Interfaces\Actor\PersonInterface; +use OCA\Social\Model\ActivityPub\ACore; +use OCA\Social\Model\ActivityPub\Activity\Delete; use OCA\Social\Model\ActivityPub\Actor\Person; +use OCA\Social\Model\InstancePath; use OCA\Social\Tools\Traits\TArrayTools; use OCP\Accounts\IAccountManager; +use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use Psr\Log\LoggerInterface; /** * Class ActorService @@ -56,7 +63,7 @@ use OCP\IUserSession; */ class AccountService { public const KEY_PAIR_LIFESPAN = 7; - + public const TIME_RETENTION = 3600; // seconds before fully delete account use TArrayTools; private IUserManager $userManager; @@ -66,10 +73,12 @@ class AccountService { private FollowsRequest $followsRequest; private StreamRequest $streamRequest; private ActorService $actorService; + private ActivityService $activityService; + private AccountService $accountService; private SignatureService $signatureService; private DocumentService $documentService; private ConfigService $configService; - private MiscService $miscService; + private LoggerInterface $logger; public function __construct( IUserManager $userManager, @@ -79,10 +88,11 @@ class AccountService { FollowsRequest $followsRequest, StreamRequest $streamRequest, ActorService $actorService, + ActivityService $activityService, DocumentService $documentService, SignatureService $signatureService, ConfigService $configService, - MiscService $miscService + LoggerInterface $logger ) { $this->userManager = $userManager; $this->userSession = $userSession; @@ -91,10 +101,11 @@ class AccountService { $this->followsRequest = $followsRequest; $this->streamRequest = $streamRequest; $this->actorService = $actorService; + $this->activityService = $activityService; $this->documentService = $documentService; $this->signatureService = $signatureService; $this->configService = $configService; - $this->miscService = $miscService; + $this->logger = $logger; } @@ -156,7 +167,7 @@ class AccountService { * @throws ItemAlreadyExistsException */ public function getActorFromUserId(string $userId, bool $create = false): Person { - $this->miscService->confirmUserId($userId); + $this->confirmUserId($userId); try { $actor = $this->actorsRequest->getFromUserId($userId); } catch (ActorDoesNotExistException $e) { @@ -191,11 +202,16 @@ class AccountService { * @throws UrlCloudException */ public function createActor(string $userId, string $username) { - $this->miscService->confirmUserId($userId); + $this->confirmUserId($userId); $this->checkActorUsername($username); try { - $this->actorsRequest->getFromUsername($username); + $actor = $this->actorsRequest->getFromUsername($username); + if ($actor->getDeleted() > 0) { + throw new AccountAlreadyExistsException( + 'actor with that name was deleted but is still in retention. Please try again later' + ); + } throw new AccountAlreadyExistsException('actor with that name already exist'); } catch (ActorDoesNotExistException $e) { /* we do nohtin */ @@ -223,6 +239,46 @@ class AccountService { } + /** + * @param string $handle + * + * @throws ItemUnknownException + * @throws SocialAppConfigException + */ + public function deleteActor(string $handle): void { + try { + $actor = $this->actorsRequest->getFromUsername($handle); + } catch (ActorDoesNotExistException $e) { + return; + } + + // set as deleted locally + $this->actorsRequest->setAsDeleted($actor->getPreferredUsername()); + + // delete related data + /** @var PersonInterface $interface */ + $interface = AP::$activityPub->getInterfaceFromType(Person::TYPE); + $interface->deleteActor($actor); + + // broadcast delete event + $delete = new Delete(); + $delete->setId($actor->getId() . '#delete'); + $delete->setActorId($actor->getId()); + $delete->setToArray([ACore::CONTEXT_PUBLIC]); + $delete->setObjectId($actor->getId()); + $delete->addInstancePath( + new InstancePath( + $actor->getInbox(), + InstancePath::TYPE_ALL, + InstancePath::PRIORITY_LOW + ) + ); + $this->signatureService->signObject($actor, $delete); + + $this->activityService->request($delete); + } + + /** * @param string $username * @@ -305,9 +361,7 @@ class AccountService { $actor->setName($displayNameProperty->getValue()); } } catch (Exception $e) { - $this->miscService->log( - 'Issue while trying to updateCacheLocalActorName: ' . $e->getMessage(), 1 - ); + $this->logger->error('Issue while trying to updateCacheLocalActorName: ' . $e->getMessage()); } } @@ -322,6 +376,29 @@ class AccountService { } + /** + * @return int + * @throws Exception + */ + public function manageDeletedActors(): int { + $entries = $this->actorsRequest->getAll(); + $deleted = 0; + foreach ($entries as $item) { + // delete after an hour + if ($item->getDeleted() === 0) { + continue; + } + + if ($item->getDeleted() < (time() - self::TIME_RETENTION)) { + $this->actorsRequest->delete($item->getPreferredUsername()); + $deleted++; + } + } + + return $deleted; + } + + /** * @return int * @throws Exception @@ -359,4 +436,23 @@ class AccountService { return $count; } + + + /** + * @param string $userId + * + * @return IUser + * @throws NoUserException + */ + public function confirmUserId(string &$userId): IUser { + $user = $this->userManager->get($userId); + + if ($user === null) { + throw new NoUserException('user does not exist'); + } + + $userId = $user->getUID(); + + return $user; + } } diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index 561883d4..793db770 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -32,6 +32,7 @@ namespace OCA\Social\Service; use Exception; use OCA\Social\AP; +use OCA\Social\Db\CacheActorsRequest; use OCA\Social\Db\FollowsRequest; use OCA\Social\Db\StreamRequest; use OCA\Social\Exceptions\ActorDoesNotExistException; @@ -74,30 +75,32 @@ class ActivityService { private StreamRequest $streamRequest; private FollowsRequest $followsRequest; + private CacheActorsRequest $cacheActorsRequest; private SignatureService $signatureService; private RequestQueueService $requestQueueService; - private AccountService $accountService; private ConfigService $configService; private CurlService $curlService; - private MiscService $miscService; private LoggerInterface $logger; private ?array $failInstances = null; public function __construct( - StreamRequest $streamRequest, FollowsRequest $followsRequest, - SignatureService $signatureService, RequestQueueService $requestQueueService, - AccountService $accountService, CurlService $curlService, ConfigService $configService, - MiscService $miscService, LoggerInterface $logger + StreamRequest $streamRequest, + FollowsRequest $followsRequest, + CacheActorsRequest $cacheActorsRequest, + SignatureService $signatureService, + RequestQueueService $requestQueueService, + CurlService $curlService, + ConfigService $configService, + LoggerInterface $logger ) { $this->streamRequest = $streamRequest; $this->followsRequest = $followsRequest; + $this->cacheActorsRequest = $cacheActorsRequest; $this->requestQueueService = $requestQueueService; - $this->accountService = $accountService; $this->signatureService = $signatureService; $this->curlService = $curlService; $this->configService = $configService; - $this->miscService = $miscService; $this->logger = $logger; } @@ -254,15 +257,15 @@ class ActivityService { } catch (UnauthorizedFediverseException | RequestResultNotJsonException $e) { $this->requestQueueService->endRequest($queue, true); } catch (ActorDoesNotExistException | RequestContentException | RequestResultSizeException $e) { - $this->miscService->log( + $this->logger->notice( 'Error while managing request: ' . json_encode($request) . ' ' . get_class($e) . ': ' - . $e->getMessage(), 1 + . $e->getMessage() ); $this->requestQueueService->deleteRequest($queue); } catch (RequestNetworkException | RequestServerException $e) { - $this->miscService->log( + $this->logger->notice( 'Temporary error while managing request: RequestServerException - ' . json_encode($request) - . ' - ' . get_class($e) . ': ' . $e->getMessage(), 1 + . ' - ' . get_class($e) . ': ' . $e->getMessage() ); $this->requestQueueService->endRequest($queue, false); $this->failInstances[] = $host; @@ -279,12 +282,19 @@ class ActivityService { private function generateInstancePaths(ACore $activity): array { $instancePaths = []; foreach ($activity->getInstancePaths() as $instancePath) { - if ($instancePath->getType() === InstancePath::TYPE_FOLLOWERS) { - $instancePaths = array_merge( - $instancePaths, $this->generateInstancePathsFollowers($instancePath) - ); - } else { - $instancePaths[] = $instancePath; + switch ($instancePath->getType()) { + case InstancePath::TYPE_FOLLOWERS: + $instancePaths = + array_merge($instancePaths, $this->generateInstancePathsFollowers($instancePath)); + break; + + case InstancePath::TYPE_ALL: + $instancePaths = array_merge($instancePaths, $this->generateInstancePathsAll()); + break; + + default: + $instancePaths[] = $instancePath; + break; } } @@ -318,14 +328,30 @@ class ActivityService { $instancePaths[] = new InstancePath( $sharedInbox, InstancePath::TYPE_GLOBAL, $instancePath->getPriority() ); -// $result[] = $this->generateRequest( -// new InstancePath($sharedInbox, InstancePath::TYPE_GLOBAL), $activity -// ); } return $instancePaths; } + + /** + * @return InstancePath[] + */ + private function generateInstancePathsAll(): array { + $sharedInboxes = $this->cacheActorsRequest->getSharedInboxes(); + $instancePaths = []; + foreach ($sharedInboxes as $sharedInbox) { + $instancePaths[] = new InstancePath( + $sharedInbox, + InstancePath::TYPE_GLOBAL, + InstancePath::PRIORITY_LOW + ); + } + + return $instancePaths; + } + + private function generateRequestFromQueue(RequestQueue $queue): NCRequest { $path = $queue->getInstance(); diff --git a/lib/Service/CacheActorService.php b/lib/Service/CacheActorService.php index 45edcd97..dc4d922e 100644 --- a/lib/Service/CacheActorService.php +++ b/lib/Service/CacheActorService.php @@ -32,6 +32,7 @@ namespace OCA\Social\Service; use Exception; use OCA\Social\AP; +use OCA\Social\Db\ActorsRequest; use OCA\Social\Db\CacheActorsRequest; use OCA\Social\Exceptions\CacheActorDoesNotExistException; use OCA\Social\Exceptions\InvalidOriginException; @@ -62,6 +63,7 @@ class CacheActorService { use TArrayTools; private \OCP\IURLGenerator $urlGenerator; + private ActorsRequest $actorsRequest; private CacheActorsRequest $cacheActorsRequest; private CurlService $curlService; private FediverseService $fediverseService; @@ -73,6 +75,7 @@ class CacheActorService { */ public function __construct( IUrlGenerator $urlGenerator, + ActorsRequest $actorsRequest, CacheActorsRequest $cacheActorsRequest, CurlService $curlService, FediverseService $fediverseService, @@ -80,6 +83,7 @@ class CacheActorService { LoggerInterface $logger ) { $this->urlGenerator = $urlGenerator; + $this->actorsRequest = $actorsRequest; $this->cacheActorsRequest = $cacheActorsRequest; $this->curlService = $curlService; $this->fediverseService = $fediverseService; @@ -170,13 +174,18 @@ class CacheActorService { list($account, $instance) = explode('@', $account, 2); } - if ($instance === '' - || $this->configService->getCloudHost() === $instance - || $this->configService->getSocialAddress() === $instance) { - return $this->cacheActorsRequest->getFromLocalAccount($account); + if ($instance !== '' + && $this->configService->getCloudHost() !== $instance + && $this->configService->getSocialAddress() !== $instance) { + throw new CacheActorDoesNotExistException('Address does is not local'); } - throw new CacheActorDoesNotExistException('Address does is not local'); + $actor = $this->actorsRequest->getFromUsername($account); + if ($actor->getDeleted() > 0) { + throw new CacheActorDoesNotExistException('Account is deleted'); + } + + return $this->cacheActorsRequest->getFromLocalAccount($account); } /** diff --git a/lib/Service/MiscService.php b/lib/Service/MiscService.php index eb0aa444..a7abbc7a 100644 --- a/lib/Service/MiscService.php +++ b/lib/Service/MiscService.php @@ -31,9 +31,7 @@ declare(strict_types=1); namespace OCA\Social\Service; -use OC\User\NoUserException; use OCA\Social\AppInfo\Application; -use OCP\IUser; use OCP\IUserManager; use OCP\Util; use Psr\Log\LoggerInterface; @@ -76,23 +74,4 @@ class MiscService { return $ver[0]; } - - - /** - * @param string $userId - * - * @return IUser - * @throws NoUserException - */ - public function confirmUserId(string &$userId): IUser { - $user = $this->userManager->get($userId); - - if ($user === null) { - throw new NoUserException('user does not exist'); - } - - $userId = $user->getUID(); - - return $user; - } }