Skip to content

Commit e3235b8

Browse files
authored
Pull in the user profile/settings/default publication/published posts (#23)
* Update README.md Adds details on how to find your user_id and how to set a password if you have an account that doesn't have one * Added in code to both find the user_id and to find the primary publication / get a list of all accessible publications / user settings / user profile. * Fixing Indent issue and changing primary pub return format. * SImpler / more complete response format * Add support for custom URL's in publication_url building * Default to users default publication. Support custom_domain sign-in (requires one extra step after login to get cookies) * Add getDraft end point call so you can load existing drafts * Update readme to use get_user_id instead of env, and to explain how to switch publications * adding in publication_url * Removing extra space
1 parent ce10185 commit e3235b8

File tree

3 files changed

+143
-14
lines changed

3 files changed

+143
-14
lines changed

README.md

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ You can install python-substack using:
2020

2121
Set the following environment variables by creating a **.env** file:
2222

23-
PUBLICATION_URL=https://ma2za.substack.com
2423
EMAIL=
2524
PASSWORD=
26-
USER_ID=
2725

28-
To discover the USER_ID go to your public profile page,
29-
in the URL bar of the browser you find the substack address
30-
followed by your USER_ID and your username:
31-
https://substack.com/profile/[USER_ID]-[username]
26+
## If you don't have a password
27+
Recently Substack has been setting up new accounts without a password. If you sign-out and sign back in it just uses your email address with a "magic" link.
28+
29+
Set a password:
30+
- Sign-out of Substack
31+
- At the sign-in page click, "Sign in with password" under the `Email` text box
32+
- Then choose, "Set a new password"
3233

3334
The .env file will be ignored by git but always be careful.
3435

@@ -47,13 +48,24 @@ from substack.post import Post
4748
api = Api(
4849
email=os.getenv("EMAIL"),
4950
password=os.getenv("PASSWORD"),
50-
publication_url=os.getenv("PUBLICATION_URL"),
5151
)
5252

53+
user_id = api.get_user_id()
54+
55+
# Switch Publications - The library defaults to your users primary publication. You can retrieve all your publications and change which one you want to use.
56+
57+
# primary publication
58+
user_publication = api.get_user_primary_publication()
59+
# all publications
60+
user_publications = api.get_user_publications()
61+
62+
# This step is only necessary if you are not using your primary publication
63+
# api.change_publication(user_publication)
64+
5365
post = Post(
5466
title="How to publish a Substack post using the Python API",
5567
subtitle="This post was published using the Python API",
56-
user_id=os.getenv("USER_ID")
68+
user_id=user_id
5769
)
5870

5971
post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'})

substack/api.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def __init__(
4242
Defaults to https://substack.com/api/v1.
4343
"""
4444
self.base_url = base_url or "https://substack.com/api/v1"
45-
self.publication_url = urljoin(publication_url, "api/v1")
4645

4746
if debug:
4847
logging.basicConfig()
@@ -53,6 +52,28 @@ def __init__(
5352
if email is not None and password is not None:
5453
self.login(email, password)
5554

55+
# if the user provided a publication url, then use that
56+
if publication_url:
57+
import re
58+
59+
# Regular expression to extract subdomain name
60+
match = re.search(r"https://(.*).substack.com", publication_url.lower())
61+
subdomain = match.group(1) if match else None
62+
63+
user_publications = self.get_user_publications()
64+
# search through publications to find the publication with the matching subdomain
65+
for publication in user_publications:
66+
if publication['subdomain'] == subdomain:
67+
# set the current publication to the users publication
68+
user_publication = publication
69+
break
70+
else:
71+
# get the users primary publication
72+
user_publication = self.get_user_primary_publication()
73+
74+
# set the current publication to the users primary publication
75+
self.change_publication(user_publication)
76+
5677
def login(self, email, password) -> dict:
5778
"""
5879
@@ -73,7 +94,25 @@ def login(self, email, password) -> dict:
7394
"redirect": "/",
7495
},
7596
)
97+
7698
return Api._handle_response(response=response)
99+
100+
def signin_for_pub(self, publication):
101+
"""
102+
Complete the signin process
103+
"""
104+
response = self._session.get(
105+
f"https://substack.com/sign-in?redirect=%2F&for_pub={publication['subdomain']}",
106+
)
107+
108+
def change_publication(self, publication):
109+
"""
110+
Change the publication URL
111+
"""
112+
self.publication_url = urljoin(publication['publication_url'], "api/v1")
113+
114+
# sign-in to the publication
115+
self.signin_for_pub(publication)
77116

78117
@staticmethod
79118
def _handle_response(response: requests.Response):
@@ -92,6 +131,70 @@ def _handle_response(response: requests.Response):
92131
except ValueError:
93132
raise SubstackRequestException("Invalid Response: %s" % response.text)
94133

