import json import sys import uuid import socket import subprocess 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, reportTemplatesPath from logger import mylog 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) ); """) 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('', newVersionText) mail_html = mail_html.replace('', newVersionText) # Report "REPORT_DATE" in Header & footer timeFormated = timeNowTZ().strftime('%Y-%m-%d %H:%M') mail_text = mail_text.replace('', timeFormated) mail_html = mail_html.replace('', timeFormated) # Report "SERVER_NAME" in Header & footer mail_text = mail_text.replace('', socket.gethostname()) mail_html = mail_html.replace('', 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('', VERSIONFILE) mail_html = mail_html.replace('', 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('', BUILDFILE) mail_html = mail_html.replace('', BUILDFILE) # Start generating the TEXT & HTML notification messages # new_devices # --- html, text = construct_notifications(self.JSON, "new_devices") mail_text = mail_text.replace('', text + '\n') mail_html = mail_html.replace('', html) mylog('verbose', ['[Notification] New Devices sections done.']) # down_devices # --- html, text = construct_notifications(self.JSON, "down_devices") mail_text = mail_text.replace('', text + '\n') mail_html = mail_html.replace('', html) mylog('verbose', ['[Notification] Down Devices sections done.']) # down_reconnected # --- html, text = construct_notifications(self.JSON, "down_reconnected") mail_text = mail_text.replace('', text + '\n') mail_html = mail_html.replace('', html) mylog('verbose', ['[Notification] Reconnected Down Devices sections done.']) # events # --- html, text = construct_notifications(self.JSON, "events") mail_text = mail_text.replace('', text + '\n') mail_html = mail_html.replace('', html) mylog('verbose', ['[Notification] Events sections done.']) # plugins # --- html, text = construct_notifications(self.JSON, "plugins") mail_text = mail_text.replace('', text + '\n') mail_html = mail_html.replace('', 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('
    ', '
      ' ).replace("null", "") # 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(""+thValue+"", ""+newThValue+"")