...
 
Commits (73)
image: minds/php:latest
services:
- docker:dind
image: minds/php:7.3
stages:
- build
......@@ -13,38 +10,34 @@ stages:
- deploy:canary
- deploy:production
cache:
paths:
- vendor
- bin
policy: pull
build:
stage: build
script:
- apk update && apk add --no-cache git
- 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
image: minds/php-tests:latest
image: minds/php:7.3
script:
- bin/phpspec run
- php -n -c Spec/php-test.ini bin/phpspec run
lint:
stage: test
image: minds/php-tests:latest
image: minds/php:7.3
script:
- bin/php-cs-fixer fix --allow-risky=yes --verbose --dry-run
prepare:fpm:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- |
......@@ -59,6 +52,8 @@ prepare:fpm:
prepare:runners:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- |
......@@ -142,6 +137,8 @@ qa:manual:
staging:fpm:
stage: deploy:staging
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="staging"
- $(aws ecr get-login --no-include-email --region us-east-1)
......@@ -168,6 +165,8 @@ staging:fpm:
canary:fpm:
stage: deploy:canary
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="canary"
- $(aws ecr get-login --no-include-email --region us-east-1)
......@@ -196,6 +195,8 @@ canary:fpm:
production:fpm:
stage: deploy:production
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="production"
- $(aws ecr get-login --no-include-email --region us-east-1)
......@@ -224,6 +225,8 @@ production:fpm:
production:runners:
stage: deploy:production
image: minds/ci:latest
services:
- docker:dind
script:
- IMAGE_LABEL="production"
- $(aws ecr get-login --no-include-email --region us-east-1)
......
......@@ -171,16 +171,17 @@ class Controller
/**
* Gets the list of publically available commands and filters out the system ones.
* @param array $additionalExcludes Additional methods to exclude from command list (optional)
*/
public function getCommands()
public function getCommands(array $additionalExcludes = [])
{
$excludedMethods = ['__construct', 'help', 'out', 'setArgs', 'setApp', 'getApp', 'getExecCommand', 'getOpt', 'getOpts', 'getAllOpts', 'getCommands', 'displayCommandHelp'];
$excludedMethods = ['__construct', 'help', 'out', 'setArgs', 'setApp', 'getApp', 'getExecCommand', 'getOpt', 'getOpts', 'getAllOpts', 'getCommands', 'displayCommandHelp', 'exec', 'gatekeeper'];
$commands = [];
foreach ((new ReflectionClass($this))->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$commands[] = $method->getName();
}
return array_diff($commands, $excludedMethods);
return array_diff($commands, $excludedMethods, $additionalExcludes);
}
public function displayCommandHelp()
......
......@@ -40,12 +40,14 @@ class CustomerSync extends Cli\Controller implements Interfaces\CliControllerInt
$guid = $data[29];
try {
$insert = new Core\Data\Cassandra\Prepared\Custom();
$insert->query("INSERT INTO user_index_to_guid (key, column1, value) VALUES (?, ?, ?)",
[
$insert->query(
"INSERT INTO user_index_to_guid (key, column1, value) VALUES (?, ?, ?)",
[
"$guid:payments",
"customer_id",
$id
]);
]
);
$this->db->request($insert);
$this->out("$guid with customer id $id done");
} catch (\Exception $e) {
......
<?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);
}
}
}
......@@ -56,18 +56,21 @@ class SubscriptionSync extends Cli\Controller implements Interfaces\CliControlle
$amount = ($result->quantity * $result->plan->amount) / 100;
$insert = new Core\Data\Cassandra\Prepared\Custom();
$insert->query("INSERT INTO plans (user_guid, plan, entity_guid, amount) VALUES (?, ?, ?, ?)",
[
$insert->query(
"INSERT INTO plans (user_guid, plan, entity_guid, amount) VALUES (?, ?, ?, ?)",
[
$plan['user_guid'],
$plan['plan'],
$plan['entity_guid'],
(int) $amount
]);
]
);
$this->db->request($insert);
if ($plan['plan'] == 'exclusive') {
$query = new Core\Data\Cassandra\Prepared\Custom();
$query->query("INSERT INTO wire
$query->query(
"INSERT INTO wire
(receiver_guid, sender_guid, method, timestamp, entity_guid, wire_guid, amount, recurring, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
[
......@@ -80,7 +83,8 @@ class SubscriptionSync extends Cli\Controller implements Interfaces\CliControlle
new \Cassandra\Decimal($amount),
true,
'success'
]);
]
);
$this->db->request($query);
}
......
......@@ -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();
}
}
......@@ -61,7 +61,7 @@ class fetch implements Interfaces\Api
->setQuality($quality)
->setOffset($_GET['offset'])
->setType($pages[0])
->setPriority(true);
->setUserGuid($user->getGUID());
if (isset($_GET['rating']) && $pages[0] == 'newsfeed') {
$cacher = Core\Data\cache\factory::build('Redis');
......@@ -79,6 +79,7 @@ class fetch implements Interfaces\Api
'boosted_guid' => (string) $guid,
'urn' => "urn:boost:content:{$guid}",
]);
/* TODO: Resolve bug below, $entity->guid is private member */
Counters::increment($entity->guid, "impression");
Counters::increment($entity->owner_guid, "impression");
}
......
......@@ -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([]);
}
......
......@@ -36,8 +36,12 @@ class report implements Interfaces\Api
/** @var Core\Reports\Repository $repository */
$repository = Di::_()->get('Reports\Repository');
$done = $repository->create($pages[0], Core\Session::getLoggedinUser(),
$reason, $reason_note);
$done = $repository->create(
$pages[0],
Core\Session::getLoggedinUser(),
$reason,
$reason_note
);
return Factory::response([
'done' => $done
......
......@@ -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')) {
......@@ -73,7 +85,7 @@ class media implements Interfaces\Api, Interfaces\ApiIgnorePam
Security\ACL::$ignore = $ignore;
}
/* No break */
/* no break */
default:
$entity->fullExport = true;
$response['entity'] = $entity->export();
......@@ -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;
}
}
......@@ -299,28 +299,6 @@ class merchant implements Interfaces\Api
$response['uploaded'] = true;
}
break;
case "charge":
$sale = (new Payments\Sale)
->setId($pages[1]);
try {
Payments\Factory::build('braintree', ['gateway'=>'merchants'])->chargeSale($sale);
} catch (\Exception $e) {
var_dump($e);
exit;
}
exit;
break;
case "void":
$sale = (new Payments\Sale)
->setId($pages[1]);
Payments\Factory::build('braintree', ['gateway'=>'merchants'])->voidSale($sale);
break;
case "refund":
$sale = (new Payments\Sale)
->setId($pages[1]);
Payments\Factory::build('braintree', ['gateway'=>'merchants'])->refundSale($sale);
break;
}
return Factory::response($response);
......
......@@ -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);
......
......@@ -181,24 +181,18 @@ class newsfeed implements Interfaces\Api
/** @var Core\Boost\Network\Iterator $iterator */
$iterator = Core\Di\Di::_()->get('Boost\Network\Iterator');
$iterator->setPriority(!get_input('offset', ''))
->setType('newsfeed')
$iterator->setType('newsfeed')
->setLimit($limit)
->setOffset($offset)
//->setRating(0)
->setQuality(0)
->setIncrement(false);
->setUserGuid(Core\Session::getLoggedinUserGuid())
->setRating((int) Core\Session::getLoggedinUser()->getBoostRating());
foreach ($iterator as $guid => $boost) {
$boost->boosted = true;
$boost->boosted_guid = (string) $guid;
array_unshift($activity, $boost);
//if (get_input('offset')) {
//bug: sometimes views weren't being calculated on scroll down
//Counters::increment($boost->guid, "impression");
//Counters::increment($boost->owner_guid, "impression");
//}
}
$cacher->set(Core\Session::getLoggedinUser()->guid . ':boost-offset:newsfeed', $iterator->getOffset(), (3600 / 2));
} catch (\Exception $e) {
......@@ -358,7 +352,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)
......@@ -378,7 +373,7 @@ class newsfeed implements Interfaces\Api
->setTitle($embeded->title)
->setBlurb($embeded->description)
->export()
)
)
->setMessage($message);
}
$save->setEntity($activity)
......@@ -416,7 +411,7 @@ class newsfeed implements Interfaces\Api
->setTitle($embeded->title)
->setBlurb($embeded->description)
->export()
)
)
->setMessage($message);
}
$save->setEntity($activity)
......@@ -525,7 +520,7 @@ class newsfeed implements Interfaces\Api
]);
}
}
$save->setEntity($activity)
->save();
......
<?php
/**
* Minds Payments API:: braintree
*
* @version 1
* @author Mark Harding
*/
namespace Minds\Controllers\api\v1\payments;
use Minds\Core;
use Minds\Helpers;
use Minds\Interfaces;
use Minds\Api\Factory;
use Minds\Core\Payments;
class braintree implements Interfaces\Api
{
/**
* Returns merchant information
* @param array $pages
*
* API:: /v1/merchant/:slug
*/
public function get($pages)
{
$response = [];
switch ($pages[0]) {
case "token":
$gateway = isset($pages[1]) ? $pages[1] : 'default';
$response['token'] = Payments\Factory::build('braintree', ['gateway'=>$gateway])->getToken();
break;
}
return Factory::response($response);
}
public function post($pages)
{
$response = [];
switch ($pages[0]) {
case "charge":
$amount = $_POST['amount'];
$fee = $amount * 0.05 + 0.30; //5% + $.30
if (!isset($_POST['merchant'])) {
$merchant = Core\Session::getLoggedInUser();
}
$sale = (new Payments\Sale())
->setAmount($amount)
->setMerchant($merchant)
->setFee($fee)
->setCustomerId(Core\Session::getLoggedInUser()->guid)
->setNonce($_POST['nonce']);
try {
$result = Payments\Factory::build('braintree', ['gateway'=>'merchants'])->setSale($sale);
} catch (\Exception $e) {
$response['status'] = "error";
$response['message'] = $e->getMessage();
}
break;
case "charge-master":
$amount = $_POST['amount'];
$sale = (new Payments\Sale())
->setAmount($amount)
->setCustomerId(Core\Session::getLoggedInUser()->guid)
->setSettle(true)
->setFee(0)
->setNonce($_POST['nonce']);
try {
$result = Payments\Factory::build('braintree', ['gateway'=>'merchants'])->setSale($sale);
} catch (\Exception $e) {
$response['status'] = "error";
$response['message'] = $e->getMessage();
}
break;
}
return Factory::response($response);
}
public function put($pages)
{
return Factory::response([]);
}
public function delete($pages)
{
return Factory::response([]);
}
}
......@@ -97,9 +97,6 @@ class subscriptions implements Interfaces\Api
}
$stripe = Core\Di\Di::_()->get('StripePayments');
$stripe->cancelSubscription($subscription);
} else {
$braintree = Payments\Factory::build("Braintree", ['gateway'=>'default']);
$braintree->cancelSubscription($subscription);
}
}
......
......@@ -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
/**
* Minds Webhook: Braintree
*
* @version 1
* @author Mark Harding
*/
namespace Minds\Controllers\api\v1\webhooks;
use Minds\Core;
use Minds\Entities;
use Minds\Interfaces;
use Minds\Api\Factory;
use Minds\Helpers;
use Minds\Core\Payments;
use Braintree_WebhookNotification;
class braintree implements Interfaces\Api, Interfaces\ApiIgnorePam
{
/**
* NOT AVAILABLE
*/
public function get($pages)
{
return Factory::response(['status'=>'error', 'message'=>'GET is not supported for this endpoint']);
}
/**
*/
public function post($pages)
{
error_log("[webhooks]:: hit first entrace point");
/*$gateway = isset($pages[0]) ? $pages[0] : 'default';
$bt = Payments\Factory::build('braintree', ['gateway'=>$gateway]);
$hooks = new Payments\Hooks();
$hooks->loadDefaults();
$webhooks = new Payments\Braintree\Webhooks($hooks, $bt);
$webhooks->setSignature($_POST['bt_signature'])
->setPayload($_POST['bt_payload'])
->run();*/
}
public function put($pages)
{
}
public function delete($pages)
{
}
}
......@@ -48,24 +48,9 @@ class stripe implements Interfaces\Api, Interfaces\ApiIgnorePam
$hooks->run();
// Do something with $event_json
http_response_code(200); // PHP 5.4 or greater
exit;
/*$gateway = isset($pages[0]) ? $pages[0] : 'default';
$bt = Payments\Factory::build('braintree', ['gateway'=>$gateway]);
$hooks = new Payments\Hooks();
$hooks->loadDefaults();
$webhooks = new Payments\Braintree\Webhooks($hooks, $bt);
$webhooks->setSignature($_POST['bt_signature'])
->setPayload($_POST['bt_payload'])
->run();*/
return Factory::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');
......
......@@ -48,8 +48,10 @@ class peer implements Interfaces\Api
case 'inbox':
default:
$review->setType(Core\Session::getLoggedinUser()->guid);
$boosts = $review->getReviewQueue(isset($_GET['limit']) ? $_GET['limit'] : 12,
isset($_GET['offset']) ? $_GET['offset'] : "");
$boosts = $review->getReviewQueue(
isset($_GET['limit']) ? $_GET['limit'] : 12,
isset($_GET['offset']) ? $_GET['offset'] : ""
);
$response['boosts'] = Factory::exportable($boosts['data']);
......
......@@ -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;
......
......@@ -56,6 +56,7 @@ class banners implements Interfaces\FS
$f->owner_guid = $entity->owner_guid ?: $entity->getOwnerObj()->guid;
$f->setFilename("group/{$entity->getGuid()}.jpg");
$f->open('read');
// no break
case "object":
break;
}
......
<?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\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;
}
}
}
}
......@@ -60,4 +60,12 @@ class EntityCentricRecord
$this->sums[$metric] = $this->sums[$metric] + $value;
return $this;
}
/**
* @return string
*/
public function getUrn(): string
{
return (string) implode('-', [ $this->getEntityUrn(), $this->getResolution(), $this->getTimestamp() ]);
}
}
......@@ -13,6 +13,8 @@ class Manager
{
/** @var array */
const SYNCHRONISERS = [
EngagementSynchroniser::class,
PartnerEarningsSynchroniser::class,
SignupsSynchroniser::class,
ActiveUsersSynchroniser::class,
ViewsSynchroniser::class,
......@@ -21,6 +23,9 @@ class Manager
/** @var Repository */
protected $repository;
/** @var Sums */
protected $sums;
/** @var int */
private $from;
......@@ -28,9 +33,11 @@ class Manager
private $to;
public function __construct(
$repository = null
$repository = null,
$sums = null
) {
$this->repository = $repository ?: new Repository();
$this->repository = $repository ?? new Repository();
$this->sums = $sums ?? new Sums();
}
/**
......@@ -81,4 +88,13 @@ class Manager
public function getAggregateByQuery(array $query): array
{
}
/**
* @param array $opts
* @retun iterable
*/
public function getListAggregatedByOwner(array $opts = []): iterable
{
return $this->sums->getByOwner($opts);
}
}
<?php
namespace Minds\Core\Analytics\EntityCentric;
use Minds\Core\Monetization\Partners\Manager as PartnersManager;
use DateTime;
use Exception;
class PartnerEarningsSynchroniser
{
/** @var PartnersManager */
private $partnersManager;
public function __construct($partnersManager = null)
{
$this->partnersManager = $partnersManager ?? new PartnersManager;
}
/**
* @param int $from
* @return self
*/
public function setFrom($from): self
{
$this->from = $from;
return $this;
}
/**
* Convert to records
* @return iterable
*/
public function toRecords(): iterable
{
$opts = [];
$opts['from'] = $this->from;
$records = [];
$i = 0;
while (true) {
$result = $this->partnersManager->getList($opts);
$opts['offset'] = $result->getPagingToken();
foreach ($result as $deposit) {
$urn = "urn:user:{$deposit->getUserGuid()}";
$record = new EntityCentricRecord();
$record->setEntityUrn($urn)
->setOwnerGuid($deposit->getUserGuid())
->setTimestamp($deposit->getTimestamp()) // TODO: confirm if this should be rounded to midnight
->setResolution('day');
// In order to increment sums, replace with what has already been seen
if (isset($records[$record->getUrn()])) {
$record = $records[$record->getUrn()];
}
$record->incrementSum('usd_earnings::total', $deposit->getAmountCents());
$record->incrementSum("usd_earnings::{$deposit->getItem()}", $deposit->getAmountCents());
$records[$record->getUrn()] = $record;
}
if ($result->isLastPage()) {
break;
}
}
foreach ($records as $record) {
var_dump($record);
yield $record;
}
}
}
<?php
/**
* EntityCentric Sums
* @author Mark
*/
namespace Minds\Core\Analytics\EntityCentric;
use DateTime;
use DateTimeZone;
use Exception;
use Minds\Common\Repository\Response;
use Minds\Core\Data\ElasticSearch\Client as ElasticClient;
use Minds\Core\Data\ElasticSearch;
use Minds\Core\Di\Di;
class Sums
{
/** @var ElasticClient */
protected $es;
/**
* Repository constructor.
* @param ElasticClient $es
*/
public function __construct(
$es = null
) {
$this->es = $es ?: Di::_()->get('Database\ElasticSearch');
}
public function getByOwner(array $opts = []): iterable
{
$opts = array_merge([
'fields' => [],
'from' => time(),
], $opts);
$must = [];
$must[] = [
'range' => [
'@timestamp' => [
'gte' => $opts['from'] * 1000,
'lt' => strtotime('+1 day', $opts['from']) * 1000,
],
],
];
$termsAgg = [];
foreach ($opts['fields'] as $field) {
$termsAgg[$field] = [
'sum' => [
'field' => $field,
],
];
$must[] = [
'exists' => [
'field' => $field,
],
];
}
$partition = 0;
$partitions = 100;
$partitionSize = 5000; // Allows for 500,000 users
while (++$partition < $partitions) {
// Do the query
$query = [
'index' => 'minds-entitycentric-*',
'size' => 0,
'body' => [
'query' => [
'bool' => [
'must' => $must,
],
],
'aggs' => [
'1' => [
'terms' => [
'field' => 'owner_guid',
'min_doc_count' => 1,
'size' => $partitionSize,
'include' => [
'partition' => $partition,
'num_partitions' => $partitions,
],
],
'aggs' => $termsAgg,
],
],
],
];
// Query elasticsearch
$prepared = new ElasticSearch\Prepared\Search();
$prepared->query($query);
$response = $this->es->request($prepared);
foreach ($response['aggregations']['1']['buckets'] as $bucket) {
yield $bucket;
}
}
}
}
......@@ -49,7 +49,7 @@ class Retention implements AnalyticsMetric
$startTs = $timestamps[$x+1];
$signups = [];
$offset = "";
echo "\n Gathering signups \n";
// echo "\n Gathering signups \n";
while (true) {
$data = $this->db->getRow("analytics:signup:day:$startTs", ['limit'=>200, 'offset' => $offset]);
if (count($data) <= 1) {
......@@ -61,13 +61,13 @@ class Retention implements AnalyticsMetric
$offset = $k;
}
}
echo " (done)";
// echo " (done)";
//now get active users from each interval after this date
$endTs = $timestamps[$x-$x+1];
//echo "[$x]:: actives: " . date('d-m-Y', $endTs) . " signups: " . date('d-m-Y', $startTs) . "\n";
$offset = "";
echo "\n Gathering actives \n";
// echo "\n Gathering actives \n";
foreach ($signups as $signup => $ts) {
if ($this->wasActive($signup, $now)) {
$this->db->insert("{$this->namespace}:$x:$now", [$signup=>time()]);
......@@ -76,7 +76,7 @@ class Retention implements AnalyticsMetric
echo "\r $x: $signup (not active) $offset";
}
}
echo "(done)";
// echo "(done)";
}
return true;
......
......@@ -74,7 +74,7 @@ class TokenSaleEvent implements BlockchainEventInterface
$purchase = $this->manager->getPurchase($transaction->getData()['phone_number_hash'], $transaction->getTx());
if (!$purchase) {
echo "purchase not found";
error_log("purchase not found");
return; //purchase not found
}
......
......@@ -85,11 +85,13 @@ class Pending
public function delete($type, $tx_id)
{
$query = new Core\Data\Cassandra\Prepared\Custom();
$query->query("DELETE FROM blockchain_pending WHERE type = ? AND tx_id = ?",
$query->query(
"DELETE FROM blockchain_pending WHERE type = ? AND tx_id = ?",
[
(string) $type,
(string) $tx_id
]);
]
);
try {
return (bool) $this->db->request($query);
......
......@@ -46,14 +46,16 @@ class Sums
$query = new Custom();
if ($this->user) {
$query->query("SELECT
$query->query(
"SELECT
SUM(amount) as balance
FROM blockchain_transactions_mainnet_by_address
WHERE user_guid = ?
AND wallet_address = 'offchain'",
[
new Varint((int) $this->user->guid)
]);
]
);
$query->setOpts([
'consistency' => \Cassandra::CONSISTENCY_ALL
]);
......
......@@ -49,14 +49,16 @@ class TestnetSums
throw new \Exception('User is not set');
}
$query->query("SELECT
$query->query(
"SELECT
SUM(amount) as balance
FROM blockchain_transactions_by_address
WHERE user_guid = ?
AND wallet_address = 'offchain'",
[
new Varint((int) $this->user->guid)
]);
]
);
$query->setOpts([
'consistency' => \Cassandra::CONSISTENCY_ALL
]);
......
......@@ -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;
}
});
}
}
......@@ -47,7 +47,8 @@ class Navigation
->setVisibility(0); //only show for loggedin
$link = new Item();
NavigationManager::add($link
NavigationManager::add(
$link
->setPriority(4)
->setIcon('subject')
->setName('Blogs')
......
......@@ -15,11 +15,18 @@ class Boost extends Feed
$iterator
->setLimit(10)
->setOffset($this->offset)
->setRating($this->rating)
->setQuality($this->quality)
->setType($this->type)
->setHydrate(false);
if (!is_null($this->currentUser)) {
$iterator->setUserGuid($this->currentUser->getGUID());
if (!is_null($this->rating)) {
$iterator->setRating($this->currentUser->getBoostRating());
}
}
/** @var Core\Boost\Network\Boost $boost */
foreach ($iterator as $boost) {
$boostUrn = new Urn("urn:boost:{$boost->getType()}:{$boost->getGuid()}");
......
......@@ -58,6 +58,15 @@ class Boost
{
use MagicAttributes;
const STATE_COMPLETED = 'completed';
const STATE_REJECTED = 'rejected';
const STATE_APPROVED = 'approved';
const STATE_REVOKED = 'revoked';
const STATE_CREATED = 'created';
const TYPE_NEWSFEED = 'newsfeed';
const TYPE_CONTENT = 'content';
/** @var int $guid */
private $guid;
......@@ -133,18 +142,18 @@ class Boost
public function getState()
{
if ($this->completedTimestamp) {
return 'completed';
return self::STATE_COMPLETED;
}
if ($this->rejectedTimestamp) {
return 'rejected';
return self::STATE_REJECTED;
}
if ($this->reviewedTimestamp) {
return 'approved';
return self::STATE_APPROVED;
}
if ($this->revokedTimestamp) {
return 'revoked';
return self::STATE_REVOKED;
}
return 'created';
return self::STATE_CREATED;
}
/**
......@@ -186,4 +195,29 @@ class Boost
'transaction_id' => $this->transactionId,
];
}
/* TODO - Spec Test this */
/**
* Validate the boost type string
* @param string $type
* @return bool
*/
public static function validType(string $type): bool
{
$validTypes = [
self::TYPE_CONTENT,
self::TYPE_NEWSFEED
];
return in_array($type, $validTypes);
}
/**
* Returns true if Boost has an entity object set
* @return bool
*/
public function hasEntity(): bool
{
return !is_null($this->entity);
}
}
This diff is collapsed.
......@@ -50,7 +50,6 @@ class Manager
public function getList($opts = []): Response
{
$opts = array_merge([
'hydrate' => true,
'useElastic' => false,
'state' => null,
], $opts);
......@@ -97,6 +96,7 @@ class Manager
/**
* Get a single boost
* @param string $urn
* @param array $opts
* @return Boost
*/
public function get($urn, $opts = [])
......
......@@ -97,10 +97,12 @@ class Repository
return $this;
}
foreach ($this->categories as $category) {
$query->query("INSERT INTO categories
$query->query(
"INSERT INTO categories
(type, category, filter, guid)
VALUES (?, ?, ?, ?)",
[ $this->type, $category, $this->filter, (string) $guid ]);
[ $this->type, $category, $this->filter, (string) $guid ]
);
try {
$result = $this->db->request($query);
} catch (\Exception $e) {
......@@ -116,9 +118,11 @@ class Repository
return $this;
}
foreach ($this->categories as $category) {
$query->query("DELETE FROM categories
$query->query(
"DELETE FROM categories
WHERE type = ? AND category = ? AND filter = ? AND guid = ?",
[ $this->type, $category, $this->filter, (string) $guid ]);
[ $this->type, $category, $this->filter, (string) $guid ]
);
try {
$result = $this->db->request($query);
} catch (\Exception $e) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.