We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.
1 | <?php declare(strict_types=1); |
||
2 | |||
3 | namespace Smr; |
||
4 | |||
5 | use Exception; |
||
6 | use Smr\Pages\Player\VoteLinkProcessor; |
||
7 | |||
8 | /** |
||
9 | * Site-independent handling of links to external game voting sites. |
||
10 | * This is used to award free turns to players for voting. |
||
11 | */ |
||
12 | class VoteLink { |
||
13 | |||
14 | public const TIME_BETWEEN_VOTING = 84600; // 23.5 hours |
||
15 | |||
16 | /** @var ?array<int, int> */ |
||
17 | private static ?array $CACHE_TIMEOUTS = null; |
||
18 | |||
19 | /** @var array<string, mixed> */ |
||
20 | public readonly array $data; |
||
21 | |||
22 | public function __construct( |
||
23 | public readonly VoteSite $site, |
||
24 | public readonly int $accountID, |
||
25 | public readonly int $gameID, |
||
26 | ) { |
||
27 | $this->data = $site->getData(); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
28 | } |
||
29 | |||
30 | public static function clearCache(): void { |
||
31 | self::$CACHE_TIMEOUTS = null; |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * Returns the earliest time (in seconds) until free turns |
||
36 | * are available across all voting sites. |
||
37 | */ |
||
38 | public static function getMinTimeUntilFreeTurns(int $accountID, int $gameID): int { |
||
39 | $waitTimes = []; |
||
40 | foreach (VoteSite::cases() as $site) { |
||
41 | $link = new self($site, $accountID, $gameID); |
||
42 | if ($link->givesFreeTurns()) { |
||
43 | $waitTimes[] = $link->getTimeUntilFreeTurns(); |
||
44 | } |
||
45 | } |
||
46 | if (count($waitTimes) === 0) { |
||
47 | throw new Exception('No enabled vote sites give free turns!'); |
||
48 | } |
||
49 | return min($waitTimes); |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Does this VoteSite have a voting callback that can be used |
||
54 | * to award free turns? |
||
55 | */ |
||
56 | public function givesFreeTurns(): bool { |
||
57 | return isset($this->data['img_star']); |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * Time until the account can get free turns from voting at this site. |
||
62 | * If the time is 0, this site is eligible for free turns. |
||
63 | */ |
||
64 | public function getTimeUntilFreeTurns(bool $forceUpdate = false): int { |
||
65 | if (!$this->givesFreeTurns()) { |
||
66 | throw new Exception('This vote site cannot award free turns!'); |
||
67 | } |
||
68 | |||
69 | // Populate timeout cache from the database |
||
70 | if ($forceUpdate || !isset(self::$CACHE_TIMEOUTS)) { |
||
71 | self::$CACHE_TIMEOUTS = []; // ensure this is set |
||
72 | $db = Database::getInstance(); |
||
73 | $dbResult = $db->read('SELECT link_id, timeout FROM vote_links WHERE account_id=' . $db->escapeNumber($this->accountID)); |
||
74 | foreach ($dbResult->records() as $dbRecord) { |
||
75 | // 'timeout' is the last time the player claimed free turns (or 0, if unclaimed) |
||
76 | self::$CACHE_TIMEOUTS[$dbRecord->getInt('link_id')] = $dbRecord->getInt('timeout'); |
||
77 | } |
||
78 | } |
||
79 | |||
80 | // If not in the vote_link database, this site is eligible now. |
||
81 | $lastClaimTime = self::$CACHE_TIMEOUTS[$this->site->value] ?? 0; |
||
82 | return max(0, $lastClaimTime + self::TIME_BETWEEN_VOTING - Epoch::time()); |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * Register that the player has clicked on a vote site that is eligible |
||
87 | * for free turns, so that we will accept incoming votes. This ensures |
||
88 | * that voting is done through an authenticated SMR session. |
||
89 | */ |
||
90 | public function setClicked(): void { |
||
91 | $db = Database::getInstance(); |
||
92 | $db->lockTable('vote_links'); |
||
93 | try { |
||
94 | if (!$this->freeTurnsReady(true)) { |
||
95 | // Player has clicked a free turns link after already getting |
||
96 | // free turns. This should not occur naturally. |
||
97 | throw new Exception('Account ID ' . $this->accountID . ' attempted vote link abuse'); |
||
98 | } |
||
99 | |||
100 | // Don't start the timeout until the vote actually goes through. |
||
101 | $db->replace('vote_links', [ |
||
102 | 'account_id' => $db->escapeNumber($this->accountID), |
||
103 | 'link_id' => $db->escapeNumber($this->site->value), |
||
104 | 'timeout' => $db->escapeNumber(0), |
||
105 | 'turns_claimed' => $db->escapeBoolean(false), |
||
106 | ]); |
||
107 | } finally { |
||
108 | $db->unlock(); |
||
109 | } |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * Register that the player has been awarded their free turns. |
||
114 | * |
||
115 | * @return bool True if account was eligible for free turns (i.e. in the setClicked state). |
||
116 | */ |
||
117 | public function setFreeTurnsAwarded(): bool { |
||
118 | $db = Database::getInstance(); |
||
119 | $db->write('UPDATE vote_links SET timeout = ' . Epoch::time() . ', turns_claimed = ' . $db->escapeBoolean(true) . ' WHERE account_id = ' . $db->escapeNumber($this->accountID) . ' AND link_id = ' . $db->escapeNumber($this->site->value) . ' AND timeout = 0 AND turns_claimed = ' . $db->escapeBoolean(false)); |
||
120 | return $db->getChangedRows() === 1; |
||
121 | } |
||
122 | |||
123 | /** |
||
124 | * Returns true if account can currently receive free turns at this site. |
||
125 | */ |
||
126 | public function freeTurnsReady(bool $forceUpdate = false): bool { |
||
127 | return $this->givesFreeTurns() && $this->gameID != 0 && $this->getTimeUntilFreeTurns($forceUpdate) <= 0; |
||
128 | } |
||
129 | |||
130 | /** |
||
131 | * Returns the image to display for this voting site. |
||
132 | */ |
||
133 | public function getImg(): string { |
||
134 | if (!$this->freeTurnsReady()) { |
||
135 | return $this->data['img_default']; |
||
136 | } |
||
137 | return $this->data['img_star']; |
||
138 | } |
||
139 | |||
140 | /** |
||
141 | * Returns the URL that should be used for this voting site. |
||
142 | */ |
||
143 | public function getUrl(): string { |
||
144 | if (!$this->freeTurnsReady()) { |
||
145 | return $this->data['url_base']; |
||
146 | } |
||
147 | return $this->data['url_func']($this->data['url_base'], $this->accountID, $this->gameID); |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Returns the SN to redirect the current page to if free turns are |
||
152 | * available; otherwise, returns false. |
||
153 | */ |
||
154 | public function getSN(): string|false { |
||
155 | if (!$this->freeTurnsReady()) { |
||
156 | return false; |
||
157 | } |
||
158 | // This page will prepare the account for the voting callback. |
||
159 | return (new VoteLinkProcessor($this->site))->href(); |
||
160 | } |
||
161 | |||
162 | } |
||
163 |