✨ read contribution calendar from github api
This commit is contained in:
@@ -2,6 +2,8 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { generateContributionSnake } from "../generateContributionSnake";
|
||||
import { parseOutputsOption } from "../outputsOptions";
|
||||
import { config } from "dotenv";
|
||||
config({ path: __dirname + "/../../../.env" });
|
||||
|
||||
jest.setTimeout(2 * 60 * 1000);
|
||||
|
||||
@@ -30,7 +32,9 @@ it(
|
||||
|
||||
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[1]).toBeDefined();
|
||||
|
||||
@@ -12,10 +12,11 @@ export const generateContributionSnake = async (
|
||||
format: "svg" | "gif";
|
||||
drawOptions: DrawOptions;
|
||||
animationOptions: AnimationOptions;
|
||||
} | null)[]
|
||||
} | null)[],
|
||||
options: { githubToken: string }
|
||||
) => {
|
||||
console.log("🎣 fetching github user contribution");
|
||||
const cells = await getGithubUserContribution(userName);
|
||||
const cells = await getGithubUserContribution(userName, options);
|
||||
|
||||
const grid = userContributionToGrid(cells);
|
||||
const snake = snake4;
|
||||
|
||||
@@ -12,11 +12,14 @@ import { parseOutputsOption } from "./outputsOptions";
|
||||
core.getInput("svg_out_path"),
|
||||
]
|
||||
);
|
||||
const githubToken = process.env.GITHUB_TOKEN!;
|
||||
|
||||
const { generateContributionSnake } = await import(
|
||||
"./generateContributionSnake"
|
||||
);
|
||||
const results = await generateContributionSnake(userName, outputs);
|
||||
const results = await generateContributionSnake(userName, outputs, {
|
||||
githubToken,
|
||||
});
|
||||
|
||||
outputs.forEach((out, i) => {
|
||||
const result = results[i];
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"@snk/types": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "0.36.1"
|
||||
"@vercel/ncc": "0.36.1",
|
||||
"dotenv": "16.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import path from "path";
|
||||
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 { 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");
|
||||
|
||||
@@ -13,7 +14,11 @@ const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
||||
onAfterSetupMiddleware: ({ app }) => {
|
||||
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
||||
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 {
|
||||
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
||||
res.statusCode = 200;
|
||||
res.json(await getGithubUserContribution(userName as string));
|
||||
res.json(
|
||||
await getGithubUserContribution(userName as string, {
|
||||
githubToken: process.env.GITHUB!,
|
||||
})
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
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 { config } from "dotenv";
|
||||
config({ path: __dirname + "/../../../.env" });
|
||||
|
||||
describe("getGithubUserContribution", () => {
|
||||
const promise = getGithubUserContribution("platane");
|
||||
const promise = getGithubUserContribution("platane", {
|
||||
githubToken: process.env.GITHUB_TOKEN!,
|
||||
});
|
||||
|
||||
it("should resolve", async () => {
|
||||
console.log(
|
||||
"process.env.GITHUB_TOKEN",
|
||||
process.env.GITHUB_TOKEN?.replace(/\d/g, "x")
|
||||
);
|
||||
|
||||
await promise;
|
||||
});
|
||||
|
||||
@@ -27,9 +36,3 @@ describe("getGithubUserContribution", () => {
|
||||
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 { formatParams, Options } from "./formatParams";
|
||||
|
||||
/**
|
||||
* get the contribution grid from a github user page
|
||||
@@ -19,57 +18,83 @@ import { formatParams, Options } from "./formatParams";
|
||||
*/
|
||||
export const getGithubUserContribution = async (
|
||||
userName: string,
|
||||
options: Options = {}
|
||||
o: { githubToken: string }
|
||||
) => {
|
||||
// either use github.com/users/xxxx/contributions for previous years
|
||||
// or github.com/xxxx ( which gives the latest update to today result )
|
||||
const url =
|
||||
"year" in options || "from" in options || "to" in options
|
||||
? `https://github.com/users/${userName}/contributions?` +
|
||||
formatParams(options)
|
||||
: `https://github.com/${userName}`;
|
||||
const query = /* GraphQL */ `
|
||||
query ($login: String!) {
|
||||
user(login: $login) {
|
||||
contributionsCollection {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
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);
|
||||
|
||||
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) => {
|
||||
// take roughly the table block
|
||||
const block = content
|
||||
.split(`aria-describedby="contribution-graph-description"`)[1]
|
||||
.split("<tbody>")[1]
|
||||
.split("</tbody>")[0];
|
||||
|
||||
const cells = block.split("</tr>").flatMap((inside, y) =>
|
||||
inside.split("</td>").flatMap((m) => {
|
||||
const date = m.match(/data-date="([^"]+)"/)?.[1];
|
||||
|
||||
const literalLevel = m.match(/data-level="([^"]+)"/)?.[1];
|
||||
const literalX = m.match(/data-ix="([^"]+)"/)?.[1];
|
||||
const literalCount = m.match(/(No|\d+) contributions? on/)?.[1];
|
||||
|
||||
if (date && literalLevel && literalX && literalCount)
|
||||
return [
|
||||
{
|
||||
x: +literalX,
|
||||
y,
|
||||
|
||||
date,
|
||||
count: +literalCount,
|
||||
level: +literalLevel,
|
||||
},
|
||||
];
|
||||
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
return cells;
|
||||
type GraphQLRes = {
|
||||
user: {
|
||||
contributionsCollection: {
|
||||
contributionCalendar: {
|
||||
weeks: {
|
||||
contributionDays: {
|
||||
contributionCount: number;
|
||||
contributionLevel:
|
||||
| "FOURTH_QUARTILE"
|
||||
| "THIRD_QUARTILE"
|
||||
| "SECOND_QUARTILE"
|
||||
| "FIRST_QUARTILE"
|
||||
| "NONE";
|
||||
date: string;
|
||||
weekday: number;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"node-fetch": "2.6.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.6.2"
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"dotenv": "16.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user