mirror of
https://github.com/OPSnet/Gazelle.git
synced 2026-01-16 18:04:34 -05:00
421 lines
14 KiB
PHP
421 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Gazelle;
|
|
|
|
class Contest extends BaseObject {
|
|
final public const pkName = 'contest_id';
|
|
final public const tableName = 'contest';
|
|
final public const CACHE_CONTEST = 'contestv2_%d';
|
|
final public const CACHE_STATS = 'contest_stats_%d';
|
|
final public const CONTEST_LEADERBOARD_CACHE_KEY = 'contest_leaderboard_%d_%d';
|
|
|
|
protected array $stats; /* entries, users */
|
|
|
|
public function flush(): static {
|
|
self::$cache->delete_multi([
|
|
sprintf(self::CACHE_CONTEST, $this->id),
|
|
sprintf(self::CACHE_STATS, $this->id),
|
|
]);
|
|
unset($this->info, $this->stats);
|
|
return $this;
|
|
}
|
|
|
|
public function link(): string {
|
|
return "<a href=\"{$this->url()}\">{$this->name()}</a>";
|
|
}
|
|
|
|
public function location(): string {
|
|
return "contest.php?id={$this->id}";
|
|
}
|
|
|
|
public function info(): array {
|
|
if (isset($this->info)) {
|
|
return $this->info;
|
|
}
|
|
$key = sprintf(self::CACHE_CONTEST, $this->id);
|
|
$info = self::$cache->get_value($key);
|
|
if ($info === false) {
|
|
$info = self::$db->rowAssoc("
|
|
SELECT t.name AS contest_type,
|
|
c.name,
|
|
c.banner,
|
|
c.description,
|
|
c.display,
|
|
c.date_begin,
|
|
c.date_end,
|
|
cbp.bonus_pool_id AS bonus_pool_id,
|
|
coalesce(cbp.status, 'none') AS bonus_status,
|
|
cbp.bonus_user,
|
|
cbp.bonus_contest,
|
|
cbp.bonus_per_entry,
|
|
IF (now() BETWEEN c.date_begin AND c.date_end, 1, 0) AS is_open,
|
|
IF (
|
|
cbp.bonus_pool_id IS NOT NULL
|
|
AND cbp.status = ?
|
|
AND now() > c.date_end,
|
|
1,
|
|
0
|
|
) AS payout_ready
|
|
FROM contest c
|
|
INNER JOIN contest_type t USING (contest_type_id)
|
|
LEFT JOIN contest_has_bonus_pool cbp USING (contest_id)
|
|
WHERE c.contest_id = ?
|
|
", 'open', $this->id
|
|
);
|
|
$info['is_open'] = (bool)$info['is_open'];
|
|
self::$cache->cache_value($key, $info, 86400 * 3);
|
|
}
|
|
|
|
// upload-flac-no-single => UploadFlacNoSingle
|
|
$className = '\\Gazelle\\Contest\\'
|
|
. implode('', array_map('ucfirst', explode('-', $info['contest_type'])));
|
|
$info['type'] = new $className($this->id, $info['date_begin'], $info['date_end']);
|
|
|
|
if ($info['bonus_pool_id']) {
|
|
$info['bonus_pool'] = new BonusPool($info['bonus_pool_id']);
|
|
$sum = 0;
|
|
foreach (['bonus_contest', 'bonus_per_entry', 'bonus_user'] as $field) {
|
|
$info[$field] = (int)$info[$field];
|
|
$sum += $info[$field];
|
|
}
|
|
// calculate the ratios of how the bonus pool is carved up
|
|
// the sum of the ratios adds up to 1.0
|
|
$info['bonus_user_ratio'] = $info['bonus_user'] / $sum;
|
|
$info['bonus_contest_ratio'] = $info['bonus_contest'] / $sum;
|
|
$info['bonus_per_entry_ratio'] = 1 - ($info['bonus_user_ratio'] + $info['bonus_contest_ratio']);
|
|
} else {
|
|
$info['bonus_pool'] = null;
|
|
$info['bonus_user_ratio'] = 0.0;
|
|
$info['bonus_contest_ratio'] = 0.0;
|
|
$info['bonus_per_entry_ratio'] = 0.0;
|
|
}
|
|
$this->info = $info;
|
|
return $this->info;
|
|
}
|
|
|
|
public function contestType(): string {
|
|
return $this->info()['contest_type'];
|
|
}
|
|
|
|
public function banner(): string {
|
|
return $this->info()['banner'];
|
|
}
|
|
|
|
public function dateBegin(): string {
|
|
return $this->info()['date_begin'];
|
|
}
|
|
|
|
public function dateEnd(): string {
|
|
return $this->info()['date_end'];
|
|
}
|
|
|
|
public function description(): string {
|
|
return $this->info()['description'];
|
|
}
|
|
|
|
public function display(): int {
|
|
return $this->info()['display'];
|
|
}
|
|
|
|
public function isOpen(): bool {
|
|
return $this->info()['is_open'];
|
|
}
|
|
|
|
public function leaderboard(int $limit, int $offset): array {
|
|
return $this->type()->leaderboard($limit, $offset); /** @phpstan-ignore-line */
|
|
}
|
|
|
|
public function name(): string {
|
|
return $this->info()['name'];
|
|
}
|
|
|
|
public function paymentReady(): bool {
|
|
return $this->info()['payout_ready'] === 1;
|
|
}
|
|
|
|
public function type(): Contest\AbstractContest {
|
|
return $this->info()['type'];
|
|
}
|
|
|
|
public function bonusPool(): ?BonusPool {
|
|
return $this->info()['bonus_pool'];
|
|
}
|
|
|
|
public function hasBonusPool(): bool {
|
|
return $this->bonusPool() instanceof BonusPool;
|
|
}
|
|
|
|
public function bonusPoolTotal(): int {
|
|
return (int)$this->bonusPool()?->total();
|
|
}
|
|
|
|
public function bonusStatus(): string {
|
|
return $this->info()['bonus_status'];
|
|
}
|
|
|
|
public function bonusPerContest(): int {
|
|
return $this->info()['bonus_contest'];
|
|
}
|
|
|
|
public function bonusPerContestRatio(): float {
|
|
return $this->info()['bonus_contest_ratio'];
|
|
}
|
|
|
|
public function bonusPerContestValue(): int {
|
|
$totalUsers = $this->totalUsers();
|
|
return $totalUsers ?
|
|
(int)floor(
|
|
$this->bonusPoolTotal() * $this->bonusPerContestRatio()
|
|
/ $totalUsers
|
|
)
|
|
: 0;
|
|
}
|
|
|
|
public function bonusPerEntry(): int {
|
|
return $this->info()['bonus_per_entry'];
|
|
}
|
|
|
|
public function bonusPerEntryRatio(): float {
|
|
return $this->info()['bonus_per_entry_ratio'];
|
|
}
|
|
|
|
public function bonusPerEntryValue(): int {
|
|
$totalEntries = $this->totalEntries();
|
|
return $totalEntries
|
|
? (int)floor(
|
|
$this->bonusPoolTotal() * $this->bonusPerEntryRatio()
|
|
/ $totalEntries
|
|
)
|
|
: 0;
|
|
}
|
|
|
|
public function bonusPerUser(): int {
|
|
return $this->info()['bonus_user'];
|
|
}
|
|
|
|
public function bonusPerUserRatio(): float {
|
|
return $this->info()['bonus_user_ratio'];
|
|
}
|
|
|
|
public function bonusPerUserValue(): int {
|
|
$totalEnabledUsers = new Stats\Users()->enabledUserTotal();
|
|
return $totalEnabledUsers
|
|
? (int)floor(
|
|
$this->bonusPoolTotal() * $this->bonusPerUserRatio()
|
|
/ $totalEnabledUsers
|
|
)
|
|
: 0;
|
|
}
|
|
|
|
public function rank(User $user): ?array {
|
|
$page = 0;
|
|
while (true) {
|
|
$leaderboard = $this->leaderboard(CONTEST_ENTRIES_PER_PAGE, $page * CONTEST_ENTRIES_PER_PAGE);
|
|
if (!$leaderboard) {
|
|
break;
|
|
}
|
|
for ($i = 0, $max = count($leaderboard); $i < $max; $i++) {
|
|
if ($user->id == $leaderboard[$i]['user_id']) {
|
|
return [
|
|
'position' => 1 + $i + $page * CONTEST_ENTRIES_PER_PAGE,
|
|
'total' => $leaderboard[$i]['entry_count'],
|
|
];
|
|
}
|
|
}
|
|
if (++$page > 1000) {
|
|
break; // sanity check
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function calculateLeaderboard(): int {
|
|
/* only called from scheduler, don't need to worry how long this takes */
|
|
[$subquery, $args] = $this->type()->ranker();
|
|
self::$db->begin_transaction();
|
|
self::$db->prepared_query('DELETE FROM contest_leaderboard WHERE contest_id = ?', $this->id);
|
|
self::$db->prepared_query($sql = "
|
|
INSERT INTO contest_leaderboard
|
|
(contest_id, user_id, entry_count, last_entry_id)
|
|
SELECT ?, LADDER.user_id, LADDER.nr, T.ID
|
|
FROM torrents_group TG
|
|
LEFT JOIN torrents_artists TA ON (TA.GroupID = TG.ID)
|
|
INNER JOIN torrents T ON (T.GroupID = TG.ID)
|
|
INNER JOIN (
|
|
$subquery
|
|
) LADDER on (LADDER.last_torrent = T.ID)
|
|
GROUP BY
|
|
LADDER.nr,
|
|
T.ID,
|
|
TG.Name,
|
|
T.created
|
|
", $this->id, ...$args
|
|
);
|
|
$n = self::$db->affected_rows();
|
|
self::$db->commit();
|
|
/* recache the pages */
|
|
$pages = range(0, (int)(ceil($n) / CONTEST_ENTRIES_PER_PAGE) - 1);
|
|
foreach ($pages as $p) {
|
|
self::$cache->delete_value(sprintf(self::CONTEST_LEADERBOARD_CACHE_KEY, $this->id, $p));
|
|
$this->type()->leaderboard(CONTEST_ENTRIES_PER_PAGE, $p); /** @phpstan-ignore-line */
|
|
}
|
|
return $n;
|
|
}
|
|
|
|
protected function participationStats(): array {
|
|
if (!isset($this->stats)) {
|
|
$key = sprintf(self::CACHE_STATS, $this->id);
|
|
$stats = self::$cache->get_value($key);
|
|
if ($stats === false) {
|
|
$stats = $this->type()->participationStats();
|
|
self::$cache->cache_value($key, $stats, 900);
|
|
}
|
|
$this->stats = $stats;
|
|
}
|
|
return $this->stats;
|
|
}
|
|
|
|
public function totalEntries(): int {
|
|
return $this->participationStats()['total_entries'];
|
|
}
|
|
|
|
public function totalUsers(): int {
|
|
return $this->participationStats()['total_users'];
|
|
}
|
|
|
|
public function setPaymentClosed(): int {
|
|
self::$db->prepared_query('
|
|
UPDATE contest_has_bonus_pool SET
|
|
status = ?
|
|
WHERE contest_id = ?
|
|
', 'paid', $this->id
|
|
);
|
|
self::$cache->delete_value(sprintf(self::CACHE_CONTEST, $this->id));
|
|
return self::$db->affected_rows();
|
|
}
|
|
|
|
public function doPayout(
|
|
bool $dryrun = false,
|
|
Manager\User $manager = new Manager\User(),
|
|
): int {
|
|
$this->flush(); // force a fresh reload from the db
|
|
$report = fopen(TMPDIR . "/payout-contest-" . $this->id . ".txt", 'a');
|
|
if ($report === false) {
|
|
return 0;
|
|
}
|
|
if (!$this->hasBonusPool()) {
|
|
fprintf($report, "# no bonus pool to distribute\n");
|
|
fclose($report);
|
|
return 0;
|
|
}
|
|
$enabledUserBonus = $this->bonusPerUserValue();
|
|
$contestBonus = $this->bonusPerContestValue();
|
|
$perEntryBonus = $this->bonusPerEntryValue();
|
|
|
|
fprintf($report, "# user=%d contest=%d entry=%d\n", $enabledUserBonus, $contestBonus, $perEntryBonus);
|
|
|
|
$participants = $this->type()->userPayout();
|
|
foreach ($participants as $p) {
|
|
$user = $manager->findById($p['user_id']);
|
|
if (is_null($user)) {
|
|
fwrite($report, "user {$p['user_id']} NOT FOUND\n");
|
|
continue;
|
|
}
|
|
$totalGain = $enabledUserBonus;
|
|
$totalEntries = $p['total_entries'];
|
|
if ($totalEntries) {
|
|
$totalGain += $contestBonus + ($perEntryBonus * $totalEntries);
|
|
}
|
|
$log = date('Y-m-d H:i:s') . " {$user->label()} n={$totalEntries} t={$totalGain}";
|
|
if ($user->hasAttr('no-fl-gifts') || $user->hasAttr('disable-bonus-points')) {
|
|
fwrite($report, "$log DECLINED\n");
|
|
continue;
|
|
}
|
|
fwrite($report, "$log DISTRIBUTED\n");
|
|
if ($dryrun) {
|
|
continue;
|
|
}
|
|
$user->inbox()->createSystem(
|
|
"You have received " . number_format($totalGain, 2) . " bonus points!",
|
|
self::$twig->render('contest/payout-uploader.bbcode.twig', [
|
|
'contest' => $this,
|
|
'contest_bonus' => $contestBonus,
|
|
'enabled_bonus' => $enabledUserBonus,
|
|
'per_entry_bonus' => $perEntryBonus,
|
|
'total_entries' => $totalEntries,
|
|
'username' => $user->username(),
|
|
])
|
|
);
|
|
new User\Bonus($user)->addPoints($totalGain);
|
|
$user->addStaffNote(
|
|
number_format($totalGain)
|
|
. " BP added for {$totalEntries} entries in {$this->name()}"
|
|
)->modify();
|
|
}
|
|
fclose($report);
|
|
return count($participants);
|
|
}
|
|
|
|
public function modify(): bool {
|
|
$success = parent::modify();
|
|
if ($success) {
|
|
self::$db->prepared_query("
|
|
UPDATE bonus_pool bp
|
|
INNER JOIN contest_has_bonus_pool chbp USING (bonus_pool_id)
|
|
INNER JOIN contest c USING (contest_id)
|
|
SET
|
|
bp.name = c.name,
|
|
bp.since_date = c.date_begin,
|
|
bp.until_date = c.date_end
|
|
WHERE c.contest_id = ?
|
|
", $this->id
|
|
);
|
|
self::$cache->delete_value(Manager\Bonus::CACHE_OPEN_POOL);
|
|
}
|
|
return $success;
|
|
}
|
|
|
|
public function remove(): int {
|
|
// mysql is really dumb when it comes to foreign keys
|
|
/* This does not work:
|
|
self::$db->prepared_query("
|
|
DELETE c, cl, chbp, bp
|
|
FROM contest c
|
|
LEFT JOIN contest_leaderboard cl USING (contest_id)
|
|
LEFT JOIN contest_has_bonus_pool chbp USING (contest_id)
|
|
LEFT JOIN bonus_pool bp USING (bonus_pool_id)
|
|
WHERE c.contest_id = ?
|
|
", $this->id
|
|
);
|
|
*/
|
|
self::$db->begin_transaction();
|
|
$affected = 0;
|
|
if ($this->hasBonusPool()) {
|
|
$pool = $this->bonusPool();
|
|
self::$db->prepared_query("
|
|
DELETE FROM contest_has_bonus_pool WHERE contest_id = ?
|
|
", $this->id
|
|
);
|
|
$affected += self::$db->affected_rows();
|
|
self::$db->prepared_query("
|
|
DELETE FROM bonus_pool WHERE bonus_pool_id = ?
|
|
", $pool->id
|
|
);
|
|
$affected += self::$db->affected_rows();
|
|
}
|
|
self::$db->prepared_query("
|
|
DELETE FROM contest_leaderboard WHERE contest_id = ?
|
|
", $this->id
|
|
);
|
|
$affected += self::$db->affected_rows();
|
|
self::$db->prepared_query("
|
|
DELETE FROM contest WHERE contest_id = ?
|
|
", $this->id
|
|
);
|
|
$affected += self::$db->affected_rows();
|
|
self::$db->commit();
|
|
$this->flush();
|
|
return $affected;
|
|
}
|
|
}
|