- 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
105 lines
3.4 KiB
Python
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)
|