diff --git a/appinfo/routes.php b/appinfo/routes.php index 47d0a763..f30b9ffa 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -92,6 +92,7 @@ return [ ['name' => 'Api#statusContext', 'url' => '/api/v1/statuses/{nid}/context', 'verb' => 'GET'], ['name' => 'Api#statusAction', 'url' => '/api/v1/statuses/{nid}/{act}', 'verb' => 'POST'], + ['name' => 'Api#relationships', 'url' => '/api/v1/accounts/relationships', 'verb' => 'GET'], ['name' => 'Api#accountStatuses', 'url' => '/api/v1/accounts/{account}/statuses', 'verb' => 'GET'], ['name' => 'Api#accountFollowers', 'url' => '/api/v1/accounts/{account}/followers', 'verb' => 'GET'], ['name' => 'Api#accountFollowing', 'url' => '/api/v1/accounts/{account}/following', 'verb' => 'GET'], diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index 70d94ee6..3f680726 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -510,6 +510,24 @@ class ApiController extends Controller { } + /** + * @NoCSRFRequired + * @PublicPage + * + * @param array $id + * + * @return DataResponse + */ + public function relationships(array $id): DataResponse { + try { + $this->initViewer(true); + + return new DataResponse($this->followService->getRelationships($id), Http::STATUS_OK); + } catch (Exception $e) { + return $this->error($e->getMessage()); + } + } + /** * @NoCSRFRequired * @PublicPage diff --git a/lib/Db/CacheActorsRequest.php b/lib/Db/CacheActorsRequest.php index 4c17abed..462f52eb 100644 --- a/lib/Db/CacheActorsRequest.php +++ b/lib/Db/CacheActorsRequest.php @@ -345,4 +345,20 @@ class CacheActorsRequest extends CacheActorsRequestBuilder { return $this->getCacheActorsFromRequest($qb); } + + /** + * As of today, returned format is not important. Remove this line if this method + * is used somewhere else with the need of a specific format + * + * @param array $ids + * + * @return array + */ + public function getFromNids(array $ids): array { + $qb = $this->getCacheActorsSelectSql(); + + $qb->limitInArray('nid', $ids); + + return $this->getCacheActorsFromRequest($qb); + } } diff --git a/lib/Db/FollowsRequest.php b/lib/Db/FollowsRequest.php index edf6e76a..920fe44b 100644 --- a/lib/Db/FollowsRequest.php +++ b/lib/Db/FollowsRequest.php @@ -333,4 +333,29 @@ class FollowsRequest extends FollowsRequestBuilder { $qb->executeStatement(); } + + + /** + * Returns everything related to a list of actorIds. + * Looking at actor_id_prim and object_id_prim. + * + * @param array $actorIds + * + * @return Follow[] + */ + public function getFollows(array $actorIds): array { + $qb = $this->getFollowsSelectSql(); + $qb->limitToType(Follow::TYPE); + + $prims = []; + foreach ($actorIds as $actorId) { + $prims[] = $qb->prim($actorId); + } + + $orX = $qb->expr()->orX(); + $orX->add($qb->exprLimitInArray('actor_id_prim', $prims)); + $orX->add($qb->exprLimitInArray('object_id_prim', $prims)); + + return $this->getFollowsFromRequest($qb); + } } diff --git a/lib/Model/Relationship.php b/lib/Model/Relationship.php new file mode 100644 index 00000000..f0f0b82e --- /dev/null +++ b/lib/Model/Relationship.php @@ -0,0 +1,193 @@ + + * @copyright 2018, Maxence Lange + * @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\Social\Model; + +use JsonSerializable; +use OCA\Social\Tools\Traits\TArrayTools; + +class Relationship implements JsonSerializable { + use TArrayTools; + + private int $id; + private bool $following = false; + private bool $showingReblogs = false; + private bool $notifying = false; + private bool $followedBy = false; + private bool $blocking = false; + private bool $blockedBy = false; + private bool $muting = false; + private bool $mutingNotifications = false; + private bool $requested = false; + private bool $domainBlocking = false; + private bool $endorsed = false; + + public function __construct(int $id = 0) { + $this->id = $id; + } + + public function setId(int $id): self { + $this->id = $id; + + return $this; + } + + public function getId(): int { + return $this->id; + } + + + public function setFollowing(bool $following): self { + $this->following = $following; + + return $this; + } + + public function isFollowing(): bool { + return $this->following; + } + + public function setShowingReblogs(bool $showingReblogs): self { + $this->showingReblogs = $showingReblogs; + + return $this; + } + + public function isShowingReblogs(): bool { + return $this->showingReblogs; + } + + public function setNotifying(bool $notifying): self { + $this->notifying = $notifying; + + return $this; + } + + public function isNotifying(): bool { + return $this->notifying; + } + + public function setFollowedBy(bool $followedBy): self { + $this->followedBy = $followedBy; + + return $this; + } + + public function isFollowedBy(): bool { + return $this->followedBy; + } + + public function setBlocking(bool $blocking): self { + $this->blocking = $blocking; + + return $this; + } + + public function isBlocking(): bool { + return $this->blocking; + } + + public function setBlockedBy(bool $blockedBy): self { + $this->blockedBy = $blockedBy; + + return $this; + } + + public function isBlockedBy(): bool { + return $this->blockedBy; + } + + public function setMuting(bool $muting): self { + $this->muting = $muting; + + return $this; + } + + public function isMuting(): bool { + return $this->muting; + } + + public function setMutingNotifications(bool $mutingNotifications): self { + $this->mutingNotifications = $mutingNotifications; + + return $this; + } + + public function isMutingNotifications(): bool { + return $this->mutingNotifications; + } + + public function setRequested(bool $requested): self { + $this->requested = $requested; + + return $this; + } + + public function isRequested(): bool { + return $this->requested; + } + + public function setDomainBlocking(bool $domainBlocking): self { + $this->domainBlocking = $domainBlocking; + + return $this; + } + + public function isDomainBlocking(): bool { + return $this->domainBlocking; + } + + public function setEndorsed(bool $endorsed): self { + $this->endorsed = $endorsed; + + return $this; + } + + public function isEndorsed(): bool { + return $this->endorsed; + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'following' => $this->isFollowing(), + 'showing_reblogs' => $this->isShowingReblogs(), + 'notifying' => $this->isNotifying(), + 'followed_by' => $this->isFollowedBy(), + 'blocking' => $this->isBlocking(), + 'blocked_by' => $this->isBlockedBy(), + 'muting' => $this->isMuting(), + 'muting_notifications' => $this->isMutingNotifications(), + 'requested' => $this->isRequested(), + 'domain_blocking' => $this->isDomainBlocking(), + 'endorsed' => $this->isEndorsed() + ]; + } +} diff --git a/lib/Service/CacheActorService.php b/lib/Service/CacheActorService.php index e64f21fd..35314163 100644 --- a/lib/Service/CacheActorService.php +++ b/lib/Service/CacheActorService.php @@ -338,4 +338,9 @@ class CacheActorService { public function probeActors(ProbeOptions $options): array { return $this->cacheActorsRequest->probeActors($options); } + + + public function getFromNids(array $ids):array { + return $this->cacheActorsRequest->getFromNids($ids); + } } diff --git a/lib/Service/FollowService.php b/lib/Service/FollowService.php index 93157b42..ccf41c8a 100644 --- a/lib/Service/FollowService.php +++ b/lib/Service/FollowService.php @@ -30,8 +30,6 @@ declare(strict_types=1); namespace OCA\Social\Service; -use OCA\Social\Tools\Exceptions\MalformedArrayException; -use OCA\Social\Tools\Traits\TArrayTools; use OCA\Social\AP; use OCA\Social\Db\FollowsRequest; use OCA\Social\Exceptions\CacheActorDoesNotExistException; @@ -41,11 +39,6 @@ use OCA\Social\Exceptions\InvalidOriginException; use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Exceptions\ItemUnknownException; use OCA\Social\Exceptions\RedundancyLimitException; -use OCA\Social\Tools\Exceptions\RequestContentException; -use OCA\Social\Tools\Exceptions\RequestNetworkException; -use OCA\Social\Tools\Exceptions\RequestResultNotJsonException; -use OCA\Social\Tools\Exceptions\RequestResultSizeException; -use OCA\Social\Tools\Exceptions\RequestServerException; use OCA\Social\Exceptions\RetrieveAccountFormatException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UnauthorizedFediverseException; @@ -55,6 +48,14 @@ use OCA\Social\Model\ActivityPub\Actor\Person; use OCA\Social\Model\ActivityPub\Object\Follow; use OCA\Social\Model\ActivityPub\OrderedCollection; use OCA\Social\Model\InstancePath; +use OCA\Social\Model\Relationship; +use OCA\Social\Tools\Exceptions\MalformedArrayException; +use OCA\Social\Tools\Exceptions\RequestContentException; +use OCA\Social\Tools\Exceptions\RequestNetworkException; +use OCA\Social\Tools\Exceptions\RequestResultNotJsonException; +use OCA\Social\Tools\Exceptions\RequestResultSizeException; +use OCA\Social\Tools\Exceptions\RequestServerException; +use OCA\Social\Tools\Traits\TArrayTools; use Psr\Log\LoggerInterface; class FollowService { @@ -291,4 +292,52 @@ class FollowService { public function getFollowersFromFollowId(string $recipient): array { return $this->followsRequest->getFollowersByFollowId($recipient); } + + /** + * @return Relationship[] + */ + public function getRelationships(array $nids): array { + $actorNids = $relationships = []; + + // retrieve actorIds from list of Nid + foreach ($this->cacheActorService->getFromNids($nids) as $actor) { + $actorNids[$actor->getNid()] = $actor->getId(); + } + + $follows = $this->followsRequest->getFollows(array_values($actorNids)); + foreach ($actorNids as $actorNid => $actorId) { + if ($actorNid === $this->viewer->getNid()) { + continue; // ignore current session + } + + // might be resource heavy, need to be checked/optimized ? + $relationships[] = $this->generateRelationship($actorNid, $actorId, $follows); + } + + return $relationships; + } + + /** + * @param int $actorNid + * @param string $actorId + * @param Follow[] $follows + * + * @return Relationship + */ + private function generateRelationship(int $actorNid, string $actorId, array $follows): Relationship { + $relationship = new Relationship($actorNid); + + foreach ($follows as $follow) { + if ($follow->getType() === Follow::TYPE) { + if ($follow->getObjectId() === $actorId) { + $relationship->setFollowing(true); + } + if ($follow->getActorId() === $actorId) { + $relationship->setFollowedBy(true); + } + } + } + + return $relationship; + } } diff --git a/lib/Tools/Db/ExtendedQueryBuilder.php b/lib/Tools/Db/ExtendedQueryBuilder.php index cda11f80..77594207 100644 --- a/lib/Tools/Db/ExtendedQueryBuilder.php +++ b/lib/Tools/Db/ExtendedQueryBuilder.php @@ -526,4 +526,31 @@ class ExtendedQueryBuilder extends QueryBuilder implements IExtendedQueryBuilder return $rows; } + + /** + * @param string $field + * @param array $value + * @param string $alias + */ + public function limitInArray(string $field, array $value, string $alias = ''): void { + $this->andWhere($this->exprLimitInArray($field, $value, $alias)); + } + + + /** + * @param string $field + * @param array $values + * @param string $alias + * + * @return string + */ + public function exprLimitInArray(string $field, array $values, string $alias = ''): string { + if ($this->getType() === DBALQueryBuilder::SELECT) { + $field = (($alias === '') ? $this->getDefaultSelectAlias() : $alias) . '.' . $field; + } + + $expr = $this->expr(); + + return $expr->in($field, $this->createNamedParameter($values, IQueryBuilder::PARAM_STR_ARRAY)); + } }