Added multi user support for the Gitea check

This commit is contained in:
florian committed 2026-10-30 16:35:09 +01:00
1 parent 146c229e45
commit 79fc87c9c5
3 files changed
+80 -28

No files matched your search

+18 -8
View File
@@ -1,15 +1,25 @@
CREATE TABLE `rss_feeds` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`type` enum('royalroad','gitea') COLLATE utf8mb4_unicode_ci NOT NULL,
`feed_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`rss_feed` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_entry` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`type` enum('royalroad','gitea') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`rss_feed` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`last_entry` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_rss_feed` (`rss_feed`),
UNIQUE KEY `uq_user_rss_feed` (`user_id`,`rss_feed`),
KEY `idx_active_type` (`active`,`type`),
CONSTRAINT `fk_rss_feeds_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
KEY `idx_active_type` (`active`,`type`)
)
CREATE TABLE `rss_feed_subscriptions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`rss_feed_id` int(10) unsigned NOT NULL,
`feed_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_user_feed` (`user_id`,`rss_feed_id`),
KEY `idx_feed_active` (`rss_feed_id`,`active`),
CONSTRAINT `fk_sub_feed` FOREIGN KEY (`rss_feed_id`) REFERENCES `rss_feeds` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sub_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
)
+4 -2
View File
@@ -1,9 +1,11 @@
import time
from dataclasses import dataclass
from typing import Tuple
from pydantic import BaseModel
import feedparser
import requests
from dataclasses import dataclass
from pydantic import BaseModel
from simple_logger_handler import setup_logger
logger = setup_logger(__name__)
+58 -18
View File
@@ -1,18 +1,21 @@
import asyncio
from contextlib import asynccontextmanager
import os
from contextlib import asynccontextmanager
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from auth_config import Caller, auth
from db_module import close_connection_pool, create_connection_pool, get_db, start_healthcheck_thread
from feed_handler import grab_gitea_release_information, grab_royal_road_chapter_information, build_gitea_runner, RoyalRoadStory
from feed_handler import (
build_gitea_runner,
grab_gitea_release_information,
grab_royal_road_chapter_information,
)
from metrics_server import REQUEST_COUNTER, metrics_api
from secret_manager import cleanup_secret_files, get_secret
from send_notifications import NotificationClient, NotificationUnavailableError, NotificationError
from send_notifications import NotificationClient, NotificationError, NotificationUnavailableError
from simple_logger_handler import LOG_LEVEL, setup_logger
from auth_config import Caller, auth
from service_auth import UserId
logger = setup_logger(__name__)
@@ -43,8 +46,8 @@ async def lifespan(app: FastAPI):
logger.info("[FastAPI] Shutdown complete.")
api = FastAPI(
title="Docker Repository Query",
description="Queries Dockerhub and GHCR for new docker images",
title="Service Rss Feeds",
description="Queries RSS feeds for updates",
version="1.0.0",
lifespan=lifespan,
dependencies=[Depends(auth.authenticate)],
@@ -70,7 +73,7 @@ async def prometheus_middleware(request: Request, call_next):
user_router = APIRouter(dependencies=[Depends(auth.require(Caller.GATEWAY))])
scheduler_router = APIRouter(dependencies=[Depends(auth.require(Caller.SCHEDULER))])
@scheduler_router.get("/gitea")
@scheduler_router.post("/gitea")
def get_release_information(request: Request, db = Depends(get_db)):
"""
Checks for new Gitea releases and updates the database accordingly.
@@ -90,14 +93,14 @@ def get_release_information(request: Request, db = Depends(get_db)):
results = {"checked": 0, "new": 0, "errors": 0}
with db.cursor() as cursor:
cursor.execute(
"SELECT id, rss_feed, feed_name, last_entry "
"SELECT id, rss_feed, last_entry "
"FROM rss_feeds WHERE active = 1 AND type = %s",
("gitea",),
)
feeds = cursor.fetchall()
logger.debug(f"[FastAPI] Found {len(feeds)} active Gitea feeds to check.")
for feed_id, rss_feed, feed_name, last_entry in feeds:
for feed_id, rss_feed, last_entry in feeds:
results["checked"] += 1
try:
release_title, link = grab_gitea_release_information(rss_feed)
@@ -107,12 +110,43 @@ def get_release_information(request: Request, db = Depends(get_db)):
continue
logger.info(f"[FastAPI] Release change for ID {feed_id}: {release_title}")
with db.cursor() as cursor:
cursor.execute(
"SELECT user_id, feed_name "
"FROM rss_feed_subscriptions "
"WHERE rss_feed_id = %s AND active = 1",
(feed_id,),
)
subscribers = cursor.fetchall()
sent = 0
failed = 0
for user_id,feed_name in subscribers:
try:
repo = link.split("/releases")[0].removeprefix("https://")
request.app.state.notifier.send(
title=feed_name,
title=feed_name or repo,
body=release_title,
link=link,
category="utility"
category="utility",
user_id=user_id,
)
sent += 1
except NotificationUnavailableError:
failed = len(subscribers) - sent
logger.error(f"[FastAPI] Notifications backend unavailable, skipping"
f"{failed} remaining user(s) for feed_id={feed_id}")
break
except NotificationError as e:
failed += 1
logger.error(f"[FastAPI] Failed to send notification to user_id={user_id} for feed_id={feed_id} "
f"status={getattr(e, 'status_code',None)}")
if subscribers and sent == 0:
results["errors"] += 1
logger.error(f"[FastAPI] All notifications failed for Feed ID:{feed_id}, will retry later")
continue
with db.cursor() as cursor:
cursor.execute(
"UPDATE rss_feeds SET last_entry = %s WHERE id = %s",
(link, feed_id),
@@ -140,7 +174,7 @@ def get_release_information(request: Request, db = Depends(get_db)):
logger.info(f"[FastAPI] Gitea check complete: {results}")
return results
@scheduler_router.get("/royalroad")
@scheduler_router.post("/royalroad")
def get_chapters(request: Request, db = Depends(get_db)):
"""
Checks for new Royalroad chapters and updates the database accordingly.
@@ -167,7 +201,6 @@ def get_chapters(request: Request, db = Depends(get_db)):
feeds = cursor.fetchall()
logger.debug(f"[FastAPI] Found {len(feeds)} active stories to check.")
cache: dict[str, str | None] = {}
for feed_id, rss_feed, last_entry in feeds:
results["checked"] += 1
try:
@@ -198,8 +231,8 @@ def get_chapters(request: Request, db = Depends(get_db)):
category="royal-road",
user_id=user_id,
)
send += 1
except NotificationUnavailableError as e:
sent += 1
except NotificationUnavailableError:
failed = len(subscribers) - sent
logger.error (f"[FastAPI] Notifications backend unavailable, skipping "
f"{failed} remaining user(s) for story={story.story_title}")
@@ -209,7 +242,7 @@ def get_chapters(request: Request, db = Depends(get_db)):
logger.error(f"[FastAPI] Failed to send notification to user_id={user_id} for story={story.story_title} "
f"status={getattr(e, 'status_code',None)}")
if subscribers and sent == 0:
results["errors"]+=failed
results["errors"] += 1
logger.error(f"[FastAPI] All notifications failed for Feed ID:{feed_id}, will retry later")
continue
with db.cursor() as cursor:
@@ -236,9 +269,16 @@ def get_chapters(request: Request, db = Depends(get_db)):
logger.info(f"[FastAPI] Royalroad check complete: {results}")
return results
api.include_router(user_router)
api.include_router(scheduler_router)
async def start_servers():
logger.info("[Server] Starting main API (port 5000) and metrics server (port 9000)...")
logger.info(f"[Server] Build commit id={os.getenv("COMMIT_ID","unknown")}")
auth.load_keys(api, Caller.GATEWAY, Caller.SCHEDULER)
cleanup_secret_files(auth.key_paths(Caller.GATEWAY, Caller.SCHEDULER))
config_main = uvicorn.Config(api, host="0.0.0.0", port=5000, log_level=LOG_LEVEL.lower(), log_config=None)
config_metrics = uvicorn.Config(metrics_api, host="0.0.0.0", port=9000, log_level=LOG_LEVEL.lower(), log_config=None)