Add backend support for notifications

This commit is contained in:
Sami Abuzakuk
2025-10-12 14:54:37 +02:00
parent c8aa5e9917
commit fcb875aaf9
3 changed files with 333 additions and 5 deletions

View File

@@ -3,7 +3,7 @@ from fastapi import FastAPI
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
from model import Log, SessionLocal, Script, Settings from model import Log, SessionLocal, Script, Settings, Subscription, Notification
from run_scripts import run_scripts, update_requirements, update_environment from run_scripts import run_scripts, update_requirements, update_environment
import uvicorn import uvicorn
@@ -52,11 +52,183 @@ def hello():
return {"message": "Welcome to the Project Monitor API"} return {"message": "Welcome to the Project Monitor API"}
# Subscriptions API Endpoints
@app.get("/subscriptions")
def list_subscriptions():
db = SessionLocal()
subscriptions = db.query(Subscription).all()
db.close()
return subscriptions
class SubscriptionCreate(BaseModel):
topic: str
class SubscriptionResponse(BaseModel):
id: int
topic: str
created_at: datetime
model_config = {"from_attributes": True}
@app.get("/subscriptions/{subscription_id}", response_model=SubscriptionResponse)
def get_subscription(subscription_id: int):
db = SessionLocal()
subscription = (
db.query(Subscription).filter(Subscription.id == subscription_id).first()
)
if not subscription:
db.close()
raise HTTPException(status_code=404, detail="Subscription not found")
db.close()
return subscription
@app.post("/subscriptions")
def add_subscription(subscription: SubscriptionCreate):
db = SessionLocal()
existing_subscription = (
db.query(Subscription).filter(Subscription.topic == subscription.topic).first()
)
if existing_subscription:
db.close()
raise HTTPException(status_code=400, detail="Subscription already exists")
new_subscription = Subscription(topic=subscription.topic)
db.add(new_subscription)
db.commit()
db.refresh(new_subscription)
db.close()
return new_subscription
@app.delete("/subscriptions/{subscription_id}")
def remove_subscription(subscription_id: int):
db = SessionLocal()
subscription = (
db.query(Subscription).filter(Subscription.id == subscription_id).first()
)
if not subscription:
db.close()
raise HTTPException(status_code=404, detail="Subscription not found")
db.delete(subscription)
db.commit()
db.close()
return {"message": "Subscription removed"}
@app.get("/subscriptions/{subscription_id}/notifications")
def list_subscription_notifications(subscription_id: int):
db = SessionLocal()
notifications = (
db.query(Notification)
.filter(Notification.subscription_id == subscription_id)
.all()
)
db.close()
return [
NotificationResponse.model_validate(notification)
for notification in notifications
]
@app.get("/notifications")
def list_notifications():
db = SessionLocal()
notifications = db.query(Notification).all()
db.close()
return [
NotificationResponse.model_validate(notification)
for notification in notifications
]
@app.delete("/notifications/{notification_id}")
def remove_notification(notification_id: int):
db = SessionLocal()
notification = (
db.query(Notification).filter(Notification.id == notification_id).first()
)
if not notification:
db.close()
raise HTTPException(status_code=404, detail="Notification not found")
db.delete(notification)
db.commit()
db.close()
return {"message": "Notification removed"}
class NotificationCreate(BaseModel):
subscription_id: int
title: str
message: str
priority: int
class NotificationUpdate(BaseModel):
subscription_id: int | None = None
title: str | None = None
message: str | None = None
priority: int | None = None
viewed: bool | None = None
class NotificationResponse(NotificationCreate):
id: int
created_at: datetime
viewed: bool
model_config = {"from_attributes": True}
@app.put("/notifications/{notification_id}", response_model=NotificationResponse)
def update_notification(notification_id: int, notification: NotificationUpdate):
db = SessionLocal()
existing_notification = (
db.query(Notification).filter(Notification.id == notification_id).first()
)
if not existing_notification:
db.close()
raise HTTPException(status_code=404, detail="Notification not found")
if notification.subscription_id is not None:
existing_notification.subscription_id = notification.subscription_id
if notification.title is not None:
existing_notification.title = notification.title
if notification.message is not None:
existing_notification.message = notification.message
if notification.priority is not None:
existing_notification.priority = notification.priority
if notification.viewed is not None:
existing_notification.viewed = notification.viewed
db.commit()
db.refresh(existing_notification)
db.close()
return existing_notification
@app.post("/notifications", response_model=NotificationResponse)
def create_notification(notification: NotificationCreate):
db = SessionLocal()
new_notification = Notification(
subscription_id=notification.subscription_id,
title=notification.title,
message=notification.message,
priority=notification.priority,
)
db.add(new_notification)
db.commit()
db.refresh(new_notification)
db.close()
return new_notification
# Define Pydantic models for Settings # Define Pydantic models for Settings
class SettingsBase(BaseModel): class SettingsBase(BaseModel):
requirements: str requirements: str
environment: str environment: str
user: str user: str
ntfy_url: str
class SettingsUpdate(SettingsBase): class SettingsUpdate(SettingsBase):
@@ -107,14 +279,17 @@ def update_setting(settings_id: int, settings: SettingsUpdate):
if not existing_setting: if not existing_setting:
raise HTTPException(status_code=404, detail="Setting not found") raise HTTPException(status_code=404, detail="Setting not found")
if existing_setting.requirements != settings.requirements: if settings.requirements and existing_setting.requirements != settings.requirements:
existing_setting.requirements = settings.requirements existing_setting.requirements = settings.requirements
update_requirements(settings) update_requirements(settings)
if existing_setting.environment != settings.environment: if settings.environment and existing_setting.environment != settings.environment:
existing_setting.environment = settings.environment existing_setting.environment = settings.environment
update_environment(settings) update_environment(settings)
if settings.ntfy_url is not None:
existing_setting.ntfy_url = settings.ntfy_url
db.commit() db.commit()
db.refresh(existing_setting) db.refresh(existing_setting)
db.close() db.close()