134+
def get_user_id(self):
135+
profile = self.get_user_profile()
136+
user_id = profile['id']
137+
138+
return user_id
139+
140+
def get_publication_url(self, publication):
141+
"""
142+
Gets the publication url
143+
"""
144+
custom_domain = publication['custom_domain']
145+
if not custom_domain:
146+
publication_url = f"https://{publication['subdomain']}.substack.com"
147+
else:
148+
publication_url = f"https://{custom_domain}"
149+
150+
return publication_url
151+
152+
def get_user_primary_publication(self):
153+
"""
154+
Gets the users primary publication
155+
"""
156+
157+
profile = self.get_user_profile()
158+
primary_publication = profile['primaryPublication']
159+
primary_publication['publication_url'] = self.get_publication_url(primary_publication)
160+
161+
return primary_publication
162+
163+
def get_user_publications(self):
164+
"""
165+
Gets the users publications
166+
"""
167+
168+
profile = self.get_user_profile()
169+
170+
# Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id"
171+
user_publications = []
172+
for publication in profile['publicationUsers']:
173+
pub = publication['publication']
174+
pub['publication_url'] = self.get_publication_url(pub)
175+
user_publications.append(pub)
176+
177+
return user_publications
178+
179+
def get_user_profile(self):
180+
"""
181+
Gets the users profile
182+
"""
183+
response = self._session.get(f"{self.base_url}/user/profile/self")
184+
185+
return Api._handle_response(response=response)
186+
187+
def get_user_settings(self):
188+
"""
189+
Get list of users.
190+
191+
Returns:
192+
193+
"""
194+
response = self._session.get(f"{self.base_url}/settings")
195+
196+
return Api._handle_response(response=response)
197+
95198
def get_publication_users(self):
96199
"""
97200
Get list of users.
@@ -115,6 +218,17 @@ def get_publication_subscriber_count(self):
115218

116219
return Api._handle_response(response=response)['subscriberCount']
117220

221+
def get_published_posts(self, offset=0, limit=25, order_by="post_date", order_direction="desc"):
222+
"""
223+
Get list of published posts for the publication.
224+
"""
225+
response = self._session.get(
226+
f"{self.publication_url}/post_management/published",
227+
params={"offset": offset, "limit": limit, "order_by": order_by, "order_direction": order_direction},
228+
)
229+
230+
return Api._handle_response(response=response)
231+
118232
def get_posts(self) -> dict:
119233
"""
120234
@@ -142,6 +256,14 @@ def get_drafts(self, filter=None, offset=None, limit=None):
142256
)
143257
return Api._handle_response(response=response)
144258

259+
def get_draft(self, draft_id):
260+
"""
261+
Gets a draft given it's id.
262+
263+
"""
264+
response = self._session.get(f"{self.publication_url}/drafts/{draft_id}")
265+
return Api._handle_response(response=response)
266+
145267
def delete_draft(self, draft_id):
146268
"""
147269

tests/substack/test_api.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ def test_login(self):
1818
api = Api(
1919
email=os.getenv("EMAIL"),
2020
password=os.getenv("PASSWORD"),
21-
publication_url=os.getenv("PUBLICATION_URL"),
2221
)
2322
self.assertIsNotNone(api)
2423

@@ -31,7 +30,6 @@ def test_get_drafts(self):
3130
api = Api(
3231
email=os.getenv("EMAIL"),
3332
password=os.getenv("PASSWORD"),
34-
publication_url=os.getenv("PUBLICATION_URL"),
3533
)
3634
drafts = api.get_drafts()
3735
self.assertIsNotNone(drafts)
@@ -40,7 +38,6 @@ def test_post_draft(self):
4038
api = Api(
4139
email=os.getenv("EMAIL"),
4240
password=os.getenv("PASSWORD"),
43-
publication_url=os.getenv("PUBLICATION_URL"),
4441
)
4542
posted_draft = api.post_draft([{"id": os.getenv("USER_ID"), "is_guest": False}])
4643
self.assertIsNotNone(posted_draft)
@@ -49,7 +46,6 @@ def test_publication_users(self):
4946
api = Api(
5047
email=os.getenv("EMAIL"),
5148
password=os.getenv("PASSWORD"),
52-
publication_url=os.getenv("PUBLICATION_URL"),
5349
)
5450
users = api.get_publication_users()
5551
self.assertIsNotNone(users)
@@ -58,7 +54,6 @@ def test_put_draft(self):
5854
api = Api(
5955
email=os.getenv("EMAIL"),
6056
password=os.getenv("PASSWORD"),
61-
publication_url=os.getenv("PUBLICATION_URL"),
6257
)
6358
posted_draft = api.put_draft("")
6459
self.assertIsNotNone(posted_draft)

0 commit comments

Comments
 (0)