Auto delete in-app notifications #1052

This commit is contained in:
jokob-sk
2025-05-10 14:33:27 +10:00
parent a392803478
commit 01f7a18dce
31 changed files with 208 additions and 90 deletions

105
server/messaging/in_app.py Executable file
View File

@@ -0,0 +1,105 @@
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
# 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, timestamp):
# 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):
# 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}')

216
server/messaging/reporting.py Executable file
View File

@@ -0,0 +1,216 @@
#---------------------------------------------------------------------------------#
# NetAlertX #
# Open Source Network Guard / WIFI & LAN intrusion detector #
# #
# reporting.py - NetAlertX Back module. Template to email reporting in HTML format #
#---------------------------------------------------------------------------------#
# Puche 2021 pi.alert.application@gmail.com GNU GPLv3 #
# jokob-sk 2022 jokob.sk@gmail.com GNU GPLv3 #
# leiweibau 2022 https://github.com/leiweibau GNU GPLv3 #
# cvc90 2023 https://github.com/cvc90 GNU GPLv3 #
#---------------------------------------------------------------------------------#
import datetime
import json
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
import conf
from const import applicationPath, logPath, apiPath, confFileName
from helper import timeNowTZ, get_file_content, write_file, get_timezone_offset, get_setting_value
from logger import logResult, mylog
#===============================================================================
# REPORTING
#===============================================================================
#-------------------------------------------------------------------------------
def get_notifications (db):
sql = db.sql #TO-DO
# Reporting section
mylog('verbose', ['[Notification] Check if something to report'])
# prepare variables for JSON construction
json_new_devices = []
json_new_devices_meta = {}
json_down_devices = []
json_down_devices_meta = {}
json_down_reconnected = []
json_down_reconnected_meta = {}
json_events = []
json_events_meta = {}
json_plugins = []
json_plugins_meta = {}
# Disable reporting on events for devices where reporting is disabled based on the MAC address
# Disable notifications (except down/down reconnected) on devices where devAlertEvents is disabled
sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1 AND eve_EventType not in ('Device Down', 'Down Reconnected', 'New Device' ) AND eve_MAC IN
(
SELECT devMac FROM Devices WHERE devAlertEvents = 0
)""")
# Disable down/down reconnected notifications on devices where devAlertDown is disabled
sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1 AND eve_EventType in ('Device Down', 'Down Reconnected') AND eve_MAC IN
(
SELECT devMac FROM Devices WHERE devAlertDown = 0
)""")
sections = get_setting_value('NTFPRCS_INCLUDED_SECTIONS')
mylog('verbose', ['[Notification] Included sections: ', sections ])
if 'new_devices' in sections:
# Compose New Devices Section (no empty lines in SQL queries!)
sqlQuery = f"""SELECT eve_MAC as MAC, eve_DateTime as Datetime, devLastIP as IP, eve_EventType as "Event Type", devName as "Device name", devComments as Comments FROM Events_Devices
WHERE eve_PendingAlertEmail = 1
AND eve_EventType = 'New Device' {get_setting_value('NTFPRCS_new_dev_condition').replace('{s-quote}',"'")}
ORDER BY eve_DateTime"""
mylog('debug', ['[Notification] new_devices SQL query: ', sqlQuery ])
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)
json_new_devices_meta = {
"title": "🆕 New devices",
"columnNames": json_obj.columnNames
}
json_new_devices = json_obj.json["data"]
if 'down_devices' in sections:
# Compose Devices Down Section
# - select only Down Alerts with pending email of devices that didn't reconnect within the specified time window
sqlQuery = f"""
SELECT devName, eve_MAC, devVendor, eve_IP, eve_DateTime, eve_EventType
FROM Events_Devices AS down_events
WHERE eve_PendingAlertEmail = 1
AND down_events.eve_EventType = 'Device Down'
AND eve_DateTime < datetime('now', '-{get_setting_value('NTFPRCS_alert_down_time')} minutes', '{get_timezone_offset()}')
AND NOT EXISTS (
SELECT 1
FROM Events AS connected_events
WHERE connected_events.eve_MAC = down_events.eve_MAC
AND connected_events.eve_EventType = 'Connected'
AND connected_events.eve_DateTime > down_events.eve_DateTime
)
ORDER BY down_events.eve_DateTime;
"""
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)
json_down_devices_meta = {
"title": "🔴 Down devices",
"columnNames": json_obj.columnNames
}
json_down_devices = json_obj.json["data"]
mylog('debug', ['[Notification] json_down_devices: ', json.dumps(json_down_devices) ])
if 'down_reconnected' in sections:
# Compose Reconnected Down Section
# - select only Devices, that were previously down and now are Connected
sqlQuery = f"""
SELECT devName, eve_MAC, devVendor, eve_IP, eve_DateTime, eve_EventType
FROM Events_Devices AS reconnected_devices
WHERE reconnected_devices.eve_EventType = 'Down Reconnected'
AND reconnected_devices.eve_PendingAlertEmail = 1
ORDER BY reconnected_devices.eve_DateTime;
"""
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)
json_down_reconnected_meta = {
"title": "🔁 Reconnected down devices",
"columnNames": json_obj.columnNames
}
json_down_reconnected = json_obj.json["data"]
mylog('debug', ['[Notification] json_down_reconnected: ', json.dumps(json_down_reconnected) ])
if 'events' in sections:
# Compose Events Section (no empty lines in SQL queries!)
sqlQuery = f"""SELECT eve_MAC as MAC, eve_DateTime as Datetime, devLastIP as IP, eve_EventType as "Event Type", devName as "Device name", devComments as Comments FROM Events_Devices
WHERE eve_PendingAlertEmail = 1
AND eve_EventType IN ('Connected', 'Down Reconnected', 'Disconnected','IP Changed') {get_setting_value('NTFPRCS_event_condition').replace('{s-quote}',"'")}
ORDER BY eve_DateTime"""
mylog('debug', ['[Notification] events SQL query: ', sqlQuery ])
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)
json_events_meta = {
"title": "⚡ Events",
"columnNames": json_obj.columnNames
}
json_events = json_obj.json["data"]
if 'plugins' in sections:
# Compose Plugins Section
sqlQuery = """SELECT Plugin, Object_PrimaryId, Object_SecondaryId, DateTimeChanged, Watched_Value1, Watched_Value2, Watched_Value3, Watched_Value4, Status from Plugins_Events"""
# Get the events as JSON
json_obj = db.get_table_as_json(sqlQuery)
json_plugins_meta = {
"title": "🔌 Plugins",
"columnNames": json_obj.columnNames
}
json_plugins = json_obj.json["data"]
final_json = {
"new_devices": json_new_devices,
"new_devices_meta": json_new_devices_meta,
"down_devices": json_down_devices,
"down_devices_meta": json_down_devices_meta,
"down_reconnected": json_down_reconnected,
"down_reconnected_meta": json_down_reconnected_meta,
"events": json_events,
"events_meta": json_events_meta,
"plugins": json_plugins,
"plugins_meta": json_plugins_meta,
}
return final_json
#-------------------------------------------------------------------------------
def skip_repeated_notifications (db):
# Skip repeated notifications
# due strfime : Overflow --> use "strftime / 60"
mylog('verbose','[Skip Repeated Notifications] Skip Repeated')
db.sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1 AND eve_MAC IN
(
SELECT devMac FROM Devices
WHERE devLastNotification IS NOT NULL
AND devLastNotification <>""
AND (strftime("%s", devLastNotification)/60 +
devSkipRepeated * 60) >
(strftime('%s','now','localtime')/60 )
)
""" )
db.commitDB()