Commit 3230977a authored by Emiliano Balbuena's avatar Emiliano Balbuena

(wip): Feeds neighbor endpoint

1 merge request!433WIP: Modal Page (+ FeedCollection)
Pipeline #106236153 failed with stages
in 6 minutes and 32 seconds
<?php
/**
* neighbor
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\feeds;
use Exception;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\FeedCollection;
use Minds\Core\Session;
use Minds\Helpers\Text;
use Minds\Interfaces;
class neighbor implements Interfaces\Api
{
/**
* @inheritDoc
*/
public function get($pages)
{
$actor = Session::getLoggedinUser() ?: null;
/** @var FeedCollection $feedCollection */
$feedCollection = Di::_()->get('Feeds\Collection');
$feedCollection
->setActor($actor);
try {
switch ($pages[2] ?? '') {
case 'activities':
$type = 'activity';
break;
case 'channels':
$type = 'user';
break;
case 'images':
$type = 'object:image';
break;
case 'videos':
$type = 'object:video';
break;
case 'groups':
$type = 'group';
break;
case 'blogs':
$type = 'object:blog';
break;
default:
$type = $pages[2] ?? '';
}
$hashtags = null;
if ($_GET['hashtag'] ?? null) {
$hashtags = Text::buildArray($_GET['hashtag']);
} elseif ($_GET['hashtags'] ?? null) {
$hashtags = Text::buildArray(explode(',', $_GET['hashtags']));
}
$nsfw = array_values(array_filter(explode(',', $_GET['nsfw'] ?? '')));
//
// TODO: Calculate based on container/owner/subscribed feed
// TODO: Create a class for calculation and use it on other feed endpoints for consistency
$accessIds = [2];
// TODO: Calculate based on container/owner/subscribed feed
// TODO: Create a class for calculation and use it on other feed endpoints for consistency
$singleOwnerThreshold = 36;
//
$feedCollection
->setFilter($pages[0] ?? '')
->setAlgorithm($pages[1] ?? '')
->setType($type)
->setPeriod($_GET['period'] ?? '12h')
->setOffset($_GET['offset'] ?? 0)
->setCap($actor->isAdmin() ? 5000 : 600)
->setAll((bool) ($_GET['all'] ?? false))
->setHashtags($hashtags)
->setSync((bool) ($_GET['sync'] ?? false))
->setPeriodFallback(false)
->setAsActivities((bool)($_GET['as_activities'] ?? true))
->setQuery(urldecode($_GET['query'] ?? ''))
->setCustomType($_GET['custom_type'] ?? '')
->setContainerGuid($_GET['container_guid'] ?? null)
->setNsfw($nsfw)
->setAccessIds($accessIds)
->setSingleOwnerThreshold($singleOwnerThreshold)
->setLimit(1);
list($prev, $next) = $feedCollection
->fetchAdjacent($_GET['guid'] ?? null);
return Factory::response([
'status' => 'success',
'prev' => $prev,
'next' => $next
]);
} catch (Exception $e) {
error_log((string) $e);
return Factory::response([
'status' => 'error',
'message' => $e->getMessage()
]);
}
}
/**
* @inheritDoc
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* @inheritDoc
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -9,9 +9,9 @@ class EntitiesBuilder
{
/**
* Build by a single guid
* @param $guid number
* @param $guid int|string
* @param $opts array
* @return Entity
* @return mixed
*/
public function single($guid, $opts = [])
{
......
......@@ -89,6 +89,7 @@ class Manager
'type' => null,
'sync' => false,
'from_timestamp' => null,
'from_id' => null,
'query' => null,
'nsfw' => null,
'single_owner_threshold' => 36,
......@@ -96,6 +97,7 @@ class Manager
'pinned_guids' => null,
'as_activities' => false,
'exclude' => null,
'reverse' => false,
], $opts);
if (isset($opts['query']) && $opts['query']) {
......
......@@ -66,11 +66,13 @@ class Repository
'query' => null,
'nsfw' => null,
'from_timestamp' => null,
'from_id' => null,
'exclude_moderated' => false,
'moderation_reservations' => null,
'pinned_guids' => null,
'future' => false,
'exclude' => null,
'reverse' => false,
], $opts);
if (!$opts['type']) {
......@@ -370,6 +372,12 @@ class Repository
//
if ($opts['from_id']) {
// TODO: Return results based on 'from_id' and 'reverse' order if necessary
}
//
$esQuery = $algorithm->getQuery();
if ($esQuery) {
$body['query']['function_score']['query'] = array_merge_recursive($body['query']['function_score']['query'], $esQuery);
......
<?php
/**
* OverflowException
*
* @author edgebal
*/
namespace Minds\Core\Feeds\Exceptions;
use Exception;
class OverflowException extends Exception
{
}
......@@ -13,7 +13,9 @@ use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Feeds\Elastic\Entities as ElasticEntities;
use Minds\Core\Feeds\Elastic\Manager as ElasticManager;
use Minds\Core\Feeds\Exceptions\OverflowException;
use Minds\Core\Hashtags\User\Manager as UserHashtagsManager;
use Minds\Core\Security\ACL;
use Minds\Entities\User;
class FeedCollection
......@@ -116,6 +118,9 @@ class FeedCollection
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var ACL */
protected $acl;
/** @var Clock */
protected $clock;
......@@ -125,6 +130,7 @@ class FeedCollection
* @param ElasticEntities $elasticEntities
* @param UserHashtagsManager $userHashtagsManager
* @param EntitiesBuilder $entitiesBuilder
* @param ACL $acl
* @param Clock $clock
*/
public function __construct(
......@@ -132,12 +138,14 @@ class FeedCollection
$elasticEntities = null,
$userHashtagsManager = null,
$entitiesBuilder = null,
$acl = null,
$clock = null
) {
$this->elasticManager = $elasticManager ?: Di::_()->get('Feeds\Elastic\Manager');
$this->elasticEntities = $elasticEntities ?: new ElasticEntities();
$this->userHashtagsManager = $userHashtagsManager ?: Di::_()->get('Hashtags\User\Manager');
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->acl = $acl ?: ACL::_();
$this->clock = $clock ?: new Clock();
}
......@@ -335,6 +343,115 @@ class FeedCollection
* @throws Exception
*/
public function fetch()
{
try {
$parameters = $this->buildParameters();
} catch (OverflowException $e) {
$emptyResponse = new Response([]);
$emptyResponse
->setPagingToken((string) $this->cap)
->setLastPage(true)
->setAttribute('overflow', true);
return $emptyResponse;
}
$opts = $parameters->getOpts();
$softLimit = $parameters->getSoftLimit();
$response = new Response();
$fallbackAt = null;
$i = 0;
while ($response->count() < $softLimit) {
$result = $this->elasticManager->getList($opts);
$response = $response
->pushArray($result->toArray());
if (
!$this->periodFallback ||
!in_array($this->algorithm, static::ALLOWED_TO_FALLBACK, true) ||
!isset(static::PERIOD_FALLBACK[$opts['period']]) ||
++$i > 2 // Stop at 2nd fallback (i.e. 12h > 7d > 30d)
) {
break;
}
$period = $opts['period'];
$from = $this->clock->now() - static::PERIODS[$period];
$opts['from_timestamp'] = $from * 1000;
$opts['period'] = static::PERIOD_FALLBACK[$period];
if (!$fallbackAt) {
$fallbackAt = $from;
}
}
if (!$this->sync) {
$this->elasticEntities
->setActor($this->actor);
$response = $response
->filter([$this->elasticEntities, 'filter']);
if ($this->asActivities) {
$response = $response
->map([$this->elasticEntities, 'cast']);
}
}
$pagingToken = $this->limit + $this->offset;
$response
->setPagingToken((string) $pagingToken)
->setAttribute('fallbackAt', $fallbackAt);
return $response;
}
/**
* @param string|null $documentId
* @return array
* @throws Exception
*/
public function fetchAdjacent(?string $documentId): array
{
if (!$documentId) {
throw new Exception('Invalid document ID');
}
$opts = $this->buildParameters([
'sync' => true,
'limit' => 1,
'from_id' => $documentId,
'reverse' => true,
])->getOpts();
$prev = $this->elasticManager
->getList($opts)
->toArray()[0] ?? null;
$opts = $this->buildParameters([
'sync' => true,
'limit' => 1,
'from_id' => $documentId
])->getOpts();
$next = $this->elasticManager
->getList($opts)
->toArray()[0] ?? null;
return [$prev, $next];
}
/**
* @param array $optsOverride
* @return FeedCollectionParameters
* @throws OverflowException
* @throws Exception
*/
protected function buildParameters(array $optsOverride = []): FeedCollectionParameters
{
if (!$this->filter) {
throw new Exception('Missing filter');
......@@ -377,13 +494,7 @@ class FeedCollection
}
if ($limit < 0) {
$emptyResponse = new Response([]);
$emptyResponse
->setPagingToken((string) $this->cap)
->setLastPage(true)
->setAttribute('overflow', true);
return $emptyResponse;
throw new OverflowException();
}
}
......@@ -417,77 +528,30 @@ class FeedCollection
}
}
// Build options
$opts = [
'cache_key' => $this->actor ? (string) $this->actor->guid : null,
'container_guid' => $this->containerGuid,
'access_id' => $this->accessIds,
'custom_type' => $this->customType,
'limit' => $this->limit,
'offset' => $this->offset,
'type' => $this->type,
'algorithm' => $this->algorithm,
'period' => $period,
'sync' => $this->sync,
'query' => $this->query,
'single_owner_threshold' => $this->singleOwnerThreshold,
'as_activities' => $this->asActivities,
'nsfw' => $this->nsfw,
'hashtags' => $hashtags,
'filter_hashtags' => $filterHashtags
];
//
$response = new Response();
$fallbackAt = null;
$i = 0;
while ($response->count() < $limit) {
$result = $this->elasticManager->getList($opts);
$response = $response
->pushArray($result->toArray());
if (
!$this->periodFallback ||
!in_array($this->algorithm, static::ALLOWED_TO_FALLBACK, true) ||
!isset(static::PERIOD_FALLBACK[$opts['period']]) ||
++$i > 2 // Stop at 2nd fallback (i.e. 12h > 7d > 30d)
) {
break;
}
$period = $opts['period'];
$from = $this->clock->now() - static::PERIODS[$period];
$opts['from_timestamp'] = $from * 1000;
$opts['period'] = static::PERIOD_FALLBACK[$period];
if (!$fallbackAt) {
$fallbackAt = $from;
}
}
if (!$this->sync) {
$this->elasticEntities
->setActor($this->actor);
$response = $response
->filter([$this->elasticEntities, 'filter']);
if ($this->asActivities) {
$response = $response
->map([$this->elasticEntities, 'cast']);
}
}
$pagingToken = $this->limit + $this->offset;
$response
->setPagingToken((string) $pagingToken)
->setAttribute('fallbackAt', $fallbackAt);
return $response;
// Build parameters
$feedCollectionParameters = new FeedCollectionParameters();
$feedCollectionParameters
->setOpts(array_merge([
'cache_key' => $this->actor ? (string) $this->actor->guid : null,
'container_guid' => $this->containerGuid,
'access_id' => $this->accessIds,
'custom_type' => $this->customType,
'limit' => $this->limit,
'offset' => $this->offset,
'type' => $this->type,
'algorithm' => $this->algorithm,
'period' => $period,
'sync' => $this->sync,
'query' => $this->query,
'single_owner_threshold' => $this->singleOwnerThreshold,
'as_activities' => $this->asActivities,
'nsfw' => $this->nsfw,
'hashtags' => $hashtags,
'filter_hashtags' => $filterHashtags
], $optsOverride))
->setSoftLimit($limit);
return $feedCollectionParameters;
}
}
<?php
/**
* FeedCollectionParameters
*
* @author edgebal
*/
namespace Minds\Core\Feeds;
use Minds\Traits\MagicAttributes;
/**
* Class FeedCollectionParameters
* @package Minds\Core\Feeds
* @method array getOpts()
* @method FeedCollectionParameters setOpts(array $opts)
* @method int getSoftLimit()
* @method FeedCollectionParameters setSoftLimit(int $softLimit)
*/
class FeedCollectionParameters
{
use MagicAttributes;
/** @var array */
protected $opts;
/** @var int */
protected $softLimit;
}
......@@ -8,7 +8,7 @@ class FeedsProvider extends Provider
{
public function register()
{
$this->di->bind('Feeds\FeedCollection', function ($di) {
$this->di->bind('Feeds\Collection', function ($di) {
return new FeedCollection();
}, ['useFactory' => true]);
......
......@@ -11,6 +11,7 @@ use Minds\Core\Feeds\Elastic\Manager as ElasticManager;
use Minds\Core\Feeds\FeedCollection;
use Minds\Core\Feeds\FeedSyncEntity;
use Minds\Core\Hashtags\User\Manager as UserHashtagsManager;
use Minds\Core\Security\ACL;
use Minds\Entities\Entity;
use Minds\Entities\User;
use PhpSpec\Exception\Example\FailureException;
......@@ -31,6 +32,9 @@ class FeedCollectionSpec extends ObjectBehavior
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/** @var ACL */
protected $acl;
/** @var Clock */
protected $clock;
......@@ -39,12 +43,14 @@ class FeedCollectionSpec extends ObjectBehavior
ElasticEntities $elasticEntities,
UserHashtagsManager $userHashtagsManager,
EntitiesBuilder $entitiesBuilder,
ACL $acl,
Clock $clock
) {
$this->elasticManager = $elasticManager;
$this->elasticEntities = $elasticEntities;
$this->userHashtagsManager = $userHashtagsManager;
$this->entitiesBuilder = $entitiesBuilder;
$this->acl = $acl;
$this->clock = $clock;
$this->beConstructedWith(
......@@ -52,6 +58,7 @@ class FeedCollectionSpec extends ObjectBehavior
$elasticEntities,
$userHashtagsManager,
$entitiesBuilder,
$acl,
$clock
);
}
......
Please register or to comment