...
 
Commits (2)
......@@ -34,4 +34,42 @@ class Moderation extends Cli\Controller implements Interfaces\CliControllerInter
$manager->run($this->getOpt('jury') ?? 'initial');
}
public function summon()
{
error_reporting(E_ALL);
ini_set('display_errors', 1);
/** @var Core\Reports\Repository $reportsRepository */
$reportsRepository = Di::_()->get('Reports\Repository');
/** @var Core\Reports\Summons\Manager $summonsManager */
$summonsManager = Di::_()->get('Moderation\Summons\Manager');
$userId = $this->getOpt('user');
$reportUrn = $this->getOpt('report');
if (!$userId || !$reportUrn) {
$this->out('Usage: cli.php moderation summon --user=<username_or_guid> --report=<report_urn>');
exit(1);
}
$user = new Entities\User($userId, false);
if (!$user || !$user->guid) {
$this->out('Error: Invalid user');
exit(1);
}
$report = $reportsRepository->get($reportUrn);
if (!$report) {
$this->out('Error: Invalid report');
exit(1);
}
$appeal = new Core\Reports\Appeals\Appeal();
$appeal->setReport($report);
$summonsManager->summon($appeal, [ $user->guid ]);
}
}
<?php
/**
* summon
*
* @author edgebal
*/
namespace Minds\Controllers\api\v2\moderation;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Reports\Repository as ReportsRepository;
use Minds\Core\Reports\Summons\Manager;
use Minds\Core\Reports\Summons\Summon as SummonEntity;
use Minds\Core\Session;
use Minds\Interfaces;
class summon implements Interfaces\Api
{
/**
* 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)
{
$reportUrn = $_POST['report_urn'] ?? null;
$juryType = $_POST['jury_type'] ?? null;
$userGuid = Session::getLoggedInUserGuid();
$status = $_POST['status'] ?? null;
/** @var Manager $summonsManager */
$summonsManager = Di::_()->get('Moderation\Summons\Manager');
/** @var ReportsRepository $reportsRepository */
$reportsRepository = Di::_()->get('Reports\Repository');
$summon = new SummonEntity();
try {
$summon
->setReportUrn($reportUrn)
->setJuryType($juryType)
->setJurorGuid((string) $userGuid)
->setStatus($status);
} catch (\Exception $e) {
return Factory::response([
'status' => 'error',
'message' => $e->getMessage(),
]);
}
if (!$summonsManager->isSummoned($summon)) {
return Factory::response([
'status' => 'error',
'message' => 'You\'re not summoned',
]);
}
$summonsManager->respond($summon);
$response = [
'summon' => $summon->getStatus(),
'expires_in' => $summon->getTtl(),
];
if ($summon->isAccepted()) {
$response['report'] = $reportsRepository->get($summon->getReportUrn());
}
return Factory::response($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
/**
* ReportsAppealSummon
*
* @author edgebal
*/
namespace Minds\Core\Queue\Runners;
use Minds\Core\Di\Di;
use Minds\Core\Queue\Message;
use Minds\Core\Queue\Client;
use Minds\Core\Queue\Interfaces\QueueClient;
use Minds\Core\Queue\Interfaces\QueueRunner;
use Minds\Core\Reports\Summons\Manager;
class ReportsAppealSummon implements QueueRunner
{
/**
* Run the queue
* @return void
* @throws \Exception
*/
public function run()
{
/** @var QueueClient $client */
$client = Client::build();
$client
->setQueue(static::class)
->receive(function (Message $data) {
$params = $data->getData();
$appeal = $params['appeal'] ?? null;
$cohort = $params['cohort'] ?? null;
if (!$appeal) {
echo 'Invalid empty appeal' . PHP_EOL;
return;
}
/** @var Manager $manager */
$manager = Di::_()->get('Moderation\Summons\Manager');
$manager->summon($appeal, $cohort);
});
}
}
......@@ -4,25 +4,27 @@
*/
namespace Minds\Core\Reports\Appeals;
use Minds\Core\Reports\Report;
use Minds\Traits\MagicAttributes;
/**
* @method Report getOwnerGuid(): long
* @method Report getReport(): Report
* @method Report getTimestamp: int
* @method Report getNote(): int
* @method int|string getOwnerGuid()
* @method Report getReport()
* @method Appeal setReport(Report $report)
* @method int getTimestamp
* @method string getNote()
*/
class Appeal
{
use MagicAttributes;
/** @var long $timestamp -< in ms*/
/** @var int $timestamp -< in ms*/
private $timestamp;
/** @var Report $report */
private $report;
/** @var int $note */
/** @var string $note */
private $note;
/**
......
<?php
/**
* SummonDelegate
*
* @author edgebal
*/
namespace Minds\Core\Reports\Appeals\Delegates;
use Minds\Core\Queue\Client;
use Minds\Core\Queue\Interfaces\QueueClient;
use Minds\Core\Queue\Runners\ReportsAppealSummon;
use Minds\Core\Reports\Appeals\Appeal;
class SummonDelegate
{
/** @var QueueClient */
protected $queue;
public function __construct(
$queue = null
)
{
$this->queue = $queue ?: Client::build();
}
public function onAppeal(Appeal $appeal)
{
$this->queue
->setQueue(ReportsAppealSummon::class)
->send([
'appeal' => $appeal,
'cohort' => null, // TODO: It can be an array of user guids. For development purposes only.
]);
}
}
......@@ -20,26 +20,32 @@ class Manager
/** @var Repository $repository */
private $repository;
/** @var NotificationDelegate $notificationDelegate */
private $notificationDelegate;
/** @var EntitiesResolver $entitiesResolver */
private $entitiesResolver;
/** @var Delegates\NotificationDelegate $notificationDelegate */
private $notificationDelegate;
/** @var Delegates\SummonDelegate $summonDelegate */
private $summonDelegate;
public function __construct(
$repository = null,
$entitiesResolver = null,
$notificationDelegate = null
$notificationDelegate = null,
$summonDelegate = null
)
{
$this->repository = $repository ?: new Repository;
$this->entitiesResolver = $entitiesResolver ?: new EntitiesResolver;
$this->notificationDelegate = $notificationDelegate ?: new Delegates\NotificationDelegate;
$this->summonDelegate = $summonDelegate ?: new Delegates\SummonDelegate();
}
/**
* @param array $opts
* @return Response
* @throws \Exception
*/
public function getList($opts = [])
{
......@@ -77,6 +83,7 @@ class Manager
$added = $this->repository->add($appeal);
$this->summonDelegate->onAppeal($appeal);
$this->notificationDelegate->onAction($appeal);
return $added;
......
......@@ -41,5 +41,9 @@ class Provider extends DiProvider
$this->di->bind('Moderation\Strikes\Manager', function ($di) {
return new Strikes\Manager;
}, [ 'useFactory'=> true ]);
$this->di->bind('Moderation\Summons\Manager', function ($di) {
return new Summons\Manager();
}, [ 'useFactory'=> true ]);
}
}
......@@ -4,23 +4,24 @@
*/
namespace Minds\Core\Reports;
use Minds\Core\Reports\UserReport;
use Minds\Core\Reports\Jury\Decision;
use Minds\Core\Reports\UserReports\UserReport;
use Minds\Entities\Entity;
use Minds\Traits\MagicAttributes;
/**
* Class Report
* @method Report getEntityGuid(): long
* @method Report getEntityUrn(): string
* @method Report getReports(): []
* @method Report getEntity(): Entity
* @method Report isAppeal(): boolean
* @method Report getInitialJuryDecisions: []
* @method Report getAppealJuryDecisions: []
* @method Report getAppealTimestamp: int
* @method Report getReasonCode(): int
* @method Report getSubReasonCode(): int
* @method int getEntityGuid()
* @method string getEntityUrn()
* @method UserReport[] getReports()
* @method Entity getEntity()
* @method boolean isAppeal()
* @method Decision[] getInitialJuryDecisions()
* @method Decision[] getAppealJuryDecisions()
* @method int getAppealTimestamp()
* @method int getReasonCode()
* @method int getSubReasonCode()
* @method Report setState(string $string)
* @method Report getState(): string
* @method Report setTimestamp(int $timestamp)
* @method Report setReasonCode(int $value)
* @method Report setSubReasonCode(int $value)
......
......@@ -138,7 +138,8 @@ class Repository
/**
* Return a single report
* @param string $urn
* @return ReportEntity
* @return Report
* @throws \Exception
*/
public function get($urn)
{
......
<?php
/**
* Cohort
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons;
use Minds\Core\Data\ElasticSearch\Client as ElasticsearchClient;
use Minds\Core\Data\ElasticSearch\Prepared\Search;
use Minds\Core\Di\Di;
use Minds\Helpers\Text;
class Cohort
{
/** @var ElasticsearchClient */
protected $elasticsearch;
/** @var string */
protected $index;
/**
* Repository constructor.
* @param ElasticsearchClient $elasticsearch
* @param string $index
*/
public function __construct(
$elasticsearch = null,
$index = null
)
{
$this->elasticsearch = $elasticsearch ?: Di::_()->get('Database\ElasticSearch');
$this->index = $index ?: 'minds-metrics-*';
}
/**
* @param array $opts
* @return \Generator
* @yields string
*/
public function getList(array $opts = [])
{
$opts = array_merge([
'for' => null,
'active_threshold' => 0,
'platform' => null,
'validated' => false,
'limit' => 10,
'offset' => 0,
], $opts);
$now = (int) (microtime(true) * 1000);
$fromTimestamp = $now - ($opts['active_threshold'] * 1000);
$body = [
'_source' => [
'user_guid',
],
'query' => [
'bool' => [
'must' => [
[
'range' => [
'@timestamp' => [
'gte' => $fromTimestamp,
],
],
],
[
'term' => [
'type' => 'action',
],
],
],
],
],
'aggs' => [
'entities' => [
'terms' => [
'field' => 'user_guid.keyword',
'size' => $opts['limit'],
],
],
],
'size' => 0,
];
if ($opts['platform']) {
$body['query']['bool']['must'][] = [
'terms' => [
'platform' => Text::buildArray($opts['platform']),
],
];
}
if ($opts['for']) {
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
}
$body['query']['bool']['must_not'][] = [
'term' => [
'user_guid' => (string) $opts['for'],
],
];
$body['query']['bool']['must_not'][] = [
'terms' => [
'user_guid' => [
'index' => 'minds-graph',
'type' => 'subscriptions',
'id' => (string) $opts['for'],
'path' => 'guids',
],
],
];
}
if ($opts['validated']) {
$body['query']['bool']['must'][] = [
'exists' => [
'field' => 'user_phone_number_hash',
],
];
if (!isset($body['query']['bool']['must_not'])) {
$body['query']['bool']['must_not'] = [];
}
$body['query']['bool']['must_not'][] = [
'term' => [
'user_phone_number_hash' => '',
],
];
}
$query = [
'index' => $this->index,
'type' => 'action',
'body' => $body,
'size' => $opts['limit'],
'from' => $opts['offset'],
];
$prepared = new Search();
$prepared->query($query);
$result = $this->elasticsearch->request($prepared);
foreach ($result['aggregations']['entities']['buckets'] as $bucket) {
yield $bucket['key'];
}
}
}
<?php
/**
* SocketDelegate
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons\Delegates;
use Minds\Core\Reports\Summons\Summon;
use Minds\Core\Sockets\Events as SocketEvents;
class SocketDelegate
{
/** @var SocketEvents */
protected $socketEvents;
/**
* SocketDelegate constructor.
* @param SocketEvents $socketEvents
*/
public function __construct(
$socketEvents = null
)
{
$this->socketEvents = $socketEvents ?: new SocketEvents();
}
/**
* @param Summon $summon
* @throws \Exception
*/
public function onSummon(Summon $summon)
{
$this->socketEvents
->setUser($summon->getJurorGuid())
->emit('moderation_summon', json_encode($summon));
}
}
<?php
/**
* Manager
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons;
use Minds\Core\Reports\Appeals\Appeal;
use Minds\Core\Reports\Summons\Delegates;
class Manager
{
/** @var Cohort $cohort */
protected $cohort;
/** @vat Repository $repository */
protected $repository;
/** @var Delegates\SocketDelegate $socketDelegate */
protected $socketDelegate;
/**
* Manager constructor.
* @param Cohort $cohort
* @param Repository $repository
* @param Delegates\SocketDelegate $socketDelegate
*/
public function __construct(
$cohort = null,
$repository = null,
$socketDelegate = null
)
{
$this->cohort = $cohort ?: new Cohort();
$this->repository = $repository ?: new Repository();
$this->socketDelegate = $socketDelegate ?: new Delegates\SocketDelegate();
}
/**
* @param Appeal $appeal
* @param array $cohort
* @throws \Exception
*/
public function summon(Appeal $appeal, $cohort = null)
{
$cohort = $cohort ?: $this->cohort->getList([
'active_threshold' => 5 * 60,
'platform' => 'browser',
'for' => $appeal->getOwnerGuid(),
'validated' => true,
'limit' => 12,
]);
foreach ($cohort as $juror) {
$summon = new Summon();
$summon
->setReportUrn($appeal->getReport()->getUrn())
->setJuryType('appeal_jury')
->setJurorGuid($juror)
->setTtl(120)
->setStatus('awaiting');
$this->repository->add($summon);
$this->socketDelegate->onSummon($summon);
}
}
/**
* @param Summon $summon
* @return bool
*/
public function isSummoned(Summon $summon)
{
return $this->repository->exists($summon);
}
/**
* @param Summon $summon
* @return Summon
*/
public function respond(Summon $summon)
{
$summon
->setTtl(10 * 60);
$this->repository->add($summon);
return $summon;
}
}
<?php
/**
* Repository
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons;
use Cassandra\Bigint;
use Minds\Core\Data\Cassandra\Client as CassandraClient;
use Minds\Core\Data\Cassandra\Prepared\Custom;
use Minds\Core\Di\Di;
class Repository
{
/** @var CassandraClient */
protected $db;
/**
* Repository constructor.
* @param CassandraClient $db
*/
public function __construct(
$db = null
)
{
$this->db = $db ?: Di::_()->get('Database\Cassandra\Cql');
}
/**
* @param array $opts
* @throws \NotImplementedException
*/
public function getList(array $opts = [])
{
throw new \NotImplementedException();
}
/**
* @param Summon $summon
* @return bool
*/
public function add(Summon $summon)
{
$expires = time() + ((int) $summon->getTtl());
$cql = "INSERT INTO moderation_summons (report_urn, jury_type, juror_guid, status, expires) VALUES (?, ?, ?, ?, ?) USING TTL ?";
$values = [
$summon->getReportUrn(),
$summon->getJuryType(),
new Bigint($summon->getJurorGuid()),
$summon->getStatus(),
$expires,
(int) $summon->getTtl(),
];
$prepared = new Custom();
$prepared->query($cql, $values);
try {
return (bool) $this->db->request($prepared, true);
} catch (\Exception $e) {
error_log($e);
return false;
}
}
/**
* @param Summon $summon
* @return bool
*/
public function exists(Summon $summon)
{
$cql = "SELECT COUNT(*) as total FROM moderation_summons WHERE report_urn = ? AND jury_type = ? AND juror_guid = ?";
$values = [
$summon->getReportUrn(),
$summon->getJuryType(),
new Bigint($summon->getJurorGuid()),
];
$prepared = new Custom();
$prepared->query($cql, $values);
try {
$response = $this->db->request($prepared);
return $response[0]['total'] > 0;
} catch (\Exception $e) {
error_log($e);
return false;
}
}
/**
* @param Summon $summon
* @throws \NotImplementedException
*/
public function delete(Summon $summon)
{
throw new \NotImplementedException();
}
}
<?php
/**
* Summon
*
* @author edgebal
*/
namespace Minds\Core\Reports\Summons;
use Minds\Traits\MagicAttributes;
/**
* Class Summon
* @package Minds\Core\Reports\Summons
* @method string getReportUrn()
* @method Summon setReportUrn(string $reportUrn)
* @method string getJuryType()
* @method Summon setJuryType(string $juryType)
* @method int|string getJurorGuid()
* @method Summon setJurorGuid(int|string $jurorGuid)
* @method string getStatus()
* @method int getTtl()
* @method Summon setTtl(int $ttl)
*/
class Summon implements \JsonSerializable
{
use MagicAttributes;
/** @var string */
protected $reportUrn;
/** @var string */
protected $juryType;
/** @var int|string */
protected $jurorGuid;
/** @var $status */
protected $status;
/** @var int */
protected $ttl;
/**
* @param string $status
* @return $this
* @throws \Exception
*/
public function setStatus($status)
{
if (!in_array($status, ['awaiting', 'accepted', 'declined'])) {
throw new \Exception('Invalid status');
}
$this->status = $status;
return $this;
}
/**
* @return bool
*/
public function isAwaiting()
{
return $this->status === 'awaiting';
}
/**
* @return bool
*/
public function isAccepted()
{
return $this->status === 'accepted';
}
/**
* @return bool
*/
public function isDeclined()
{
return $this->status === 'declined';
}
/**
* @return array
*/
public function export()
{
return [
'report_urn' => $this->reportUrn,
'jury_type' => $this->juryType,
'juror_guid' => (string) $this->jurorGuid,
'status' => $this->status,
'ttl' => $this->ttl,
];
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return $this->export();
}
}
......@@ -18,17 +18,20 @@ class ManagerSpec extends ObjectBehavior
private $repository;
private $entitiesResolver;
private $notificationDelegate;
private $summonDelegate;
function let(
Repository $repository,
EntitiesResolver $entitiesResolver,
Delegates\NotificationDelegate $notificationDelegate
Delegates\NotificationDelegate $notificationDelegate,
Delegates\SummonDelegate $summonDelegate
)
{
$this->beConstructedWith($repository, $entitiesResolver, $notificationDelegate);
$this->beConstructedWith($repository, $entitiesResolver, $notificationDelegate, $summonDelegate);
$this->repository = $repository;
$this->entitiesResolver = $entitiesResolver;
$this->notificationDelegate = $notificationDelegate;
$this->summonDelegate = $summonDelegate;
}
function it_is_initializable()
......@@ -84,6 +87,10 @@ class ManagerSpec extends ObjectBehavior
$this->notificationDelegate->onAction($appeal)
->shouldBeCalled();
$this->summonDelegate
->onAppeal(Argument::type(Appeal::class))
->shouldBeCalled();
$appeal->getReport()
->willReturn($report);
......