This commit is contained in:
jokob-sk
2025-09-18 16:00:11 +10:00
2 changed files with 146 additions and 112 deletions

View File

@@ -186,9 +186,15 @@ def main ():
pm.run_plugin_scripts('on_notification') pm.run_plugin_scripts('on_notification')
notification.setAllProcessed() notification.setAllProcessed()
# clear pending email flag
# and the plugin events
notification.clearPendingEmailFlag() notification.clearPendingEmailFlag()
else: else:
# If there are no notifications to process,
# we still need to clear all plugin events
notification.clearPluginEvents()
mylog('verbose', ['[Notification] No changes to report']) mylog('verbose', ['[Notification] No changes to report'])
# Commit SQL # Commit SQL

View File

@@ -1,35 +1,36 @@
import datetime
import os
import _io
import json import json
import sys import sys
import uuid import uuid
import socket import socket
import subprocess import subprocess
import requests
from yattag import indent from yattag import indent
from json2table import convert from json2table import convert
# Register NetAlertX directories # Register NetAlertX directories
INSTALL_PATH="/app" INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/server"])
# Register NetAlertX modules # Register NetAlertX modules
import conf import conf
from const import applicationPath, logPath, apiPath, confFileName, reportTemplatesPath from const import applicationPath, logPath, apiPath, reportTemplatesPath
from logger import logResult, mylog from logger import mylog
from helper import generate_mac_links, removeDuplicateNewLines, timeNowTZ, get_file_content, write_file, get_setting_value, get_timezone_offset from helper import generate_mac_links, \
removeDuplicateNewLines, \
timeNowTZ, \
write_file, \
get_setting_value, \
get_timezone_offset
from messaging.in_app import write_notification from messaging.in_app import write_notification
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Notification object handling # Notification object handling
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class NotificationInstance: class NotificationInstance:
def __init__(self, db): def __init__(self, db):
self.db = db self.db = db
# Create Notifications table if missing # Create Notifications table if missing
self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "Notifications" ( self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "Notifications" (
"Index" INTEGER, "Index" INTEGER,
"GUID" TEXT UNIQUE, "GUID" TEXT UNIQUE,
@@ -48,24 +49,23 @@ class NotificationInstance:
self.save() self.save()
# Method to override processing of notifications # Method to override processing of notifications
def on_before_create(self, JSON, Extra): def on_before_create(self, JSON, Extra):
return JSON, Extra return JSON, Extra
# Create a new DB entry if new notifications available, otherwise skip # Create a new DB entry if new notifications available, otherwise skip
def create(self, JSON, Extra=""): def create(self, JSON, Extra=""):
JSON, Extra = self.on_before_create(JSON, Extra) JSON, Extra = self.on_before_create(JSON, Extra)
# Write output data for debug # Write output data for debug
write_file (logPath + '/report_output.json', json.dumps(JSON)) write_file(logPath + '/report_output.json', json.dumps(JSON))
# Check if nothing to report, end # Check if nothing to report, end
if JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == [] and JSON["down_reconnected"] == []: if JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == [] and JSON["down_reconnected"] == []:
self.HasNotifications = False self.HasNotifications = False
else: else:
self.HasNotifications = True self.HasNotifications = True
self.GUID = str(uuid.uuid4()) self.GUID = str(uuid.uuid4())
self.DateTimeCreated = timeNowTZ() self.DateTimeCreated = timeNowTZ()
@@ -78,17 +78,14 @@ class NotificationInstance:
self.Extra = Extra self.Extra = Extra
if self.HasNotifications: if self.HasNotifications:
# if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html: # if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html:
# mylog('debug', '[Notification] notiStruc is empty') # mylog('debug', '[Notification] notiStruc is empty')
# else: # else:
# mylog('debug', ['[Notification] notiStruc:', json.dumps(notiStruc.__dict__, indent=4)]) # mylog('debug', ['[Notification] notiStruc:', json.dumps(notiStruc.__dict__, indent=4)])
Text = ""
HTML = ""
template_file_path = reportTemplatesPath + 'report_template.html'
Text = ""
HTML = ""
template_file_path = reportTemplatesPath + 'report_template.html'
# Open text Template # Open text Template
mylog('verbose', ['[Notification] Open text Template']) mylog('verbose', ['[Notification] Open text Template'])
@@ -99,44 +96,60 @@ class NotificationInstance:
# Open html Template # Open html Template
mylog('verbose', ['[Notification] Open html Template']) mylog('verbose', ['[Notification] Open html Template'])
template_file = open(template_file_path, 'r') template_file = open(template_file_path, 'r')
mail_html = template_file.read() mail_html = template_file.read()
template_file.close() template_file.close()
# prepare new version text # prepare new version text
newVersionText = '' newVersionText = ''
if conf.newVersionAvailable : if conf.newVersionAvailable:
newVersionText = '🚀A new version is available.' newVersionText = '🚀A new version is available.'
mail_text = mail_text.replace ('<NEW_VERSION>', newVersionText) mail_text = mail_text.replace('<NEW_VERSION>', newVersionText)
mail_html = mail_html.replace ('<NEW_VERSION>', newVersionText) mail_html = mail_html.replace('<NEW_VERSION>', newVersionText)
# Report "REPORT_DATE" in Header & footer # Report "REPORT_DATE" in Header & footer
timeFormated = timeNowTZ().strftime ('%Y-%m-%d %H:%M') timeFormated = timeNowTZ().strftime('%Y-%m-%d %H:%M')
mail_text = mail_text.replace ('<REPORT_DATE>', timeFormated) mail_text = mail_text.replace('<REPORT_DATE>', timeFormated)
mail_html = mail_html.replace ('<REPORT_DATE>', timeFormated) mail_html = mail_html.replace('<REPORT_DATE>', timeFormated)
# Report "SERVER_NAME" in Header & footer # Report "SERVER_NAME" in Header & footer
mail_text = mail_text.replace ('<SERVER_NAME>', socket.gethostname() ) mail_text = mail_text.replace('<SERVER_NAME>', socket.gethostname())
mail_html = mail_html.replace ('<SERVER_NAME>', socket.gethostname() ) mail_html = mail_html.replace('<SERVER_NAME>', socket.gethostname())
# Report "VERSION" in Header & footer # Report "VERSION" in Header & footer
VERSIONFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/version.php']).decode('utf-8') try:
mail_text = mail_text.replace ('<BUILD_VERSION>', VERSIONFILE) VERSIONFILE = subprocess.check_output(
mail_html = mail_html.replace ('<BUILD_VERSION>', VERSIONFILE) ['php', applicationPath + '/front/php/templates/version.php'],
timeout=5
).decode('utf-8')
except Exception as e:
mylog('debug', [f'[Notification] Unable to read version.php: {e}'])
VERSIONFILE = 'unknown'
mail_text = mail_text.replace('<BUILD_VERSION>', VERSIONFILE)
mail_html = mail_html.replace('<BUILD_VERSION>', VERSIONFILE)
# Report "BUILD" in Header & footer # Report "BUILD" in Header & footer
BUILDFILE = subprocess.check_output(['php', applicationPath + '/front/php/templates/build.php']).decode('utf-8') try:
mail_text = mail_text.replace ('<BUILD_DATE>', BUILDFILE) BUILDFILE = subprocess.check_output(
mail_html = mail_html.replace ('<BUILD_DATE>', BUILDFILE) ['php', applicationPath + '/front/php/templates/build.php'],
timeout=5
).decode('utf-8')
except Exception as e:
mylog('debug', [f'[Notification] Unable to read build.php: {e}'])
BUILDFILE = 'unknown'
mail_text = mail_text.replace('<BUILD_DATE>', BUILDFILE)
mail_html = mail_html.replace('<BUILD_DATE>', BUILDFILE)
# Start generating the TEXT & HTML notification messages # Start generating the TEXT & HTML notification messages
# new_devices # new_devices
# --- # ---
html, text = construct_notifications(self.JSON, "new_devices") html, text = construct_notifications(self.JSON, "new_devices")
mail_text = mail_text.replace ('<NEW_DEVICES_TABLE>', text + '\n') mail_text = mail_text.replace('<NEW_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', html) mail_html = mail_html.replace('<NEW_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] New Devices sections done.']) mylog('verbose', ['[Notification] New Devices sections done.'])
# down_devices # down_devices
@@ -144,56 +157,56 @@ class NotificationInstance:
html, text = construct_notifications(self.JSON, "down_devices") html, text = construct_notifications(self.JSON, "down_devices")
mail_text = mail_text.replace ('<DOWN_DEVICES_TABLE>', text + '\n') mail_text = mail_text.replace('<DOWN_DEVICES_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', html) mail_html = mail_html.replace('<DOWN_DEVICES_TABLE>', html)
mylog('verbose', ['[Notification] Down Devices sections done.']) mylog('verbose', ['[Notification] Down Devices sections done.'])
# down_reconnected # down_reconnected
# --- # ---
html, text = construct_notifications(self.JSON, "down_reconnected") html, text = construct_notifications(self.JSON, "down_reconnected")
mail_text = mail_text.replace ('<DOWN_RECONNECTED_TABLE>', text + '\n') mail_text = mail_text.replace('<DOWN_RECONNECTED_TABLE>', text + '\n')
mail_html = mail_html.replace ('<DOWN_RECONNECTED_TABLE>', html) mail_html = mail_html.replace('<DOWN_RECONNECTED_TABLE>', html)
mylog('verbose', ['[Notification] Reconnected Down Devices sections done.']) mylog('verbose', ['[Notification] Reconnected Down Devices sections done.'])
# events # events
# --- # ---
html, text = construct_notifications(self.JSON, "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) mail_text = mail_text.replace('<EVENTS_TABLE>', text + '\n')
mylog('verbose', ['[Notification] Events sections done.']) mail_html = mail_html.replace('<EVENTS_TABLE>', html)
mylog('verbose', ['[Notification] Events sections done.'])
# plugins # plugins
# --- # ---
html, text = construct_notifications(self.JSON, "plugins") html, text = construct_notifications(self.JSON, "plugins")
mail_text = mail_text.replace ('<PLUGINS_TABLE>', text + '\n') mail_text = mail_text.replace('<PLUGINS_TABLE>', text + '\n')
mail_html = mail_html.replace ('<PLUGINS_TABLE>', html) mail_html = mail_html.replace('<PLUGINS_TABLE>', html)
mylog('verbose', ['[Notification] Plugins sections done.']) mylog('verbose', ['[Notification] Plugins sections done.'])
final_text = removeDuplicateNewLines(mail_text) final_text = removeDuplicateNewLines(mail_text)
# Create clickable MAC links # Create clickable MAC links
mail_html = generate_mac_links (mail_html, conf.REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=') mail_html = generate_mac_links(mail_html, conf.REPORT_DASHBOARD_URL + '/deviceDetails.php?mac=')
final_html = indent( final_html = indent(
mail_html, mail_html,
indentation = ' ', indentation=' ',
newline = '\r\n', newline='\r\n',
indent_text = True indent_text=True
) )
send_api(self.JSON, final_text, final_html) send_api(self.JSON, final_text, final_html)
# Write output data for debug # Write output data for debug
write_file (logPath + '/report_output.txt', final_text) write_file(logPath + '/report_output.txt', final_text)
write_file (logPath + '/report_output.html', final_html) write_file(logPath + '/report_output.html', final_html)
mylog('minimal', ['[Notification] Udating API files']) mylog('minimal', ['[Notification] Udating API files'])
@@ -201,10 +214,10 @@ class NotificationInstance:
self.HTML = final_html self.HTML = final_html
# Notify frontend # Notify frontend
write_notification(f'Report:{self.GUID}', "alert", self.DateTimeCreated ) write_notification(f'Report:{self.GUID}', "alert", self.DateTimeCreated)
self.upsert() self.upsert()
return self return self
# Only updates the status # Only updates the status
@@ -216,9 +229,9 @@ class NotificationInstance:
def updatePublishedVia(self, newPublishedVia): def updatePublishedVia(self, newPublishedVia):
self.PublishedVia = newPublishedVia self.PublishedVia = newPublishedVia
self.DateTimePushed = timeNowTZ() self.DateTimePushed = timeNowTZ()
self.upsert() self.upsert()
# create or update a notification # create or update a notification
def upsert(self): def upsert(self):
self.db.sql.execute(""" self.db.sql.execute("""
INSERT OR REPLACE INTO Notifications (GUID, DateTimeCreated, DateTimePushed, Status, JSON, Text, HTML, PublishedVia, Extra) INSERT OR REPLACE INTO Notifications (GUID, DateTimeCreated, DateTimePushed, Status, JSON, Text, HTML, PublishedVia, Extra)
@@ -256,57 +269,63 @@ class NotificationInstance:
self.save() self.save()
# Clear the Pending Email flag from all events and devices
def clearPendingEmailFlag(self): def clearPendingEmailFlag(self):
# Clean Pending Alert Events # Clean Pending Alert Events
self.db.sql.execute ("""UPDATE Devices SET devLastNotification = ? self.db.sql.execute("""
WHERE devMac IN ( UPDATE Devices SET devLastNotification = ?
SELECT eve_MAC FROM Events WHERE devMac IN (
WHERE eve_PendingAlertEmail = 1 SELECT eve_MAC FROM Events
) WHERE eve_PendingAlertEmail = 1
""", (timeNowTZ(),) ) )
""", (timeNowTZ(),))
self.db.sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 self.db.sql.execute("""
WHERE eve_PendingAlertEmail = 1 UPDATE Events SET eve_PendingAlertEmail = 0
AND eve_EventType !='Device Down' """) WHERE eve_PendingAlertEmail = 1
AND eve_EventType !='Device Down' """)
# Clear down events flag after the reporting window passed # Clear down events flag after the reporting window passed
self.db.sql.execute (f"""UPDATE Events SET eve_PendingAlertEmail = 0 minutes = int(get_setting_value('NTFPRCS_alert_down_time') or 0)
WHERE eve_PendingAlertEmail = 1 tz_offset = get_timezone_offset()
AND eve_EventType =='Device Down' self.db.sql.execute("""
AND eve_DateTime < datetime('now', '-{get_setting_value('NTFPRCS_alert_down_time')} minutes', '{get_timezone_offset()}') UPDATE Events
""") SET eve_PendingAlertEmail = 0
WHERE eve_PendingAlertEmail = 1
AND eve_EventType = 'Device Down'
AND eve_DateTime < datetime('now', ?, ?)
""", (f"-{minutes} minutes", tz_offset))
mylog('minimal', ['[Notification] Notifications changes: ',
self.db.sql.rowcount])
# clear plugin events # clear plugin events
self.db.sql.execute ("DELETE FROM Plugins_Events") self.clearPluginEvents()
# DEBUG - print number of rows updated
mylog('minimal', ['[Notification] Notifications changes: ', self.db.sql.rowcount])
def clearPluginEvents(self):
# clear plugin events table
self.db.sql.execute("DELETE FROM Plugins_Events")
self.save() self.save()
def save(self): def save(self):
# Commit changes # Commit changes
self.db.commitDB() self.db.commitDB()
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# Reporting # Reporting
#------------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# ------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
def construct_notifications(JSON, section): def construct_notifications(JSON, section):
jsn = JSON[section] jsn = JSON[section]
# Return if empty # Return if empty
if jsn == []: if jsn == []:
return '','' return '', ''
tableTitle = JSON[section + "_meta"]["title"] tableTitle = JSON[section + "_meta"]["title"]
headers = JSON[section + "_meta"]["columnNames"] headers = JSON[section + "_meta"]["columnNames"]
@@ -314,22 +333,34 @@ def construct_notifications(JSON, section):
html = '' html = ''
text = '' text = ''
table_attributes = {"style" : "border-collapse: collapse; font-size: 12px; color:#70707", "width" : "100%", "cellspacing" : 0, "cellpadding" : "3px", "bordercolor" : "#C0C0C0", "border":"1"} 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' " headerProps = "width='120px' style='color:white; font-size: 16px;' bgcolor='#64a0d6' "
thProps = "width='120px' style='color:#F0F0F0' bgcolor='#64a0d6' " thProps = "width='120px' style='color:#F0F0F0' bgcolor='#64a0d6' "
build_direction = "TOP_TO_BOTTOM" build_direction = "TOP_TO_BOTTOM"
text_line = '{}\t{}\n' text_line = '{}\t{}\n'
if len(jsn) > 0: if len(jsn) > 0:
text = tableTitle + "\n---------\n" text = tableTitle + "\n---------\n"
# Convert a JSON into an HTML table # Convert a JSON into an HTML table
html = convert({"data": jsn}, build_direction=build_direction, table_attributes=table_attributes) html = convert({"data": jsn}, build_direction=build_direction, table_attributes=table_attributes)
# Cleanup the generated HTML table notification # 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>") 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 # prepare text-only message
for device in jsn: for device in jsn:
@@ -337,7 +368,7 @@ def construct_notifications(JSON, section):
padding = "" padding = ""
if len(header) < 4: if len(header) < 4:
padding = "\t" padding = "\t"
text += text_line.format ( header + ': ' + padding, device[header]) text += text_line.format(header + ': ' + padding, device[header])
text += '\n' text += '\n'
# Format HTML table headers # Format HTML table headers
@@ -346,24 +377,21 @@ def construct_notifications(JSON, section):
return html, text return html, text
#-------------------------------------------------------------------------------
# -----------------------------------------------------------------------------
def send_api(json_final, mail_text, mail_html): def send_api(json_final, mail_text, mail_html):
mylog('verbose', ['[Send API] Updating notification_* files in ', apiPath]) mylog('verbose', ['[Send API] Updating notification_* files in ', apiPath])
write_file(apiPath + 'notification_text.txt' , mail_text) write_file(apiPath + 'notification_text.txt', mail_text)
write_file(apiPath + 'notification_text.html' , mail_html) write_file(apiPath + 'notification_text.html', mail_html)
write_file(apiPath + 'notification_json_final.json' , json.dumps(json_final)) write_file(apiPath + 'notification_json_final.json', json.dumps(json_final))
# -----------------------------------------------------------------------------
#-------------------------------------------------------------------------------
# Replacing table headers # Replacing table headers
def format_table (html, thValue, props, newThValue = ''): def format_table(html, thValue, props, newThValue=''):
if newThValue == '': if newThValue == '':
newThValue = thValue newThValue = thValue
return html.replace("<th>"+thValue+"</th>", "<th "+props+" >"+newThValue+"</th>" ) return html.replace("<th>"+thValue+"</th>", "<th "+props+" >"+newThValue+"</th>")