-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpatreon_api.py
180 lines (132 loc) · 5.54 KB
/
patreon_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import dataclasses
from typing import Literal, TypedDict
import aiohttp
from discord.abc import Snowflake
from patreon.utils import user_agent_string
from config import ConfigurationManager
_auth_headers = {
'Authorization': f"Bearer {ConfigurationManager().get_config()["patreon_access_token"]}",
'User-Agent': user_agent_string()
}
class PatreonAPIError(Exception):
pass
async def make_request(path: str, meth: str, params: dict = None, headers: dict = None,
data: dict = None, json: dict = None, **kwargs) -> dict:
async with aiohttp.ClientSession() as session:
if path.startswith('/'):
path = path[1:]
url = f"https://www.patreon.com/api/oauth2/v2/{path}"
if not headers:
headers = {}
headers.update(_auth_headers)
response = await session.request(
meth,
url,
params=params,
headers=headers,
data=data,
json=json,
**kwargs
)
if response.status != 200:
raise PatreonAPIError(f"Error making request to Patreon API: {response.status}")
return await response.json()
@dataclasses.dataclass
class CustomTier:
id: str
title: str
discord_role_ids: list[Snowflake]
@dataclasses.dataclass
class CustomPledge:
id: str
discord_id: Snowflake
email: str
patron_status: Literal['declined_patron'] | Literal['active_patron'] | Literal['former_patron'] | None
entitled_tiers: list[CustomTier]
class PatreonResponse(TypedDict):
data: list[dict]
included: list[dict]
async def get_all_pledge_data(campaign_id: str | int, per_page: int = 500) -> list[CustomPledge]:
""" Gets all pledge data from a Patreon campaign
:param campaign_id: the ID of the campaign
:param per_page: the number of pledges to get per page,
:return: a list of Custom
"""
if per_page > 500 or per_page < 1:
raise ValueError("per_page must be between 1 and 500") # according to the Patreon API docs
params = {
'page[count]': per_page,
'include': 'user,currently_entitled_tiers',
'fields[member]': 'patron_status,email',
'fields[tier]': 'title,discord_role_ids',
'fields[user]': 'social_connections',
}
# initial request
patreon_resp = await make_request(f"/campaigns/{campaign_id}/members", "GET", params=params)
pledge_data = await get_pledge_data_from_page(patreon_resp) # get all the pledges from the initial page
while next_page := patreon_resp.get("links", {}).get("next"):
patreon_resp = await make_request(next_page, "GET", params=params)
pledge_data.extend(await get_pledge_data_from_page(patreon_resp))
return pledge_data
async def get_pledge_data_from_page(resp_json: PatreonResponse) -> list[CustomPledge]:
""" Given one pagination page of Patreon pledges, return a list of CustomPledge objects
:param resp_json: The JSON response from the Patreon API
:return: A list of CustomPledge objects
"""
tier_id_to_discord_roles: dict[str, CustomTier] = {} # tier_id: CustomTier
patreon_id_to_discord_id: dict[str, Snowflake] = {} # patreon_id: discord_id
for inc_data in resp_json["included"]:
if inc_data["type"] == "tier":
tier_id = inc_data["id"]
tier_id_to_discord_roles[tier_id] = CustomTier(
id=tier_id,
title=inc_data.get("attributes", {}).get("title", ""),
discord_role_ids=inc_data.get("attributes", {}).get("discord_role_ids", [])
)
elif inc_data["type"] == "user":
patreon_id = inc_data["id"]
_social = inc_data['attributes'].get('social_connections')
if not _social:
continue
_disc = _social.get('discord', None)
if not _disc:
continue
if not (discord_id := _disc.get('user_id', None)):
continue
if discord_id:
patreon_id_to_discord_id[patreon_id] = discord_id
pledge_data: list[CustomPledge] = []
for usr_data in resp_json["data"]:
entitled_tiers: list[dict] = usr_data["relationships"]["currently_entitled_tiers"]["data"]
pledge = CustomPledge(
id=usr_data["id"],
discord_id=patreon_id_to_discord_id.get(usr_data["relationships"]["user"]["data"]["id"]),
email=usr_data["attributes"]["email"],
patron_status=usr_data["attributes"]["patron_status"],
entitled_tiers=[]
)
for tier in entitled_tiers:
tier_id = tier["id"]
tier_data = tier_id_to_discord_roles.get(tier_id)
if not tier_data:
continue
pledge.entitled_tiers.append(tier_data)
pledge_data.append(pledge)
return pledge_data
async def get_patreon_tiers(campaign_id: str | int) -> list[CustomTier]:
""" Fetches all the tier data for a given campaign
"""
params = {
"include": "tiers",
'fields[tier]': 'title,discord_role_ids',
}
patreon_resp: PatreonResponse = await make_request(f"/campaigns/{campaign_id}", "GET", params=params)
all_tiers: list[CustomTier] = []
for inc_data in patreon_resp["included"]:
if inc_data["type"] == "tier":
all_tiers.append(CustomTier(
id=inc_data["id"],
title=inc_data.get("attributes", {}).get("title", ""),
discord_role_ids=inc_data.get("attributes", {}).get("discord_role_ids", [])
))
return all_tiers