Skip to content
This repository was archived by the owner on Nov 10, 2022. It is now read-only.

Commit 38ad8f1

Browse files
ilyavolodinKai Cataldo
authored and
Kai Cataldo
committed
Update: Pull GitHub Sponsors donation data (#658)
1 parent 8601f36 commit 38ad8f1

File tree

3 files changed

+212
-83
lines changed

3 files changed

+212
-83
lines changed

_tools/fetch-sponsors.js

+142-53
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
2-
* @fileoverview Script to fetch sponsor data from Open Collective. Call using:
2+
* @fileoverview Script to fetch sponsor data from Open Collective and GitHub.
3+
* Call using:
34
* node _tools/fetch-sponsors.js
45
* @author Nicholas C. Zakas
56
*/
@@ -14,99 +15,187 @@
1415

1516
const fs = require("fs");
1617
const fetch = require("node-fetch");
18+
const { graphql: githubGraphQL } = require("@octokit/graphql");
1719

1820
//-----------------------------------------------------------------------------
1921
// Data
2022
//-----------------------------------------------------------------------------
2123

22-
// filename to output to
23-
const filename = "./_data/sponsors.json";
24+
// filename to output sponsors to
25+
const sponsorsFilename = "./_data/sponsors.json";
2426

2527
// simplified data structure
26-
const sponsors = {
28+
const tierSponsors = {
2729
gold: [],
2830
silver: [],
2931
bronze: [],
3032
backers: []
3133
};
3234

33-
const graphqlEndpoint = "https://api.opencollective.com/graphql/v2";
34-
35-
const graphqlQuery = `{
36-
account(slug: "eslint") {
37-
orders(status: ACTIVE) {
38-
totalCount
39-
nodes {
40-
fromAccount {
41-
name
42-
website
43-
imageUrl
44-
}
45-
amount {
46-
value
35+
const { ESLINT_GITHUB_TOKEN } = process.env;
36+
37+
if (!ESLINT_GITHUB_TOKEN) {
38+
throw new Error("Missing ESLINT_GITHUB_TOKEN.");
39+
}
40+
41+
//-----------------------------------------------------------------------------
42+
// Helpers
43+
//-----------------------------------------------------------------------------
44+
45+
/**
46+
* Returns the tier ID for a given donation amount.
47+
* @param {int} monthlyDonation The monthly donation in dollars.
48+
* @returns {string} The ID of the tier the donation belongs to.
49+
*/
50+
function getTierSlug(monthlyDonation) {
51+
if (monthlyDonation >= 1000) {
52+
return "gold-sponsor";
53+
}
54+
55+
if (monthlyDonation >= 500) {
56+
return "silver-sponsor";
57+
}
58+
59+
if (monthlyDonation >= 200) {
60+
return "bronze-sponsor";
61+
}
62+
63+
return "backer";
64+
}
65+
66+
/**
67+
* Fetch order data from Open Collective using the GraphQL API.
68+
* @returns {Array} An array of sponsors.
69+
*/
70+
async function fetchOpenCollectiveSponsors() {
71+
72+
const endpoint = "https://api.opencollective.com/graphql/v2";
73+
74+
const query = `{
75+
account(slug: "eslint") {
76+
orders(status: ACTIVE) {
77+
totalCount
78+
nodes {
79+
fromAccount {
80+
name
81+
website
82+
imageUrl
83+
}
84+
amount {
85+
value
86+
}
87+
tier {
88+
slug
89+
}
90+
frequency
91+
totalDonations {
92+
value
93+
}
94+
}
95+
}
4796
}
48-
tier {
49-
slug
97+
}`;
98+
99+
const result = await fetch(endpoint, {
100+
method: "POST",
101+
headers: { "Content-Type": "application/json" },
102+
body: JSON.stringify({ query })
103+
});
104+
105+
const payload = await result.json();
106+
107+
return payload.data.account.orders.nodes.map(order => ({
108+
name: order.fromAccount.name,
109+
url: order.fromAccount.website,
110+
image: order.fromAccount.imageUrl,
111+
monthlyDonation: order.frequency === "year" ? Math.round(order.amount.value * 100 / 12) : order.amount.value * 100,
112+
totalDonations: order.totalDonations.value * 100,
113+
source: "opencollective",
114+
tier: order.tier ? order.tier.slug : null
115+
}));
116+
117+
}
118+
119+
/**
120+
* Fetches GitHub Sponsors data using the GraphQL API.
121+
* @returns {Array} An array of sponsors.
122+
*/
123+
async function fetchGitHubSponsors() {
124+
125+
const { organization } = await githubGraphQL(`query {
126+
organization(login: "eslint") {
127+
sponsorshipsAsMaintainer (first: 100) {
128+
nodes {
129+
sponsor {
130+
name,
131+
login,
132+
avatarUrl,
133+
url,
134+
websiteUrl
135+
},
136+
tier {
137+
monthlyPriceInDollars
138+
}
139+
}
140+
}
50141
}
51-
frequency
52-
totalDonations {
53-
value
142+
}`, {
143+
headers: {
144+
authorization: `token ${ESLINT_GITHUB_TOKEN}`
54145
}
55-
}
56-
}
57-
}
58-
}`;
146+
});
147+
148+
// return an array in the same format as Open Collective
149+
return organization.sponsorshipsAsMaintainer.nodes.map(({ sponsor, tier }) => ({
150+
name: sponsor.name,
151+
image: sponsor.avatarUrl,
152+
url: sponsor.websiteUrl || sponsor.url,
153+
monthlyDonation: tier.monthlyPriceInDollars * 100,
154+
source: "github",
155+
tier: getTierSlug(tier.monthlyPriceInDollars)
156+
}));
157+
158+
}
59159

60160
//-----------------------------------------------------------------------------
61161
// Main
62162
//-----------------------------------------------------------------------------
63163

64164
(async() => {
65165

66-
// fetch the data
67-
const result = await fetch(graphqlEndpoint, {
68-
method: "POST",
69-
headers: { "Content-Type": "application/json" },
70-
body: JSON.stringify({ query: graphqlQuery })
71-
});
72-
const orders = await result.json().then(res => res.data.account.orders.nodes);
73-
74-
// process into a useful format
75-
for (const order of orders) {
166+
const [openCollectiveSponsors, githubSponsors] = await Promise.all([
167+
fetchOpenCollectiveSponsors(),
168+
fetchGitHubSponsors()
169+
]);
76170

77-
const sponsor = {
78-
name: order.fromAccount.name,
79-
url: order.fromAccount.website,
80-
image: order.fromAccount.imageUrl,
81-
monthlyDonation: order.frequency === "year" ? Math.round(order.amount.value * 100 / 12) : order.amount.value * 100,
82-
totalDonations: order.totalDonations.value * 100
83-
};
171+
const sponsors = openCollectiveSponsors.concat(githubSponsors);
84172

85-
const tierSlug = order.tier ? order.tier.slug : null;
173+
// process into a useful format
174+
for (const sponsor of sponsors) {
86175

87-
switch (tierSlug) {
176+
switch (sponsor.tier) {
88177
case "gold-sponsor":
89-
sponsors.gold.push(sponsor);
178+
tierSponsors.gold.push(sponsor);
90179
break;
91180

92181
case "silver-sponsor":
93-
sponsors.silver.push(sponsor);
182+
tierSponsors.silver.push(sponsor);
94183
break;
95184

96185
case "bronze-sponsor":
97-
sponsors.bronze.push(sponsor);
186+
tierSponsors.bronze.push(sponsor);
98187
break;
99188

100189
default:
101-
sponsors.backers.push(sponsor);
190+
tierSponsors.backers.push(sponsor);
102191

103192
}
104193
}
105194

106195
// sort order based on total donations
107-
for (const key of Object.keys(sponsors)) {
108-
sponsors[key].sort((a, b) => b.monthlyDonation - a.monthlyDonation);
196+
for (const key of Object.keys(tierSponsors)) {
197+
tierSponsors[key].sort((a, b) => b.monthlyDonation - a.monthlyDonation);
109198
}
110199

111-
fs.writeFileSync(filename, JSON.stringify(sponsors, null, " "), { encoding: "utf8" });
200+
fs.writeFileSync(sponsorsFilename, JSON.stringify(tierSponsors, null, " "), { encoding: "utf8" });
112201
})();

0 commit comments

Comments
 (0)