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
+157 -105

No files matched your search

+135 -95
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,48 +93,79 @@ 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:
results["checked"] += 1
try:
release_title, link = grab_gitea_release_information(rss_feed)
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={link}")
for feed_id, rss_feed, last_entry in feeds:
results["checked"] += 1
try:
release_title, link = grab_gitea_release_information(rss_feed)
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={link}")
if link == last_entry:
continue
if link == last_entry:
continue
logger.info(f"[FastAPI] Release change for ID {feed_id}: {release_title}")
request.app.state.notifier.send(
title=feed_name,
body=release_title,
link=link,
category="utility"
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 or repo,
body=release_title,
link=link,
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),
)
db.commit()
logger.debug(f"[FastAPI] Notification sent and database updated for Gitea feed ID {feed_id}")
results["new"] += 1
db.commit()
logger.debug(f"[FastAPI] Notification sent and database updated for Gitea feed ID {feed_id}")
results["new"] += 1
if link.startswith("https://gitea.com/gitea/runner/releases/tag/v"):
gitea_build_success = build_gitea_runner(release_title, request.app.state.gitea_token)
logger.info(f"[FastAPI] Gitea build ran successfully:{gitea_build_success}")
if link.startswith("https://gitea.com/gitea/runner/releases/tag/v"):
gitea_build_success = build_gitea_runner(release_title, request.app.state.gitea_token)
logger.info(f"[FastAPI] Gitea build ran successfully:{gitea_build_success}")
except Exception as e:
db.rollback()
results["errors"] += 1
logger.error(
f"[FastAPI] Failed for feed ID {feed_id} ({rss_feed}): {e}",
exc_info=True,
)
except Exception as e:
db.rollback()
results["errors"] += 1
logger.error(
f"[FastAPI] Failed for feed ID {feed_id} ({rss_feed}): {e}",
exc_info=True,
)
if results["errors"]:
logger.warning(f"[FastAPI] Gitea check finished with errors: {results}")
@@ -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,67 +201,66 @@ 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:
story = grab_royal_road_chapter_information(rss_feed)
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={story.chapter_id}")
for feed_id, rss_feed, last_entry in feeds:
results["checked"] += 1
try:
story = grab_royal_road_chapter_information(rss_feed)
logger.debug(f"[FastAPI] ID:{feed_id},RSS Feed {rss_feed}: db={last_entry}, online={story.chapter_id}")
if story.chapter_id == last_entry:
continue
if story.chapter_id == last_entry:
continue
logger.info(f"[FastAPI] New chapter detected for story ID {feed_id}: {story.story_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:
request.app.state.notifier.send(
title=feed_name or story.story_title,
body=story.chapter_title,
link=story.link,
category="royal-road",
user_id=user_id,
)
send += 1
except NotificationUnavailableError as e:
failed = len(subscribers) - sent
logger.error (f"[FastAPI] Notifications backend unavailable, skipping"
f"{failed} remaining user(s) for story={story.story_title}")
break
except NotificationError as e:
failed += 1
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
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",
(story.chapter_id, feed_id),
)
db.commit()
logger.debug(f"[FastAPI] Notification sent and database updated for story ID {feed_id}")
results["new"] += 1
except Exception as e:
db.rollback()
results["errors"] += 1
logger.error(
f"[Royalroad] Failed for feed ID {feed_id} ({rss_feed}): {e}",
exc_info=True,
logger.info(f"[FastAPI] New chapter detected for story ID {feed_id}: {story.story_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:
request.app.state.notifier.send(
title=feed_name or story.story_title,
body=story.chapter_title,
link=story.link,
category="royal-road",
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 story={story.story_title}")
break
except NotificationError as e:
failed += 1
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"] += 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",
(story.chapter_id, feed_id),
)
db.commit()
logger.debug(f"[FastAPI] Notification sent and database updated for story ID {feed_id}")
results["new"] += 1
except Exception as e:
db.rollback()
results["errors"] += 1
logger.error(
f"[Royalroad] Failed for feed ID {feed_id} ({rss_feed}): {e}",
exc_info=True,
)
if results["errors"]:
logger.warning(f"[FastAPI] Royalroad check finished with errors: {results}")
@@ -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)