Skip to content

Commit df6533d

Browse files
FaustoJohnmarkharding
authored andcommitted
#2192 - Account quality score
1 parent b403761 commit df6533d

15 files changed

+475
-99
lines changed

Core/AccountQuality/Controller.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\AccountQuality\ResponseBuilders\GetAccountQualityScoreResponseBuilder;
6+
use Minds\Core\AccountQuality\Validators\GetAccountQualityScoreRequestValidator;
7+
use Minds\Exceptions\UserErrorException;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Zend\Diactoros\Response\JsonResponse;
10+
11+
/**
12+
* The controller for the Account Quality module
13+
*/
14+
class Controller
15+
{
16+
public function __construct(
17+
private ?ManagerInterface $manager = null
18+
) {
19+
$this->manager = $this->manager ?? new Manager();
20+
}
21+
22+
/**
23+
* Http route: api/v3/account-quality/:targetUserGuid
24+
* @param ServerRequestInterface $request
25+
* @return JsonResponse
26+
* @throws UserErrorException
27+
*/
28+
public function getAccountQualityScore(ServerRequestInterface $request): JsonResponse
29+
{
30+
$parameters = $request->getAttributes()["parameters"];
31+
$requestValidator = new GetAccountQualityScoreRequestValidator();
32+
33+
$responseBuilder = new GetAccountQualityScoreResponseBuilder();
34+
35+
if (!$requestValidator->validate($parameters)) {
36+
return $responseBuilder->buildBadRequestResponse($requestValidator->getErrors());
37+
}
38+
39+
$results = $this->manager->getAccountQualityScore($parameters['targetUserGuid']);
40+
41+
return $responseBuilder->buildSuccessfulResponse($results);
42+
}
43+
}

