From 03dbe58db71c21196789c595cf72bfd703de21bd Mon Sep 17 00:00:00 2001 From: 0x000Saji Date: Fri, 25 Apr 2025 20:48:11 +0330 Subject: [PATCH 1/3] "feat: allow cookie-based auth to bypass recaptcha" --- substack/api.py | 14 ++++++-------- substack/cookie_session.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 substack/cookie_session.py diff --git a/substack/api.py b/substack/api.py index 6e1dd53..af4a35a 100644 --- a/substack/api.py +++ b/substack/api.py @@ -10,7 +10,7 @@ import os from datetime import datetime from urllib.parse import urljoin - +from substack.cookie_session import CookieSession import requests from substack.exceptions import SubstackAPIException, SubstackRequestException @@ -64,9 +64,7 @@ def __init__( # Load cookies from file if provided # Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud if cookies_path is not None: - with open(cookies_path) as f: - cookies = json.load(f) - self._session.cookies.update(cookies) + self._session = CookieSession(cookies_path) elif email is not None and password is not None: self.login(email, password) @@ -189,11 +187,11 @@ def get_publication_url(publication: dict) -> str: Args: publication: """ - custom_domain = publication["custom_domain"] - if not custom_domain: - publication_url = f"https://{publication['subdomain']}.substack.com" - else: + if publication['custom_domain_optional']: + custom_domain = publication["custom_domain"] publication_url = f"https://{custom_domain}" + else: + publication_url = f"https://{publication['subdomain']}.substack.com" return publication_url diff --git a/substack/cookie_session.py b/substack/cookie_session.py new file mode 100644 index 0000000..aa21305 --- /dev/null +++ b/substack/cookie_session.py @@ -0,0 +1,16 @@ +# substack/cookie_session.py +import json, os, requests +from requests.utils import cookiejar_from_dict + +class CookieSession(requests.Session): + """ + A requests.Session that loads cookies from Chrome/Firefox export + (JSON list of {name,value,domain,path,...}). If a CSRF token is + present it is copied to the required headers so every POST works. + """ + def __init__(self, cookie_file: str | os.PathLike, verify_ssl: bool = True): + super().__init__() + with open(cookie_file, "r", encoding="utf-8") as f: + raw = json.load(f) + self.cookies = cookiejar_from_dict({c["name"]: c["value"] for c in raw}) + self.verify = verify_ssl From b20b09ba7778895e17ac2395d28f3e16ae464243 Mon Sep 17 00:00:00 2001 From: 0x000Saji Date: Wed, 30 Apr 2025 04:42:56 +0330 Subject: [PATCH 2/3] fix: avoiding KeyError --- substack/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/substack/api.py b/substack/api.py index af4a35a..cdd1280 100644 --- a/substack/api.py +++ b/substack/api.py @@ -187,7 +187,7 @@ def get_publication_url(publication: dict) -> str: Args: publication: """ - if publication['custom_domain_optional']: + if publication.get('custom_domain_optional', None): custom_domain = publication["custom_domain"] publication_url = f"https://{custom_domain}" else: From 68569422b17749c49ee83d3eb90202614bc97b9e Mon Sep 17 00:00:00 2001 From: 0x000Saji Date: Wed, 30 Apr 2025 04:50:06 +0330 Subject: [PATCH 3/3] fix: update cookies in session instance --- substack/api.py | 8 +++++--- substack/cookie_session.py | 16 ---------------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 substack/cookie_session.py diff --git a/substack/api.py b/substack/api.py index cdd1280..6cf733d 100644 --- a/substack/api.py +++ b/substack/api.py @@ -10,9 +10,8 @@ import os from datetime import datetime from urllib.parse import urljoin -from substack.cookie_session import CookieSession import requests - +from requests.utils import cookiejar_from_dict from substack.exceptions import SubstackAPIException, SubstackRequestException logger = logging.getLogger(__name__) @@ -64,7 +63,10 @@ def __init__( # Load cookies from file if provided # Helps with Captcha errors by reusing cookies from "local" auth, then switching to running code in the cloud if cookies_path is not None: - self._session = CookieSession(cookies_path) + with open(cookies_path, "r", encoding="utf-8") as f: + raw = json.load(f) + cookies = cookiejar_from_dict({c["name"]: c["value"] for c in raw}) + self._session.cookies.update(cookies) elif email is not None and password is not None: self.login(email, password) diff --git a/substack/cookie_session.py b/substack/cookie_session.py deleted file mode 100644 index aa21305..0000000 --- a/substack/cookie_session.py +++ /dev/null @@ -1,16 +0,0 @@ -# substack/cookie_session.py -import json, os, requests -from requests.utils import cookiejar_from_dict - -class CookieSession(requests.Session): - """ - A requests.Session that loads cookies from Chrome/Firefox export - (JSON list of {name,value,domain,path,...}). If a CSRF token is - present it is copied to the required headers so every POST works. - """ - def __init__(self, cookie_file: str | os.PathLike, verify_ssl: bool = True): - super().__init__() - with open(cookie_file, "r", encoding="utf-8") as f: - raw = json.load(f) - self.cookies = cookiejar_from_dict({c["name"]: c["value"] for c in raw}) - self.verify = verify_ssl