From a069b710a292271a16b1f8de9cef9c46d7e3d749 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 12:18:45 -0700 Subject: [PATCH 01/10] 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 --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 01531c7..66b4ce2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,22 @@ in the URL bar of the browser you find the substack address followed by your USER_ID and your username: https://substack.com/profile/[USER_ID]-[username] +## If you don't have a password +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. + +Set a password: + - Sign-out of Substack + - At the sign-in page click, "Sign in with password" under the `Email` text box + - Then choose, "Set a new password" + +## If you can't find your USER_ID +If you do not see your USER_ID in the url you can find it through your browsers Developer Tools. + - In Substack open your Home, Posts, or Draft page + - Open Developer Tools (F12 on Windows, Option + ⌘ + J on Mac). + - Go to the Console tab + - Paste this command and hit enter: `window._preloads.user.id` + - Copy the result into your `.env` file as the USER_ID + The .env file will be ignored by git but always be careful. --- From f0e4c1d3c291157209bc87e3f51fdcd0cc7e444d Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 20:55:46 +0000 Subject: [PATCH 02/10] 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. --- README.md | 8 ------- substack/api.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 66b4ce2..0c432e3 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,6 @@ Set a password: - At the sign-in page click, "Sign in with password" under the `Email` text box - Then choose, "Set a new password" -## If you can't find your USER_ID -If you do not see your USER_ID in the url you can find it through your browsers Developer Tools. - - In Substack open your Home, Posts, or Draft page - - Open Developer Tools (F12 on Windows, Option + ⌘ + J on Mac). - - Go to the Console tab - - Paste this command and hit enter: `window._preloads.user.id` - - Copy the result into your `.env` file as the USER_ID - The .env file will be ignored by git but always be careful. --- diff --git a/substack/api.py b/substack/api.py index 44fc707..4dcd11a 100644 --- a/substack/api.py +++ b/substack/api.py @@ -42,7 +42,8 @@ def __init__( Defaults to https://substack.com/api/v1. """ self.base_url = base_url or "https://substack.com/api/v1" - self.publication_url = urljoin(publication_url, "api/v1") + if publication_url: + self.change_publication(publication_url) if debug: logging.basicConfig() @@ -74,6 +75,12 @@ def login(self, email, password) -> dict: }, ) return Api._handle_response(response=response) + + def change_publication(self, publication_url): + """ + Change the publication URL + """ + self.publication_url = urljoin(publication_url, "api/v1") @staticmethod def _handle_response(response: requests.Response): @@ -92,6 +99,61 @@ def _handle_response(response: requests.Response): except ValueError: raise SubstackRequestException("Invalid Response: %s" % response.text) + def get_user_id(self): + profile = self.get_user_profile() + user_id = profile['id'] + + return user_id + + def get_user_primary_publication(self): + """ + Gets the users primary publication + """ + + profile = self.get_user_profile() + primary_publication = profile['primaryPublication'] + return { + "id": primary_publication['id'], + "name": primary_publication['name'], + "publication_url": f"https://{primary_publication['subdomain']}.substack.com" + } + + def get_user_publications(self): + """ + Gets the users publications + """ + + profile = self.get_user_profile() + + # Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id" + user_publications = [] + for publication in profile['publicationUsers']: + user_publications.append({"id": publication['publication_id'], + "name": publication['publication']['name'], + "publication_url": f"https://{publication['publication']['subdomain']}.substack.com" + }) + + return user_publications + + def get_user_profile(self): + """ + Gets the users profile + """ + response = self._session.get(f"{self.base_url}/user/profile/self") + + return Api._handle_response(response=response) + + def get_user_settings(self): + """ + Get list of users. + + Returns: + + """ + response = self._session.get(f"{self.base_url}/settings") + + return Api._handle_response(response=response) + def get_publication_users(self): """ Get list of users. From a77ddbf3a31ba5e4292123916aee5a6652781619 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 21:06:24 +0000 Subject: [PATCH 03/10] Fixing Indent issue and changing primary pub return format. --- substack/api.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/substack/api.py b/substack/api.py index 4dcd11a..ba129db 100644 --- a/substack/api.py +++ b/substack/api.py @@ -112,11 +112,8 @@ def get_user_primary_publication(self): profile = self.get_user_profile() primary_publication = profile['primaryPublication'] - return { - "id": primary_publication['id'], - "name": primary_publication['name'], - "publication_url": f"https://{primary_publication['subdomain']}.substack.com" - } + primary_publication['publication_url'] = f"https://{primary_publication['subdomain']}.substack.com" + return primary_publication def get_user_publications(self): """ From 6171675134181493826d844d3bd52d2fb893e6ea Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 21:13:09 +0000 Subject: [PATCH 04/10] SImpler / more complete response format --- substack/api.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/substack/api.py b/substack/api.py index ba129db..2e8e815 100644 --- a/substack/api.py +++ b/substack/api.py @@ -114,7 +114,7 @@ def get_user_primary_publication(self): primary_publication = profile['primaryPublication'] primary_publication['publication_url'] = f"https://{primary_publication['subdomain']}.substack.com" return primary_publication - + def get_user_publications(self): """ Gets the users publications @@ -125,13 +125,12 @@ def get_user_publications(self): # Loop through users "publicationUsers" list, and return a list of dictionaries of "name", and "subdomain", and "id" user_publications = [] for publication in profile['publicationUsers']: - user_publications.append({"id": publication['publication_id'], - "name": publication['publication']['name'], - "publication_url": f"https://{publication['publication']['subdomain']}.substack.com" - }) - + pub = publication['publication'] + pub['publication_url'] = f"https://{pub['subdomain']}.substack.com" + user_publications.append(pub) + return user_publications - + def get_user_profile(self): """ Gets the users profile @@ -139,7 +138,7 @@ def get_user_profile(self): response = self._session.get(f"{self.base_url}/user/profile/self") return Api._handle_response(response=response) - + def get_user_settings(self): """ Get list of users. From 027e165f46a4d3e314e033419d15be428eb5c70f Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 21:29:19 +0000 Subject: [PATCH 05/10] Add support for custom URL's in publication_url building --- substack/api.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/substack/api.py b/substack/api.py index 2e8e815..277ea57 100644 --- a/substack/api.py +++ b/substack/api.py @@ -104,6 +104,18 @@ def get_user_id(self): user_id = profile['id'] return user_id + + def get_publication_url(self, publication): + """ + Gets the publication url + """ + custom_domain = publication['custom_domain'] + if not custom_domain: + publication_url = f"https://{publication['subdomain']}.substack.com" + else: + publication_url = f"https://{custom_domain}" + + return publication_url def get_user_primary_publication(self): """ @@ -112,7 +124,8 @@ def get_user_primary_publication(self): profile = self.get_user_profile() primary_publication = profile['primaryPublication'] - primary_publication['publication_url'] = f"https://{primary_publication['subdomain']}.substack.com" + primary_publication['publication_url'] = self.get_publication_url(primary_publication) + return primary_publication def get_user_publications(self): @@ -126,7 +139,7 @@ def get_user_publications(self): user_publications = [] for publication in profile['publicationUsers']: pub = publication['publication'] - pub['publication_url'] = f"https://{pub['subdomain']}.substack.com" + pub['publication_url'] = self.get_publication_url(pub) user_publications.append(pub) return user_publications From e01bf1bda2cb1f0a7003b4882c0debbc0ce4e987 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 22:29:26 +0000 Subject: [PATCH 06/10] Default to users default publication. Support custom_domain sign-in (requires one extra step after login to get cookies) --- substack/api.py | 35 ++++++++++++++++++++++++++++++----- tests/substack/test_api.py | 5 ----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/substack/api.py b/substack/api.py index 277ea57..8940a68 100644 --- a/substack/api.py +++ b/substack/api.py @@ -25,7 +25,6 @@ def __init__( email=None, password=None, base_url=None, - publication_url=None, debug=False, ): """ @@ -42,8 +41,6 @@ def __init__( Defaults to https://substack.com/api/v1. """ self.base_url = base_url or "https://substack.com/api/v1" - if publication_url: - self.change_publication(publication_url) if debug: logging.basicConfig() @@ -54,6 +51,11 @@ def __init__( if email is not None and password is not None: self.login(email, password) + # get the users primary publication + user_publication = self.get_user_primary_publication() + # set the current publication to the users primary publication + self.change_publication(user_publication) + def login(self, email, password) -> dict: """ @@ -74,13 +76,25 @@ def login(self, email, password) -> dict: "redirect": "/", }, ) + return Api._handle_response(response=response) - def change_publication(self, publication_url): + def signin_for_pub(self, publication): + """ + Complete the signin process + """ + response = self._session.get( + f"https://substack.com/sign-in?redirect=%2F&for_pub={publication['subdomain']}", + ) + + def change_publication(self, publication): """ Change the publication URL """ - self.publication_url = urljoin(publication_url, "api/v1") + self.publication_url = urljoin(publication['publication_url'], "api/v1") + + # sign-in to the publication + self.signin_for_pub(publication) @staticmethod def _handle_response(response: requests.Response): @@ -186,6 +200,17 @@ def get_publication_subscriber_count(self): return Api._handle_response(response=response)['subscriberCount'] + def get_published_posts(self, offset=0, limit=25, order_by="post_date", order_direction="desc"): + """ + Get list of published posts for the publication. + """ + response = self._session.get( + f"{self.publication_url}/post_management/published", + params={"offset": offset, "limit": limit, "order_by": order_by, "order_direction": order_direction}, + ) + + return Api._handle_response(response=response) + def get_posts(self) -> dict: """ diff --git a/tests/substack/test_api.py b/tests/substack/test_api.py index e0cadf6..0d76597 100644 --- a/tests/substack/test_api.py +++ b/tests/substack/test_api.py @@ -18,7 +18,6 @@ def test_login(self): api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) self.assertIsNotNone(api) @@ -31,7 +30,6 @@ def test_get_drafts(self): api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) drafts = api.get_drafts() self.assertIsNotNone(drafts) @@ -40,7 +38,6 @@ def test_post_draft(self): api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) posted_draft = api.post_draft([{"id": os.getenv("USER_ID"), "is_guest": False}]) self.assertIsNotNone(posted_draft) @@ -49,7 +46,6 @@ def test_publication_users(self): api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) users = api.get_publication_users() self.assertIsNotNone(users) @@ -58,7 +54,6 @@ def test_put_draft(self): api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) posted_draft = api.put_draft("") self.assertIsNotNone(posted_draft) From 065d30165e54ae4a1e23fd2adffd8e20534a6636 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 22:51:12 +0000 Subject: [PATCH 07/10] Add getDraft end point call so you can load existing drafts --- substack/api.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/substack/api.py b/substack/api.py index 8940a68..26a35e7 100644 --- a/substack/api.py +++ b/substack/api.py @@ -238,6 +238,14 @@ def get_drafts(self, filter=None, offset=None, limit=None): ) return Api._handle_response(response=response) + def get_draft(self, draft_id): + """ + Gets a draft given it's id. + + """ + response = self._session.get(f"{self.publication_url}/drafts/{draft_id}") + return Api._handle_response(response=response) + def delete_draft(self, draft_id): """ From 78b2046ca4bfb6c528b57cf41e9592f61cf19c20 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Fri, 1 Dec 2023 23:00:36 +0000 Subject: [PATCH 08/10] Update readme to use get_user_id instead of env, and to explain how to switch publications --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0c432e3..a6f72f5 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,8 @@ You can install python-substack using: Set the following environment variables by creating a **.env** file: - PUBLICATION_URL=https://ma2za.substack.com EMAIL= PASSWORD= - USER_ID= - -To discover the USER_ID go to your public profile page, -in the URL bar of the browser you find the substack address -followed by your USER_ID and your username: -https://substack.com/profile/[USER_ID]-[username] ## If you don't have a password 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. @@ -55,13 +48,24 @@ from substack.post import Post api = Api( email=os.getenv("EMAIL"), password=os.getenv("PASSWORD"), - publication_url=os.getenv("PUBLICATION_URL"), ) +user_id = api.get_user_id() + +# Switch Publications - The library defaults to your users primary publication. You can retrieve all your publications and change which one you want to use. + +# primary publication +user_publication = api.get_user_primary_publication() +# all publications +user_publications = api.get_user_publications() + +# This step is only necessary if you are not using your primary publication +# api.change_publication(user_publication) + post = Post( title="How to publish a Substack post using the Python API", subtitle="This post was published using the Python API", - user_id=os.getenv("USER_ID") + user_id=user_id ) post.add({'type': 'paragraph', 'content': 'This is how you add a new paragraph to your post!'}) From 8bff1de75dea30baa5dbbf366ea99952a6252fd6 Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Mon, 4 Dec 2023 17:24:55 +0000 Subject: [PATCH 09/10] adding in publication_url --- substack/api.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/substack/api.py b/substack/api.py index 26a35e7..5987b8a 100644 --- a/substack/api.py +++ b/substack/api.py @@ -25,6 +25,7 @@ def __init__( email=None, password=None, base_url=None, + publication_url=None, debug=False, ): """ @@ -51,8 +52,25 @@ def __init__( if email is not None and password is not None: self.login(email, password) - # get the users primary publication - user_publication = self.get_user_primary_publication() + # if the user provided a publication url, then use that + if publication_url: + import re + + # Regular expression to extract subdomain name + match = re.search(r"https://(.*).substack.com", publication_url.lower()) + subdomain = match.group(1) if match else None + + user_publications = self.get_user_publications() + # search through publications to find the publication with the matching subdomain + for publication in user_publications: + if publication['subdomain'] == subdomain: + # set the current publication to the users publication + user_publication = publication + break + else: + # get the users primary publication + user_publication = self.get_user_primary_publication() + # set the current publication to the users primary publication self.change_publication(user_publication) From 3fb996149d363cab010929d6430b82598c8883ee Mon Sep 17 00:00:00 2001 From: Peter Wicks Date: Mon, 4 Dec 2023 11:02:25 -0700 Subject: [PATCH 10/10] Removing extra space --- substack/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substack/api.py b/substack/api.py index 5987b8a..048618d 100644 --- a/substack/api.py +++ b/substack/api.py @@ -25,7 +25,7 @@ def __init__( email=None, password=None, base_url=None, - publication_url=None, + publication_url=None, debug=False, ): """