1+ #! /bin/bash
2+ # filepath: fetch_patreon_members.sh
3+
4+ set -e
5+
6+ # Check for required environment variable
7+ if [ -z " $PATREON_ACCESS_TOKEN " ]; then
8+ echo " Error: PATREON_ACCESS_TOKEN environment variable is not set"
9+ echo " Usage: export PATREON_ACCESS_TOKEN='your_token_here' && ./fetch_patreon_members.sh"
10+ exit 1
11+ fi
12+
13+ # Get campaign ID first (you'll need to get your campaign ID)
14+ # If you know your campaign ID, you can hardcode it here
15+ CAMPAIGN_ID=" ${PATREON_CAMPAIGN_ID:- } "
16+
17+ if [ -z " $CAMPAIGN_ID " ]; then
18+ echo " Fetching campaign ID..."
19+ CAMPAIGN_RESPONSE=$( curl -s -X GET \
20+ " https://www.patreon.com/api/oauth2/v2/campaigns" \
21+ -H " Authorization: Bearer $PATREON_ACCESS_TOKEN " \
22+ -H " Content-Type: application/json" )
23+
24+ CAMPAIGN_ID=$( echo " $CAMPAIGN_RESPONSE " | jq -r ' .data[0].id' )
25+
26+ if [ -z " $CAMPAIGN_ID " ] || [ " $CAMPAIGN_ID " = " null" ]; then
27+ echo " Error: Could not fetch campaign ID"
28+ echo " Response: $CAMPAIGN_RESPONSE "
29+ exit 1
30+ fi
31+
32+ echo " Campaign ID: $CAMPAIGN_ID "
33+ fi
34+
35+ # Fetch members with pagination support
36+ OUTPUT_FILE=" website/_data/patreons.json"
37+ TEMP_FILE=$( mktemp)
38+ ALL_MEMBERS=" []"
39+ NEXT_CURSOR=" "
40+
41+ echo " Fetching Patreon members..."
42+
43+ while true ; do
44+ # Build the URL with cursor for pagination
45+ if [ -z " $NEXT_CURSOR " ]; then
46+ URL=" https://www.patreon.com/api/oauth2/v2/campaigns/${CAMPAIGN_ID} /members?include=currently_entitled_tiers,user&fields%5Bmember%5D=full_name,patron_status&fields%5Btier%5D=title&fields%5Buser%5D=full_name,url"
47+ else
48+ URL=" https://www.patreon.com/api/oauth2/v2/campaigns/${CAMPAIGN_ID} /members?include=currently_entitled_tiers,user&fields%5Bmember%5D=full_name,patron_status&fields%5Btier%5D=title&fields%5Buser%5D=full_name,url&page%5Bcursor%5D=${NEXT_CURSOR} "
49+ fi
50+
51+ RESPONSE=$( curl -s -X GET " $URL " \
52+ -H " Authorization: Bearer $PATREON_ACCESS_TOKEN " \
53+ -H " Content-Type: application/json" )
54+
55+ # Check for errors
56+ if echo " $RESPONSE " | jq -e ' .errors' > /dev/null 2>&1 ; then
57+ echo " Error from API:"
58+ echo " $RESPONSE " | jq ' .errors'
59+ exit 1
60+ fi
61+
62+ # Process the response
63+ echo " $RESPONSE " > " $TEMP_FILE "
64+
65+ # Extract members from this page
66+ PAGE_MEMBERS=$( jq ' [
67+ .data[] |
68+ . as $member |
69+ {
70+ member_id: .id,
71+ name: (.attributes.full_name // ""),
72+ tier_id: (.relationships.currently_entitled_tiers.data[0].id // ""),
73+ patron_status: .attributes.patron_status
74+ }
75+ ]' " $TEMP_FILE " )
76+
77+ # Extract included data (tiers and users)
78+ INCLUDED=$( jq ' .included // []' " $TEMP_FILE " )
79+
80+ # Simplified merge - include all patrons with active status, excluding free tier
81+ PAGE_RESULT=$( jq -n \
82+ --argjson members " $PAGE_MEMBERS " \
83+ --argjson included " $INCLUDED " \
84+ ' $members | map(
85+ . as $m |
86+ ($included[] | select(.type == "tier" and .id == $m.tier_id)) as $tier |
87+ {
88+ name: $m.name,
89+ tier: ($tier.attributes.title // "Unknown"),
90+ active: ($m.patron_status == "active_patron")
91+ }
92+ ) | map(select(.tier != "Unknown" and .tier != "" and .tier != "Free")) | sort_by(.tier) | reverse' )
93+
94+ # Merge with all members
95+ ALL_MEMBERS=$( jq -n \
96+ --argjson all " $ALL_MEMBERS " \
97+ --argjson page " $PAGE_RESULT " \
98+ ' $all + $page' )
99+
100+ echo " Fetched $( echo " $PAGE_RESULT " | jq ' length' ) members..."
101+
102+ # Check for next page
103+ NEXT_CURSOR=$( jq -r ' .meta.pagination.cursors.next // ""' " $TEMP_FILE " )
104+
105+ if [ -z " $NEXT_CURSOR " ] || [ " $NEXT_CURSOR " = " null" ]; then
106+ break
107+ fi
108+ done
109+
110+ # Merge with existing data to preserve custom fields and historical data
111+ if [ -f " $OUTPUT_FILE " ]; then
112+ echo " Merging with existing data to preserve custom fields and historical patrons..."
113+ EXISTING_DATA=$( cat " $OUTPUT_FILE " )
114+
115+ # Merge strategy:
116+ # 1. Update existing members with new data from API (name, tier, active status)
117+ # 2. Keep old members that aren't in the new data (they've left but we keep them for history)
118+ # 3. Add new members from API
119+ # 4. Preserve custom fields like 'url' from existing data
120+ MERGED_DATA=$( jq -n \
121+ --argjson new " $ALL_MEMBERS " \
122+ --argjson old " $EXISTING_DATA " \
123+ '
124+ # Create lookup maps
125+ ($new | map({(.name): .}) | add) as $newMap |
126+ ($old | map({(.name): .}) | add) as $oldMap |
127+
128+ # Get all unique names from both old and new
129+ (($new | map(.name)) + ($old | map(.name)) | unique) as $allNames |
130+
131+ # For each name, merge appropriately
132+ $allNames | map(
133+ . as $name |
134+ if $newMap[$name] and $oldMap[$name] then
135+ # Member exists in both - merge, keeping custom fields from old but updating tier/active from new
136+ $oldMap[$name] + $newMap[$name]
137+ elif $newMap[$name] then
138+ # New member - use from API
139+ $newMap[$name]
140+ else
141+ # Old member not in new data - keep as is (they left but we preserve history)
142+ $oldMap[$name]
143+ end
144+ )
145+ ' )
146+
147+ echo " $MERGED_DATA " | jq ' .' > " $OUTPUT_FILE "
148+ else
149+ # No existing file, just save new data
150+ echo " $ALL_MEMBERS " | jq ' .' > " $OUTPUT_FILE "
151+ fi
152+
153+ echo " Successfully saved $( jq ' length' " $OUTPUT_FILE " ) patrons to $OUTPUT_FILE "
154+
155+ # Clean up
156+ rm -f " $TEMP_FILE "
0 commit comments