...
 
Commits (32)
......@@ -13,22 +13,16 @@ stages:
- deploy:canary
- deploy:production
cache:
paths:
- vendor
- bin
policy: pull
build:
stage: build
script:
- apk update && apk add --no-cache git
- sh tools/setup.sh production
cache:
artifacts:
name: '$CI_COMMIT_REF_SLUG'
paths:
- vendor
- bin
policy: push
test:
stage: test
......
<?php
namespace Minds\Controllers\Cli;
use Minds\Core;
use Minds\Core\Monetization\Partners\Manager;
use Minds\Cli;
use Minds\Interfaces;
use Minds\Exceptions;
use Minds\Entities;
class PartnerEarnings extends Cli\Controller implements Interfaces\CliControllerInterface
{
public function __construct()
{
}
public function help($command = null)
{
$this->out('TBD');
}
public function exec()
{
$this->out('Missing subcommand');
}
public function sync()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
$daysAgo = $this->getOpt('daysAgo') ?: 0;
$from = $this->getOpt('from') ?: strtotime("midnight $daysAgo days ago");
$manager = new Manager();
$i = 0;
foreach ($manager->issueDeposits([ 'from' => $from ]) as $record) {
$this->out(++$i);
}
}
}
......@@ -19,6 +19,13 @@ class review implements Interfaces\Api
$group = Entities\Factory::build($pages[0]);
$user = Core\Session::getLoggedInUser();
if (!$group) {
return Factory::response([
'status' => 'error',
'message' => 'Group not found'
]);
}
if (!$group->isOwner($user) && !$group->isModerator($user)) {
return Factory::response([
'status' => 'error',
......
......@@ -39,6 +39,11 @@ class config implements Interfaces\Api, Interfaces\ApiIgnorePam
"plus" => Minds\Core\Config::_()->get('plus'),
"report_reasons" => Minds\Core\Config::_()->get('report_reasons'),
"last_tos_update" => (Minds\Core\Config::_()->get('last_tos_update') ?: time()),
'handlers' => [
'plus' => Minds\Core\Di\Di::_()->get('Config')->get('plus')['handler'] ?? null,
'pro' => Minds\Core\Di\Di::_()->get('Config')->get('pro')['handler'] ?? null,
],
'upgrades' => Minds\Core\Di\Di::_()->get('Config')->get('upgrades'),
];
return Factory::response($minds);
......
......@@ -25,13 +25,22 @@ class plus implements Interfaces\Api
*/
public function get($pages)
{
$response = [];
$user = Core\Session::getLoggedInUser();
if (!$user) {
return Factory::response([
'status' => 'error',
'message' => 'Invalid user'
]);
}
$plus = new Core\Plus\Subscription();
$plus->setUser(Core\Session::getLoggedInUser());
$response['active'] = $plus->isActive();
$plus->setUser($user);
return Factory::response($response);
return Factory::response([
'active' => $plus->isActive(),
'can_be_cancelled' => $plus->canBeCancelled()
]);
}
public function post($pages)
......
......@@ -26,78 +26,37 @@ class subscribe implements Interfaces\Api
*/
public function get($pages)
{
$manager = new Subscriptions\Manager();
$response = [];
switch ($pages[0]) {
case 'subscriptions':
$db = new \Minds\Core\Data\Call('friends');
$subscribers= $db->getRow($pages[1], ['limit'=>get_input('limit', 12), 'offset'=>get_input('offset', '')]);
if (!$subscribers) {
return Factory::response([]);
}
$users = [];
foreach ($subscribers as $guid => $subscriber) {
if ($guid == get_input('offset')) {
continue;
}
if (is_numeric($subscriber)) {
//this is a local, old style subscription
$users[] = new \Minds\Entities\User($guid);
continue;
}
$users[] = new \Minds\Entities\User(json_decode($subscriber, true));
}
$users = array_values(array_filter($users, function ($user) {
return ($user->enabled != 'no' && $user->banned != 'yes');
}));
$response['users'] = factory::exportable($users);
$response['load-next'] = (string) end($users)->guid;
$response['load-previous'] = (string) key($users)->guid;
break;
case 'subscribers':
if ($pages[1] == "100000000000000519") {
break;
}
$db = new \Minds\Core\Data\Call('friendsof');
$subscribers= $db->getRow($pages[1], ['limit'=>get_input('limit', 12), 'offset'=>get_input('offset', '')]);
if (!$subscribers) {
return Factory::response([]);
}
$users = [];
if (get_input('offset') && key($subscribers) != get_input('offset')) {
$response['load-previous'] = (string) get_input('offset');
} else {
foreach ($subscribers as $guid => $subscriber) {
if ($guid == get_input('offset')) {
unset($subscribers[$guid]);
continue;
}
if (is_numeric($subscriber)) {
//this is a local, old style subscription
$users[] = new \Minds\Entities\User($guid);
continue;
}
//var_dump(print_r($users,true));die();
$users[] = new \Minds\Entities\User(json_decode($subscriber, true));
}
$users = array_values(array_filter($users, function ($user) {
return ($user->enabled != 'no' && $user->banned != 'yes')
&& $user->guid && $user->username;
}));
$response['users'] = factory::exportable($users);
$response['load-next'] = (string) end($users)->guid;
$response['load-previous'] = (string) key($users)->guid;
}
break;
$guid = $pages[1] ?? Core\Session::getLoggedInUser()->guid;
$type = $pages[0] ?? "subscribers";
$limit = $_GET['limit'] ?? 12;
$offset = $_GET['offset'] ?? "";
$opts = [
'guid'=>$guid,
'type'=>$type,
'limit'=>$limit,
'offset'=>$offset,
];
$users = $manager->getList($opts);
if (!$users) {
return Factory::response([
'status' => 'error',
'message' => 'Unable to find '.$type,
]);
}
$pagingToken = (string) $users->getPagingToken();
$users = array_filter(Factory::exportable($users->toArray()), function ($user) {
return ($user->enabled != 'no' && $user->banned != 'yes');
});
$response['users'] = $users;
$response['load-next'] = $pagingToken;
return Factory::response($response);
}
......
<?php
namespace Minds\Controllers\api\v2\analytics;
use Minds\Api\Factory;
use Minds\Core;
use Minds\Core\Session;
use Minds\Core\Di\Di;
use Minds\Common\Cookie;
use Minds\Entities;
use Minds\Helpers\Counters;
use Minds\Interfaces;
class dashboards implements Interfaces\Api
{
public function get($pages)
{
$dashboardsManager = Di::_()->get('Analytics\Dashboards\Manager');
$id = $pages[0] ?? 'unknown';
$dashboard = $dashboardsManager->getDashboardById($id);
$dashboard->setUser(Session::getLoggedInUser());
if (isset($_GET['timespan'])) {
$dashboard->setTimespanId($_GET['timespan']);
}
if (isset($_GET['filter'])) {
$filterIds = explode(',', $_GET['filter']);
$dashboard->setFilterIds($filterIds);
}
if (isset($_GET['metric'])) {
$dashboard->setMetricId($_GET['metric']);
}
return Factory::response([
'dashboard' => $dashboard->export(),
]);
}
public function post($pages)
{
return Factory::response([]);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -8,6 +8,7 @@ namespace Minds\Controllers\api\v2\pro;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain as ProDomain;
use Minds\Core\Pro\Manager;
use Minds\Core\Session;
use Minds\Entities\User;
......@@ -81,6 +82,18 @@ class settings implements Interfaces\Api
]);
}
if (isset($_POST['domain'])) {
/** @var ProDomain $proDomain */
$proDomain = Di::_()->get('Pro\Domain');
if (!$proDomain->isAvailable($_POST['domain'], (string) $user->guid)) {
return Factory::response([
'status' => 'error',
'message' => 'This domain is taken',
]);
}
}
try {
$success = $manager->set($_POST);
......
<?php
/**
* settings assets
* @author edgebal
*/
namespace Minds\Controllers\api\v2\pro\settings;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Manager;
use Minds\Core\Pro\Assets\Manager as AssetsManager;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
use Zend\Diactoros\ServerRequest;
class assets implements Interfaces\Api
{
/** @var ServerRequest */
public $request;
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
*/
public function get($pages)
{
return Factory::response([]);
}
/**
* Equivalent to HTTP POST method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function post($pages)
{
$type = $pages[0] ?? null;
// Check and validate user
$user = Session::getLoggedinUser();
if (isset($pages[1]) && $pages[1]) {
if (!Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not authorized',
]);
}
$user = new User($pages[1]);
}
// Check uploaded file
/** @var \Zend\Diactoros\UploadedFile[] $files */
$files = $this->request->getUploadedFiles();
if (!$files || !isset($files['file'])) {
return Factory::response([
'status' => 'error',
'message' => 'Missing file',
]);
}
$file = $files['file'];
if ($file->getError()) {
return Factory::response([
'status' => 'error',
'message' => sprintf('Error %s when uploading file', $files['file']->getError()),
]);
}
// Get Pro managers
/** @var Manager $manager */
$manager = Di::_()->get('Pro\Manager');
$manager
->setUser($user)
->setActor(Session::getLoggedinUser());
if (!$manager->isActive()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not Pro',
]);
}
/** @var AssetsManager $assetsManager */
$assetsManager = Di::_()->get('Pro\Assets\Manager');
$assetsManager
->setType($type)
->setUser($user)
->setActor(Session::getLoggedinUser());
try {
$success = $assetsManager
->set($file);
if (!$success) {
throw new Exception(sprintf("Cannot save Pro %s asset", $type));
}
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
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
/**
* domain
* @author edgebal
*/
namespace Minds\Controllers\api\v2\pro\settings;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Pro\Domain as ProDomain;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Interfaces;
use Minds\Api\Factory;
class domain implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws Exception
*/
public function get($pages)
{
$user = Session::getLoggedinUser();
if (isset($pages[0]) && $pages[0]) {
if (!Session::isAdmin()) {
return Factory::response([
'status' => 'error',
'message' => 'You are not authorized',
]);
}
$user = new User($pages[0]);
}
/** @var ProDomain $proDomain */
$proDomain = Di::_()->get('Pro\Domain');
return Factory::response([
'isValid' => $proDomain->isAvailable($_GET['domain'], (string) $user->guid)
]);
}
/**
* 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
namespace Minds\Controllers\Api\v2\settings;
namespace Minds\Controllers\api\v2\settings;
use Minds\Api\Factory;
use Minds\Core;
......
<?php
namespace Minds\Controllers\Api\v2\settings;
namespace Minds\Controllers\api\v2\settings;
use Minds\Api\Factory;
use Minds\Core;
......
<?php
/**
* pro
* @author edgebal
*/
namespace Minds\Controllers\fs\v1;
use Minds\Core\Pro\Assets\Asset;
use Minds\Interfaces;
class pro implements Interfaces\FS
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @return mixed|null
* @throws \IOException
* @throws \InvalidParameterException
* @throws \Exception
*/
public function get($pages)
{
$asset = new Asset();
$asset
->setType($pages[1] ?? null)
->setUserGuid($pages[0] ?? null);
$file = $asset->getFile();
$file->open('read');
$contents = $file->read();
header(sprintf("Content-Type: %s", $asset->getMimeType()));
header(sprintf("Expires: %s", date('r', time() + 864000)));
header('Pragma: public');
header('Cache-Control: public');
echo $contents;
exit;
}
}
......@@ -16,5 +16,9 @@ class AnalyticsProvider extends Provider
$this->di->bind('Analytics\Graphs\Repository', function ($di) {
return new Graphs\Repository();
}, ['useFactory' => true]);
$this->di->bind('Analytics\Dashboards\Manager', function ($di) {
return new Dashboards\Manager();
}, ['useFactory' => true]);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards;
interface DashboardCollectionInterface
{
/**
* Export everything in the collection
* @param array $extras
* @return array
*/
public function export(array $extras = []): array;
}
<?php
namespace Minds\Core\Analytics\Dashboards;
interface DashboardInterface
{
/**
* Build the dashboard
* NOTE: return type not specified due to php
* having terrible typing support
* @return self
*/
public function build();
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array;
}
<?php
/**
* Earnings Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method EarningsDashboard setTimespanId(string $timespanId)
* @method EarningsDashboard setFilterIds(array $filtersIds)
* @method EarningsDashboard setUser(User $user)
*/
class EarningsDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'active_users';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\Earnings\TotalEarningsMetric(),
new Metrics\Earnings\ViewsEarningsMetric(),
new Metrics\Earnings\ReferralsEarningsMetric(),
new Metrics\Earnings\SalesEarningsMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'earnings',
'label' => 'Pro Earnings',
'description' => 'Earnings for PRO members will be paid out within 30 days upon reaching a minumum balance of $100.00.',
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
abstract class AbstractFilter
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $description;
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var FilterOptions */
protected $options;
/** @var string */
protected $selectedOption;
/**
* Set the selected option and toggle if selected
* @param string $selectedOptionId
* @return self
*/
public function setSelectedOption(string $selectedOptionId): self
{
$this->selectedOption = $selectedOptionId;
foreach ($this->options->getOptions() as $k => $option) {
if ($option->getId() === $selectedOptionId) {
$option->setSelected(true);
}
}
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'description' => (string) $this->description,
'options' => (array) $this->options->export(),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class ChannelFilter extends AbstractFilter
{
/** @var string */
protected $id = "channel";
/** @var string */
protected $label = "Channel";
/** @var array */
protected $permissions = [ 'admin' ];
/** @var string */
protected $description = "Filter by channels or by the full site";
/** @var string */
protected $selectedOption = "all";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("all")
->setLabel("All")
->setDescription("Global, site-wide metrics"),
(new FilterOptionsOption())
->setId("self")
->setLabel("Me")
->setDescription("Your currently logged in user"),
(new FilterOptionsOption())
->setId("custom")
->setLabel("Custom (Search)")
->setDescription("Search for a channel to view their metrics")
);
}
}
<?php
/**
*
*/
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
class FilterOptions
{
use MagicAttributes;
/** @var FilterOptionsOption[] */
private $options = [];
/**
* Set options
* @param FilterOptionsOption $options
* @return self
*/
public function setOptions(FilterOptionsOption ...$options): self
{
$this->options = $options;
return $this;
}
/**
* Export
* @param array $export
* @return array
*/
public function export(array $export = []): array
{
$options = [];
foreach ($this->options as $option) {
$options[] = $option->export();
}
return $options;
}
}
<?php
/**
*
*/
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Traits\MagicAttributes;
class FilterOptionsOption
{
use MagicAttributes;
/** @var string */
private $id;
/** @var string */
private $label;
/** @var string */
private $description;
/** @var bool */
private $available = true;
/** @var bool */
private $selected = false;
/**
* Export
* @param array $export
* @return array
*/
public function export(array $export = []): array
{
return [
'id' => $this->id,
'label' => $this->label,
'description' => $this->description,
'available' => (bool) $this->available,
'selected' => (bool) $this->selected,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
use Minds\Entities\User;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class FiltersCollection implements DashboardCollectionInterface
{
/** @var AbstractFilter[] */
private $filters = [];
/** @var string[] */
private $selectedIds;
/** @var User */
private $user;
/**
* Set the selected metric id
* @param string[]
* @return self
*/
public function setSelectedIds(array $selectedIds): self
{
$this->selectedIds = $selectedIds;
return $this;
}
/**
* Selected ids
* @return string[]
*/
public function getSelectedIds(): array
{
return $this->selectedIds;
}
/**
* @param User $user
* @return self
*/
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getSelected(): array
{
// Filters have scoped key pairs like
// key::value
// platform::browser
$selected = [];
foreach ($this->selectedIds as $selectedId) {
list($key, $value) = explode('::', $selectedId);
if (!isset($this->filters[$key])) {
continue;
}
$selected[$key] = $this->filters[$key];
$selected[$key]->setSelectedOption($value);
}
return $selected;
}
/**
* Set the filters
* @param AbstractFilter[] $filters
* @return self
*/
public function addFilters(AbstractFilter ...$filters): self
{
foreach ($filters as $filter) {
if (
in_array('admin', $filter->getPermissions(), true)
&& !$this->user->isAdmin()
&& !in_array('user', $filter->getPermissions(), true)
) {
continue;
}
$this->filters[$filter->getId()] = $filter;
}
return $this;
}
/**
* Return the set metrics
* @return AbstractFilter[]
*/
public function getFilters(): array
{
return $this->filters;
}
public function clear(): self
{
$this->filters = [];
$this->selectedIds = [];
return $this;
}
// public function build(): self
// {
// foreach ($this->filters as $filter) {
// $filter->build();
// }
// return $this;
// }
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->filters as $filter) {
$export[] = $filter->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class PlatformFilter extends AbstractFilter
{
/** @var string */
protected $id = "platform";
/** @var string */
protected $label = "Platform";
/** @var string */
protected $description = "Filter by device types";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("all")
->setLabel("All")
->setDescription("Browsers, Mobile and APIs"),
(new FilterOptionsOption())
->setId("browser")
->setLabel("Browser")
->setDescription("Browsers"),
(new FilterOptionsOption())
->setId("mobile")
->setLabel("Mobile")
->setDescription("Native mobile applications")
);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Filters;
class ViewTypeFilter extends AbstractFilter
{
/** @var string */
protected $id = "view_type";
/** @var string */
protected $label = "View types";
/** @var string */
protected $description = "Filter by the breakdown of views";
public function __construct()
{
$this->options = (new FilterOptions())
->setOptions(
(new FilterOptionsOption())
->setId("total")
->setLabel("Total")
->setDescription("All views recorded on assets"),
(new FilterOptionsOption())
->setId("organic")
->setLabel("Organic")
->setDescription("Views on assets that excludes boosted impressions"),
(new FilterOptionsOption())
->setId("boosted")
->setLabel("Boosted")
->setDescription("Views recorded on assets that were boosted"),
(new FilterOptionsOption())
->setId("single")
->setLabel("Pageview")
->setDecription("Views recorded on single pages, not in feeds")
);
}
}
<?php
namespace Minds\Core\Analytics\Dashboards;
class Manager
{
const DASHBOARDS = [
'traffic' => TrafficDashboard::class,
'trending' => TrendingDashboard::class,
'earnings' => EarningsDashboard::class,
];
/**
* @param string $id
* @return DashboardInterface
*/
public function getDashboardById(string $id): DashboardInterface
{
$class = self::DASHBOARDS[$id];
return new $class;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method AbstractMetric setTimespansCollection(TimespansCollection $timespansCollection)
* @method AbstractMetric setFiltersCollection(FiltersCollection $filtersCollection)
* @method string getId()
* @method string getLabel()
* @method MetricSummary getSummary()
* @method array getPermissions()
* @method AbstractMetric setUser(User $user)
*/
abstract class AbstractMetric
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $description;
/** @var string */
protected $unit = 'number';
/** @var string[] */
protected $permissions;
/** @var MetricSummary */
protected $summary;
/** @var VisualisationInterface */
protected $visualisation;
/** @var TimespansCollection */
protected $timespansCollection;
/** @var FiltersCollection */
protected $filtersCollection;
/** @var User */
protected $user;
/**
* Return the usd guid for metrics
* @return string
*/
protected function getUserGuid(): ?string
{
$filters = $this->filtersCollection->getSelected();
$channelFilter = $filters['channel'] ?? null;
if (!$channelFilter) {
if (!$this->user) {
throw new \Exception("You must be loggedin");
}
if ($this->user->isAdmin()) {
return "";
}
return $this->user->getGuid();
}
if ($channelFilter->getSelectedOption() === 'all') {
if ($this->user->isAdmin()) {
return "";
}
$channelFilter->setSelectedOption('self');
}
if ($channelFilter->getSelectedOption() === 'self') {
return $this->user->getGuid();
}
// TODO: check permissions first
return $channelFilter->getSelectedOption();
}
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'description' => (string) $this->description,
'unit' => (string) $this->unit,
'permissions' => (array) $this->permissions,
'summary' => $this->summary ? (array) $this->summary->export() : null,
'visualisation' => $this->visualisation ? (array) $this->visualisation->export() : null,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\ElasticSearch;
class ActiveUsersMetric extends AbstractMetric
{
/** @var ElasticSearch\Client */
private $es;
/** @var string */
protected $id = 'active_users';
/** @var string */
protected $label = 'Active Users';
/** @var string */
protected $description = 'Users who make at least one single request to Minds';
/** @var array */
protected $permissions = [ 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metric summary
* @return self
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// Field name to use for the aggregation
$aggField = "active::total";
// The aggregation type, this differs by resolution
$aggType = "sum";
// The resolution to use
$resolution = 'day';
switch ($timespan->getId()) {
case 'today':
$resolution = 'day';
$aggType = "sum";
break;
case '30d':
case 'mtd':
$resolution = 'month';
$aggType = "max";
break;
case '1y':
case 'ytd':
$resolution = 'month';
$aggType = "avg";
break;
}
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $resolution,
],
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
$aggType => [
'field' => $aggField,
],
],
],
],
];
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Specify the resolution to avoid duplicates
$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 1,
],
'aggs' => [
'2' => [
'sum' => [
'field' => 'active::total',
],
],
],
],
],
],
];
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
abstract class AbstractEarningsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = '';
/** @var string */
protected $label = '';
/** @var string */
protected $description = '';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $unit = 'usd';
/** @var string */
protected $aggField = '';
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $this->aggField,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ReferralsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_referrals';
/** @var string */
protected $label = 'Referrals USD';
/** @var string */
protected $description = "Total earnings for your active referrals. You earn $0.10 for every active referral. A referral must log in at least 3 of 7 days after registration to be credited.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::referrals';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class SalesEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_sales';
/** @var string */
protected $label = 'Sales USD';
/** @var string */
protected $description = "Total earnings for the sales you have referred. You earn a 25% commission when your referrals purchase Plus, Pro or Minds Tokens.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::sales';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class TotalEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_total';
/** @var string */
protected $label = 'Total Earnings';
/** @var string */
protected $description = 'Total earnings for the selected timespan.';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Earnings;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Analytics\Dashboards\Metrics\AbstractMetric;
use Minds\Core\Analytics\Dashboards\Metrics\MetricSummary;
use Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
class ViewsEarningsMetric extends AbstractEarningsMetric
{
/** @var string */
protected $id = 'earnings_views';
/** @var string */
protected $label = 'Pageviews USD';
/** @var string */
protected $description = "Total earnings for the pageviews on your channel's assets. You earn $1 for every 1,000 pageviews.";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'usd_earnings::views';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
/**
* @method MetricSummary setValue(int $value)
* @method MetricSummary setComparisonValue(int $value)
* @method MetricSummary setComparisonInterval(int $interval)
*/
class MetricSummary
{
use MagicAttributes;
/** @var int */
private $value = 0;
/** @var int */
private $comparisonValue = 0;
/** @var int */
private $comparisonInterval = 1;
/** @var bool */
private $comparisonPositivity = true;
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'current_value' => (int) $this->value,
'comparison_value' => (int) $this->comparisonValue,
'comparison_interval' => (int) $this->comparisonInterval,
'comparison_positive_inclination' => (bool) $this->comparisonPositivity,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Traits\MagicAttributes;
class MetricTimeseries
{
/** @var array */
private $dateHistogram = [];
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'date_histogram' => (array) $this->dateHistogram,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Entities\User;
use Minds\Core\Di\Di;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class MetricsCollection implements DashboardCollectionInterface
{
/** @var AbstractMetric[] */
private $metrics = [];
/** @var string */
private $selectedId;
/** @var TimespansCollection */
private $timespansCollection;
/** @var FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
/**
* @param TimespansCollection $timespansCollection
* @return self
*/
public function setTimespansCollection(TimespansCollection $timespansCollection): self
{
$this->timespansCollection = $timespansCollection;
return $this;
}
/**
* @param FiltersCollection $filtersCollection
* @return self
*/
public function setFiltersCollection(?FiltersCollection $filtersCollection): self
{
$this->filtersCollection = $filtersCollection;
return $this;
}
/**
* @param User $user
* @return self
*/
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
/**
* Set the selected metric id
* @param string
* @return self
*/
public function setSelectedId(string $selectedId): self
{
$this->selectedId = $selectedId;
return $this;
}
/**
* Set the metrics
* @param AbstractMetric[] $metric
* @return self
*/
public function addMetrics(AbstractMetric ...$metrics): self
{
foreach ($metrics as $metric) {
if (
in_array('admin', $metric->getPermissions(), true)
&& !$this->user->isAdmin()
&& !in_array('user', $metric->getPermissions(), true)
) {
continue;
}
$metric->setUser($this->user);
$this->metrics[$metric->getId()] = $metric;
}
return $this;
}
/**
* Return the selected metric
* @return AbstractMetric
*/
public function getSelected(): AbstractMetric
{
if (!isset($this->metrics[$this->selectedId])) {
$this->selectedId = key($this->metrics);
}
return $this->metrics[$this->selectedId];
}
/**
* Return the set metrics
* @return AbstractMetric[]
*/
public function getMetrics(): array
{
return $this->metrics;
}
/**
* Build the metrics
* @return self
*/
public function build(): self
{
// Build all summaries
$this->buildSummaries();
// Build current visualisation
$this->getSelected()->buildVisualisation();
return $this;
}
public function buildSummaries(): self
{
foreach ($this->metrics as $metric) {
$metric
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->buildSummary();
}
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->metrics as $metric) {
$export[] = $metric->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
class PageviewsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'pageviews';
/** @var string */
protected $label = 'Pageviews';
/** @var string */
protected $description = "Total pageviews on all of your channel's assets. A pageview is registered when a unique page is viewed and does not include feeds.";
/** @var array */
protected $permissions = [ 'admin', 'user' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// TODO: Allow this to be changed based on supplied filters
$aggField = "views::single";
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$this->filtersCollection->clear();
// TODO: make this respect the filters
$field = "views::single";
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\Elasticsearch;
class SignupsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'signups';
/** @var string */
protected $label = 'Signups';
/** @var string */
protected $description = 'New accounts registered';
/** @var array */
protected $permissions = [ 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
if ($this->getUserGuid()) {
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$comparisonTsMs = strtotime("-{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
$aggField = "signups::total";
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
// Return our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global',
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval());
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
if ($this->getUserGuid()) {
$this->visualisation = (new Visualisations\ChartVisualisation());
return $this;
}
$timespan = $this->timespansCollection->getSelected();
$xValues = [];
$yValues = [];
// TODO: make this respect the filters
$field = "signups::total";
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Use our global metrics
$must[]['term'] = [
'entity_urn' => 'urn:metric:global'
];
// Specify the resolution to avoid duplicates
/*$must[] = [
'term' => [
'resolution' => $timespan->getInterval(),
],
];*/
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 1,
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$xValues[] = $date;
$yValues[] = $bucket['2']['value'];
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXValues($xValues)
->setYValues($yValues)
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Session;
use Minds\Core\Data\ElasticSearch;
class ViewsMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var string */
protected $id = 'views';
/** @var string */
protected $label = 'Impressions';
/** @var string */
protected $description = "Impressions on all of your channel's assets. An impression is registered when your content is displayed and includes feeds.";
/** @var array */
protected $permissions = [ 'admin', 'user' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$comparisonTsMs = strtotime("midnight -{$timespan->getComparisonInterval()} days", $timespan->getFromTsMs() / 1000) * 1000;
$currentTsMs = $timespan->getFromTsMs();
// TODO: Allow this to be changed based on supplied filters
$aggField = "views::total";
if ($filters['view_type'] ?? false) {
$aggField = "views::" . $filters['view_type']->getSelectedOption();
}
$values = [];
foreach ([ 'value' => $currentTsMs, 'comparison' => $comparisonTsMs ] as $key => $tsMs) {
$must = [];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lte' => strtotime("midnight +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'sum' => [
'field' => $aggField,
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$values[$key] = $response['aggregations']['1']['value'];
}
$this->summary = new MetricSummary();
$this->summary
->setValue($values['value'])
->setComparisonValue($values['comparison'])
->setComparisonInterval($timespan->getComparisonInterval())
->setComparisonPositivity(true);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
$xValues = [];
$yValues = [];
// TODO: make this respect the filters
$field = "views::total";
if ($filters['view_type'] ?? false) {
$field = "views::" . $filters['view_type']->getSelectedOption();
}
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'date_histogram' => [
'field' => '@timestamp',
'interval' => $timespan->getInterval(),
'min_doc_count' => 0,
'extended_bounds' => [
'min' => $timespan->getFromTsMs(),
'max' => time() * 1000,
],
],
'aggs' => [
'2' => [
'sum' => [
'field' => $field,
//'min_doc_count' => 0,
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$date = date(Visualisations\ChartVisualisation::DATE_FORMAT, $bucket['key'] / 1000);
$buckets[] = [
'key' => $bucket['key'],
'date' => date('c', $bucket['key'] / 1000),
'value' => $bucket['2']['value']
];
}
$this->visualisation = (new Visualisations\ChartVisualisation())
->setXLabel('Date')
->setYLabel('Count')
->setBuckets($buckets);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Di\Di;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Entities\Resolver;
use Minds\Common\Urn;
class ViewsTableMetric extends AbstractMetric
{
/** @var Elasticsearch\Client */
private $es;
/** @var Resolver */
private $entitiesResolver;
/** @var string */
protected $id = 'views_table';
/** @var string */
protected $label = 'Views breakdown';
/** @var string */
protected $description = 'Views by post';
/** @var array */
protected $permissions = [ 'user', 'admin' ];
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
$this->entitiesResolver = $entitiesResolver ?? new Resolver();
}
/**
* Build the metrics
* @return self
*/
public function buildSummary(): self
{
$this->summary = new MetricSummary();
$this->summary
->setValue(0)
->setComparisonValue(0)
->setComparisonInterval(null);
return $this;
}
/**
* Build a visualisation for the metric
* @return self
*/
public function buildVisualisation(): self
{
$timespan = $this->timespansCollection->getSelected();
$filters = $this->filtersCollection->getSelected();
// TODO: make this respect the filters
$field = "views::total";
if ($filters['view_type']) {
$field = "views::" . $filters['view_type']->getSelectedOption();
}
$must = [];
// Range must be from previous period
$must[]['range'] = [
'@timestamp' => [
'gte' => $timespan->getFromTsMs(),
],
];
// Specify the resolution to avoid duplicates
// $must[] = [
// 'term' => [
// 'resolution' => $timespan->getInterval(),
// ],
// ];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'entity_urn',
'min_doc_count' => 1,
'order' => [
$field => 'desc',
],
],
'aggs' => [
'views::total' => [
'sum' => [
'field' => 'views::total',
],
],
'views::organic' => [
'sum' => [
'field' => 'views::organic',
],
],
'views::single' => [
'sum' => [
'field' => 'views::single',
],
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
$buckets = [];
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
$entity = $this->entitiesResolver->single(new Urn($bucket['key']));
$buckets[] = [
'key' => $bucket['key'],
'values' => [
'entity' => $entity ? $entity->export() : null,
'views::total' => $bucket['views::total']['value'],
'views::organic' => $bucket['views::organic']['value'],
'views::single' => $bucket['views::single']['value'],
],
];
}
$this->visualisation = (new Visualisations\TableVisualisation())
->setBuckets($buckets)
->setColumns([
[
'id' => 'entity',
'label' => '',
'order' => 0,
],
[
'id' => 'views::total',
'label' => 'Total Views',
'order' => 1,
],
[
'id' => 'views::organic',
'label' => 'Organic',
'order' => 2,
],
[
'id' => 'views::single',
'label' => 'Pageviews',
'order' => 3,
]
]);
return $this;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
abstract class AbstractVisualisation implements VisualisationInterface
{
use MagicAttributes;
/** @var string */
private $type;
public function export(array $extras = []): array
{
return [
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
class ChartVisualisation extends AbstractVisualisation
{
use MagicAttributes;
const DATE_FORMAT = "d-m-Y";
/** @var string */
private $type = 'chart';
/** @var string */
private $xLabel = 'Date';
/** @var array */
private $xValues;
/** @var string */
private $yLabel = 'count';
/** @var array */
private $yValues;
/** @var array */
private $buckets = [];
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'type' => $this->type,
'segments' => [
[
'buckets' => (array) $this->buckets,
],
]
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
use Minds\Traits\MagicAttributes;
class TableVisualisation extends AbstractVisualisation
{
use MagicAttributes;
const DATE_FORMAT = "d-m-Y";
/** @var string */
private $type = 'table';
/** @var array */
private $buckets;
/** @var array */
private $columns;
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
return [
'type' => $this->type,
'buckets' => (array) $this->buckets,
'columns' => (array) $this->columns,
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Visualisations;
interface VisualisationInterface
{
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
use Minds\Traits\MagicAttributes;
/**
* @method string getId()
* @method string getLabel()
* @method string getInterval()
* @method int getComparisonInterval()
* @method int getFromTsMs()
*/
abstract class AbstractTimespan
{
use MagicAttributes;
/** @var string */
protected $id;
/** @var string */
protected $label;
/** @var string */
protected $interval;
/** @var bool */
protected $selected = false;
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 'day';
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'id' => (string) $this->id,
'label' => (string) $this->label,
'interval' => (string) $this->interval,
'selected' => (bool) $this->selected,
'comparison_interval' => (int) $this->comparisonInterval,
'from_ts_ms' => (int) $this->fromTsMs,
'from_ts_iso' => date('c', $this->fromTsMs / 1000),
];
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class MtdTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'mtd';
/** @var string */
protected $label = 'Month to date';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
$this->fromTsMs = strtotime('midnight first day of this month') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
use Minds\Core\Analytics\Dashboards\DashboardCollectionInterface;
class TimespansCollection implements DashboardCollectionInterface
{
/** @var AbstractTimespan[] */
private $timespans = [];
/** @var string */
private $selectedId;
/**
* Set the current timespan we are using
* @param string $selectedId
* @return self
*/
public function setSelectedId(string $selectedId): self
{
$this->selectedId = $selectedId;
return $this;
}
/**
* Return the selected timespan
* @return AbstractTimespan
*/
public function getSelected(): AbstractTimespan
{
return $this->timespans[$this->selectedId];
}
/**
* Set the timespans
* @param AbstractTimespan[] $timespans
* @return self
*/
public function addTimespans(AbstractTimespan ...$timespans): self
{
foreach ($timespans as $timespan) {
$this->timespans[$timespan->getId()] = $timespan;
}
return $this;
}
/**
* Return the set timestamps
* @return TimestampAbstract[]
*/
public function getTimespans(): array
{
return $this->timespans;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$export = [];
foreach ($this->timespans as $timespan) {
if ($timespan->getId() === $this->selectedId) {
$timespan->setSelected(true);
}
$export[] = $timespan->export();
}
return $export;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
/**
* @method string getId()
* @method string getLabel()
* @method string getInterval()
*/
class TodayTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'today';
/** @var string */
protected $label = 'Today';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 1;
public function __construct()
{
$this->fromTsMs = strtotime('midnight') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class YtdTimespan extends AbstractTimespan
{
/** @var string */
protected $id = 'ytd';
/** @var string */
protected $label = 'Year to date';
/** @var string */
protected $interval = 'month';
/** @var int */
protected $fromTsMs;
/** @var string */
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight first day of January') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _1yTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '1y';
/** @var string */
protected $label = 'Last 12 months';
/** @var string */
protected $interval = 'month';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 365;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 365 days ago') * 1000;
}
}
<?php
namespace Minds\Core\Analytics\Dashboards\Timespans;
class _30dTimespan extends AbstractTimespan
{
/** @var string */
protected $id = '30d';
/** @var string */
protected $label = 'Last 30 days';
/** @var string */
protected $interval = 'day';
/** @var int */
protected $fromTsMs;
/** @var int */
protected $comparisonInterval = 28;
public function __construct()
{
$this->fromTsMs = strtotime('midnight 30 days ago') * 1000;
}
}
<?php
/**
* Traffic Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method TrafficDashboard setTimespanId(string $timespanId)
* @method TrafficDashboard setFilterIds(array $filtersIds)
* @method TrafficDashboard setUser(User $user)
*/
class TrafficDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'active_users';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\ActiveUsersMetric(),
new Metrics\SignupsMetric(),
new Metrics\PageviewsMetric(),
new Metrics\ViewsMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'traffic',
'label' => 'Traffic',
'description' => null,
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
<?php
/**
* Trending Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method TrendingDashboard setTimespanId(string $timespanId)
* @method TrendingDashboard setFilterIds(array $filtersIds)
* @method TrendingDashboard setUser(User $user)
*/
class TrendingDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'views_table';
/** @var Timespans\TimespansCollection */
private $timespansCollection;
/** @var Metrics\MetricsCollection */
private $metricsCollection;
/** @var Filters\FiltersCollection */
private $filtersCollection;
/** @var User */
private $user;
public function __construct(
$timespansCollection = null,
$metricsCollection = null,
$filtersCollection = null
) {
$this->timespansCollection = $timespansCollection ?? new Timespans\TimespansCollection();
$this->metricsCollection = $metricsCollection ?? new Metrics\MetricsCollection();
$this->filtersCollection = $filtersCollection ?? new Filters\FiltersCollection();
}
/**
* Build the dashboard
* @return self
*/
public function build(): self
{
$this->timespansCollection
->setSelectedId($this->timespanId)
->addTimespans(
new Timespans\TodayTimespan(),
new Timespans\_30dTimespan(),
new Timespans\_1yTimespan(),
new Timespans\MtdTimespan(),
new Timespans\YtdTimespan()
);
$this->filtersCollection
->setSelectedIds($this->filterIds)
->setUser($this->user)
->addFilters(
// new Filters\PlatformFilter(),
new Filters\ViewTypeFilter(),
new Filters\ChannelFilter()
);
$this->metricsCollection
->setTimespansCollection($this->timespansCollection)
->setFiltersCollection($this->filtersCollection)
->setSelectedId($this->metricId)
->setUser($this->user)
->addMetrics(
new Metrics\ViewsTableMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'trending',
'label' => 'Trending',
'description' => null,
'timespan' => $this->timespansCollection->getSelected()->getId(),
'timespans' => $this->timespansCollection->export(),
'metric' => $this->metricsCollection->getSelected()->getId(),
'metrics' => $this->metricsCollection->export(),
'filter' => $this->filtersCollection->getSelectedIds(),
'filters' => $this->filtersCollection->export(),
];
}
}
......@@ -60,4 +60,12 @@ class EntityCentricRecord
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
/**
* @return string
*/
public function getUrn(): string
{
return (string) implode('-', [ $this->getEntityUrn(), $this->getResolution(), $this->getTimestamp() ]);
}
}
......@@ -13,6 +13,7 @@ class Manager
{
/** @var array */
const SYNCHRONISERS = [
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
......@@ -21,6 +22,9 @@ class Manager
/** @var Repository */
protected $repository;
/** @var Sums */
protected $sums;
/** @var int */
private $from;
......@@ -28,9 +32,11 @@ class Manager
private $to;
public function __construct(
$repository = null
$repository = null,
$sums = null
) {
$this->repository = $repository ?: new Repository();
$this->repository = $repository ?? new Repository();
$this->sums = $sums ?? new Sums();
}
/**
......@@ -81,4 +87,13 @@ class Manager
public function getAggregateByQuery(array $query): array
{
}
/**
* @param array $opts
* @retun iterable
*/
public function getListAggregatedByOwner(array $opts = []): iterable
{
return $this->sums->getByOwner($opts);
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Monetization\Partners\Manager as PartnersManager;
use DateTime;
use Exception;
class PartnerEarningsSynchroniser
{
/** @var PartnersManager */
private $partnersManager;
public function __construct($partnersManager = null)
{
$this->partnersManager = $partnersManager ?? new PartnersManager;
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$opts = [];
$opts['from'] = $this->from;
$records = [];
$i = 0;
while (true) {
$result = $this->partnersManager->getList($opts);
$opts['offset'] = $result->getPagingToken();
foreach ($result as $deposit) {
$urn = "urn:user:{$deposit->getUserGuid()}";
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($deposit->getUserGuid())
->setTimestamp($deposit->getTimestamp()) // TODO: confirm if this should be rounded to midnight
->setResolution('day');
// In order to increment sums, replace with what has already been seen
if (isset($records[$record->getUrn()])) {
$record = $records[$record->getUrn()];
}
$record->incrementSum('usd_earnings::total', $deposit->getAmountCents());
$record->incrementSum("usd_earnings::{$deposit->getItem()}", $deposit->getAmountCents());
$records[$record->getUrn()] = $record;
}
if ($result->isLastPage()) {
break;
}
}
foreach ($records as $record) {
var_dump($record);
yield $record;
}
}
}
<?php
/**
* EntityCentric Sums
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use DateTime;
use DateTimeZone;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\ElasticSearch\Client as ElasticClient;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
class Sums
{
/** @var ElasticClient */
protected $es;
/**
* Repository constructor.
* @param ElasticClient $es
*/
public function __construct(
$es = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
}
public function getByOwner(array $opts = []): iterable
{
$opts = array_merge([
'fields' => [],
'from' => time(),
], $opts);
$must = [];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $opts['from'] * 1000,
'lt' => strtotime('+1 day', $opts['from']) * 1000,
],
],
];
$termsAgg = [];
foreach ($opts['fields'] as $field) {
$termsAgg[$field] = [
'sum' => [
'field' => $field,
],
];
$must[] = [
'exists' => [
'field' => $field,
],
];
}
$partition = 0;
$partitions = 100;
$partitionSize = 5000; // Allows for 500,000 users
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'owner_guid',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
'aggs' => $termsAgg,
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
}
......@@ -49,7 +49,7 @@ class Retention implements AnalyticsMetric
$startTs = $timestamps[$x+1];
$signups = [];
$offset = "";
echo "\n Gathering signups \n";
// echo "\n Gathering signups \n";
while (true) {
$data = $this->db->getRow("analytics:signup:day:$startTs", ['limit'=>200, 'offset' => $offset]);
if (count($data) <= 1) {
......@@ -61,13 +61,13 @@ class Retention implements AnalyticsMetric
$offset = $k;
}
}
echo " (done)";
// echo " (done)";
//now get active users from each interval after this date
$endTs = $timestamps[$x-$x+1];
//echo "[$x]:: actives: " . date('d-m-Y', $endTs) . " signups: " . date('d-m-Y', $startTs) . "\n";
$offset = "";
echo "\n Gathering actives \n";
// echo "\n Gathering actives \n";
foreach ($signups as $signup => $ts) {
if ($this->wasActive($signup, $now)) {
$this->db->insert("{$this->namespace}:$x:$now", [$signup=>time()]);
......@@ -76,7 +76,7 @@ class Retention implements AnalyticsMetric
echo "\r $x: $signup (not active) $offset";
}
}
echo "(done)";
// echo "(done)";
}
return true;
......
......@@ -37,4 +37,15 @@ class Custom implements Interfaces\PreparedInterface
{
return $this->opts;
}
/**
* Gets the template of the custom query
* e.g. "SELECT * FROM friendsof WHERE column1 = ?"
*
* @return string the template.
*/
public function getTemplate(): string
{
return $this->template;
}
}
You've received a gift of 1 Minds token! You can spend this token to earn 1,000 extra views on your content with [Boost](https://www.minds.com/boost?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) or to tip your favorite content creators with [Wire](https://www.minds.com/wire?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>).
Please use the button below to claim your gift (note: you will need to open the link in a web browser, the mobile app is not yet supported):
| |
|:--:|
| [![Claim Gift](https://cdn-assets.minds.com/emails/claim-gift.png){=150x}](https://www.minds.com/wallet/tokens/transactions?__e_ct_guid=<?= $vars['guid']?>&campaign=<?= $vars['campaign']?>&topic=<?= $vars['topic'] ?>&validator=<?= $vars['validator'] ?>) |
| |
......@@ -49,6 +49,17 @@ class Manager
return $this->image->getImageBlob();
}
public function getPng()
{
if (!$this->image) {
throw new \Exception('Output was not generated');
}
$this->image->setImageFormat('png');
return $this->image->getImageBlob();
}
/**
* @param $value
* @return $this
......@@ -61,6 +72,14 @@ class Manager
return $this;
}
public function setImageFromBlob($blob, $fileName = null)
{
$this->image = new \Imagick();
$this->image->readImageBlob($blob, $fileName);
return $this;
}
/**
* @return $this
*/
......
<?php
/**
* EarningsDeposit
*/
namespace Minds\Core\Monetization\Partners;
use Minds\Traits\MagicAttributes;
class EarningsDeposit
{
use MagicAttributes;
/** @var string */
private $userGuid;
/** @var int */
private $timestamp;
/** @var string */
private $item;
/** @var int */
private $amountCents;
/** @var int */
private $amountTokens;
/**
* Export
* @param array $extras
* @return array
*/
public function export($extras = []): array
{
return [
'user_guid' => $this->userGuid,
'timestamp' => $this->timestamp,
'item' => $this->item,
'amount_cents' => (int) $this->amountCents,
'amount_usd' => (int) $this->amountCents / 100,
'amount_tokens' => (string) $this->amountTokens,
];
}
}
<?php
/**
* Manager
*/
namespace Minds\Core\Monetization\Partners;
use Minds\Core\Analytics\EntityCentric\Manager as EntityCentricManager;
use Minds\Core\EntitiesBuilder;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use DateTime;
class Manager
{
const VIEWS_RPM_CENTS = 100; // $1 USD
/** @var Repository */
private $repository;
/** @var EntityCentricManager */
private $entityCentricManager;
/** @var EntitiesBuilder */
private $entitiesBuilder;
public function __construct($repository = null, $entityCentricManager = null, $entitiesBuilder = null)
{
$this->repository = $repository ?? new Repository();
$this->entityCentricManager = $entityCentricManager ?? new EntityCentricManager();
$this->entitiesBuilder = $entitiesBuilder ?? Di::_()->get('EntitiesBuilder');
}
/**
* Return a list of earning deposits
* @param array $opts
* @return Response
*/
public function getList(array $opts = []): Response
{
return $this->repository->getList($opts);
}
/**
* Add an earnings deposit
* @param EarningsDeposit $deposit
* @return bool
*/
public function add(EarningsDeposit $deposit): bool
{
$this->repository->add($deposit);
return true;
}
/**
* @param array $opts
* @return iterable
*/
public function issueDeposits(array $opts = []): iterable
{
$opts = array_merge([
'from' => strtotime('midnight'),
], $opts);
$users = [];
foreach ($this->entityCentricManager->getListAggregatedByOwner([
'fields' => [ 'views::single' ],
'from' => $opts['from'],
]) as $ownerSum) {
$views = $ownerSum['views::single']['value'];
$amountCents = ($views / 1000) * static::VIEWS_RPM_CENTS;
if ($amountCents < 1) { // Has to be at least 1 cent / $0.01
continue;
}
// Is this user in the pro program?
$owner = $this->entitiesBuilder->single($ownerSum['key']);
if (!$owner || !$owner->isPro()) {
continue;
}
$deposit = new EarningsDeposit();
$deposit->setTimestamp($opts['from'])
->setUserGuid($ownerSum['key'])
->setAmountCents($amountCents)
->setItem("views");
$this->repository->add($deposit);
yield $deposit;
}
}
}
<?php
namespace Minds\Core\Monetization\Partners;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom as Prepared;
use Minds\Common\Repository\Response;
use Minds\Core\Di\Di;
use Cassandra\Bigint;
use Cassandra\Timestamp;
class Repository
{
/** @var Client */
private $db;
public function __construct($db = null)
{
$this->db = $db ?? Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = []): Response
{
$opts = array_merge([
'from' => null,
'to' => null,
'user_guid' => null,
'allow_filtering' => false,
], $opts);
$statement = "SELECT * FROM partner_earnings_ledger";
$values = [];
$where = [];
if ($opts['user_guid']) {
$where[] = "user_guid = ?";
$values[] = new Bigint($opts['user_guid']);
}
if ($opts['from']) {
$where[] = "timestamp >= ?";
$values[] = new Timestamp($opts['from']);
if (!$opts['user_guid']) { // This is a temporary work around (MH)
$opts['allow_filtering'] = true;
}
}
if ($opts['to']) {
$where[] = "timestamp < ?";
$values[] = new Timestamp($opts['to']);
}
$statement .= " WHERE " . implode(' ', $where);
if ($opts['allow_filtering']) {
$statement .= " ALLOW FILTERING";
}
$prepared = new Prepared();
$prepared->query($statement, $values);
$result = $this->db->request($prepared);
if (!$result || !$result[0]) {
return (new Response())->setLastPage(true);
}
$response = new Response();
foreach ($result as $row) {
$deposit = new EarningsDeposit();
$deposit->setTimestamp($row['timestamp']->time())
->setItem($row['item'])
->setUserGuid((string) $row['user_guid'])
->setAmountCents($row['amount_cents'])
->setAmountTokens($row['amount_tokens'] ? $row['amount_tokens']->value() : 0);
$response[] = $deposit;
}
$response->setLastPage($result->isLastPage());
return $response;
}
/**
* @param string $urn
* @return EarningsDeposit
*/
public function get($urn): EarningsDeposit
{
// TODO
}
/**
* @param EarningsDeposit $deposit
* @return bool
*/
public function add(EarningsDeposit $deposit): bool
{
$prepared = new Prepared();
$prepared->query("INSERT INTO partner_earnings_ledger (user_guid, timestamp, item, amount_cents, amount_tokens) VALUES (?,?,?,?,?)",
[
new Bigint($deposit->getUserGuid()),
new Timestamp($deposit->getTimestamp()),
$deposit->getItem(),
$deposit->getAmountCents() ? (int) $deposit->getAmountCents() : null,
$deposit->getAmountTokens() ? new Bigint($deposit->getAmountTokens()) : null,
]);
return (bool) $this->db->request($prepared);
}
/**
* @param EarningsDeposit $deposit
* @param array $fields
* @return bool
*/
public function update(EarningsDeposit $deposit, $fields = []): bool
{
}
/**
* @param EarningsDeposit $deposit
* @return bool
*/
public function delete(EarningsDeposit $deposit): bool
{
}
}
......@@ -13,6 +13,7 @@ class Subscription
{
private $stripe;
private $repo;
/** @var User */
protected $user;
/** @var Manager $subscriptionsManager */
protected $subscriptionsManager;
......@@ -44,13 +45,15 @@ class Subscription
*/
public function isActive()
{
$subscription = $this->getSubscription();
if (!$subscription) {
return false;
}
return $this->user->isPlus();
}
return $subscription->getStatus() == 'active';
/**
* @return bool
*/
public function canBeCancelled()
{
return ((int) $this->user->plus_expires) > time();
}
/**
......
<?php
/**
* Info
* @author edgebal
*/
namespace Minds\Core\Pro\Assets;
use ElggFile;
use Exception;
use Minds\Traits\MagicAttributes;
/**
* Class Asset
* @package Minds\Core\Pro\Assets
* @method string getType()
* @method int|string getUserGuid()
* @method Asset setUserGuid(int|string $userGuid)
*/
class Asset
{
use MagicAttributes;
/** @var string */
protected $type;
/** @var int|string */
protected $userGuid;
/** @var string[] */
const TYPES = ['logo', 'background'];
/**
* @param string $type
* @return Asset
* @throws Exception
*/
public function setType(string $type): Asset
{
if (!in_array($type, static::TYPES, true)) {
throw new Exception('Invalid Asset type');
}
$this->type = $type;
return $this;
}
/**
* @return string
* @throws Exception
*/
public function getExt(): string
{
switch ($this->type) {
case 'logo':
return 'png';
case 'background':
return 'jpg';
}
throw new Exception('Invalid Asset');
}
/**
* @return string
* @throws Exception
*/
public function getMimeType(): string
{
switch ($this->type) {
case 'logo':
return 'image/png';
case 'background':
return 'image/jpg';
}
throw new Exception('Invalid Asset');
}
/**
* @return ElggFile
* @throws Exception
*/
public function getFile(): ElggFile
{
$file = new ElggFile();
$file->owner_guid = $this->userGuid;
$file->setFilename(sprintf("pro/%s.%s", $this->type, $this->getExt()));
return $file;
}
}
<?php
/**
* Manager
* @author edgebal
*/
namespace Minds\Core\Pro\Assets;
use ElggFile;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Media\Imagick\Manager as ImageManager;
use Minds\Entities\User;
use Zend\Diactoros\UploadedFile;
class Manager
{
/** @var ImageManager */
protected $imageManager;
/** @var string */
protected $type;
/** @var User */
protected $user;
/** @var User */
protected $actor;
/**
* Manager constructor.
* @param ImageManager $imageManager
*/
public function __construct(
$imageManager = null
) {
$this->imageManager = $imageManager ?: Di::_()->get('Media\Imagick\Manager');
}
/**
* @param string $type
* @return Manager
*/
public function setType(string $type): Manager
{
$this->type = $type;
return $this;
}
/**
* @param User $user
* @return Manager
*/
public function setUser(User $user): Manager
{
$this->user = $user;
return $this;
}
/**
* @param User $actor
* @return Manager
*/
public function setActor(User $actor): Manager
{
$this->actor = $actor;
return $this;
}
/**
* @param UploadedFile $file
* @param Asset|null $asset
* @return bool
* @throws Exception
*/
public function set(UploadedFile $file, Asset $asset = null)
{
if (!$this->user) {
throw new Exception('Invalid user');
} elseif (!$this->type || !in_array($this->type, Asset::TYPES, true)) {
throw new Exception('Invalid asset type');
}
// Load image
$this->imageManager
->setImageFromBlob(
$file->getStream()->getContents(),
$file->getClientFilename()
);
// Setup asset
if (!$asset) {
$asset = new Asset();
}
$asset
->setType($this->type)
->setUserGuid($this->user->guid);
// Handle asset type
switch ($this->type) {
case 'logo':
$blob = $this->imageManager
->resize(1920, 1080, false, false) // Max: 2K
->getPng();
break;
case 'background':
$blob = $this->imageManager
->autorotate()
->resize(3840, 2160, false, false) // Max: 4K
->getJpeg(85);
break;
default:
throw new Exception('Invalid asset type handler');
}
$file = $asset->getFile();
$file->open('write');
$file->write($blob);
$file->close();
return true;
}
}
......@@ -43,10 +43,11 @@ class HydrateSettingsDelegate
public function onGet(User $user, Settings $settings): Settings
{
try {
$logoImage = $settings->getLogoGuid() ? sprintf(
'%sfs/v1/thumbnail/%s/master',
$logoImage = $settings->hasCustomLogo() ? sprintf(
'%sfs/v1/pro/%s/logo/%s',
$this->config->get('cdn_url'),
$settings->getLogoGuid()
$settings->getUserGuid(),
$settings->getTimeUpdated()
) : $user->getIconURL('large');
if ($logoImage) {
......@@ -58,17 +59,32 @@ class HydrateSettingsDelegate
}
try {
$carousels = $this->entitiesBuilder->get(['subtype' => 'carousel', 'owner_guid' => (string) $user->guid]);
$carousel = $carousels[0] ?? null;
$backgroundImage = null;
if ($carousel) {
$settings
->setBackgroundImage(sprintf(
if ($settings->hasCustomBackground()) {
$backgroundImage = sprintf(
'%sfs/v1/pro/%s/background/%s',
$this->config->get('cdn_url'),
$settings->getUserGuid(),
$settings->getTimeUpdated()
);
} else {
$carousels = $this->entitiesBuilder->get(['subtype' => 'carousel', 'owner_guid' => (string) $user->guid]);
$carousel = $carousels[0] ?? null;
if ($carousel) {
$backgroundImage = sprintf(
'%sfs/v1/banners/%s/fat/%s',
$this->config->get('cdn_url'),
$carousel->guid,
$carousel->last_updated
));
);
}
}
if ($backgroundImage) {
$settings
->setBackgroundImage($backgroundImage);
}
} catch (\Exception $e) {
error_log($e);
......@@ -96,6 +112,7 @@ class HydrateSettingsDelegate
error_log($e);
}
$settings->setPublished($user->isProPublished());
return $settings;
}
}
......@@ -9,6 +9,7 @@ namespace Minds\Core\Pro;
use Exception;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Util\StringValidator;
use Minds\Entities\User;
class Domain
......@@ -49,6 +50,27 @@ class Domain
])->first();
}
/**
* @param string $domain
* @param string $userGuid
* @return bool|null
*/
public function isAvailable(string $domain, string $userGuid): ?bool
{
$rootDomains = $this->config->get('pro')['root_domains'] ?? [];
if (in_array(strtolower($domain), $rootDomains, true)) {
return false;
}
if (!StringValidator::isDomain($domain)) {
return null;
}
$settings = $this->lookup($domain);
return !$settings || ((string) $settings->getUserGuid() === $userGuid);
}
/**
* @param Settings $settings
* @param User|null $owner
......
......@@ -258,18 +258,6 @@ class Manager
->setTileRatio($values['tile_ratio']);
}
if (isset($values['logo_guid']) && $values['logo_guid'] !== '') {
$image = $this->entitiesBuilder->single($values['logo_guid']);
// if the image doesn't exist or the guid doesn't correspond to an image
if (!$image || ($image->type !== 'object' || $image->subtype !== 'image')) {
throw new \Exception('logo_guid must be a valid image guid');
}
$settings
->setLogoGuid(trim($values['logo_guid']));
}
if (isset($values['footer_text'])) {
$footer_text = trim($values['footer_text']);
......@@ -322,6 +310,25 @@ class Manager
->setCustomHead($values['custom_head']);
}
if (isset($values['has_custom_logo'])) {
$settings
->setHasCustomLogo((bool) $values['has_custom_logo']);
}
if (isset($values['has_custom_background'])) {
$settings
->setHasCustomBackground((bool) $values['has_custom_background']);
}
if (isset($values['published'])) {
$this->user->setProPublished($values['published']);
$this->saveAction
->setEntity($this->user)
->save();
}
$settings->setTimeUpdated(time());
$this->setupRoutingDelegate
->onUpdate($settings);
......
......@@ -39,5 +39,9 @@ class ProProvider extends Provider
$this->di->bind('Pro\Channel\Manager', function ($di) {
return new Channel\Manager();
}, ['useFactory' => true]);
$this->di->bind('Pro\Assets\Manager', function ($di) {
return new Assets\Manager();
}, ['useFactory' => true]);
}
}
......@@ -92,13 +92,15 @@ class Repository
->setTextColor($data['text_color'] ?? '')
->setPrimaryColor($data['primary_color'] ?? '')
->setPlainBackgroundColor($data['plain_background_color'] ?? '')
->setLogoGuid($data['logo_guid'] ?? '')
->setTileRatio($data['tile_ratio'] ?? '')
->setFooterText($data['footer_text'] ?? '')
->setFooterLinks($data['footer_links'] ?? [])
->setTagList($data['tag_list'] ?? [])
->setScheme($data['scheme'] ?? '')
->setCustomHead($data['custom_head'] ?? '')
->setHasCustomLogo($data['has_custom_logo'] ?? false)
->setHasCustomBackground($data['has_custom_background'] ?? false)
->setTimeUpdated($data['time_updated'] ?? 0)
;
$response[] = $settings;
......@@ -140,12 +142,14 @@ class Repository
'primary_color' => $settings->getPrimaryColor(),
'plain_background_color' => $settings->getPlainBackgroundColor(),
'tile_ratio' => $settings->getTileRatio(),
'logo_guid' => $settings->getLogoGuid(),
'footer_text' => $settings->getFooterText(),
'footer_links' => $settings->getFooterLinks(),
'tag_list' => $settings->getTagList(),
'scheme' => $settings->getScheme(),
'custom_head' => $settings->getCustomHead(),
'has_custom_logo' => $settings->hasCustomLogo(),
'has_custom_background' => $settings->hasCustomBackground(),
'time_updated' => $settings->getTimeUpdated(),
]),
];
......
......@@ -28,8 +28,6 @@ use Minds\Traits\MagicAttributes;
* @method Settings setPlainBackgroundColor(string $plainBackgroundColor)
* @method string getTileRatio()
* @method Settings setTileRatio(string $tileRatio)
* @method int|string getLogoGuid()
* @method Settings setLogoGuid(int|string $logoGuid)
* @method string getFooterText()
* @method Settings setFooterText(string $footerText)
* @method array getFooterLinks()
......@@ -46,6 +44,14 @@ use Minds\Traits\MagicAttributes;
* @method Settings setFeaturedContent(array $featuredContent)
* @method string getCustomHead()
* @method Settings setCustomHead(string $customHead)
* @method bool isPublished()
* @method Settings setPublished(bool $published)
* @method bool hasCustomLogo()
* @method Settings setHasCustomLogo(bool $customLogo)
* @method bool hasCustomBackground()
* @method Settings setHasCustomBackground(bool $customBackground)
* @method int getTimeUpdated()
* @method Settings setTimeUpdated(int $timeUpdated)
*/
class Settings implements JsonSerializable
{
......@@ -90,14 +96,17 @@ class Settings implements JsonSerializable
/** @var string */
protected $plainBackgroundColor;
/** @var int */
protected $logoGuid;
/** @var string */
protected $tileRatio = '16:9';
/** @var bool */
protected $hasCustomBackground;
/** @var string */
protected $backgroundImage;
/** @var string */
protected $tileRatio = '16:9';
/** @var bool */
protected $hasCustomLogo;
/** @var string */
protected $logoImage;
......@@ -120,6 +129,12 @@ class Settings implements JsonSerializable
/** @var string */
protected $customHead = '';
/** @var bool */
protected $published;
/** @var int */
protected $timeUpdated;
/**
* @return string
*/
......@@ -150,14 +165,17 @@ class Settings implements JsonSerializable
'footer_text' => $this->footerText,
'footer_links' => $this->footerLinks,
'tag_list' => $this->tagList,
'logo_guid' => (string) $this->logoGuid,
'background_image' => $this->backgroundImage,
'has_custom_logo' => $this->hasCustomLogo,
'logo_image' => $this->logoImage,
'has_custom_background' => $this->hasCustomBackground,
'background_image' => $this->backgroundImage,
'featured_content' => $this->featuredContent,
'scheme' => $this->scheme,
'custom_head' => $this->customHead,
'one_line_headline' => $this->getOneLineHeadline(),
'styles' => $this->buildStyles(),
'published' => $this->published,
'time_updated' => $this->timeUpdated,
];
}
......
......@@ -1510,3 +1510,18 @@ CREATE MATERIALIZED VIEW minds.pro_by_domain AS
WHERE user_guid IS NOT NULL AND domain IS NOT NULL
PRIMARY KEY (domain, user_guid)
WITH CLUSTERING ORDER BY (user_guid ASC);
CREATE TABLE minds.subscription_requests (
publisher_guid bigint,
subscriber_guid bigint,
timestamp timestamp,
declined boolean,
PRIMARY KEY (publisher_guid, subscriber_guid)
);
CREATE TABLE minds.notification_batches (
user_guid varint,
batch_id text,
primary key (user_guid, batch_id)
);
......@@ -355,11 +355,6 @@ class Defaults
});
$marketing = [
'plus' => [
'title' => 'Minds Plus',
'description' => 'Upgrade your channel for premium features',
'image' => 'assets/photos/fractal.jpg'
],
'wallet' => [
'title' => 'Wallet',
'description' => 'Manage all of your transactions and earnings on Minds',
......@@ -424,6 +419,21 @@ class Defaults
'title' => 'Minds Mobile App',
'description' => 'Download the Minds mobile app for Android & iOS.',
'image' => 'assets/photos/mobile-app.jpg',
],
'upgrades' => [
'title' => 'Upgrade your Minds experience',
'description' => 'Minds offers a unique range of powerful upgrades that will supercharge your experience.',
'image' => 'assets/marketing/upgrades-1.jpg',
],
'plus' => [
'title' => 'Minds Plus',
'description' => 'Upgrade your channel and unlock premium features.',
'image' => 'assets/photos/browsing-mobileapp-discovery.jpg',
],
'pro' => [
'title' => 'Minds Pro',
'description' => 'The ultimate platform for creators and brands.',
'image' => 'assets/photos/podcast-people.jpg',
]
];
......
......@@ -171,7 +171,7 @@ class EntityMapping implements MappingInterface
$fullText .= ' ' . $map['message'];
}
$htRe = '/(^|\s||)#(\w*[a-zA-Z_]+\w*)/';
$htRe = '/(^|\s||)#(\w*[a-zA-Z0-9_]+\w*)/';
$matches = [];
preg_match_all($htRe, $fullText, $matches);
......
......@@ -61,6 +61,32 @@ class Manager
$this->checkRateLimitDelegate = $checkRateLimitDelegate ?: new CheckRateLimit();
}
/**
* Gets a subscription or subscribers list from the repository.
*
* @param array $opts -
* guid - required!
* type - either 'subscribers' or 'subscriptions'.
* limit - limit.
* offset - offset.
* @return Response response objet
*/
public function getList($opts)
{
if (!$opts['guid']) {
return [];
}
$opts = array_merge([
'limit' => 12,
'offset' => '',
'guid' => '',
'type' => 'subscribers',
], $opts);
return $this->repository->getList($opts);
}
public function setSubscriber($user)
{
$this->subscriber = $user;
......
......@@ -5,9 +5,9 @@ namespace Minds\Core\Subscriptions;
use Cassandra;
use Minds\Common\Repository\Response;
use Minds\Core\Data\Cassandra\Client;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
use Minds\Core\Util\UUIDGenerator;
use Minds\Core\Data\Cassandra\Prepared;
use Minds\Entities\User;
class Repository
{
......@@ -20,17 +20,67 @@ class Repository
}
/**
* @param array $opts
* @return Response
* Gets a subscription or subscribers list from cassandra.
*
* @param array $opts -
* guid - required!
* type - either 'subscribers' or 'subscriptions'.
* limit - limit.
* offset - offset.
* @return Response response object.
*/
public function getList(array $opts = [])
public function getList(array $opts = []): Response
{
$opts = array_merge([
'limit' => 10,
'offset' => 0,
'uuid' => '',
'recursive' => false,
'limit' => 12,
'offset' => '',
'guid' => null,
'type' => null,
], $opts);
if (!$opts['guid']) {
throw new \Exception('GUID is required');
}
$response = new Response;
if ($opts['type'] === 'subscribers') {
$statement = "SELECT * FROM friendsof";
} else {
$statement = "SELECT * FROM friends";
}
$where = ["key = ?"];
$values = [$opts['guid']];
$statement .= " WHERE " . implode(' AND ', $where);
$cqlOpts = [];
if ($opts['limit']) {
$cqlOpts['page_size'] = (int) $opts['limit'];
}
if ($opts['offset']) {
$cqlOpts['paging_state_token'] = base64_decode($opts['offset'], true);
}
$query = new Prepared\Custom();
$query->query($statement, $values);
$query->setOpts($cqlOpts);
try {
$rows = $this->client->request($query);
foreach ($rows as $row) {
$user = new User($row['column1']);
$response[] = $user;
}
$response->setPagingToken(base64_encode($rows->pagingStateToken()));
$response->setLastPage($rows->isLastPage());
} catch (\Exception $e) {
// do nothing.
}
return $response;
}
/**
......
<?php
/**
* Upgrades Delegate
*/
namespace Minds\Core\Wire\Delegates;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Wire\Wire;
use Minds\Core\Pro\Manager as ProManager;
class UpgradesDelegate
{
/** @var Config */
private $config;
/** @var EntitiesBuilder */
private $entitiesBuilder;
/** @var ProManager */
private $proManager;
public function __construct($config = null, $entitiesBuilder = null, $proManager = null)
{
$this->config = $config ?: Di::_()->get('Config');
$this->entitiesBuilder = $entitiesBuilder ?: Di::_()->get('EntitiesBuilder');
$this->proManager = $proManager ?? Di::_()->get('Pro\Manager');
}
/**
* On Wire
* @param Wire $wire
* @param string $receiver_address
* @return Wire $wire
*/
public function onWire($wire, $receiver_address): Wire
{
switch ($wire->getReceiver()->guid) {
case $this->config->get('blockchain')['contracts']['wire']['plus_guid']:
return $this->onPlusUpgrade($wire, $receiver_address);
break;
case $this->config->get('pro')['handler']:
return $this->onProUpgrade($wire, $receiver_address);
break;
}
return $wire; // Not expected
}
private function onPlusUpgrade($wire, $receiver_address): Wire
{
/*if (
!(
$receiver_address == 'offchain'
|| $receiver_address == $this->config->get('blockchain')['contracts']['wire']['plus_address']
)
) {
return $wire; //not offchain or potential onchain fraud
}
// 20 tokens
if ($wire->getAmount() != "20000000000000000000") {
return $wire; //incorrect wire amount sent
}*/
//set the plus period for this user
$user = $wire->getSender();
// rebuild the user as we can't trust upstream
$user = $this->entitiesBuilder->single($user->getGuid(), [
'cache' => false,
]);
if (!$user) {
return $wire;
}
$days = 30;
$monthly = $this->config->get('upgrades')['plus']['monthly'];
$yearly = $this->config->get('upgrades')['plus']['yearly'];
switch ($wire->getMethod()) {
case 'tokens':
if ($monthly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 30;
} elseif ($yearly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 365;
} else {
return $wire;
}
break;
case 'usd':
if ($monthly['usd'] == $wire->getAmount() / 100) {
$days = 30;
} elseif ($yearly['usd'] == $wire->getAmount() / 100) {
$days = 365;
} else {
return $wire;
}
break;
default:
return $wire;
}
$expires = strtotime("+{$days} days", $wire->getTimestamp());
$user->setPlusExpires($expires);
$user->save();
//$wire->setSender($user);
return $wire;
}
private function onProUpgrade($wire, $receiver_address): Wire
{
//set the plus period for this user
$user = $wire->getSender();
// rebuild the user as we can't trust upstream
$user = $this->entitiesBuilder->single($user->getGuid(), [
'cache' => false,
]);
if (!$user) {
return $wire;
}
$days = 30;
$monthly = $this->config->get('upgrades')['pro']['monthly'];
$yearly = $this->config->get('upgrades')['pro']['yearly'];
error_log($wire->getMethod());
switch ($wire->getMethod()) {
case 'tokens':
error_log($wire->getAmount());
if ($monthly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 30;
} elseif ($yearly['tokens'] == $wire->getAmount() / (10 ** 18)) {
$days = 365;
} else {
return $wire;
}
break;
case 'usd':
if ($monthly['usd'] == $wire->getAmount() / 100) {
$days = 30;
} elseif ($yearly['usd'] == $wire->getAmount() / 100) {
$days = 365;
} else {
return $wire;
}
break;
default:
return $wire;
}
$expires = strtotime("+{$days} days", $wire->getTimestamp());
$this->proManager->setUser($user)
->enable($expires);
return $wire;
}
}
......@@ -61,8 +61,8 @@ class Manager
/** @var Core\Blockchain\Wallets\OffChain\Cap $cap */
protected $cap;
/** @var Delegates\Plus $plusDelegate */
protected $plusDelegate;
/** @var Delegates\UpgradesDelegate */
protected $upgradesDelegate;
/** @var Delegates\RecurringDelegate $recurringDelegate */
protected $recurringDelegate;
......@@ -87,7 +87,7 @@ class Manager
$client = null,
$token = null,
$cap = null,
$plusDelegate = null,
$upgradesDelegate = null,
$recurringDelegate = null,
$notificationDelegate = null,
$cacheDelegate = null,
......@@ -101,7 +101,8 @@ class Manager
$this->client = $client ?: Di::_()->get('Blockchain\Services\Ethereum');
$this->token = $token ?: Di::_()->get('Blockchain\Token');
$this->cap = $cap ?: Di::_()->get('Blockchain\Wallets\OffChain\Cap');
$this->plusDelegate = $plusDelegate ?: new Delegates\Plus();
$this->upgradesDelegate = $upgradesDelegate ?? new Delegates\UpgradesDelegate();
;
$this->recurringDelegate = $recurringDelegate ?: new Delegates\RecurringDelegate();
$this->notificationDelegate = $notificationDelegate ?: new Delegates\NotificationDelegate();
$this->cacheDelegate = $cacheDelegate ?: new Delegates\CacheDelegate();
......@@ -248,8 +249,8 @@ class Manager
$wire->setAddress('offchain');
// Notify plus
$this->plusDelegate
// Notify plus/pro
$this->upgradesDelegate
->onWire($wire, 'offchain');
// Send notification
......@@ -287,6 +288,10 @@ class Manager
// Save the wire to the Repository
$this->repository->add($wire);
// Notify plus/pro
$this->upgradesDelegate
->onWire($wire, 'usd');
// Send notification
$this->notificationDelegate->onAdd($wire);
......@@ -330,7 +335,7 @@ class Manager
->setCompleted(true);
$this->txRepo->add($transaction);
$this->plusDelegate
$this->upgradesDelegate
->onWire($wire, $data['receiver_address']);
$this->notificationDelegate->onAdd($wire);
......
......@@ -431,7 +431,7 @@ class Group extends NormalizedEntity
{
$guids = $this->getOwnerGuids();
return $guids
? guids[0]
? $guids[0]
: $this->getOwnerObj()->guid;
}
......
......@@ -32,6 +32,7 @@ class User extends \ElggUser
$this->attributes['plus'] = 0; //TODO: REMOVE
$this->attributes['plus_expires'] = 0;
$this->attributes['pro_expires'] = 0;
$this->attributes['pro_published'] = 0;
$this->attributes['verified'] = 0;
$this->attributes['founder'] = 0;
$this->attributes['disabled_boost'] = 0;
......@@ -824,6 +825,7 @@ class User extends \ElggUser
$export['programs'] = $this->getPrograms();
$export['plus'] = (bool) $this->isPlus();
$export['pro'] = (bool) $this->isPro();
$export['pro_published'] = (bool) $this->isProPublished();
$export['verified'] = (bool) $this->verified;
$export['founder'] = (bool) $this->founder;
$export['disabled_boost'] = (bool) $this->disabled_boost;
......@@ -900,11 +902,11 @@ class User extends \ElggUser
/**
* Is the user a plus user.
*
* @return int
* @return bool
*/
public function isPlus()
{
return (bool) ((int) $this->plus_expires > time());
return $this->isPro() || ((int) $this->plus_expires > time());
}
/**
......@@ -945,6 +947,35 @@ class User extends \ElggUser
return $this->getProExpires() >= time();
}
/**
* Set if pro is published
* @param bool $published
* @return self
*/
public function setProPublished(bool $published): self
{
$this->pro_published = $published ? 1 : 0;
return $this;
}
/**
* Return if is published
* @return bool
*/
public function getProPublished(): bool
{
return (bool) $this->pro_published;
}
/**
* Return if is published
* @return bool
*/
public function isProPublished(): bool
{
return (bool) $this->pro_published;
}
/**
* Gets the categories to which the user is subscribed.
*
......
......@@ -75,8 +75,8 @@ class EmailRewards
$validator = $_GET['validator'];
//$key = '.md';
//return;
if ($validator == sha1($campaign . 'gift-30-09-19.mdl' . $topic . $user->guid . Config::_()->get('emails_secret'))) {
$tokens = 2 * (10 ** 18);
if ($validator == sha1($campaign . 'gift-30-10-19.mdl' . $topic . $user->guid . Config::_()->get('emails_secret'))) {
$tokens = 1 * (10 ** 18);
$campaign = $validator; //hack
} else {
return;
......
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Filters;
use Minds\Entities\User;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\ViewTypeFilter;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class FiltersCollectionSpec extends ObjectBehavior
{
public function it_is_initializable()
{
$this->shouldHaveType(FiltersCollection::class);
}
public function it_should_add_filters_to_collection()
{
$this->setUser(new User());
$this->addFilters(new ViewTypeFilter);
$filters = $this->getFilters();
$filters['view_type']->getId()
->shouldBe('view_type');
}
public function it_should_export_filters()
{
$this->setUser(new User());
$this->addFilters(new ViewTypeFilter);
$export = $this->export();
$export[0]['id']
->shouldBe('view_type');
$export[0]['options']
->shouldHaveCount(4);
}
}
<?php
namespace Spec\Minds\Core\Analytics\Dashboards\Metrics;
use Minds\Core\Analytics\Dashboards\Metrics\ActiveUsersMetric;
use Minds\Core\Analytics\Dashboards\Timespans\TimespansCollection;
use Minds\Core\Analytics\Dashboards\Timespans\AbstractTimespan;
use Minds\Core\Analytics\Dashboards\Filters\FiltersCollection;
use Minds\Core\Analytics\Dashboards\Filters\AbstractFilter;
use Minds\Core\Data\Elasticsearch\Client;
use Minds\Entities\User;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class ActiveUsersMetricSpec extends ObjectBehavior
{
private $es;
private $timespansCollection;
private $filtersCollection;
public function let(Client $es, TimespansCollection $timespansCollection, FiltersCollection $filtersCollection)
{
$this->beConstructedWith($es);
$this->es = $es;
$this->setTimespansCollection($timespansCollection);
$this->setFiltersCollection($filtersCollection);
$this->timespansCollection = $timespansCollection;
$this->filtersCollection = $filtersCollection;
}
public function it_is_initializable()
{
$this->shouldHaveType(ActiveUsersMetric::class);
}
public function it_should_build_summary(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn(
[
'aggregations' => [
'1' => [
'value' => 128,
],
]
],
[
'aggregations' => [
'1' => [
'value' => 256,
],
]
]
);
$this->buildSummary();
$this->getSummary()->getValue()
->shouldBe(128);
$this->getSummary()->getComparisonValue()
->shouldBe(256);
}
public function it_should_build_visualisation(AbstractTimespan $mockTimespan, AbstractFilter $mockFilter)
{
$this->setUser(new User());
$this->timespansCollection->getSelected()
->willReturn($mockTimespan);
$this->filtersCollection->getSelected()
->willReturn([$mockFilter]);
$this->es->request(Argument::any())
->willReturn([
'aggregations' => [
'1' => [
'buckets' => [
[
'key' => strtotime('Midnight 1st December 2018') * 1000,
'2' => [
'value' => 256,
],
],
[
'key' => strtotime('Midnight 1st January 2019') * 1000,
'2' => [
'value' => 128,
],
],
[
'key' => strtotime('Midnight 1st February 2019') * 1000,
'2' => [
'value' => 4,
],
],
[
'key' => strtotime('Midnight 1st March 2019') * 1000,
'2' => [
'value' => 685,
],
],
],
],
]
]);
$this->buildVisualisation();
$xValues = $this->getVisualisation()->getXValues();
$xValues[0]->shouldBe('01-12-2018');
$xValues[1]->shouldBe('01-01-2019');
$xValues[2]->shouldBe('01-02-2019');
$xValues[3]->shouldBe('01-03-2019');
$yValues = $this->getVisualisation()->getYValues();
$yValues[0]->shouldBe(256);
$yValues[1]->shouldBe(128);
$yValues[2]->shouldBe(4);
$yValues[3]->shouldBe(685);
}
}
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.