Notification rework - Apprise v1 - working

This commit is contained in:
Jokob-sk
2023-10-08 14:52:22 +11:00
parent 79c47015f4
commit be4e0acdfc
11 changed files with 152 additions and 182 deletions

View File

@@ -1,7 +1,7 @@
Report Date: <REPORT_DATE> Report Date: <REPORT_DATE>
Server: <SERVER_NAME> Server: <SERVER_NAME>
<SECTION_NEW_DEVICES> <NEW_DEVICES_TABLE>
<SECTION_DEVICES_DOWN> <DOWN_DEVICES_TABLE>
<SECTION_EVENTS> <EVENTS_TABLE>
<PLUGINS_TABLE> <PLUGINS_TABLE>

View File

@@ -1,90 +1,76 @@
[ [
{ {
"headers": { "headers": {
"host": "192.168.1.82:5678", "host": "192.168.1.82:5678",
"user-agent": "curl/7.74.0", "user-agent": "curl/7.74.0",
"accept": "*/*", "accept": "*/*",
"content-type": "application/json", "content-type": "application/json",
"content-length": "872" "content-length": "872"
}, },
"params": {}, "params": {},
"query": {}, "query": {},
"body": { "body": {
"username": "Pi.Alert", "username": "Pi.Alert",
"text": "There are new notifications", "text": "There are new notifications",
"attachments": [ "attachments": [
{ {
"title": "Pi.Alert Notifications", "title": "Pi.Alert Notifications",
"title_link": "", "title_link": "",
"text": { "text": {
"internet": [], "internet": [],
"new_devices": [{ "new_devices": [
"MAC": "74:ac:74:ac:74:ac", {
"Datetime": "2023-01-30 22:15:09", "MAC": "74:ac:74:ac:74:ac",
"IP": "192.168.1.1", "Datetime": "2023-01-30 22:15:09",
"Event Type": "New Device", "IP": "192.168.1.1",
"Device name": "(name not found)", "Event Type": "New Device",
"Comments": null, "Device name": "(name not found)",
"Device Vendor": null "Comments": null,
}], "Device Vendor": null
"down_devices": [], }
"events": [{ ],
"MAC": "74:ac:74:ac:74:ac", "down_devices": [],
"Datetime": "2023-01-30 22:15:09", "events": [
"IP": "192.168.1.92", {
"Event Type": "Disconnected", "MAC": "74:ac:74:ac:74:ac",
"Device name": "(name not found)", "Datetime": "2023-01-30 22:15:09",
"Comments": null, "IP": "192.168.1.92",
"Device Vendor": null "Event Type": "Disconnected",
}, { "Device name": "(name not found)",
"MAC": "74:ac:74:ac:74:ac", "Comments": null,
"Datetime": "2023-01-30 22:15:09", "Device Vendor": null
"IP": "192.168.1.150", },
"Event Type": "Disconnected", {
"Device name": "(name not found)", "MAC": "74:ac:74:ac:74:ac",
"Comments": null, "Datetime": "2023-01-30 22:15:09",
"Device Vendor": null "IP": "192.168.1.150",
}], "Event Type": "Disconnected",
"ports": [{ "Device name": "(name not found)",
"new": { "Comments": null,
"Name": "New device", "Device Vendor": null
"MAC": "74:ac:74:ac:74:ac", }
"Port": "22/tcp", ],
"State": "open", "plugins": [
"Service": "ssh", {
"Extra": "" "Index": 138,
} "Plugin": "INTRSPD",
}, { "Object_PrimaryID": "Speedtest",
"new": { "Object_SecondaryID": "2023-10-08 02:01:16+02:00",
"Name": "New device", "DateTimeCreated": "2023-10-08 02:01:16",
"MAC": "74:ac:74:ac:74:ac", "DateTimeChanged": "2023-10-08 02:32:15",
"Port": "53/tcp", "Watched_Value1": "-1",
"State": "open", "Watched_Value2": "-1",
"Service": "domain", "Watched_Value3": "null",
"Extra": "" "Watched_Value4": "null",
} "Status": "missing-in-last-scan",
}, { "Extra": "null",
"new": { "UserData": "null",
"Name": "New device", "ForeignKey": "null"
"MAC": "74:ac:74:ac:74:ac", }
"Port": "80/tcp",
"State": "open",
"Service": "http",
"Extra": ""
}
}, {
"new": {
"Name": "New device",
"MAC": "74:ac:74:ac:74:ac",
"Port": "443/tcp",
"State": "open",
"Service": "https",
"Extra": ""
}
}]
}
}
] ]
}
} }
]
} }
}
] ]

