Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions substack_api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,14 +531,18 @@ def publication_id(self) -> int:
"""The publication ID for this chat."""
return self._publication_id

def _fetch_threads_data(self, force_refresh: bool = False) -> Dict[str, Any]:
def _fetch_threads_data(
self, force_refresh: bool = False, limit: Optional[int] = None
) -> Dict[str, Any]:
"""
Fetch threads data from the API.

Parameters
----------
force_refresh : bool
If True, fetch fresh data even if cached data exists.
limit : Optional[int]
Stop after fetching this many threads.

Returns
-------
Expand All @@ -557,6 +561,9 @@ def _fetch_threads_data(self, force_refresh: bool = False) -> Dict[str, Any]:
if self._threads_data is not None and not force_refresh:
return self._threads_data

if force_refresh:
self._threads_data = None

if not self.auth or not self.auth.authenticated:
raise ChatAuthenticationRequired(
"Authentication is required to access publication chats."
Expand Down Expand Up @@ -584,8 +591,28 @@ def _fetch_threads_data(self, force_refresh: bool = False) -> Dict[str, Any]:
)

response.raise_for_status()
self._threads_data = response.json()
return self._threads_data
data = response.json()

while data.get("moreBefore") and data.get("threads"):
if limit is not None and len(data["threads"]) >= limit:
break

before = data["threads"][-1]["communityPost"]["created_at"]
response = self.auth.get(url, params={"before": before}, timeout=30)
response.raise_for_status()
page = response.json()
page_threads = page.get("threads", [])

if not page_threads:
break

data["threads"].extend(page_threads)
data["moreBefore"] = page.get("moreBefore", False)

if limit is None:
self._threads_data = data

return data

def get_threads(
self, limit: Optional[int] = None, force_refresh: bool = False
Expand All @@ -596,9 +623,7 @@ def get_threads(
Parameters
----------
limit : Optional[int]
Client-side truncation of the first page of results returned by the
API. The full page is always fetched; this just slices the list.
If None, returns all threads from the page.
Maximum threads returned.
force_refresh : bool
If True, fetch fresh data from the API.

Expand All @@ -614,7 +639,7 @@ def get_threads(
ChatNotFound
If the publication is not found.
"""
data = self._fetch_threads_data(force_refresh=force_refresh)
data = self._fetch_threads_data(force_refresh=force_refresh, limit=limit)
threads = [
ChatThread(
publication_id=self._publication_id,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,27 @@ def test_get_threads_caching(self, mock_auth, sample_threads_data):
chat.get_threads(force_refresh=True)
assert mock_auth.get.call_count == 2

def test_get_threads_paginates_before(self, mock_auth):
def response(threads):
result = MagicMock(status_code=200)
result.json.return_value = {"threads": threads, "moreBefore": True}
return result

newer = {"communityPost": {"id": "newer", "created_at": "2026-01-20"}}
older = {"communityPost": {"id": "older", "created_at": "2026-01-10"}}
chat = Chat(publication_id=4906951, auth=mock_auth)

mock_auth.get.side_effect = [response([newer])]
assert [thread.id for thread in chat.get_threads(limit=1)] == ["newer"]
assert mock_auth.get.call_count == 1

mock_auth.get.reset_mock()
mock_auth.get.side_effect = [response([newer]), response([older]), response([])]

threads = chat.get_threads()
assert [thread.id for thread in threads] == ["newer", "older"]
assert mock_auth.get.call_args.kwargs["params"] == {"before": "2026-01-10"}

def test_get_threads_unauthenticated(self, mock_unauth):
"""Test Chat.get_threads raises error when not authenticated."""
chat = Chat(publication_id=4906951, auth=mock_unauth)
Expand Down
Loading