mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
add script to update patreons.json
This commit is contained in:
@@ -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
@@ -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}": [
|
||||
|
||||
Reference in New Issue
Block a user