magpie/app/api/endpoints/subs.py
Alan Weinstock 35c6e5f7bc feat(discovery): add Lemmy community search, fix dead request, add platform field
- Add app/services/lemmy/discovery.py: searches 5 major Lemmy instances,
  deduplicates by actor_id (AP canonical URL), skips NSFW communities,
  uses community@instance naming convention matching existing Lemmy client
- Update POST /subs/discover: accepts platforms[] param (default both),
  fans out to Reddit + Lemmy search, merges and sorts by subscribers
- Add platform field to all discovery result dicts (Reddit and Lemmy)
- Fix: remove dead _get() call left in search_subs() during earlier refactor
- Frontend: show platform badge on each discovery row, correct hyperlink
  format for Lemmy (https://{instance}/c/{community}), pass r.platform
  to upsertRules on import so Lemmy subs land in the lemmy platform slot
2026-06-13 22:23:31 -07:00

105 lines
3.4 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
platforms: list[str] = ["reddit", "lemmy"]
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):
platforms = set(body.platforms or ["reddit", "lemmy"])
results: list[dict] = []
if "reddit" in platforms:
from app.services.reddit.discovery import search_and_analyze
existing_reddit = {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
results.extend(search_and_analyze(
keyword=body.keyword,
limit=body.limit,
cookies=cookies,
known_subs=existing_reddit,
))
if "lemmy" in platforms:
from app.services.lemmy.discovery import search_lemmy
existing_lemmy = {r["sub"].lower() for r in store.list_sub_rules("lemmy")}
results.extend(search_lemmy(
keyword=body.keyword,
limit=body.limit,
known_subs=existing_lemmy,
))
# Merge and sort by subscribers descending
results.sort(key=lambda x: x.get("subscribers", 0), reverse=True)
return results
return await asyncio.to_thread(_in_thread, _run)