- Add app/services/reddit/discovery.py:
- search_subs(): searches /subreddits/search.json by keyword
- analyze_sub(): fetches /about.json + /about/rules.json per sub
- _classify_rules(): keyword-pattern classifier for promo policy
(banned / conditional / unknown; hard to positively confirm allowed)
- search_and_analyze(): combined search + per-sub analysis entry point
- Unauthenticated-friendly (uses auth cookies when available)
- Add POST /subs/discover endpoint: returns candidate list with
promo_allowed, flair_required, subscriber count, notes excerpt,
and already_tracked flag. Nothing stored until user imports.
- Add SubDiscoveryResult interface and api.subs.discover() in api.ts
- Rework SubRulesView: slide-in discovery panel (right drawer),
per-row Import button, auto-marks already-tracked subs, immutable
result update on import
Closes: #2
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.config import get_settings
|
|
from app.db.store import Store
|
|
|
|
router = APIRouter(prefix="/subs", tags=["subs"])
|
|
|
|
|
|
class DiscoverBody(BaseModel):
|
|
keyword: str
|
|
limit: int = 15
|
|
|
|
|
|
def _in_thread(fn):
|
|
store = Store(get_settings().db_path)
|
|
try:
|
|
return fn(store)
|
|
finally:
|
|
store.close()
|
|
|
|
|
|
class SubRulesUpsert(BaseModel):
|
|
flair_required: bool = False
|
|
flair_to_use: str | None = None
|
|
promo_allowed: bool | None = None # None = unknown
|
|
rule_warning: bool = False
|
|
notes: str | None = None
|
|
post_url: str | None = None # override link for Copy & Post (e.g. megathread)
|
|
|
|
|
|
@router.get("")
|
|
async def list_sub_rules(platform: str = "reddit"):
|
|
return await asyncio.to_thread(_in_thread, lambda s: s.list_sub_rules(platform))
|
|
|
|
|
|
@router.get("/{sub}")
|
|
async def get_sub_rules(sub: str, platform: str = "reddit"):
|
|
result = await asyncio.to_thread(_in_thread, lambda s: s.get_sub_rules(sub, platform))
|
|
if result is None:
|
|
raise HTTPException(404, f"No rules on record for r/{sub}")
|
|
return result
|
|
|
|
|
|
@router.put("/{sub}")
|
|
async def upsert_sub_rules(sub: str, body: SubRulesUpsert, platform: str = "reddit"):
|
|
fields = body.model_dump()
|
|
# Convert bool | None to int | None for SQLite
|
|
if fields.get("promo_allowed") is not None:
|
|
fields["promo_allowed"] = 1 if fields["promo_allowed"] else 0
|
|
fields["flair_required"] = 1 if fields["flair_required"] else 0
|
|
fields["rule_warning"] = 1 if fields["rule_warning"] else 0
|
|
return await asyncio.to_thread(
|
|
_in_thread, lambda s: s.upsert_sub_rules(sub, platform, **fields)
|
|
)
|
|
|
|
|
|
@router.post("/discover")
|
|
async def discover_subs(body: DiscoverBody):
|
|
"""
|
|
Search Reddit for subreddits matching a keyword and analyze their posting rules.
|
|
|
|
Returns a list of candidates with promo classification. Nothing is stored —
|
|
the caller decides which subs to import via PUT /subs/{sub}.
|
|
"""
|
|
from app.services.reddit.discovery import search_and_analyze
|
|
|
|
def _run(store: Store):
|
|
# Collect already-tracked sub names so the UI can flag them
|
|
existing = {r["sub"].lower() for r in store.list_sub_rules("reddit")}
|
|
try:
|
|
from app.services.reddit.client import RedditClient
|
|
cookies = RedditClient().cookies
|
|
except Exception:
|
|
cookies = None
|
|
return search_and_analyze(
|
|
keyword=body.keyword,
|
|
limit=body.limit,
|
|
cookies=cookies,
|
|
known_subs=existing,
|
|
)
|
|
|
|
return await asyncio.to_thread(_in_thread, _run)
|