diff --git a/README.md b/README.md index 07436529..76f6153a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ To enable a theme, append `&theme=` followed by the theme name to the end of the -
en - English
English 100%
ar - العربية
العربية 100%
bn - বাংলা
বাংলা 100%
es - español
español 100%
fa - فارسی
فارسی 100%
he - עברית
עברית 100%
hi - हिन्दी
हिन्दी 100%
ja - 日本語
日本語 100%
ko - 한국어
한국어 100%
mr - मराठी
मराठी 100%
pl - polski
polski 100%
pt_BR - português (Brasil)
português (Brasil) 100%
zh_Hans - 中文(简体)
中文(简体) 100%
zh_Hant - 中文(繁體)
中文(繁體) 100%
da - dansk
dansk 67%
de - Deutsch
Deutsch 67%
fr - français
français 67%
id - Indonesia
Indonesia 67%
it - italiano
italiano 67%
kn - ಕನ್ನಡ
ಕನ್ನಡ 67%
nl - Nederlands
Nederlands 67%
ru - русский
русский 67%
ta - தமிழ்
தமிழ் 67%
tr - Türkçe
Türkçe 67%
vi - Tiếng Việt
Tiếng Việt 67%
+
en - English
English 100%
ar - العربية
العربية 100%
bg - български
български 100%
bn - বাংলা
বাংলা 100%
es - español
español 100%
fa - فارسی
فارسی 100%
fr - français
français 100%
he - עברית
עברית 100%
hi - हिन्दी
हिन्दी 100%
ja - 日本語
日本語 100%
ko - 한국어
한국어 100%
mr - मराठी
मराठी 100%
pl - polski
polski 100%
ps - پښتو
پښتو 100%
pt_BR - português (Brasil)
português (Brasil) 100%
ru - русский
русский 100%
uk - українська
українська 100%
yo - Èdè Yorùbá
Èdè Yorùbá 100%
zh_Hans - 中文(简体)
中文(简体) 100%
zh_Hant - 中文(繁體)
中文(繁體) 100%
da - dansk
dansk 67%
de - Deutsch
Deutsch 67%
id - Indonesia
Indonesia 67%
it - italiano
italiano 67%
kn - ಕನ್ನಡ
ಕನ್ನಡ 67%
nl - Nederlands
Nederlands 67%
ta - தமிழ்
தமிழ் 67%
tr - Türkçe
Türkçe 67%
vi - Tiếng Việt
Tiếng Việt 67%
@@ -144,8 +144,11 @@ The Inkscape dependency is required for PNG rendering, as well as Segoe UI font [![Heroku_logo](https://user-images.githubusercontent.com/20955511/136292872-ab2b3918-3350-4878-93a2-aa1f569b095a.png)](https://heroku.com) +Heroku costs around $5-$7/month minimum for a single app, but you can contact the Open Source program +at ospo-heroku-credits@salesforce.com to possibly get free credits. +
- Instructions for Deploying to Heroku for Free + Instructions for Deploying to Heroku ### Step-by-step instructions for deploying to Heroku @@ -165,6 +168,37 @@ The Inkscape dependency is required for PNG rendering, as well as Segoe UI font
+[![Vercel_logo](https://user-images.githubusercontent.com/20955511/209479243-5b14048b-e9ae-42da-aec3-1cc88a97aaee.png)](https://vercel.com) + +Vercel is a free hosting service that can be used to run PHP. **Note:** The intl library seems to not be available through Vercel at the moment +(https://github.com/vercel-community/php/issues/367), so the automatic number and date formats for locales other than English will not work. +PNG mode is also not supported since Inkscape will not be installed. + +
+ Instructions for Deploying to Vercel for Free + +### Step-by-step instructions for deploying to Vercel + +1. Sign in to **Vercel** or create a new account at +2. Clone this repository with `git clone https://github.com/DenverCoder1/github-readme-streak-stats.git` + - You may also fork the repository and clone your fork instead if you intend to make changes +3. Enter the directory with `cd github-readme-streak-stats` +4. Switch branches to the `vercel` branch with `git checkout vercel` +5. Make sure you have the [Vercel CLI](https://vercel.com/download) installed +6. Run `vercel` and follow the prompts to link your Vercel account and select a project name +7. The app will be deployed to `.vercel.app` +8. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token (no scopes required) +9. Scroll to the bottom and click **"Generate token"** +10. Visit the [Vercel dashboard](https://vercel.com/dashboard) and select your project, then click **"Settings"**, then **"Environment Variables"**. +11. Add a new variable with the key `TOKEN` and the value as your token from step 9 and click "Save". + +![image](https://user-images.githubusercontent.com/20955511/209588756-8bf5b0cd-9aa6-41e8-909c-97bf41e525b3.png) + +> **Note** +> To set up automatic Vercel deployments from GitHub, make sure to turn **off** "Include source files outside of the Root Directory" in the General settings and use `vercel` as the production branch in the Git settings. + +
+ ## 🤗 Contributing Contributions are welcome! Feel free to [open an issue](https://github.com/DenverCoder1/github-readme-streak-stats/issues/new/choose) or submit a [pull request](https://github.com/DenverCoder1/github-readme-streak-stats/compare) if you have a way to improve this project. diff --git a/composer.json b/composer.json index 1a0fa13a..bf460e42 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ "stats" ], "license": "MIT", - "version": "0.25.0", + "version": "0.26.0", "homepage": "https://github.com/DenverCoder1/github-readme-streak-stats", "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 1de25444..e7195848 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dafd149b850dfcef2b8cdec081c5410e", + "content-hash": "5c08d15859f1b257e2ff7475b6fab683", "packages": [ { "name": "graham-campbell/result-type", @@ -479,30 +479,30 @@ "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -529,7 +529,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -545,7 +545,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "myclabs/deep-copy", @@ -775,16 +775,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.22", + "version": "9.2.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e4bf60d2220b4baaa0572986b5d69870226b06df" + "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e4bf60d2220b4baaa0572986b5d69870226b06df", - "reference": "e4bf60d2220b4baaa0572986b5d69870226b06df", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", + "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", "shasum": "" }, "require": { @@ -840,7 +840,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.22" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" }, "funding": [ { @@ -848,7 +848,7 @@ "type": "github" } ], - "time": "2022-12-18T16:40:55+00:00" + "time": "2022-12-28T12:41:10+00:00" }, { "name": "phpunit/php-file-iterator", diff --git a/src/card.php b/src/card.php index 08025205..52d52fd1 100644 --- a/src/card.php +++ b/src/card.php @@ -56,13 +56,13 @@ function formatDate(string $dateString, string|null $format, string $locale): st /** * Check theme and color customization parameters to generate a theme mapping * - * @param array $params Request parameters - * @return array The chosen theme or default + * @param array $params Request parameters + * @return array The chosen theme or default */ function getRequestedTheme(array $params): array { /** - * @var array> $THEMES + * @var array> $THEMES * List of theme names mapped to labeled colors */ $THEMES = include "themes.php"; @@ -125,7 +125,7 @@ function getRequestedTheme(array $params): array * than the word width. * @return string The given string wrapped at the specified length */ -function utf8WordWrap($string, $width = 75, $break = "\n", $cut_long_words = false) +function utf8WordWrap(string $string, int $width = 75, string $break = "\n", bool $cut_long_words = false): string { // match anything 1 to $width chars long followed by whitespace or EOS $string = preg_replace("/(.{1,$width})(?:\s|$)/uS", "$1$break", $string); @@ -145,7 +145,7 @@ function utf8WordWrap($string, $width = 75, $break = "\n", $cut_long_words = fal * @param string $string The input string * @return int The length of the string */ -function utf8Strlen($string) +function utf8Strlen(string $string): int { return preg_match_all("/./us", $string, $matches); } @@ -156,7 +156,6 @@ function utf8Strlen($string) * @param string $text Text to split * @param int $maxChars Maximum number of characters per line * @param int $line1Offset Offset for the first line - * * @return string Original text if one line, or split text with elements */ function splitLines(string $text, int $maxChars, int $line1Offset): string @@ -184,7 +183,6 @@ function splitLines(string $text, int $maxChars, int $line1Offset): string * Normalize a locale code * * @param string $localeCode Locale code - * * @return string Normalized locale code */ function normalizeLocaleCode(string $localeCode): string @@ -210,10 +208,9 @@ function normalizeLocaleCode(string $localeCode): string * Get the translations for a locale code after normalizing it * * @param string $localeCode Locale code - * * @return array Translations for the locale code */ -function getTranslations(string $localeCode) +function getTranslations(string $localeCode): array { // normalize locale code $localeCode = normalizeLocaleCode($localeCode); @@ -239,9 +236,8 @@ function getTranslations(string $localeCode) /** * Generate SVG output for a stats array * - * @param array $stats Streak stats - * @param array|NULL $params Request parameters - * + * @param array $stats Streak stats + * @param array|NULL $params Request parameters * @return string The generated SVG Streak Stats card * * @throws InvalidArgumentException If a locale does not exist @@ -424,8 +420,7 @@ function generateCard(array $stats, array $params = null): string * Generate SVG displaying an error message * * @param string $message The error message to display - * @param array|NULL $params Request parameters - * + * @param array|NULL $params Request parameters * @return string The generated SVG error card */ function generateErrorCard(string $message, array $params = null): string @@ -485,7 +480,6 @@ function generateErrorCard(string $message, array $params = null): string * Converts an SVG card to a PNG image * * @param string $svg The SVG for the card as a string - * * @return string The generated PNG data */ function convertSvgToPng(string $svg): string @@ -500,50 +494,84 @@ function convertSvgToPng(string $svg): string $svg = preg_replace("/(animation: currstreak[^;'\"]+)/m", "font-size: 28px;", $svg); $svg = preg_replace("/(\X*?)<\/a>/m", '\1', $svg); - // save svg to random file - $filename = uniqid(); - file_put_contents("$filename.svg", $svg); + // escape svg for shell + $svg = escapeshellarg($svg); + + // `--pipe`: read input from pipe (stdin) + // `--export-filename -`: write output to stdout + // `-w 495 -h 195`: set width and height of the output image + // `--export-type png`: set the output format to PNG + $cmd = "echo {$svg} | inkscape --pipe --export-filename - -w 495 -h 195 --export-type png"; // convert svg to png - $out = shell_exec("inkscape -w 495 -h 195 {$filename}.svg -o {$filename}.png"); // skipcq: PHP-A1009 - if ($out !== null) { - throw new Exception("Error converting SVG to PNG: $out"); + $png = shell_exec($cmd); // skipcq: PHP-A1009 + + // check if the conversion was successful + if (empty($png)) { + // `2>&1`: redirect stderr to stdout + $error = shell_exec("$cmd 2>&1"); // skipcq: PHP-A1009 + throw new InvalidArgumentException("Failed to convert SVG to PNG: {$error}", 500); } - // read png data and delete temporary files - $png = file_get_contents("{$filename}.png"); - unlink("{$filename}.svg"); - unlink("{$filename}.png"); + // return the generated png return $png; } /** - * Set headers and echo response based on type + * Return headers and response based on type * * @param string|array $output The stats (array) or error message (string) to display + * @return array The Content-Type header and the response body, and status code in case of an error */ -function renderOutput(string|array $output, int $responseCode = 200): void +function generateOutput(string|array $output): array { $requestedType = $_REQUEST["type"] ?? "svg"; - http_response_code($responseCode); // output JSON data if ($requestedType === "json") { - // set content type to JSON - header("Content-Type: application/json"); // generate array from output $data = gettype($output) === "string" ? ["error" => $output] : $output; - // output as JSON - echo json_encode($data); + return [ + "contentType" => "application/json", + "body" => json_encode($data), + ]; } - // output SVG or PNG card - else { - // set content type to SVG or PNG - header("Content-Type: image/" . ($requestedType === "png" ? "png" : "svg+xml")); - // render SVG card - $svg = gettype($output) === "string" ? generateErrorCard($output) : generateCard($output); - // output PNG if PNG is requested, otherwise output SVG - echo $requestedType === "png" ? convertSvgToPng($svg) : $svg; + // Generate SVG card + $svg = gettype($output) === "string" ? generateErrorCard($output) : generateCard($output); + // output PNG card + if ($requestedType === "png") { + try { + $png = convertSvgToPng($svg); + return [ + "contentType" => "image/png", + "body" => $png, + ]; + } catch (Exception $e) { + return [ + "contentType" => "image/svg+xml", + "status" => 500, + "body" => generateErrorCard($e->getMessage()), + ]; + } } - exit(); + // output SVG card + return [ + "contentType" => "image/svg+xml", + "body" => $svg, + ]; +} + +/** + * Set headers and output response + * + * @param string|array $output The Content-Type header and the response body + * @param int $responseCode The HTTP response code to send + * @return void The function exits after sending the response + */ +function renderOutput(string|array $output, int $responseCode = 200): void +{ + $response = generateOutput($output); + http_response_code($response["status"] ?? $responseCode); + header("Content-Type: {$response["contentType"]}"); + exit($response["body"]); } diff --git a/src/colors.php b/src/colors.php index 1f166f2d..9003f8a9 100644 --- a/src/colors.php +++ b/src/colors.php @@ -27,8 +27,8 @@ "darkcyan", "darkgoldenrod", "darkgray", - "darkgrey", "darkgreen", + "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", @@ -56,9 +56,9 @@ "gold", "goldenrod", "gray", - "grey", "green", "greenyellow", + "grey", "honeydew", "hotpink", "indianred", @@ -74,8 +74,8 @@ "lightcyan", "lightgoldenrodyellow", "lightgray", - "lightgrey", "lightgreen", + "lightgrey", "lightpink", "lightsalmon", "lightseagreen", @@ -143,6 +143,7 @@ "teal", "thistle", "tomato", + "transparent", "turquoise", "violet", "wheat", diff --git a/src/stats.php b/src/stats.php index e77aa74c..73bac082 100644 --- a/src/stats.php +++ b/src/stats.php @@ -3,22 +3,21 @@ declare(strict_types=1); /** - * Build a query for a contribution graph + * Build a GraphQL query for a contribution graph * * @param string $user GitHub username to get graphs for * @param int $year Year to get graph for - * * @return string GraphQL query */ -function buildContributionGraphQuery(string $user, int $year) +function buildContributionGraphQuery(string $user, int $year): string { $start = "$year-01-01T00:00:00Z"; $end = "$year-12-31T23:59:59Z"; return "query { user(login: \"$user\") { contributionsCollection(from: \"$start\", to: \"$end\") { + contributionYears contributionCalendar { - totalContributions weeks { contributionDays { contributionCount @@ -32,60 +31,103 @@ function buildContributionGraphQuery(string $user, int $year) } /** - * Get all HTTP request responses for user's contributions + * Execute multiple requests with cURL and handle GitHub API rate limits and errors * * @param string $user GitHub username to get graphs for - * - * @return array List of contribution graph response objects + * @param array $years Years to get graphs for + * @return array List of GraphQL response objects with years as keys */ -function getContributionGraphs(string $user): array +function executeContributionGraphRequests(string $user, array $years): array { - // Get the years the user has contributed - $contributionYears = getContributionYears($user); - // build a list of individual requests + $tokens = []; $requests = []; - foreach ($contributionYears as $year) { - // create query for year + // build handles for each year + foreach ($years as $year) { + $tokens[$year] = getGitHubToken(); $query = buildContributionGraphQuery($user, $year); - // create curl request - $requests[$year] = getGraphQLCurlHandle($query); + $requests[$year] = getGraphQLCurlHandle($query, $tokens[$year]); } // build multi-curl handle $multi = curl_multi_init(); - foreach ($requests as $request) { - curl_multi_add_handle($multi, $request); + foreach ($requests as $handle) { + curl_multi_add_handle($multi, $handle); } // execute queries $running = null; do { curl_multi_exec($multi, $running); } while ($running); - // collect responses from last to first - $response = []; - foreach ($requests as $year => $request) { - $contents = curl_multi_getcontent($request); + // collect responses + $responses = []; + foreach ($requests as $year => $handle) { + $contents = curl_multi_getcontent($handle); $decoded = is_string($contents) ? json_decode($contents) : null; - // if response is empty or invalid, retry request one time - if (empty($decoded) || empty($decoded->data)) { + // if response is empty or invalid, retry request one time or throw an error + if (empty($decoded) || empty($decoded->data) || !empty($decoded->errors)) { + $message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred."); + $error_type = $decoded->errors[0]->type ?? ""; + // Missing SSL certificate + if (curl_errno($handle) === 60) { + throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 500); + } + // Other cURL error + elseif (curl_errno($handle)) { + throw new AssertionError("cURL error: " . curl_error($handle), 500); + } + // GitHub API error - Not Found + elseif ($error_type === "NOT_FOUND") { + throw new InvalidArgumentException("Could not find a user with that name.", 404); + } + // if rate limit is exceeded, don't retry with same token + if (str_contains($message, "rate limit exceeded")) { + removeGitHubToken($tokens[$year]); + } + error_log("First attempt to decode response for $user's $year contributions failed. $message"); + // retry request $query = buildContributionGraphQuery($user, $year); - $request = getGraphQLCurlHandle($query); + $token = getGitHubToken(); + $request = getGraphQLCurlHandle($query, $token); $contents = curl_exec($request); $decoded = is_string($contents) ? json_decode($contents) : null; // if the response is still empty or invalid, log an error and skip the year if (empty($decoded) || empty($decoded->data)) { $message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred."); + if (str_contains($message, "rate limit exceeded")) { + removeGitHubToken($token); + } error_log("Failed to decode response for $user's $year contributions after 2 attempts. $message"); continue; } } - array_unshift($response, $decoded); + $responses[$year] = $decoded; } // close the handles foreach ($requests as $request) { - curl_multi_remove_handle($multi, $request); + curl_multi_remove_handle($multi, $handle); } curl_multi_close($multi); - return $response; + return $responses; +} + +/** + * Get all HTTP request responses for user's contributions + * + * @param string $user GitHub username to get graphs for + * @return array List of contribution graph response objects + */ +function getContributionGraphs(string $user): array +{ + // get the list of years the user has contributed and the current year's contribution graph + $currentYear = intval(date("Y")); + $responses = executeContributionGraphRequests($user, [$currentYear]); + $contributionYears = $responses[$currentYear]->data->user->contributionsCollection->contributionYears; + // remove the current year from the list since it's already been fetched + $contributionYears = array_filter($contributionYears, function ($year) use ($currentYear) { + return $year !== $currentYear; + }); + // get the contribution graphs for the previous years + $responses += executeContributionGraphRequests($user, $contributionYears); + return $responses; } /** @@ -93,7 +135,7 @@ function getContributionGraphs(string $user): array * * @return array List of tokens */ -function getGitHubTokens() +function getGitHubTokens(): array { // result is already calculated if (isset($GLOBALS["ALL_TOKENS"])) { @@ -112,16 +154,54 @@ function getGitHubTokens() return $tokens; } +/** + * Get a token from the token pool + * + * @return string GitHub token + * + * @throws AssertionError if no tokens are available + */ +function getGitHubToken(): string +{ + $all_tokens = getGitHubTokens(); + // if there is no available token, throw an error (this should never happen) + if (empty($all_tokens)) { + throw new AssertionError("There is no GitHub token available.", 500); + } + return $all_tokens[array_rand($all_tokens)]; +} + +/** + * Remove a token from the token pool + * + * @param string $token Token to remove + * @return void + * + * @throws AssertionError if no tokens are available after removing the token + */ +function removeGitHubToken(string $token): void +{ + $index = array_search($token, $GLOBALS["ALL_TOKENS"]); + if ($index !== false) { + unset($GLOBALS["ALL_TOKENS"][$index]); + } + // if there is no available token, throw an error + if (empty($GLOBALS["ALL_TOKENS"])) { + throw new AssertionError( + "We are being rate-limited! Check git.io/streak-ratelimit for details.", + 429 + ); + } +} + /** Create a CurlHandle for a POST request to GitHub's GraphQL API * * @param string $query GraphQL query - * + * @param string $token GitHub token to use for the request * @return CurlHandle The curl handle for the request */ -function getGraphQLCurlHandle(string $query) +function getGraphQLCurlHandle(string $query, string $token): CurlHandle { - $all_tokens = getGitHubTokens(); - $token = $all_tokens[array_rand($all_tokens)]; $headers = [ "Authorization: bearer $token", "Content-Type: application/json", @@ -141,81 +221,11 @@ function getGraphQLCurlHandle(string $query) return $ch; } -/** - * Create a POST request to GitHub's GraphQL API - * - * @param string $query GraphQL query - * - * @return stdClass An object from the json response of the request - * - * @throws AssertionError If SSL verification fails - */ -function fetchGraphQL(string $query): stdClass -{ - $ch = getGraphQLCurlHandle($query); - $response = curl_exec($ch); - curl_close($ch); - $obj = json_decode($response); - // handle curl errors - if ($response === false || $obj === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) { - // set response code to curl error code - http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE)); - // Missing SSL certificate - if (str_contains(curl_error($ch), "unable to get local issuer certificate")) { - throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 400); - } - // Handle errors such as "Bad credentials" - if ($obj && $obj->message) { - throw new AssertionError("Error: $obj->message \n", 401); - } - throw new AssertionError("An error occurred when getting a response from GitHub.\n", 502); - } - return $obj; -} - -/** - * Get the years the user has contributed - * - * @param string $user GitHub username to get years for - * - * @return array List of years the user has contributed - * - * @throws InvalidArgumentException If the user doesn't exist or there is an error - */ -function getContributionYears(string $user): array -{ - $query = "query { - user(login: \"$user\") { - contributionsCollection { - contributionYears - } - } - }"; - $response = fetchGraphQL($query); - // User not found - if (!empty($response->errors)) { - $type = $response->errors[0]->type ?? ""; - if ($type === "NOT_FOUND") { - throw new InvalidArgumentException("Could not find a user with that name.", 404); - } - $message = $response->errors[0]->message ?? "An API error occurred."; - // Other errors that contain a message field - throw new InvalidArgumentException($message, 502); - } - // API did not return data - if (!isset($response->data) && isset($response->message)) { - // Other errors that contain a message field - throw new InvalidArgumentException($response->message, 204); - } - return $response->data->user->contributionsCollection->contributionYears; -} - /** * Get an array of all dates with the number of contributions * - * @param array $contributionCalendars List of GraphQL response objects - * - * @return array Y-M-D dates mapped to the number of contributions + * @param array $contributionCalendars List of GraphQL response objects by year + * @return array Y-M-D dates mapped to the number of contributions */ function getContributionDates(array $contributionGraphs): array { @@ -245,8 +255,8 @@ function getContributionDates(array $contributionGraphs): array /** * Get a stats array with the contribution count, daily streak, and dates * - * @param array $contributions Y-M-D contribution dates with contribution counts - * @return array Streak stats + * @param array $contributions Y-M-D contribution dates with contribution counts + * @return array Streak stats */ function getContributionStats(array $contributions): array { @@ -323,8 +333,8 @@ function getPreviousSunday(string $date): string /** * Get a stats array with the contribution count, weekly streak, and dates * - * @param array $contributions Y-M-D contribution dates with contribution counts - * @return array Streak stats + * @param array $contributions Y-M-D contribution dates with contribution counts + * @return array Streak stats */ function getWeeklyContributionStats(array $contributions): array { diff --git a/src/translations.php b/src/translations.php index ce1a14f7..26ec3f2d 100644 --- a/src/translations.php +++ b/src/translations.php @@ -42,12 +42,20 @@ "ar" => [ "rtl" => true, "Total Contributions" => "إجمالي المساهمات", - "Current Streak" => "حالِيا سلسلة متتالية", - "Longest Streak" => "طَويل سلسلة متتالية", - "Week Streak" => "أُسْبوع سلسلة متتالية", - "Longest Week Streak" => "طَويل أُسْبوع سلسلة متتالية", + "Current Streak" => "السلسلة المتتالية الحالية", + "Longest Streak" => "أُطول سلسلة متتالية", + "Week Streak" => "السلسلة المتتالية الأُسبوعية", + "Longest Week Streak" => "أُطول سلسلة متتالية أُسبوعية", "Present" => "الحاضر", ], + "bg" => [ + "Total Contributions" => "Общ принос", + "Current Streak" => "Дневна серия", + "Longest Streak" => "Най-дълга дневна серия", + "Week Streak" => "Седмична серия", + "Longest Week Streak" => "Най-дълга седмична серия", + "Present" => "Сега", + ], "bn" => [ "Total Contributions" => "মোট অবদান", "Current Streak" => "কারেন্ট স্ট্রীক", @@ -69,7 +77,7 @@ "Present" => "Heute", ], "es" => [ - "Total Contributions" => "Todas Contribuciones", + "Total Contributions" => "Contribuciones Totales", "Current Streak" => "Racha Actual", "Longest Streak" => "Racha Más Larga", "Week Streak" => "Racha Semanal", @@ -89,6 +97,8 @@ "Total Contributions" => "Contributions totales", "Current Streak" => "Séquence actuelle", "Longest Streak" => "Plus longue séquence", + "Week Streak" => "Séquence de la semaine", + "Longest Week Streak" => "Plus longue séquence hebdomadaire", "Present" => "Aujourd'hui", ], "he" => [ @@ -165,6 +175,14 @@ "Longest Week Streak" => "Najdłuższa Seria Tygodni", "Present" => "Dziś", ], + "ps" => [ + "Total Contributions" => "ټولې ونډې", + "Current Streak" => "اوسنی پرمختګ", + "Longest Streak" => "تر ټولو اوږد پرمختګ", + "Week Streak" => "د اونۍ پرمختګ", + "Longest Week Streak" => "د اونۍ تر ټولو اوږد پرمختګ", + "Present" => "اوس", + ], "pt_BR" => [ "Total Contributions" => "Total de Contribuições", "Current Streak" => "Sequência Atual", @@ -177,6 +195,8 @@ "Total Contributions" => "Общий вклад", "Current Streak" => "Текущая серия", "Longest Streak" => "Самая длинная серия", + "Week Streak" => "Текущая серия недель", + "Longest Week Streak" => "Самая длинная серия недель", "Present" => "Сейчас", ], "ta" => [ @@ -191,12 +211,28 @@ "Longest Streak" => "En Uzun Seri", "Present" => "Şu an", ], + "uk" => [ + "Total Contributions" => "Загальний вклад", + "Current Streak" => "Поточна діяльність", + "Longest Streak" => "Найдовша діяльність", + "Week Streak" => "Діяльність за тиждень", + "Longest Week Streak" => "Найбільша к-сть тижнів", + "Present" => "Наразі", + ], "vi" => [ "Total Contributions" => "Tổng số đóng góp", "Current Streak" => "Chuỗi đóng góp\nhiện tại", "Longest Streak" => "Chuỗi đóng góp lớn nhất", "Present" => "Hiện tại", ], + "yo" => [ + "Total Contributions" => "Lapapọ ilowosi", + "Current Streak" => "ṣiṣan lọwọlọwọ", + "Longest Streak" => "ṣiṣan ti o gun julọ", + "Week Streak" => "ṣiṣan ọsẹ", + "Longest Week Streak" => "gunjulo ọsẹ ṣiṣan", + "Present" => "lọwọlọwọ", + ], "zh" => "zh_Hans", "zh_Hans" => [ "Total Contributions" => "合计贡献", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy