mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 01:26:11 -08:00
Auto delete in-app notifications #1052
This commit is contained in:
369
server/models/notification_instance.py
Executable file
369
server/models/notification_instance.py
Executable 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>" )
|
||||
|
||||
|
||||
|
||||
85
server/models/user_events_queue_instance.py
Executable file
85
server/models/user_events_queue_instance.py
Executable 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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user