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 from logger import mylog from helper import ( timeNowTZ, ) 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 = timeNowTZ() # 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)}