Feature/user id support #6
No files matched your search
+18
-8
@@ -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
@@ -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__)
|
||||
|
||||
+60
-20
@@ -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,18 +231,18 @@ 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"
|
||||
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}"
|
||||
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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user