...
 
Commits (58)
......@@ -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\Analytics\EntityCentric\Manager;
use Minds\Cli;
use Minds\Interfaces;
use Minds\Exceptions;
use Minds\Entities;
class EntityCentric 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();
$manager->setFrom($from);
$i = 0;
foreach ($manager->sync() as $record) {
$this->out(++$i);
}
}
}
<?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);
}
}
}
......@@ -18,11 +18,12 @@ class Transcode extends Cli\Controller implements Interfaces\CliControllerInterf
{
$this->out('TBD');
}
public function exec()
{
$transcoder = new Core\Media\Services\FFMpeg;
$transcoder->setKey($this->getOpt('guid'));
$transcoder->transcode();
$transcoder->setFullHD($this->getOpt('full_hd') ?? false);
$transcoder->onQueue();
}
}
......@@ -347,15 +347,22 @@ class comments implements Interfaces\Api
$comment = $manager->getByLuid($pages[0]);
if ($comment && $comment->canEdit()) {
if (!$comment) {
return Factory::response([
'status' => 'error',
'message' => 'Comment not found',
]);
}
if ($comment->canEdit()) {
$manager->delete($comment);
return Factory::response([]);
}
//check if owner of activity trying to remove
$entity = Entities\Factory::build($comment->getEntityGuid());
if ($entity->owner_guid == Core\Session::getLoggedInUserGuid()) {
$manager->delete($comment, [ 'force' => true ]);
$manager->delete($comment, ['force' => true]);
return Factory::response([]);
}
......
......@@ -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',
......
......@@ -36,6 +36,13 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
return Factory::response(['status' => 'error']);
}
if (!in_array($this->getType($entity), ['object:video', 'object:image'], true)) {
return Factory::response([
'status' => 'error',
'message' => 'Entity is not a media entity',
]);
}
switch ($entity->subtype) {
case "video":
// Helpers\Counters::increment($pages[0], 'plays');
......@@ -43,10 +50,12 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
if (isset($pages[1]) && $pages[1] == 'play') {
http_response_code(302);
$res = !empty($_GET['res']) && in_array($_GET['res'], ['360', '720', '1080'], true) ?$_GET['res'] : '360';
if ($entity->subtype == 'audio') {
\forward($entity->getSourceUrl('128.mp3'));
} else {
\forward($entity->getSourceUrl('360.mp4'));
\forward($entity->getSourceUrl("{$res}.mp4"));
}
exit;
......@@ -58,8 +67,11 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
$response = $entities[0];
$response['transcodes'] = [
'360.mp4' => $entity->getSourceUrl('360.mp4'),
'720.mp4' => $entity->getSourceUrl('720.mp4')
'720.mp4' => $entity->getSourceUrl('720.mp4'),
];
if ($entity->getFlag('full_hd')) {
$response['transcodes']['1080.mp4'] = $entity->getSourceUrl('1080.mp4');
}
}
if (method_exists($entity, 'getWireThreshold')) {
......@@ -158,6 +170,7 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
$body = $req['body'];
fwrite($fp, $body);
$video->access_id = 0;
$video->patch(['full_hd', Core\Session::getLoggedinUser()->isPro()]);
$video->upload($tmpFilename);
$guid = $video->save();
fclose($fp);
......@@ -258,7 +271,8 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
'access_id' => 0,
'owner_guid' => $user->guid,
'hidden' => $container_guid !== null,
'container_guid' => $container_guid
'container_guid' => $container_guid,
'full_hd' => $user->isPro(),
]);
$assets = Core\Media\AssetsFactory::build($entity);
......@@ -267,7 +281,7 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
$entity->setAssets($assets->upload($media, $data));
// Save initial entity
$success = $save
->setEntity($entity)
->save(true);
......@@ -356,4 +370,9 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
return $response;
}
private function getType($entity): string
{
return $entity->subtype ? "{$entity->type}:{$entity->subtype}" : $entity->type;
}
}
......@@ -9,6 +9,7 @@ namespace Minds\Controllers\api\v1\minds;
use Minds;
use Minds\Core;
use Minds\Core\Rewards\Contributions\ContributionValues;
use Minds\Interfaces;
use Minds\Api\Factory;
......@@ -39,6 +40,12 @@ 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'),
'contribution_values' => ContributionValues::export(),
];
return Factory::response($minds);
......
......@@ -358,7 +358,8 @@ class newsfeed implements Interfaces\Api
->setCustom('video', [
'thumbnail_src' => $embeded->getIconUrl(),
'guid' => $embeded->guid,
'mature' => $embeded instanceof Flaggable ? $embeded->getFlag('mature') : false
'mature' => $embeded instanceof Flaggable ? $embeded->getFlag('mature') : false,
'full_hd' => $embeded->getFlag('full_hd') ?? false,
])
->setTitle($embeded->title)
->setBlurb($embeded->description)
......@@ -525,7 +526,7 @@ class newsfeed implements Interfaces\Api
]);
}
}
$save->setEntity($activity)
->save();
......
......@@ -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([]);
}
}
......@@ -25,6 +25,8 @@ class wallet implements Interfaces\Api
*/
public function get($pages)
{
Factory::isLoggedIn();
/** @var abstractCacher $cache */
$cache = Di::_()->get('Cache');
......
......@@ -10,6 +10,8 @@ namespace Minds\Controllers\api\v2\media;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Media\ClientUpload\Manager;
use Minds\Core\Session;
use Minds\Interfaces;
use Minds\Core\Media\ClientUpload\ClientUploadLease;
......@@ -38,6 +40,7 @@ class upload implements Interfaces\Api
*/
public function put($pages)
{
/** @var Manager $manager */
$manager = Di::_()->get("Media\ClientUpload\Manager");
switch ($pages[0]) {
case 'prepare':
......@@ -55,7 +58,9 @@ class upload implements Interfaces\Api
$lease->setGuid($guid)
->setMediaType($mediaType);
$manager->complete($lease);
$manager
->setFullHD(Session::getLoggedinUser()->isPro())
->complete($lease);
break;
}
return Factory::response([]);
......
......@@ -28,7 +28,14 @@ class transactions implements Interfaces\Api
'message' => 'There was an error returning the usd account',
]);
}
if (!$account) {
return Factory::response([
'status' => 'error',
'message' => 'Stripe account not found',
]);
}
$transactionsManger = new Stripe\Transactions\Manager();
$transactions = $transactionsManger->getByAccount($account);
......
......@@ -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;
}
}
......@@ -11,7 +11,7 @@ use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\SapiEmitter;
class Implicit extends core\page implements Interfaces\page
class implicit extends core\page implements Interfaces\page
{
public function get($pages)
{
......
......@@ -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
/**
* Engagement Dashboard
*/
namespace Minds\Core\Analytics\Dashboards;
use Minds\Entities\User;
use Minds\Traits\MagicAttributes;
/**
* @method EngagementDashboard setTimespanId(string $timespanId)
* @method EngagementDashboard setFilterIds(array $filtersIds)
* @method EngagementDashboard setUser(User $user)
*/
class EngagementDashboard implements DashboardInterface
{
use MagicAttributes;
/** @var string */
private $timespanId = '30d';
/** @var string[] */
private $filterIds = [ 'platform::browser' ];
/** @var string */
private $metricId = 'votes_up';
/** @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\Engagement\VotesUpMetric(),
new Metrics\Engagement\CommentsMetric(),
new Metrics\Engagement\RemindsMetric(),
new Metrics\Engagement\SubscribersMetric()
)
->build();
return $this;
}
/**
* Export
* @param array $extras
* @return array
*/
public function export(array $extras = []): array
{
$this->build();
return [
'category' => 'engagement',
'label' => 'Engagement',
'description' => '',
'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,
'engagement' => EngagementDashboard::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("midnight -{$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[]['exists'] = [
'field' => 'active::total',
];
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lt' => strtotime("midnight tomorrow +{$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'
];
$must[]['exists'] = [
'field' => 'active::total',
];
// 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,
'lt' => strtotime("midnight tomorrow +{$timespan->getComparisonInterval()} days", $tsMs / 1000) * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$must[] = [
'exists' => [
'field' => $this->aggField,
],
];
$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,
],
];
}
$must[] = [
'exists' => [
'field' => $this->aggField,
],
];
// 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\Engagement;
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 AbstractEngagementMetric 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 = 'number';
/** @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 = [];
$maxTs = strtotime("midnight tomorrow +{$timespan->getComparisonInterval()} days", $tsMs / 1000);
$must[]['range'] = [
'@timestamp' => [
'gte' => $tsMs,
'lt' => $maxTs * 1000,
],
];
if ($userGuid = $this->getUserGuid()) {
$must[] = [
'term' => [
'owner_guid' => $userGuid,
],
];
}
$must[] = [
'exists' => [
'field' => $this->aggField,
],
];
$indexes = implode(',', [
'minds-entitycentric-' . date('m-Y', $tsMs / 1000),
'minds-entitycentric-' . date('m-Y', $maxTs),
]);
$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,
],
];
}
$must[] = [
'exists' => [
'field' => $this->aggField,
],
];
// 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\Engagement;
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 CommentsMetric extends AbstractEngagementMetric
{
/** @var string */
protected $id = 'comments';
/** @var string */
protected $label = 'Comments';
/** @var string */
protected $description = "Number of comments you have received on your content";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'comment::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Engagement;
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 RemindsMetric extends AbstractEngagementMetric
{
/** @var string */
protected $id = 'reminds';
/** @var string */
protected $label = 'Reminds';
/** @var string */
protected $description = "Number of reminds you have received on your content";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'remind::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Engagement;
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 SubscribersMetric extends AbstractEngagementMetric
{
/** @var string */
protected $id = 'subscribers';
/** @var string */
protected $label = 'Subscribes';
/** @var string */
protected $description = "Number of subscribers your channel has gained";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'subscribe::total';
}
<?php
namespace Minds\Core\Analytics\Dashboards\Metrics\Engagement;
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 VotesUpMetric extends AbstractEngagementMetric
{
/** @var string */
protected $id = 'votes_up';
/** @var string */
protected $label = 'Votes up';
/** @var string */
protected $description = "Number of votes up you have received on your content";
/** @var array */
protected $permissions = [ 'user', 'admin' ];
/** @var string */
protected $aggField = 'vote:up::total';
}
<?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,
'lt' => strtotime("midnight tomorrow +{$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' => 'Content',
'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 = 30;
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 = 30;
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(),
];
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\Metrics\Active;
use DateTime;
use Exception;
class ActiveUsersSynchroniser
{
/** @var array */
private $records = [];
/** @var Active */
private $activeMetric;
public function __construct($activeMetric = null)
{
$this->activeMetric = $activeMetric ?? new Active();
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$date = (new DateTime())->setTimestamp($this->from);
$now = new DateTime();
$days = (int) $date->diff($now)->format('%a');
$months = round($days / 28);
// Daily resolution
foreach ($this->activeMetric->get($days ?: 1) as $bucket) {
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('day')
->incrementSum('active::total', $bucket['total']);
$this->records[] = $record;
}
// Monthly resolution
foreach ($this->activeMetric->get($months ?: 1, 'month') as $bucket) {
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('month')
->incrementSum('active::total', $bucket['total']);
$this->records[] = $record;
}
foreach ($this->records as $record) {
yield $record;
}
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
use DateTime;
use Exception;
class EngagementSynchroniser
{
/** @var array */
private $records = [];
/** @var ElasticSearch\Client */
private $es;
public function __construct($es = null)
{
$this->es = $es ?? Di::_()->get('Database\ElasticSearch');
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$date = (new DateTime())->setTimestamp($this->from);
$now = new DateTime();
$days = (int) $date->diff($now)->format('%a');
$months = round($days / 28);
$i = 0;
foreach ($this->getEntitiesMetrics() as $buckets) {
$urn = null;
$ownerGuid = null;
if (!$buckets['type']['buckets'][0]['key'] && $buckets['metrics']['buckets'][0]['key'] === 'subscribe') {
$urn = "urn:user:{$buckets['key']}";
$ownerGuid = (string) $buckets['key'];
} elseif (!$buckets['type']['buckets'][0]['key']) {
echo "\nEngagement: skipping as no type";
continue;
} else {
$urn = "urn:{$buckets['type']['buckets'][0]['key']}:{$buckets['key']}";
$ownerGuid = (string) $buckets['owner']['buckets'][0]['key'];
if ($buckets['type']['buckets'][0]['key'] === 'object') {
$urn = "urn:{$buckets['subtype']['buckets'][0]['key']}:{$buckets['key']}";
}
}
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($ownerGuid)
->setTimestamp($this->from)
->setResolution('day');
foreach ($buckets['metrics']['buckets'] as $metrics) {
$record->incrementSum($metrics['key'] . '::total', (int) $metrics['doc_count']);
}
$this->records[] = $record;
++$i;
error_log("Engagement: $i");
}
foreach ($this->records as $record) {
yield $record;
}
}
private function getEntitiesMetrics()
{
$opts = array_merge([
'fields' => [],
'from' => time(),
], []);
$must = [];
// $must[] = [
// 'term' => [
// 'action.keyword' => 'subscribe',
// ],
//];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $this->from * 1000,
'lt' => strtotime('+1 day', $this->from) * 1000,
],
],
];
$partition = 0;
$partitions = 50;
$partitionSize = 5000; // Allows for 250,000 entities
$index = 'minds-metrics-' . date('m-Y', $this->from);
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => $index,
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'entity_guid.keyword',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
'aggs' => [
'metrics' => [
'terms' => [
'field' => 'action.keyword',
'min_doc_count' => 1,
],
],
'owner' => [
'terms' => [
'field' => 'entity_owner_guid.keyword',
'min_doc_count' => 1,
],
],
'type' => [
'terms' => [
'field' => 'entity_type.keyword',
],
],
'subtype' => [
'terms' => [
'field' => 'entity_subtype.keyword',
]
],
],
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
}
<?php
/**
* EntityCentricRecord
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Traits\MagicAttributes;
/**
* Class EntityCentricRecord
* @package Minds\Core\Analytics\EntityCentric
* @method EntityCentricRecord setResolution(int $year)
* @method string getResolution()
* @method EntityCentricRecord setEntityUrn(string $entityUrn)
* @method string getEntityUrn()
* @method EntityCentricRecord setOwnerGuid(string $ownerGuid)
* @method string getOwnerGuid()
* @method EntityCentricRecord setTimestampMs(int $timestampMs)
* @method int getTimestampMs()
* @method EntityCentricRecord setTimestamp(int $timestamp)
* @method int getTimestamp()
* @method EntityCentricRecord setSums(array $sums)
* @method int getSums()
*/
class EntityCentricRecord
{
use MagicAttributes;
/** @var string */
private $resolution;
/** @var int */
protected $timestamp;
/** @var int */
protected $timestampMs;
/** @var string */
protected $entityUrn;
/** @var string */
protected $ownerGuid;
/** @var array */
private $sums;
/**
* Increment views
* @param string $metric
* @param int $value
* @return EntityCentricRecord
*/
public function incrementSum($metric, $value = 1): EntityCentricRecord
{
if (!isset($this->sums[$metric])) {
$this->sums[$metric] = 0;
}
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
/**
* @return string
*/
public function getUrn(): string
{
return (string) implode('-', [ $this->getEntityUrn(), $this->getResolution(), $this->getTimestamp() ]);
}
}
<?php
/**
* EntityCentric Manager
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use DateTime;
use Exception;
class Manager
{
/** @var array */
const SYNCHRONISERS = [
EngagementSynchroniser::class,
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
];
/** @var Repository */
protected $repository;
/** @var Sums */
protected $sums;
/** @var int */
private $from;
/** @var int */
private $to;
public function __construct(
$repository = null,
$sums = null
) {
$this->repository = $repository ?? new Repository();
$this->sums = $sums ?? new Sums();
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Synchronise views from cassandra to elastic
* @return iterable
*/
public function sync(): iterable
{
foreach (Manager::SYNCHRONISERS as $synchroniserClass) {
$synchroniser = new $synchroniserClass;
$date = (new DateTime())->setTimestamp($this->from);
$synchroniser->setFrom($this->from);
foreach ($synchroniser->toRecords() as $record) {
$this->add($record);
yield $record;
}
// Call again incase any leftover
$this->repository->bulk();
}
echo "done";
}
/**
* Add an entity centric record to the database
* @param EntityCentricRecord $record
* @return bool
*/
public function add(EntityCentricRecord $record): bool
{
return (bool) $this->repository->add($record);
}
/**
* Query aggregate
* @param array $query
* @return array
*/
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 Repository
* @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\Di\Di;
class Repository
{
/** @var ElasticClient */
protected $es;
/** @var array $pendingBulkInserts * */
private $pendingBulkInserts = [];
/**
* Repository constructor.
* @param ElasticClient $es
*/
public function __construct(
$es = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
}
/**
* @param array $opts
* @return Response
*/
public function getList(array $opts = [])
{
$response = new Response();
return $response;
}
/**
* @param EntityCentricRecord $record
* @return bool
* @throws Exception
*/
public function add(EntityCentricRecord $record)
{
$index = 'minds-entitycentric-' . date('m-Y', $record->getTimestamp());
$body = [
'resolution' => $record->getResolution(),
'@timestamp' => $record->getTimestamp() * 1000,
'entity_urn' => $record->getEntityUrn(),
'owner_guid' => $record->getOwnerGuid(),
];
$body = array_merge($body, $record->getSums());
$body = array_filter($body, function ($val) {
if ($val === '' || $val === null) {
return false;
}
return true;
});
$this->pendingBulkInserts[] = [
'update' => [
'_id' => (string) implode('-', [ $record->getEntityUrn(), $record->getResolution(), $record->getTimestamp() ]),
'_index' => $index,
'_type' => '_doc',
],
];
$this->pendingBulkInserts[] = [
'doc' => $body,
'doc_as_upsert' => true,
];
if (count($this->pendingBulkInserts) > 2000) { //1000 inserts
$this->bulk();
}
}
/**
* Bulk insert results
*/
public function bulk()
{
if (count($this->pendingBulkInserts) > 0) {
$res = $this->es->bulk(['body' => $this->pendingBulkInserts]);
$this->pendingBulkInserts = [];
}
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\Metrics\Signup;
use DateTime;
use Exception;
class SignupsSynchroniser
{
/** @var array */
private $records = [];
/** @var Signup */
private $signupMetric;
public function __construct($signupMetric = null)
{
$this->signupMetric = $signupMetric ?? new Signup;
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$date = (new DateTime())->setTimestamp($this->from);
$now = new DateTime();
$days = (int) $date->diff($now)->format('%a');
foreach ($this->signupMetric->get($days) as $bucket) {
error_log($bucket['date']);
$record = new EntityCentricRecord();
$record->setEntityUrn("urn:metric:global")
->setOwnerGuid((string) 0) // Site is owner
->setTimestamp($bucket['timestamp'])
->setResolution('day')
->incrementSum('signups::total', $bucket['total']);
$this->records[] = $record;
}
foreach ($this->records as $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;
}
}
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Analytics\Views\Repository as ViewsRepository;
use DateTime;
use Exception;
class ViewsSynchroniser
{
/** @var array */
private $records = [];
/** @var ViewsRepository */
private $viewsRepository;
public function __construct($viewsRepository = null)
{
$this->viewsRepository = $viewsRepository ?: new ViewsRepository();
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$date = (new DateTime())->setTimestamp($this->from);
$opts['day'] = intval($date->format('d'));
$opts['month'] = intval($date->format('m'));
$opts['year'] = $date->format('Y');
$opts['from'] = $this->from;
$i = 0;
while (true) {
$result = $this->viewsRepository->getList($opts);
$opts['offset'] = $result->getPagingToken();
foreach ($result as $view) {
// if (!in_array($view->getSource(), [ 'single', 'feed/channel'])) {
// continue;
// }
$this->downsampleViewToRecord($view);
error_log(++$i);
}
if ($result->isLastPage()) {
break;
}
}
foreach ($this->records as $record) {
yield $record;
}
}
/**
* Add entity to map
* @param View $view
* @return void
*/
private function downsampleViewToRecord($view): void
{
$entityUrn = $view->getEntityUrn();
if (!isset($this->records[$view->getEntityUrn()])) {
$timestamp = (new \DateTime())->setTimestamp($view->getTimestamp())->setTime(0, 0, 0);
$record = new EntityCentricRecord();
$record->setEntityUrn($view->getEntityUrn())
->setOwnerGuid($view->getOwnerGuid())
->setTimestamp($timestamp->getTimestamp())
->setResolution('day');
$this->records[$view->getEntityUrn()] = $record;
}
if ($view->getCampaign()) {
$this->records[$view->getEntityUrn()]->incrementSum('views::boosted');
} else {
$this->records[$view->getEntityUrn()]->incrementSum('views::organic');
}
if ($view->getSource() === 'single') {
$this->records[$view->getEntityUrn()]->incrementSum('views::single');
}
$this->records[$view->getEntityUrn()]->incrementSum('views::total');
}
}
......@@ -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;
......
......@@ -5,6 +5,7 @@ namespace Minds\Core\Blogs;
use Minds\Core\Di\Di;
use Minds\Core\Events\Event;
use Minds\Core\Events\EventsDispatcher;
use Minds\Core\Session;
class Events
{
......@@ -37,5 +38,29 @@ class Events
$manager = Di::_()->get('Blogs\Manager');
$event->setResponse($manager->update($blog));
});
$this->eventsDispatcher->register('export:extender', 'blog', function (Event $event) {
$params = $event->getParameters();
/** @var Core\Blogs\Blog $blog */
$blog = $params['entity'];
$export = $event->response() ?: [];
$currentUser = Session::getLoggedInUserGuid();
$dirty = false;
if ($blog->isPaywall() && $blog->owner_guid != $currentUser) {
$export['description'] = '';
$export['body'] = '';
$dirty = true;
}
if ($dirty) {
return $event->setResponse($export);
}
if (!$currentUser) {
return;
}
});
}
}
......@@ -86,7 +86,7 @@ class Repository
$opts['parent_guid_l1'] = $parent_guids[0] ?? 0;
$opts['parent_guid_l2'] = $parent_guids[1] ?? 0;
$opts['parent_guid_l3'] = 0; //do not support l3 yet
$cql = "SELECT * from comments";
$values = [];
$cqlOpts = [];
......@@ -94,16 +94,25 @@ class Repository
$where = [];
if ($opts['entity_guid']) {
if (!is_numeric($opts['entity_guid'])) {
return new Response();
}
$where[] = 'entity_guid = ?';
$values[] = new Varint($opts['entity_guid']);
}
if ($opts['parent_guid_l1'] !== null) {
if (!is_numeric($opts['parent_guid_l1'])) {
return new Response();
}
$where[] = 'parent_guid_l1 = ?';
$values[] = new Varint((int) $opts['parent_guid_l1']);
}
if ($opts['parent_guid_l2'] !== null) {
if (!is_numeric($opts['parent_guid_l2'])) {
return new Response();
}
$where[] = 'parent_guid_l2 = ?';
$values[] = new Varint((int) $opts['parent_guid_l2']);
}
......
......@@ -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'] ?>) |
| |
......@@ -12,15 +12,29 @@ use Minds\Entities\Video;
class Manager
{
/** @var FFMepg */
/** @var FFMpeg */
private $ffmpeg;
/** @var Guid $guid */
private $guid;
/** @var bool */
private $full_hd;
/** @var Save $save */
private $save;
/**
* @param bool $value
* @return Manager
*/
public function setFullHD(bool $value): Manager
{
$this->full_hd = $value;
return $this;
}
public function __construct(
FFMpeg $FFMpeg = null,
GuidBuilder $guid = null,
......@@ -70,12 +84,16 @@ class Manager
$video->set('guid', $lease->getGuid());
$video->set('cinemr_guid', $lease->getGuid());
$video->set('access_id', 0); // Hide until published
$video->setFlag('full_hd', $this->full_hd);
// Save the video
$this->save->setEntity($video)->save();
$this->ffmpeg->setKey($lease->getGuid());
// Set the full hd flag
$this->ffmpeg->setFullHD($this->full_hd);
// Start the transcoding process
$this->ffmpeg->transcode();
......
......@@ -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
*/
......
This diff is collapsed.
<?php
namespace Minds\Core\Media;
use Minds\Core;
......@@ -23,8 +24,13 @@ class Thumbnails
$loggedInUser = Core\Session::getLoggedinUser();
if (!Di::_()->get('Wire\Thresholds')->isAllowed($loggedInUser, $entity)) {
return false;
try {
if (!Di::_()->get('Wire\Thresholds')->isAllowed($loggedInUser, $entity)) {
return false;
}
} catch (\Exception $e) {
error_log('[Core/Media/Thumbnails::get] ' . $e->getMessage());
// don't do anything if the entity cannot be paywalled
}
$user = $entity->getOwnerEntity(false);
......
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.
......@@ -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]);
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.