Core/AccountQuality/Manager.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\AccountQuality\Models\UserQualityScore;
6+
7+
/**
8+
* Responsible for the business logic in order to retrieve the relevant details required to the controller
9+
*/
10+
class Manager implements ManagerInterface
11+
{
12+
public function __construct(
13+
private ?RepositoryInterface $repository = null
14+
) {
15+
$this->repository = $this->repository ?? new Repository();
16+
}
17+
18+
/**
19+
* Retrieves the account quality score based on the userId provided
20+
* @param string $userId
21+
* @return UserQualityScore
22+
*/
23+
public function getAccountQualityScore(string $userId): UserQualityScore
24+
{
25+
return $this->repository->getAccountQualityScore($userId);
26+
}
27+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\AccountQuality\Models\UserQualityScore;
6+
7+
interface ManagerInterface
8+
{
9+
/**
10+
* Retrieves the account quality score based on the userId provided
11+
* @param string $userId
12+
* @return UserQualityScore
13+
*/
14+
public function getAccountQualityScore(string $userId): UserQualityScore;
15+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality\Models;
4+
5+
use Minds\Core\Config\Config;
6+
use Minds\Core\Di\Di;
7+
use Minds\Traits\MagicAttributes;
8+
9+
/**
10+
* Entity representing the user quality score.
11+
* @method float|null getScore()
12+
* @method self setScore(float $score)
13+
* @method string getCategory()
14+
* @method self setCategory(string $category)
15+
*/
16+
class UserQualityScore
17+
{
18+
use MagicAttributes;
19+
20+
private ?float $score = null;
21+
private string $category = "";
22+
23+
public function __construct(
24+
private ?Config $config = null
25+
) {
26+
$this->config ??= Di::_()->get("Config");
27+
}
28+
29+
/**
30+
* Checks if the score is below the risk threshold of being considered a spam account
31+
* @return bool
32+
*/
33+
public function isBelowSpamRiskThreshold(): bool
34+
{
35+
return is_numeric($this->score) && $this->score < $this->config->get("user_quality_score")['belowSpamRiskThreshold'];
36+
}
37+
}

Core/AccountQuality/Module.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Interfaces\ModuleInterface;
6+
7+
class Module implements ModuleInterface
8+
{
9+
public array $submodules = [];
10+
11+
public function onInit()
12+
{
13+
(new Provider())->register();
14+
(new Routes())->register();
15+
}
16+
}

Core/AccountQuality/Provider.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\Di\Provider as DiProvider;
6+
7+
class Provider extends DiProvider
8+
{
9+
public function register(): void
10+
{
11+
$this->di->bind('AccountQuality\Manager', function ($di) {
12+
return new Manager();
13+
});
14+
$this->di->bind('AccountQuality\Controller', function ($di) {
15+
return new Controller();
16+
});
17+
}
18+
}

Core/AccountQuality/Repository.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\AccountQuality\Models\UserQualityScore;
6+
use Minds\Core\Data\Cassandra\Client as CassandraClient;
7+
use Minds\Core\Data\Cassandra\Prepared\Custom as CustomQuery;
8+
use Minds\Core\Di\Di;
9+
10+
/**
11+
* Responsible to fetch the data from the relevant data sources
12+
*/
13+
class Repository implements RepositoryInterface
14+
{
15+
private const TABLE_NAME = "user_quality_scores";
16+
17+
public function __construct(
18+
private ?CassandraClient $cassandraClient = null
19+
) {
20+
$this->cassandraClient = $this->cassandraClient ?? Di::_()->get('Database\Cassandra\Cql');
21+
}
22+
23+
/**
24+
* Retrieves the account quality score based on the userId provided
25+
* @param string $userId
26+
* @return UserQualityScore
27+
*/
28+
public function getAccountQualityScore(string $userId): UserQualityScore
29+
{
30+
$statement = "SELECT score, category
31+
FROM
32+
" . self::TABLE_NAME . "
33+
WHERE
34+
user_id = ?
35+
LIMIT 1";
36+
37+
$query = $this->prepareQuery($statement, [$userId]);
38+
39+
$results = $this->cassandraClient->request($query);
40+
$entry = $results->first();
41+
return (new UserQualityScore())
42+
->setScore((float)$entry["score"])
43+
->setCategory($entry["category"]);
44+
}
45+
46+
/**
47+
* Returns a Cassandra prepared statement using the query and values provided
48+
* @param string $statement
49+
* @param array $values The values for the parameters in the query statement
50+
* @return CustomQuery
51+
*/
52+
private function prepareQuery(string $statement, array $values): CustomQuery
53+
{
54+
$query = new CustomQuery();
55+
$query->query($statement, $values);
56+
57+
return $query;
58+
}
59+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\AccountQuality\Models\UserQualityScore;
6+
7+
interface RepositoryInterface
8+
{
9+
/**
10+
* Retrieves the account quality score based on the userId provided
11+
* @param string $userId
12+
* @return UserQualityScore
13+
*/
14+
public function getAccountQualityScore(string $userId): UserQualityScore;
15+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality\ResponseBuilders;
4+
5+
use Minds\Core\AccountQuality\Models\UserQualityScore;
6+
use Minds\Entities\ValidationErrorCollection;
7+
use Minds\Exceptions\UserErrorException;
8+
use Zend\Diactoros\Response\JsonResponse;
9+
10+
/**
11+
* Builds the relevant responses for GET api/v3/account-quality/:targetUserId
12+
*/
13+
class GetAccountQualityScoreResponseBuilder
14+
{
15+
/**
16+
* Builds the successful response object for the request
17+
* @param UserQualityScore $response
18+
* @return JsonResponse
19+
*/
20+
public function buildSuccessfulResponse(UserQualityScore $response): JsonResponse
21+
{
22+
return new JsonResponse([
23+
'status' => 'success',
24+
'results' => [
25+
'score' => abs($response->getScore() - 1)
26+
]
27+
]);
28+
}
29+
30+
/**
31+
* @param ValidationErrorCollection $errors
32+
* @return JsonResponse
33+
* @throws UserErrorException
34+
*/
35+
public function buildBadRequestResponse(ValidationErrorCollection $errors): JsonResponse
36+
{
37+
throw new UserErrorException(
38+
"Some validation errors have been found with the request.",
39+
400,
40+
$errors
41+
);
42+
}
43+
}

Core/AccountQuality/Routes.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality;
4+
5+
use Minds\Core\Di\Ref;
6+
use Minds\Core\Router\Middleware\AdminMiddleware;
7+
use Minds\Core\Router\ModuleRoutes;
8+
use Minds\Core\Router\Route;
9+
10+
class Routes extends ModuleRoutes
11+
{
12+
public function register(): void
13+
{
14+
$this->route
15+
->withPrefix('api/v3/account-quality')
16+
->do(function (Route $route) {
17+
// Temporarily accessible only to Admins
18+
$route
19+
->withMiddleware([
20+
AdminMiddleware::class
21+
])
22+
->do(function (Route $route) {
23+
$route->get(
24+
'',
25+
Ref::_('AccountQuality\Controller', 'getAccountQualityScores')
26+
);
27+
$route->get(
28+
':targetUserGuid',
29+
Ref::_('AccountQuality\Controller', 'getAccountQualityScore')
30+
);
31+
});
32+
});
33+
}
34+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Minds\Core\AccountQuality\Validators;
4+
5+
use Minds\Entities\ValidationError;
6+
use Minds\Entities\ValidationErrorCollection;
7+
use Minds\Helpers\UserValidator;
8+
use Minds\Interfaces\ValidatorInterface;
9+
10+
/**
11+
* Responsible to run validation of the GET /api/v3/account-quality request
12+
*/
13+
class GetAccountQualityScoreRequestValidator implements ValidatorInterface
14+
{
15+
private ?ValidationErrorCollection $errors;
16+
17+
private function reset(): void
18+
{
19+
$this->errors = new ValidationErrorCollection();
20+
}
21+
22+
public function validate(array $dataToValidate): bool
23+
{
24+
$this->reset();
25+
26+
if (empty($dataToValidate['targetUserGuid'])) {
27+
$this->errors->add(new ValidationError("targetUserId", "The user guid must be provided."));
28+
}
29+
30+
if (!UserValidator::isValidUserId($dataToValidate['targetUserGuid'])) {
31+
$this->errors->add(new ValidationError("targetUserId", "The user guid must be a valid user id."));
32+
}
33+
34+
return $this->errors->count() == 0;
35+
}
36+
37+
public function getErrors(): ?ValidationErrorCollection
38+
{
39+
return $this->errors;
40+
}
41+
}

Core/Minds.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Minds extends base
6262
Votes\Module::class,
6363
Helpdesk\Zendesk\Module::class,
6464
SocialCompass\Module::class,
65+
AccountQuality\Module::class,
6566
Recommendations\Module::class
6667
];
6768

Core/Provisioner/Provisioners/cassandra-provision.cql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,3 +1717,12 @@ CREATE TABLE minds.social_compass_answers (
17171717
current_value int,
17181718
PRIMARY KEY (user_guid, question_id)
17191719
);
1720+
1721+
CREATE TABLE minds.user_quality_scores
1722+
(
1723+
user_id text,
1724+
timestamp timestamp,
1725+
category text,
1726+
score float,
1727+
PRIMARY KEY (user_id, timestamp)
1728+
) WITH CLUSTERING ORDER BY (timestamp DESC);

Helpers/UserValidator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Minds\Helpers;
4+
5+
/**
6+
* User validation helper methods
7+
*/
8+
class UserValidator
9+
{
10+
public static function isValidUserId(string $userId): bool
11+
{
12+
return is_numeric($userId);
13+
}
14+
}

0 commit comments

Comments
 (0)