add script to update patreons.json

This commit is contained in:
Zagrios
2025-08-31 20:54:57 +02:00
parent 9964940020
commit 390167f0b5
2 changed files with 172 additions and 1 deletions
+170
View File
@@ -0,0 +1,170 @@
import fetch from "node-fetch";
import fs from "fs";
import path from "path";
const API_BASE = "https://www.patreon.com/api/oauth2/v2";
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
function mapTierTitleToType(tierTitle) {
if (!tierTitle) return undefined;
const normalized = String(tierTitle).toLowerCase();
if (normalized.includes("diamond")) return "diamond";
if (normalized.includes("gold")) return "gold";
return undefined;
}
function extractPreferredLink(userAttributes) {
if (!userAttributes || !userAttributes.social_connections) return undefined;
const sc = userAttributes.social_connections;
// Prefer Twitch, then YouTube, then Twitter/X
const providers = ["twitch", "youtube", "twitter"];
for (const provider of providers) {
const entry = sc[provider];
if (entry && entry.url) return entry.url;
}
return undefined;
}
async function fetchAllMembers(accessToken, campaignId) {
let url =
`${API_BASE}/campaigns/${encodeURIComponent(
campaignId
)}/members` +
"?include=user,currently_entitled_tiers" +
"&fields[member]=patron_status,full_name,pledge_relationship_start,last_charge_date" +
"&fields[user]=full_name,vanity,url,social_connections" +
"&fields[tier]=title" +
"&page[count]=100";
const headers = {
Authorization: `Bearer ${accessToken}`,
"User-Agent": process.env.PATREON_USER_AGENT || "BSManager - Patreon Sync",
};
const allMembers = [];
const usersById = new Map();
const tiersById = new Map();
while (url) {
const res = await fetch(url, { headers });
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed to fetch members (${res.status}): ${text}`);
}
const json = await res.json();
if (Array.isArray(json.included)) {
for (const inc of json.included) {
if (inc.type === "user") {
usersById.set(inc.id, inc);
} else if (inc.type === "tier") {
tiersById.set(inc.id, inc);
}
}
}
if (Array.isArray(json.data)) {
allMembers.push(...json.data);
}
url = json.links && json.links.next ? json.links.next : undefined;
}
return { members: allMembers, usersById, tiersById };
}
function buildPatreonsList({ members, usersById, tiersById, existingLinkByUsername }) {
const uniqueByUsername = new Map();
for (const member of members) {
const attrs = member.attributes || {};
if (attrs.patron_status !== "active_patron") continue;
const userRel = member.relationships && member.relationships.user && member.relationships.user.data;
const user = userRel ? usersById.get(userRel.id) : undefined;
const userAttrs = (user && user.attributes) || {};
const tiersRel =
member.relationships &&
member.relationships.currently_entitled_tiers &&
member.relationships.currently_entitled_tiers.data;
let type;
if (Array.isArray(tiersRel) && tiersRel.length > 0) {
// Use first tier title match to determine type
const tier = tiersById.get(tiersRel[0].id);
type = mapTierTitleToType(tier && tier.attributes && tier.attributes.title);
}
const username = (userAttrs.vanity || userAttrs.full_name || user?.id || "[ ]").trim();
const link = type === "diamond" ? extractPreferredLink(userAttrs) : undefined;
// Determine first payment/relationship start date (fallback to last_charge_date)
const firstDateStr = attrs.pledge_relationship_start || attrs.last_charge_date || null;
let ts = Number.MAX_SAFE_INTEGER;
if (firstDateStr) {
const parsed = Date.parse(firstDateStr);
ts = Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
}
const entry = { username };
if (type) entry.type = type;
// Preserve link from existing JSON if present; otherwise use new link
const preservedLink = existingLinkByUsername && existingLinkByUsername.get(username);
if (preservedLink) entry.link = preservedLink;
else if (link) entry.link = link;
// Ensure uniqueness by username
if (!uniqueByUsername.has(username)) {
uniqueByUsername.set(username, { entry, ts });
}
}
return Array.from(uniqueByUsername.values())
.sort((a, b) => a.ts - b.ts)
.map((x) => x.entry);
}
async function main() {
const ACCESS_TOKEN = requireEnv("PATREON_ACCESS_TOKEN");
const CAMPAIGN_ID = requireEnv("PATREON_CAMPAIGN_ID");
const jsonPath = path.resolve(process.cwd(), "assets", "jsons", "patreons.json");
let existing = [];
try {
if (fs.existsSync(jsonPath)) {
const raw = fs.readFileSync(jsonPath, "utf8");
existing = JSON.parse(raw);
}
} catch (_) {
existing = [];
}
const existingLinkByUsername = new Map();
if (Array.isArray(existing)) {
for (const e of existing) {
if (e && e.username && e.link) existingLinkByUsername.set(e.username, e.link);
}
}
const { members, usersById, tiersById } = await fetchAllMembers(ACCESS_TOKEN, CAMPAIGN_ID);
const patreons = buildPatreonsList({ members, usersById, tiersById, existingLinkByUsername });
const output = `${JSON.stringify(patreons, null, "\t")}\n`;
fs.writeFileSync(jsonPath, output, "utf8");
// eslint-disable-next-line no-console
console.log(`Updated ${jsonPath} with ${patreons.length} active patrons.`);
}
main().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
process.exitCode = 1;
});
+2 -1
View File
@@ -22,7 +22,8 @@
"test:unit": "jest ./src/__tests__/unit",
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64",
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak"
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak",
"update:patreon": "ts-node ./.erb/scripts/update-patreon.js"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [