...
 
Commits (12)
......@@ -259,6 +259,16 @@ class Response implements \Iterator, \ArrayAccess, \Countable, \JsonSerializable
return count($this->data);
}
/**
* @param array $data
* @return Response
*/
public function pushArray(array $data)
{
array_push($this->data, ...$data);
return $this;
}
/**
* Exports the data array
* @return array
......
......@@ -2,13 +2,22 @@
/**
* Converts a static class to use instances
*/
namespace Minds\Common;
use ReflectionClass;
use ReflectionException;
class StaticToInstance
{
/** @var $class */
/** @var ReflectionClass */
private $class;
/**
* StaticToInstance constructor.
* @param $class
* @throws ReflectionException
*/
public function __construct($class)
{
$this->setClass($class);
......@@ -16,11 +25,13 @@ class StaticToInstance
/**
* Set the class in question
* @return StripeStaticToOO
* @param $class
* @return static
* @throws ReflectionException
*/
public function setClass($class)
{
$this->class = new \ReflectionClass($class);
$this->class = new ReflectionClass($class);
return clone $this;
}
......@@ -28,7 +39,7 @@ class StaticToInstance
* Call the static functions as OO style
* @param string $method
* @param array $arguments
* @return midex
* @return mixed
*/
public function __call($method, $arguments)
{
......
......@@ -4,7 +4,7 @@ namespace Minds\Controllers\Cli;
use Minds\Core\Minds;
use Minds\Cli;
use Minds\Core\Feeds\Top\Manager;
use Minds\Core\Feeds\Elastic\Manager;
use Minds\Exceptions\CliException;
use Minds\Interfaces;
......
<?php
namespace Minds\Controllers\Cli\Top;
use Exception;
use Minds\Core\Feeds\Elastic\Sync;
use Minds\Core\Minds;
use Minds\Cli;
use Minds\Exceptions\CliException;
use Minds\Interfaces;
class All extends Cli\Controller implements Interfaces\CliControllerInterface
{
/** @var Sync */
private $sync;
/**
* Top constructor.
*/
public function __construct()
{
$minds = new Minds();
$minds->start();
$this->sync = new Sync();
}
/**
* @param null $command
* @return void
*/
public function help($command = null)
{
$this->out('Syntax usage: cli top all sync_<type> --metric=? --from=? --to=?');
}
/**
* @return void
*/
public function exec()
{
$this->help();
}
/**
* @throws CliException
*/
public function sync_activity(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('activity', null, $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_images(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'image', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_videos(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'video', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_blogs(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('object', 'blog', $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_groups(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('group', null, $this->getOpt('metric'), $from, $to);
}
/**
* @throws CliException
*/
public function sync_channels(): void
{
list($from, $to) = $this->getTimeRangeFromArgs();
$this->syncBy('user', null, $this->getOpt('metric'), $from, $to);
}
/**
* @return int[]
* @throws CliException
*/
protected function getTimeRangeFromArgs(): array
{
$to = $this->getOpt('to') ?: time();
if ($this->getOpt('from') && $this->getOpt('secsAgo')) {
throw new CliException('Cannot specify both `from` and `secsAgo`');
} elseif (!$this->getOpt('from') && !$this->getOpt('secsAgo')) {
throw new CliException('You should specify either `from` or `secsAgo`');
}
if ($this->getOpt('secsAgo')) {
$from = time() - $this->getOpt('secsAgo');
} else {
$from = $this->getOpt('from');
}
return [$from, $to];
}
/**
* @param $type
* @param $subtype
* @param $metric
* @param $from
* @param $to
* @throws CliException
* @throws Exception
*/
protected function syncBy($type, $subtype, $metric, $from, $to): void
{
if (!$metric) {
throw new CliException('Missing `metric`');
}
if (!$from || !is_numeric($from)) {
throw new CliException('Missing or invalid `from` value');
}
if (!$to || !is_numeric($to)) {
throw new CliException('Invalid `to` value');
}
if ($from > $to) {
throw new CliException('`from` must be lesser than `to`');
}
error_reporting(E_ALL);
ini_set('display_errors', 1);
$displayType = trim(implode(':', [$type, $subtype]), ':');
$this->out(sprintf(
"%s -> %s",
date('r', $from),
date('r', $to)
));
$this->out("Syncing {$displayType} / {$metric}");
$this->sync
->setType($type ?: '')
->setSubtype($subtype ?: '')
->setMetric($metric)
->setFrom($from * 1000)
->setTo($to * 1000)
->run();
$this->out("\nCompleted syncing '{$displayType}'.");
}
}
......@@ -220,6 +220,11 @@ class blog implements Interfaces\Api
$blog->setMature(!!$_POST['mature']);
}
if (isset($_POST['nsfw'])) {
$nsfw = !is_array($_POST['nsfw']) ? json_decode($_POST['nsfw']) : $_POST['nsfw'];
$blog->setNsfw($nsfw);
}
if (isset($_POST['wire_threshold'])) {
$threshold = is_string($_POST['wire_threshold']) ? json_decode($_POST['wire_threshold']) : $_POST['wire_threshold'];
$blog->setWireThreshold($threshold);
......@@ -266,7 +271,7 @@ class blog implements Interfaces\Api
}
if ($blog->isMonetized()) {
if ($blog->isMature()) {
if ($blog->getNsfw() || $blog->isMature()) {
return Factory::response([
'status' => 'error',
'message' => 'Cannot monetize an explicit blog'
......@@ -283,7 +288,8 @@ class blog implements Interfaces\Api
}
}
if (isset($_POST['mature']) && $_POST['mature']) {
if ((isset($_POST['nsfw']) && $_POST['nsfw'])
|| (isset($_POST['mature']) && $_POST['mature'])) {
$user = Core\Session::getLoggedInUser();
if (!$user->getMatureContent()) {
......
......@@ -263,10 +263,10 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
$entity = Core\Media\Factory::build($clientType);
$container_guid = isset($data['container_guid']) && is_numeric($data['container_guid']) ? $data['container_guid'] : null;
$entity->patch([
'title' => isset($data['name']) ? $data['name'] : '',
'mature' => isset($data['mature']) && !!$data['mature'],
'nsfw' => !is_array($_POST['nsfw']) ? json_decode($_POST['nsfw']) : $_POST['nsfw'],
'batch_guid' => 0,
'access_id' => 0,
'owner_guid' => $user->guid,
......
......@@ -539,6 +539,7 @@ class newsfeed implements Interfaces\Api
$activity = new Activity();
$activity->setMature(isset($_POST['mature']) && !!$_POST['mature']);
$activity->setNsfw($_POST['nsfw'] ?? []);
$user = Core\Session::getLoggedInUser();
......
......@@ -112,8 +112,8 @@ class firehose implements Interfaces\Api, Interfaces\ApiAdminPam
}
if ($type !== 'activity') {
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$activities = $activities->map([$entities, 'cast']);
}
......
......@@ -4,6 +4,7 @@ namespace Minds\Controllers\api\v2;
use Minds\Api\Exportable;
use Minds\Api\Factory;
use Minds\Common\Repository\Response;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities\Factory as EntitiesFactory;
......@@ -12,6 +13,13 @@ use Minds\Interfaces;
class feeds implements Interfaces\Api
{
const PERIOD_FALLBACK = [
'12h' => '7d',
'24h' => '7d',
'7d' => '30d',
'30d' => '1y'
];
/**
* Gets a list of suggested hashtags, including the ones the user has opted in
* @param array $pages
......@@ -21,6 +29,9 @@ class feeds implements Interfaces\Api
{
Factory::isLoggedIn();
$now = time();
$periodsInSecs = Core\Feeds\Elastic\Repository::PERIODS;
/** @var User $currentUser */
$currentUser = Core\Session::getLoggedinUser();
......@@ -119,6 +130,8 @@ class feeds implements Interfaces\Api
$sync = (bool) ($_GET['sync'] ?? false);
$periodFallback = (bool) ($_GET['period_fallback'] ?? false);
$asActivities = (bool) ($_GET['as_activities'] ?? true);
$query = isset($_GET['query']) ? urldecode($_GET['query']) : null;
......@@ -137,12 +150,13 @@ class feeds implements Interfaces\Api
}
}
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
$entities->setActor($currentUser);
/** @var Core\Feeds\Elastic\Entities $elasticEntities */
$elasticEntities = new Core\Feeds\Elastic\Entities();
$elasticEntities
->setActor($currentUser);
$opts = [
'cache_key' => Core\Session::getLoggedInUserGuid(),
......@@ -185,22 +199,49 @@ class feeds implements Interfaces\Api
}
try {
$result = $manager->getList($opts);
$entities = new Response();
$fallbackAt = null;
$i = 0;
while ($entities->count() < $limit) {
$rows = $manager->getList($opts);
$entities = $entities->pushArray($rows->toArray());
if (
!$periodFallback ||
$opts['algorithm'] !== 'top' ||
!isset(static::PERIOD_FALLBACK[$opts['period']]) ||
++$i > 2 // Stop at 2nd fallback (i.e. 12h > 7d > 30d)
) {
break;
}
$period = $opts['period'];
$from = $now - $periodsInSecs[$period];
$opts['from_timestamp'] = $from * 1000;
$opts['period'] = static::PERIOD_FALLBACK[$period];
if (!$fallbackAt) {
$fallbackAt = $from;
}
}
if (!$sync) {
// Remove all unlisted content, if ES document is not in sync, it'll
// also remove pending activities
$result = $result->filter([$entities, 'filter']);
$entities = $entities->filter([$elasticEntities, 'filter']);
if ($asActivities) {
// Cast to ephemeral Activity entities, if another type
$result = $result->map([$entities, 'cast']);
$entities = $entities->map([$elasticEntities, 'cast']);
}
}
return Factory::response([
'status' => 'success',
'entities' => Exportable::_($result),
'entities' => Exportable::_($entities),
'fallback_at' => $fallbackAt,
'load-next' => $limit + $offset,
]);
} catch (\Exception $e) {
......
......@@ -108,11 +108,11 @@ class container implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......
......@@ -123,11 +123,11 @@ class scheduled implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......@@ -151,7 +151,7 @@ class scheduled implements Interfaces\Api
'query' => $query,
'single_owner_threshold' => 0,
'pinned_guids' => $type === 'activity' ? array_reverse($container->getPinnedPosts()) : null,
'time_created_upper' => false,
'future' => true,
'owner_guid' => $currentUser->guid,
];
......
......@@ -85,11 +85,11 @@ class subscribed implements Interfaces\Api
$custom_type = isset($_GET['custom_type']) && $_GET['custom_type'] ? [$_GET['custom_type']] : null;
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$opts = [
......
......@@ -111,8 +111,8 @@ class content implements Interfaces\Api
$query = $_GET['query'];
}
/** @var Core\Feeds\Top\Entities $entities */
$entities = new Core\Feeds\Top\Entities();
/** @var Core\Feeds\Elastic\Entities $entities */
$entities = new Core\Feeds\Elastic\Entities();
$entities->setActor($currentUser);
$isOwner = false;
......@@ -181,7 +181,7 @@ class content implements Interfaces\Api
}
/**
* @param Core\Feeds\Top\Entities $entities
* @param Core\Feeds\Elastic\Entities $entities
* @param array $opts
* @param bool $asActivities
* @param bool $sync
......@@ -191,8 +191,8 @@ class content implements Interfaces\Api
private function getData($entities, $opts, $asActivities, $sync)
{
/** @var Core\Feeds\Top\Manager $manager */
$manager = Di::_()->get('Feeds\Top\Manager');
/** @var Core\Feeds\Elastic\Manager $manager */
$manager = Di::_()->get('Feeds\Elastic\Manager');
$result = $manager->getList($opts);
if (!$sync) {
......
......@@ -502,6 +502,7 @@ class Blog extends RepositoryEntity
}
}
$this->nsfw = $array;
$this->markAsDirty('nsfw');
return $this;
}
......
......@@ -60,6 +60,7 @@ class CreateActivity
->setThumbnail($blog->getIconUrl())
->setFromEntity($blog)
->setMature($blog->isMature())
->setNsfw($blog->getNsfw())
->setOwner($owner->export())
->setWireThreshold($blog->getWireThreshold())
->setPaywall($blog->isPaywall());
......
......@@ -138,6 +138,7 @@ class Manager
}
$this->paywallReview->queue($blog);
$this->propagateProperties->from($blog);
}
return $saved;
......
<?php
/**
* @author edgebal
*/
namespace Minds\Core;
use Minds\Common\StaticToInstance;
use Minds\Helpers\Counters as CountersHelper;
use ReflectionException;
/**
* Class Counters
* @package Minds\Core
* @method increment($entity, $metric, $value = 1, $client = null)
* @method decrement($entity, $metric, $value = 1, $client = null)
* @method incrementBatch($entities, $metric, $value = 1, $client = null)
* @method get($entity, $metric, $cache = true, $client = null)
* @method clear($entity, $metric, $value = 0, $client = null)
*/
class Counters extends StaticToInstance
{
/**
* Counters constructor.
* @throws ReflectionException
*/
public function __construct()
{
parent::__construct(new CountersHelper());
}
}
......@@ -10,7 +10,7 @@ namespace Minds\Core\Entities\Delegates;
use Minds\Common\Urn;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Feeds\Top\Entities as TopEntities;
use Minds\Core\Feeds\Elastic\Entities as TopEntities;
class EntityGuidResolverDelegate implements ResolverDelegate
{
......
......@@ -23,10 +23,11 @@ class Manager
/** @var Cookie $cookie */
private $cookie;
public function __construct($config = null, $cookie = null)
public function __construct($config = null, $cookie = null, $user = null)
{
$this->config = $config ?: Di::_()->get('Config');
$this->cookie = $cookie ?: new Cookie;
$this->user = $user ?? Session::getLoggedInUser();
}
/**
......@@ -59,6 +60,10 @@ class Manager
return true;
}
if ($features[$feature] === 'canary' && $this->user && $this->user->get('canary')) {
return true;
}
return $features[$feature] === true;
}
......
......@@ -5,7 +5,7 @@
* @author: Emiliano Balbuena <edgebal>
*/
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Core\Blogs\Blog;
use Minds\Core\Di\Di;
......
<?php
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Common\Repository\Response;
use Minds\Common\Urn;
......
<?php
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Core\Trending\Aggregates;
......
<?php
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Traits\MagicAttributes;
/**
* Class MetricsSync
* @package Minds\Core\Feeds\Top
* @method string getMetric()
* @method string getPeriod()
* @package Minds\Core\Feeds\Elastic
* @method int|string getGuid()
* @method MetricsSync setGuid(int|string $guid)
* @method string getType()
* @method MetricsSync setType(string $type)
* @method string getMetric()
* @method MetricsSync setMetric(string $metric)
* @method int getCount()
* @method MetricsSync setCount(int $count)
* @method string getPeriod()
* @method MetricsSync setPeriod(string $period)
* @method int getSynced()
* @method int|string getGuid()
* @method MetricsSync setSynced(int $synced)
*/
class MetricsSync
{
use MagicAttributes;
private $guid;
/** @var int|string */
protected $guid;
private $type;
/** @var string */
protected $type;
private $metric;
/** @var string */
protected $metric;
private $count;
/** @var int */
protected $count;
private $period;
/** @var string */
protected $period;
private $synced;
/** @var int */
protected $synced;
}
<?php
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Core\Data\ElasticSearch\Client as ElasticsearchClient;
use Minds\Core\Data\ElasticSearch\Prepared;
use Minds\Core\Di\Di;
use Minds\Core\Features\Manager as Features;
use Minds\Core\Search\SortingAlgorithms;
use Minds\Helpers\Text;
class Repository
{
const PERIODS = [
'12h' => 43200,
'24h' => 86400,
'7d' => 604800,
'30d' => 2592000,
'1y' => 31536000,
];
/** @var ElasticsearchClient */
protected $client;
/** @var Features */
protected $features;
/** @var string */
protected $index;
/** @var array $pendingBulkInserts * */
private $pendingBulkInserts = [];
public function __construct($client = null, $config = null)
public function __construct($client = null, $config = null, $features = null)
{
$this->client = $client ?: Di::_()->get('Database\ElasticSearch');
$config = $config ?: Di::_()->get('Config');
$this->features = $features ?: Di::_()->get('Features');
$this->index = $config->get('elasticsearch')['index'];
}
......@@ -53,7 +68,7 @@ class Repository
'exclude_moderated' => false,
'moderation_reservations' => null,
'pinned_guids' => null,
'time_created_upper' => time(),
'future' => false,
'exclude' => null,
], $opts);
......@@ -65,7 +80,7 @@ class Repository
throw new \Exception('Algorithm must be provided');
}
if (!in_array($opts['period'], ['12h', '24h', '7d', '30d', '1y'], true)) {
if (!in_array($opts['period'], array_keys(static::PERIODS), true)) {
throw new \Exception('Unsupported period');
}
......@@ -102,28 +117,15 @@ class Repository
'sort' => [],
];
/*if ($type === 'group' && false) {
if (!isset($body['query']['function_score']['query']['bool']['must_not'])) {
$body['query']['function_score']['query']['bool']['must_not'] = [];
}
$body['query']['function_score']['query']['bool']['must_not'][] = [
'terms' => [
'access_id' => ['0', '1', '2'],
],
];
} elseif ($type === 'user') {
$body['query']['function_score']['query']['bool']['must'][] = [
'term' => [
'access_id' => '2',
],
];
}*/
//
switch ($opts['algorithm']) {
case "top":
$algorithm = new SortingAlgorithms\Top();
if ($this->features->has('top-feeds-by-age')) {
$algorithm = new SortingAlgorithms\TopByPostAge();
} else {
$algorithm = new SortingAlgorithms\Top();
}
break;
case "controversial":
$algorithm = new SortingAlgorithms\Controversial();
......@@ -248,31 +250,59 @@ class Repository
];
}
if ($opts['from_timestamp']) {
if ($type === 'group') {
$body['query']['function_score']['query']['bool']['must'][] = [
'range' => [
'@timestamp' => [
'lte' => (int) $opts['from_timestamp'],
],
],
'access_id' => [
'gt' => 2,
]
]
];
}
// Filter by time created to cut out scheduled feeds
$time_created_upper = $opts['time_created_upper'] ? 'lte' : 'gt';
if (!isset($body['query']['function_score']['query']['bool']['must'])) {
$body['query']['function_score']['query']['bool']['must'] = [];
// Time bounds
$timestampUpperBounds = []; // LTE
$timestampLowerBounds = []; // GT
if ($algorithm->isTimestampConstrain()) {
$timestampLowerBounds[] = (time() - static::PERIODS[$opts['period']]) * 1000;
}
if ($opts['from_timestamp']) {
$timestampUpperBounds[] = (int) $opts['from_timestamp'];
}
if ($opts['future']) {
$timestampLowerBounds[] = time() * 1000;
} else {
$timestampUpperBounds[] = time() * 1000;
}
$body['query']['function_score']['query']['bool']['must'][] = [
'range' => [
'@timestamp' => [
$time_created_upper => ((int) ($opts['time_created_upper'] ?: time())) * 1000,
if ($timestampUpperBounds || $timestampLowerBounds) {
if (!isset($body['query']['function_score']['query']['bool']['must'])) {
$body['query']['function_score']['query']['bool']['must'] = [];
}
$range = [];
if ($timestampUpperBounds) {
$range['lte'] = min($timestampUpperBounds);
}
if ($timestampLowerBounds) {
$range['gt'] = max($timestampLowerBounds);
}
$body['query']['function_score']['query']['bool']['must'][] = [
'range' => [
'@timestamp' => $range,
],
],
];
];
}
//
if ($opts['query']) {
$words = explode(' ', $opts['query']);
......@@ -366,6 +396,10 @@ class Repository
$esType = $opts['type'];
if ($type === 'user' || $type === 'group') {
$esType = 'activity,object:image,object:video,object:blog';
}
if ($esType === 'all') {
$esType = 'object:image,object:video,object:blog';
}
......@@ -396,9 +430,31 @@ class Repository
]);
}
}
$docs = $response['hits']['hits'];
// Sort channels / groups by post scores
if ($type === 'user' || $type === 'group') {
$newDocs = []; // New array so we return only users and groups, not posts
foreach ($docs as $doc) {
$key = $doc['_source'][$this->getSourceField($type)];
$newDocs[$key] = $newDocs[$key] ?? [
'_source' => [
'guid' => $key,
'owner_guid' => $key,
$this->getSourceField($type) => $key,
'@timestamp' => $doc['_source']['@timestamp'],
],
'_type' => $type,
'_score' => 0,
];
$newDocs[$key]['_score'] = log10($newDocs[$key]['_score'] + $algorithm->fetchScore($doc));
}
$docs = $newDocs;
}
$guids = [];
foreach ($response['hits']['hits'] as $doc) {
foreach ($docs as $doc) {
$guid = $doc['_source'][$this->getSourceField($opts['type'])];
if (isset($guids[$guid])) {
continue;
......@@ -416,12 +472,12 @@ class Repository
private function getSourceField(string $type)
{
switch ($type) {
//case 'user':
// return 'owner_guid';
// break;
//case 'group':
// return 'container_guid';
// break;
case 'user':
return 'owner_guid';
break;
case 'group':
return 'container_guid';
break;
default:
return 'guid';
break;
......@@ -430,12 +486,16 @@ class Repository
public function add(MetricsSync $metric)
{
$body = [];
$key = $metric->getMetric();
$key = $metric->getMetric() . ':' . $metric->getPeriod();
$body[$key] = $metric->getCount();
if ($metric->getPeriod()) {
$key .= ":{$metric->getPeriod()}";
}
$body[$key . ':synced'] = $metric->getSynced();
$body = [
$key => $metric->getCount(),
"{$key}:synced" => $metric->getSynced()
];
$this->pendingBulkInserts[] = [
'update' => [
......
......@@ -5,13 +5,13 @@
* @author: Emiliano Balbuena <edgebal>
*/
namespace Minds\Core\Feeds\Top;
namespace Minds\Core\Feeds\Elastic;
use Minds\Traits\MagicAttributes;
/**
* Class ScoredGuid
* @package Minds\Core\Feeds\Top
* @package Minds\Core\Feeds\Elastic
* @method int|string getGuid()
* @method ScoredGuid setGuid(int|string $guid)
* @method float getScore()
......
<?php
/**
* Sync
* @author edgebal
*/
namespace Minds\Core\Feeds\Elastic;
use Exception;
use Minds\Core\Counters;
use Minds\Core\Di\Di;
use Minds\Core\Trending\Aggregates;
class Sync
{
/** @var string */
protected $type;
/** @var string */
protected $subtype;
/** @var int */
protected $from;
/** @var int */
protected $to;
/** @var string */
protected $metric;
/** @var Repository */
protected $repository;
/** @var Counters */
protected $counters;
/**
* Sync constructor.
* @param Repository $repository
* @param Counters $counters
*/
public function __construct(
$repository = null,
$counters = null
) {
$this->repository = $repository ?: new Repository();
$this->counters = $counters ?: Di::_()->get('Entities\Counters');
}
/**
* @param string $type
* @return Sync
*/
public function setType(string $type): Sync
{
$this->type = $type;
return $this;
}
/**
* @param string $subtype
* @return Sync
*/
public function setSubtype(string $subtype): Sync
{
$this->subtype = $subtype;
return $this;
}
/**
* @param int $from
* @return Sync
*/
public function setFrom(int $from): Sync
{
$this->from = $from;
return $this;
}
/**
* @param int $to
* @return Sync
*/
public function setTo(int $to): Sync
{
$this->to = $to;
return $this;
}
/**
* @param string $metric
* @return Sync
*/
public function setMetric(string $metric): Sync
{
$this->metric = $metric;
return $this;
}
/**
* @throws Exception
*/
public function run(): void
{
$type = $this->type;
if ($this->subtype) {
$type = implode(':', [$this->type, $this->subtype]);
}
switch ($this->metric) {
case 'up':
$metricMethod = 'getVotesUp';
$metricId = 'votes:up';
$cassandraCountersMetricId = 'thumbs:up';
break;
case 'down':
$metricMethod = 'getVotesDown';
$metricId = 'votes:down';
$cassandraCountersMetricId = 'thumbs:down';
break;
default:
throw new Exception('Invalid metric');
}
// Sync
$i = 0;
foreach ($this->{$metricMethod}($this->from, $this->to) as $guid => $uniqueCountValue) {
try {
$count = $this->counters->get($guid, $cassandraCountersMetricId);
} catch (Exception $e) {
error_log((string)$e);
$count = (int) abs($uniqueCountValue ?: 0);
}
$metric = new MetricsSync();
$metric
->setGuid($guid)
->setType($type)
->setMetric($metricId)
->setCount($count)
->setSynced(time());
try {
$this->repository->add($metric);
} catch (Exception $e) {
error_log((string)$e);
}
echo sprintf("\n#%s: %s -> %s = %s", ++$i, $guid, $metricId, $count);
}
// Clear any pending bulk inserts
$this->repository->bulk();
}
/**
* @param int $from
* @param int $to
* @return iterable
*/
protected function getVotesUp(int $from, int $to): iterable
{
$aggregates = new Aggregates\Votes;
$aggregates
->setLimit(10000)
->setType($this->type)
->setSubtype($this->subtype)
->setFrom($from)
->setTo($to);
return $aggregates->get();
}
/**
* @param int $from
* @param int $to
* @return iterable
*/
protected function getVotesDown(int $from, int $to): iterable
{
$aggregates = new Aggregates\DownVotes;
$aggregates
->setLimit(10000)
->setType($this->type)
->setSubtype($this->subtype)
->setFrom($from)
->setTo($to);
return $aggregates->get();
}
}
......@@ -8,8 +8,8 @@ class FeedsProvider extends Provider
{
public function register()
{
$this->di->bind('Feeds\Top\Manager', function ($di) {
return new Top\Manager();
$this->di->bind('Feeds\Elastic\Manager', function ($di) {
return new Elastic\Manager();
});
$this->di->bind('Feeds\Firehose\Manager', function ($di) {
......
......@@ -5,7 +5,7 @@ namespace Minds\Core\Feeds\Firehose;
use Minds\Entities\User;
use Minds\Core\Entities\Actions\Save;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\Top\Manager as TopFeedsManager;
use Minds\Core\Feeds\Elastic\Manager as TopFeedsManager;
use Minds\Core\Entities\PropagateProperties;
class Manager
......@@ -25,7 +25,7 @@ class Manager
Save $save = null,
PropagateProperties $propagateProperties = null
) {
$this->topFeedsManager = $topFeedsManager ?: Di::_()->get('Feeds\Top\Manager');
$this->topFeedsManager = $topFeedsManager ?: Di::_()->get('Feeds\Elastic\Manager');
$this->moderationCache = $moderationCache ?: new ModerationCache();
$this->save = $save ?: new Save();
$this->propagateProperties = $propagateProperties ?? Di::_()->get('PropagateProperties');
......
......@@ -9,7 +9,7 @@ namespace Minds\Core\Pro\Channel;
use Exception;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\Top\Manager as TopManager;
use Minds\Core\Feeds\Elastic\Manager as ElasticManager;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
use Minds\Entities\User;
......@@ -21,8 +21,8 @@ class Manager
/** @var Repository */
protected $repository;
/** @var TopManager */
protected $top;
/** @var ElasticManager */
protected $elastic;
/** @var abstractCacher */
protected $cache;
......@@ -33,7 +33,7 @@ class Manager
/**
* Manager constructor.
* @param Repository $repository
* @param TopManager $top
* @param ElasticManager $top
* @param abstractCacher $cache
*/
public function __construct(
......@@ -42,7 +42,7 @@ class Manager
$cache = null
) {
$this->repository = $repository ?: new Repository();
$this->top = $top ?: new TopManager();
$this->elastic = $top ?: new ElasticManager();
$this->cache = $cache ?: Di::_()->get('Cache');
}
......@@ -103,11 +103,11 @@ class Manager
'single_owner_threshold' => 0,
];
$content = $this->top->getList($opts)->toArray();
$content = $this->elastic->getList($opts)->toArray();
if (count($content) < 2) {
$opts['algorithm'] = 'latest';
$content = $this->top->getList($opts)->toArray();
$content = $this->elastic->getList($opts)->toArray();
}
$output[] = [
......
......@@ -5,16 +5,16 @@ namespace Minds\Core\SEO\Sitemaps\Modules;
use Minds\Core\Di\Di;
use Minds\Core\Entities;
use Minds\Core\SEO\Sitemaps\SitemapModule;
use Minds\Core\Feeds\Top\Manager;
use Minds\Core\Feeds\Elastic\Manager;
class SitemapTrending extends SitemapModule
{
/** @var Manager */
protected $topManager;
protected $elasticManager;
public function __construct()
{
$this->topManager = Di::_()->get('Feeds\Top\Manager');
$this->elasticManager = Di::_()->get('Feeds\Elastic\Manager');
}
public function collect($pages, $segments)
......@@ -80,7 +80,7 @@ class SitemapTrending extends SitemapModule
return [];
}
$result = $this->topManager->getList([
$result = $this->elasticManager->getList([
'type' => $key,
'limit' => 500,
'sync' => false,
......
......@@ -9,6 +9,14 @@ namespace Minds\Core\Search\SortingAlgorithms;
class Chronological implements SortingAlgorithm
{
/**
* @return bool
*/
public function isTimestampConstrain(): bool
{
return false; // Old period-based algorithms shouldn't be constrained
}
/**
* @param string $period
* @return $this
......
......@@ -10,6 +10,14 @@ class Controversial implements SortingAlgorithm
{
protected $period;
/**
* @return bool
*/
public function isTimestampConstrain(): bool
{
return false; // Old period-based algorithms shouldn't be constrained
}
/**
* @param string $period
* @return $this
......
......@@ -7,17 +7,40 @@
namespace Minds\Core\Search\SortingAlgorithms;
use Minds\Core\Di\Di;
use Minds\Core\Features\Manager as Features;
class Hot implements SortingAlgorithm
{
/** @var Features */
protected $features;
/** @var string */
protected $period;
public function __construct($features = null)
{
$this->features = $features ?? Di::_()->get('Features');
}
/**
* @return bool
*/
public function isTimestampConstrain(): bool
{
return false; // Old period-based algorithms shouldn't be constrained
}
/**
* @param string $period
* @return $this
*/
public function setPeriod($period)
{
$this->period = $period;
if (!$this->features->has('top-feeds-by-age')) {
$this->period = $period;
}
return $this;
}
......@@ -26,24 +49,34 @@ class Hot implements SortingAlgorithm
*/
public function getQuery()
{
return [
'bool' => [
'must' => [
[
'range' => [
"votes:up:{$this->period}:synced" => [
'gte' => strtotime("1 hour ago", time()),
if ($this->period) {
return [
'bool' => [
'must' => [
[
'range' => [
"votes:up:{$this->period}:synced" => [
'gte' => strtotime("7 days ago", time()),
],
],
],
/*'range' => [
"votes:up:{$this->period}" => [
'gte' => 1,
],
],*/
],
],
]
];
]
];
}
return [
'bool' => [
'must' => [
[
'range' => [
"votes:up" => [
'gte' => 1,
],
],
],
],
]
];
}
/**
......@@ -52,17 +85,32 @@ class Hot implements SortingAlgorithm
public function getScript()
{
$time = time();
if ($this->period) {
return "
def up = doc['votes:up:{$this->period}'].value ?: 0;
def down = doc['votes:down:{$this->period}'].value ?: 0;
def age = $time - (doc['@timestamp'].value.millis / 1000) - 1546300800;
def votes = up - down;
def sign = (votes > 0) ? 1 : (votes < 0 ? -1 : 0);
def order = Math.log(Math.max(Math.abs(votes), 1));
return (sign * order) - (age / 43200);
";
}
return "
def up = doc['votes:up:{$this->period}'].value ?: 0;
def down = doc['votes:down:{$this->period}'].value ?: 0;
def up = doc['votes:up'].value ?: 0;
def down = doc['votes:down'].value ?: 0;
def age = $time - (doc['@timestamp'].value.millis / 1000) - 1546300800;
def age = doc['@timestamp'].value.millis / 1000;
def votes = up - down;
def sign = (votes > 0) ? 1 : (votes < 0 ? -1 : 0);
def order = Math.log(Math.max(Math.abs(votes), 1));
return (sign * order) - (age / 43200);
// Rounds to 7
return Math.round((sign * order + age / 43200) * 1000000) / 1000000;
";
}
......
......@@ -8,6 +8,11 @@ namespace Minds\Core\Search\SortingAlgorithms;
interface SortingAlgorithm
{
/**
* @return bool
*/
public function isTimestampConstrain(): bool;
/**
* @param string $period
* @return $this
......
......@@ -10,6 +10,14 @@ class Top implements SortingAlgorithm
{
protected $period;
/**
* @return bool
*/
public function isTimestampConstrain(): bool
{
return false; // Old period-based algorithms shouldn't be constrained
}
/**
* @param string $period
* @return $this
......
<?php
/**
* TopByPostAge
*
* @author: Emiliano Balbuena <edgebal>
*/
namespace Minds\Core\Search\SortingAlgorithms;
class TopByPostAge implements SortingAlgorithm
{
protected $period;
/**
* @return bool
*/
public function isTimestampConstrain(): bool
{
return true;
}
/**
* @param string $period
* @return $this
*/
public function setPeriod($period)
{
return $this;
}
/**
* @return array
*/
public function getQuery()
{
return [
'bool' => [
'must' => [
[
'exists' => [
'field' => "votes:up",
],
],
],
]
];
}
/**
* @return string
*/
public function getScript()
{
return "
def up = (doc['votes:up'].value ?: 0) * 1.0;
def down = (doc['votes:down'].value ?: 0) * 1.0;
def magnitude = up + down;
if (magnitude <= 0) {
return -10;
}
def score = ((up + 1.9208) / (up + down) - 1.96 * Math.sqrt((up * down) / (up + down) + 0.9604) / (up + down)) / (1 + 3.8416 / (up + down));
return score;
";
}
/**
* @return array
*/
public function getSort()
{
return [
'_score' => [
'order' => 'desc'
]
];
}
/**
* @param array $doc
* @return int|float
*/
public function fetchScore($doc)
{
return $doc['_score'];
}
}
......@@ -3,6 +3,7 @@
namespace Minds\Entities;
use Minds\Core\Di\Provider;
use Minds\Core\Counters;
use Minds\Core\Entities;
use Minds\Core\EntitiesBuilder;
......@@ -20,6 +21,9 @@ class EntitiesProvider extends Provider
$this->di->bind('EntitiesBuilder', function ($di) {
return new EntitiesBuilder();
}, ['useFactory' => true]);
$this->di->bind('Entities\Counters', function ($di) {
return new Counters();
}, ['useFactory' => true]);
$this->di->bind('Entities\Factory', function ($di) {
return new EntitiesFactory();
}, ['useFactory' => true]);
......
......@@ -265,6 +265,7 @@ class Image extends File
'description' => null,
'license' => null,
'mature' => null,
'nsfw' => null,
'boost_rejection_reason' => null,
'hidden' => null,
'batch_guid' => null,
......@@ -283,6 +284,7 @@ class Image extends File
'access_id',
'container_guid',
'mature',
'nsfw',
'boost_rejection_reason',
'rating',
'time_sent',
......@@ -297,7 +299,8 @@ class Image extends File
$data[$field] = (int) $data[$field];
} elseif ($field == 'mature') {
$this->setFlag('mature', !!$data['mature']);
continue;
} elseif ($field == 'nsfw') {
$this->setNsfw($data['nsfw']);
}
$this->$field = $data[$field];
......@@ -344,6 +347,7 @@ class Image extends File
'src' => \elgg_get_site_url() . 'fs/v1/thumbnail/' . $this->guid,
'href' => \elgg_get_site_url() . 'media/' . ($this->container_guid ? $this->container_guid . '/' : '') . $this->guid,
'mature' => $this->getFlag('mature'),
'nsfw' => $this->nsfw ?: [],
'width' => $this->width ?? 0,
'height' => $this->height ?? 0,
'gif' => (bool) ($this->gif ?? false),
......
......@@ -825,7 +825,7 @@ class User extends \ElggUser
$export['programs'] = $this->getPrograms();
$export['plus'] = (bool) $this->isPlus();
$export['pro'] = (bool) $this->isPro();
$export['pro_published'] = (bool) $this->isProPublished();
$export['pro_published'] = $this->isPro() && $this->isProPublished();
$export['verified'] = (bool) $this->verified;
$export['founder'] = (bool) $this->founder;
$export['disabled_boost'] = (bool) $this->disabled_boost;
......
......@@ -189,6 +189,7 @@ class Video extends MindsObject
'description' => null,
'license' => null,
'mature' => null,
'nsfw' => null,
'boost_rejection_reason' => null,
'hidden' => null,
'access_id' => null,
......@@ -206,6 +207,7 @@ class Video extends MindsObject
'access_id',
'container_guid',
'mature',
'nsfw',
'boost_rejection_reason',
'rating',
'time_sent',
......
......@@ -55,11 +55,15 @@ class CreateActivitySpec extends ObjectBehavior
$blog->getIconUrl()
->shouldBeCalled()
->willReturn('http://phpspec/icon.spec.ext');
$blog->isMature()
->shouldBeCalled()
->willReturn(false);
$blog->getNsfw()
->shouldBeCalled()
->willReturn([]);
$blog->getWireThreshold()
->shouldBeCalled()
->willReturn(null);
......
<?php
namespace Spec\Minds\Core\Feeds\Top;
namespace Spec\Minds\Core\Feeds\Elastic;
use Minds\Core\Blogs\Blog;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Feeds\Top\Entities;
use Minds\Core\Feeds\Elastic\Entities;
use Minds\Core\Security\ACL;
use Minds\Entities\Activity;
use Minds\Entities\Image;
......
<?php
namespace Spec\Minds\Core\Feeds\Top;
namespace Spec\Minds\Core\Feeds\Elastic;
use Minds\Common\Repository\Response;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Feeds\Top\Manager;
use Minds\Core\Feeds\Top\Repository;
use Minds\Core\Feeds\Top\ScoredGuid;
use Minds\Core\Feeds\Elastic\Manager;
use Minds\Core\Feeds\Elastic\Repository;
use Minds\Core\Feeds\Elastic\ScoredGuid;
use Minds\Core\Search\Search;
use Minds\Entities\Entity;
use PhpSpec\Exception\Example\FailureException;
......
<?php
namespace Spec\Minds\Core\Feeds\Top;
namespace Spec\Minds\Core\Feeds\Elastic;
use Minds\Core\Config;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Feeds\Top\MetricsSync;
use Minds\Core\Feeds\Top\Repository;
use Minds\Core\Feeds\Elastic\MetricsSync;
use Minds\Core\Feeds\Elastic\Repository;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
......@@ -93,7 +93,7 @@ class RepositorySpec extends ObjectBehavior
$this->client->request(Argument::that(function ($query) {
$query = $query->build();
return $query['type'] === 'user' && in_array('guid', $query['body']['_source'], true);
return $query['type'] === 'activity,object:image,object:video,object:blog' && in_array('owner_guid', $query['body']['_source'], true);
}))
->shouldBeCalled()
->willReturn([
......@@ -126,10 +126,10 @@ class RepositorySpec extends ObjectBehavior
$gen = $this->getList($opts);
$gen->current()->getGuid()->shouldReturn('1');
$gen->current()->getScore()->shouldReturn(100.0);
$gen->current()->getScore()->shouldReturn(log10(100.0));
$gen->next();
$gen->current()->getGuid()->shouldReturn('2');
$gen->current()->getScore()->shouldReturn(50.0);
$gen->current()->getScore()->shouldReturn(log10(50.0));
}
public function it_should_query_a_list_of_group_guids()
......@@ -143,7 +143,7 @@ class RepositorySpec extends ObjectBehavior
$this->client->request(Argument::that(function ($query) {
$query = $query->build();
return $query['type'] === 'group' && in_array('guid', $query['body']['_source'], true);
return $query['type'] === 'activity,object:image,object:video,object:blog' && in_array('container_guid', $query['body']['_source'], true);
}))
->shouldBeCalled()
->willReturn([
......@@ -178,10 +178,10 @@ class RepositorySpec extends ObjectBehavior
$gen = $this->getList($opts);
$gen->current()->getGuid()->shouldReturn('1');
$gen->current()->getScore()->shouldReturn(100.0);
$gen->current()->getScore()->shouldReturn(log10(100));
$gen->next();
$gen->current()->getGuid()->shouldReturn('2');
$gen->current()->getScore()->shouldReturn(50.0);
$gen->current()->getScore()->shouldReturn(log10(50));
}
// Seems like yielded functions have issues with PHPSpec
......
<?php
namespace Spec\Minds\Core\Feeds\Top;
namespace Spec\Minds\Core\Feeds\Elastic;
use Minds\Core\Feeds\Top\ScoredGuid;
use Minds\Core\Feeds\Elastic\ScoredGuid;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
......
......@@ -11,7 +11,7 @@ use Minds\Entities\Activity;
use Minds\Entities\Entity;
use Minds\Core\Blogs\Blog;
use Minds\Entities\Image;
use Minds\Core\Feeds\Top\Manager as TopFeedsManager;
use Minds\Core\Feeds\Elastic\Manager as TopFeedsManager;
use Minds\Core\Feeds\Firehose\ModerationCache;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Data\Call;
......
......@@ -4,7 +4,7 @@ namespace Spec\Minds\Core\Pro\Channel;
use Minds\Common\Repository\Response;
use Minds\Core\Data\cache\abstractCacher;
use Minds\Core\Feeds\Top\Manager as TopManager;
use Minds\Core\Feeds\Elastic\Manager as ElasticManager;
use Minds\Core\Pro\Channel\Manager;
use Minds\Core\Pro\Repository;
use Minds\Core\Pro\Settings;
......@@ -17,22 +17,22 @@ class ManagerSpec extends ObjectBehavior
/** @var Repository */
protected $repository;
/** @var TopManager */
protected $top;
/** @var ElasticManager */
protected $elastic;
/** @var abstractCacher */
protected $cache;
public function let(
Repository $repository,
TopManager $top,
ElasticManager $elastic,
abstractCacher $cache
) {
$this->repository = $repository;
$this->top = $top;
$this->elastic = $elastic;
$this->cache = $cache;
$this->beConstructedWith($repository, $top, $cache);
$this->beConstructedWith($repository, $elastic, $cache);
}
public function it_is_initializable()
......@@ -74,7 +74,7 @@ class ManagerSpec extends ObjectBehavior
['tag' => 'test2', 'label' => 'Test 2'],
]);
$this->top->getList(Argument::that(function (array $opts) {
$this->elastic->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'top' && $opts['hashtags'] === ['test1'];
}))
->shouldBeCalled()
......@@ -84,7 +84,7 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn([5000, 5001, 5002]);
$this->top->getList(Argument::that(function (array $opts) {
$this->elastic->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'top' && $opts['hashtags'] === ['test2'];
}))
->shouldBeCalled()
......@@ -94,7 +94,7 @@ class ManagerSpec extends ObjectBehavior
->shouldBeCalled()
->willReturn([]);
$this->top->getList(Argument::that(function (array $opts) {
$this->elastic->getList(Argument::that(function (array $opts) {
return $opts['algorithm'] === 'latest' && $opts['hashtags'] === ['test2'];
}))
->shouldBeCalled()
......
......@@ -481,6 +481,7 @@ $CONFIG->set('features', [
'permissions' => false,
'pro' => false,
'webtorrent' => false,
'top-feeds-by-age' => true,
]);
$CONFIG->set('email', [
......
......@@ -10,7 +10,7 @@ rm -rf ../vendor
# Setup composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php -r "if (hash_file('SHA384', 'composer-setup.php') === '106d3d32cc30011325228b9272424c1941ad75207cb5244bee161e5f9906b0edf07ab2a733e8a1c945173eb9b1966197') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
......