obs-StreamFX/tools/generate-contributors.js
Michael Fabian 'Xaymar' Dirks e3302fa163 ci, tools: Automatically generate contributor attribution files
Also includes a tool to convert Patreon Membership .csv files into a support patch set.
2023-04-05 18:51:50 +02:00

329 lines
7.8 KiB
JavaScript

const HTTPS = require("https");
const PATH = require("path");
const FS = require("fs/promises");
const CHILD_PROCESS = require("child_process");
function query_git() {
// git shortlog -sn --all | sed -E "s/^\W+[0-9]+\W+//"
return (async () => {
let users = new Map();
let git = new Promise((resolve, reject) => {
try {
const child = CHILD_PROCESS.spawn('git', ['shortlog', '-sn', '--all']);
let sout = "";
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
sout += data;
});
let serr = "";
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
serr += data;
});
child.on('close', (code) => {
if (code != 0) {
reject([code, serr]);
} else {
resolve(sout);
}
});
} catch (e) {
reject(e);
}
});
let result = await git;
let lines = result.matchAll(/^[\s]+([0-9]+)[\s]+(.+)$/gim);
for (let line of lines) {
let name = line[2];
if (!users.has(name)) {
users.set(name, `https://github.com/${process.env.GITHUB_REPOSITORY}/graphs/contributors`)
}
}
return users;
})();
}
function query_crowdin(project_id, project_auth_key) {
const limit = 100;
function _query_page(page) {
return new Promise((resolve, reject) => {
try {
let query = {
protocol: "https:",
host: "crowdin.com",
path: `/api/v2/projects/${project_id}/members?limit=${limit}&offset=${page * limit}`,
method: "GET",
headers: {
"accept": "application/json",
"authorization": `Bearer ${project_auth_key}`
}
};
let req = HTTPS.request(query, (res) => {
let data = "";
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
res.data = data;
resolve(res);
});
});
req.end();
} catch (e) {
reject(e);
}
});
}
return (async () => {
let page_is_last = false;
let page = 0;
let users = new Map();
while (!page_is_last) {
let res = await _query_page(page++);
if (res.statusCode == 200) {
let json = JSON.parse(res.data);
let count = json.data.length;
for (let user of json.data) {
// Don't credit blocked users for work not done.
if (user.data.role == 'blocked') {
continue;
}
let key = (user.data.fullName !== null) && (user.data.fullName !== undefined) && (user.data.fullName.length > 0) ? user.data.fullName : user.data.username;
users.set(key, `https://crowdin.com/profile/${user.data.username}`);
}
page_is_last = (count < limit);
} else {
throw new Error(JSON.parse(res.data));
}
}
return users;
})();
}
function query_github_sponsors(auth_token) {
const HTTPS = require("https");
let build_query = function (cursor) {
if (cursor === undefined) {
return `query {
viewer {
sponsors(after: null, first: 0) {
totalCount
}
}
}`;
} else {
return `query {
viewer {
sponsors(after: ${typeof (cursor) == "string" ? `"${cursor}"` : "null"}, first: 100) {
nodes {
__typename
... on User {
resourcePath
login
name
}
... on Organization {
resourcePath
login
name
}
}
pageInfo {
endCursor
startCursor
}
}
}
}`;
}
}
let do_query = function (schema) {
return new Promise((resolve, reject) => {
let text_query = JSON.stringify({ query: schema });
let request = HTTPS.request(
"https://api.github.com/graphql",
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `bearer ${auth_token}`,
'User-Agent': 'Node.JS'
},
}, (res) => {
let data = "";
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('error', () => {
reject();
});
res.on('end', () => {
res.data = JSON.parse(data);
resolve(res);
});
});
request.write(text_query);
request.end();
})
}
return (async () => {
let users = new Map();
let total = 0;
// Find the total amount.
{
let query = build_query();
let response = await do_query(query);
let data = response.data.data;
total = data.viewer.sponsors.totalCount;
}
// Index by page, in 100 user increments.
let cursor = null;
for (let idx = 0, edx = total; idx < edx; idx += 100) {
let query = build_query(cursor);
let response = await do_query(query);
let data = response.data.data;
// Insert data info Map.
for (let entry of data.viewer.sponsors.nodes) {
let name = entry.login;
if (typeof (entry.name) === "string") {
name = entry.name;
}
users.set(name, `https://github.com${entry.resourcePath}`);
}
// Update cursor for further queries.
cursor = data.viewer.sponsors.pageInfo.endCursor;
}
return users;
})();
}
(async () => {
let json = {};
let markdown = `# StreamFX Contributors & Supporters
## Contributors
`;
// Contributors
{// - Git
markdown += `### Code, Media
Thanks go to the following people, who have either wrangled with code or wrangled with image editors while saving often in the hopes of not losing any changes:
`;
json.contributor = {};
let info = await query_git();
let extra = JSON.parse(await FS.readFile(PATH.join(__dirname, "patch-contributors-git.json")));
for (let key in extra) {
info.set(key, extra[key]);
}
for (let key of Array.from(info.keys()).sort()) {
let value = info.get(key);
json.contributor[key] = value;
markdown += `* [${key}](${value})\n`;
}
markdown += "\n";
}
{// - Crowdin
markdown += `### Translators
[StreamFX relies on crowd-sourced translations to be available in your language.](https://github.com/Xaymar/obs-StreamFX/issues/36) Much thanks go out to all volunteer translators who have taken some time to submit translations on Crowdin.
`;
json.translator = {};
let info = await query_crowdin(process.env.CROWDIN_PROJECTID, process.env.CROWDIN_TOKEN);
let extra = JSON.parse(await FS.readFile(PATH.join(__dirname, "patch-contributors-git.json")));
for (let key in extra) {
info.set(key, extra[key]);
}
for (let key of Array.from(info.keys()).sort()) {
let value = info.get(key);
json.translator[key] = value;
markdown += `* [${key}](${value})\n`;
}
markdown += "\n";
}
// Supporters
markdown += `## Supporters
The StreamFX project relies on generous donations from you through [Patreon](https://patreon.com/xaymar), [GitHub](https://github.com/sponsors/xaymar) or [PayPal](https://paypal.me/xaymar). Huge thanks go out to the following people for supporting StreamFX:
`;
json.supporter = {}
{// - GitHub
markdown += `### GitHub Sponsors
`;
json.supporter.github = {};
let info = await query_github_sponsors(process.env.GITHUB_TOKEN);
let extra = JSON.parse(await FS.readFile(PATH.join(__dirname, "patch-supporters-github.json")));
for (let key in extra) {
info.set(key, extra[key]);
}
for (let key of Array.from(info.keys()).sort()) {
let value = info.get(key);
json.supporter.github[key] = value;
markdown += `* [${key}](${value})\n`;
}
markdown += "\n";
}
{// - Patreon
// TODO: How do we OAuth without OAuth?!
markdown += `### Patreon Patrons
`;
json.supporter.patreon = {};
let info = new Map();
let extra = JSON.parse(await FS.readFile(PATH.join(__dirname, "patch-supporters-patreon.json")));
for (let key in extra) {
info.set(key, extra[key]);
}
for (let key of Array.from(info.keys()).sort()) {
let value = info.get(key);
json.supporter.patreon[key] = value;
markdown += `* [${key}](${value})\n`;
}
markdown += "\n";
}
// Write Markdown file
FS.writeFile(process.argv[2],
markdown,
{
encoding: 'utf8'
}
)
// Write JSON file
FS.writeFile(process.argv[3],
JSON.stringify(json, undefined, '\t'),
{
encoding: 'utf8'
}
)
})();