KOL feeds: fix dead/blocked sources, drop stale feeds (29→25)

Feed-health pass over KOL_FEEDS:
- raoulpal: stale Substack (last 2024-05) → Real Vision podcast feed
- dampedspring: paywalled (0 entries) → free "Damped Spring 101" Substack
- unchained: Cloudflare 403 → canonical Megaphone podcast feed
- lynalden: Cloudflare 202 → FeedBurner mirror
- glassnode: recovered via httpx http2=True (was 403 on HTTP/1.1)
- browser User-Agent + Accept headers on feed fetch
- removed dead feeds with no active replacement: placeholder,
  dragonfly, niccarter, eugene
- pin h2==4.3.0 (required by http2=True)

All 25 remaining feeds verified fetching real body content; newest
post per feed ≤88d. Bundles in-flight KOL-module work already in the
working tree (kol_x ingest, migration 027, tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
k
2026-06-09 22:55:16 +08:00
parent 213bb911e3
commit 54884f3e24
38 changed files with 2340 additions and 322 deletions
+68 -7
View File
@@ -87,14 +87,21 @@ class InitResponse(BaseModel):
expires_in_seconds: int
# ── Endpoints ────────────────────────────────────────────────────────────
# ── Helpers ──────────────────────────────────────────────────────────────
@router.get("/{wallet}/status", response_model=StatusResponse)
async def status(wallet: str, db: AsyncSession = Depends(get_db)) -> StatusResponse:
"""Public-by-wallet read. Returns whether server is configured AND
whether this wallet has bound a Telegram chat."""
wallet = wallet.lower().strip()
async def _build_status(
wallet: str,
db: AsyncSession,
authenticated: bool = False,
) -> StatusResponse:
"""Shared status-building logic used by both the GET endpoint and
update_preferences so both always return a consistent StatusResponse.
B37: previously update_preferences called `await status(wallet, db)`,
which passed db as the `timestamp` positional arg and left the DI-managed
`db` param as a raw Depends() wrapper — crashing on `.execute()`.
"""
configured = bool(settings.telegram_bot_token and settings.telegram_bot_username)
b = (await db.execute(
select(TelegramBinding).where(TelegramBinding.wallet_address == wallet)
@@ -104,6 +111,14 @@ async def status(wallet: str, db: AsyncSession = Depends(get_db)) -> StatusRespo
return StatusResponse(configured=configured,
bot_username=settings.telegram_bot_username or None,
bound=False)
if not authenticated:
return StatusResponse(
configured=configured,
bot_username=settings.telegram_bot_username or None,
bound=True,
)
return StatusResponse(
configured=configured,
bot_username=settings.telegram_bot_username or None,
@@ -123,6 +138,49 @@ async def status(wallet: str, db: AsyncSession = Depends(get_db)) -> StatusRespo
)
# ── Endpoints ────────────────────────────────────────────────────────────
@router.get("/{wallet}/status", response_model=StatusResponse)
async def status(
wallet: str,
timestamp: Optional[int] = None,
signature: Optional[str] = None,
db: AsyncSession = Depends(get_db),
) -> StatusResponse:
"""Wallet Telegram status.
Unauthenticated: returns configured + bound (boolean only).
Authenticated (timestamp + signature): returns full binding details.
This prevents third parties from de-anonymising wallets via tg_username/chat_id.
"""
wallet = wallet.lower().strip()
# Verify ownership if credentials provided (best-effort — never block on failure).
# Accept either "view_telegram_status" (dedicated action) or "view_user"
# (broad read action that the frontend already caches for other endpoints).
# Accepting view_user avoids requiring a separate wallet signature just to
# see Telegram status — the frontend reuses its cached view_user envelope.
authenticated = False
if timestamp is not None and signature:
for _action in ("view_telegram_status", "view_user"):
try:
verify_signed_request(
action=_action,
wallet=wallet,
timestamp_ms=timestamp,
signature=signature,
body=None,
allow_replay=True,
)
authenticated = True
break
except Exception:
pass # Try next action; fall through to redacted response if all fail
return await _build_status(wallet, db, authenticated=authenticated)
@router.post("/{wallet}/init", response_model=InitResponse)
async def init_binding(
wallet: str, body: SignedEnvelope,
@@ -192,7 +250,10 @@ async def update_preferences(
await db.commit()
await db.refresh(b)
return await status(wallet, db)
# Return full authenticated status — the caller already proved ownership
# via the signed request verified above (B37: was `await status(wallet, db)`
# which passed db as the timestamp arg, crashing inside status()).
return await _build_status(wallet, db, authenticated=True)
@router.post("/{wallet}/unbind")