* @license Unlicense https://unlicense.org/ * @link https://github.com/ksolomon/Torn-Vault-Tracker */ /** * Checks if the database is new and empty. * * This function checks if the 'vault' table in the database has any entries. * If it does not, it returns true, indicating that the database is new and * empty. Otherwise, it returns false. * * @return boolean True if the database is new and empty, false otherwise. */ function dbNew() { $pdo = getDatabaseConnection(); $stmt = $pdo->query("SELECT COUNT(*) FROM vault"); return $stmt->fetchColumn() == 0; } /** * Ensures that a user has an API key configured. * * @param string $user The user to check the API key for * * @return void * * @throws Exception If the user does not have an API key configured */ function ensureUserHasApiKey($user) { if (!array_key_exists($user, USER_KEYS)) { throw new ApiKeyMissingException("User does not have an API key configured."); } } /** * Executes a GET request to the given API endpoint and returns the JSON response as an associative array. * * Uses the v2 Torn API authentication scheme by sending the API key in an * `Authorization: ApiKey ` header. The key is never transmitted as a * query parameter. * * @param string $apiEndpoint The URL of the API endpoint to call * @param string $apiKey The v2 API key to authenticate with * * @throws ApiKeyMissingException If the API key is empty * @throws CurlErrorException If the cURL call fails * @throws JsonDataException If the response body cannot be decoded as JSON * * @return array The JSON response from the API */ function executeApiCall($apiEndpoint, $apiKey) { if (empty($apiKey)) { throw new ApiKeyMissingException('API key is required for executeApiCall.'); } $headers = ["Authorization: ApiKey $apiKey"]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiEndpoint); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); if (curl_errno($ch)) { throw new CurlErrorException("cURL error: " . curl_error($ch)); } curl_close($ch); $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new JsonDataException("Failed to decode JSON response: " . json_last_error_msg()); } return $responseData; } /** * Validates the API response data for the presence of a log array. * * @param array $responseData The API response data to validate. * * @return void * * @throws Exception If the log data is missing or not an array. */ function validateApiResponse($responseData) { if (!isset($responseData['log']) || !is_array($responseData['log'])) { throw new ApiValidationException('Invalid log data received from the API.'); } } /** * Process an array of log entries retrieved from the Torn v2 API. * * For each entry, extracts the v2 shape (`id`, `timestamp`, * `details.title`, `data.deposited` | `data.withdrawn`) and inserts it * via the prepared statement, which uses `ON CONFLICT(id) DO NOTHING` * for idempotency. * * @param array $logEntries The array of v2 log entries to process * @param string $user The user whose log entries are being processed * @param PDOStatement $insertStmt A prepared statement for the idempotent * insert (`INSERT … ON CONFLICT(id) DO NOTHING`) * * @return void */ function processLogEntries($logEntries, $user, $insertStmt) { foreach ($logEntries as $entry) { $id = $entry['id'] ?? null; if (!$id) { consoleLog('Skipping entry with no id: ' . print_r($entry, true)); continue; } $timestamp = $entry['timestamp'] ?? null; $description = $entry['details']['title'] ?? null; $hasDeposit = isset($entry['data']['deposited']); $hasWithdraw = isset($entry['data']['withdrawn']); if ($hasDeposit) { $amount = (int)$entry['data']['deposited']; } elseif ($hasWithdraw) { $amount = -((int)$entry['data']['withdrawn']); } else { consoleLog('Skipping entry ' . $id . ' with no deposited/withdrawn: ' . print_r($entry, true)); continue; } if ($timestamp === null || $description === null) { consoleLog('Skipping entry ' . $id . ' missing timestamp or details.title: ' . print_r($entry, true)); continue; } $runningBalance = isset($entry['data']['balance']) ? (int)$entry['data']['balance'] : null; $insertStmt->bindValue(':id', $id); $insertStmt->bindValue(':user', $user); $insertStmt->bindValue(':timestamp', $timestamp); $insertStmt->bindValue(':description', $description); $insertStmt->bindValue(':amount', $amount); $insertStmt->bindValue(':running_balance', $runningBalance); $insertStmt->execute(); } } /** * Fetch vault records from the database * If a user is provided, only that user's records * are returned. Otherwise, all records are returned. * * @param string $user (optional) The user to filter by * * @return array An array of records, or an empty array on error */ function fetchVaultRecords($user = null) { $pdo = getDatabaseConnection(); if ($user) { $query = "SELECT * FROM vault WHERE user = :user ORDER BY timestamp DESC"; $params = [':user' => $user]; } else { $query = "SELECT * FROM vault ORDER BY timestamp DESC"; $params = []; } try { $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { echo "Error fetching records: " . $e->getMessage(); return []; } } /** * Iterate over all vault records for the given user (or all users if no user is given), * * @param string|null $name (optional) The user to loop over records for. * If not provided, all users' records will be looped over. * * @return int The total balance of the given user (or all users if no user is given). */ function vaultLoop ($name=null) { $vault = fetchVaultRecords($name); $balance = 0; foreach ($vault as $entry) { $balance += $entry['amount']; } return $balance; } /** * Returns a string indicating the sign of a number, with a dollar symbol. * * @param int|float $number The number to check. * * @return string A string containing a dollar sign and a negative sign if the * number is negative. */ function getSign($number) { if (substr($number, 0, 1) == '-') { return '-$'; } return '$'; } /** * Fetch a single page of log entries from the Torn v2 API and store them. * * Performs one HTTP request, validates the response, inserts each vault * log entry (idempotently via INSERT ... ON CONFLICT), and returns the * pagination cursor (`_metadata.links.next`) if more pages remain. * * @param PDO $pdo Database connection used to insert vault entries. * @param string $user The user whose log entries are being fetched. * @param string $url Full URL for the v2 API request. * * @throws ApiKeyMissingException If no API key is configured for the user. * @throws CurlErrorException If the HTTP request fails. * @throws JsonDataException If the response body is not valid JSON. * @throws ApiValidationException If the response is missing the `log` array. * * @return string|null The `_metadata.links.next` URL, or null when there are no more pages. */ function fetchAndStoreLogPage($pdo, $user, $url) { if (!array_key_exists($user, USER_KEYS)) { throw new ApiKeyMissingException("User does not have an API key configured."); } $responseData = executeApiCall($url, USER_KEYS[$user]); validateApiResponse($responseData); $insertStmt = $pdo->prepare( 'INSERT INTO vault (id, user, timestamp, description, amount, running_balance) ' . 'VALUES (:id, :user, :timestamp, :description, :amount, :running_balance) ' . 'ON CONFLICT(id) DO NOTHING' ); processLogEntries($responseData['log'], $user, $insertStmt); return $responseData['_metadata']['links']['next'] ?? null; } /** * Print a variable to the console for debugging purposes. * * @param mixed $data The data to print to the console. * * @return void */ function consoleLog( $data ) { echo ''; }