...
 
Commits (7)
......@@ -118,6 +118,10 @@ class Exportable implements \JsonSerializable
$exported['ownerObj']['guid'] = (string) $exported['ownerObj']['guid'];
}
if (isset($exported['urn']) && isset($_SERVER['HTTP_APP_VERSION'])) {
$exported['urn'] = "urn:entity:{$exported['guid']}";
}
foreach ($this->exceptions as $exception) {
$exported[$exception] = $item->{$exception};
}
......
<?php
/**
* Boost Fetch
*
* @version 2
* @author emi
*
*/
namespace Minds\Controllers\api\v2\boost;
use Minds\Api\Exportable;
use Minds\Common\Urn;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Helpers;
use Minds\Entities;
use Minds\Interfaces;
use Minds\Api\Factory;
class feed implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
public function get($pages)
{
Factory::isLoggedIn();
/** @var Entities\User $currentUser */
$currentUser = Core\Session::getLoggedinUser();
if ($currentUser->disabled_boost && $currentUser->isPlus()) {
return Factory::response([
'boosts' => [],
]);
}
// Parse parameters
$type = $pages[0] ?? 'newsfeed';
$limit = abs(intval($_GET['limit'] ?? 2));
$offset = $_GET['offset'] ?? null;
$rating = intval($_GET['rating'] ?? $currentUser->getBoostRating());
$platform = $_GET['platform'] ?? 'other';
$quality = 0;
if ($limit === 0) {
return Factory::response([
'boosts' => [],
]);
} elseif ($limit > 500) {
$limit = 500;
}
$cacher = Core\Data\cache\factory::build('Redis');
$offset = $cacher->get(Core\Session::getLoggedinUser()->guid . ':boost-offset-rotator');
// Options specific to newly created users (<=1 hour) and iOS users
if ($platform === 'ios') {
$rating = 1; // they can only see safe content
$quality = 90;
} elseif (time() - $currentUser->getTimeCreated() <= 3600) {
$rating = 1; // they can only see safe content
$quality = 75;
}
//
$boosts = [];
$next = null;
switch ($type) {
case 'newsfeed':
// Newsfeed boosts
/** @var Core\Boost\Network\Iterator $iterator */
$iterator = Core\Di\Di::_()->get('Boost\Network\Iterator');
$iterator
->setLimit($limit)
->setOffset($offset)
->setRating($rating)
->setQuality($quality)
->setType($type)
->setPriority(true)
->setHydrate(false);
foreach ($iterator as $boost) {
$feedSyncEntity = new Core\Feeds\FeedSyncEntity();
$feedSyncEntity
->setGuid((string) $boost->getGuid())
->setOwnerGuid((string) $boost->getOwnerGuid())
->setUrn(new Urn("urn:boost:{$boost->getType()}:{$boost->getGuid()}"));
$boosts[] = $feedSyncEntity;
}
// $boosts = iterator_to_array($iterator, false);
$next = $iterator->getOffset();
$cacher->set(Core\Session::getLoggedinUser()->guid . ':boost-offset-rotator', $next);
break;
case 'content':
// TODO: Content boosts
default:
return Factory::response([
'status' => 'error',
'message' => 'Unsupported boost type'
]);
}
return Factory::response([
'entities' => Exportable::_($boosts),
'load-next' => $next ?: null,
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @return mixed|null
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -132,6 +132,7 @@ class container implements Interfaces\Api
'from_timestamp' => $fromTimestamp,
'query' => $query,
'single_owner_threshold' => 0,
'pinned_guids' => $type === 'activity' ? $container->getPinnedPosts() : null,
];
if (isset($_GET['nsfw'])) {
......
......@@ -635,6 +635,8 @@ class Blog extends RepositoryEntity
unset($output['deleted']);
}
$output['urn'] = $this->getUrn();
$output = array_merge(
$output,
$this->_eventsDispatcher->trigger('export:extender', 'blog', [ 'entity' => $this ], [])
......
......@@ -34,7 +34,15 @@ class EntityGuidResolverDelegate implements ResolverDelegate
*/
public function shouldResolve(Urn $urn)
{
return $urn->getNid() === 'entity' || $urn->getNid() === 'activity' || $urn->getNid() === 'user';
return in_array($urn->getNid(), [
'entity',
'activity',
'image',
'video',
'blog',
'user',
'group',
]);
}
/**
......
......@@ -25,7 +25,6 @@ use Minds\Traits\MagicAttributes;
class FeedSyncEntity
{
use MagicAttributes;
use Exportable;
/** @var int|string */
protected $guid;
......@@ -39,17 +38,21 @@ class FeedSyncEntity
/** @var string */
protected $urn;
/** @var Entity */
protected $entity;
/**
* Specifies the exportable properties
* @return array<string|\Closure>
* Export to public API
* @return array
*/
public function getExportable()
public function export()
{
return [
'urn',
'guid',
'ownerGuid',
'timestamp',
'guid' => (string) $this->guid,
'owner_guid' => (string) $this->ownerGuid,
'timestamp' => $this->timestamp,
'urn' => $this->urn,
'entity' => $this->entity ? $this->entity->export() : null,
];
}
}
......@@ -89,6 +89,7 @@ class Manager
'nsfw' => null,
'single_owner_threshold' => 36,
'filter_hashtags' => false,
'pinned_guids' => null,
], $opts);
if (isset($opts['query']) && $opts['query']) {
......@@ -125,10 +126,21 @@ class Manager
++$i; // Update here as we don't want to count skipped
$entityType = $scoredGuid->getType() ?? 'entity';
if (strpos($entityType, 'object:', 0) === 0) {
$entityType = str_replace('object:', '', $entityType);
}
$urn = implode(':', [
'urn',
$entityType,
$scoredGuid->getGuid(),
]);
$feedSyncEntities[] = (new FeedSyncEntity())
->setGuid((string) $scoredGuid->getGuid())
->setOwnerGuid((string) $ownerGuid)
->setUrn(new Urn($scoredGuid->getGuid()))
->setUrn(new Urn($urn))
->setTimestamp($scoredGuid->getTimestamp());
$scores[(string) $scoredGuid->getGuid()] = $scoredGuid->getScore();
......@@ -142,29 +154,44 @@ class Manager
return min($feedSyncEntity->getTimestamp() ?: INF, $carry);
}, INF) - 1);
if (!$opts['sync']) {
$guids = array_map(function (FeedSyncEntity $feedSyncEntity) {
return $feedSyncEntity->getGuid();
}, $feedSyncEntities);
$hydrateGuids = array_map(function (FeedSyncEntity $feedSyncEntity) {
return $feedSyncEntity->getGuid();
}, array_slice($feedSyncEntities, 0, 12)); // hydrate the first 12
$entities = $this->entitiesBuilder->get(['guids' => $guids]);
} else {
$entities = $feedSyncEntities;
}
$hydratedEntities = $this->entitiesBuilder->get(['guids' => $hydrateGuids]);
foreach ($hydratedEntities as $entity) {
if ($opts['pinned_guids'] && in_array($entity->getGuid(), $opts['pinned_guids'])) {
$entity->pinned = true;
}
$entities[] = (new FeedSyncEntity)
->setGuid($entity->getGuid())
->setOwnerGuid($entity->getOwnerGuid())
->setUrn($entity->getUrn())
->setEntity($entity);
}
usort($entities, function ($a, $b) use ($scores) {
$aGuid = $a instanceof FeedSyncEntity ? $a->getGuid() : $a->guid;
$bGuid = $b instanceof FeedSyncEntity ? $b->getGuid() : $b->guid;
// TODO: Optimize this
foreach (array_slice($feedSyncEntities, 12) as $entity) {
$entities[] = $entity;
}
$aScore = $scores[(string) $aGuid];
$bScore = $scores[(string) $bGuid];
// TODO: confirm if the following is actually necessary
// especially after the first 12
if ($aScore === $bScore) {
return 0;
}
/*usort($entities, function ($a, $b) use ($scores) {
$aGuid = $a instanceof FeedSyncEntity ? $a->getGuid() : $a->guid;
$bGuid = $b instanceof FeedSyncEntity ? $b->getGuid() : $b->guid;
return $aScore < $bScore ? 1 : -1;
});
$aScore = $scores[(string) $aGuid];
$bScore = $scores[(string) $bGuid];
if ($aScore === $bScore) {
return 0;
}
return $aScore < $bScore ? 1 : -1;
});*/
}
$response = new Response($entities);
......@@ -192,7 +219,7 @@ class Manager
$feedSyncEntities[] = (new FeedSyncEntity())
->setGuid((string) $row['guid'])
->setOwnerGuid((string) $row['guid'])
->setUrn(new Urn($row['guid']))
->setUrn("urn:user:{$row['guid']}")
->setTimestamp($row['time_created'] * 1000);
}
}
......@@ -209,12 +236,35 @@ class Manager
$feedSyncEntities[] = (new FeedSyncEntity())
->setGuid($row)
->setOwnerGuid(-1)
->setUrn(new Urn($row))
->setUrn("urn:group:{$row['guid']}")
->setTimestamp(0);
}
}
return $feedSyncEntities;
$entities = [];
$hydrateGuids = array_map(function (FeedSyncEntity $feedSyncEntity) {
return $feedSyncEntity->getGuid();
}, array_slice($feedSyncEntities, 0, 12)); // hydrate the first 12
if ($hydrateGuids) {
$hydratedEntities = $this->entitiesBuilder->get(['guids' => $hydrateGuids]);
foreach ($hydratedEntities as $entity) {
$entities[] = (new FeedSyncEntity)
->setGuid($entity->getGuid())
->setOwnerGuid($entity->getOwnerGuid())
->setUrn($entity->getUrn())
->setEntity($entity);
}
}
// TODO: Optimize this
foreach (array_slice($feedSyncEntities, 12) as $entity) {
$entities[] = $entity;
}
return $entities;
}
public function run($opts = [])
......
......@@ -51,7 +51,8 @@ class Repository
'nsfw' => null,
'from_timestamp' => null,
'exclude_moderated' => false,
'moderation_reservations' => null
'moderation_reservations' => null,
'pinned_guids' => null,
], $opts);
if (!$opts['type']) {
......@@ -338,6 +339,20 @@ class Repository
$response = $this->client->request($prepared);
if ($opts['pinned_guids']) { // Hack the response so we can have pinned posts
foreach ($opts['pinned_guids'] as $pinned_guid) {
array_unshift($response['hits']['hits'], [
'_type' => 'activity',
'_source' => [
'guid' => $pinned_guid,
'owner_guid' => null,
'score' => 0,
'timestamp' => 0,
],
]);
}
}
$guids = [];
foreach ($response['hits']['hits'] as $doc) {
$guid = $doc['_source'][$this->getSourceField($opts['type'])];
......@@ -347,6 +362,7 @@ class Repository
$guids[$guid] = true;
yield (new ScoredGuid())
->setGuid($doc['_source'][$this->getSourceField($opts['type'])])
->setType($doc['_type'])
->setScore($algorithm->fetchScore($doc))
->setOwnerGuid($doc['_source']['owner_guid'])
->setTimestamp($doc['_source']['@timestamp']);
......
......@@ -20,6 +20,8 @@ use Minds\Traits\MagicAttributes;
* @method ScoredGuid setOwnerGuid(int|string $ownerGuid)
* @method int getTimestamp()
* @method ScoredGuid setTimestamp(int $timestamp)
* @method string getType()
* @method ScoredGuid setType(string $type)
*/
class ScoredGuid
{
......@@ -28,6 +30,9 @@ class ScoredGuid
/** @var int|string */
protected $guid;
/** @var string */
protected $type;
/** @var float */
protected $score;
......
......@@ -218,6 +218,7 @@ class Activity extends Entity
'rating',
'ephemeral',
'hide_impressions',
'pinned',
));
}
......
......@@ -839,8 +839,16 @@ class Group extends NormalizedEntity
$export['is:creator'] = $userIsAdmin || $this->isCreator(Core\Session::getLoggedInUser());
$export['is:awaiting'] = $this->isAwaiting(Core\Session::getLoggedInUser());
$export['urn'] = $this->getUrn();
$export = array_merge($export, Dispatcher::trigger('export:extender', 'group', [ 'entity' => $this ], []));
return $export;
}
public function getUrn()
{
return "urn:group:{$this->guid}";
}
}
......@@ -220,6 +220,7 @@ class Image extends File
$export['width'] = $this->width ?: 0;
$export['height'] = $this->height ?: 0;
$export['gif'] = (bool) $this->gif;
$export['urn'] = $this->getUrn();
if (!Helpers\Flags::shouldDiscloseStatus($this) && isset($export['flags']['spam'])) {
unset($export['flags']['spam']);
......@@ -352,4 +353,9 @@ class Image extends File
{
return $this->boost_rejection_reason;
}
public function getUrn()
{
return "urn:image:{$this->guid}";
}
}
......@@ -260,4 +260,9 @@ class Video extends Object
{
return $this->boost_rejection_reason;
}
public function getUrn()
{
return "urn:video:{$this->getGuid()}";
}
}
......@@ -65,10 +65,22 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn(2);
$entity1->get('guid')
$scoredGuid1->getType()
->shouldBeCalled()
->willReturn('object:image');
$entity1->getGUID()
->shouldBeCalled()
->willReturn(5000);
$entity1->getOwnerGUID()
->shouldBeCalled()
->willReturn(1000);
$entity1->getUrn()
->shouldBeCalled()
->willReturn("urn:image:500");
$scoredGuid2->getGuid()
->shouldBeCalled()
->willReturn(5001);
......@@ -85,10 +97,22 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn(1);
$entity2->get('guid')
$scoredGuid2->getType()
->shouldBeCalled()
->willReturn('activity');
$entity2->getGUID()
->shouldBeCalled()
->willReturn(5001);
$entity2->getOwnerGUID()
->shouldBeCalled()
->willReturn(1001);
$entity2->getUrn()
->shouldBeCalled()
->willReturn("urn:activity:5001");
$this->repository->getList(Argument::withEntry('cache_key', 'phpspec'))
->shouldBeCalled()
->willReturn([$scoredGuid1, $scoredGuid2]);
......@@ -97,11 +121,14 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn([$entity1, $entity2]);
$this
$response = $this
->getList([
'cache_key' => 'phpspec',
])
->shouldBeAResponse([$entity2, $entity1]);
]);
$response[0]->getUrn()
->shouldBe('urn:image:500');
$response[1]->getUrn()
->shouldBe('urn:activity:5001');
}
public function it_should_get_list_by_query(
......@@ -127,10 +154,22 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn(2);
$entity1->get('guid')
$scoredGuid1->getType()
->shouldBeCalled()
->willReturn('activity');
$entity1->getGUID()
->shouldBeCalled()
->willReturn(5000);
$entity1->getOwnerGUID()
->shouldBeCalled()
->willReturn(1000);
$entity1->getUrn()
->shouldBeCalled()
->willReturn("urn:activity:5000");
$scoredGuid2->getGuid()
->shouldBeCalled()
->willReturn(5001);
......@@ -146,11 +185,23 @@ class ManagerSpec extends ObjectBehavior
$scoredGuid2->getTimestamp()
->shouldBeCalled()
->willReturn(1);
$scoredGuid2->getType()
->shouldBeCalled()
->willReturn('activity');
$entity2->get('guid')
$entity2->getGUID()
->shouldBeCalled()
->willReturn(5001);
$entity2->getOwnerGUID()
->shouldBeCalled()
->willReturn(1001);
$entity2->getUrn()
->shouldBeCalled()
->willReturn("urn:activity:5001");
$this->repository->getList(Argument::withEntry('query', 'activity with hashtags'))
->shouldBeCalled()
->willReturn([$scoredGuid1, $scoredGuid2]);
......@@ -159,11 +210,15 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn([$entity1, $entity2]);
$this
$response = $this
->getList([
'query' => 'Activity with #hashtags',
])
->shouldBeAResponse([$entity2, $entity1]);
]);
$response[0]->getUrn()
->shouldBe('urn:activity:5000');
$response[1]->getUrn()
->shouldBe('urn:activity:5001');
}
function getMatchers()
......
......@@ -56,7 +56,8 @@ class RepositorySpec extends ObjectBehavior
'time_created' => 1,
'@timestamp' => 1000,
],
'_score' => 100
'_score' => 100,
'_type' => 'activity',
],
[
'_source' => [
......@@ -65,7 +66,8 @@ class RepositorySpec extends ObjectBehavior
'time_created' => 1,
'@timestamp' => 1000,
],
'_score' => 50
'_score' => 50,
'_type' => 'activity',
],
]
]
......@@ -104,7 +106,8 @@ class RepositorySpec extends ObjectBehavior
'time_created' => 1,
'@timestamp' => 1000,
],
'_score' => 100
'_score' => 100,
'_type' => 'user',
],
[
'_source' => [
......@@ -113,7 +116,8 @@ class RepositorySpec extends ObjectBehavior
'time_created' => 2,
'@timestamp' => 2000,
],
'_score' => 50
'_score' => 50,
'_type' => 'user',
],
]
]
......@@ -153,7 +157,8 @@ class RepositorySpec extends ObjectBehavior
'@timestamp' => 1000,
'container_guid' => '1',
],
'_score' => 100
'_score' => 100,
'_type' => 'group',
],
[
'_source' => [
......@@ -163,7 +168,8 @@ class RepositorySpec extends ObjectBehavior
'@timestamp' => 2000,
'container_guid' => '2',
],
'_score' => 50
'_score' => 50,
'_type' => 'group',
],
]
]
......
......@@ -1370,7 +1370,7 @@ abstract class ElggEntity extends ElggData implements
*/
public function getExportableValues() {
return array(
'guid',
'guid',
'type',
'subtype',
'time_created',
......@@ -1395,7 +1395,8 @@ abstract class ElggEntity extends ElggData implements
$export = array_merge($export, \Minds\Core\Events\Dispatcher::trigger('export:extender', 'all', array('entity'=>$this), []) ?: []);
$export = \Minds\Helpers\Export::sanitize($export);
$export['nsfw'] = $this->getNsfw();
$export['nsfw_lock'] = $this->getNsfwLock();
$export['nsfw_lock'] = $this->getNsfwLock();
$export['urn']= $this->getUrn();
return $export;
}
......