You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
449 lines
16 KiB
PHP
449 lines
16 KiB
PHP
<?php
|
|
namespace Isekai\UserPoints\Service;
|
|
|
|
use Isekai\UserPoints\Utils;
|
|
use User;
|
|
use stdClass;
|
|
use MediaWiki\MediaWikiServices;
|
|
|
|
/**
|
|
* @property int $user_id
|
|
* @property string $type
|
|
* @property int $points
|
|
* @property string|array $timed_points_data
|
|
* @property int $locked_points
|
|
* @property string|array $locked_points_data
|
|
* @property int $next_refresh_time
|
|
*/
|
|
class IsekaiUserPointsData extends stdClass {}
|
|
|
|
/**
|
|
* @property int $points
|
|
* @property array $timedPointsData
|
|
* @property int $lockedPoints
|
|
* @property array $lockedPointsData
|
|
* @property int $nextRefreshTime
|
|
*/
|
|
class IsekaiUserPoints {
|
|
public const DEFAULT_TRANSACTION_TIMEOUT = 60 * 10;
|
|
public const MAX_TRANSACTION_TIMEOUT = 60 * 60;
|
|
|
|
/** @var MediaWikiServices */
|
|
private $services;
|
|
|
|
/** @var \Config */
|
|
private $config;
|
|
|
|
/** @var User */
|
|
public $user;
|
|
|
|
/** @var string */
|
|
public $type;
|
|
|
|
/** @var string */
|
|
public $pointName = '';
|
|
|
|
/** @var string */
|
|
private $rawName = '';
|
|
|
|
/** @var IsekaiUserPointsData|null */
|
|
private $mPointData = null;
|
|
|
|
/** @var bool */
|
|
private $pointRecordExists = false;
|
|
|
|
private $pointDataKeysMap = [
|
|
'points' => 'points',
|
|
'timedPointsData' => 'timed_points_data',
|
|
'lockedPoints' => 'locked_points',
|
|
'lockedPointsData' => 'locked_points_data',
|
|
'nextRefreshTime' => 'next_refresh_time'
|
|
];
|
|
|
|
public function __construct(User $user, string $type, array $pointConfig, MediaWikiServices $mediaWikiServices = null) {
|
|
if (!$mediaWikiServices) {
|
|
$this->services = MediaWikiServices::getInstance();
|
|
} else {
|
|
$this->services = $mediaWikiServices;
|
|
}
|
|
$this->config = $this->services->getMainConfig();
|
|
|
|
$this->user = $user;
|
|
$this->type = $type;
|
|
|
|
if (isset($pointConfig['namemsg'])) {
|
|
$this->pointName = wfMessage($pointConfig['namemsg'])->text();
|
|
$this->rawName = $pointConfig['name'] ?? $this->pointName;
|
|
} elseif (isset($pointConfig['name'])) {
|
|
$this->pointName = $this->rawName = $pointConfig['name'];
|
|
}
|
|
}
|
|
|
|
public function setData($pointData) {
|
|
$this->mPointData = $pointData;
|
|
}
|
|
|
|
private function loadPointData() {
|
|
if (!$this->mPointData) {
|
|
$dbr = $this->services->getDBLoadBalancer()->getConnection(DB_REPLICA);
|
|
$this->mPointData = $dbr->selectRow(
|
|
'isekai_user_points',
|
|
'*',
|
|
[
|
|
'user_id' => $this->user->getId(),
|
|
'type' => $this->type
|
|
],
|
|
__METHOD__
|
|
);
|
|
|
|
if (!$this->mPointData) {
|
|
$this->initDefaultData();
|
|
} else {
|
|
$this->mPointData->timed_points_data = json_decode($this->mPointData->timed_points_data, true) ?? [];
|
|
$this->mPointData->locked_points_data = json_decode($this->mPointData->locked_points_data, true) ?? [];
|
|
$this->pointRecordExists = true;
|
|
$this->removeExpiredPoints();
|
|
}
|
|
}
|
|
}
|
|
|
|
private function initDefaultData() {
|
|
$this->mPointData = new IsekaiUserPointsData();
|
|
$this->mPointData->user_id = $this->user->getId();
|
|
$this->mPointData->type = $this->type;
|
|
$this->mPointData->points = 0;
|
|
$this->mPointData->timed_points_data = [];
|
|
$this->mPointData->locked_points = 0;
|
|
$this->mPointData->locked_points_data = [];
|
|
$this->mPointData->next_refresh_time = null;
|
|
}
|
|
|
|
/**
|
|
* Remove expired points
|
|
*/
|
|
private function removeExpiredPoints() {
|
|
$currentTime = time();
|
|
if ($this->mPointData->next_refresh_time !== null &&
|
|
$currentTime > $this->mPointData->next_refresh_time &&
|
|
is_array($this->mPointData->timed_points_data)) {
|
|
|
|
$touched = false;
|
|
$originalPoints = $this->mPointData->points;
|
|
foreach ($this->mPointData->timed_points_data as $key => $data) {
|
|
list($expireTime, $points) = $data;
|
|
if ($currentTime > $expireTime) {
|
|
$this->mPointData->points -= $points;
|
|
unset($this->mPointData->timed_points_data[$key]);
|
|
$touched = true;
|
|
}
|
|
}
|
|
if ($touched) {
|
|
// Update next refresh time
|
|
if (count($this->mPointData->timed_points_data) > 0) {
|
|
// Sort by expire time
|
|
usort($this->mPointData->timed_points_data, function($a, $b) {
|
|
return $a[0] - $b[0];
|
|
});
|
|
|
|
$this->mPointData->next_refresh_time = $this->mPointData->timed_points_data[0][0];
|
|
} else {
|
|
$this->mPointData->next_refresh_time = null;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
$deltaPoints = $originalPoints - $this->mPointData->points;
|
|
|
|
$hookContainer = $this->services->getHookContainer();
|
|
$hookContainer->run('IsekaiUserPoints::PointsExpired', [$this, $deltaPoints]);
|
|
|
|
$this->savePointLog(-$deltaPoints, 'system', 'point-expired');
|
|
}
|
|
}
|
|
}
|
|
|
|
public function __get($name) {
|
|
$this->loadPointData();
|
|
if (isset($this->pointDataKeysMap[$name])) {
|
|
$key = $this->pointDataKeysMap[$name];
|
|
return $this->mPointData->$key;
|
|
} else {
|
|
throw new \Exception("Invalid property $name");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add user points
|
|
* @param int $points Points to add
|
|
* @param int $expireTime Expire time in seconds. -1 for never expire
|
|
* @param string $service Service name that add points. Message key is `isekai-userpoints-service-$service`.
|
|
* @param ?string $action Action name that add points. Message key is `isekai-userpoints-action-$service-$action`.
|
|
*/
|
|
public function addPoints($points, $expireTime = -1, $service = 'system', $action = null) {
|
|
$this->loadPointData();
|
|
|
|
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
|
|
|
$hookContainer->run('IsekaiUserPoints::BeforeAddPoints', [$this, &$points, &$expireTime, &$service, &$action]);
|
|
|
|
$this->mPointData->points += $points;
|
|
|
|
// Handle expire time
|
|
$expireTime = intval($expireTime);
|
|
$resolvedExipreTime = null;
|
|
if ($expireTime > 0) {
|
|
// Quantify expire time to 1 day
|
|
$expireTime = ceil($expireTime / 86400) * 86400;
|
|
|
|
$resolvedExipreTime = time() + $expireTime;
|
|
|
|
$recordExists = false;
|
|
foreach ($this->mPointData->timed_points_data as $key => $data) {
|
|
$oneExpireTime = $data[0];
|
|
if ($resolvedExipreTime === $oneExpireTime) {
|
|
$this->mPointData->timed_points_data[$key][1] += $points;
|
|
$recordExists = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$recordExists) {
|
|
$this->mPointData->timed_points_data[] = [$resolvedExipreTime, $points];
|
|
}
|
|
if ($this->mPointData->next_refresh_time === null) {
|
|
$this->mPointData->next_refresh_time = $resolvedExipreTime;
|
|
}
|
|
}
|
|
|
|
/** @todo Add point log */
|
|
|
|
$this->save();
|
|
|
|
$this->savePointLog($points, $service, $action, $resolvedExipreTime);
|
|
$hookContainer->run('IsekaiUserPoints::AfterAddPoints', [$this, $points, $expireTime, $service, $action]);
|
|
}
|
|
|
|
/**
|
|
* Detect if the user has enough points to consume
|
|
* @param int $points Points to consume
|
|
*/
|
|
public function hasEnoughPoints($points) {
|
|
$this->loadPointData();
|
|
return $this->mPointData->points >= $points;
|
|
}
|
|
|
|
/**
|
|
* Consume points
|
|
* @param int $points Points to consume
|
|
* @param bool $force Force consume points even if the user has not enough points
|
|
* @param bool $ignoreSave Ignore save points data after consume points
|
|
* @param string $service Service name that consume points. Message key is `isekai-userpoints-service-$service`.
|
|
* @param ?string $action Action name that consume points. Message key is `isekai-userpoints-action-$service-$action`.
|
|
* @return bool True if the user has enough points to consume
|
|
*/
|
|
public function consumePoints($points, $force = false, $ignoreSave = false, $service = 'system', $action = null) {
|
|
$this->loadPointData();
|
|
|
|
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
|
|
|
$hookContainer->run('IsekaiUserPoints::BeforeConsumePoints', [$this, &$points, &$service, &$action]);
|
|
|
|
if (!$this->hasEnoughPoints($points) && !$force) {
|
|
return false;
|
|
}
|
|
|
|
$this->mPointData->points = max(0, $this->mPointData->points - $points);
|
|
|
|
if ($this->mPointData->next_refresh_time !== null) { // Update timed points data
|
|
$consumedPoints = $points;
|
|
$touched = false;
|
|
foreach ($this->mPointData->timed_points_data as $key => $data) {
|
|
list($expireTime, $onePoints) = $data;
|
|
if ($onePoints <= $consumedPoints) {
|
|
$consumedPoints -= $onePoints;
|
|
unset($this->mPointData->timed_points_data[$key]);
|
|
$touched = true;
|
|
} else { // All points consumed
|
|
$this->mPointData->timed_points_data[$key][1] -= $consumedPoints;
|
|
$consumedPoints = 0;
|
|
$touched = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($touched) {
|
|
// Update next refresh time
|
|
if (count($this->mPointData->timed_points_data) > 0) {
|
|
// Sort by expire time
|
|
usort($this->mPointData->timed_points_data, function($a, $b) {
|
|
return $a[0] - $b[0];
|
|
});
|
|
$this->mPointData->next_refresh_time = $this->mPointData->timed_points_data[0][0];
|
|
} else {
|
|
$this->mPointData->next_refresh_time = null;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (!$ignoreSave) {
|
|
$this->save();
|
|
}
|
|
|
|
$this->savePointLog(-$points, $service, $action);
|
|
$hookContainer->run('IsekaiUserPoints::AfterConsumePoints', [$this, $points, $service, $action]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Start a consume points transaction
|
|
* @param int $points Points to consume
|
|
* @param int|null $timeout Transaction timeout in seconds
|
|
* @param string $service Service name that consume points. Message key is `isekai-userpoints-service-$service`.
|
|
* @param ?string $action Action name that consume points. Message key is `isekai-userpoints-action-$service-$action`.
|
|
* @return bool True if the transaction started successfully
|
|
*/
|
|
public function startConsumePointsTransaction($points, $timeout = null, $service = 'system', $action = null) {
|
|
$this->loadPointData();
|
|
|
|
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
|
|
|
$hookContainer->run('IsekaiUserPoints::BeforeConsumePoints', [$this, &$points, &$service, &$action]);
|
|
|
|
if (!$this->hasEnoughPoints($points)) {
|
|
return false;
|
|
}
|
|
|
|
$timeout = min($timeout ?? self::DEFAULT_TRANSACTION_TIMEOUT, self::MAX_TRANSACTION_TIMEOUT);
|
|
$expireTime = time() + $timeout;
|
|
|
|
// Generate random string for transaction id
|
|
$transactionId = '';
|
|
do {
|
|
$transactionId = bin2hex(random_bytes(8));
|
|
} while (isset($this->mPointData->locked_points_data[$transactionId]));
|
|
|
|
$result = $this->consumePoints($points, false, true);
|
|
if (!$result) return false;
|
|
|
|
$this->mPointData->locked_points += $points;
|
|
$this->mPointData->locked_points_data[$transactionId] = [$expireTime, $points, $service, $action];
|
|
|
|
$this->save();
|
|
|
|
return $transactionId;
|
|
}
|
|
|
|
/**
|
|
* Commit a consume points transaction
|
|
* @param string $transactionId Transaction id
|
|
* @return bool True if the transaction is committed successfully
|
|
*/
|
|
public function commitConsumePointsTransaction($transactionId) {
|
|
if (!isset($this->mPointData->locked_points_data[$transactionId])) {
|
|
return false;
|
|
}
|
|
|
|
list($expireTime, $points, $service, $action) = $this->mPointData->locked_points_data[$transactionId];
|
|
$this->mPointData->locked_points = max(0, $this->mPointData->locked_points - $points);
|
|
unset($this->mPointData->locked_points_data[$transactionId]);
|
|
|
|
$this->save();
|
|
|
|
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
|
|
|
|
$this->savePointLog(-$points, $service, $action);
|
|
$hookContainer->run('IsekaiUserPoints::AfterConsumePoints', [$this, $points, $service, $action]);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Rollback a consume points transaction
|
|
* @param string $transactionId Transaction id
|
|
* @return bool True if the transaction is rolled back successfully
|
|
*/
|
|
public function rollbackConsumePointsTransaction($transactionId) {
|
|
if (!isset($this->mPointData->locked_points_data[$transactionId])) {
|
|
return false;
|
|
}
|
|
|
|
list($expireTime, $points) = $this->mPointData->locked_points_data[$transactionId];
|
|
$this->mPointData->locked_points = max(0, $this->mPointData->locked_points - $points);
|
|
$this->mPointData->points += $points;
|
|
unset($this->mPointData->locked_points_data[$transactionId]);
|
|
|
|
$this->save();
|
|
|
|
return true;
|
|
}
|
|
|
|
public function getPointInfo() {
|
|
return [
|
|
'name' => Utils::getPointName($this->type),
|
|
'icon' => Utils::getPointIcon($this->type),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Save point data
|
|
*/
|
|
private function save() {
|
|
$dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY);
|
|
if ($this->pointRecordExists) {
|
|
$dbw->update(
|
|
'isekai_user_points',
|
|
[
|
|
'points' => $this->mPointData->points,
|
|
'timed_points_data' => json_encode($this->mPointData->timed_points_data),
|
|
'locked_points' => $this->mPointData->locked_points,
|
|
'locked_points_data' => json_encode($this->mPointData->locked_points_data),
|
|
'next_refresh_time' => $this->mPointData->next_refresh_time
|
|
],
|
|
[
|
|
'user_id' => $this->user->getId(),
|
|
'type' => $this->type,
|
|
],
|
|
__METHOD__
|
|
);
|
|
} else {
|
|
$dbw->insert(
|
|
'isekai_user_points',
|
|
[
|
|
'user_id' => $this->mPointData->user_id,
|
|
'type' => $this->mPointData->type,
|
|
'points' => $this->mPointData->points,
|
|
'timed_points_data' => json_encode($this->mPointData->timed_points_data),
|
|
'locked_points' => $this->mPointData->locked_points,
|
|
'locked_points_data' => json_encode($this->mPointData->locked_points_data),
|
|
'next_refresh_time' => $this->mPointData->next_refresh_time,
|
|
],
|
|
__METHOD__
|
|
);
|
|
$this->pointRecordExists = true;
|
|
}
|
|
}
|
|
|
|
private function savePointLog($points, $service, $action, $expireTime = null) {
|
|
$dbw = $this->services->getDBLoadBalancer()->getConnection(DB_PRIMARY);
|
|
|
|
if ($expireTime) {
|
|
$expireDate = date('Y-m-d', $expireTime);
|
|
} else {
|
|
$expireDate = null;
|
|
}
|
|
|
|
$dbw->insert(
|
|
'isekai_user_points_log',
|
|
[
|
|
'user_id' => $this->user->getId(),
|
|
'point_type' => $this->type,
|
|
'points' => $points,
|
|
'point_expire' => $expireDate,
|
|
'service' => $service,
|
|
'action' => $action,
|
|
'timestamp' => wfTimestampNow()
|
|
],
|
|
__METHOD__
|
|
);
|
|
}
|
|
} |