View File

@@ -0,0 +1,107 @@
import os
import requests
from datetime import datetime
from model import SessionLocal, Subscription, Settings, Notification
import json
# Constants
NTFY_TOKEN = os.getenv("NTFY_TOKEN") or "tk_cdmwd6ix255g3qgo4dx3r0gakw4y3"
def fetch_ntfy_notifications(base_url, subscriptions):
"""Fetch notifications from the ntfy.sh server for the given subscriptions using streaming."""
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {NTFY_TOKEN}" if NTFY_TOKEN else None,
}
notifications = []
for subscription in subscriptions:
topic = subscription.topic
last_message_id = subscription.last_message_id
since_param = "all" if last_message_id is None else last_message_id
url = f"{base_url}/{topic}/json?poll=1&since={since_param}"
response = requests.get(url, headers=headers, stream=True)
response.raise_for_status()
for line in response.iter_lines():
if line:
notification = json.loads(line)
if notification.get("event") == "message":
notifications.append(notification)
print(f"Fetched {len(notifications)} notifications")
print(notifications)
return notifications
def save_notifications_to_db(notifications, topic_to_subscription, db):
"""Save the fetched notifications to the database and update last_message_id."""
db = SessionLocal()
last_message_ids = {}
for notification in notifications:
topic = notification["topic"]
last_message_ids[topic] = notification["id"]
subscription_id = topic_to_subscription.get(notification["topic"])
if subscription_id:
new_notification = Notification(
title=notification.get("title", "No Title"),
message=notification.get("message", ""),
priority=notification.get("priority", 3),
created_at=datetime.fromtimestamp(notification["time"]),
subscription_id=subscription_id,
)
db.add(new_notification)
for topic, message_id in last_message_ids.items():
subscription_id = topic_to_subscription.get(topic)
if subscription_id:
subscription = (
db.query(Subscription)
.filter(Subscription.id == subscription_id)
.first()
)
if subscription:
subscription.last_message_id = message_id
db.commit()
db.close()
def main():
"""Main function to fetch and save notifications."""
db = SessionLocal()
# Get the ntfy base URL from settings
settings = db.query(Settings).filter(Settings.user == "default").first()
if not settings:
print("Default user settings not found.")
return
ntfy_url = settings.ntfy_url
if not ntfy_url:
print("Ntfy URL not found in settings.")
return
# Get all subscribed topics
subscriptions = db.query(Subscription).all()
topic_to_subscription = {
subscription.topic: subscription.id for subscription in subscriptions
}
topic_to_subscription = {
subscription.topic: subscription.id for subscription in subscriptions
}
db.close()
# Fetch notifications from ntfy.sh
notifications = fetch_ntfy_notifications(ntfy_url, subscriptions)
# Save notifications to the database
save_notifications_to_db(notifications, topic_to_subscription, db)
if __name__ == "__main__":
main()

View File

@@ -1,9 +1,8 @@
from sqlalchemy import create_engine, Column, Integer, String, Text, ForeignKey from sqlalchemy import create_engine, Column, Integer, String, Text, ForeignKey, Boolean
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.functions import func from sqlalchemy.sql.functions import func
from sqlalchemy.sql.sqltypes import DateTime from sqlalchemy.sql.sqltypes import DateTime
from sqlalchemy.types import Boolean
# Initialize the database # Initialize the database
DATABASE_URL = "sqlite:///./project_monitor.db" DATABASE_URL = "sqlite:///./project_monitor.db"
@@ -50,7 +49,54 @@ class Settings(Base):
requirements = Column(String, nullable=False) requirements = Column(String, nullable=False)
environment = Column(String, nullable=False) environment = Column(String, nullable=False)
user = Column(String, nullable=False) user = Column(String, nullable=False)
ntfy_url = Column(String, nullable=True)
class Subscription(Base):
__tablename__ = "subscriptions"
id = Column(Integer, primary_key=True, index=True)
topic = Column(String, nullable=False, unique=True)
last_message_id = Column(String, nullable=True)
created_at = Column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class Notification(Base):
__tablename__ = "notifications"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
message = Column(Text, nullable=False)
priority = Column(Integer, nullable=False, default=3)
viewed = Column(Boolean, default=False)
sent = Column(Boolean, default=False)
subscription_id = Column(Integer, ForeignKey("subscriptions.id"), nullable=False)
created_at = Column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
# Create the database tables # Create the database tables
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# Ensure a default setting line exists
def ensure_default_setting():
db = SessionLocal()
default_setting = db.query(Settings).filter(Settings.user == "default").first()
if not default_setting:
new_setting = Settings(
requirements="",
environment="",
user="default",
ntfy_url="https://ntfy.abzk.fr",
)
db.add(new_setting)
db.commit()
db.close()
ensure_default_setting()