Commit ad8aba3d authored by Emiliano Balbuena's avatar Emiliano Balbuena

(feat): FeedCollection and /api/v2/feeds

No related merge requests found
Pipeline #101852118 failed with stages
in 2 minutes and 53 seconds
This diff is collapsed.
<?php
/**
* Clock.
*
* @author edgebal
*/
namespace Minds\Core;
class Clock
{
/**
* Returns the current system time
* @return int
*/
public function now()
{
return time();
}
}
......@@ -8,10 +8,44 @@ namespace Minds\Core\Feeds;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Clock;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\Elastic\Entities as ElasticEntities;
use Minds\Core\Feeds\Elastic\Manager as ElasticManager;
use Minds\Core\Hashtags\User\Manager as UserHashtagsManager;
use Minds\Entities\User;
class FeedCollection
{
/** @var string[] */
const ALGORITHMS = [
'top',
'hot',
'latest'
];
/** @var array */
const PERIODS = [
'12h' => 43200,
'24h' => 86400,
'7d' => 604800,
'30d' => 2592000,
'1y' => 31536000,
];
/** @var array */
const PERIOD_FALLBACK = [
'12h' => '7d',
'24h' => '7d',
'7d' => '30d',
'30d' => '1y'
];
/** @var string[] */
const ALLOWED_TO_FALLBACK = [
'top'
];
/** @var User|null */
protected $actor = null;
......@@ -39,9 +73,68 @@ class FeedCollection
/** @var bool */
protected $all = true;
/** @var array|null */
/** @var string[]|null */
protected $hashtags = null;
/** @var bool */
protected $sync = false;
/** @var bool */
protected $periodFallback = false;
/** @var bool */
protected $asActivities = false;
/** @var string */
protected $query = '';
/** @var string */
protected $customType = '';
/** @var string|null */
protected $containerGuid;
/** @var string[]|null */
protected $nsfw = [];
/** @var int[]|null */
protected $accessIds = null;
/** @var int */
protected $singleOwnerThreshold = 0;
/** @var ElasticManager */
protected $elasticManager;
/** @var ElasticEntities */
protected $elasticEntities;
/** @var UserHashtagsManager */
protected $userHashtagsManager;
/** @var Clock */
protected $clock;
/**
* FeedCollection constructor.
* @param ElasticManager $elasticManager
* @param ElasticEntities $elasticEntities
* @param UserHashtagsManager $userHashtagsManager
* @param Clock $clock
*/
public function __construct(
$elasticManager = null,
$elasticEntities = null,
$userHashtagsManager = 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->clock = $clock ?: new Clock();
}
/**
* @param User|null $actor
* @return FeedCollection
......@@ -142,6 +235,96 @@ class FeedCollection
return $this;
}
/**
* @param bool $sync
* @return FeedCollection
*/
public function setSync(bool $sync): FeedCollection
{
$this->sync = $sync;
return $this;
}
/**
* @param bool $periodFallback
* @return FeedCollection
*/
public function setPeriodFallback(bool $periodFallback): FeedCollection
{
$this->periodFallback = $periodFallback;
return $this;
}
/**
* @param bool $asActivities
* @return FeedCollection
*/
public function setAsActivities(bool $asActivities): FeedCollection
{
$this->asActivities = $asActivities;
return $this;
}
/**
* @param string $query
* @return FeedCollection
*/
public function setQuery(string $query): FeedCollection
{
$this->query = $query;
return $this;
}
/**
* @param string $customType
* @return FeedCollection
*/
public function setCustomType(string $customType): FeedCollection
{
$this->customType = $customType;
return $this;
}
/**
* @param string|null $containerGuid
* @return FeedCollection
*/
public function setContainerGuid(?string $containerGuid): FeedCollection
{
$this->containerGuid = $containerGuid;
return $this;
}
/**
* @param string[]|null $nsfw
* @return FeedCollection
*/
public function setNsfw(?array $nsfw): FeedCollection
{
$this->nsfw = $nsfw;
return $this;
}
/**
* @param int[]|null $accessIds
* @return FeedCollection
*/
public function setAccessIds(?array $accessIds): FeedCollection
{
$this->accessIds = $accessIds;
return $this;
}
/**
* @param int $singleOwnerThreshold
* @return FeedCollection
*/
public function setSingleOwnerThreshold(int $singleOwnerThreshold): FeedCollection
{
$this->singleOwnerThreshold = $singleOwnerThreshold;
return $this;
}
/**
* @throws Exception
*/
......@@ -163,6 +346,22 @@ class FeedCollection
throw new Exception('Missing period');
}
// Normalize period
$period = $this->period;
switch ($this->algorithm) {
case 'hot':
$period = '12h';
break;
case 'latest':
$period = '1y';
break;
}
// Normalize and calculate limit
$offset = abs(intval($this->offset ?: 0));
$limit = abs(intval($this->limit ?: 0));
......@@ -182,9 +381,104 @@ class FeedCollection
}
}
// Normalize hashtags
$all = !$this->hashtags && $this->all;
$hashtags = $this->hashtags;
$filterHashtags = true;
// Fetch preferred hashtags
if (!$all && !$hashtags && $this->actor) {
$hashtags = $this->userHashtagsManager
->setUser($this->actor)
->values([
'limit' => 50,
'trending' => false,
'defaults' => false,
]);
$filterHashtags = false;
}
// Check container readability
if ($this->containerGuid) {
$container = $this->entitiesBuilder->single($this->containerGuid);
if (!$container || !$this->acl->read($container)) {
throw new Exception('Forbidden container');
}
}
// 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[$this->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']);
}
}
$response
->setPagingToken($this->limit + $this->offset)
->setAttribute('fallbackAt', $fallbackAt);
return $response;
}
......
......@@ -8,6 +8,10 @@ class FeedsProvider extends Provider
{
public function register()
{
$this->di->bind('Feeds\FeedCollection', function ($di) {
return new FeedCollection();
}, ['useFactory' => true]);
$this->di->bind('Feeds\Elastic\Manager', function ($di) {
return new Elastic\Manager();
});
......
......@@ -148,6 +148,16 @@ class Manager
return array_slice(array_values($output), 0, count($selected) + $opts['limit']);
}
/**
* @param array $opts
* @return array
* @throws \Exception
*/
public function values(array $opts = [])
{
return array_column($this->get($opts) ?: [], 'value');
}
/**
* @param HashtagEntity[] $hashtags
* @return bool
......
Please register or to comment