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:
+68
-7
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user