From 16de2614772589c0abfdef32b36164f9856f24a8 Mon Sep 17 00:00:00 2001 From: Jokob-sk Date: Fri, 6 Oct 2023 08:10:18 +1100 Subject: [PATCH] Notification rework v0.1 --- front/plugins/_publisher_apprise/apprise.py | 101 ++++++++++++++++++++ front/plugins/_publisher_apprise/script.py | 61 ------------ pialert/__main__.py | 3 +- pialert/database.py | 17 +--- pialert/helper.py | 23 ++++- pialert/plugin.py | 82 ++++++++++++++++ pialert/plugin_utils.py | 80 ---------------- pialert/reporting.py | 70 ++++++++++++-- 8 files changed, 269 insertions(+), 168 deletions(-) create mode 100755 front/plugins/_publisher_apprise/apprise.py delete mode 100755 front/plugins/_publisher_apprise/script.py diff --git a/front/plugins/_publisher_apprise/apprise.py b/front/plugins/_publisher_apprise/apprise.py new file mode 100755 index 00000000..ac2fc7d0 --- /dev/null +++ b/front/plugins/_publisher_apprise/apprise.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# Based on the work of https://github.com/leiweibau/Pi.Alert + +import json +import subprocess +import argparse +import os +import pathlib +import sys +from datetime import datetime + +# Replace these paths with the actual paths to your Pi.Alert directories +sys.path.extend(["/home/pi/pialert/front/plugins", "/home/pi/pialert/pialert"]) + +import conf +from plugin_helper import Plugin_Objects +from logger import mylog, append_line_to_file +from helper import timeNowTZ, noti_struc + + +CUR_PATH = str(pathlib.Path(__file__).parent.resolve()) +RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log') + +def main(): + + mylog('verbose', ['[APPRISE](publisher) In script']) + + parser = argparse.ArgumentParser(description='APPRISE publisher Plugin') + values = parser.parse_args() + + plugin_objects = Plugin_Objects(RESULT_FILE) + + speedtest_result = send() + + plugin_objects.add_object( + primaryId = 'APPRISE', + secondaryId = timeNowTZ(), + watched1 = speedtest_result['download_speed'], + watched2 = speedtest_result['upload_speed'], + watched3 = 'null', + watched4 = 'null', + extra = 'null', + foreignKey = 'null' + ) + + plugin_objects.write_result_file() + +#------------------------------------------------------------------------------- +def check_config(): + if conf.APPRISE_URL == '' or conf.APPRISE_HOST == '': + mylog('none', ['[Check Config] Error: Apprise service not set up correctly. Check your pialert.conf APPRISE_* variables.']) + return False + else: + return True + +#------------------------------------------------------------------------------- +def send(msg: noti_struc): + html = msg.html + text = msg.text + + payloadData = '' + + # limit = 1024 * 1024 # 1MB limit (1024 bytes * 1024 bytes = 1MB) + limit = conf.APPRISE_SIZE + + # truncate size + if conf.APPRISE_PAYLOAD == 'html': + if len(msg.html) > limit: + payloadData = msg.html[:limit] + "

(text was truncated)

