From 874b1632c783edeb2181b5665b315cc7dd4118e8 Mon Sep 17 00:00:00 2001 From: Matthew Ryan Dillon Date: Sun, 20 Apr 2025 07:37:23 -0400 Subject: [PATCH] rebooting project sans history --- .python-version | 1 + Dockerfile | 6 + README.md | 32 ++ docker-compose.yml | 44 ++ main.py | 73 +++ pyproject.toml | 12 + secrets.sh.age | Bin 0 -> 449 bytes uv.lock | 1117 ++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1285 insertions(+) create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 secrets.sh.age create mode 100644 uv.lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8437b95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.12-slim-bookworm +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ +ADD . /app +WORKDIR /app +RUN uv sync --frozen --no-cache +CMD ["/app/.venv/bin/fastapi", "run", "main.py", "--port", "8887"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..795c208 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# rss + +collection of rss-related tools + +```bash +docker compose -f docker-compose.yml up -d +``` + +example secrets file: + +```bash +# secrets.sh +export RSS_HOST="http://localhost:8888" +export REDDIT_CLIENT_ID="blah" +export REDDIT_CLIENT_SECRET="blah" +export REDDIT_USERNAME="blah" +export REDDIT_PASSWORD="bla" +``` + +editing secrets: + +```bash +age --decrypt -i path/to/key -o secrets.sh secrets.sh.age +age -r $RECIPIENT -o secrets.sh.age secrets.sh +``` + +example envrc: + +```bash +# .envrc +age --decrypt -i path/to/key -o secrets.sh secrets.sh.age && source secrets.sh && rm secrets.sh +``` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e6144f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + miniflux: + image: miniflux/miniflux:latest + ports: + - "8888:8080" + depends_on: + db: + condition: service_healthy + environment: + - DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable + - RUN_MIGRATIONS=1 + - CREATE_ADMIN=1 + - ADMIN_USERNAME=admin + - ADMIN_PASSWORD=test123 + - HTTP_CLIENT_TIMEOUT=300 + + db: + image: postgres:17-alpine + environment: + - POSTGRES_USER=miniflux + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=miniflux + volumes: + - miniflux-db:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "miniflux"] + interval: 10s + start_period: 30s + + reddit_rss: + build: + context: ./ + dockerfile: Dockerfile + ports: + - "8887:8887" + environment: + - REDDIT_CLIENT_ID + - REDDIT_CLIENT_SECRET + - REDDIT_PASSWORD + - REDDIT_USERNAME + - RSS_HOST + +volumes: + miniflux-db: diff --git a/main.py b/main.py new file mode 100644 index 0000000..902b9ba --- /dev/null +++ b/main.py @@ -0,0 +1,73 @@ +from contextlib import asynccontextmanager +from datetime import datetime, timezone +from os import environ + +import asyncpraw +from async_lru import alru_cache +from fastapi import FastAPI, Response, HTTPException +from feedgen.feed import FeedGenerator + + +REDDIT_CLIENT_ID = environ["REDDIT_CLIENT_ID"] +REDDIT_CLIENT_SECRET = environ["REDDIT_CLIENT_SECRET"] +REDDIT_PASSWORD = environ["REDDIT_PASSWORD"] +REDDIT_USERNAME = environ["REDDIT_USERNAME"] +RSS_HOST = environ["RSS_HOST"] + + +@alru_cache(ttl=15 * 60) +async def fetch(feed, comments): + fg = FeedGenerator() + fg.id(f'{RSS_HOST}/{feed}') + fg.title(f'r/{feed}') + fg.link(href=f'{RSS_HOST}/{feed}') + fg.description(f'r/{feed}') + + client = asyncpraw.Reddit( + user_agent="stuff", + client_id=REDDIT_CLIENT_ID, + client_secret=REDDIT_CLIENT_SECRET, + username=REDDIT_USERNAME, + password=REDDIT_PASSWORD, + ) + + async with client as reddit: + subreddit = await reddit.subreddit(feed) + async for submission in subreddit.top(limit=30, time_filter="week"): + if submission.selftext == "": + continue + + fe = fg.add_entry() + fe.id(submission.id) + fe.title(submission.title) + fe.link(href=submission.url) + fe.pubDate(datetime.fromtimestamp(submission.created_utc, timezone.utc)) + author = 'none' if submission.author is None else submission.author.name + content = f"

{author} ({submission.score})

" + content += f"

{submission.selftext_html}

" + if comments: + comments = "" + for comment in await submission.comments(): + if isinstance(comment, asyncpraw.models.MoreComments): + continue + if comment.author is None: + continue + author = comment.author.name + if author.lower() == 'automoderator': + continue + comments += f"
  • {author} ({comment.score}) {comment.body_html}" + content += f"" + + fe.content(content) + + rss = fg.rss_str(pretty=True) + return rss + + +app = FastAPI() + + +@app.get("/{feed}") +async def root(feed: str, comments: bool = False): + rss = await fetch(feed, comments) + return Response(content=rss, media_type="application/xml") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f6a4c4e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "rss" +version = "0.1.0" +description = "reformats reddit for rss consumption" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "async-lru>=2.0.5", + "asyncpraw>=7.8.1", + "fastapi[standard]>=0.115.12", + "feedgen>=1.0.0", +] diff --git a/secrets.sh.age b/secrets.sh.age new file mode 100644 index 0000000000000000000000000000000000000000..b85658431efd9df44164d3e305885c832e605d33 GIT binary patch literal 449 zcmV;y0Y3g=XJsvAZewzJaCB*JZZ2or79zMe~%MMXb7pIC(mW5!u zI$|xm6rKUcO1X!kX+3~WHpRa4m4QN|KQ>C|XBFe-Bxv7FWb#>6=8~YI!D}fI^|v5r zjATR4gJW>*Sqn_