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

View File

@@ -0,0 +1,369 @@
import datetime
import os
import _io
import json
import sys
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
from messaging.in_app import write_notification
#-------------------------------------------------------------------------------
# Notification object handling
#-------------------------------------------------------------------------------
class NotificationInstance:
def __init__(self, db):
self.db = db
# Create Notifications table if missing
self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "Notifications" (
"Index" INTEGER,
"GUID" TEXT UNIQUE,
"DateTimeCreated" TEXT,
"DateTimePushed" TEXT,
"Status" TEXT,
"JSON" TEXT,
"Text" TEXT,
"HTML" TEXT,
"PublishedVia" TEXT,
"Extra" TEXT,
PRIMARY KEY("Index" AUTOINCREMENT)
);
""")
self.save()
# Method to override processing of notifications
def on_before_create(self, JSON, Extra):
return JSON, Extra
# Create a new DB entry if new notifications available, otherwise skip
def create(self, JSON, Extra=""):
JSON, Extra = self.on_before_create(JSON, Extra)
# Write output data for debug
write_file (logPath + '/report_output.json', json.dumps(JSON))
# Check if nothing to report, end
if JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == [] and JSON["down_reconnected"] == []:
self.HasNotifications = False
else:
self.HasNotifications = True
self.GUID = str(uuid.uuid4())
self.DateTimeCreated = timeNowTZ()
self.DateTimePushed = ""
self.Status = "new"
self.JSON = JSON
self.Text = ""
self.HTML = ""
self.PublishedVia = ""
self.Extra = Extra
if self.HasNotifications:
# if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html:
# mylog('debug', '[Notification] notiStruc is empty')
# else:
# mylog('debug', ['[Notification] notiStruc:', json.dumps(notiStruc.__dict__, indent=4)])
Text = ""
HTML = ""
template_file_path = reportTemplatesPath + 'report_template.html'
# Open text Template
mylog('verbose', ['[Notification] Open text Template'])
template_file = open(reportTemplatesPath + 'report_template.txt', 'r')
mail_text = template_file.read()
template_file.close()
# Open html Template
mylog('verbose', ['[Notification] Open html Template'])
template_file = open(template_file_path, 'r')
mail_html = template_file.read()
template_file.close()
# prepare new version text
newVersionText = ''
if conf.newVersionAvailable :
newVersionText = '🚀A new version is available.'
mail_text = mail_text.replace ('<NEW_VERSION>', newVersionText)
mail_html = mail_html.replace ('<NEW_VERSION>', newVersionText)
# Report "REPORT_DATE" in Header & footer
timeFormated = timeNowTZ().strftime ('%Y-%m-%d %H:%M')
mail_text = mail_text.replace ('<REPORT_DATE>', timeFormated)
mail_html = mail_html.replace ('<REPORT_DATE>', timeFormated)
# Report "SERVER_NAME" in Header & footer
mail_text = mail_text.replace ('<SERVER_NAME>', socket.gethostname() )
mail_html = mail_html.replace ('<SERVER_NAME>', socket.gethostname() )
# Report "VERSION" in Header & footer
VERSIONFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/version.php']).decode('utf-8')
mail_text = mail_text.replace ('<BUILD_VERSION>', VERSIONFILE)
mail_html = mail_html.replace ('<BUILD_VERSION>', VERSIONFILE)
# Report "BUILD" in Header & footer
BUILDFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/build.php']).decode('utf-8')
mail_text = mail_text.replace ('<BUILD_DATE>', BUILDFILE)
mail_html = mail_html.replace ('<BUILD_DATE>', BUILDFILE)
# Start generating the TEXT & HTML notification messages
# new_devices
# ---
html, text = construct_notifications(self.JSON, "new_devices")
mail_text = mail_text.replace ('<NEW_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] New Devices sections done.'])
# down_devices
# ---
html, text = construct_notifications(self.JSON, "down_devices")
mail_text = mail_text.replace ('<DOWN_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] Down Devices sections done.'])
# down_reconnected
# ---
html, text = construct_notifications(self.JSON, "down_reconnected")
mail_text = mail_text.replace ('<DOWN_RECONNECTED_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_RECONNECTED_TABLE>', html)
mylog('verbose', ['[Notification] Reconnected Down Devices sections done.'])
# events
# ---
html, text = construct_notifications(self.JSON, "events")
mail_text = mail_text.replace ('<EVENTS_TABLE>', text + '\n')
mail_html = mail_html.replace ('<EVENTS_TABLE>', html)
mylog('verbose', ['[Notification] Events sections done.'])
# plugins
# ---
html, text = construct_notifications(self.JSON, "plugins")
mail_text = mail_text.replace ('<PLUGINS_TABLE>', text + '\n')
mail_html = mail_html.replace ('<PLUGINS_TABLE>', html)
mylog('verbose', ['[Notification] Plugins sections done.'])
final_text = removeDuplicateNewLines(mail_text)
# Create clickable MAC links
mail_html = generate_mac_links (mail_html, conf.REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=')
final_html = indent(
mail_html,
indentation = ' ',
newline = '\r\n',
indent_text = True
)
send_api(self.JSON, final_text, final_html)
# Write output data for debug
write_file (logPath + '/report_output.txt', final_text)
write_file (logPath + '/report_output.html', final_html)
mylog('minimal', ['[Notification] Udating API files'])
self.Text = final_text
self.HTML = final_html
# Notify frontend
write_notification(f'Report:{self.GUID}', "alert", self.DateTimeCreated )
self.upsert()
return self
# Only updates the status
def updateStatus(self, newStatus):
self.Status = newStatus
self.upsert()
# Updates the Published properties
def updatePublishedVia(self, newPublishedVia):
self.PublishedVia = newPublishedVia
self.DateTimePushed = timeNowTZ()
self.upsert()
# create or update a notification
def upsert(self):
self.db.sql.execute("""
INSERT OR REPLACE INTO Notifications (GUID, DateTimeCreated, DateTimePushed, Status, JSON, Text, HTML, PublishedVia, Extra)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (self.GUID, self.DateTimeCreated, self.DateTimePushed, self.Status, json.dumps(self.JSON), self.Text, self.HTML, self.PublishedVia, self.Extra))
self.save()
# Remove notification object by GUID
def remove(self, GUID):
# Execute an SQL query to delete the notification with the specified GUID
self.db.sql.execute("""
DELETE FROM Notifications
WHERE GUID = ?
""", (GUID,))
self.save()
# Get all with the "new" status
def getNew(self):
self.db.sql.execute("""
SELECT * FROM Notifications
WHERE Status = "new"
""")
return self.db.sql.fetchall()
# Set all to "processed" status
def setAllProcessed(self):
# Execute an SQL query to update the status of all notifications
self.db.sql.execute("""
UPDATE Notifications
SET Status = "processed"
WHERE Status = "new"
""")
self.save()
def clearPendingEmailFlag(self):
# Clean Pending Alert Events
self.db.sql.execute ("""UPDATE Devices SET devLastNotification = ?
WHERE devMac IN (
SELECT eve_MAC FROM Events
WHERE eve_PendingAlertEmail = 1
)
""", (timeNowTZ(),) )
self.db.sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType !='Device Down' """)
# Clear down events flag after the reporting window passed
self.db.sql.execute (f"""UPDATE Events SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType =='Device Down'
AND eve_DateTime < datetime('now', '-{get_setting_value('NTFPRCS_alert_down_time')} minutes', '{get_timezone_offset()}')
""")
# clear plugin events
self.db.sql.execute ("DELETE FROM Plugins_Events")
# DEBUG - print number of rows updated
mylog('minimal', ['[Notification] Notifications changes: ', self.db.sql.rowcount])
self.save()
def save(self):
# Commit changes
self.db.commitDB()
#-------------------------------------------------------------------------------
# Reporting
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def construct_notifications(JSON, section):
jsn = JSON[section]
# Return if empty
if jsn == []:
return '',''
tableTitle = JSON[section + "_meta"]["title"]
headers = JSON[section + "_meta"]["columnNames"]
html = ''
text = ''
table_attributes = {"style" : "border-collapse: collapse; font-size: 12px; color:#70707", "width" : "100%", "cellspacing" : 0, "cellpadding" : "3px", "bordercolor" : "#C0C0C0", "border":"1"}
headerProps = "width='120px' style='color:white; font-size: 16px;' bgcolor='#64a0d6' "
thProps = "width='120px' style='color:#F0F0F0' bgcolor='#64a0d6' "
build_direction = "TOP_TO_BOTTOM"
text_line = '{}\t{}\n'
if len(jsn) > 0:
text = tableTitle + "\n---------\n"
# Convert a JSON into an HTML table
html = convert({"data": jsn}, build_direction=build_direction, table_attributes=table_attributes)
# Cleanup the generated HTML table notification
html = format_table(html, "data", headerProps, tableTitle).replace('<ul>','<ul style="list-style:none;padding-left:0">').replace("<td>null</td>", "<td></td>")
# prepare text-only message
for device in jsn:
for header in headers:
padding = ""
if len(header) < 4:
padding = "\t"
text += text_line.format ( header + ': ' + padding, device[header])
text += '\n'
# Format HTML table headers
for header in headers:
html = format_table(html, header, thProps)
return html, text
#-------------------------------------------------------------------------------
def send_api(json_final, mail_text, mail_html):
mylog('verbose', ['[Send API] Updating notification_* files in ', apiPath])
write_file(apiPath + 'notification_text.txt' , mail_text)
write_file(apiPath + 'notification_text.html' , mail_html)
write_file(apiPath + 'notification_json_final.json' , json.dumps(json_final))
#-------------------------------------------------------------------------------
# Replacing table headers
def format_table (html, thValue, props, newThValue = ''):
if newThValue == '':
newThValue = thValue
return html.replace("<th>"+thValue+"</th>", "<th "+props+" >"+newThValue+"</th>" )

View File

@@ -0,0 +1,85 @@
import os
import sys
# Register NetAlertX directories
INSTALL_PATH="/app"
sys.path.extend([f"{INSTALL_PATH}/server"])
# Register NetAlertX modules
from const import pluginsPath, logPath, applicationPath, reportTemplatesPath
from logger import mylog
class UserEventsQueueInstance:
"""
Handles the execution queue log file, allowing reading, writing,
and removing processed events.
"""
def __init__(self):
self.log_path = logPath
self.log_file = os.path.join(self.log_path, "execution_queue.log")
def has_update_devices(self):
lines = self.read_log()
for line in lines:
if 'update_api|devices' in line:
return True
return False
def read_log(self):
"""
Reads the log file and returns all lines.
Returns an empty list if the file doesn't exist.
"""
if not os.path.exists(self.log_file):
mylog('none', ['[UserEventsQueueInstance] Log file not found: ', self.log_file])
return [] # No log file, return empty list
with open(self.log_file, "r") as file:
return file.readlines()
def write_log(self, lines):
"""
Overwrites the log file with the provided lines.
"""
with open(self.log_file, "w") as file:
file.writelines(lines)
def finalize_event(self, event):
"""
Removes the first occurrence of the specified event from the log file.
Retains all other lines untouched.
Returns:
bool: True if the event was found and removed, False otherwise.
"""
if not os.path.exists(self.log_file):
return False # No log file to process
updated_lines = []
removed = False
# Process the log file line by line
with open(self.log_file, "r") as file:
for line in file:
columns = line.strip().split('|')[2:4] # Extract event and param columns
if len(columns) == 2:
event_name, _ = columns
if event_name == event and not removed:
# Skip this line (remove the processed event)
removed = True
continue
updated_lines.append(line)
# Write back the remaining lines
self.write_log(updated_lines)
mylog('minimal', ['[UserEventsQueueInstance] Processed event: ', event])
return removed