fix(ingest): skip tier-gated Patreon posts entirely (#874)
CI / lint (push) Failing after 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Failing after 3m20s

Patreon serves only blurred locked-preview thumbnails for posts the
authenticated account can't fully view; the native ingester was
downloading those as real media. current_user_can_view was already in
_FIELDS_POST but never read.

Add PatreonClient.post_is_gated (gate ONLY on explicit
current_user_can_view=False; missing/None → viewable, never over-filter)
and skip gated posts at the top of the ingest_core run() and preview()
loops — no media download AND no post-record stub (operator: 'no stub
for gated content'). Skipped before the post-record block so gated posts
never inflate the #862 body canary; surfaced as 'N gated-skipped' in the
run summary. Same gate in preview() for preview/apply parity (rule 93).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 14:43:00 -04:00
parent 60a9c9e6ef
commit b3afc2437e
4 changed files with 125 additions and 1 deletions
+17
View File
@@ -524,6 +524,23 @@ class PatreonClient:
"date": published if isinstance(published, str) else None,
}
@staticmethod
def post_is_gated(post: dict) -> bool:
"""True when the authenticated account CANNOT view this post's content
(#874). Patreon serves only BLURRED locked-preview thumbnails for
paywalled / insufficient-tier posts, and `current_user_can_view` on the
post attributes is the access flag (it IS in `_FIELDS_POST`). The walk
skips a gated post ENTIRELY — no media, no post-record stub — so those
unusable previews never get downloaded.
Gate ONLY on an explicit `current_user_can_view == False`. A missing /
None flag (older posts, a sparse fieldset, or API drift) is treated as
viewable, so we never over-filter accessible posts on an absent field.
Part of the client contract the core consumes via getattr — an optional
seam, so stub clients / not-yet-migrated platforms simply never gate."""
attrs = post.get("attributes") or {}
return attrs.get("current_user_can_view") is False
@staticmethod
def post_record_key(post: dict) -> tuple[str, str] | None:
"""`(ledger_key, post_id)` for a media-less post's seen-ledger entry, or