diff --git a/README.md b/README.md index 01531c7..a6f72f5 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,16 @@ 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. + +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" The .env file will be ignored by git but always be careful. @@ -47,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!'}) diff --git a/substack/api.py b/substack/api.py index 44fc707..048618d 100644 --- a/substack/api.py +++ b/substack/api.py @@ -42,7 +42,6 @@ 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 debug: logging.basicConfig() @@ -53,6 +52,28 @@ def __init__( if email is not None and password is not None: self.login(email, password) + # 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) + def login(self, email, password) -> dict: """ @@ -73,7 +94,25 @@ def login(self, email, password) -> dict: "redirect": "/", }, ) + return Api._handle_response(response=response) + + 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['publication_url'], "api/v1") + + # sign-in to the publication + self.signin_for_pub(publication) @staticmethod def _handle_response(response: requests.Response): @@ -92,6 +131,70 @@ 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_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): + """ + Gets the users primary publication + """ + + profile = self.get_user_profile() + primary_publication = profile['primaryPublication'] + primary_publication['publication_url'] = self.get_publication_url(primary_publication) + + return primary_publication + + 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']: + pub = publication['publication'] + pub['publication_url'] = self.get_publication_url(pub) + user_publications.append(pub) + + 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. @@ -115,6 +218,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: """ @@ -142,6 +256,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): """ 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)