feat(patreon): resolve the creator's campaign from a single-post URL
A source URL like https://www.patreon.com/posts/mimic-in-dungeon-158372536 is a single-post permalink, not a creator page — the resolver grabbed "posts" as the vanity and failed (operator-flagged 2026-06-07). Add a resolution path: extract the trailing post id and follow it to the owning campaign via the Patreon post API (/api/posts/<id>?include=campaign), so pasting any post URL subscribes to that creator's whole feed. `posts/` is excluded from the vanity regex so it can't masquerade as a creator slug. Resolution order is now: cached override → id: URL → /posts/<id> → vanity (campaigns API + creator-page scrape). Tests cover the post→campaign resolve and that /posts/ URLs aren't treated as vanities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -183,3 +183,35 @@ def test_extract_vanity_handles_all_path_prefixes():
|
||||
assert extract_vanity("https://patreon.com/cw/Atole") == "Atole"
|
||||
# id: URLs are NOT vanities (handled by the id path).
|
||||
assert extract_vanity("https://www.patreon.com/id:123") is None
|
||||
# /posts/ permalinks are NOT vanities (handled by the post path) — must not
|
||||
# masquerade as a creator slug named "posts".
|
||||
assert extract_vanity(
|
||||
"https://www.patreon.com/posts/mimic-in-dungeon-158372536"
|
||||
) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_for_source_resolves_post_url_to_campaign():
|
||||
"""A /posts/<slug>-<id> permalink resolves to the OWNING creator's campaign
|
||||
(a subscription walks the whole feed, not one post)."""
|
||||
post_resp = MagicMock()
|
||||
post_resp.status_code = 200
|
||||
post_resp.json.return_value = {
|
||||
"data": {
|
||||
"id": "158372536", "type": "post",
|
||||
"relationships": {"campaign": {"data": {"id": "9988", "type": "campaign"}}},
|
||||
}
|
||||
}
|
||||
calls = []
|
||||
|
||||
def _get(url, **kwargs):
|
||||
calls.append(url)
|
||||
return post_resp
|
||||
|
||||
with patch("backend.app.services.patreon_resolver.requests.get", side_effect=_get):
|
||||
cid, resolved = await resolve_campaign_id_for_source(
|
||||
"https://www.patreon.com/posts/mimic-in-dungeon-158372536", None, {},
|
||||
)
|
||||
assert cid == "9988"
|
||||
assert resolved == "9988" # newly resolved → caller caches it
|
||||
assert calls and "/api/posts/158372536" in calls[0]
|
||||
|
||||
Reference in New Issue
Block a user