mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-06 17:15:38 -08:00
269 lines
8.3 KiB
Python
Executable File
269 lines
8.3 KiB
Python
Executable File
import os
|
|
import sys
|
|
import _io
|
|
import json
|
|
import uuid
|
|
import time
|
|
|
|
from flask import jsonify
|
|
|
|
# Register NetAlertX directories
|
|
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
|
|
sys.path.extend([f"{INSTALL_PATH}/server"])
|
|
|
|
from const import apiPath # noqa: E402 [flake8 lint suppression]
|
|
from logger import mylog # noqa: E402 [flake8 lint suppression]
|
|
from utils.datetime_utils import timeNowDB # noqa: E402 [flake8 lint suppression]
|
|
|
|
|
|
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'
|
|
|
|
|
|
# Show Frontend User Notification
|
|
def write_notification(content, level="alert", timestamp=None):
|
|
"""
|
|
Create and append a new user notification entry to the notifications file.
|
|
|
|
Args:
|
|
content (str): The message content to display to the user.
|
|
level (str, optional): Notification severity (e.g., 'info', 'alert', 'warning').
|
|
Defaults to 'alert'.
|
|
timestamp (datetime, optional): Custom timestamp; if None, uses current time.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if timestamp is None:
|
|
timestamp = timeNowDB()
|
|
|
|
# Generate GUID
|
|
guid = str(uuid.uuid4())
|
|
|
|
# Prepare notification dictionary
|
|
notification = {
|
|
"timestamp": str(timestamp),
|
|
"guid": guid,
|
|
"read": 0,
|
|
"level": level,
|
|
"content": content,
|
|
}
|
|
|
|
# If file exists, load existing data, otherwise initialize as empty list
|
|
if os.path.exists(NOTIFICATION_API_FILE):
|
|
with open(NOTIFICATION_API_FILE, "r") as file:
|
|
# Check if the file object is of type _io.TextIOWrapper
|
|
if isinstance(file, _io.TextIOWrapper):
|
|
file_contents = file.read() # Read file contents
|
|
if file_contents == "":
|
|
file_contents = "[]" # If file is empty, initialize as empty list
|
|
|
|
# mylog('debug', ['[Notification] User Notifications file: ', file_contents])
|
|
notifications = json.loads(file_contents) # Parse JSON data
|
|
else:
|
|
mylog("none", "[Notification] File is not of type _io.TextIOWrapper")
|
|
notifications = []
|
|
else:
|
|
notifications = []
|
|
|
|
# Append new notification
|
|
notifications.append(notification)
|
|
|
|
# Write updated data back to file
|
|
with open(NOTIFICATION_API_FILE, "w") as file:
|
|
json.dump(notifications, file, indent=4)
|
|
|
|
|
|
# Trim notifications
|
|
def remove_old(keepNumberOfEntries):
|
|
"""
|
|
Trim the notifications file, keeping only the most recent N entries.
|
|
|
|
Args:
|
|
keepNumberOfEntries (int): Number of latest notifications to retain.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
# Check if file exists
|
|
if not os.path.exists(NOTIFICATION_API_FILE):
|
|
mylog("info", "[Notification] No notifications file to clean.")
|
|
return
|
|
|
|
# Load existing notifications
|
|
try:
|
|
with open(NOTIFICATION_API_FILE, "r") as file:
|
|
file_contents = file.read().strip()
|
|
if file_contents == "":
|
|
notifications = []
|
|
else:
|
|
notifications = json.loads(file_contents)
|
|
except Exception as e:
|
|
mylog("none", f"[Notification] Error reading notifications file: {e}")
|
|
return
|
|
|
|
if not isinstance(notifications, list):
|
|
mylog("none", "[Notification] Invalid format: not a list")
|
|
return
|
|
|
|
# Sort by timestamp descending
|
|
try:
|
|
notifications.sort(key=lambda x: x["timestamp"], reverse=True)
|
|
except KeyError:
|
|
mylog("none", "[Notification] Missing timestamp in one or more entries")
|
|
return
|
|
|
|
# Trim to the latest entries
|
|
trimmed = notifications[:keepNumberOfEntries]
|
|
|
|
# Write back the trimmed list
|
|
try:
|
|
with open(NOTIFICATION_API_FILE, "w") as file:
|
|
json.dump(trimmed, file, indent=4)
|
|
mylog(
|
|
"verbose",
|
|
f"[Notification] Trimmed notifications to latest {keepNumberOfEntries}",
|
|
)
|
|
except Exception as e:
|
|
mylog("none", f"Error writing trimmed notifications file: {e}")
|
|
|
|
|
|
def mark_all_notifications_read():
|
|
"""
|
|
Mark all existing notifications as read.
|
|
|
|
Returns:
|
|
dict: JSON-compatible dictionary containing:
|
|
{
|
|
"success": bool,
|
|
"error": str (optional)
|
|
}
|
|
"""
|
|
if not os.path.exists(NOTIFICATION_API_FILE):
|
|
return {"success": True}
|
|
|
|
try:
|
|
with open(NOTIFICATION_API_FILE, "r") as f:
|
|
notifications = json.load(f)
|
|
except Exception as e:
|
|
mylog("none", f"[Notification] Failed to read notifications: {e}")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
for n in notifications:
|
|
n["read"] = 1
|
|
|
|
try:
|
|
with open(NOTIFICATION_API_FILE, "w") as f:
|
|
json.dump(notifications, f, indent=4)
|
|
except Exception as e:
|
|
mylog("none", f"[Notification] Failed to write notifications: {e}")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
mylog("debug", "[Notification] All notifications marked as read.")
|
|
return {"success": True}
|
|
|
|
|
|
def delete_notifications():
|
|
"""
|
|
Delete all notifications from the JSON file.
|
|
|
|
Returns:
|
|
A JSON response with {"success": True}.
|
|
"""
|
|
with open(NOTIFICATION_API_FILE, "w") as f:
|
|
json.dump([], f, indent=4)
|
|
mylog("debug", "[Notification] All notifications deleted.")
|
|
return jsonify({"success": True})
|
|
|
|
|
|
def get_unread_notifications():
|
|
"""
|
|
Retrieve all unread notifications from the JSON file.
|
|
|
|
Returns:
|
|
A JSON array of unread notification objects.
|
|
"""
|
|
if not os.path.exists(NOTIFICATION_API_FILE):
|
|
return jsonify([])
|
|
|
|
with open(NOTIFICATION_API_FILE, "r") as f:
|
|
notifications = json.load(f)
|
|
|
|
unread = [n for n in notifications if n.get("read", 0) == 0]
|
|
return jsonify(unread)
|
|
|
|
|
|
def mark_notification_as_read(guid=None, max_attempts=3):
|
|
"""
|
|
Mark a notification as read based on GUID.
|
|
If guid is None, mark all notifications as read.
|
|
|
|
Args:
|
|
guid (str, optional): The GUID of the notification to mark. Defaults to None.
|
|
max_attempts (int, optional): Number of attempts to read/write file. Defaults to 3.
|
|
|
|
Returns:
|
|
dict: {"success": True} on success, {"success": False, "error": "..."} on failure
|
|
"""
|
|
attempts = 0
|
|
|
|
while attempts < max_attempts:
|
|
try:
|
|
if os.path.exists(NOTIFICATION_API_FILE) and os.access(
|
|
NOTIFICATION_API_FILE, os.R_OK | os.W_OK
|
|
):
|
|
with open(NOTIFICATION_API_FILE, "r") as f:
|
|
notifications = json.load(f)
|
|
|
|
if notifications is not None:
|
|
for notification in notifications:
|
|
if guid is None or notification.get("guid") == guid:
|
|
notification["read"] = 1
|
|
|
|
with open(NOTIFICATION_API_FILE, "w") as f:
|
|
json.dump(notifications, f, indent=4)
|
|
|
|
return {"success": True}
|
|
except Exception as e:
|
|
mylog("none", f"[Notification] Attempt {attempts + 1} failed: {e}")
|
|
|
|
attempts += 1
|
|
time.sleep(0.5) # Sleep 0.5 seconds before retrying
|
|
|
|
error_msg = f"Failed to read/write notification file after {max_attempts} attempts."
|
|
mylog("none", f"[Notification] {error_msg}")
|
|
return {"success": False, "error": error_msg}
|
|
|
|
|
|
def delete_notification(guid):
|
|
"""
|
|
Delete a notification from the notifications file based on its GUID.
|
|
|
|
Args:
|
|
guid (str): The GUID of the notification to delete.
|
|
|
|
Returns:
|
|
dict: {"success": True} on success, {"success": False, "error": "..."} on failure
|
|
"""
|
|
if not guid:
|
|
return {"success": False, "error": "GUID is required"}
|
|
|
|
if not os.path.exists(NOTIFICATION_API_FILE):
|
|
return {"success": True} # Nothing to delete
|
|
|
|
try:
|
|
with open(NOTIFICATION_API_FILE, "r") as f:
|
|
notifications = json.load(f)
|
|
|
|
# Filter out the notification with the specified GUID
|
|
filtered_notifications = [n for n in notifications if n.get("guid") != guid]
|
|
|
|
# Write the updated notifications back
|
|
with open(NOTIFICATION_API_FILE, "w") as f:
|
|
json.dump(filtered_notifications, f, indent=4)
|
|
|
|
return {"success": True}
|
|
|
|
except Exception as e:
|
|
mylog("none", f"[Notification] Failed to delete notification {guid}: {e}")
|
|
return {"success": False, "error": str(e)}
|