✨ read contribution calendar from github api
This commit is contained in:
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@@ -17,6 +17,8 @@ jobs:
|
|||||||
- run: npm run type
|
- run: npm run type
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run test --ci
|
- run: npm run test --ci
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
test-action:
|
test-action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -36,6 +38,8 @@ jobs:
|
|||||||
dist/github-contribution-grid-snake.svg
|
dist/github-contribution-grid-snake.svg
|
||||||
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
||||||
dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
dist/github-contribution-grid-snake.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: ensure the generated file exists
|
- name: ensure the generated file exists
|
||||||
run: |
|
run: |
|
||||||
@@ -75,6 +79,8 @@ jobs:
|
|||||||
outputs: |
|
outputs: |
|
||||||
dist/github-contribution-grid-snake.svg
|
dist/github-contribution-grid-snake.svg
|
||||||
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: ensure the generated file exists
|
- name: ensure the generated file exists
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ npm-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
dist
|
dist
|
||||||
!svg-only/dist
|
!svg-only/dist
|
||||||
build
|
build
|
||||||
|
.env
|
||||||
@@ -2,6 +2,8 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { generateContributionSnake } from "../generateContributionSnake";
|
import { generateContributionSnake } from "../generateContributionSnake";
|
||||||
import { parseOutputsOption } from "../outputsOptions";
|
import { parseOutputsOption } from "../outputsOptions";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
config({ path: __dirname + "/../../../.env" });
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000);
|
jest.setTimeout(2 * 60 * 1000);
|
||||||
|
|
||||||
@@ -30,7 +32,9 @@ it(
|
|||||||
|
|
||||||
const outputs = parseOutputsOption(entries);
|
const outputs = parseOutputsOption(entries);
|
||||||
|
|
||||||
const results = await generateContributionSnake("platane", outputs);
|
const results = await generateContributionSnake("platane", outputs, {
|
||||||
|
githubToken: process.env.GITHUB_TOKEN!,
|
||||||
|
});
|
||||||
|
|
||||||
expect(results[0]).toBeDefined();
|
expect(results[0]).toBeDefined();
|
||||||
expect(results[1]).toBeDefined();
|
expect(results[1]).toBeDefined();
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ export const generateContributionSnake = async (
|
|||||||
format: "svg" | "gif";
|
format: "svg" | "gif";
|
||||||
drawOptions: DrawOptions;
|
drawOptions: DrawOptions;
|
||||||
animationOptions: AnimationOptions;
|
animationOptions: AnimationOptions;
|
||||||
} | null)[]
|
} | null)[],
|
||||||
|
options: { githubToken: string }
|
||||||
) => {
|
) => {
|
||||||
console.log("🎣 fetching github user contribution");
|
console.log("🎣 fetching github user contribution");
|
||||||
const cells = await getGithubUserContribution(userName);
|
const cells = await getGithubUserContribution(userName, options);
|
||||||
|
|
||||||
const grid = userContributionToGrid(cells);
|
const grid = userContributionToGrid(cells);
|
||||||
const snake = snake4;
|
const snake = snake4;
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ import { parseOutputsOption } from "./outputsOptions";
|
|||||||
core.getInput("svg_out_path"),
|
core.getInput("svg_out_path"),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
const githubToken = process.env.GITHUB_TOKEN!;
|
||||||
|
|
||||||
const { generateContributionSnake } = await import(
|
const { generateContributionSnake } = await import(
|
||||||
"./generateContributionSnake"
|
"./generateContributionSnake"
|
||||||
);
|
);
|
||||||
const results = await generateContributionSnake(userName, outputs);
|
const results = await generateContributionSnake(userName, outputs, {
|
||||||
|
githubToken,
|
||||||
|
});
|
||||||
|
|
||||||
outputs.forEach((out, i) => {
|
outputs.forEach((out, i) => {
|
||||||
const result = results[i];
|
const result = results[i];
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"@snk/types": "1.0.0"
|
"@snk/types": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "0.36.1"
|
"@vercel/ncc": "0.36.1",
|
||||||
|
"dotenv": "16.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
|
|
||||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
|
||||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
|
||||||
import webpack from "webpack";
|
import webpack from "webpack";
|
||||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
import type { Configuration as WebpackConfiguration } from "webpack";
|
||||||
|
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||||
|
config({ path: __dirname + "/../../.env" });
|
||||||
|
|
||||||
const demos: string[] = require("./demo.json");
|
const demos: string[] = require("./demo.json");
|
||||||
|
|
||||||
@@ -13,7 +14,11 @@ const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
|||||||
onAfterSetupMiddleware: ({ app }) => {
|
onAfterSetupMiddleware: ({ app }) => {
|
||||||
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
||||||
const userName: string = req.params.userName;
|
const userName: string = req.params.userName;
|
||||||
res.send(await getGithubUserContribution(userName));
|
res.send(
|
||||||
|
await getGithubUserContribution(userName, {
|
||||||
|
githubToken: process.env.GITHUB_TOKEN!,
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
try {
|
try {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.json(await getGithubUserContribution(userName as string));
|
res.json(
|
||||||
|
await getGithubUserContribution(userName as string, {
|
||||||
|
githubToken: process.env.GITHUB!,
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
|||||||
import { formatParams } from "../formatParams";
|
|
||||||
|
|
||||||
const params = [
|
|
||||||
//
|
|
||||||
[{}, ""],
|
|
||||||
[{ year: 2017 }, "from=2017-01-01&to=2017-12-31"],
|
|
||||||
[{ from: "2017-12-03" }, "from=2017-12-03"],
|
|
||||||
[{ to: "2017-12-03" }, "to=2017-12-03"],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
params.forEach(([params, res]) =>
|
|
||||||
it(`should format ${JSON.stringify(params)}`, () => {
|
|
||||||
expect(formatParams(params)).toBe(res);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
it("should fail if the date is in the future", () => {
|
|
||||||
expect(() => formatParams({ to: "9999-01-01" })).toThrow(Error);
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
import { getGithubUserContribution } from "..";
|
import { getGithubUserContribution } from "..";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
config({ path: __dirname + "/../../../.env" });
|
||||||
|
|
||||||
describe("getGithubUserContribution", () => {
|
describe("getGithubUserContribution", () => {
|
||||||
const promise = getGithubUserContribution("platane");
|
const promise = getGithubUserContribution("platane", {
|
||||||
|
githubToken: process.env.GITHUB_TOKEN!,
|
||||||
|
});
|
||||||
|
|
||||||
it("should resolve", async () => {
|
it("should resolve", async () => {
|
||||||
|
console.log(
|
||||||
|
"process.env.GITHUB_TOKEN",
|
||||||
|
process.env.GITHUB_TOKEN?.replace(/\d/g, "x")
|
||||||
|
);
|
||||||
|
|
||||||
await promise;
|
await promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,9 +36,3 @@ describe("getGithubUserContribution", () => {
|
|||||||
expect(undefinedDays).toEqual([]);
|
expect(undefinedDays).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("should match snapshot for year=2019", async () => {
|
|
||||||
expect(
|
|
||||||
await getGithubUserContribution("platane", { year: 2019 })
|
|
||||||
).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
export type Options = { from?: string; to?: string } | { year: number };
|
|
||||||
|
|
||||||
export const formatParams = (options: Options = {}) => {
|
|
||||||
const sp = new URLSearchParams();
|
|
||||||
|
|
||||||
const o: any = { ...options };
|
|
||||||
|
|
||||||
if ("year" in options) {
|
|
||||||
o.from = `${options.year}-01-01`;
|
|
||||||
o.to = `${options.year}-12-31`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const s of ["from", "to"])
|
|
||||||
if (o[s]) {
|
|
||||||
const value = o[s];
|
|
||||||
|
|
||||||
if (value >= formatDate(new Date()))
|
|
||||||
throw new Error(
|
|
||||||
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
|
|
||||||
);
|
|
||||||
|
|
||||||
sp.set(s, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sp.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (d: Date) => {
|
|
||||||
const year = d.getUTCFullYear();
|
|
||||||
const month = d.getUTCMonth() + 1;
|
|
||||||
const date = d.getUTCDate();
|
|
||||||
|
|
||||||
return [
|
|
||||||
year,
|
|
||||||
month.toString().padStart(2, "0"),
|
|
||||||
date.toString().padStart(2, "0"),
|
|
||||||
].join("-");
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { formatParams, Options } from "./formatParams";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the contribution grid from a github user page
|
* get the contribution grid from a github user page
|
||||||
@@ -19,57 +18,83 @@ import { formatParams, Options } from "./formatParams";
|
|||||||
*/
|
*/
|
||||||
export const getGithubUserContribution = async (
|
export const getGithubUserContribution = async (
|
||||||
userName: string,
|
userName: string,
|
||||||
options: Options = {}
|
o: { githubToken: string }
|
||||||
) => {
|
) => {
|
||||||
// either use github.com/users/xxxx/contributions for previous years
|
const query = /* GraphQL */ `
|
||||||
// or github.com/xxxx ( which gives the latest update to today result )
|
query ($login: String!) {
|
||||||
const url =
|
user(login: $login) {
|
||||||
"year" in options || "from" in options || "to" in options
|
contributionsCollection {
|
||||||
? `https://github.com/users/${userName}/contributions?` +
|
contributionCalendar {
|
||||||
formatParams(options)
|
weeks {
|
||||||
: `https://github.com/${userName}`;
|
contributionDays {
|
||||||
|
contributionCount
|
||||||
|
contributionLevel
|
||||||
|
weekday
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const variables = { login: userName };
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch("https://api.github.com/graphql", {
|
||||||
|
headers: {
|
||||||
|
Authorization: `bearer ${o.githubToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ variables, query }),
|
||||||
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error(res.statusText);
|
if (!res.ok) throw new Error(res.statusText);
|
||||||
|
|
||||||
const resText = await res.text();
|
const { data, errors } = (await res.json()) as {
|
||||||
|
data: GraphQLRes;
|
||||||
|
errors?: { message: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
return parseUserPage(resText);
|
if (errors?.[0]) throw errors[0];
|
||||||
|
|
||||||
|
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(
|
||||||
|
({ contributionDays }, x) =>
|
||||||
|
contributionDays.map((d) => ({
|
||||||
|
x,
|
||||||
|
y: d.weekday,
|
||||||
|
date: d.date,
|
||||||
|
count: d.contributionCount,
|
||||||
|
level:
|
||||||
|
(d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
||||||
|
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
||||||
|
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
||||||
|
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
||||||
|
0,
|
||||||
|
}))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseUserPage = (content: string) => {
|
type GraphQLRes = {
|
||||||
// take roughly the table block
|
user: {
|
||||||
const block = content
|
contributionsCollection: {
|
||||||
.split(`aria-describedby="contribution-graph-description"`)[1]
|
contributionCalendar: {
|
||||||
.split("<tbody>")[1]
|
weeks: {
|
||||||
.split("</tbody>")[0];
|
contributionDays: {
|
||||||
|
contributionCount: number;
|
||||||
const cells = block.split("</tr>").flatMap((inside, y) =>
|
contributionLevel:
|
||||||
inside.split("</td>").flatMap((m) => {
|
| "FOURTH_QUARTILE"
|
||||||
const date = m.match(/data-date="([^"]+)"/)?.[1];
|
| "THIRD_QUARTILE"
|
||||||
|
| "SECOND_QUARTILE"
|
||||||
const literalLevel = m.match(/data-level="([^"]+)"/)?.[1];
|
| "FIRST_QUARTILE"
|
||||||
const literalX = m.match(/data-ix="([^"]+)"/)?.[1];
|
| "NONE";
|
||||||
const literalCount = m.match(/(No|\d+) contributions? on/)?.[1];
|
date: string;
|
||||||
|
weekday: number;
|
||||||
if (date && literalLevel && literalX && literalCount)
|
}[];
|
||||||
return [
|
}[];
|
||||||
{
|
};
|
||||||
x: +literalX,
|
};
|
||||||
y,
|
};
|
||||||
|
|
||||||
date,
|
|
||||||
count: +literalCount,
|
|
||||||
level: +literalLevel,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return [];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return cells;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"node-fetch": "2.6.12"
|
"node-fetch": "2.6.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node-fetch": "2.6.2"
|
"@types/node-fetch": "2.6.4",
|
||||||
|
"dotenv": "16.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@@ -1264,14 +1264,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
||||||
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
|
||||||
|
|
||||||
"@types/node-fetch@2.6.2":
|
|
||||||
version "2.6.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
|
|
||||||
integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==
|
|
||||||
dependencies:
|
|
||||||
"@types/node" "*"
|
|
||||||
form-data "^3.0.0"
|
|
||||||
|
|
||||||
"@types/node-fetch@2.6.3":
|
"@types/node-fetch@2.6.3":
|
||||||
version "2.6.3"
|
version "2.6.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.3.tgz#175d977f5e24d93ad0f57602693c435c57ad7e80"
|
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.3.tgz#175d977f5e24d93ad0f57602693c435c57ad7e80"
|
||||||
@@ -1280,6 +1272,14 @@
|
|||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
form-data "^3.0.0"
|
form-data "^3.0.0"
|
||||||
|
|
||||||
|
"@types/node-fetch@2.6.4":
|
||||||
|
version "2.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660"
|
||||||
|
integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
form-data "^3.0.0"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "14.0.23"
|
version "14.0.23"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806"
|
||||||
@@ -2695,6 +2695,11 @@ dot-case@^3.0.4:
|
|||||||
no-case "^3.0.4"
|
no-case "^3.0.4"
|
||||||
tslib "^2.0.3"
|
tslib "^2.0.3"
|
||||||
|
|
||||||
|
dotenv@16.3.1:
|
||||||
|
version "16.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||||
|
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
|
||||||
|
|
||||||
download@^6.2.2:
|
download@^6.2.2:
|
||||||
version "6.2.5"
|
version "6.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714"
|
resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714"
|
||||||
|
|||||||
Reference in New Issue
Block a user