import json import uuid import socket from yattag import indent from json2table import convert # Register NetAlertX modules import conf from const import logPath, apiPath, reportTemplatesPath from logger import mylog, Logger from helper import ( generate_mac_links, removeDuplicateNewLines, write_file, get_setting_value, getBuildTimeStampAndVersion, ) from messaging.in_app import write_notification from utils.datetime_utils import timeNowDB, get_timezone_offset # ----------------------------------------------------------------------------- # Notification object handling # ----------------------------------------------------------------------------- class NotificationInstance: def __init__(self, db): self.db = db self.serverUrl = get_setting_value("REPORT_DASHBOARD_URL") # 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 = timeNowDB() 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)]) 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 = timeNowDB() 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 buildTimestamp, newBuildVersion = getBuildTimeStampAndVersion() mail_text = mail_text.replace("BUILD_VERSION", newBuildVersion) mail_html = mail_html.replace("BUILD_VERSION", newBuildVersion) # Report "BUILD" in Header & footer mail_text = mail_text.replace("BUILD_DATE", str(buildTimestamp)) mail_html = mail_html.replace("BUILD_DATE", str(buildTimestamp)) # Report "REPORT_DASHBOARD_URL" in footer mail_text = mail_text.replace("REPORT_DASHBOARD_URL", self.serverUrl) mail_html = mail_html.replace("REPORT_DASHBOARD_URL", self.serverUrl) # 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 = timeNowDB() 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 ) """, (timeNowDB(),)) 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("