Skip to content

Commit

Permalink
fix: Migration to GraphQL API (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
DenverCoder1 authored Oct 6, 2021
1 parent 6ed28c7 commit 14c63c0
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 133 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
TOKEN=
USERNAME=
# replace ghp_example with your GitHub PAT token
TOKEN=ghp_example
1 change: 0 additions & 1 deletion .github/workflows/phpunit-ci-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,3 @@ jobs:
args: --testdox
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: DenverCoder1
12 changes: 4 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,12 @@ cd github-readme-streak-stats

To get the GitHub API to run locally you will need to provide a token.

1. Go to https://github.com/settings/tokens.
2. Click **"Generate new token."**
3. Add a note (ex. **"GitHub Readme Streak Stats"**), then scroll to the bottom and click **"Generate token."**
4. **Copy** the token to your clipboard.
5. **Create** a file `config.php` in the `src` directory and replace `ghp_example123` with **your token** and `DenverCoder1` with **your username**:
1. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token
2. Scroll to the bottom and click **"Generate token"**
3. **Make a copy** of `.env.example` named `.env` in the root directory and add **your token** after `TOKEN=`.

```php
<?php
putenv("TOKEN=ghp_example123");
putenv("USERNAME=DenverCoder1");
TOKEN=<your-token>
```

This comment has been minimized.

Copy link
@gauravjangid2711

### Install dependencies
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,10 @@ To get the GitHub API to run locally you will need to provide a token.

1. Visit [this link](https://github.com/settings/tokens/new?description=GitHub%20Readme%20Streak%20Stats) to create a new Personal Access Token
2. Scroll to the bottom and click **"Generate token"**
3. **Make a copy** of `.env.example` named `.env` in the root directory and add **your token** after `TOKEN=` and **your username** after `USERNAME=`:
3. **Make a copy** of `.env.example` named `.env` in the root directory and add **your token** after `TOKEN=`:

```php
TOKEN=
USERNAME=
TOKEN=<your-token>
```

### Running the app locally
Expand Down
4 changes: 0 additions & 4 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
"TOKEN": {
"description": "GitHub personal access token obtained from https://github.com/settings/tokens/new",
"required": false
},
"USERNAME": {
"description": "GitHub username associated with the token",
"required": false
}
},
"formation": {
Expand Down
7 changes: 3 additions & 4 deletions src/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@
$dotenv->safeLoad();

// if environment variables are not loaded, display error
if (!$_SERVER["TOKEN"] || !$_SERVER["USERNAME"]) {
$message = file_exists(dirname(__DIR__ . '.env', 1))
? "Missing token or username in config. Check Contributing.md for details."
if (!isset($_SERVER["TOKEN"])) {
$message = file_exists(dirname(__DIR__ . '../.env', 1))
? "Missing token in config. Check Contributing.md for details."
: ".env was not found. Check Contributing.md for details.";

renderOutput($message);
}

Expand Down
223 changes: 121 additions & 102 deletions src/stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,156 +6,175 @@
* Get all HTTP request responses for user's contributions
*
* @param string $user GitHub username to get graphs for
* @return array<string> List of HTML contribution graphs
*
* @return array<stdClass> List of contribution graph response objects
*/
function getContributionGraphs(string $user): array
{
// get the start year based on when the user first contributed
$startYear = getYearJoined($user);
$currentYear = intval(date("Y"));
// Get the years the user has contributed
$contributionYears = getContributionYears($user);
// build a list of individual requests
$urls = array();
for ($year = $currentYear; $year >= $startYear; $year--) {
// create url with year set as end date
$url = "https://github.com/users/${user}/contributions?to=${year}-12-31";
$requests = array();
foreach ($contributionYears as $year) {
// create query for year
$start = "$year-01-01T00:00:00Z";
$end = "$year-12-31T23:59:59Z";
$query = "query {
user(login: \"$user\") {
contributionsCollection(from: \"$start\", to: \"$end\") {
contributionCalendar {
totalContributions
weeks {
contributionDays {
contributionCount
date
}
}
}
}
}
}";
// create curl request
$urls[$year] = curl_init();
// set options for curl
curl_setopt($urls[$year], CURLOPT_AUTOREFERER, true);
curl_setopt($urls[$year], CURLOPT_HEADER, false);
curl_setopt($urls[$year], CURLOPT_RETURNTRANSFER, true);
curl_setopt($urls[$year], CURLOPT_URL, $url);
curl_setopt($urls[$year], CURLOPT_FOLLOWLOCATION, true);
curl_setopt($urls[$year], CURLOPT_VERBOSE, false);
curl_setopt($urls[$year], CURLOPT_SSL_VERIFYPEER, true);
$requests[$year] = getGraphQLCurlHandle($query);
}
// build multi-curl handle
$multi = curl_multi_init();
foreach ($urls as $url) {
curl_multi_add_handle($multi, $url);
foreach ($requests as $request) {
curl_multi_add_handle($multi, $request);
}
// execute queries
$running = null;
do {
curl_multi_exec($multi, $running);
} while ($running);
// close the handles
foreach ($urls as $url) {
curl_multi_remove_handle($multi, $url);
foreach ($requests as $request) {
curl_multi_remove_handle($multi, $request);
}
curl_multi_close($multi);
// collect responses from last to first
$response = array();
foreach ($urls as $url) {
array_unshift($response, curl_multi_getcontent($url));
foreach ($requests as $request) {
array_unshift($response, json_decode(curl_multi_getcontent($request)));
}
return $response;
}

/**
* Get an array of all dates with the number of contributions
*
* @param array<string> $contributionGraphs List of HTML pages with contributions
* @return array<string, int> Y-M-D dates mapped to the number of contributions
/** Create a CurlHandle for a POST request to GitHub's GraphQL API
*
* @param string $query GraphQL query
*
* @return CurlHandle The curl handle for the request
*/
function getContributionDates(array $contributionGraphs): array
function getGraphQLCurlHandle(string $query)
{
// get contributions from HTML
$contributions = array();
$today = date("Y-m-d");
$tomorrow = date("Y-m-d", strtotime("tomorrow"));
foreach ($contributionGraphs as $graph) {
// if HTML contains "Please wait", we are being rate-limited
if (strpos($graph, "Please wait") !== false) {
throw new AssertionError("We are being rate-limited! Check <a href='https://git.io/streak-ratelimit' font-weight='bold'>git.io/streak-ratelimit</a> for details.");
}
// split into lines
$lines = explode("\n", $graph);
// add the dates and contribution counts to the array
foreach ($lines as $line) {
preg_match("/ data-date=\"([0-9\-]{10})\"/", $line, $dateMatch);
preg_match("/ data-count=\"(\d+?)\"/", $line, $countMatch);
if (isset($dateMatch[1]) && isset($countMatch[1])) {
$date = $dateMatch[1];
$count = (int) $countMatch[1];
// count contributions up until today
// also count next day if user contributed already
if ($date <= $today || ($date == $tomorrow && $count > 0)) {
// add contributions to the array
$contributions[$date] = $count;
}
}
}
}
return $contributions;
$token = $_SERVER["TOKEN"];
$headers = array(
"Authorization: bearer $token",
"Content-Type: application/json",
"Accept: application/vnd.github.v4.idl",
"User-Agent: GitHub-Readme-Streak-Stats"
);
$body = array("query" => $query);
// create curl request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.github.com/graphql");
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_VERBOSE, false);
return $ch;
}

/**
* Get the contents of a single URL passing headers for GitHub API
* Create a POST request to GitHub's GraphQL API
*
* @param string $query GraphQL query
*
* @param string $url URL to fetch
* @return string Response from page as a string
* @return stdClass An object from the json response of the request
*
* @throws AssertionError If SSL verification fails
*/
function getGitHubApiResponse(string $url): string
function fetchGraphQL(string $query): stdClass
{
$ch = curl_init();
$token = $_SERVER["TOKEN"];
$username = $_SERVER["USERNAME"];
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Accept: application/vnd.github.v3+json",
"Authorization: token $token",
"User-Agent: $username",
]);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_VERBOSE, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$ch = getGraphQLCurlHandle($query);
$response = curl_exec($ch);
// handle curl errors
if ($response === false) {
if (str_contains(curl_error($ch), 'unable to get local issuer certificate')) {
throw new InvalidArgumentException("You don't have a valid SSL Certificate installed or XAMPP.");
throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.");
}
throw new InvalidArgumentException("An error occurred when getting a response from GitHub.");
throw new AssertionError("An error occurred when getting a response from GitHub.");
}
// close curl handle and return response
curl_close($ch);
return $response;
return json_decode($response);
}

/**
* Get the first year a user contributed
* 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
*
* @param string $user GitHub username to look up
* @return int first contribution year
* @throws InvalidArgumentException If the user doesn't exist or there is an error
*/
function getYearJoined(string $user): int
function getContributionYears(string $user): array
{
// load the user's profile info
$response = getGitHubApiResponse("https://api.github.com/users/${user}");
$json = json_decode($response);
// find the year the user was created
if ($json && isset($json->type) && $json->type == "User" && isset($json->created_at)) {
return intval(substr($json->created_at, 0, 4));
}
// Account is not a user (eg. Organization account)
if (isset($json->type)) {
throw new InvalidArgumentException("The username given is not a user.");
$query = "query {
user(login: \"$user\") {
contributionsCollection {
contributionYears
}
}
}";
$response = fetchGraphQL($query);
// User not found
if (!empty($response->errors) && $response->errors[0]->type === "NOT_FOUND") {
throw new InvalidArgumentException("Could not find a user with that name.");
}
// API Error
if ($json && isset($json->message)) {
// User not found
if ($json->message == "Not Found") {
throw new InvalidArgumentException("User could not be found.");
}
if (!empty($response->errors)) {
// Other errors that contain a message field
throw new InvalidArgumentException($json->message);
throw new InvalidArgumentException($response->data->errors[0]->message);
}
// Response doesn't contain a message field
throw new InvalidArgumentException("An unknown error occurred.");
return $response->data->user->contributionsCollection->contributionYears;
}

/**
* Get an array of all dates with the number of contributions
*
* @param array<string> $contributionCalendars List of GraphQL response objects
*
* @return array<string, int> Y-M-D dates mapped to the number of contributions
*/
function getContributionDates(array $contributionGraphs): array
{
// get contributions from HTML
$contributions = array();
$today = date("Y-m-d");
$tomorrow = date("Y-m-d", strtotime("tomorrow"));
foreach ($contributionGraphs as $graph) {
if (!empty($graph->errors)) {
throw new AssertionError($graph->data->errors[0]->message);
}
$weeks = $graph->data->user->contributionsCollection->contributionCalendar->weeks;
foreach ($weeks as $week) {
foreach ($week->contributionDays as $day) {
$date = $day->date;
$count = $day->contributionCount;
// count contributions up until today
// also count next day if user contributed already
if ($date <= $today || ($date == $tomorrow && $count > 0)) {
// add contributions to the array
$contributions[$date] = $count;
}
}
}
}
return $contributions;
}

/**
Expand Down
Loading

0 comments on commit 14c63c0

Please sign in to comment.