fix: normalize recent Patreon support
This commit is contained in:
parent
1955e022bf
commit
63c8245f98
4 changed files with 235 additions and 69 deletions
141
.github/workflows/pages.yml
vendored
141
.github/workflows/pages.yml
vendored
|
|
@ -69,38 +69,115 @@ jobs:
|
|||
env:
|
||||
OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$OCS_PATREON_TOKEN" ]; then
|
||||
echo '[]' > dist/supporters.json; exit 0
|
||||
fi
|
||||
CID=$(curl -sf -H "Authorization: Bearer $OCS_PATREON_TOKEN" \
|
||||
"https://www.patreon.com/api/oauth2/v2/campaigns" \
|
||||
| jq -r '.data[0].id // empty' || true)
|
||||
if [ -z "$CID" ]; then
|
||||
echo '[]' > dist/supporters.json; exit 0
|
||||
fi
|
||||
curl -sf -H "Authorization: Bearer $OCS_PATREON_TOKEN" \
|
||||
"https://www.patreon.com/api/oauth2/v2/campaigns/$CID/members?include=pledge_history&fields%5Bmember%5D=full_name,patron_status,currently_entitled_amount_cents&fields%5Bpledge-event%5D=amount_cents,date,payment_status&page%5Bcount%5D=200" \
|
||||
| jq -c '
|
||||
(.included // []
|
||||
| map(select(.type == "pledge-event"))
|
||||
| map({key: .id, value: .attributes})
|
||||
| from_entries) as $events
|
||||
| [.data[]
|
||||
| select(.attributes.patron_status == "active_patron")
|
||||
| . as $member
|
||||
| ([($member.relationships.pledge_history.data // [])[]
|
||||
| $events[.id]
|
||||
| select(.payment_status == "Paid")
|
||||
| select(.amount_cents > 0)]
|
||||
| sort_by(.date)
|
||||
| last) as $last_paid
|
||||
| {name: ((.attributes.full_name // "") | gsub("^\\s+|\\s+$"; "")),
|
||||
cents: ($last_paid.amount_cents // .attributes.currently_entitled_amount_cents)}
|
||||
| select(.name != "")
|
||||
| select(.cents > 0)]
|
||||
| sort_by(-.cents)' \
|
||||
> dist/supporters.json || echo '[]' > dist/supporters.json
|
||||
echo "supporters: $(jq 'length' dist/supporters.json)"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from pathlib import Path
|
||||
|
||||
output = Path("dist/supporters.json")
|
||||
token = os.environ.get("OCS_PATREON_TOKEN", "")
|
||||
if not token:
|
||||
output.write_text("[]\n", encoding="utf-8")
|
||||
raise SystemExit(0)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"User-Agent": "OpenCADStudio-supporters",
|
||||
}
|
||||
|
||||
def fetch_json(url, authenticated=True):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
headers=headers if authenticated else {"User-Agent": headers["User-Agent"]},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
return json.load(response)
|
||||
|
||||
campaigns = fetch_json("https://www.patreon.com/api/oauth2/v2/campaigns")
|
||||
campaign_id = campaigns["data"][0]["id"]
|
||||
params = urllib.parse.urlencode({
|
||||
"include": "pledge_history",
|
||||
"fields[member]": "full_name",
|
||||
"fields[pledge-event]": "amount_cents,currency_code,date,payment_status",
|
||||
"page[count]": "200",
|
||||
})
|
||||
url = (
|
||||
f"https://www.patreon.com/api/oauth2/v2/campaigns/"
|
||||
f"{campaign_id}/members?{params}"
|
||||
)
|
||||
|
||||
cutoff_date = (datetime.now(timezone.utc) - timedelta(days=31)).date()
|
||||
payments = []
|
||||
for _ in range(50):
|
||||
page = fetch_json(url)
|
||||
included = {
|
||||
(item.get("type"), item.get("id")): item.get("attributes", {})
|
||||
for item in page.get("included", [])
|
||||
if item.get("type") == "pledge-event"
|
||||
}
|
||||
for member in page.get("data", []):
|
||||
latest = None
|
||||
history = (
|
||||
member.get("relationships", {})
|
||||
.get("pledge_history", {})
|
||||
.get("data", [])
|
||||
)
|
||||
for relationship in history:
|
||||
event = included.get((relationship.get("type"), relationship.get("id")))
|
||||
if not event or event.get("payment_status") != "Paid":
|
||||
continue
|
||||
cents = int(event.get("amount_cents") or 0)
|
||||
currency = str(event.get("currency_code") or "").strip().upper()
|
||||
date_text = str(event.get("date") or "")
|
||||
try:
|
||||
paid_at = datetime.fromisoformat(date_text.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
continue
|
||||
if cents <= 0 or not currency or paid_at.date() < cutoff_date:
|
||||
continue
|
||||
if latest is None or paid_at > latest[0]:
|
||||
latest = (paid_at, cents, currency)
|
||||
name = str(member.get("attributes", {}).get("full_name") or "").strip()
|
||||
if latest is not None and name:
|
||||
payments.append((name, latest[1], latest[2]))
|
||||
url = page.get("links", {}).get("next")
|
||||
if not url:
|
||||
break
|
||||
|
||||
rates = {"USD": Decimal("1")}
|
||||
if any(currency != "USD" for _, _, currency in payments):
|
||||
rate_entries = fetch_json(
|
||||
"https://api.frankfurter.dev/v2/rates?base=USD",
|
||||
authenticated=False,
|
||||
)
|
||||
rates.update({
|
||||
str(entry["quote"]).upper(): Decimal(str(entry["rate"]))
|
||||
for entry in rate_entries
|
||||
if Decimal(str(entry.get("rate", 0))) > 0
|
||||
})
|
||||
|
||||
supporters = []
|
||||
for name, cents, currency in payments:
|
||||
rate = rates.get(currency)
|
||||
if rate is None:
|
||||
continue
|
||||
usd_cents = int(
|
||||
(Decimal(cents) / rate).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||||
)
|
||||
if usd_cents > 0:
|
||||
supporters.append({"name": name, "cents": usd_cents})
|
||||
|
||||
supporters.sort(key=lambda item: (-item["cents"], item["name"]))
|
||||
output.write_text(
|
||||
json.dumps(supporters, ensure_ascii=False, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"supporters: {len(supporters)}")
|
||||
PY
|
||||
|
||||
# Browsers cannot reliably fetch YouTube playlist/oEmbed responses because
|
||||
# of CORS. Build a same-origin listing and thumbnail directory for the
|
||||
|
|
|
|||
Loading…
Reference in a new issue