Loading Controllers/api/v2/boost.php +15 −7 Original line number Original line Diff line number Diff line Loading @@ -297,6 +297,14 @@ class boost implements Interfaces\Api ]); ]); } } if ($manager->isBoostLimitExceededBy($boost)) { $maxDaily = Di::_()->get('Config')->get('max_daily_boost_views') / 1000; return Factory::response([ 'status' => 'error', 'message' => "Exceeded maximum of ".$maxDaily." offchain tokens per day." ]); } // Pre-set GUID // Pre-set GUID if ($bidType == 'tokens' && isset($_POST['guid'])) { if ($bidType == 'tokens' && isset($_POST['guid'])) { Loading Core/Boost/Network/ElasticRepository.php +23 −5 Original line number Original line Diff line number Diff line Loading @@ -30,11 +30,13 @@ class ElasticRepository 'rating' => 3, 'rating' => 3, 'token' => 0, 'token' => 0, 'offset' => null, 'offset' => null, 'order' => null, 'offchain' => null, ], $opts); ], $opts); $must = []; $must = []; $must_not = []; $must_not = []; $sort = [ '@timestamp' => 'asc' ]; $sort = [ '@timestamp' => $opts['order'] ?? 'asc' ]; $must[] = [ $must[] = [ 'term' => [ 'term' => [ Loading Loading @@ -67,8 +69,16 @@ class ElasticRepository if ($opts['entity_guid']) { if ($opts['entity_guid']) { $must[] = [ $must[] = [ 'term' => [ 'term' => [ 'entity_guid' => $opts['entity_guid'] 'entity_guid' => $opts['entity_guid'], ] ], ]; } if ($opts['owner_guid']) { $must[] = [ 'term' => [ 'owner_guid' => $opts['owner_guid'], ], ]; ]; } } Loading @@ -87,6 +97,14 @@ class ElasticRepository ]; ]; } } if ($opts['offchain']) { $must[] = [ 'term' => [ 'token_method' => 'offchain', ], ]; } if ($opts['state'] === 'review') { if ($opts['state'] === 'review') { $must_not[] = [ $must_not[] = [ 'exists' => [ 'exists' => [ Loading @@ -96,7 +114,7 @@ class ElasticRepository $sort = ['@timestamp' => 'asc']; $sort = ['@timestamp' => 'asc']; } } if ($opts['state'] === 'approved' || $opts['state'] === 'review') { if ($opts['state'] === 'approved' || $opts['state'] === 'review' || $opts['state'] === 'active') { $must_not[] = [ $must_not[] = [ 'exists' => [ 'exists' => [ 'field' => '@completed', 'field' => '@completed', Loading Core/Boost/Network/Manager.php +56 −3 Original line number Original line Diff line number Diff line Loading @@ -25,16 +25,21 @@ class Manager /** @var GuidBuilder $guidBuilder */ /** @var GuidBuilder $guidBuilder */ private $guidBuilder; private $guidBuilder; /** @var Config $config */ private $config; public function __construct( public function __construct( $repository = null, $repository = null, $elasticRepository = null, $elasticRepository = null, $entitiesBuilder = null, $entitiesBuilder = null, $guidBuilder = null $guidBuilder = null, $config = null ) { ) { $this->repository = $repository ?: new Repository; $this->repository = $repository ?: new Repository; $this->elasticRepository = $elasticRepository ?: new ElasticRepository; $this->elasticRepository = $elasticRepository ?: new ElasticRepository; $this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder'); $this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder'); $this->guidBuilder = $guidBuilder ?: new GuidBuilder; $this->guidBuilder = $guidBuilder ?: new GuidBuilder; $this->config = $config ?: Di::_()->get('Config'); } } /** /** Loading @@ -50,14 +55,14 @@ class Manager 'state' => null, 'state' => null, ], $opts); ], $opts); if ($opts['state'] == 'review') { if ($opts['state'] == 'review' || $opts['state'] == 'active') { $opts['useElastic'] = true; $opts['useElastic'] = true; } } if ($opts['useElastic']) { if ($opts['useElastic']) { $response = $this->elasticRepository->getList($opts); $response = $this->elasticRepository->getList($opts); if ($opts['state'] === 'review') { if ($opts['state'] === 'review' || $opts['state'] === 'active') { $opts['guids'] = array_map(function ($boost) { $opts['guids'] = array_map(function ($boost) { return $boost->getGuid(); return $boost->getGuid(); }, $response->toArray()); }, $response->toArray()); Loading Loading @@ -156,4 +161,52 @@ class Manager return $existingBoost->count() > 0; return $existingBoost->count() > 0; } } /** * True if the boost is invalid due to the offchain boost limit being reached * * @param Boost $type the Boost object. * @return boolean true if the boost limit has been reached. */ public function isBoostLimitExceededBy($boost) { //get offchain boosts $offchain = $this->getOffchainBoosts($boost); //filter to get todays offchain transactions $offlineToday = array_filter($offchain->toArray(), function ($result) { return $result->getCreatedTimestamp() > time() - (60 * 60 * 24); }); //reduce the impressions to count the days boosts. $acc = array_reduce($offlineToday, function ($carry, $_boost) { $carry += $_boost->getImpressions(); return $carry; }, 0); $maxDaily = $this->config->get('max_daily_boost_views'); return $acc + $boost->getImpressions() > $maxDaily; //still allow 10k } /** * Gets the users last offchain boosts, from the most recent boost backwards in time. * * @param string $type the type of the boost * @param integer $limit default to 10. * @return $existingBoosts */ public function getOffchainBoosts($boost, $limit = 10) { $existingBoosts = $this->getList([ 'useElastic' => true, 'state' => 'active', 'type' => $boost->getType(), 'limit' => $limit, 'order' => 'desc', 'offchain' => true, 'owner_guid' => $boost->getOwnerGuid(), ]); return $existingBoosts; } } } Spec/Core/Boost/Network/ManagerSpec.php +108 −2 Original line number Original line Diff line number Diff line Loading @@ -13,6 +13,7 @@ use Minds\Entities\Activity; use Minds\Entities\User; use Minds\Entities\User; use PhpSpec\ObjectBehavior; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Prophecy\Argument; use Minds\Core\Di\Di; class ManagerSpec extends ObjectBehavior class ManagerSpec extends ObjectBehavior { { Loading Loading @@ -271,12 +272,12 @@ class ManagerSpec extends ObjectBehavior public function it_should_check_if_the_entity_was_already_boosted(Boost $boost) public function it_should_check_if_the_entity_was_already_boosted(Boost $boost) { { $this->elasticRepository->getList([ $this->elasticRepository->getList([ 'hydrate' => true, 'useElastic' => true, 'useElastic' => true, 'state' => 'review', 'state' => 'review', 'type' => 'newsfeed', 'type' => 'newsfeed', 'entity_guid' => '123', 'entity_guid' => '123', 'limit' => 1, 'limit' => 1 'hydrate' => true, ]) ]) ->shouldBeCalled() ->shouldBeCalled() ->willReturn(new Response([$boost], '')); ->willReturn(new Response([$boost], '')); Loading @@ -295,4 +296,109 @@ class ManagerSpec extends ObjectBehavior $this->checkExisting($boost)->shouldReturn(true); $this->checkExisting($boost)->shouldReturn(true); } } public function it_should_request_offchain_boosts(Boost $boost) { $this->elasticRepository->getList([ "hydrate" => true, "useElastic" => true, "state" => "active", "type" => "newsfeed", "limit" => 10, "order" => "desc", "offchain" => true, "owner_guid" => "123" ]) ->shouldBeCalled() ->willReturn(new Response([$boost], '')); $this->repository->getList(Argument::any()) ->shouldBeCalled() ->willReturn(new Response([$boost])); $boost->getType() ->shouldBeCalled() ->willReturn('newsfeed'); $boost->getOwnerGuid() ->shouldBeCalled() ->willReturn('123'); $this->getOffchainBoosts($boost)->shouldHaveType('Minds\Common\Repository\Response'); } public function it_should_recognise_a_user_has_reached_the_offchain_boost_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 10; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(1000); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(true); } public function it_should_recognise_a_user_has_NOT_reached_the_offchain_boost_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 9; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(1000); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(false); } public function it_should_recognise_a_boost_would_take_user_above_offchain_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 2; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(4501); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(true); } public function runThroughGetList($boost, $existingBoosts) { $this->elasticRepository->getList([ "hydrate" => true, "useElastic" => true, "state" => "active", "type" => "newsfeed", "limit" => 10, "order" => "desc", "offchain" => true, "owner_guid" => "123" ]) ->shouldBeCalled() ->willReturn(new Response($existingBoosts, '')); $this->repository->getList(Argument::any()) ->shouldBeCalled() ->willReturn(new Response($existingBoosts)); $boost->getType() ->shouldBeCalled() ->willReturn('newsfeed'); $boost->getOwnerGuid() ->shouldBeCalled() ->willReturn('123'); $boost->getImpressions() ->shouldBeCalled() ->willReturn(1000); } } } settings.example.php +3 −0 Original line number Original line Diff line number Diff line Loading @@ -269,6 +269,9 @@ $CONFIG->set('boost', [ ], ], ]); ]); /* Maximum view per day */ $CONFIG->set('max_daily_boost_views', 10000); $CONFIG->set('encryptionKeys', [ $CONFIG->set('encryptionKeys', [ 'email' => [ 'email' => [ 'private' => '{{email-private-key}}', 'private' => '{{email-private-key}}', Loading Loading
Controllers/api/v2/boost.php +15 −7 Original line number Original line Diff line number Diff line Loading @@ -297,6 +297,14 @@ class boost implements Interfaces\Api ]); ]); } } if ($manager->isBoostLimitExceededBy($boost)) { $maxDaily = Di::_()->get('Config')->get('max_daily_boost_views') / 1000; return Factory::response([ 'status' => 'error', 'message' => "Exceeded maximum of ".$maxDaily." offchain tokens per day." ]); } // Pre-set GUID // Pre-set GUID if ($bidType == 'tokens' && isset($_POST['guid'])) { if ($bidType == 'tokens' && isset($_POST['guid'])) { Loading
Core/Boost/Network/ElasticRepository.php +23 −5 Original line number Original line Diff line number Diff line Loading @@ -30,11 +30,13 @@ class ElasticRepository 'rating' => 3, 'rating' => 3, 'token' => 0, 'token' => 0, 'offset' => null, 'offset' => null, 'order' => null, 'offchain' => null, ], $opts); ], $opts); $must = []; $must = []; $must_not = []; $must_not = []; $sort = [ '@timestamp' => 'asc' ]; $sort = [ '@timestamp' => $opts['order'] ?? 'asc' ]; $must[] = [ $must[] = [ 'term' => [ 'term' => [ Loading Loading @@ -67,8 +69,16 @@ class ElasticRepository if ($opts['entity_guid']) { if ($opts['entity_guid']) { $must[] = [ $must[] = [ 'term' => [ 'term' => [ 'entity_guid' => $opts['entity_guid'] 'entity_guid' => $opts['entity_guid'], ] ], ]; } if ($opts['owner_guid']) { $must[] = [ 'term' => [ 'owner_guid' => $opts['owner_guid'], ], ]; ]; } } Loading @@ -87,6 +97,14 @@ class ElasticRepository ]; ]; } } if ($opts['offchain']) { $must[] = [ 'term' => [ 'token_method' => 'offchain', ], ]; } if ($opts['state'] === 'review') { if ($opts['state'] === 'review') { $must_not[] = [ $must_not[] = [ 'exists' => [ 'exists' => [ Loading @@ -96,7 +114,7 @@ class ElasticRepository $sort = ['@timestamp' => 'asc']; $sort = ['@timestamp' => 'asc']; } } if ($opts['state'] === 'approved' || $opts['state'] === 'review') { if ($opts['state'] === 'approved' || $opts['state'] === 'review' || $opts['state'] === 'active') { $must_not[] = [ $must_not[] = [ 'exists' => [ 'exists' => [ 'field' => '@completed', 'field' => '@completed', Loading
Core/Boost/Network/Manager.php +56 −3 Original line number Original line Diff line number Diff line Loading @@ -25,16 +25,21 @@ class Manager /** @var GuidBuilder $guidBuilder */ /** @var GuidBuilder $guidBuilder */ private $guidBuilder; private $guidBuilder; /** @var Config $config */ private $config; public function __construct( public function __construct( $repository = null, $repository = null, $elasticRepository = null, $elasticRepository = null, $entitiesBuilder = null, $entitiesBuilder = null, $guidBuilder = null $guidBuilder = null, $config = null ) { ) { $this->repository = $repository ?: new Repository; $this->repository = $repository ?: new Repository; $this->elasticRepository = $elasticRepository ?: new ElasticRepository; $this->elasticRepository = $elasticRepository ?: new ElasticRepository; $this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder'); $this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder'); $this->guidBuilder = $guidBuilder ?: new GuidBuilder; $this->guidBuilder = $guidBuilder ?: new GuidBuilder; $this->config = $config ?: Di::_()->get('Config'); } } /** /** Loading @@ -50,14 +55,14 @@ class Manager 'state' => null, 'state' => null, ], $opts); ], $opts); if ($opts['state'] == 'review') { if ($opts['state'] == 'review' || $opts['state'] == 'active') { $opts['useElastic'] = true; $opts['useElastic'] = true; } } if ($opts['useElastic']) { if ($opts['useElastic']) { $response = $this->elasticRepository->getList($opts); $response = $this->elasticRepository->getList($opts); if ($opts['state'] === 'review') { if ($opts['state'] === 'review' || $opts['state'] === 'active') { $opts['guids'] = array_map(function ($boost) { $opts['guids'] = array_map(function ($boost) { return $boost->getGuid(); return $boost->getGuid(); }, $response->toArray()); }, $response->toArray()); Loading Loading @@ -156,4 +161,52 @@ class Manager return $existingBoost->count() > 0; return $existingBoost->count() > 0; } } /** * True if the boost is invalid due to the offchain boost limit being reached * * @param Boost $type the Boost object. * @return boolean true if the boost limit has been reached. */ public function isBoostLimitExceededBy($boost) { //get offchain boosts $offchain = $this->getOffchainBoosts($boost); //filter to get todays offchain transactions $offlineToday = array_filter($offchain->toArray(), function ($result) { return $result->getCreatedTimestamp() > time() - (60 * 60 * 24); }); //reduce the impressions to count the days boosts. $acc = array_reduce($offlineToday, function ($carry, $_boost) { $carry += $_boost->getImpressions(); return $carry; }, 0); $maxDaily = $this->config->get('max_daily_boost_views'); return $acc + $boost->getImpressions() > $maxDaily; //still allow 10k } /** * Gets the users last offchain boosts, from the most recent boost backwards in time. * * @param string $type the type of the boost * @param integer $limit default to 10. * @return $existingBoosts */ public function getOffchainBoosts($boost, $limit = 10) { $existingBoosts = $this->getList([ 'useElastic' => true, 'state' => 'active', 'type' => $boost->getType(), 'limit' => $limit, 'order' => 'desc', 'offchain' => true, 'owner_guid' => $boost->getOwnerGuid(), ]); return $existingBoosts; } } }
Spec/Core/Boost/Network/ManagerSpec.php +108 −2 Original line number Original line Diff line number Diff line Loading @@ -13,6 +13,7 @@ use Minds\Entities\Activity; use Minds\Entities\User; use Minds\Entities\User; use PhpSpec\ObjectBehavior; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Prophecy\Argument; use Minds\Core\Di\Di; class ManagerSpec extends ObjectBehavior class ManagerSpec extends ObjectBehavior { { Loading Loading @@ -271,12 +272,12 @@ class ManagerSpec extends ObjectBehavior public function it_should_check_if_the_entity_was_already_boosted(Boost $boost) public function it_should_check_if_the_entity_was_already_boosted(Boost $boost) { { $this->elasticRepository->getList([ $this->elasticRepository->getList([ 'hydrate' => true, 'useElastic' => true, 'useElastic' => true, 'state' => 'review', 'state' => 'review', 'type' => 'newsfeed', 'type' => 'newsfeed', 'entity_guid' => '123', 'entity_guid' => '123', 'limit' => 1, 'limit' => 1 'hydrate' => true, ]) ]) ->shouldBeCalled() ->shouldBeCalled() ->willReturn(new Response([$boost], '')); ->willReturn(new Response([$boost], '')); Loading @@ -295,4 +296,109 @@ class ManagerSpec extends ObjectBehavior $this->checkExisting($boost)->shouldReturn(true); $this->checkExisting($boost)->shouldReturn(true); } } public function it_should_request_offchain_boosts(Boost $boost) { $this->elasticRepository->getList([ "hydrate" => true, "useElastic" => true, "state" => "active", "type" => "newsfeed", "limit" => 10, "order" => "desc", "offchain" => true, "owner_guid" => "123" ]) ->shouldBeCalled() ->willReturn(new Response([$boost], '')); $this->repository->getList(Argument::any()) ->shouldBeCalled() ->willReturn(new Response([$boost])); $boost->getType() ->shouldBeCalled() ->willReturn('newsfeed'); $boost->getOwnerGuid() ->shouldBeCalled() ->willReturn('123'); $this->getOffchainBoosts($boost)->shouldHaveType('Minds\Common\Repository\Response'); } public function it_should_recognise_a_user_has_reached_the_offchain_boost_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 10; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(1000); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(true); } public function it_should_recognise_a_user_has_NOT_reached_the_offchain_boost_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 9; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(1000); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(false); } public function it_should_recognise_a_boost_would_take_user_above_offchain_limit(Boost $boost) { $boostArray = []; for ($i = 0; $i < 2; $i++) { $newBoost = new Boost(); $newBoost->setCreatedTimestamp('9999999999999999'); $newBoost->setImpressions(4501); array_push($boostArray, $newBoost); } Di::_()->get('Config')->set('max_daily_boost_views', 10000); $this->runThroughGetList($boost, $boostArray); $this->isBoostLimitExceededBy($boost)->shouldReturn(true); } public function runThroughGetList($boost, $existingBoosts) { $this->elasticRepository->getList([ "hydrate" => true, "useElastic" => true, "state" => "active", "type" => "newsfeed", "limit" => 10, "order" => "desc", "offchain" => true, "owner_guid" => "123" ]) ->shouldBeCalled() ->willReturn(new Response($existingBoosts, '')); $this->repository->getList(Argument::any()) ->shouldBeCalled() ->willReturn(new Response($existingBoosts)); $boost->getType() ->shouldBeCalled() ->willReturn('newsfeed'); $boost->getOwnerGuid() ->shouldBeCalled() ->willReturn('123'); $boost->getImpressions() ->shouldBeCalled() ->willReturn(1000); } } }
settings.example.php +3 −0 Original line number Original line Diff line number Diff line Loading @@ -269,6 +269,9 @@ $CONFIG->set('boost', [ ], ], ]); ]); /* Maximum view per day */ $CONFIG->set('max_daily_boost_views', 10000); $CONFIG->set('encryptionKeys', [ $CONFIG->set('encryptionKeys', [ 'email' => [ 'email' => [ 'private' => '{{email-private-key}}', 'private' => '{{email-private-key}}', Loading