View File

@@ -619,7 +619,8 @@ The UI will adjust how columns are displayed in the UI based on the resolvers de
| Supported Types | Description | | Supported Types | Description |
| -------------- | ----------- | | -------------- | ----------- |
| `label` | Displays a column only. | | `label` | Displays a column only. |
| `text` | Makes a column editable, and a save icon is displayed next to it. See below for information on `threshold`, `replace`. | | `textarea_readonly` | Generates a read only text area and cleans up the text to display it somewhat formatted with new lines preserved. |
| See below for information on `threshold`, `replace`. | |
| | | | | |
| `options` Property | Used in conjunction with types like `threshold`, `replace`, `regex`. | | `options` Property | Used in conjunction with types like `threshold`, `replace`, `regex`. |
| `threshold` | The `options` array contains objects ordered from the lowest `maximum` to the highest. The corresponding `hexColor` is used for the value background color if it's less than the specified `maximum` but more than the previous one in the `options` array. | | `threshold` | The `options` array contains objects ordered from the lowest `maximum` to the highest. The corresponding `hexColor` is used for the value background color if it's less than the specified `maximum` but more than the previous one in the `options` array. |

View File

@@ -137,9 +137,9 @@
}, },
{ {
"column": "Watched_Value2", "column": "Watched_Value2",
"css_classes": "col-sm-2", "css_classes": "col-sm-8",
"show": true, "show": true,
"type": "label", "type": "textarea_readonly",
"default_value":"", "default_value":"",
"options": [], "options": [],
"localized": ["name"], "localized": ["name"],

View File

@@ -89,6 +89,11 @@ function processColumnValue(dbColumnDef, value, index, type) {
case 'label': case 'label':
value = `<span>${value}<span>`; value = `<span>${value}<span>`;
break; break;
case 'textarea_readonly':
value = `<textarea cols="70" rows="3" wrap="off" readonly style="white-space: pre-wrap;">
${value.replace(/^b'(.*)'$/gm, '$1').replace(/\\n/g, '\n').replace(/\\r/g, '\r')}
</textarea>`;
break;
case 'textbox_save': case 'textbox_save':
value = value == 'null' ? '' : value; // hide 'null' values value = value == 'null' ? '' : value; // hide 'null' values

View File

@@ -155,10 +155,10 @@ def main ():
# Write the notifications into the DB # Write the notifications into the DB
notification = Notification_obj(db) notification = Notification_obj(db)
hasNotification = notification.create(notiStructure.json, notiStructure.text, notiStructure.html, "") notificationObj = notification.create(notiStructure.json, notiStructure.text, notiStructure.html, "")
# run all enabled publisher gateways # run all enabled publisher gateways
if hasNotification: if notificationObj.HasNotifications:
pluginsState = run_plugin_scripts(db, 'on_notification', pluginsState) pluginsState = run_plugin_scripts(db, 'on_notification', pluginsState)
notification.setAllProcessed() notification.setAllProcessed()
@@ -177,6 +177,8 @@ def main ():
# DEBUG - print number of rows updated # DEBUG - print number of rows updated
mylog('minimal', ['[Notification] Notifications changes: ', sql.rowcount]) mylog('minimal', ['[Notification] Notifications changes: ', sql.rowcount])
else:
mylog('verbose', ['[Notification] No changes to report'])
# Commit SQL # Commit SQL
db.commitDB() db.commitDB()

View File

@@ -244,27 +244,24 @@ def write_file(pPath, pText):
# Return whole setting touple # Return whole setting touple
def get_setting(key): def get_setting(key):
settingsFile = apiPath + '/table_settings.json' settingsFile = apiPath + 'table_settings.json'
try: try:
with open(settingsFile, 'r') as json_file: with open(settingsFile, 'r') as json_file:
data = json.load(json_file) data = json.load(json_file)
# if not isinstance(data, list):
# mylog('minimal', [f' [Settings] Data is not a list of dictionaries (file: {settingsFile})'])
for item in data.get("data",[]): for item in data.get("data",[]):
if item.get("Code_Name") == key: if item.get("Code_Name") == key:
return item return item
mylog('minimal', [f'[Settings] Error - setting_missing - Setting not found for key: {key} in file {settingsFile}']) mylog('debug', [f'[Settings] Error - setting_missing - Setting not found for key: {key} in file {settingsFile}'])
return None return None
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e: except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
# Handle the case when the file is not found, JSON decoding fails, or data is not in the expected format # Handle the case when the file is not found, JSON decoding fails, or data is not in the expected format
mylog('minimal', [f'[Settings] Error - JSONDecodeError or FileNotFoundError for file {settingsFile}']) mylog('none', [f'[Settings] Error - JSONDecodeError or FileNotFoundError for file {settingsFile}'])
return None return None
@@ -274,19 +271,32 @@ def get_setting(key):
# Return setting value # Return setting value
def get_setting_value(key): def get_setting_value(key):
set = get_setting(key) setting = get_setting(key)
if get_setting(key) is not None: if setting is not None:
setVal = set["Value"] # setting value set_value = setting["Value"] # Setting value
setTyp = set["Type"] # setting type set_type = setting["Type"] # Setting type
return setVal # Handle different types of settings
if set_type in ['text', 'string', 'password', 'readonly', 'text.select']:
return str(set_value)
elif set_type in ['boolean', 'integer.checkbox']:
return bool(set_value)
elif set_type in ['integer.select', 'integer']:
return int(set_value)
elif set_type in ['text.multiselect', 'list', 'subnets']:
# Assuming set_value is a list in this case
return set_value
elif set_type == '.template':
# Assuming set_value is a JSON object in this case
return json.loads(set_value)
return '' return ''
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# IP validation methods # IP validation methods
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------

View File

@@ -40,11 +40,8 @@ class Notification_obj:
# Check if nothing to report, end # Check if nothing to report, end
if JSON["internet"] == [] and JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == []: if JSON["internet"] == [] and JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == []:
self.HasNotifications = False self.HasNotifications = False
# end if nothing to report else:
return self.HasNotifications self.HasNotifications = True
# continue and save into DB if notifications available
self.HasNotifications = True
self.GUID = str(uuid.uuid4()) self.GUID = str(uuid.uuid4())
self.DateTimeCreated = timeNowTZ() self.DateTimeCreated = timeNowTZ()
@@ -56,9 +53,10 @@ class Notification_obj:
self.PublishedVia = "" self.PublishedVia = ""
self.Extra = Extra self.Extra = Extra
self.upsert() if self.HasNotifications:
self.upsert()
return self.HasNotifications return self
# Only updates the status # Only updates the status
def updateStatus(self, newStatus): def updateStatus(self, newStatus):
@@ -71,8 +69,6 @@ class Notification_obj:
self.DateTimePushed = timeNowTZ() self.DateTimePushed = timeNowTZ()
self.upsert() self.upsert()
# TODO Index vs hash to minimize SQL calls, finish CRUD operations, expose via API, use API in plugins
# create or update a notification # create or update a notification
def upsert(self): def upsert(self):
self.db.sql.execute(""" self.db.sql.execute("""
@@ -82,6 +78,15 @@ class Notification_obj:
self.save() 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 # Get all with the "new" status
def getNew(self): def getNew(self):
self.db.sql.execute(""" self.db.sql.execute("""

View File

@@ -9,11 +9,12 @@ from collections import namedtuple
# pialert modules # pialert modules
import conf import conf
from const import pluginsPath, logPath from const import pluginsPath, logPath, pialertPath
from logger import mylog from logger import mylog
from helper import timeNowTZ, updateState, get_file_content, write_file, get_setting, get_setting_value from helper import timeNowTZ, updateState, get_file_content, write_file, get_setting, get_setting_value
from api import update_api from api import update_api
from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, get_plugin_setting_value, handle_empty, custom_plugin_decoder from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting, print_plugin_info, list_to_csv, combine_plugin_objects, resolve_wildcards_arr, handle_empty, custom_plugin_decoder
from notification import Notification_obj
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
@@ -484,9 +485,6 @@ def process_plugin_events(db, plugin, pluginsState, plugEventsArr):
history_to_insert = [] history_to_insert = []
objects_to_update = [] objects_to_update = []
statuses_to_report_on = get_plugin_setting_value(plugin, "REPORT_ON")
mylog('debug', ['[Plugins] statuses_to_report_on: ', statuses_to_report_on])
for plugObj in pluginObjects: for plugObj in pluginObjects:
# keep old createdTime time if the plugObj already was created before # keep old createdTime time if the plugObj already was created before
@@ -505,6 +503,8 @@ def process_plugin_events(db, plugin, pluginsState, plugEventsArr):
objects_to_update.append(values + (plugObj.index,)) # Include index for UPDATE objects_to_update.append(values + (plugObj.index,)) # Include index for UPDATE
# only generate events that we want to be notified on # only generate events that we want to be notified on
statuses_to_report_on = get_setting_value(plugObj.pluginPref + "_REPORT_ON")
if plugObj.status in statuses_to_report_on: if plugObj.status in statuses_to_report_on:
events_to_insert.append(values) events_to_insert.append(values)
@@ -777,39 +777,24 @@ def handle_run(runType, db, pluginsState):
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def handle_test(runType, db, pluginsState): def handle_test(runType, db, pluginsState):
mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType]) mylog('minimal', ['[', timeNowTZ(), '] [Test] START Test: ', runType])
# TODO finish # Prepare test samples
sample_txt = get_file_content(pialertPath + '/back/report_sample.txt')
sample_html = get_file_content(pialertPath + '/back/report_sample.html')
sample_json = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"]
# # Open text sample # Create fake notification
# sample_txt = get_file_content(pialertPath + '/back/report_sample.txt') notification = Notification_obj(db)
notificationObj = notification.create(sample_json, sample_txt, sample_html, "")
# # Open html sample # Run test
# sample_html = get_file_content(pialertPath + '/back/report_sample.html') pluginsState = handle_run(runType, db, pluginsState)
# # Open json sample and get only the payload part # Remove sample notification
# sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"] notificationObj.remove(notificationObj.GUID)
# sample_msg = noti_obj(sample_json_payload, sample_txt, sample_html, "test_sample") mylog('minimal', ['[Test] END Test: ', runType])
pluginsState = handle_run(param, db, pluginsState)
# 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])
return pluginsState return pluginsState

View File

@@ -78,10 +78,7 @@ def list_to_csv(arr):
mylog('debug', '[Plugins] Flattening the below array') mylog('debug', '[Plugins] Flattening the below array')
mylog('debug', arr) mylog('debug', arr)
mylog('debug', f'[Plugins] isinstance(arr, list) : {isinstance(arr, list)} | isinstance(arr, str) : {isinstance(arr, str)}')
mylog('debug', f'[Plugins] isinstance(arr, list) : {isinstance(arr, list)}')
mylog('debug', f'[Plugins] isinstance(arr, str) : {isinstance(arr, str)}')
if isinstance(arr, str): if isinstance(arr, str):
return arr.replace('[','').replace(']','').replace("'", '') # removing brackets and single quotes (not allowed) return arr.replace('[','').replace(']','').replace("'", '') # removing brackets and single quotes (not allowed)
@@ -172,18 +169,6 @@ def get_plugins_configs():
return pluginsList # Return the list of plugin configurations return pluginsList # Return the list of plugin configurations
#-------------------------------------------------------------------------------
# Gets the setting value
def get_plugin_setting_value(plugin, function_key):
resultObj = get_plugin_setting(plugin, function_key)
if resultObj != None:
return resultObj["value"]
return None
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
def custom_plugin_decoder(pluginDict): def custom_plugin_decoder(pluginDict):
return namedtuple('X', pluginDict.keys())(*pluginDict.values()) return namedtuple('X', pluginDict.keys())(*pluginDict.values())

View File

@@ -192,7 +192,7 @@ def get_notifications (db):
# collect "new_devices" for the webhook json # collect "new_devices" for the webhook json
json_new_devices = notiStruc.json["data"] json_new_devices = notiStruc.json["data"]
mail_text = mail_text.replace ('<SECTION_NEW_DEVICES>', notiStruc.text + '\n') mail_text = mail_text.replace ('<NEW_DEVICES_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', notiStruc.html) mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', notiStruc.html)
mylog('verbose', ['[Notification] New Devices sections done.']) mylog('verbose', ['[Notification] New Devices sections done.'])
@@ -208,7 +208,7 @@ def get_notifications (db):
# collect "down_devices" for the webhook json # collect "down_devices" for the webhook json
json_down_devices = notiStruc.json["data"] json_down_devices = notiStruc.json["data"]
mail_text = mail_text.replace ('<SECTION_DEVICES_DOWN>', notiStruc.text + '\n') mail_text = mail_text.replace ('<DOWN_DEVICES_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', notiStruc.html) mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', notiStruc.html)
mylog('verbose', ['[Notification] Down Devices sections done.']) mylog('verbose', ['[Notification] Down Devices sections done.'])
@@ -225,7 +225,7 @@ def get_notifications (db):
# collect "events" for the webhook json # collect "events" for the webhook json
json_events = notiStruc.json["data"] json_events = notiStruc.json["data"]
mail_text = mail_text.replace ('<SECTION_EVENTS>', notiStruc.text + '\n') mail_text = mail_text.replace ('<EVENTS_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<EVENTS_TABLE>', notiStruc.html) mail_html = mail_html.replace ('<EVENTS_TABLE>', notiStruc.html)
mylog('verbose', ['[Notification] Events sections done.']) mylog('verbose', ['[Notification] Events sections done.'])
@@ -265,12 +265,7 @@ def get_notifications (db):
return noti_obj(final_json, final_text, final_html) return noti_obj(final_json, final_text, final_html)
# # Notify is something to report
# if hasNotifications:
# mylog('none', ['[Notification] Changes detected, sending reports'])
# msg = noti_obj(json_final, mail_text, mail_html)
# mylog('minimal', ['[Notification] Udating API files']) # mylog('minimal', ['[Notification] Udating API files'])
# send_api() # send_api()
@@ -281,12 +276,8 @@ def get_notifications (db):
# send_email (msg ) # send_email (msg )
# else : # else :
# mylog('verbose', ['[Notification] Skip email']) # mylog('verbose', ['[Notification] Skip email'])
# if conf.REPORT_APPRISE and check_config('apprise'): #
# updateState("Send: Apprise")
# mylog('minimal', ['[Notification] Sending report by Apprise'])
# send_apprise (msg)
# else :
# mylog('verbose', ['[Notification] Skip Apprise'])
# if conf.REPORT_WEBHOOK and check_config('webhook'): # if conf.REPORT_WEBHOOK and check_config('webhook'):
# updateState("Send: Webhook") # updateState("Send: Webhook")
# mylog('minimal', ['[Notification] Sending report by Webhook']) # mylog('minimal', ['[Notification] Sending report by Webhook'])