Commit 514184e4 authored by Mark Harding's avatar Mark Harding
Browse files

Merge branch 'feat/rate-limit-boost-475' into 'master'

[Sprint/InterestingIguana](feat): Offchain boost rate limiting #475

Closes #475

See merge request !251
parents 365b8c4b e16ed1d3
Loading
Loading
Loading
Loading
+15 −7
Original line number Original line Diff line number Diff line
@@ -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'])) {
+23 −5
Original line number Original line Diff line number Diff line
@@ -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' => [
@@ -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'],
                ],
            ];
            ];
        }
        }


@@ -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' => [
@@ -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',
+56 −3
Original line number Original line Diff line number Diff line
@@ -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');
    }
    }


    /**
    /**
@@ -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());
@@ -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;
    }
}
}
+108 −2
Original line number Original line Diff line number Diff line
@@ -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
{
{
@@ -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], ''));
@@ -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);
    }
}
}
+3 −0
Original line number Original line Diff line number Diff line
@@ -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}}',