Files
NetAlertX/server/models/notification_instance.py
2025-11-09 17:03:25 +00:00

421 lines
14 KiB
Python
Executable File

import json
import uuid
import socket
import subprocess
from yattag import indent
from json2table import convert
# Register NetAlertX modules
import conf
from const import applicationPath, logPath, apiPath, reportTemplatesPath
from logger import mylog, Logger
from helper import (
generate_mac_links,
removeDuplicateNewLines,
timeNowTZ,
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)
);
""")
# Make sure log level is initialized correctly
Logger(get_setting_value("LOG_LEVEL"))
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
try:
VERSIONFILE = subprocess.check_output(
["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
try:
BUILDFILE = subprocess.check_output(
["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
# 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()
# Clear the Pending Email flag from all events and devices
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
minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0)
tz_offset = get_timezone_offset()
self.db.sql.execute(
"""
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
self.clearPluginEvents()
def clearPluginEvents(self):
# clear plugin events table
self.db.sql.execute("DELETE FROM Plugins_Events")
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>"
)