" + else: + payloadData = msg.html + if conf.APPRISE_PAYLOAD == 'text': + if len(msg.text) > limit: + payloadData = msg.text[:limit] + " (text was truncated)" + else: + payloadData = msg.text + + # Define Apprise compatible payload (https://github.com/caronc/apprise-api#stateless-solution) + + _json_payload = { + "urls": conf.APPRISE_URL, + "title": "Pi.Alert Notifications", + "format": conf.APPRISE_PAYLOAD, + "body": payloadData + } + + try: + # try runnning a subprocess + p = subprocess.Popen(["curl","-i","-X", "POST" ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), conf.APPRISE_HOST], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + stdout, stderr = p.communicate() + # write stdout and stderr into .log files for debugging if needed + + + # Log the stdout and stderr + mylog('debug', [stdout, stderr]) # TO-DO should be changed to mylog + except subprocess.CalledProcessError as e: + # An error occurred, handle it + mylog('none', [e.output]) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/front/plugins/_publisher_apprise/script.py b/front/plugins/_publisher_apprise/script.py deleted file mode 100755 index 019d34dc..00000000 --- a/front/plugins/_publisher_apprise/script.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python -# Based on the work of https://github.com/leiweibau/Pi.Alert - -import argparse -import os -import pathlib -import sys -from datetime import datetime -import speedtest - -# Replace these paths with the actual paths to your Pi.Alert directories -sys.path.extend(["/home/pi/pialert/front/plugins", "/home/pi/pialert/pialert"]) - -from plugin_helper import Plugin_Objects -from logger import mylog, append_line_to_file -from helper import timeNowTZ - -CUR_PATH = str(pathlib.Path(__file__).parent.resolve()) -RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log') - -def main(): - - mylog('verbose', ['[APPRISE] In script']) - - parser = argparse.ArgumentParser(description='Speedtest Plugin for Pi.Alert') - values = parser.parse_args() - - plugin_objects = Plugin_Objects(RESULT_FILE) - speedtest_result = run_speedtest() - plugin_objects.add_object( - primaryId = 'Speedtest', - secondaryId = timeNowTZ(), - watched1 = speedtest_result['download_speed'], - watched2 = speedtest_result['upload_speed'], - watched3 = 'null', - watched4 = 'null', - extra = 'null', - foreignKey = 'null' - ) - plugin_objects.write_result_file() - -def run_speedtest(): - try: - st = speedtest.Speedtest() - st.get_best_server() - download_speed = round(st.download() / 10**6, 2) # Convert to Mbps - upload_speed = round(st.upload() / 10**6, 2) # Convert to Mbps - - return { - 'download_speed': download_speed, - 'upload_speed': upload_speed, - } - except Exception as e: - mylog('verbose', [f"Error running speedtest: {str(e)}"]) - return { - 'download_speed': -1, - 'upload_speed': -1, - } - -if __name__ == '__main__': - sys.exit(main()) diff --git a/pialert/__main__.py b/pialert/__main__.py index 679d5df5..ae59a7d7 100755 --- a/pialert/__main__.py +++ b/pialert/__main__.py @@ -30,8 +30,7 @@ from networkscan import process_scan from initialise import importConfigs from database import DB, get_all_devices from reporting import send_notifications -from plugin_utils import check_and_run_user_event -from plugin import run_plugin_scripts +from plugin import run_plugin_scripts, check_and_run_user_event #=============================================================================== diff --git a/pialert/database.py b/pialert/database.py index 6953c2b4..eab5fa05 100755 --- a/pialert/database.py +++ b/pialert/database.py @@ -214,7 +214,7 @@ class DB(): initOrSetParam(self, 'Back_App_State','Initializing') # ------------------------------------------------------------------------- - # Parameters table setup DEPRECATED after 1/1/2024 + # Nmap_Scan table setup DEPRECATED after 1/1/2024 # ------------------------------------------------------------------------- # indicates, if Nmap_Scan table is available @@ -260,21 +260,6 @@ class DB(): self.sql.execute("DROP TABLE Nmap_Scan;") nmapScanMissing = True - # if nmapScanMissing: - # mylog('verbose', ["[upgradeDB] Re-creating Nmap_Scan table"]) - # self.sql.execute(""" - # CREATE TABLE "Nmap_Scan" ( - # "Index" INTEGER, - # "MAC" TEXT, - # "Port" TEXT, - # "Time" TEXT, - # "State" TEXT, - # "Service" TEXT, - # "Extra" TEXT, - # PRIMARY KEY("Index" AUTOINCREMENT) - # ); - # """) - # ------------------------------------------------------------------------- # Plugins tables setup # ------------------------------------------------------------------------- diff --git a/pialert/helper.py b/pialert/helper.py index 3da5aada..35414f8a 100755 --- a/pialert/helper.py +++ b/pialert/helper.py @@ -528,6 +528,15 @@ class AppStateEncoder(json.JSONEncoder): return obj.__dict__ return super().default(obj) +#------------------------------------------------------------------------------- +# Checks if the object has a __dict__ attribute. If it does, it assumes that it's an instance of a class and serializes its attributes dynamically. +class NotiStrucEncoder(json.JSONEncoder): + def default(self, obj): + if hasattr(obj, '__dict__'): + # If the object has a '__dict__', assume it's an instance of a class + return obj.__dict__ + return super().default(obj) + #------------------------------------------------------------------------------- # Creates a JSON object from a DB row def row_to_json(names, row): @@ -611,7 +620,17 @@ class json_struc: #------------------------------------------------------------------------------- class noti_struc: - def __init__(self, json, text, html): + def __init__(self, json, text, html, notificationType): self.json = json self.text = text - self.html = html \ No newline at end of file + self.html = html + + jsonFile = apiPath + f'/notifications_{notificationType}.json' + + mylog('debug', [f"[Notifications] Writing {jsonFile}"]) + + if notificationType != '': + + # Update .json file + with open(jsonFile, 'w') as jsonFile: + json.dump(self, jsonFile, cls=NotiStrucEncoder, indent=4) diff --git a/pialert/plugin.py b/pialert/plugin.py index bcfb971d..6dea2247 100755 --- a/pialert/plugin.py +++ b/pialert/plugin.py @@ -727,3 +727,85 @@ class plugin_object_class: self.watchedHash = str(hash(tmp)) + +#=============================================================================== +# Handling of user initialized front-end events +#=============================================================================== + +#------------------------------------------------------------------------------- +def check_and_run_user_event(db, pluginsState): + + sql = db.sql # TO-DO + sql.execute(""" select * from Parameters where par_ID = "Front_Event" """) + rows = sql.fetchall() + + event, param = ['',''] + if len(rows) > 0 and rows[0]['par_Value'] != 'finished': + keyValue = rows[0]['par_Value'].split('|') + + if len(keyValue) == 2: + event = keyValue[0] + param = keyValue[1] + else: + return pluginsState + + if event == 'test': + handle_test(param) + if event == 'run': + pluginsState = handle_run(param, db, pluginsState) + + # clear event execution flag + sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'") + + # commit to DB + db.commitDB() + + return pluginsState + +#------------------------------------------------------------------------------- +def handle_run(runType, db, pluginsState): + + mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType]) + + # run the plugin to run + for plugin in conf.plugins: + if plugin["unique_prefix"] == runType: + pluginsState = execute_plugin(db, plugin, pluginsState) + + mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType]) + return pluginsState + + + +#------------------------------------------------------------------------------- +def handle_test(testType): + + mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType]) + + # Open text sample + sample_txt = get_file_content(pialertPath + '/back/report_sample.txt') + + # Open html sample + sample_html = get_file_content(pialertPath + '/back/report_sample.html') + + # Open json sample and get only the payload part + sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"] + + sample_msg = noti_struc(sample_json_payload, sample_txt, sample_html, "test_sample") + + + if testType == 'Email': + send_email(sample_msg) + elif testType == 'Webhooks': + send_webhook (sample_msg) + elif testType == 'Apprise': + send_apprise (sample_msg) + elif testType == 'NTFY': + send_ntfy (sample_msg) + elif testType == 'PUSHSAFER': + send_pushsafer (sample_msg) + else: + mylog('none', ['[Test Publishers] No test matches: ', testType]) + + mylog('minimal', ['[Test Publishers] END Test: ', testType]) + diff --git a/pialert/plugin_utils.py b/pialert/plugin_utils.py index 2f050c8b..ed85cf8d 100755 --- a/pialert/plugin_utils.py +++ b/pialert/plugin_utils.py @@ -7,7 +7,6 @@ from const import pluginsPath, logPath from helper import timeNowTZ, updateState, get_file_content, write_file, get_setting, get_setting_value - #------------------------------------------------------------------------------- def logEventStatusCounts(objName, pluginEvents): status_counts = {} # Dictionary to store counts for each status @@ -186,83 +185,4 @@ def handle_empty(value): return value -#=============================================================================== -# Handling of user initialized front-end events -#=============================================================================== -#------------------------------------------------------------------------------- -def check_and_run_user_event(db, pluginsState): - - sql = db.sql # TO-DO - sql.execute(""" select * from Parameters where par_ID = "Front_Event" """) - rows = sql.fetchall() - - event, param = ['',''] - if len(rows) > 0 and rows[0]['par_Value'] != 'finished': - keyValue = rows[0]['par_Value'].split('|') - - if len(keyValue) == 2: - event = keyValue[0] - param = keyValue[1] - else: - return pluginsState - - if event == 'test': - handle_test(param) - if event == 'run': - pluginsState = handle_run(param, db, pluginsState) - - # clear event execution flag - sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'") - - # commit to DB - db.commitDB() - - return pluginsState - -#------------------------------------------------------------------------------- -def handle_run(runType, db, pluginsState): - - mylog('minimal', ['[', timeNowTZ(), '] START Run: ', runType]) - - # run the plugin to run - for plugin in conf.plugins: - if plugin["unique_prefix"] == runType: - pluginsState = execute_plugin(db, plugin, pluginsState) - - mylog('minimal', ['[', timeNowTZ(), '] END Run: ', runType]) - return pluginsState - - - -#------------------------------------------------------------------------------- -def handle_test(testType): - - mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType]) - - # Open text sample - sample_txt = get_file_content(pialertPath + '/back/report_sample.txt') - - # Open html sample - sample_html = get_file_content(pialertPath + '/back/report_sample.html') - - # Open json sample and get only the payload part - sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"] - - sample_msg = noti_struc(sample_json_payload, sample_txt, sample_html ) - - - if testType == 'Email': - send_email(sample_msg) - elif testType == 'Webhooks': - send_webhook (sample_msg) - elif testType == 'Apprise': - send_apprise (sample_msg) - elif testType == 'NTFY': - send_ntfy (sample_msg) - elif testType == 'PUSHSAFER': - send_pushsafer (sample_msg) - else: - mylog('none', ['[Test Publishers] No test matches: ', testType]) - - mylog('minimal', ['[Test Publishers] END Test: ', testType]) diff --git a/pialert/reporting.py b/pialert/reporting.py index 25d851f6..99fb6838 100755 --- a/pialert/reporting.py +++ b/pialert/reporting.py @@ -50,10 +50,10 @@ json_final = [] #------------------------------------------------------------------------------- -def construct_notifications(db, sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None): +def construct_notifications(db, sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None, notificationType=''): if suppliedJsonStruct is None and sqlQuery == "": - return noti_struc("", "", "") + return noti_struc("", "", "", notificationType) 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' " @@ -97,7 +97,7 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied for header in headers: html = format_table(html, header, thProps) - notiStruc = noti_struc(jsn, text, html) + notiStruc = noti_struc(jsn, text, html, notificationType) if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html: @@ -189,7 +189,7 @@ def send_notifications (db): AND eve_EventType = 'New Device' ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "New devices") + notiStruc = construct_notifications(db, sqlQuery, "New devices", "new_devices") # collect "new_devices" for the webhook json json_new_devices = notiStruc.json["data"] @@ -205,7 +205,7 @@ def send_notifications (db): AND eve_EventType = 'Device Down' ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "Down devices") + notiStruc = construct_notifications(db, sqlQuery, "Down devices", "down_Devices") # collect "down_devices" for the webhook json json_down_devices = notiStruc.json["data"] @@ -222,7 +222,7 @@ def send_notifications (db): 'IP Changed') ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "Events") + notiStruc = construct_notifications(db, sqlQuery, "Events", "events") # collect "events" for the webhook json json_events = notiStruc.json["data"] @@ -266,12 +266,15 @@ def send_notifications (db): write_file (logPath + '/report_output.txt', mail_text) write_file (logPath + '/report_output.html', mail_html) + # Write the notifications into the DB + # TODO + # Notify is something to report if json_internet != [] or json_new_devices != [] or json_down_devices != [] or json_events != [] or json_ports != [] or plugins_report: mylog('none', ['[Notification] Changes detected, sending reports']) - msg = noti_struc(json_final, mail_text, mail_html) + msg = noti_struc(json_final, mail_text, mail_html, 'master') mylog('minimal', ['[Notification] Udating API files']) send_api() @@ -432,3 +435,56 @@ def skip_repeated_notifications (db): db.commitDB() +#------------------------------------------------------------------------------- +# Notification object handling +#------------------------------------------------------------------------------- +class Notifications: + def __init__(self, db): + + self.db = db + + # Create Notifications table if missing + self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "Notifications" ( + "Index" INTEGER, + "DateTimeCreated" TEXT, + "DateTimePushed" TEXT, + "Status" TEXT, + "JSON" TEXT, + "Text" TEXT, + "HTML" TEXT, + "PublishedVia" TEXT, + "Extra" TEXT, + PRIMARY KEY("Index" AUTOINCREMENT) + ); + """) + + self.save() + + def create(self, JSON, Text, HTML, Extra): + self.JSON = JSON + self.Text = Text + self.HTML = HTML + self.Extra = Extra + self.Status = "new" + + # TODO Init values that can be auto initialized + # TODO Check for nulls + # TODO Index vs hash to minimize SQL calls, finish CRUD operations, expose via API, use API in plugins + + # current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # self.db.sql.execute(""" + # INSERT INTO Notifications (DateTimeCreated, DateTimePushed, Status, JSON, Text, HTML, PublishedVia, Extra) + # VALUES (?, ?, ?, ?, ?, ?, ?, ?) + # """, (current_time, DateTimePushed, Status, JSON, Text, HTML, PublishedVia, Extra)) + + + + def save(self): + + # Commit changes + self.db.commitDB() + + + + +