diff --git a/appinfo/database.xml b/appinfo/database.xml index 4f980f25..c4c3184f 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -77,6 +77,13 @@ true + + type + text + 31 + false + + actor_id text @@ -380,6 +387,13 @@ true + + details + text + 3000 + false + + creation timestamp diff --git a/appinfo/info.xml b/appinfo/info.xml index 88ffacd0..c6c624be 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Social 🎉 Nextcloud becomes part of the federated social networks! - 0.0.52 + 0.0.60 agpl Maxence Lange Julius Härtl diff --git a/composer.lock b/composer.lock index 0a63850a..37c1c411 100644 --- a/composer.lock +++ b/composer.lock @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/daita/my-small-php-tools.git", - "reference": "39ea0ce3f9442cb507c0bfaa4be36c3e90d6903e" + "reference": "36ea85a58ceb57a521c8f5a43effb4a7330e7b4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/39ea0ce3f9442cb507c0bfaa4be36c3e90d6903e", - "reference": "39ea0ce3f9442cb507c0bfaa4be36c3e90d6903e", + "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/36ea85a58ceb57a521c8f5a43effb4a7330e7b4c", + "reference": "36ea85a58ceb57a521c8f5a43effb4a7330e7b4c", "shasum": "" }, "require": { @@ -40,7 +40,7 @@ } ], "description": "My small PHP Tools", - "time": "2018-11-29T12:56:09+00:00" + "time": "2018-11-29T16:46:38+00:00" } ], "packages-dev": [], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 243217e2..d20ff7f1 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -32,7 +32,6 @@ namespace OCA\Social\AppInfo; use OCP\AppFramework\App; -use OCP\AppFramework\IAppContainer; class Application extends App { diff --git a/lib/Command/CacheRefresh.php b/lib/Command/CacheRefresh.php index fae76806..26a688fe 100644 --- a/lib/Command/CacheRefresh.php +++ b/lib/Command/CacheRefresh.php @@ -105,6 +105,9 @@ class CacheRefresh extends Base { $result = $this->actorService->manageCacheLocalActors(); $output->writeLn($result . ' local accounts regenerated'); + $result = $this->personService->missingCacheRemoteActors(); + $output->writeLn($result . ' remote accounts created'); + $result = $this->personService->manageCacheRemoteActors(); $output->writeLn($result . ' remote accounts updated'); diff --git a/lib/Command/QueueProcess.php b/lib/Command/QueueProcess.php index 780a87da..d9991b9d 100644 --- a/lib/Command/QueueProcess.php +++ b/lib/Command/QueueProcess.php @@ -31,9 +31,7 @@ declare(strict_types=1); namespace OCA\Social\Command; -use Exception; use OC\Core\Command\Base; -use OCA\Social\Exceptions\ActorDoesNotExistException; use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Service\ActivityService; diff --git a/lib/Controller/ActivityPubController.php b/lib/Controller/ActivityPubController.php index acd79fa3..83646e36 100644 --- a/lib/Controller/ActivityPubController.php +++ b/lib/Controller/ActivityPubController.php @@ -74,9 +74,6 @@ class ActivityPubController extends Controller { /** @var MiscService */ private $miscService; - /** @var NavigationController */ - private $navigationController; - /** * ActivityPubController constructor. @@ -94,13 +91,11 @@ class ActivityPubController extends Controller { IRequest $request, SocialPubController $socialPubController, ActivityService $activityService, ImportService $importService, FollowService $followService, ActorService $actorService, NotesRequest $notesRequest, - NavigationController $navigationController, MiscService $miscService ) { parent::__construct(Application::APP_NAME, $request); $this->socialPubController = $socialPubController; - $this->navigationController = $navigationController; $this->activityService = $activityService; $this->importService = $importService; @@ -128,7 +123,7 @@ class ActivityPubController extends Controller { */ public function actor(string $username): Response { if (!$this->checkSourceActivityStreams()) { - return $this->navigationController->public($username); + return $this->socialPubController->actor($username); } try { @@ -258,7 +253,7 @@ class ActivityPubController extends Controller { * * @return Response */ - public function followers(string $username, $data): Response { + public function followers(string $username): Response { if (!$this->checkSourceActivityStreams()) { return $this->socialPubController->followers($username); diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index dc2ee989..3059ec14 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -25,11 +25,8 @@ declare(strict_types=1); namespace OCA\Social\Controller; -use OCA\Activity\Data; -use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Service\ConfigService; use OCP\AppFramework\Controller; -use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; @@ -46,16 +43,12 @@ class ConfigController extends Controller { /** * @param string $cloudAddress + * * @return DataResponse */ public function setCloudAddress(string $cloudAddress): DataResponse { - try { - $this->configService->setCloudAddress($cloudAddress); - return new DataResponse([]); - } catch (SocialAppConfigException $e) { - return new DataResponse([ - 'message' => $e->getMessage() - ], Http::STATUS_BAD_REQUEST); - } + $this->configService->setCloudAddress($cloudAddress); + + return new DataResponse([]); } -} \ No newline at end of file +} diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index eab56caa..03dba180 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -219,6 +219,9 @@ class LocalController extends Controller { * @NoAdminRequired * @NoSubAdminRequired * + * @param int $since + * @param int $limit + * * @return DataResponse */ public function streamDirect(int $since = 0, int $limit = 5): DataResponse { @@ -294,12 +297,22 @@ class LocalController extends Controller { * @param string $search * * @return DataResponse + * @throws Exception */ public function accountsSearch(string $search): DataResponse { + try { + $viewer = $this->actorService->getActorFromUserId($this->userId, true); + } catch (Exception $e) { + throw new Exception(); + } + + $this->personService->setViewerId($viewer->getId()); + /* Look for an exactly matching account */ $match = null; try { $match = $this->personService->getFromAccount($search, false); + $match->setCompleteDetails(true); } catch (Exception $e) { } @@ -410,6 +423,7 @@ class LocalController extends Controller { * @NoCSRFRequired * @NoAdminRequired * @NoSubAdminRequired + * * @param string $id * @return DataResponse */ @@ -419,8 +433,10 @@ class LocalController extends Controller { if ($actor->gotIcon()) { $avatar = $actor->getIcon(); $document = $this->documentService->getFromCache($avatar->getId()); + return new FileDisplayResponse($document); } + return new NotFoundResponse(); } catch (Exception $e) { return $this->fail($e); diff --git a/lib/Controller/NavigationController.php b/lib/Controller/NavigationController.php index 3b76ea91..0911b12f 100644 --- a/lib/Controller/NavigationController.php +++ b/lib/Controller/NavigationController.php @@ -30,11 +30,9 @@ declare(strict_types=1); namespace OCA\Social\Controller; -use daita\MySmallPhpTools\Traits\TArrayTools; use daita\MySmallPhpTools\Traits\Nextcloud\TNCDataResponse; +use daita\MySmallPhpTools\Traits\TArrayTools; use Exception; -use OC\Files\Node\File; -use OC\Files\SimpleFS\SimpleFile; use OC\User\NoUserException; use OCA\Social\AppInfo\Application; use OCA\Social\Exceptions\AccountAlreadyExistsException; @@ -48,9 +46,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; -use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\Response; -use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; use OCP\IL10N; @@ -132,9 +128,11 @@ class NavigationController extends Controller { * @NoAdminRequired * @NoSubAdminRequired * + * @param string $path + * * @return TemplateResponse */ - public function navigate($path = ''): TemplateResponse { + public function navigate(string $path = ''): TemplateResponse { $data = [ 'serverData' => [ 'public' => false, @@ -230,10 +228,11 @@ class NavigationController extends Controller { * @NoAdminRequired * @NoSubAdminRequired * + * @param string $path + * * @return TemplateResponse - * @throws NoUserException */ - public function timeline($path = ''): TemplateResponse { + public function timeline(string $path = ''): TemplateResponse { return $this->navigate(); } @@ -244,43 +243,14 @@ class NavigationController extends Controller { * @NoAdminRequired * @NoSubAdminRequired * + * @param string $path + * * @return TemplateResponse - * @throws NoUserException */ - public function account($path = ''): TemplateResponse { + public function account(string $path = ''): TemplateResponse { return $this->navigate(); } - /** - * @NoCSRFRequired - * @PublicPage - * - * @param $username - * - * @return RedirectResponse|PublicTemplateResponse - */ - public function public($username) { - // Redirect to external instances - if (preg_match('/@[\w._-]+@[\w._-]+/', $username) === 1) { - $actor = $this->personService->getFromAccount(substr($username, 1)); - return new RedirectResponse($actor->getUrl()); - } - if (\OC::$server->getUserSession() - ->isLoggedIn()) { - return $this->navigate(); - } - - $data = [ - 'serverData' => [ - 'public' => true, - ] - ]; - $page = new PublicTemplateResponse(Application::APP_NAME, 'main', $data); - $page->setHeaderTitle($this->l10n->t('Social') . ' ' . $username); - - return $page; - } - /** * diff --git a/lib/Controller/QueueController.php b/lib/Controller/QueueController.php index 26cdddfa..c85dee2b 100644 --- a/lib/Controller/QueueController.php +++ b/lib/Controller/QueueController.php @@ -32,12 +32,10 @@ namespace OCA\Social\Controller; use daita\MySmallPhpTools\Traits\TAsync; use OCA\Social\AppInfo\Application; -use OCA\Social\Exceptions\ActorDoesNotExistException; use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Model\RequestQueue; use OCA\Social\Service\ActivityService; -use OCA\Social\Service\CurlService; use OCA\Social\Service\MiscService; use OCA\Social\Service\QueueService; use OCP\AppFramework\Controller; diff --git a/lib/Controller/SocialPubController.php b/lib/Controller/SocialPubController.php index 4783c90e..efd35049 100644 --- a/lib/Controller/SocialPubController.php +++ b/lib/Controller/SocialPubController.php @@ -31,13 +31,19 @@ namespace OCA\Social\Controller; use daita\MySmallPhpTools\Traits\Nextcloud\TNCDataResponse; +use Exception; use OCA\Social\AppInfo\Application; -use OCA\Social\Service\ActivityService; +use OCA\Social\Exceptions\CacheActorDoesNotExistException; +use OCA\Social\Model\ActivityPub\Person; +use OCA\Social\Service\ActivityPub\PersonService; use OCA\Social\Service\ActorService; use OCA\Social\Service\MiscService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\NotFoundResponse; use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\Template\PublicTemplateResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; use OCP\IRequest; class SocialPubController extends Controller { @@ -45,12 +51,18 @@ class SocialPubController extends Controller { use TNCDataResponse; - /** @var ActivityService */ - private $activityService; + /** @var string */ + private $userId; + + /** @var IL10N */ + private $l10n; /** @var ActorService */ private $actorService; + /** @var PersonService */ + private $personService; + /** @var MiscService */ private $miscService; @@ -58,19 +70,23 @@ class SocialPubController extends Controller { /** * SocialPubController constructor. * - * @param ActivityService $activityService - * @param ActorService $actorService + * @param $userId * @param IRequest $request + * @param IL10N $l10n + * @param ActorService $actorService + * @param PersonService $personService * @param MiscService $miscService */ public function __construct( - ActivityService $activityService, ActorService $actorService, IRequest $request, - MiscService $miscService + $userId, IRequest $request, IL10N $l10n, ActorService $actorService, + PersonService $personService, MiscService $miscService ) { parent::__construct(Application::APP_NAME, $request); - $this->activityService = $activityService; + $this->userId = $userId; + $this->l10n = $l10n; $this->actorService = $actorService; + $this->personService = $personService; $this->miscService = $miscService; } @@ -81,14 +97,48 @@ class SocialPubController extends Controller { * * @NoCSRFRequired * @PublicPage - * e* * * @param string $username * - * @return TemplateResponse + * @return Response */ - public function actor(string $username): TemplateResponse { - return new TemplateResponse(Application::APP_NAME, 'actor', [], 'blank'); + public function actor(string $username): Response { + + try { + $actor = $this->personService->getFromLocalAccount($username); + $actor->setCompleteDetails(true); + + $logged = false; + $ownAccount = false; + if ($this->userId !== null) { + $logged = true; + $local = $this->actorService->getActorFromUserId($this->userId, true); + if ($local->getId() === $actor->getId()) { + $ownAccount = true; + } else { + $this->fillActorWithLinks($actor, $local); + } + } + + $data = [ + 'serverData' => [ + 'public' => true, + ], + 'actor' => $actor, + 'logged' => $logged, + 'ownAccount' => $ownAccount + ]; + + + $page = new PublicTemplateResponse(Application::APP_NAME, 'main', $data); + $page->setHeaderTitle($this->l10n->t('Social') . ' ' . $username); + + return $page; + } catch (CacheActorDoesNotExistException $e) { + return new NotFoundResponse(); + } catch (Exception $e) { + return $this->fail($e); + } } @@ -139,6 +189,15 @@ class SocialPubController extends Controller { return $this->success([$username, $postId]); } + + /** + * @param Person $actor + * @param Person $local + */ + private function fillActorWithLinks(Person $actor, Person $local) { + $links = $this->actorService->getLinksBetweenPersons($local, $actor); + $actor->addDetailArray('link', $links); + } + } - diff --git a/lib/Cron/Queue.php b/lib/Cron/Queue.php index 58e759a4..1dd1b8c4 100644 --- a/lib/Cron/Queue.php +++ b/lib/Cron/Queue.php @@ -31,18 +31,11 @@ declare(strict_types=1); namespace OCA\Social\Cron; -use Exception; use OC\BackgroundJob\TimedJob; use OCA\Social\AppInfo\Application; -use OCA\Social\Exceptions\ActorDoesNotExistException; use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SocialAppConfigException; -use OCA\Social\Service\ActivityPub\DocumentService; -use OCA\Social\Service\ActivityPub\PersonService; use OCA\Social\Service\ActivityService; -use OCA\Social\Service\ActorService; -use OCA\Social\Service\CacheService; -use OCA\Social\Service\ConfigService; use OCA\Social\Service\MiscService; use OCA\Social\Service\QueueService; use OCP\AppFramework\QueryException; diff --git a/lib/Db/CacheActorsRequest.php b/lib/Db/CacheActorsRequest.php index f5b4454e..3a9a07b0 100644 --- a/lib/Db/CacheActorsRequest.php +++ b/lib/Db/CacheActorsRequest.php @@ -33,6 +33,7 @@ namespace OCA\Social\Db; use DateTime; use Exception; use OCA\Social\Exceptions\CacheActorDoesNotExistException; +use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Model\ActivityPub\Person; use OCA\Social\Service\ConfigService; use OCA\Social\Service\MiscService; @@ -85,6 +86,7 @@ class CacheActorsRequest extends CacheActorsRequestBuilder { ->setValue('summary', $qb->createNamedParameter($actor->getSummary())) ->setValue('public_key', $qb->createNamedParameter($actor->getPublicKey())) ->setValue('source', $qb->createNamedParameter($actor->getSource())) + ->setValue('details', $qb->createNamedParameter(json_encode($actor->getDetails()))) ->setValue( 'creation', $qb->createNamedParameter(new DateTime('now'), IQueryBuilder::PARAM_DATE) @@ -132,14 +134,61 @@ class CacheActorsRequest extends CacheActorsRequestBuilder { * * @param string $account * + * @param string $viewerId + * * @return Person * @throws CacheActorDoesNotExistException */ - public function getFromAccount(string $account): Person { + public function getFromAccount(string $account, string $viewerId = ''): Person { $qb = $this->getCacheActorsSelectSql(); $this->limitToAccount($qb, $account); $this->leftJoinCacheDocuments($qb, 'icon_id'); + if ($viewerId !== '') { + $this->leftJoinFollowAsViewer($qb, 'id', $viewerId, true, 'as_follower'); + $this->leftJoinFollowAsViewer($qb, 'id', $viewerId, false, 'as_followed'); + } + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new CacheActorDoesNotExistException(); + } + + $account = $this->parseCacheActorsSelectSql($data); + + try { + $this->parseFollowLeftJoin($data, 'as_follower'); + $account->addDetailBool('follower', true); + } catch (InvalidResourceException $e) { + } + + try { + $this->parseFollowLeftJoin($data, 'as_followed'); + $account->addDetailBool('following', true); + } catch (InvalidResourceException $e) { + } + + return $account; + } + + + /** + * get Cached version of a local Actor, based on the preferred username + * + * @param string $account + * + * @return Person + * @throws CacheActorDoesNotExistException + */ + public function getFromLocalAccount(string $account): Person { + $qb = $this->getCacheActorsSelectSql(); + $this->limitToPreferredUsername($qb, $account); + $this->limitToLocal($qb, true); + $this->leftJoinCacheDocuments($qb, 'icon_id'); + $cursor = $qb->execute(); $data = $cursor->fetch(); $cursor->closeCursor(); @@ -154,18 +203,42 @@ class CacheActorsRequest extends CacheActorsRequestBuilder { /** * @param string $search + * @param string $viewerId * * @return Person[] */ - public function searchAccounts(string $search): array { + public function searchAccounts(string $search, string $viewerId = ''): array { $qb = $this->getCacheActorsSelectSql(); $this->searchInAccount($qb, $search); $this->leftJoinCacheDocuments($qb, 'icon_id'); + if ($viewerId !== '') { + $this->leftJoinFollowAsViewer($qb, 'id', $viewerId, true, 'as_follower'); + $this->leftJoinFollowAsViewer($qb, 'id', $viewerId, false, 'as_followed'); + } + $accounts = []; $cursor = $qb->execute(); while ($data = $cursor->fetch()) { - $accounts[] = $this->parseCacheActorsSelectSql($data); + $account = $this->parseCacheActorsSelectSql($data); + + if ($viewerId !== '') { + try { + $this->parseFollowLeftJoin($data, 'as_follower'); + $account->addDetailBool('following', true); + } catch (InvalidResourceException $e) { + } + + try { + $this->parseFollowLeftJoin($data, 'as_followed'); + $account->addDetailBool('follower', true); + } catch (InvalidResourceException $e) { + } + + $account->setCompleteDetails(true); + } + + $accounts[] = $account; } $cursor->closeCursor(); diff --git a/lib/Db/CacheActorsRequestBuilder.php b/lib/Db/CacheActorsRequestBuilder.php index 1d6e9668..cb9ce58a 100644 --- a/lib/Db/CacheActorsRequestBuilder.php +++ b/lib/Db/CacheActorsRequestBuilder.php @@ -80,7 +80,7 @@ class CacheActorsRequestBuilder extends CoreRequestBuilder { 'ca.id', 'ca.account', 'ca.following', 'ca.followers', 'ca.inbox', 'ca.shared_inbox', 'ca.outbox', 'ca.featured', 'ca.url', 'ca.type', 'ca.preferred_username', 'ca.name', 'ca.summary', - 'ca.public_key', 'ca.local', 'ca.source', 'ca.creation' + 'ca.public_key', 'ca.local', 'ca.details', 'ca.source', 'ca.creation' ) ->from(self::TABLE_CACHE_ACTORS, 'ca'); diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php index 8d4179e6..114dc671 100644 --- a/lib/Db/CoreRequestBuilder.php +++ b/lib/Db/CoreRequestBuilder.php @@ -37,6 +37,7 @@ use Doctrine\DBAL\Query\QueryBuilder; use Exception; use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Model\ActivityPub\Document; +use OCA\Social\Model\ActivityPub\Follow; use OCA\Social\Model\ActivityPub\Image; use OCA\Social\Model\ActivityPub\Person; use OCA\Social\Service\ConfigService; @@ -134,7 +135,7 @@ class CoreRequestBuilder { * @param string $username */ protected function limitToPreferredUsername(IQueryBuilder &$qb, string $username) { - $this->limitToDBField($qb, 'preferred_username', $username); + $this->limitToDBField($qb, 'preferred_username', $username, false); } /** @@ -195,6 +196,18 @@ class CoreRequestBuilder { } + /** + * Limit the request to the FollowId + * + * @param IQueryBuilder $qb + * @param bool $accepted + */ + protected function limitToAccepted(IQueryBuilder &$qb, bool $accepted) { + $this->limitToDBField($qb, 'accepted', ($accepted) ? '1' : '0'); + + } + + /** * Limit the request to the ServiceId * @@ -272,6 +285,17 @@ class CoreRequestBuilder { } + /** + * Limit the request to the url + * + * @param IQueryBuilder $qb + * @param string $actorId + */ + protected function limitToAttributedTo(IQueryBuilder &$qb, string $actorId) { + $this->limitToDBField($qb, 'attributed_to', $actorId, false); + } + + /** * Limit the request to the status * @@ -507,7 +531,6 @@ class CoreRequestBuilder { $expr = $qb->expr(); $pf = $this->defaultSelectAlias; -// /** @noinspection PhpMethodParametersCountMismatchInspection */ $qb->selectAlias('ca.id', 'cacheactor_id') ->selectAlias('ca.type', 'cacheactor_type') ->selectAlias('ca.account', 'cacheactor_account') @@ -569,7 +592,6 @@ class CoreRequestBuilder { $expr = $qb->expr(); $pf = $this->defaultSelectAlias; -// /** @noinspection PhpMethodParametersCountMismatchInspection */ $qb->selectAlias('cd.id', 'cachedocument_id') ->selectAlias('cd.type', 'cachedocument_type') ->selectAlias('cd.mime_type', 'cachedocument_mime_type') @@ -612,6 +634,75 @@ class CoreRequestBuilder { return $document; } + + /** + * @param IQueryBuilder $qb + * @param string $fieldActorId + * @param string $viewerId + * @param bool $asFollower + * @param string $prefix + */ + protected function leftJoinFollowAsViewer( + IQueryBuilder &$qb, string $fieldActorId, string $viewerId, bool $asFollower = true, + string $prefix = 'follow' + ) { + if ($qb->getType() !== QueryBuilder::SELECT) { + return; + } + + $expr = $qb->expr(); + $pf = $this->defaultSelectAlias; + + $andX = $expr->andX(); + if ($asFollower === true) { + $andX->add($expr->eq($pf . '.' . $fieldActorId, $prefix . '_f.object_id')); + $andX->add($expr->eq($prefix . '_f.actor_id', $qb->createNamedParameter($viewerId))); + } else { + $andX->add($expr->eq($pf . '.' . $fieldActorId, $prefix . '_f.actor_id')); + $andX->add($expr->eq($prefix . '_f.object_id', $qb->createNamedParameter($viewerId))); + } + + $qb->selectAlias($prefix . '_f.id', $prefix . '_id') + ->selectAlias($prefix . '_f.type', $prefix . '_type') + ->selectAlias($prefix . '_f.actor_id', $prefix . '_actor_id') + ->selectAlias($prefix . '_f.object_id', $prefix . '_object_id') + ->selectAlias($prefix . '_f.follow_id', $prefix . '_follow_id') + ->selectAlias($prefix . '_f.creation', $prefix . '_creation') + ->leftJoin( + $this->defaultSelectAlias, CoreRequestBuilder::TABLE_SERVER_FOLLOWS, $prefix . '_f', + $andX + ); + } + + + /** + * @param array $data + * @param string $prefix + * + * @return Follow + * @throws InvalidResourceException + */ + protected function parseFollowLeftJoin(array $data, string $prefix): Follow { + $new = []; + + $length = strlen($prefix) + 1; + foreach ($data as $k => $v) { + if (substr($k, 0, $length) === $prefix . '_') { + $new[substr($k, $length)] = $v; + } + } + + $follow = new Follow(); + $follow->importFromDatabase($new); + + if ($follow->getType() !== Follow::TYPE) { + throw new InvalidResourceException(); + } + + return $follow; + } + + } diff --git a/lib/Db/FollowsRequest.php b/lib/Db/FollowsRequest.php index b4197b17..fdcb04ff 100644 --- a/lib/Db/FollowsRequest.php +++ b/lib/Db/FollowsRequest.php @@ -31,6 +31,7 @@ declare(strict_types=1); namespace OCA\Social\Db; +use daita\MySmallPhpTools\Traits\TArrayTools; use OCA\Social\Exceptions\FollowDoesNotExistException; use OCA\Social\Model\ActivityPub\Follow; @@ -43,6 +44,9 @@ use OCA\Social\Model\ActivityPub\Follow; class FollowsRequest extends FollowsRequestBuilder { + use TArrayTools; + + /** * Insert a new Note in the database. * @@ -52,6 +56,7 @@ class FollowsRequest extends FollowsRequestBuilder { $qb = $this->getFollowsInsertSql(); $qb->setValue('id', $qb->createNamedParameter($follow->getId())) ->setValue('actor_id', $qb->createNamedParameter($follow->getActorId())) + ->setValue('type', $qb->createNamedParameter($follow->getType())) ->setValue('object_id', $qb->createNamedParameter($follow->getObjectId())) ->setValue('follow_id', $qb->createNamedParameter($follow->getFollowId())); @@ -96,6 +101,42 @@ class FollowsRequest extends FollowsRequestBuilder { } + /** + * @param string $actorId + * + * @return int + */ + public function countFollowers(string $actorId): int { + $qb = $this->countFollowsSelectSql(); + $this->limitToObjectId($qb, $actorId); + $this->limitToAccepted($qb, true); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + return $this->getInt('count', $data, 0); + } + + + /** + * @param string $actorId + * + * @return int + */ + public function countFollowing(string $actorId): int { + $qb = $this->countFollowsSelectSql(); + $this->limitToActorId($qb, $actorId); + $this->limitToAccepted($qb, true); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + return $this->getInt('count', $data, 0); + } + + /** * @param string $followId * diff --git a/lib/Db/FollowsRequestBuilder.php b/lib/Db/FollowsRequestBuilder.php index 1947dd4c..0f50b37e 100644 --- a/lib/Db/FollowsRequestBuilder.php +++ b/lib/Db/FollowsRequestBuilder.php @@ -83,7 +83,23 @@ class FollowsRequestBuilder extends CoreRequestBuilder { $qb = $this->dbConnection->getQueryBuilder(); /** @noinspection PhpMethodParametersCountMismatchInspection */ - $qb->select('f.id', 'f.actor_id', 'f.object_id', 'f.follow_id', 'f.creation') + $qb->select('f.id', 'f.type', 'f.actor_id', 'f.object_id', 'f.follow_id', 'f.creation') + ->from(self::TABLE_SERVER_FOLLOWS, 'f'); + + $this->defaultSelectAlias = 'f'; + + return $qb; + } + + + /** + * Base of the Sql Select request for Shares + * + * @return IQueryBuilder + */ + protected function countFollowsSelectSql(): IQueryBuilder { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->selectAlias($qb->createFunction('COUNT(*)'), 'count') ->from(self::TABLE_SERVER_FOLLOWS, 'f'); $this->defaultSelectAlias = 'f'; @@ -112,10 +128,7 @@ class FollowsRequestBuilder extends CoreRequestBuilder { */ protected function parseFollowsSelectSql($data): Follow { $follow = new Follow(); - $follow->setId($data['id']) - ->setActorId($data['actor_id']) - ->setObjectId($data['object_id']); - $follow->setFollowId($data['follow_id']); + $follow->importFromDatabase($data); try { $actor = $this->parseCacheActorsLeftJoin($data); diff --git a/lib/Db/NotesRequest.php b/lib/Db/NotesRequest.php index e2c7d9c8..f4bf6842 100644 --- a/lib/Db/NotesRequest.php +++ b/lib/Db/NotesRequest.php @@ -137,6 +137,23 @@ class NotesRequest extends NotesRequestBuilder { } + /** + * @param string $actorId + * + * @return int + */ + public function countNotesFromActorId(string $actorId): int { + $qb = $this->countNotesSelectSql(); + $this->limitToAttributedTo($qb, $actorId); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + return $this->getInt('count', $data, 0); + } + + /** * @param string $actorId * @param int $since diff --git a/lib/Db/NotesRequestBuilder.php b/lib/Db/NotesRequestBuilder.php index 90ff24e1..e6f2cb18 100644 --- a/lib/Db/NotesRequestBuilder.php +++ b/lib/Db/NotesRequestBuilder.php @@ -92,6 +92,22 @@ class NotesRequestBuilder extends CoreRequestBuilder { } + /** + * Base of the Sql Select request for Shares + * + * @return IQueryBuilder + */ + protected function countNotesSelectSql(): IQueryBuilder { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->selectAlias($qb->createFunction('COUNT(*)'), 'count') + ->from(self::TABLE_SERVER_NOTES, 'sn'); + + $this->defaultSelectAlias = 'sn'; + + return $qb; + } + + /** * Base of the Sql Delete request * diff --git a/lib/Db/RequestQueueRequest.php b/lib/Db/RequestQueueRequest.php index 4e07ffba..941c1913 100644 --- a/lib/Db/RequestQueueRequest.php +++ b/lib/Db/RequestQueueRequest.php @@ -35,7 +35,6 @@ use DateTime; use Exception; use OCA\Social\Exceptions\QueueStatusException; use OCA\Social\Model\RequestQueue; -use OCA\Social\Service\QueueService; use OCP\DB\QueryBuilder\IQueryBuilder; diff --git a/lib/Model/ActivityPub/ACore.php b/lib/Model/ActivityPub/ACore.php index 6cc36c17..80f134a4 100644 --- a/lib/Model/ActivityPub/ACore.php +++ b/lib/Model/ActivityPub/ACore.php @@ -872,7 +872,6 @@ abstract class ACore implements JsonSerializable { $this->setPublished($this->get('published', $data, '')); $this->setActorId($this->get('actor', $data, '')); $this->setObjectId($this->get('object', $data, '')); - $this->setLocal(($this->getInt('local', $data, 0) === 1)); } diff --git a/lib/Model/ActivityPub/Follow.php b/lib/Model/ActivityPub/Follow.php index c7818826..26546265 100644 --- a/lib/Model/ActivityPub/Follow.php +++ b/lib/Model/ActivityPub/Follow.php @@ -88,6 +88,16 @@ class Follow extends ACore implements JsonSerializable { } + /** + * @param array $data + */ + public function importFromDatabase(array $data) { + parent::importFromDatabase($data); + + $this->setFollowId($this->get('follow_id', $data, '')); + } + + /** * @return array */ diff --git a/lib/Model/ActivityPub/Person.php b/lib/Model/ActivityPub/Person.php index 41f3b375..bfa25a16 100644 --- a/lib/Model/ActivityPub/Person.php +++ b/lib/Model/ActivityPub/Person.php @@ -85,6 +85,10 @@ class Person extends ACore implements JsonSerializable { /** @var string */ private $featured = ''; + /** @var array */ + private $details = []; + + /** * Person constructor. * @@ -339,6 +343,73 @@ class Person extends ACore implements JsonSerializable { } + /** + * @return array + */ + public function getDetails(): array { + return $this->details; + } + + /** + * @param string $detail + * @param string $value + * + * @return Person + */ + public function addDetail(string $detail, string $value): Person { + $this->details[$detail] = $value; + + return $this; + } + + /** + * @param string $detail + * @param int $value + * + * @return Person + */ + public function addDetailInt(string $detail, int $value): Person { + $this->details[$detail] = $value; + + return $this; + } + + /** + * @param string $detail + * @param array $value + * + * @return Person + */ + public function addDetailArray(string $detail, array $value): Person { + $this->details[$detail] = $value; + + return $this; + } + + /** + * @param string $detail + * @param bool $value + * + * @return Person + */ + public function addDetailBool(string $detail, bool $value): Person { + $this->details[$detail] = $value; + + return $this; + } + + /** + * @param array $details + * + * @return Person + */ + public function setDetails(array $details): Person { + $this->details = $details; + + return $this; + } + + /** * @param array $data * @@ -365,12 +436,6 @@ class Person extends ACore implements JsonSerializable { $this->setIcon($icon); } - -// ->setCreation($this->getInt('creation', $data, 0)); - -// if ($this->getPreferredUsername() === '') { -// $this->setType('Invalid'); -// } } @@ -390,11 +455,8 @@ class Person extends ACore implements JsonSerializable { ->setFollowing($this->get('following', $data, '')) ->setSharedInbox($this->get('shared_inbox', $data, '')) ->setFeatured($this->get('featured', $data, '')) + ->setDetails($this->getArray('details', $data, [])) ->setCreation($this->getInt('creation', $data, 0)); - -// if ($this->getPreferredUsername() === '') { -// $this->setType('Invalid'); -// } } @@ -402,7 +464,7 @@ class Person extends ACore implements JsonSerializable { * @return array */ public function jsonSerialize(): array { - return array_merge( + $result = array_merge( parent::jsonSerialize(), [ 'aliases' => [ @@ -425,8 +487,12 @@ class Person extends ACore implements JsonSerializable { ] ] ); + + if ($this->isCompleteDetails()) { + $result['details'] = $this->getDetails(); + } + + return $result; } - } - diff --git a/lib/Service/ActivityPub/NoteService.php b/lib/Service/ActivityPub/NoteService.php index e457e135..ebce852b 100644 --- a/lib/Service/ActivityPub/NoteService.php +++ b/lib/Service/ActivityPub/NoteService.php @@ -336,9 +336,12 @@ class NoteService implements ICoreService { /** * @param Person $actor * + * @param int $since + * @param int $limit + * * @return Note[] */ - public function getDirectNotesForActor(Person $actor, $since, $limit): array { + public function getDirectNotesForActor(Person $actor, int $since = 0, int $limit = 5): array { return $this->notesRequest->getDirectNotesForActorId($actor->getId(), $since, $limit); } diff --git a/lib/Service/ActivityPub/PersonService.php b/lib/Service/ActivityPub/PersonService.php index c170f988..c6d9a074 100644 --- a/lib/Service/ActivityPub/PersonService.php +++ b/lib/Service/ActivityPub/PersonService.php @@ -76,6 +76,10 @@ class PersonService implements ICoreService { private $miscService; + /** @var string */ + private $viewerId = ''; + + /** * UndoService constructor. * @@ -97,6 +101,18 @@ class PersonService implements ICoreService { } + /** + * @param string $viewerId + */ + public function setViewerId(string $viewerId) { + $this->viewerId = $viewerId; + } + + public function getViewerId(): string { + return $this->viewerId; + } + + /** * @param Person $actor * @param bool $refresh @@ -169,7 +185,7 @@ class PersonService implements ICoreService { public function getFromAccount(string $account, bool $retrieve = true): Person { try { - $actor = $this->cacheActorsRequest->getFromAccount($account); + $actor = $this->cacheActorsRequest->getFromAccount($account, $this->getViewerId()); } catch (CacheActorDoesNotExistException $e) { if (!$retrieve) { throw new CacheActorDoesNotExistException(); @@ -189,6 +205,17 @@ class PersonService implements ICoreService { } + /** + * @param string $account + * + * @return Person + * @throws CacheActorDoesNotExistException + */ + public function getFromLocalAccount(string $account): Person { + return $this->cacheActorsRequest->getFromLocalAccount($account); + } + + /** * @param array $object * @@ -208,18 +235,7 @@ class PersonService implements ICoreService { } $actor->setSource(json_encode($object, JSON_UNESCAPED_SLASHES)); -// $actor->setPreferredUsername($this->get('preferredUsername', $object, '')); -// $actor->setPublicKey($this->get('publicKey.publicKeyPem', $object)); -// $actor->setSharedInbox($this->get('endpoints.sharedInbox', $object)); -// $actor->setAccount($actor->getPreferredUsername() . '@' . $this->get('_host', $object)); -// -// $icon = new Image($actor); -// $icon->setUrlCloud($this->configService->getCloudAddress()); -// $icon->import($this->getArray('icon', $object, [])); -// if ($icon->getType() === Image::TYPE) { -// $actor->setIcon($icon); -// } -// + return $actor; } @@ -229,7 +245,7 @@ class PersonService implements ICoreService { * @return Person[] */ public function searchCachedAccounts(string $search): array { - return $this->cacheActorsRequest->searchAccounts($search); + return $this->cacheActorsRequest->searchAccounts($search, $this->getViewerId()); } @@ -260,6 +276,25 @@ class PersonService implements ICoreService { } + /** + * @throws Exception + * @return int + */ + public function missingCacheRemoteActors(): int { + // TODO - looking for missing cache remote actors... + $missing = []; + + foreach ($missing as $item) { + try { + $this->getFromId($item->getId()); + } catch (Exception $e) { + } + } + + return sizeof($missing); + } + + /** * @throws Exception * @return int diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index 37bf5606..57ba561c 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -284,8 +284,12 @@ class ActivityService { ); } catch (ActorDoesNotExistException $e) { $this->queueService->deleteRequest($queue); + + return; } catch (Request410Exception $e) { $this->queueService->deleteRequest($queue); + + return; } try { diff --git a/lib/Service/ActorService.php b/lib/Service/ActorService.php index 70fb824e..33ee2cfd 100644 --- a/lib/Service/ActorService.php +++ b/lib/Service/ActorService.php @@ -34,8 +34,11 @@ use daita\MySmallPhpTools\Traits\TArrayTools; use Exception; use OC\User\NoUserException; use OCA\Social\Db\ActorsRequest; +use OCA\Social\Db\FollowsRequest; +use OCA\Social\Db\NotesRequest; use OCA\Social\Exceptions\AccountAlreadyExistsException; use OCA\Social\Exceptions\ActorDoesNotExistException; +use OCA\Social\Exceptions\FollowDoesNotExistException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Model\ActivityPub\Person; use OCA\Social\Service\ActivityPub\PersonService; @@ -55,6 +58,12 @@ class ActorService { /** @var ActorsRequest */ private $actorsRequest; + /** @var FollowsRequest */ + private $followsRequest; + + /** @var NotesRequest */ + private $notesRequest; + /** @var PersonService */ private $personService; @@ -69,15 +78,19 @@ class ActorService { * ActorService constructor. * * @param ActorsRequest $actorsRequest + * @param FollowsRequest $followsRequest + * @param NotesRequest $notesRequest * @param PersonService $personService * @param ConfigService $configService * @param MiscService $miscService */ public function __construct( - ActorsRequest $actorsRequest, PersonService $personService, ConfigService $configService, - MiscService $miscService + ActorsRequest $actorsRequest, FollowsRequest $followsRequest, NotesRequest $notesRequest, + PersonService $personService, ConfigService $configService, MiscService $miscService ) { $this->actorsRequest = $actorsRequest; + $this->followsRequest = $followsRequest; + $this->notesRequest = $notesRequest; $this->personService = $personService; $this->configService = $configService; $this->miscService = $miscService; @@ -114,15 +127,26 @@ class ActorService { /** * @param string $userId + * @param bool $create * * @return Person + * @throws AccountAlreadyExistsException * @throws ActorDoesNotExistException * @throws NoUserException * @throws SocialAppConfigException */ - public function getActorFromUserId(string $userId): Person { + public function getActorFromUserId(string $userId, bool $create = false): Person { $this->miscService->confirmUserId($userId); - $actor = $this->actorsRequest->getFromUserId($userId); + try { + $actor = $this->actorsRequest->getFromUserId($userId); + } catch (ActorDoesNotExistException $e) { + if ($create) { + $this->createActor($userId, $userId); + $actor = $this->actorsRequest->getFromUserId($userId); + } else { + throw new ActorDoesNotExistException(); + } + } return $actor; } @@ -177,6 +201,35 @@ class ActorService { } + /** + * @param Person $local + * @param Person $actor + * + * @return array + */ + public function getLinksBetweenPersons(Person $local, Person $actor): array { + + $links = [ + 'follower' => false, + 'following' => false + ]; + + try { + $this->followsRequest->getByPersons($local->getId(), $actor->getId()); + $links['following'] = true; + } catch (FollowDoesNotExistException $e) { + } + + try { + $this->followsRequest->getByPersons($actor->getId(), $local->getId()); + $links['follower'] = true; + } catch (FollowDoesNotExistException $e) { + } + + return $links; + } + + /** * @param string $username * @param bool $refresh @@ -185,7 +238,14 @@ class ActorService { */ public function cacheLocalActorByUsername(string $username, bool $refresh = false) { try { - $actor = $this->getActor($username); + $actor = $this->getActor($username);; + $count = [ + 'followers', $this->followsRequest->countFollowers($actor->getId()), + 'following', $this->followsRequest->countFollowing($actor->getId()), + 'post', $this->notesRequest->countNotesFromActorId($actor->getId()) + ]; + $actor->addDetailArray('count', $count); + $this->personService->cacheLocalActor($actor, $refresh); } catch (ActorDoesNotExistException $e) { } diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 968859dd..83741f16 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -237,8 +237,6 @@ class ConfigService { /** * @param string $cloudAddress - * - * @return string */ public function setCloudAddress(string $cloudAddress) { $this->setAppValue(self::SOCIAL_ADDRESS, $cloudAddress); diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index 2218a52d..e073cdf7 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -249,8 +249,6 @@ class CurlService { /** * @param int $code * - * @param Request $request - * * @throws Request410Exception */ private function parseRequestResultCode410(int $code) { diff --git a/lib/Service/MiscService.php b/lib/Service/MiscService.php index 0feb57fc..6be52d60 100644 --- a/lib/Service/MiscService.php +++ b/lib/Service/MiscService.php @@ -31,7 +31,6 @@ declare(strict_types=1); namespace OCA\Social\Service; -use Exception; use OC\User\NoUserException; use OCA\Social\AppInfo\Application; use OCP\ILogger; diff --git a/lib/Service/PostService.php b/lib/Service/PostService.php index 980b6089..0ea482be 100644 --- a/lib/Service/PostService.php +++ b/lib/Service/PostService.php @@ -33,7 +33,6 @@ namespace OCA\Social\Service; use Exception; use OC\User\NoUserException; use OCA\Social\Exceptions\ActorDoesNotExistException; -use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Model\Post; diff --git a/lib/Service/QueueService.php b/lib/Service/QueueService.php index 36cc46dd..d7228acb 100644 --- a/lib/Service/QueueService.php +++ b/lib/Service/QueueService.php @@ -78,6 +78,7 @@ class QueueService { /** * @param array $instancePaths * @param ACore $item + * @param string $author * * @return string * @throws Exception diff --git a/whenanaccountisdeleter.json b/whenanaccountisdeleter.json new file mode 100644 index 00000000..af8c879e --- /dev/null +++ b/whenanaccountisdeleter.json @@ -0,0 +1,45 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "https://mastodon.social/users/twospirit#delete", + "type": "Delete", + "actor": "https://mastodon.social/users/twospirit", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": "https://mastodon.social/users/twospirit", + "signature": { + "type": "RsaSignature2017", + "creator": "https://mastodon.social/users/twospirit#main-key", + "created": "2018-11-26T14:33:30Z", + "signatureValue": "e20O0AYwAGLgHyb/xD1fO+v7gnE8aONFR3iMb/4ULd+Qjz4fV1/ay0IRFdatWGRP4uG/XzjdW6hJTR69wjQUag9k0JOvHkOssUHIXkmBEsKZURyEVWrjT1+Dyx1ZFwsbhjSXCkvbz70mq+1JPx2zDK+fTsG8TPIKBpvj9LYmQbF3Z4n7wRRxibCCL8oGlrHlwWwABYkZKoruLYJya9a6eVvbCD1P5TO+M1CUdMHhqez8k3ll50ZsGRlHqnojR0V9AjMixuMNMx4qvcC+wwJb4QBorfPJDh843Pw4lybwbs5/bXwErxR+Infc61w/dkyw1MvZdPYD2dy+uOHdndRRdQ==" + } +}