...
 
Commits (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();
......
This diff is collapsed.
<?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;
}
}
}
......@@ -65,6 +65,7 @@ class Manager
'client_network' => $blockchainConfig['client_network'],
'wallet_address' => $blockchainConfig['wallet_address'],
'boost_wallet_address' => $blockchainConfig['contracts']['boost']['wallet_address'],
'boost_campaigns_wallet_address' => $blockchainConfig['contracts']['boost_campaigns']['wallet_address'],
'token_distribution_event_address' => $blockchainConfig['contracts']['token_sale_event']['contract_address'],
'rate' => $blockchainConfig['eth_rate'],
'plus_address' => $blockchainConfig['contracts']['wire']['plus_address'],
......
......@@ -103,7 +103,7 @@ class Manager
public function run()
{
$result = $this->repo->getList([
'user_guid' => $this->user_guid,
'user_guid' => $this->user_guid,
'timestamp' => [
'eq' => $this->timestamp,
],
......@@ -154,7 +154,7 @@ class Manager
(new $topicHandlerClass())->event($topic, $log, $transaction);
error_log("Tx[{$this->tx}][{$topicHandlerClass}] {$topic}... OK!");
} catch (\Exception $e) {
error_log("Tx[{$this->tx}][{$topicHandlerClass}] {$topic} threw " . get_class($e) . ": {$e->getMessage()}");
error_log("Tx[{$this->tx}][{$topicHandlerClass}] {$topic} threw {$e}");
}
}
}
......@@ -169,17 +169,30 @@ class Manager
/**
* Adds a transaction to the queue
* @param $transaction
* @param bool $handleEvents
*/
public function add($transaction)
public function add($transaction, $handleEvents = true)
{
$this->repo->add($transaction);
$this->queue->setQueue("BlockchainTransactions")
->send([
'user_guid' => $transaction->getUserGuid(),
'timestamp' => $transaction->getTimestamp(),
'wallet_address' => $transaction->getWalletAddress(),
'tx' => $transaction->getTx(),
]);
if ($handleEvents) {
$this->queue->setQueue("BlockchainTransactions")
->send([
'user_guid' => $transaction->getUserGuid(),
'timestamp' => $transaction->getTimestamp(),
'wallet_address' => $transaction->getWalletAddress(),
'tx' => $transaction->getTx(),
]);
}
}
/**
* @param $tx
* @return int
* @throws \Exception
*/
public function exists($tx)
{
return $this->repo->exists($tx);
}
}
......@@ -8,6 +8,7 @@ use Cassandra;
use Cassandra\Varint;
use Cassandra\Decimal;
use Cassandra\Timestamp;
use Exception;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
......@@ -44,7 +45,7 @@ class Repository
VALUES (?,?,?,?,?,?,?,?,?)";
foreach ($transactions as $transaction) {
$requests[] = [
'string' => $template,
'string' => $template,
'values' => [
new Varint($transaction->getUserGuid()),
$transaction->getWalletAddress(),
......@@ -155,7 +156,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch (Exception $e) {
error_log($e->getMessage());
return [];
}
......@@ -171,7 +172,7 @@ class Repository
->setUserGuid((int) $row['user_guid'])
->setWalletAddress($row['wallet_address'])
->setTimestamp((int) $row['timestamp']->time())
->setContract($row['contract'])
->setContract($row['contract'])
->setAmount((string) BigNumber::_($row['amount']))
->setCompleted((bool) $row['completed'])
->setFailed((bool) $row['failed'])
......@@ -197,7 +198,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch (Exception $e) {
error_log($e->getMessage());
return [];
}
......@@ -214,14 +215,30 @@ class Repository
->setUserGuid((int) $row['user_guid'])
->setWalletAddress($row['wallet_address'])
->setTimestamp((int) $row['timestamp']->time())
->setContract($row['contract'])
->setContract($row['contract'])
->setAmount((string) BigNumber::_($row['amount']))
->setCompleted((bool) $row['completed'])
->setFailed((bool) $row['failed'])
->setData(json_decode($row['data'], true));
return $transaction;
}
public function exists($tx)
{
$cql = "SELECT count(*) AS total FROM blockchain_transactions_mainnet_by_tx WHERE tx = ?";
$values = [ (string) $tx ];
$query = new Custom();
$query->query($cql, $values);
$result = $this->db->request($query);
if (!isset($result[0]['total'])) {
throw new Exception('Error checking transaction existence');
}
return (int) $result[0]['total'];
}
public function update($transaction, array $dirty = [])
......@@ -260,7 +277,7 @@ class Repository
try {
$success = $this->db->request($query);
} catch (\Exception $e) {
} catch (Exception $e) {
return false;
}
......@@ -276,7 +293,7 @@ class Repository
try{
$rows = $this->db->request($query);
} catch(\Exception $e) {
} catch (Exception $e) {
error_log($e->getMessage());
return [];
}
......
......@@ -2,19 +2,17 @@
namespace Minds\Core\Boost;
use Minds\Core\Boost\Network;
use Minds\Core\Data;
use Minds\Core\Data\Client;
use Minds\Core\Di\Provider;
use Minds\Core\Di;
/**
* Boost Providers
*/
class BoostProvider extends Provider
class BoostProvider extends Di\Provider
{
/**
* Registers providers onto DI
* @return void
* @throws Di\ImmutableException
*/
public function register()
{
......@@ -22,43 +20,44 @@ class BoostProvider extends Provider
return new Repository();
}, ['useFactory' => true]);
$this->di->bind('Boost\Network', function ($di) {
return new Network([], Client::build('MongoDB'), new Data\Call('entities_by_time'));
}, ['useFactory' => true]);
$this->di->bind('Boost\Network\Manager', function ($di) {
return new Network\Manager;
return new Network\Manager();
}, ['useFactory' => false]);
$this->di->bind('Boost\Network\Iterator', function ($di) {
return new Network\Iterator();
}, ['useFactory' => false]);
$this->di->bind('Boost\Network\Metrics', function ($di) {
return new Network\Metrics(Client::build('MongoDB'));
}, ['useFactory' => false]);
$this->di->bind('Boost\Network\Review', function ($di) {
return new Network\Review();
}, ['useFactory' => false]);
$this->di->bind('Boost\Network\Expire', function ($di) {
return new Network\Expire();
}, ['useFactory' => false]);
$this->di->bind('Boost\Newsfeed', function ($di) {
return new Newsfeed([], Client::build('MongoDB'), new Data\Call('entities_by_time'));
}, ['useFactory' => true]);
$this->di->bind('Boost\Content', function ($di) {
return new Content([], Client::build('MongoDB'), new Data\Call('entities_by_time'));
}, ['useFactory' => true]);
$this->di->bind('Boost\Peer', function ($di) {
return new Peer();
}, ['useFactory' => true]);
$this->di->bind('Boost\Peer\Metrics', function ($di) {
return new Peer\Metrics(Client::build('MongoDB'));
}, ['useFactory' => false]);
$this->di->bind('Boost\Peer\Review', function ($di) {
return new Peer\Review();
}, ['useFactory' => false]);
$this->di->bind('Boost\Payment', function ($di) {
return new Payment();
}, ['useFactory' => true]);
}
$this->di->bind(Campaigns\Dispatcher::getDiAlias(), function ($di) {
return new Campaigns\Dispatcher();
}, ['useFactory' => true]);
$this->di->bind(Campaigns\Metrics::getDiAlias(), function ($di) {
return new Campaigns\Metrics();
}, ['useFactory' => true]);
$this->di->bind(Campaigns\Manager::getDiAlias(), function ($di) {
return new Campaigns\Manager();
}, ['useFactory' => true]);
$this->di->bind(Campaigns\Repository::getDiAlias(), function ($di) {
return new Campaigns\Repository();
}, ['useFactory' => true]);
$this->di->bind(Campaigns\Stats::getDiAlias(), function ($di) {
return new Campaigns\Stats();
}, ['useFactory' => true]);
}
}
<?php
namespace Minds\Core\Boost\Campaigns;
use Exception;
use JsonSerializable;
use Minds\Common\Urn;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* Class Campaign
* @package Minds\Core\Boost\Campaigns
* @method string getUrn()
* @method Campaign setUrn(string $urn)
* @method int|string getOwnerGuid()
* @method Campaign setOwnerGuid(int|string $ownerGuid)
* @method string getName()
* @method Campaign setName(string $name)
* @method string getType()
* @method Campaign setType(string $type)
* @method string[] getEntityUrns()
* @method Campaign setEntityUrns(string[] $entityUrns)
* @method string[] getHashtags()
* @method Campaign setHashtags(string[] $hashtags)
* @method int[] getNsfw()
* @method int getStart()
* @method Campaign setStart(int $start)
* @method int getEnd()
* @method Campaign setEnd(int $end)
* @method string getBudget()
* @method Campaign setBudget(string $budget)
* @method string getBudgetType()
* @method Campaign setBudgetType(string $budgetType)
* @method Payment[] getPayments()
* @method Campaign setPayments(Payment[] $payments)
* @method string getChecksum()
* @method Campaign setChecksum(string $checksum)
* @method int getImpressions()
* @method Campaign setImpressions(int $impressions)
* @method int getImpressionsMet()
* @method Campaign setImpressionsMet(int $impressionsMet)
* @method int getRating()
* @method Campaign setRating(int $value)
* @method int getQuality()
* @method Campaign setQuality(int $value)
* @method int getCreatedTimestamp()
* @method Campaign setCreatedTimestamp(int $createdTimestamp)
* @method int getReviewedTimestamp()
* @method Campaign setReviewedTimestamp(int $reviewedTimestamp)
* @method int getRevokedTimestamp()
* @method Campaign setRevokedTimestamp(int $revokedTimestamp)
* @method int getRejectedTimestamp()
* @method Campaign setRejectedTimestamp(int $rejectedTimestamp)
* @method int getCompletedTimestamp()
* @method Campaign setCompletedTimestamp(int $completedTimestamp)
*/
class Campaign implements JsonSerializable
{
use MagicAttributes;
/** @var string */
const STATUS_PENDING = 'pending';
/** @var string */
const STATUS_CREATED = 'created';
/** @var string */
const STATUS_APPROVED = 'approved';
/** @var string */
const STATUS_REJECTED = 'rejected';
/** @var string */
const STATUS_REVOKED = 'revoked';
/** @var string */
const STATUS_COMPLETED = 'completed';
/** @var int */
const RATING_SAFE = 1;
/** @var int */
const RATING_OPEN = 2;
/** @var string */
protected $urn;
/** @var string */
protected $type;
/** @var int|string */
protected $ownerGuid;
/** @var string */
protected $name;
/** @var string[] */
protected $entityUrns = [];
/** @var string[] */
protected $hashtags;
/** @var int[] */
protected $nsfw;
/** @var int */
protected $start;
/** @var int */
protected $end;
/** @var string */
protected $budget;
/** @var string */
protected $budgetType;
/** @var array */
protected $payments = [];
/** @var string */
protected $checksum;
/** @var int */
protected $impressions;
/** @var int */
protected $impressionsMet;
/** @var int */
protected $rating;
/** @var int */
protected $quality;
/** @var int */
protected $createdTimestamp;
/** @var int */
protected $reviewedTimestamp;
/** @var int */
protected $revokedTimestamp;
/** @var int */
protected $rejectedTimestamp;
/** @var int */
protected $completedTimestamp;
public function getGuid(): string
{
if (!$this->urn) {
return '';
}
try {
return (new Urn($this->urn))->getNss();
} catch (Exception $exception) {
return '';
}
}
/**
* @param User $owner
* @return Campaign
*/
public function setOwner(User $owner = null): self
{
$this->ownerGuid = $owner ? $owner->guid : null;
return $this;
}
/**
* @param Payment $payment
* @return Campaign
*/
public function pushPayment(Payment $payment): self
{
$this->payments[] = $payment;
return $this;
}
public function getDeliveryStatus(): string
{
if ($this->completedTimestamp) {
return static::STATUS_COMPLETED;
} elseif ($this->rejectedTimestamp) {
return static::STATUS_REJECTED;
} elseif ($this->revokedTimestamp) {
return static::STATUS_REVOKED;
} elseif ($this->reviewedTimestamp) {
return static::STATUS_APPROVED;
} elseif ($this->createdTimestamp) {
return static::STATUS_CREATED;
}
return static::STATUS_PENDING;
}
/**
* @param int[] $value
* @return Campaign
*/
public function setNsfw($value): self
{
$this->nsfw = $value;
$this->setRating(count($this->getNsfw()) > 0 ? static::RATING_OPEN : static::RATING_SAFE); // 2 = open; 1 = safe
return $this;
}
public function cpm(): float
{
if (!$this->impressions || $this->impressions === 0) {
return 0;
}
return ($this->budget / $this->impressions) * 1000;
}
public function isDelivering(): bool
{
return $this->getDeliveryStatus() === static::STATUS_APPROVED;
}
public function shouldBeStarted(int $now): bool
{
$isCreated = $this->getDeliveryStatus() === static::STATUS_CREATED;
$started = $now >= $this->getStart() && $now < $this->getEnd();
return $isCreated && $started;
}
public function shouldBeCompleted(int $now): bool
{
$isDelivering = $this->isDelivering();
$ended = $now >= $this->getEnd();
$fulfilled = $this->getImpressionsMet() >= $this->getImpressions();
return $isDelivering && ($ended || $fulfilled);
}
public function hasStarted(): bool
{
return !in_array($this->getDeliveryStatus(), [static::STATUS_PENDING, static::STATUS_CREATED], true);
}
public function hasFinished(): bool
{
return in_array($this->getDeliveryStatus(), [
static::STATUS_COMPLETED,
static::STATUS_REJECTED,
static::STATUS_REVOKED,
], false);
}
public function export(bool $isGetData = false): array
{
$data = [
'urn' => $this->urn,
'name' => $this->name,
'entity_urns' => $this->entityUrns,
'hashtags' => $this->hashtags,
'nsfw' => $this->nsfw,
'start' => $this->start,
'end' => $this->end,
'budget' => $this->budget,
'budget_type' => $this->budgetType,
'checksum' => $this->checksum,
'impressions' => $this->impressions,
'impressions_met' => $this->impressionsMet,
'created_timestamp' => $this->createdTimestamp,
'reviewed_timestamp' => $this->reviewedTimestamp,
'revoked_timestamp' => $this->revokedTimestamp,
'rejected_timestamp' => $this->rejectedTimestamp,
'completed_timestamp' => $this->completedTimestamp,
];
if ($isGetData) {
$data['owner_guid'] = (string)$this->ownerGuid;
$data['rating'] = $this->rating;
$data['quality'] = $this->quality;
} else {
$data['type'] = $this->type;
$data['payments'] = $this->payments;
$data['delivery_status'] = $this->getDeliveryStatus();
$data['cpm'] = $this->cpm();
}
return $data;
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return $this->export();
}
public function getData(): array
{
return $this->export(true);
}
}
<?php
/**
* CampaignException
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns;
use Exception;
class CampaignException extends Exception
{
}
<?php
namespace Minds\Core\Boost\Campaigns\Delegates;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
use Minds\Core\Di\Di;
use Minds\Core\GuidBuilder;
class CampaignUrnDelegate
{
/** @var GuidBuilder */
protected $guid;
/**
* CampaignUrnDelegate constructor.
* @param GuidBuilder $guid
*/
public function __construct(?GuidBuilder $guid = null)
{
$this->guid = $guid ?: Di::_()->get('Guid');
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign): Campaign
{
if ($campaign->getUrn()) {
throw new CampaignException('Campaign already has an URN');
}
$guid = $this->guid->build();
return $campaign->setUrn("urn:campaign:{$guid}");
}
}
<?php
/**
* NormalizeDates
* @author edgebal
*/
namespace Minds\Core\Boost\Campaigns\Delegates;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
class NormalizeDatesDelegate
{
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign)
{
$start = $this->normaliseStartTime($campaign->getStart());
$end = $this->normaliseEndTime($campaign->getEnd());
$this->validateStartTime($start);
$this->validateEndTime($end);
$this->validateStartAgainstEnd($start, $end);
return $campaign->setStart($start)->setEnd($end);
}
/**
* @param Campaign $campaign
* @param Campaign $campaignRef
* @return Campaign
* @throws CampaignException
*/
public function onUpdate(Campaign $campaign, Campaign $campaignRef)
{
// TODO: Ensure date updates from ref are valid against original campaign budget, etc.
$start = $this->normaliseStartTime($campaignRef->getStart());
$end = $this->normaliseEndTime($campaignRef->getEnd());
$this->validateStartTime($start);
$this->validateEndTime($end);
$this->validateStartAgainstEnd($start, $end);
if (!$campaign->hasStarted()) {
$campaign->setStart($start);
}
if (!$campaign->hasFinished() && $campaign->getEnd() < $end) {
$campaign->setEnd($end);
}
return $campaign;
}
private function normaliseStartTime(int $startTime): int
{
return strtotime(date('Y-m-d', $startTime / 1000) . ' 00:00:00') * 1000;
}
private function normaliseEndTime(int $endTime): int
{
return strtotime(date('Y-m-d', $endTime / 1000) . ' 23:59:59') * 1000;
}
private function validateStartTime(int $startTime): void
{
if ($startTime <= 0) {
throw new CampaignException('Campaign should have a start date');
}
$today = strtotime(date('Y-m-d') . ' 00:00:00') * 1000;
if ($startTime < $today) {
throw new CampaignException('Campaign start should not be in the past');
}
}
private function validateEndTime(int $endTime): void
{
if ($endTime <= 0) {
throw new CampaignException('Campaign should have an end date');
}
}
private function validateStartAgainstEnd(int $start, int $end): void
{
if ($start >= $end) {
throw new CampaignException('Campaign end before starting');
}
$startPlusOneMonth = strtotime('+1 month', $start / 1000) * 1000;
if ($startPlusOneMonth < $end) {
throw new CampaignException('Campaign must not be longer than 1 month');
}
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Delegates;
use Exception;
use Minds\Common\Urn;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
use Minds\Core\Di\Di;
use Minds\Core\EntitiesBuilder;
use Minds\Core\Security\ACL;
use Minds\Entities\User;
use Minds\Helpers\Text;
class NormalizeEntityUrnsDelegate
{
/** @var ACL */
protected $acl;
/** @var EntitiesBuilder */
protected $entitiesBuilder;
/**
* NormalizeEntityUrnsDelegate constructor.
* @param ACL $acl
* @param EntitiesBuilder $entitiesBuilder
*/
public function __construct(
$acl = null,
$entitiesBuilder = null
) {
$this->acl = $acl ?: ACL::_();
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign)
{
$owner = new User();
$owner->set('guid', $campaign->getOwnerGuid());
$entityUrns = array_values(array_unique(array_filter(Text::buildArray($campaign->getEntityUrns()))));
if (!$entityUrns) {
throw new CampaignException('Campaign should have at least an entity');
}
$entityUrns = array_map(function ($entityUrn) {
if (is_numeric($entityUrn)) {
$entityUrn = "urn:entity:{$entityUrn}";
}
return $entityUrn;
}, $entityUrns);
foreach ($entityUrns as $entityUrn) {
// TODO: Should we use entity resolver?
try {
$entityUrn = new Urn($entityUrn);
} catch (Exception $e) {
throw new CampaignException("URN {$entityUrn} is not valid: {$e->getMessage()}");
}
$guid = $entityUrn->getNss();
$entity = $this->entitiesBuilder->single($guid);
if (!$entity) {
throw new CampaignException("Entity {$entityUrn} doesn't exist");
} elseif (!$this->acl->read($entity, $owner)) {
throw new CampaignException("Entity {$entityUrn} is not readable");
}
}
return $campaign->setEntityUrns($entityUrns);
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Delegates;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
class NormalizeHashtagsDelegate
{
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign): Campaign
{
$hashtags = $campaign->getHashtags();
if (is_string($hashtags)) {
$hashtags = explode(' ', $hashtags);
}
$hashtags = array_values(array_unique(array_filter(array_map(function ($hashtag) {
return preg_replace('/[^a-zA-Z_]/', '', $hashtag);
}, $hashtags))));
if (count($hashtags) > 5) {
throw new CampaignException('Campaigns have a maximum of 5 hashtags');
}
return $campaign->setHashtags($hashtags);
}
/**
* @param Campaign $campaign
* @param Campaign $campaignRef
* @return Campaign
* @throws CampaignException
*/
public function onUpdate(Campaign $campaign, Campaign $campaignRef): Campaign
{
$campaign->setHashtags($campaignRef->getHashtags());
return $this->onCreate($campaign);
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Delegates;
use Exception;
use Minds\Core\Boost\Campaigns\Campaign;
use Minds\Core\Boost\Campaigns\CampaignException;
use Minds\Core\Boost\Campaigns\Metrics;
use Minds\Core\Boost\Campaigns\Payments;
use Minds\Core\Boost\Campaigns\Payments\Payment;
use Minds\Core\Config;
use Minds\Core\Di\Di;
class PaymentsDelegate
{
/** @var Config */
protected $config;
/** @var Payments\Onchain */
protected $onchainPayments;
/** @var Metrics */
protected $metrics;
/**
* PaymentsDelegate constructor.
* @param Config $config
* @param Payments\Onchain $onchainPayments
* @param Metrics $metrics
* @throws Exception
*/
public function __construct(
$config = null,
$onchainPayments = null,
$metrics = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->onchainPayments = $onchainPayments ?: new Payments\Onchain();
$this->metrics = $metrics ?: new Metrics();
}
/**
* @param Campaign $campaign
* @param mixed $paymentPayload
* @return Campaign
* @throws CampaignException
*/
public function onCreate(Campaign $campaign, $paymentPayload = null)
{
$this->validateBudget($campaign);
if (!$paymentPayload) {
throw new CampaignException('Missing payment');
}
$this->pay($campaign, $paymentPayload);
$this->validatePayments($campaign);
return $this->updateImpressionsByCpm($campaign);
}
/**
* @param Campaign $campaign
* @param Campaign $campaignRef
* @param mixed $paymentPayload
* @return Campaign
* @throws CampaignException
*/
public function onUpdate(Campaign $campaign, Campaign $campaignRef, $paymentPayload = null)
{
$campaignRef->setBudgetType($campaign->getBudgetType());
$this->validateBudget($campaignRef);
if ($paymentPayload) {
// TODO: This looks wrong, we should act upon campaign, not campaignRef
$this->pay($campaignRef, $paymentPayload);
$this->validatePayments($campaignRef);
}
return $this->updateImpressionsByCpm($campaign);
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function onStateChange(Campaign $campaign)
{
$isFinished = in_array($campaign->getDeliveryStatus(), [
Campaign::STATUS_REJECTED,
Campaign::STATUS_REVOKED,
Campaign::STATUS_COMPLETED
], true);
if ($isFinished) {
$this->refund($campaign);
}
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function validateBudget(Campaign $campaign)
{
if (!$campaign->getBudget() || $campaign->getBudget() <= 0) {
throw new CampaignException('Campaign should have a budget');
}
if (!in_array($campaign->getBudgetType(), ['tokens'], true)) {
throw new CampaignException("Invalid budget type: {$campaign->getBudgetType()}");
}
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
*/
public function validatePayments(Campaign $campaign)
{
// TODO: Validate all payments
return $campaign;
}
/**
* @param Campaign $campaign
* @param mixed $payload
* @return Campaign
* @throws CampaignException
*/
public function pay(Campaign $campaign, $payload)
{
switch ($campaign->getBudgetType()) {
case 'tokens':
if (!$payload || !$payload['txHash'] || !$payload['address'] || !$payload['amount']) {
throw new CampaignException('Invalid payment signature');
}
$payment = new Payment();
$payment
->setOwnerGuid($campaign->getOwnerGuid())
->setCampaignGuid($campaign->getGuid())
->setTx($payload['txHash'])
->setSource($payload['address'])
->setAmount((double) $payload['amount'])
->setTimeCreated(time());
try {
$this->onchainPayments->record($payment);
} catch (Exception $e) {
throw new CampaignException("Error registering payment: {$e->getMessage()}");
}
$campaign->pushPayment($payment);
break;
default:
throw new CampaignException('Unknown budget type');
}
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
* @throws Exception
*/
public function refund(Campaign $campaign)
{
$latestPaymentSource = '';
$paid = 0;
$refundThreshold = 0.1;
foreach ($campaign->getPayments() as $payment) {
$paid += $payment->getAmount();
if ($payment->getAmount() > 0 && $payment->getSource()) {
$latestPaymentSource = $payment->getSource();
}
}
if ($paid <= $refundThreshold) {
return $campaign;
}
$impressionsMet = $this->metrics->setCampaign($campaign)->getImpressionsMet();
$cost = ($impressionsMet / 1000) * $campaign->cpm();
$amount = $paid - $cost;
if ($amount < $refundThreshold) {
return $campaign;
}
switch ($campaign->getBudgetType()) {
case 'tokens':
$payment = new Payment();
$payment
->setOwnerGuid($campaign->getOwnerGuid())
->setCampaignGuid($campaign->getGuid())
->setSource($latestPaymentSource)
->setAmount(-$amount)
->setTimeCreated(time());
try {
$this->onchainPayments->refund($payment);
} catch (Exception $e) {
throw new CampaignException("Error registering refund: {$e->getMessage()}");
}
$campaign->pushPayment($payment);
break;
default:
throw new CampaignException('Unknown budget type');
}
return $campaign;
}
/**
* @param Campaign $campaign
* @param Payment $paymentRef
* @return Campaign
*/
public function onConfirm(Campaign $campaign, Payment $paymentRef)
{
// TODO: Check ALL other payments to ensure budget
$campaign->setCreatedTimestamp(time() * 1000);
return $campaign;
}
/**
* @param Campaign $campaign
* @param Payment $paymentRef
* @return Campaign
*/
public function onFail(Campaign $campaign, Payment $paymentRef)
{
$campaign
->setCreatedTimestamp(time() * 1000)
->setRevokedTimestamp(time() * 1000);
return $campaign;
}
/**
* @param Campaign $campaign
* @return Campaign
* @throws CampaignException
*/
public function updateImpressionsByCpm(Campaign $campaign)
{
$cpm = (float) ($this->config->get('boost')['cpm'] ?? 1);
if (!$cpm) {
throw new CampaignException('Missing CPM');
}
$impressions = floor((1000 * $campaign->getBudget()) / $cpm);
$campaign
->setImpressions($impressions);
if (!$campaign->getImpressions()) {
throw new CampaignException('Impressions value cannot be 0');
}
return $campaign;
}
}
<?php
namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Traits\DiAlias;
class Dispatcher
{
use DiAlias;
const IMPRESSIONS_SYNC_THRESHOLD = 5;
/** @var Manager */
protected $manager;
/** @var Metrics */
protected $metrics;
/** @var int */
protected $impressionsSyncThreshold;
/** @var Campaign */
protected $campaign;
/** @var int */
protected $now;
/**
* Dispatcher constructor.
* @param Manager $manager
* @param Metrics $metrics
* @param int $impressionsSyncThreshold
* @throws Exception
*/
public function __construct(
$manager = null,
$metrics = null,
$impressionsSyncThreshold = null
) {
$this->manager = $manager ?: new Manager();
$this->metrics = $metrics ?: new Metrics();
$this->impressionsSyncThreshold = $impressionsSyncThreshold ?: static::IMPRESSIONS_SYNC_THRESHOLD;
}
/**
* @param string $campaignUrn
* @throws Exception
*/
public function onLifecycle(string $campaignUrn)
{
$this->now = time() * 1000;
$this->campaign = $this->manager->getCampaignByUrn($campaignUrn);
$this->metrics->setCampaign($this->campaign);
$this->syncIfImpressionsThresholdMet();
$this->completeCampaign();
$this->startCampaign();
}
/**
* Sync to database if impressions threshold is met
* @throws Exception
*/
public function syncIfImpressionsThresholdMet(): void
{
if ($this->campaign->isDelivering()) {
$currentImpressionsMet = $this->campaign->getImpressionsMet();
$newImpressionsMet = $this->metrics->getImpressionsMet();
if ($newImpressionsMet - $currentImpressionsMet >= $this->impressionsSyncThreshold) {
$this->campaign->setImpressionsMet($newImpressionsMet);
error_log("[BoostCampaignsDispatcher] Saving updated {$this->campaign->getUrn()}...");
$this->manager->sync($this->campaign);
}
}
}
/**
* Record the campaign as completed
* @throws CampaignException
*/
public function completeCampaign(): void
{
if ($this->campaign->shouldBeCompleted($this->now)) {
error_log("[BoostCampaignsDispatcher] Completing {$this->campaign->getUrn()}...");
$this->manager->completeCampaign($this->campaign);
}
}
/**
* Record the campaign as started
* @throws CampaignException
*/
public function startCampaign(): void
{
if ($this->campaign->shouldBeStarted($this->now)) {
error_log("[BoostCampaignsDispatcher] Starting {$this->campaign->getUrn()}...");
$this->manager->start($this->campaign);
}
}
}
<?php
namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Boost\Network\Boost;
use Minds\Core\Data\ElasticSearch\Client as ElasticSearchClient;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Data\ElasticSearch\Prepared\Update;
use Minds\Core\Di\Di;
use Minds\Helpers\Number;
use Minds\Helpers\Text;
class ElasticRepository
{
/** @var ElasticSearchClient */
protected $es;
/** @var ElasticRepositoryQueryBuilder */
protected $queryBuilder;
/**
* Options for fetching queries
* @var array
*/
protected $opts;
public function __construct(
?ElasticSearchClient $es = null,
?ElasticRepositoryQueryBuilder $queryBuilder = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
$this->queryBuilder = $queryBuilder ?: new ElasticRepositoryQueryBuilder();
}
/**
* @param array $opts
* @return Response
*/
public function getCampaigns(array $opts = [])
{
$this->opts = array_merge([
'limit' => 12,
'from' => 0,
'type' => ''
], $opts);
$this->queryBuilder->setOpts($opts);
$prepared = new Search();
$prepared->query([
'index' => 'minds-boost-campaigns',
'type' => $this->opts['type'],
'body' => $this->queryBuilder->query(),
'size' => $this->opts['limit'],
'from' => (int)($this->opts['from'] ?? 0),
]);
$result = $this->es->request($prepared);
$response = new Response();
$offset = 0;
foreach ($result['hits']['hits'] as $doc) {
$campaign = new Campaign();
$campaign
->setUrn("urn:campaign:{$doc['_id']}")
->setType($doc['_source']['type'])
->setOwnerGuid($doc['_source']['owner_guid'])
->setName($doc['_source']['name'])
->setEntityUrns(Text::buildArray($doc['_source']['entity_urns']))
->setHashtags(Text::buildArray($doc['_source']['hashtags']))
->setNsfw(Number::buildIntArray($doc['_source']['nsfw']))
->setStart((int) $doc['_source']['start'])
->setEnd((int) $doc['_source']['end'])
->setBudget((string) $doc['_source']['budget'])
->setBudgetType($doc['_source']['budget_type'])
->setChecksum($doc['_source']['checksum'])
->setImpressions((int) $doc['_source']['impressions'])
->setImpressionsMet($doc['_source']['impressions_met'])
->setRating($doc['_source']['rating'])
->setQuality($doc['_source']['quality'])
->setCreatedTimestamp(($doc['_source']['@timestamp']) ?? null)
->setReviewedTimestamp(($doc['_source']['@reviewed']) ?? null)
->setRejectedTimestamp(($doc['_source']['@rejected']) ?? null)
->setRevokedTimestamp(($doc['_source']['@revoked']) ?? null)
->setCompletedTimestamp(($doc['_source']['@completed']) ?? null);
$response[] = $campaign;
$offset = $campaign->getCreatedTimestamp();
}
$response->setPagingToken($offset);
return $response;
}
public function getCampaignsAndBoosts(array $opts = [])
{
$this->opts = array_merge([
'limit' => 24,
'from' => 0,
'type' => ''
], $opts);
$this->queryBuilder->setOpts($this->opts);
$prepared = new Search();
$prepared->query([
'index' => 'minds-boost,minds-boost-campaigns',
'type' => $this->opts['type'],
'body' => $this->queryBuilder->query(),
'from' => $this->opts['from'] ?? 0,
'size' => $this->opts['limit'],
]);
$result = $this->es->request($prepared);
$data = [];
foreach ($result['hits']['hits'] as $doc) {
$entity = null;
switch ($doc['_index']) {
case 'minds-boost':
$entity = (new Boost())
->setGuid($doc['_id'])
->setOwnerGuid($doc['_source']['owner_guid'])
->setCreatedTimestamp($doc['_source']['@timestamp'])
->setType($doc['_source']['type']);
break;
case 'minds-boost-campaigns':
$entity = (new Campaign())
->setUrn("urn:campaign:{$doc['_id']}")
->setType($doc['_source']['type'])
->setOwnerGuid($doc['_source']['owner_guid'])
->setCreatedTimestamp(((int) $doc['_source']['@timestamp']) ?: null);
break;
default:
continue 2;
}
$data[] = $entity;
}
$data = array_slice($data, 0, $this->opts['limit']);
$response = new Response($data, count($data));
return $response;
}
/**
* @param Campaign $campaign
* @return bool
* @throws Exception
*/
public function putCampaign(Campaign $campaign)
{
$body = [
'doc' => [
'type' => $campaign->getType(),
'owner_guid' => (string) $campaign->getOwnerGuid(),
'name' => $campaign->getName(),
'entity_urns' => $campaign->getEntityUrns(),
'hashtags' => $campaign->getHashtags(),
'nsfw' => $campaign->getNsfw(),
'start' => (int) $campaign->getStart(),
'end' => (int) $campaign->getEnd(),
'budget' => $campaign->getBudget(),
'budget_type' => $campaign->getBudgetType(),
'checksum' => $campaign->getChecksum(),
'impressions' => $campaign->getImpressions(),
'@timestamp' => $campaign->getCreatedTimestamp(),
],
'doc_as_upsert' => true,
];
if ($campaign->getImpressionsMet()) {
$body['doc']['impressions_met'] = $campaign->getImpressionsMet();
}
if ($campaign->getRating()) {
$body['doc']['rating'] = $campaign->getRating();
}
if ($campaign->getQuality()) {
$body['doc']['quality'] = $campaign->getQuality();
}
if ($campaign->getReviewedTimestamp()) {
$body['doc']['@reviewed'] = $campaign->getReviewedTimestamp();
}
if ($campaign->getRejectedTimestamp()) {
$body['doc']['@rejected'] = $campaign->getRejectedTimestamp();
}
if ($campaign->getRevokedTimestamp()) {
$body['doc']['@revoked'] = $campaign->getRevokedTimestamp();
}
if ($campaign->getCompletedTimestamp()) {
$body['doc']['@completed'] = $campaign->getCompletedTimestamp();
}
$prepared = new Update();
$prepared->query([
'index' => 'minds-boost-campaigns',
'type' => '_doc',
'body' => $body,
'id' => (string) $campaign->getGuid(),
]);
return (bool) $this->es->request($prepared);
}
}
<?php
namespace Minds\Core\Boost\Campaigns;
class ElasticRepositoryQueryBuilder
{
protected $opts;
protected $must;
protected $mustNot;
protected $sort;
public function setOpts(array $opts): self
{
$this->opts = $opts;
return $this;
}
public function reset()
{
$this->opts = [
'type' => null,
'guid' => null,
'owner_guid' => null,
'entity_urn' => null,
'state' => null,
'rating' => null,
'quality' => null,
'offset' => null,
'sort' => 'asc'
];
$this->must = [];
$this->mustNot = [];
$this->sort = [];
}
public function query(): array
{
$this->reset();
$this->parseType();
$this->parseGuid();
$this->parseEntityUrn();
$this->parseState();
$this->parseRating();
$this->parseQuality();
$this->parseOffset();
$this->parseSort();
return $this->body();
}
public function parseType(): void
{
if ($this->opts['type']) {
$this->must[] = [
'term' => [
'type' => $this->opts['type'],
],
];
}
}
public function parseGuid(): void
{
if ($this->opts['guid']) {
$this->must[] = [
'term' => [
'_id' => (string)$this->opts['guid'],
],
];
} elseif ($this->opts['owner_guid']) {
$this->must[] = [
'term' => [
'owner_guid' => (string)$this->opts['owner_guid'],
],
];
}
}
public function parseEntityUrn(): void
{
if ($this->opts['entity_urn']) {
$this->must[] = [
'term' => [
'entity_urn' => $this->opts['entity_urn'],
],
];
}
}
public function parseState(): void
{
if ($this->opts['state'] === 'approved') {
$this->must[] = [
'exists' => [
'field' => '@reviewed',
],
];
} elseif ($this->opts['state'] === 'in_review') {
$this->mustNot[] = [
'exists' => [
'field' => '@reviewed',
],
];
}
if ($this->opts['state'] === 'approved' || $this->opts['state'] === 'in_review') {
$this->mustNot[] = [
'exists' => [
'field' => '@completed',
],
];
$this->mustNot[] = [
'exists' => [
'field' => '@rejected',
],
];
$this->mustNot[] = [
'exists' => [
'field' => '@revoked',
],
];
}
}
public function parseRating(): void
{
if ($this->opts['rating']) {
$this->must[] = [
'range' => [
'rating' => [
'lte' => $this->opts['rating'],
],
],
];
}
}
public function parseQuality(): void
{
if ($this->opts['quality']) {
$this->must[] = [
'range' => [
'quality' => [
'gte' => $this->opts['quality'],
],
],
];
}
}
public function parseOffset(): void
{
if ($this->opts['offset']) {
$rangeKey = $this->opts['sort'] === 'asc' ? 'gt' : 'lt';
$this->must[] = [
'range' => [
'@timestamp' => [
$rangeKey => $this->opts['offset'],
],
],
];
}
}
public function parseSort(): void
{
$this->sort = [
'@timestamp' => $this->opts['sort'] ?? 'asc',
];
}
public function body(): array
{
return [
'query' => [
'bool' => [
'must' => $this->must,
'must_not' => $this->mustNot,
],
],
'sort' => $this->sort,
];
}
}
<?php
namespace Minds\Core\Boost\Campaigns;
use Minds\Core\Di\Di;
use Minds\Traits\DiAlias;
class Iterator implements \Iterator
{
use DiAlias;
/** @var Manager */
protected $manager;
protected $limit = 12;
protected $from = 0;
protected $offset = null;
protected $sort = 'asc';
protected $type = 'newsfeed';
protected $ownerGuid = null;
protected $state = null;
protected $rating = null;
protected $quality = null;
/** @var array */
protected $list = null;
public function __construct(Manager $manager = null)
{
$this->manager = $manager ?: Di::_()->get(Manager::getDiAlias());
}
/**
* @param int $limit
* @return Iterator
*/
public function setLimit(int $limit)
{
$this->limit = $limit;
return $this;
}
/**
* @param int $from
* @return Iterator
*/
public function setFrom(int $from)
{
$this->from = $from;
return $this;
}
/**
* @return int|null
*/
public function getOffset()
{
return $this->offset;
}
/**
* @param int|null $offset
* @return Iterator
*/
public function setOffset($offset)
{
$this->offset = $offset;
return $this;
}
/**
* @param string $sort
* @return Iterator
*/
public function setSort(string $sort)
{
$this->sort = $sort;
return $this;
}
/**
* @param mixed $type
* @return Iterator
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
/**
* @param null $ownerGuid
* @return Iterator
*/
public function setOwnerGuid($ownerGuid)
{
$this->ownerGuid = $ownerGuid;
return $this;
}
/**
* @param mixed $state
* @return Iterator
*/
public function setState($state)
{
$this->state = $state;
return $this;
}
/**
* @param int $rating
* @return Iterator
*/
public function setRating(int $rating)
{
$this->rating = $rating;
return $this;
}
/**
* @param int $quality
* @return Iterator
*/
public function setQuality(int $quality)
{
$this->quality = $quality;
return $this;
}
public function getList()
{
$response = $this->manager->getCampaignsAndBoosts([
'limit' => $this->limit,
'from' => $this->from,
'offset' => $this->offset,
'type' => $this->type,
'owner_guid' => $this->ownerGuid,
'state' => $this->state,
'rating' => $this->rating,
'quality' => $this->quality,
]);
$this->offset = $response->getPagingToken();
$this->list = $response;
}
/**
* @return Campaign
*/
public function current()
{
return current($this->list);
}
public function next()
{
next($this->list);
}
public function key()
{
return key($this->list);
}
public function valid()
{
if (!$this->list) {
return false;
}
return key($this->list) !== null;
}
public function rewind()
{
if ($this->list) {
reset($this->list);
}
$this->getList();
}
}
This diff is collapsed.
<?php
namespace Minds\Core\Boost\Campaigns;
use Exception;
use Minds\Common\Urn;
use Minds\Core\Counters\Manager as Counters;
use Minds\Core\Di\Di;
use Minds\Core\Entities\Resolver;
use Minds\Traits\DiAlias;
class Metrics
{
use DiAlias;
/** @var Counters */
protected $counters;
/** @var Resolver */
protected $resolver;
/** @var Campaign */
protected $campaign;
/**
* Metrics constructor.
* @param Counters $counters
* @param Resolver $resolver
* @throws Exception
*/
public function __construct(
$counters = null,
$resolver = null
) {
$this->counters = $counters ?: Di::_()->get('Counters');
$this->resolver = $resolver ?: new Resolver();
}
/**
* @param Campaign $campaign
* @return Metrics
* @throws Exception
*/
public function setCampaign(Campaign $campaign): self
{
$this->campaign = $campaign;
return $this;
}
/**
* @throws Exception
*/
public function increment(): void
{
$this->incrementGlobalBoostCounter();
$this->incrementCampaignBoostCounter();
$this->incrementEntityCounters();
}
protected function incrementGlobalBoostCounter(): void
{
$this->counters
->setEntityGuid(0)
->setMetric('boost_impressions')
->increment();
}
protected function incrementCampaignBoostCounter(): void
{
$this->counters
->setEntityGuid($this->campaign->getGuid())
->setMetric('boost_impressions')
->increment();
}
protected function incrementEntityCounters(): void
{
foreach ($this->campaign->getEntityUrns() as $entityUrn) {
// 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)
$entity = $this->resolver->single(new Urn($entityUrn));
if ($entity) {
$this->counters
->setEntityGuid($entity->guid)
->setMetric('impression')
->increment();
$this->counters
->setEntityGuid($entity->owner_guid)
->setMetric('impression')
->increment();
}
}
}
/**
* @return int
* @throws Exception
*/
public function getImpressionsMet(): int
{
return $this->counters
->setEntityGuid($this->campaign->getGuid())
->setMetric('boost_impressions')
->get(false);
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Payments;
use Exception;
use Minds\Core\Blockchain\Services\Ethereum;
use Minds\Core\Blockchain\Transactions\Manager as TransactionsManager;
use Minds\Core\Blockchain\Transactions\Transaction;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Util\BigNumber;
class Onchain
{
/** @var Config */
protected $config;
/** @var Ethereum */
protected $eth;
/** @var Repository */
protected $repository;
/** @var TransactionsManager */
protected $txManager;
public function __construct(
$config = null,
$eth = null,
$repository = null,
$txManager = null
) {
$this->config = $config ?: Di::_()->get('Config');
$this->eth = $eth ?: Di::_()->get('Blockchain\Services\Ethereum');
$this->repository = $repository ?: new Repository();
$this->txManager = $txManager ?: Di::_()->get('Blockchain\Transactions\Manager');
}
/**
* @param Payment $payment
* @return bool
* @throws Exception
*/
public function record(Payment $payment)
{
if ($this->txManager->exists($payment->getTx())) {
throw new Exception('Payment transaction already exists');
}
$transaction = new Transaction();
$transaction
->setTx($payment->getTx())
->setContract('boost_campaign')
->setAmount((string) BigNumber::toPlain($payment->getAmount(), 18)->neg())
->setWalletAddress($payment->getSource())
->setTimestamp($payment->getTimeCreated())
->setUserGuid($payment->getOwnerGuid())
->setData([
'payment' => $payment->export(),
]);
$this->repository->add($payment);
$this->txManager->add($transaction);
return true;
}
/**
* @param Payment $payment
* @return Payment
* @throws Exception
*/
public function refund(Payment $payment)
{
$token = $this->config->get('blockchain')['token_address'];
$wallet = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_address'] ?? null;
$walletKey = $this->config->get('blockchain')['contracts']['boost_campaigns']['wallet_pkey'] ?? null;
if (!$token) {
throw new Exception('Invalid token contract address used for refund');
} elseif (!$wallet || !$walletKey) {
throw new Exception('Invalid Boost Campaigns wallet address used as refund source');
} elseif (!$payment->getSource()) {
throw new Exception('Invalid Boost Campaign refund destination wallet address');
} elseif ($payment->getAmount() > 0) {
throw new Exception('Refunds can only happen on negative payment amounts');
}
$txWeiAmount = BigNumber::toPlain($payment->getAmount(), 18)->neg();
$refundTx = $this->eth->sendRawTransaction($walletKey, [
'from' => $wallet,
'to' => $token,
'gasLimit' => BigNumber::_(4612388)->toHex(true),
'gasPrice' => BigNumber::_(10000000000)->toHex(true),
'data' => $this->eth->encodeContractMethod('transfer(address,uint256)', [
$payment->getSource(),
$txWeiAmount->toHex(true),
]),
]);
$payment->setTx($refundTx);
$transaction = new Transaction();
$transaction
->setTx($payment->getTx())
->setContract('boost_campaign')
->setAmount((string) $txWeiAmount)
->setWalletAddress($payment->getSource())
->setTimestamp($payment->getTimeCreated())
->setUserGuid($payment->getOwnerGuid())
->setData([
'payment' => $payment->export(),
]);
$this->repository->add($payment);
$this->txManager->add($transaction, false);
return $payment;
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Payments;
use JsonSerializable;
use Minds\Traits\MagicAttributes;
/**
* Class Payment
* @package Minds\Core\Boost\Campaigns\Payments
* @method int|string getOwnerGuid()
* @method Payment setOwnerGuid(int|string $ownerGuid)
* @method int|string getCampaignGuid()
* @method Payment setCampaignGuid(int|string $campaignGuid)
* @method string getTx()
* @method Payment setTx(string $tx)
* @method string getSource()
* @method Payment setSource(string $source)
* @method double getAmount()
* @method Payment setAmount(double $amount)
* @method int getTimeCreated()
* @method Payment setTimeCreated(int $timeCreated)
*/
class Payment implements JsonSerializable
{
use MagicAttributes;
/** @var int|string */
protected $ownerGuid;
/** @var int|string */
protected $campaignGuid;
/** @var string */
protected $tx;
/** @var string */
protected $source;
/** @var string */
protected $amount;
/** @var int */
protected $timeCreated;
/**
* @return array
*/
public function export()
{
return [
'owner_guid' => (string) $this->ownerGuid,
'campaign_guid' => (string) $this->campaignGuid,
'tx' => $this->tx,
'source' => $this->source,
'amount' => $this->amount,
'time_created' => $this->timeCreated,
];
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return $this->export();
}
}
<?php
namespace Minds\Core\Boost\Campaigns\Payments;
use Cassandra\Bigint;
use Cassandra\Decimal;
use Cassandra\Timestamp;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\Cassandra\Client as CassandraClient;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
use NotImplementedException;
class Repository
{
/** @var CassandraClient */
protected $db;
/**
* Repository constructor.
* @param CassandraClient $db
*/
public function __construct(
$db = null
) {
$this->db = $db ?: Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = [])
{
$opts = array_merge([
'owner_guid' => null,
'campaign_guid' => null,
'tx' => null,
], $opts);
$cql = "SELECT * FROM boost_campaigns_payments";
$where = [];
$values = [];
if ($opts['owner_guid']) {
$where[] = 'owner_guid = ?';
$values[] = new Bigint($opts['owner_guid']);
}
if ($opts['campaign_guid']) {
$where[] = 'campaign_guid = ?';
$values[] = new Bigint($opts['campaign_guid']);
}
if ($opts['tx']) {
$where[] = 'tx = ?';
$values[] = (string) $opts['tx'];
}
if ($where) {
$cql .= sprintf(" WHERE %s", implode(' AND ', $where));
}
$prepared = new Custom();
$prepared->query($cql, $values);
$response = new Response();
try {
// TODO: Use Cassandra Scroll for getList
$rows = $this->db->request($prepared) ?: [];
foreach ($rows as $row) {
$payment = new Payment();
$payment
->setOwnerGuid($row['owner_guid']->toInt())
->setCampaignGuid($row['campaign_guid']->toInt())
->setTx($row['tx'])
->setSource($row['source'])
->setAmount($row['amount']->toDouble())
->setTimeCreated($row['time_created']->time());
$response[] = $payment;
}
} catch (Exception $e) {
$response->setException($e);
}
return $response;
}
/**
* @param Payment $payment
* @return bool
*/
public function add(Payment $payment)
{
$cql = "INSERT INTO boost_campaigns_payments (owner_guid, campaign_guid, tx, source, amount, time_created) VALUES (?, ?, ?, ?, ?, ?)";
$values = [
new Bigint($payment->getOwnerGuid()),
new Bigint($payment->getCampaignGuid()),
(string) $payment->getTx(),
(string) $payment->getSource(),
new Decimal($payment->getAmount()),
new Timestamp($payment->getTimeCreated())
];
$prepared = new Custom();
$prepared->query($cql, $values);
return (bool) $this->db->request($prepared, true);
}
/**
* @param Payment $payment
* @return bool
*/
public function update(Payment $payment)
{
return $this->add($payment);
}
/**
* @param Payment $payment
* @throws NotImplementedException
*/
public function delete(Payment $payment)
{
throw new NotImplementedException();
}
}
This diff is collapsed.
<?php
namespace Minds\Core\Boost\Campaigns;
use Minds\Core\Analytics\EntityCentric\BoostViewsDaily;
use Minds\Core\Time;
use Minds\Traits\DiAlias;
class Stats
{
use DiAlias;
/** @var Campaign */
protected $campaign;
/** @var BoostViewsDaily */
protected $boostViewsDaily;
public function __construct(BoostViewsDaily $boostViewsDaily = null)
{
$this->boostViewsDaily = $boostViewsDaily ?: new BoostViewsDaily();
}
/**
* @param Campaign $campaign
* @return Stats
*/
public function setCampaign(Campaign $campaign): self
{
$this->campaign = $campaign;
return $this;
}
/**
* @return array
*/
public function getAll(): array
{
/* TODO: Evaluate the campaign targetting parameters against our data */
$campaignDurationDays = ($this->campaign->getEnd() - $this->campaign->getStart()) / Time::ONE_DAY_MS;
$campaignViewsPerDayReq = ($campaignDurationDays > 0) ? $this->campaign->getImpressions() / $campaignDurationDays : 0;
$globalViewsPerDay = $this->boostViewsDaily->getAvg();
return [
'canBeDelivered' => ($campaignViewsPerDayReq < $globalViewsPerDay),
'durationDays' => $campaignDurationDays,
'viewsPerDayRequested' => $campaignViewsPerDayReq,
'globalViewsPerDay' => $globalViewsPerDay
];
}
}
This diff is collapsed.
<?php
/**
* Description
*
* @author emi
*/
namespace Minds\Core\Boost;
use Minds\Entities\Entity;
......
......@@ -4,7 +4,6 @@ namespace Minds\Core\Boost;
use Minds\Core\Email\Campaigns;
use Minds\Core\Events\Dispatcher;
use Minds\Core\Payments;
/**
* Minds Payments Events.
......
<?php
namespace Minds\Core\Boost\Exceptions;
use Throwable;
class EntityAlreadyBoostedException extends \Exception
{
public function __construct($code = 0, Throwable $previous = null)
{
parent::__construct("There's already an ongoing boost for this entity", $code, $previous);
}
}
<?php
namespace Minds\Core\Boost;
use Minds\Core\Data;
use Minds\Interfaces;
/**
* A factory providing handlers boosting items
*/
class Factory
{
public static function getClassHandler($handler)
{
$handler = ucfirst($handler);
$handler = "Minds\\Core\\Boost\\$handler";
if (class_exists($handler)) {
return $handler;
}
throw new \Exception("Handler not found");
}
/**
* Build the handler
* @param string $handler
* @param array $options (optional)
* @return BoostHandlerInterface
*/
public static function build($handler, $options = array(), $db = null)
{
if($handler == 'newsfeed')
$handler = 'network';
$handler = ucfirst($handler);
$handler = "Minds\\Core\\Boost\\$handler";
if (class_exists($handler)) {
$class = new $handler($options, $db);
if ($class instanceof Interfaces\BoostHandlerInterface) {
return $class;
}
}
throw new \Exception("Handler not found");
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.