Files
NetAlertX/server/messaging/in_app.py
jokob-sk b59bca2967 BE: API in-app messaging endpoint
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2025-10-10 17:00:53 +11:00

267 lines
8.4 KiB
Python
Executable File

import datetime
import os
import sys
import _io
import json
import uuid
import socket
import subprocess
import requests
from yattag import indent
from json2table import convert
from flask import jsonify
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
# Register NetAlertX modules
import conf
from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath
from logger import logResult, mylog
from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_file_content, write_file, get_setting_value, get_timezone_offset
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)}