Skip to content
Commits on Source (4)
<?php
namespace Minds\Api;
abstract class Api implements \Minds\Interfaces\Api
{
protected $accessControlAllowOrigin = ['*'];
protected $accessControlAllowHeaders = [];
protected $accessControlAllowMethods = [];
protected $defaultResponse = ['status' => 'success'];
const HTTP_CODES = [
100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Moved Temporarily',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Time-out',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Large',
415 => 'Unsupported Media Type',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Time-out',
505 => 'HTTP Version not supported',
];
public function __construct()
{
$this->sendAccessControlHeaders();
}
protected function sendAccessControlHeaders(): void
{
$this->sendAccessControlAllowOrigin();
$this->sendAccessControlAllowHeaders();
$this->sendAccessControlAllowMethods();
}
protected function sendAccessControlAllowOrigin(): void
{
if (!empty($this->accessControlAllowOrigin)) {
header("Access-Control-Allow-Origin: " .
$this->parseAccessControlArray($this->accessControlAllowOrigin), false);
}
}
protected function sendAccessControlAllowHeaders(): void
{
if (!empty($this->accessControlAllowHeaders)) {
header("Access-Control-Allow-Headers: " .
$this->parseAccessControlArray($this->accessControlAllowHeaders), false);
}
}
protected function sendAccessControlAllowMethods(): void
{
if (!empty($this->accessControlAllowMethods)) {
header("Access-Control-Allow-Methods: " .
$this->parseAccessControlArray($this->accessControlAllowMethods), false);
}
}
protected function parseAccessControlArray(array $accessControlArray): string
{
$output = "";
$lastHeader = end($accessControlArray);
foreach ($accessControlArray as $header) {
$output .= $header;
if ($header !== $lastHeader) {
$output .= ",";
}
}
return $output;
}
protected function setResponseCode(int $code = 200): int
{
if (!isset(self::HTTP_CODES[$code])) {
exit('Unknown http status code "' . htmlentities($code) . '"');
}
$text = self::HTTP_CODES[$code];
$protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
header("${protocol} ${code} ${text}");
return $code;
}
protected function sendArrayOfObjects($array, int $code = 200): void
{
$this->send(array_values($array), $code);
}
protected function send($responseArray, int $code = 200, $jsonOptions = 0): void
{
$responseArray = array_merge($this->defaultResponse, $responseArray);
$returnString = json_encode($responseArray, $jsonOptions);
$this->sendJsonString($returnString, $code);
}
protected function sendJsonString(string $jsonString, int $code = 200): void
{
header('Content-Type: application/json');
header('Content-Length:' . strlen($jsonString));
$this->setResponseCode($code);
echo $jsonString;
}
protected function sendInternalServerError(): void
{
$this->sendError(500);
}
protected function sendBadRequest(): void
{
$this->sendError(400);
}
protected function sendNotImplemented(): void
{
$this->sendError(501);
}
protected function sendNotModified(): void
{
$this->sendError(304);
}
protected function sendNotAcceptable(): void
{
$this->sendError(406);
}
protected function sendUnauthorised(): void
{
$this->sendError(401);
}
protected function sendSuccess(): void
{
$this->send([]);
}
protected function sendError(int $code = 406, string $message = null): void
{
if (is_null($message)) {
$message = self::HTTP_CODES[$code];
}
$this->send($this->buildError($message), $code);
}
protected function buildError(string $message): array
{
return [
'status' => 'error',
'message' => $message
];
}
public function get($pages): void
{
$this->sendNotImplemented();
}
public function post($pages): void
{
$this->sendNotImplemented();
}
public function put($pages): void
{
$this->sendNotImplemented();
}
public function delete($pages): void
{
$this->sendNotImplemented();
}
}
......@@ -160,16 +160,17 @@ class Controller
/**
* Gets the list of publically available commands and filters out the system ones.
* @param array $additionalExcludes Additional methods to exclude from command list (optional)
*/
public function getCommands()
public function getCommands(array $additionalExcludes = [])
{
$excludedMethods = ['__construct', 'help', 'out', 'setArgs', 'setApp', 'getApp', 'getExecCommand', 'getOpt', 'getOpts', 'getAllOpts', 'getCommands', 'displayCommandHelp'];
$excludedMethods = ['__construct', 'help', 'out', 'setArgs', 'setApp', 'getApp', 'getExecCommand', 'getOpt', 'getOpts', 'getAllOpts', 'getCommands', 'displayCommandHelp', 'exec', 'gatekeeper'];
$commands = [];
foreach ((new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$commands[] = $method->getName();
}
return array_diff($commands, $excludedMethods);
return array_diff($commands, $excludedMethods, $additionalExcludes);
}
public function displayCommandHelp()
......
......@@ -30,7 +30,12 @@ class Analytics extends Cli\Controller implements Interfaces\CliControllerInterf
$this->out('Prints the counts of a user');
$this->out('--from={timestamp in milliseconds} the day to start count. Default is yesterday');
$this->out('--guid={user guid} REQUIRED the user to aggregate');
// no break
break;
case 'boostViews':
$this->out('Return total boost views for period with daily breakdown');
$this->out('--from={timestamp} the start day for view range. Default is 10 days ago');
$this->out('--to={timestamp} the end day for view range. Default is yesterday');
// no break
default:
$this->out('Syntax usage: cli analytics <type>');
$this->displayCommandHelp();
......@@ -85,6 +90,7 @@ class Analytics extends Cli\Controller implements Interfaces\CliControllerInterf
}
public function sync_graphs()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
......@@ -120,7 +126,7 @@ class Analytics extends Cli\Controller implements Interfaces\CliControllerInterf
foreach ($aggregates as $aggregate) {
$this->out("Syncing {$aggregate}");
$manager->sync([
'aggregate' => $aggregate,
'all' => true,
......@@ -155,4 +161,13 @@ class Analytics extends Cli\Controller implements Interfaces\CliControllerInterf
$this->out('Done');
}
public function boostViews()
{
$from = $this->getOpt('from') ?: strtotime('-10 days');
$to = $this->getOpt('to') ?: strtotime('-1 day');
$boostViews = new Core\Analytics\EntityCentric\BoostViewsDaily();
$data = $boostViews->getDataSetForDateRange($from, $to);
print_r($data);
}
}
<?php
namespace Minds\Controllers\Cli;
use DateTime;
use Elasticsearch\ClientBuilder;
use Minds\Cli;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
use Minds\Helpers\Flags;
use Minds\Interfaces;
use Minds\Core\Rewards\Contributions\UsersIterator;
class BoostCampaigns extends Cli\Controller implements Interfaces\CliControllerInterface
{
public function help($command = null)
{
$this->out('Syntax usage: cli trending <type>');
}
public function exec()
{
}
public function start()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
$offset = $this->getOpt('offset') ?? null;
$type = $this->getOpt('type') ?? 'newsfeed';
$ownerGuid = $this->getOpt('ownerGuid') ?? null;
/** @var Core\Boost\Campaigns\Manager $manager */
$manager = Di::_()->get('Boost\Campaigns\Manager');
$iterator = (new Core\Boost\Campaigns\Iterator())
->setOffset($offset)
->setState(Core\Boost\Campaigns\Campaign::STATUS_CREATED)
->setOwnerGuid($ownerGuid)
->setType($type);
foreach ($iterator as $campaign) {
try {
$manager->start($campaign);
} catch (\Exception $e) {
error_log(get_class($e) . ': ' . $e->getMessage());
continue;
}
}
}
}
<?php
/**
* Minds Admin: Boost Analytics
*
* @version 1
* @author Emi Balbuena
*
*/
namespace Minds\Controllers\api\v1\admin\boosts;
use Minds\Controllers\api\v1\newsfeed\preview;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Helpers;
use Minds\Entities;
use Minds\Interfaces;
use Minds\Api\Factory;
class analytics implements Interfaces\Api, Interfaces\ApiAdminPam
{
/**
* GET
*/
public function get($pages)
{
$response = [];
$type = isset($pages[0]) ? $pages[0] : 'newsfeed';
/** @var Core\Boost\Network\Review $review */
$review = Di::_()->get('Boost\Network\Review');
$review->setType($type);
/** @var Core\Boost\Network\Metrics $metrics */
$metrics = Di::_()->get('Boost\Network\Metrics');
$metrics->setType($type);
$cache = Di::_()->get('Cache');
$cacheKey = "admin:boosts:analytics:{$type}";
if ($cached = $cache->get($cacheKey)) {
return Factory::response($cached);
}
$reviewQueue = $review->getReviewQueueCount();
$backlog = $metrics->getBacklogCount();
$priorityBacklog = $metrics->getPriorityBacklogCount();
$impressions = $metrics->getBacklogImpressionsSum();
$avgApprovalTime = $metrics->getAvgApprovalTime();
$avgImpressions = round($impressions / ($backlog ?: 1));
$timestamp = time();
$response = compact(
'reviewQueue',
'backlog',
'priorityBacklog',
'impressions',
'avgApprovalTime',
'avgImpressions',
'timestamp'
);
$cache->set($cacheKey, $response, 15 * 60 /* 15min cache */);
return Factory::response($response);
}
/**
* POST
*/
public function post($pages)
{
return Factory::response([]);
}
/**
* PUT
*/
public function put($pages)
{
return Factory::response([]);
}
/**
* DELETE
*/
public function delete($pages)
{
return Factory::response([]);
}
}
<?php
/**
* Minds Boost Api endpoint
*
* @version 1
* @author Mark Harding
*
*/
namespace Minds\Controllers\api\v1\boost;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
use Minds\Entities\Entity;
use Minds\Helpers\Counters;
use Minds\Interfaces;
use Minds\Core\Boost;
class fetch implements Interfaces\Api
{
/**
* Return a list of boosts that a user needs to review
* @param array $pages
......@@ -29,14 +22,16 @@ class fetch implements Interfaces\Api
$user = Core\Session::getLoggedinUser();
if (!$user) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'You must be loggedin to view boosts',
]);
return;
}
if ($user->disabled_boost && $user->isPlus()) {
return Factory::response([]);
Factory::response([]);
return;
}
$limit = isset($_GET['limit']) ? (int) $_GET['limit'] : 2;
......@@ -46,35 +41,33 @@ class fetch implements Interfaces\Api
// options specific to newly created users (<=1 hour) and iOS users
if (time() - $user->getTimeCreated() <= 3600) {
$rating = 1; // they can only see safe content
$rating = Boost\Network\Boost::RATING_SAFE;
$quality = 75;
}
if ($platform === 'ios') {
$rating = 1; // they can only see safe content
$rating = Boost\Network\Boost::RATING_SAFE;
$quality = 90;
}
/** @var Core\Boost\Network\Iterator $iterator */
$iterator = Core\Di\Di::_()->get('Boost\Network\Iterator');
/** @var $iterator */
$iterator = new Core\Boost\Network\Iterator();
$iterator->setLimit($limit)
->setRating($rating)
->setQuality($quality)
->setOffset($_GET['offset'])
->setType($pages[0])
->setPriority(true);
->setUserGuid($user->getGUID());
if (isset($_GET['rating']) && $pages[0] == 'newsfeed') {
if (isset($_GET['rating']) && $pages[0] == Boost\Network\Boost::TYPE_NEWSFEED) {
$cacher = Core\Data\cache\factory::build('Redis');
$offset = $cacher->get(Core\Session::getLoggedinUser()->guid . ':boost-offset:newsfeed');
$iterator->setOffset($offset);
}
switch ($pages[0]) {
case 'content':
//$iterator->setOffset('');
$iterator->setIncrement(true);
case Boost\Network\Boost::TYPE_CONTENT:
/** @var $entity Entity */
foreach ($iterator as $guid => $entity) {
$response['boosts'][] = array_merge($entity->export(), [
'boosted_guid' => (string) $guid,
......@@ -88,7 +81,7 @@ class fetch implements Interfaces\Api
if (!$response['boosts']) {
$result = Di::_()->get('Trending\Repository')->getList([
'type' => 'images',
'rating' => isset($rating) ? (int) $rating : 1,
'rating' => isset($rating) ? (int) $rating : Boost\Network\Boost::RATING_SAFE,
'limit' => $limit,
]);
......@@ -98,7 +91,7 @@ class fetch implements Interfaces\Api
}
}
break;
case 'newsfeed':
case Boost\Network\Boost::TYPE_NEWSFEED:
foreach ($iterator as $guid => $entity) {
$response['boosts'][] = array_merge($entity->export(), [
'boosted' => true,
......@@ -110,100 +103,24 @@ class fetch implements Interfaces\Api
if (isset($_GET['rating']) && $pages[0] == 'newsfeed') {
$cacher->set(Core\Session::getLoggedinUser()->guid . ':boost-offset:newsfeed', $iterator->getOffset(), (3600 / 2));
}
if (!$iterator->list && false) {
$cacher = Core\Data\cache\factory::build('apcu');
$offset = (int) $cacher->get(Core\Session::getLoggedinUser()->guid . ":newsfeed-fallover-boost-offset") ?: 0;
$posts = $this->getSuggestedPosts([
'offset' => $offset,
'limit' => $limit,
'rating' => $rating,
]);
foreach ($posts as $entity) {
$entity->boosted = true;
$response['boosts'][] = array_merge($entity->export(), [ 'boosted' => true ]);
}
if (!$response['boosts'] || count($response['boosts']) < 5) {
$cacher->destroy(Core\Session::getLoggedinUser()->guid . ":newsfeed-fallover-boost-offset");
} else {
$cacher->set(Core\Session::getLoggedinUser()->guid . ":newsfeed-fallover-boost-offset", ((int) $offset) + count($posts));
}
}
break;
}
return Factory::response($response);
Factory::response($response);
}
/**
*/
public function post($pages)
{
/* Not Implemented */
}
/**
* @param array $pages
*/
public function put($pages)
{
$expire = Core\Di\Di::_()->get('Boost\Network\Expire');
$metrics = Core\Di\Di::_()->get('Boost\Network\Metrics');
$boost = Core\Boost\Factory::build($pages[0])->getBoostEntity($pages[1]);
if (!$boost) {
return Factory::response([
'status' => 'error',
'message' => 'Boost not found'
]);
}
$count = $metrics->incrementViews($boost);
if ($count > $boost->getImpressions()) {
$expire->setBoost($boost);
$expire->expire();
}
Counters::increment($boost->getEntity()->guid, "impression");
Counters::increment($boost->getEntity()->owner_guid, "impression");
return Factory::response([]);
/* Not Implemented */
}
/**
*/
public function delete($pages)
{
}
private function getSuggestedPosts($opts = [])
{
$opts = array_merge([
'offset' => 0,
'limit' => 12,
'rating' => 1,
], $opts);
/** @var Core\Feeds\Suggested\Manager $repo */
$repo = Di::_()->get('Feeds\Suggested\Manager');
$opts = [
'user_guid' => Core\Session::getLoggedInUserGuid(),
'rating' => $opts['rating'],
'limit' => $opts['limit'],
'offset' => $opts['offset'],
'type' => 'newsfeed',
'all' => true,
];
$result = $repo->getFeed($opts);
// Remove all unlisted content if it appears
$result = array_values(array_filter($result, function($entity) {
return $entity->getAccessId() != 0;
}));
return $result;
/* Not Implemented */
}
}
......@@ -182,24 +182,18 @@ class newsfeed implements Interfaces\Api
/** @var Core\Boost\Network\Iterator $iterator */
$iterator = Core\Di\Di::_()->get('Boost\Network\Iterator');
$iterator->setPriority(!get_input('offset', ''))
->setType('newsfeed')
$iterator->setType('newsfeed')
->setLimit($limit)
->setOffset($offset)
//->setRating(0)
->setQuality(0)
->setIncrement(false);
->setUserGuid(Core\Session::getLoggedinUserGuid())
->setRating((int) Core\Session::getLoggedinUser()->getBoostRating());
foreach ($iterator as $guid => $boost) {
$boost->boosted = true;
$boost->boosted_guid = (string) $guid;
array_unshift($activity, $boost);
//if (get_input('offset')) {
//bug: sometimes views weren't being calculated on scroll down
//Counters::increment($boost->guid, "impression");
//Counters::increment($boost->owner_guid, "impression");
//}
}
$cacher->set(Core\Session::getLoggedinUser()->guid . ':boost-offset:newsfeed', $iterator->getOffset(), (3600 / 2));
} catch (\Exception $e) {
......@@ -484,7 +478,7 @@ class newsfeed implements Interfaces\Api
}
if (isset($_POST['nsfw'])) {
$activity->setNsfw($_POST['nsfw']);
$activity->setNsfw($_POST['nsfw']);
}
$user = Core\Session::getLoggedInUser();
......@@ -513,7 +507,7 @@ class newsfeed implements Interfaces\Api
$activity->indexes = ["activity:$activity->owner_guid:edits"]; //don't re-index on edit
(new Core\Translation\Storage())->purge($activity->guid);
$save->setEntity($activity)
->save();
......
<?php
namespace Minds\Controllers\api\v2\analytics;
use Minds\Api\Factory;
use Minds\Common\Urn;
use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Entities;
use Minds\Helpers\Counters;
use Minds\Interfaces;
use Minds\Core\Boost;
class views implements Interfaces\Api
{
......@@ -19,31 +18,81 @@ class views implements Interfaces\Api
return Factory::response([]);
}
/**
* @param array $pages
* @return void
* @throws \Exception
*/
public function post($pages)
{
$viewsManager = new Core\Analytics\Views\Manager();
/** @var Core\Boost\Campaigns\Manager $campaignsManager */
$campaignsManager = Di::_()->get(Boost\Campaigns\Manager::getDiAlias());
/** @var Core\Boost\Campaigns\Metrics $campaignsMetricsManager */
$campaignsMetricsManager = Di::_()->get(Boost\Campaigns\Metrics::getDiAlias());
switch ($pages[0]) {
case 'boost':
$expire = Di::_()->get('Boost\Network\Expire');
$metrics = Di::_()->get('Boost\Network\Metrics');
$manager = Di::_()->get('Boost\Network\Manager');
$urn = new Urn(
is_numeric($pages[1]) ?
"urn:boost:newsfeed:{$pages[1]}" :
$pages[1]
);
if ($urn->getNid() === 'campaign') {
// Boost Campaigns
try {
$campaign = $campaignsManager->getCampaignByUrn((string)$urn);
$campaignsMetricsManager
->setCampaign($campaign)
->increment();
$urn = "urn:boost:newsfeed:{$pages[1]}";
$campaignsManager
->onImpression($campaign);
// NOTE: Campaigns have a _single_ entity, for now. Refactor this when we support multiple
// Ideally, we should use a composite URN, like: urn:campaign-entity:100000321:(urn:activity:100000500)
foreach ($campaign->getEntityUrns() as $entityUrn) {
$viewsManager->record(
(new Core\Analytics\Views\View())
->setEntityUrn($entityUrn)
->setClientMeta($_POST['client_meta'] ?? [])
);
}
} catch (\Exception $e) {
Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
return;
}
Factory::response([]);
return;
}
$urn = (string) $urn;
$metrics = new Boost\Network\Metrics();
$manager = new Boost\Network\Manager();
$boost = $manager->get($urn, [ 'hydrate' => true ]);
if (!$boost) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'Could not find boost'
]);
return;
}
$count = $metrics->incrementViews($boost);
if ($count > $boost->getImpressions()) {
$expire->setBoost($boost);
$expire->expire();
$manager->expire($boost);
}
Counters::increment($boost->getEntity()->guid, "impression");
......@@ -61,20 +110,22 @@ class views implements Interfaces\Api
error_log($e);
}
return Factory::response([
Factory::response([
'status' => 'success',
'impressions' => $boost->getImpressions(),
'impressions_met' => $count,
]);
return;
break;
case 'activity':
$activity = new Entities\Activity($pages[1]);
if (!$activity->guid) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'Could not find activity post'
]);
return;
}
try {
......@@ -116,17 +167,16 @@ class views implements Interfaces\Api
break;
}
return Factory::response([]);
Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
Factory::response([]);
}
}
......@@ -8,6 +8,7 @@
namespace Minds\Controllers\api\v2\blockchain;
use Minds\Core\Blockchain\Purchase\Manager;
use Minds\Core\Blockchain\Purchase\Sums;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Util\BigNumber;
......@@ -27,7 +28,9 @@ class purchase implements Interfaces\Api
{
$response = [];
/** @var Sums $sums */
$sums = Di::_()->get('Blockchain\Purchase\Sums');
/** @var Manager $manager */
$manager = Di::_()->get('Blockchain\Purchase\Manager');
$response['cap'] = $manager->getAutoIssueCap();
......
<?php
/**
* Minds Boost Api endpoint
*
* @version 2
* @author Mark Harding
*
*/
namespace Minds\Controllers\api\v2;
......@@ -14,8 +7,6 @@ use Minds\Core;
use Minds\Core\Di\Di;
use Minds\Core\Util\BigNumber;
use Minds\Entities;
use Minds\Helpers;
use Minds\Helpers\Counters;
use Minds\Interfaces;
use Minds\Core\Boost\Network;
......@@ -26,31 +17,6 @@ class boost implements Interfaces\Api
/**
* Return impressions for a request
* @param array $pages
*
* @SWG\GET(
* tags={"boost"},
* summary="Returns information regarding a boost, or the current boost rates",
* path="/v2/boost/{guid}",
* @SWG\Parameter(
* name="guid",
* in="path",
* description="the guid",
* required=true,
* type="string"
* ),
* @SWG\Response(name="200", description="Array")
* )
* @SWG\GET(
* tags={"boost"},
* summary="Returns the current boost rates",
* path="/v2/boost/rate",
* @SWG\Response(name="200", description="Array"),
* security={
* {
* "minds_oauth2": {}
* }
* }
* )
*/
public function get($pages)
{
......@@ -59,28 +25,23 @@ class boost implements Interfaces\Api
$response = [];
$limit = isset($_GET['limit']) && $_GET['limit'] ? (int)$_GET['limit'] : 12;
$offset = isset($_GET['offset']) && $_GET['offset'] ? $_GET['offset'] : '';
$config = (array) Core\Di\Di::_()->get('Config')->get('boost');
switch ($pages[0]) {
case is_numeric($pages[0]):
$review = Di::_()->get('Boost\Peer\Review');
$review = new Core\Boost\Peer\Review();
$boost = $review->getBoostEntity($pages[0]);
if ($boost->getState() != 'created') {
return Factory::response(['status' => 'error', 'message' => 'entity not in boost queue']);
Factory::response(['status' => 'error', 'message' => 'entity not in boost queue']);
return;
}
$response['entity'] = $boost->getEntity()->export();
$response['bid'] = $boost->getBid();
break;
case "rates":
$response['hasPaymentMethod'] = false;
$response['rate'] = $this->rate;
$config = array_merge([
'network' => [
'min' => 200,
'max' => 5000,
],
], (array)Core\Di\Di::_()->get('Config')->get('boost'));
$response['cap'] = $config['network']['max'];
$response['min'] = $config['network']['min'];
$response['priority'] = $this->getQueuePriorityRate();
......@@ -88,12 +49,13 @@ class boost implements Interfaces\Api
$response['minUsd'] = $this->getMinUSDCharge();
$response['tokens'] = $this->getTokensRate();
break;
case "p2p":
/** @var Core\Boost\Peer\Review $review */
$review = Di::_()->get('Boost\Peer\Review');
$review = new Core\Boost\Peer\Review();
$review->setType(Core\Session::getLoggedInUser()->guid);
$boosts = $review->getReviewQueue($limit, $offset);
$boost_entities = [];
/** @var $boost Core\Boost\Network\Boost */
foreach ($boosts['data'] as $i => $boost) {
if ($boost->getState() != 'created') {
unset($boosts[$i]);
......@@ -108,10 +70,10 @@ class boost implements Interfaces\Api
$response['boosts'] = factory::exportable($boost_entities, array('points'));
$response['load-next'] = $boosts['next'];
break;
case "newsfeed":
case "content":
/** @var Core\Boost\Network\Review $review */
$review = Di::_()->get('Boost\Network\Review');
$review = new Core\Boost\Network\Review();
$review->setType($pages[0]);
$boosts = $review->getOutbox(Core\Session::getLoggedinUser()->guid, $limit, $offset);
$response['boosts'] = Factory::exportable($boosts['data']);
......@@ -125,12 +87,13 @@ class boost implements Interfaces\Api
}
}
return Factory::response($response);
Factory::response($response);
}
/**
* Boost an entity
* @param array $pages
* @return void
*
* API:: /v2/boost/:type/:guid
*/
......@@ -139,48 +102,48 @@ class boost implements Interfaces\Api
Factory::isLoggedIn();
if (!isset($pages[0])) {
return Factory::response(array('status' => 'error', 'message' => ':type must be passed in uri'));
Factory::response(['status' => 'error', 'message' => ':type must be passed in uri']);
return;
}
if (!isset($pages[1])) {
return Factory::response(array('status' => 'error', 'message' => ':guid must be passed in uri'));
Factory::response(['status' => 'error', 'message' => ':guid must be passed in uri']);
return;
}
$impressions = (int) $_POST['impressions'];
if (!isset($impressions)) {
return Factory::response(array('status' => 'error', 'message' => 'impressions must be sent in post body'));
Factory::response(['status' => 'error', 'message' => 'impressions must be sent in post body']);
return;
}
if ($impressions <= 0) {
return Factory::response(array('status' => 'error', 'message' => 'impressions must be a positive whole number'));
Factory::response(['status' => 'error', 'message' => 'impressions must be a positive whole number']);
return;
}
$paymentMethod = isset($_POST['paymentMethod']) ? $_POST['paymentMethod'] : [];
$config = array_merge([
'network' => [
'min' => 100,
'max' => 5000,
],
], (array)Core\Di\Di::_()->get('Config')->get('boost'));
$config = (array) Core\Di\Di::_()->get('Config')->get('boost');
if ($paymentMethod['method'] === 'onchain') {
$config['network']['max'] *= 2;
}
if ($impressions < $config['network']['min'] || $impressions > $config['network']['max']) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => "You must boost between {$config['network']['min']} and {$config['network']['max']} impressions"
]);
return;
}
$response = [];
$entity = Entities\Factory::build($pages[1]);
if (!$entity) {
return Factory::response(['status' => 'error', 'message' => 'entity not found']);
Factory::response(['status' => 'error', 'message' => 'entity not found']);
return;
}
if ($pages[0] == "object" || $pages[0] == "user" || $pages[0] == "suggested" || $pages[0] == 'group') {
......@@ -213,11 +176,12 @@ class boost implements Interfaces\Api
}
if (!in_array($bidType, [ 'usd', 'tokens' ])) {
return Factory::response([
Factory::response([
'status' => 'error',
'stage' => 'initial',
'message' => 'Unknown currency'
]);
return;
}
// Amount normalizing
......@@ -227,10 +191,12 @@ class boost implements Interfaces\Api
$amount = round($amount / $this->getUSDRate(), 2) * 100;
if (($amount / 100) < $this->getMinUSDCharge()) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'You must spend at least $' . $this->getMinUSDCharge()
]);
return;
}
break;
......@@ -248,23 +214,25 @@ class boost implements Interfaces\Api
foreach ($categories as $category) {
if (!in_array($category, $validCategories)) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'Invalid category ID: ' . $category
]);
return;
}
}
// Validate entity
$boostHandler = Core\Boost\Factory::getClassHandler(ucfirst($pages[0]));
$isEntityValid = call_user_func([$boostHandler, 'validateEntity'], $entity);
$boostHandler = Core\Boost\Handler\Factory::get($pages[0]);
$isEntityValid = $boostHandler->validateEntity($entity);
if (!$isEntityValid) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'Entity cannot be boosted'
]);
return;
}
// Generate Boost entity
......@@ -289,27 +257,46 @@ class boost implements Interfaces\Api
->setType(lcfirst($pages[0]))
->setPriority(false);
if ($manager->checkExisting($boost)) {
Factory::response([
'status' => 'error',
'message' => "There's already an ongoing boost for this entity"
]);
return;
}
if ($manager->isBoostLimitExceededBy($boost)) {
$maxDaily = Di::_()->get('Config')->get('max_daily_boost_views') / 1000;
Factory::response([
'status' => 'error',
'message' => "Exceeded maximum of ".$maxDaily." offchain tokens per 24 hours."
]);
return;
}
// Pre-set GUID
if ($bidType == 'tokens' && isset($_POST['guid'])) {
$guid = $_POST['guid'];
if (!is_numeric($guid) || $guid < 1) {
return Factory::response([
Factory::response([
'status' => 'error',
'stage' => 'transaction',
'message' => 'Provided GUID is invalid'
]);
return;
}
$existingBoost = $manager->get("urn:boost:{$boost->getType()}:{$guid}");
if ($existingBoost) {
return Factory::response([
Factory::response([
'status' => 'error',
'stage' => 'transaction',
'message' => 'Provided GUID already exists'
]);
return;
}
$boost->setGuid($guid);
......@@ -320,11 +307,12 @@ class boost implements Interfaces\Api
->generate();
if ($checksum !== $calculatedChecksum) {
return Factory::response([
Factory::response([
'status' => 'error',
'stage' => 'transaction',
'message' => 'Checksum does not match. Expected: ' . $calculatedChecksum
]);
return;
}
$boost->setChecksum($checksum);
}
......@@ -352,13 +340,14 @@ class boost implements Interfaces\Api
$response['message'] = "boost handler not found";
}
} catch (\Exception $e) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => $e->getMessage()
]);
return;
}
return Factory::response($response);
Factory::response($response);
}
/**
......@@ -367,7 +356,7 @@ class boost implements Interfaces\Api
*/
public function put($pages)
{
return Factory::response([]);
Factory::response([]);
}
/**
......@@ -385,42 +374,47 @@ class boost implements Interfaces\Api
$action = $pages[2];
if (!$guid) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'We couldn\'t find that boost'
]);
return;
}
if (!$action) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => "You must provide an action: revoke"
]);
return;
}
/** @var Core\Boost\Network\Review|Core\Boost\Peer\Review $review */
$review = $type == 'peer' ? Di::_()->get('Boost\Peer\Review') : Di::_()->get('Boost\Network\Review');
$review = $type == 'peer' ? new Core\Boost\Peer\Review() : new Core\Boost\Network\Review();
$review->setType($type);
$boost = $review->getBoostEntity($guid);
if (!$boost) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'Boost not found'
]);
return;
}
if ($boost->getOwner()->guid != Core\Session::getLoggedInUserGuid()) {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'You cannot revoke that boost'
]);
return;
}
if ($boost->getState() != 'created') {
return Factory::response([
Factory::response([
'status' => 'error',
'message' => 'This boost is in the ' . $boost->getState() . ' state and cannot be refunded'
]);
return;
}
if ($action == 'revoke') {
......@@ -439,7 +433,8 @@ class boost implements Interfaces\Api
}
}
return Factory::response($response);
Factory::response($response);
return;
}
protected function getQueuePriorityRate()
......
<?php
/**
* Boost Campaigns
*
* @version 2
* @author emi
*
*/
namespace Minds\Controllers\api\v2\boost;
use Exception;
use Minds\Common\Urn;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\Manager;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Interfaces;
use Minds\Api\Factory;
class campaigns implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
$limit = $_GET['limit'] ?? 12;
$offset = $_GET['offset'] ?? '';
$urn = $pages[0] ?? null;
if ($limit > 50 || $limit < 0) {
$limit = 12;
}
$guid = '';
if ($urn) {
$limit = 1;
$offset = '';
$urn = new Urn($urn);
$guid = (string) $urn->getNss();
}
/** @var Manager $manager */
$manager = Di::_()->get('Boost\Campaigns\Manager');
$manager->setActor(Session::getLoggedInUser());
$response = $manager->getCampaigns([
'owner_guid' => Session::getLoggedinUserGuid(),
'limit' => $limit,
'offset' => $offset,
'guid' => $guid,
])->filter(function (Campaign $campaign) {
return $campaign->getOwnerGuid() == Session::getLoggedinUserGuid();
});
Factory::response([
'campaigns' => $response,
'load-next' => $response->getPagingToken(),
]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages)
{
$isEditing = false;
$urn = null;
if ($pages[0]) {
$isEditing = true;
$urn = $pages[0];
}
$campaign = new Campaign();
if (!$isEditing) {
$campaign
->setType($_POST['type'] ?? '')
->setEntityUrns($_POST['entity_urns'] ?? [])
->setBudgetType($_POST['budget_type'] ?? '')
->setChecksum($_POST['checksum'] ?? '');
} else {
$campaign
->setUrn($urn);
}
$campaign
->setName(trim($_POST['name'] ?? ''))
->setHashtags($_POST['hashtags'] ?? [])
->setStart((int) ($_POST['start'] ?? 0))
->setEnd((int) ($_POST['end'] ?? 0))
->setBudget((float) ($_POST['budget'] ?? 0));
/** @var Manager $manager */
$manager = Di::_()->get('Boost\Campaigns\Manager');
$manager->setActor(Session::getLoggedInUser());
try {
if (!$isEditing) {
$campaign = $manager->createCampaign($campaign, $_POST['payment'] ?? null);
} else {
$campaign = $manager->updateCampaign($campaign, $_POST['payment'] ?? null);
}
Factory::response([
'campaign' => $campaign,
]);
} catch (\Exception $e) {
Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
/**
* Equivalent to HTTP PUT method
* @param array $pages
*/
public function put($pages)
{
Factory::response([]);
}
/**
* Equivalent to HTTP DELETE method
* @param array $pages
*/
public function delete($pages)
{
$urn = $pages[0] ?? null;
if (!$urn[0]) {
Factory::response([
'status' => 'error',
'message' => 'Missing URN',
]);
return;
}
$campaign = new Campaign();
$campaign
->setUrn($urn);
/** @var Manager $manager */
$manager = Di::_()->get('Boost\Campaigns\Manager');
$manager->setActor(Session::getLoggedInUser());
try {
$campaign = $manager->cancel($campaign);
Factory::response([
'campaign' => $campaign,
]);
} catch (\Exception $e) {
Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
}
}
<?php
namespace Minds\Controllers\api\v2\boost\campaigns;
use Minds\Api\Api;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
class analytics extends Api
{
public function get($pages): void
{
switch ($pages[0]) {
case 'rate':
// Get current boost rate
$avgRate = (new BoostViewsDaily())->lastSevenDays()->getAvg();
$this->send(['rate' => $avgRate]);
break;
case 'days':
$days = (new BoostViewsDaily())->lastSevenDays()->getAll();
$this->send(['days' => $days]);
break;
default:
$this->sendBadRequest();
}
}
}
<?php
namespace Minds\Controllers\api\v2\boost\campaigns;
use Minds\Api\Api;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\Stats;
use Minds\Core\Di\Di;
class preview extends Api
{
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
*/
public function post($pages): void
{
$campaign = (new Campaign())
->setType($_POST['type'] ?? '')
->setEntityUrns($_POST['entity_urns'] ?? [])
->setBudgetType($_POST['budget_type'] ?? '')
->setHashtags($_POST['hashtags'] ?? [])
->setStart((int)($_POST['start'] ?? 0))
->setEnd((int)($_POST['end'] ?? 0))
->setBudget((float)($_POST['budget'] ?? 0))
->setImpressions($_POST['impressions']);
/** @var Stats $statsManager */
$statsManager = Di::_()->get('Boost\Campaigns\Stats');
$this->send([
'preview' => $statsManager->setCampaign($campaign)->getAll()
]);
}
}
<?php
namespace Minds\Controllers\api\v2\boost;
use Minds\Api\Exportable;
use Minds\Core;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
class feed implements Interfaces\Api
{
/** @var User */
protected $currentUser;
/** @var array */
protected $boosts = [];
protected $next;
protected $type;
protected $limit;
protected $offset;
protected $rating;
protected $platform;
protected $quality = 0;
protected $isBoostFeed;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
public function get($pages)
{
Factory::isLoggedIn();
$this->currentUser = Core\Session::getLoggedinUser();
if (!$this->parseAndValidateParams() || !$this->validBoostUser()) {
$this->sendResponse();
return;
}
$this->type = $pages[0] ?? 'newsfeed';
if ($this->isBoostFeed) {
$this->offset = $_GET['from_timestamp'] ?? 0;
}
switch ($this->type) {
case 'newsfeed':
$this->getBoosts();
break;
default:
$this->sendError('Unsupported boost type');
return;
}
$this->sendResponse();
}
protected function parseAndValidateParams(): bool
{
$this->limit = abs(intval($_GET['limit'] ?? 2));
$this->offset = $_GET['offset'] ?? 0;
$this->rating = intval($_GET['rating'] ?? $this->currentUser->getBoostRating());
$this->platform = $_GET['platform'] ?? 'other';
$this->isBoostFeed = $_GET['boostfeed'] ?? false;
if ($this->limit === 0) {
return false;
}
if ($this->limit > 500) {
$this->limit = 500;
}
return true;
}
protected function validBoostUser(): bool
{
return !($this->currentUser->disabled_boost && $this->currentUser->isPlus());
}
protected function sendResponse(): void
{
$boosts = empty($this->boosts) ? [] : Exportable::_($this->boosts);
Factory::response([
'entities' => $boosts,
'load-next' => $this->next,
]);
}
protected function sendError(string $message): void
{
Factory::response([
'status' => 'error',
'message' => $message
]);
}
protected function getBoosts()
{
$feed = new Core\Boost\Feeds\Boost($this->currentUser);
$this->boosts = $feed->setLimit($this->limit)
->setOffset($this->offset)
->setRating($this->rating)
->setPlatform($this->platform)
->setQuality($this->quality)
->get();
$this->next = $feed->getOffset();
}
/**
* 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([]);
}
}
<?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 fetch 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;
$sync = (bool) ($_GET['sync'] ?? true);
if ($limit === 0) {
return Factory::response([
'boosts' => [],
]);
} elseif ($sync && $limit > 500) {
$limit = 500;
} elseif (!$sync && $limit > 50) {
$limit = 50;
}
// 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(!$sync);
if ($sync) {
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;
}
} else {
$boosts = iterator_to_array($iterator, false);
}
$next = $iterator->getOffset();
break;
case 'content':
// TODO: Content boosts
default:
return Factory::response([
'status' => 'error',
'message' => 'Unsupported boost type'
]);
}
return Factory::response([
'boosts' => 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([]);
}
}
<?php
/**
* Boost & Boost Campaigns fetch
* @author: eiennohi.
*/
namespace Minds\Controllers\api\v2\boost\fetch;
use Minds\Api\Exportable;
use Minds\Api\Factory;
use Minds\Common\Urn;
use Minds\Core;
use Minds\Core\Boost;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Network;
use Minds\Core\Di\Di;
use Minds\Core\Feeds\FeedSyncEntity;
use Minds\Core\Session;
use Minds\Entities;
use Minds\Interfaces;
class campaigns implements Interfaces\Api
{
/**
* @param array $pages
* @return mixed|void|null
*/
public function get($pages)
{
Factory::isLoggedIn();
/** @var Entities\User $currentUser */
$currentUser = Session::getLoggedinUser();
if ($currentUser->disabled_boost && $currentUser->isPlus()) {
Factory::response([
'entities' => [],
]);
return;
}
// Parse parameters
$type = $pages[0] ?? 'newsfeed';
if (!in_array($type, ['newsfeed', 'content'], true)) {
Factory::response([
'status' => 'error',
'message' => 'Unsupported boost type',
]);
return;
}
$limit = abs(intval($_GET['limit'] ?? 2));
$rating = intval($_GET['rating'] ?? $currentUser->getBoostRating());
$platform = $_GET['platform'] ?? 'other';
$quality = 0;
if ($limit === 0) {
Factory::response([
'boosts' => [],
]);
return;
} elseif ($limit > 500) {
$limit = 500;
}
// Options specific to newly created users (<=1 hour) and iOS users
if ($platform === 'ios') {
$rating = Network\Boost::RATING_SAFE;
$quality = 90;
} elseif (time() - $currentUser->getTimeCreated() <= 3600) {
$rating = Network\Boost::RATING_SAFE;
$quality = 75;
}
$userGuid = Core\Session::getLoggedinUser()->guid;
$cacher = Core\Data\cache\factory::build('Redis');
$cacheKey = "{$userGuid}:boost-offset-rotator:{$type}:{$quality}:{$rating}";
// TODO: ENABLE ME AGAIN!
// $offset = $cacher->get($cacheKey);
$offset = null;
if (!$offset) {
$offset = 0;
}
/** @var Boost\Campaigns\Manager $manager */
$manager = Di::_()->get(Boost\Campaigns\Manager::getDiAlias());
$data = [];
try {
$result = $manager->getCampaignsAndBoosts([
'limit' => $limit,
'from' => $offset,
'rating' => $rating,
'quality' => $quality,
'type' => $type,
]);
$offset = $result->getPagingToken();
foreach ($result as $entity) {
$feedSyncEntity = (new FeedSyncEntity())
->setGuid((string) $entity->getGuid())
->setOwnerGuid((string) $entity->getOwnerGuid())
->setTimestamp($entity->getCreatedTimestamp());
if ($entity instanceof Campaign) {
$feedSyncEntity->setUrn($entity->getUrn());
} elseif ($entity instanceof Network\Boost) {
$feedSyncEntity->setUrn(new Urn("urn:boost:{$entity->getType()}:{$entity->getGuid()}"));
}
$data[] = $feedSyncEntity;
}
if (isset($data[2])) { // Always offset to 3rd in list
$offset += 2;
}
$ttl = 1800; // 30 minutes
if (($data[0] / 1000) < strtotime('48 hours ago')) {
$ttl = 300; // 5 minutes;
}
$cacher->set($cacheKey, $offset, $ttl);
} catch (\Exception $e) {
error_log($e);
}
Factory::response([
'entities' => Exportable::_($data),
'load-next' => $offset ?: null,
]);
}
public function post($pages)
{
Factory::response([]);
}
public function put($pages)
{
Factory::response([]);
}
public function delete($pages)
{
Factory::response([]);
}
}
......@@ -54,13 +54,13 @@ class suggested implements Interfaces\Api
$offset = intval($_GET['offset']);
}
$rating = Core\Session::getLoggedinUser()->boost_rating ?: 1;
$rating = Core\Session::getLoggedinUser()->boost_rating ?: Core\Boost\Network\Boost::RATING_SAFE;
if (isset($_GET['rating'])) {
$rating = intval($_GET['rating']);
}
if ($type == 'user') {
$rating = 1;
$rating = Core\Boost\Network\Boost::RATING_SAFE;
}
$hashtag = null;
......
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Data\ElasticSearch\Client;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Core\Time;
class BoostViewsDaily
{
/** @var Client */
protected $es;
/** @var array */
protected $dailyViews = [];
/** @var int */
protected $totalViews = 0;
/** @var int */
protected $startDayMs;
/** @var int */
protected $endDayMs;
public function __construct(Client $esClient = null)
{
$this->es = $esClient ?: Di::_()->get('Database\ElasticSearch');
$this->lastSevenDays();
}
protected function clearData(): void
{
$this->dailyViews = [];
$this->totalViews = 0;
}
public function lastSevenDays(): self
{
return $this->dateRange(strtotime('yesterday -1 week'), strtotime('yesterday'));
}
public function dateRange(int $start, int $end): self
{
$this->clearData();
$this->startDayMs = Time::toInterval($start, Time::ONE_DAY) * 1000;
$this->endDayMs = Time::toInterval($end, Time::ONE_DAY) * 1000;
return $this;
}
protected function query(): void
{
if (!empty($this->dailyViews)) {
return;
}
$prepared = new Search();
$prepared->query($this->buildQuery());
$response = $this->es->request($prepared);
if (isset($response['aggregations']['boost_views_total'])) {
$this->totalViews = $response['aggregations']['boost_views_total']['value'];
}
if (isset($response['aggregations']['boost_views_daily']['buckets'])) {
foreach ($response['aggregations']['boost_views_daily']['buckets'] as $bucket) {
$this->dailyViews[$bucket['key']] = $bucket['boost_views']['value'];
}
}
}
protected function buildQuery(): array
{
$must = [
'range' => [
'@timestamp' => [
'gte' => $this->startDayMs,
'lte' => $this->endDayMs,
]
]
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'boost_views_total' => [
'sum' => [
'field' => 'views::boosted',
],
],
'boost_views_daily' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => '1d'
],
'aggs' => [
'boost_views' => [
'sum' => [
'field' => 'views::boosted'
]
]
]
]
]
]
];
return $query;
}
public function getAll(): array
{
$this->query();
return $this->dailyViews;
}
public function getTotal(): int
{
$this->query();
return $this->totalViews;
}
public function getMax(): int
{
$this->query();
return max($this->dailyViews);
}
public function getAvg(): float
{
$this->query();
return !empty($this->dailyViews) ? array_sum($this->dailyViews) / count($this->dailyViews) : 0;
}
}
......@@ -10,6 +10,7 @@ namespace Minds\Core\Blockchain;
use Minds\Core\Blockchain\Events\BlockchainEventInterface;
use Minds\Core\Blockchain\Events\BoostEvent;
use Minds\Core\Blockchain\Events\TokenEvent;
use Minds\Core\Blockchain\Events\TokenSaleEvent;
use Minds\Core\Blockchain\Events\WireEvent;
use Minds\Core\Blockchain\Events\WithdrawEvent;
......@@ -20,9 +21,10 @@ class Events
{
protected static $handlers = [
TokenSaleEvent::class,
TokenEvent::class,
WireEvent::class,
BoostEvent::class,
WithdrawEvent::class
WithdrawEvent::class,
];
public function register()
......
<?php
/**
* TokenEvent
* @author edgebal
*/
namespace Minds\Core\Blockchain\Events;
use Exception;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Blockchain\Util;
use Minds\Core\Boost\Campaigns\Manager as BoostCampaignsManager;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Util\BigNumber;
class TokenEvent implements BlockchainEventInterface
{
/** @var array */
public static $eventsMap = [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' => 'tokenTransfer',
'blockchain:fail' => 'tokenFail',
];
/** @var Config */
protected $config;
/** @var BoostCampaignsManager */
protected $boostCampaignsManager;
/**
* TokenEvent constructor.
* @param Config $config
* @param BoostCampaignsManager $boostCampaignsManager
*/
public function __construct(
$config = null,
$boostCampaignsManager = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->boostCampaignsManager = $boostCampaignsManager ?: Di::_()->get('Boost\Campaigns\Manager');
}
/**
* @return array
*/
public function getTopics()
{
return array_keys(static::$eventsMap);
}
/**
* @param $topic
* @param array $log
* @param Transaction $transaction
* @return void
* @throws Exception
*/
public function event($topic, array $log, $transaction)
{
$method = static::$eventsMap[$topic];
if ($log['address'] != $this->config->get('blockchain')['token_address']) {
throw new Exception('Event does not match address');
}
if (method_exists($this, $method)) {
$this->{$method}($log, $transaction);
} else {
throw new Exception('Method not found');
}
}
/**
* @param array $log
* @param Transaction $transaction
* @throws Exception
*/
public function tokenTransfer($log, $transaction)
{
list($amount) = Util::parseData($log['data'], [Util::NUMBER]);
list($destination) = Util::parseData($log['topics'][2], [Util::ADDRESS]);
$data = $transaction->getData();
if (!$destination) {
throw new Exception('Invalid transfer destination');
}
switch ($transaction->getContract()) {
case 'boost_campaign':
$wallet = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_address'] ?? null;
if ($destination != $wallet) {
throw new Exception('Invalid Boost Campaign wallet address');
}
$payment = new Payment();
$payment
->setOwnerGuid($data['payment']['owner_guid'])
->setCampaignGuid($data['payment']['campaign_guid'])
->setTx($data['payment']['tx'])
->setAmount(BigNumber::fromPlain(BigNumber::fromHex($amount), 18)->toDouble());
$this->boostCampaignsManager->onPaymentSuccess($payment);
break;
}
}
/**
* @param array $log
* @param Transaction $transaction
* @throws Exception
*/
public function tokenFail($log, $transaction)
{
list($amount) = Util::parseData($log['data'], [Util::NUMBER]);
list($destination) = Util::parseData($log['topics'][2], [Util::ADDRESS]);
$data = $transaction->getData();
if (!$destination) {
throw new Exception('Invalid transfer destination');
}
switch ($transaction->getContract()) {
case 'boost_campaign':
$wallet = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_address'] ?? null;
if ($destination != $wallet) {
throw new Exception('Invalid Boost Campaign wallet address');
}
$payment = new Payment();
$payment
->setOwnerGuid($data['payment']['owner_guid'])
->setCampaignGuid($data['payment']['campaign_guid'])
->setTx($data['payment']['tx'])
->setAmount(BigNumber::fromPlain(BigNumber::fromHex($amount), 18)->toDouble());
$this->boostCampaignsManager->onPaymentFailed($payment);
break;
}
}
}