diff --git a/back/report_template.txt b/back/report_template.txt index 85f8575a..80c9562c 100755 --- a/back/report_template.txt +++ b/back/report_template.txt @@ -1,7 +1,7 @@ Report Date: Server: - - - + + + diff --git a/back/webhook_json_sample.json b/back/webhook_json_sample.json index 940630d0..a782fa16 100755 --- a/back/webhook_json_sample.json +++ b/back/webhook_json_sample.json @@ -1,90 +1,76 @@ [ - { - "headers": { - "host": "192.168.1.82:5678", - "user-agent": "curl/7.74.0", - "accept": "*/*", - "content-type": "application/json", - "content-length": "872" - }, - "params": {}, - "query": {}, - "body": { - "username": "Pi.Alert", - "text": "There are new notifications", - "attachments": [ - { - "title": "Pi.Alert Notifications", - "title_link": "", - "text": { - "internet": [], - "new_devices": [{ - "MAC": "74:ac:74:ac:74:ac", - "Datetime": "2023-01-30 22:15:09", - "IP": "192.168.1.1", - "Event Type": "New Device", - "Device name": "(name not found)", - "Comments": null, - "Device Vendor": null - }], - "down_devices": [], - "events": [{ - "MAC": "74:ac:74:ac:74:ac", - "Datetime": "2023-01-30 22:15:09", - "IP": "192.168.1.92", - "Event Type": "Disconnected", - "Device name": "(name not found)", - "Comments": null, - "Device Vendor": null - }, { - "MAC": "74:ac:74:ac:74:ac", - "Datetime": "2023-01-30 22:15:09", - "IP": "192.168.1.150", - "Event Type": "Disconnected", - "Device name": "(name not found)", - "Comments": null, - "Device Vendor": null - }], - "ports": [{ - "new": { - "Name": "New device", - "MAC": "74:ac:74:ac:74:ac", - "Port": "22/tcp", - "State": "open", - "Service": "ssh", - "Extra": "" - } - }, { - "new": { - "Name": "New device", - "MAC": "74:ac:74:ac:74:ac", - "Port": "53/tcp", - "State": "open", - "Service": "domain", - "Extra": "" - } - }, { - "new": { - "Name": "New device", - "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": "" - } - }] - } - } + { + "headers": { + "host": "192.168.1.82:5678", + "user-agent": "curl/7.74.0", + "accept": "*/*", + "content-type": "application/json", + "content-length": "872" + }, + "params": {}, + "query": {}, + "body": { + "username": "Pi.Alert", + "text": "There are new notifications", + "attachments": [ + { + "title": "Pi.Alert Notifications", + "title_link": "", + "text": { + "internet": [], + "new_devices": [ + { + "MAC": "74:ac:74:ac:74:ac", + "Datetime": "2023-01-30 22:15:09", + "IP": "192.168.1.1", + "Event Type": "New Device", + "Device name": "(name not found)", + "Comments": null, + "Device Vendor": null + } + ], + "down_devices": [], + "events": [ + { + "MAC": "74:ac:74:ac:74:ac", + "Datetime": "2023-01-30 22:15:09", + "IP": "192.168.1.92", + "Event Type": "Disconnected", + "Device name": "(name not found)", + "Comments": null, + "Device Vendor": null + }, + { + "MAC": "74:ac:74:ac:74:ac", + "Datetime": "2023-01-30 22:15:09", + "IP": "192.168.1.150", + "Event Type": "Disconnected", + "Device name": "(name not found)", + "Comments": null, + "Device Vendor": null + } + ], + "plugins": [ + { + "Index": 138, + "Plugin": "INTRSPD", + "Object_PrimaryID": "Speedtest", + "Object_SecondaryID": "2023-10-08 02:01:16+02:00", + "DateTimeCreated": "2023-10-08 02:01:16", + "DateTimeChanged": "2023-10-08 02:32:15", + "Watched_Value1": "-1", + "Watched_Value2": "-1", + "Watched_Value3": "null", + "Watched_Value4": "null", + "Status": "missing-in-last-scan", + "Extra": "null", + "UserData": "null", + "ForeignKey": "null" + } ] + } } + ] } + } ] \ No newline at end of file diff --git a/docs/ICONS.md b/docs/ICONS.md index 61ea561b..bceb56b8 100755 --- a/docs/ICONS.md +++ b/docs/ICONS.md @@ -1,6 +1,6 @@ ## Icons overview -Icons are used to visually distinguish devices in the app in most of the device listing tables and the [network tree](/docs/NETWORK_TREE.md). Currently only free [Font Awesome](https://fontawesome.com/search?o=r&m=free) icons (up-to v 6.4.0) are supported (I have an unblockable [sponsorship goal](https://github.com/sponsors/jokob-sk) to add the material design icon pack). +Icons are used to visually distinguish devices in the app in most of the device listing tables and the [network tree](/docs/NETWORK_TREE.md). Currently only free [Font Awesome](https://fontawesome.com/search?o=r&m=free) icons (up-to v 6.4.0) are supported. ![Raspberry Pi with a brand icon](/docs/img/ICONS/devices-icons.png) @@ -8,6 +8,8 @@ Icons are used to visually distinguish devices in the app in most of the device You can assign icons individually on each device in the Details tab. +![preview](/docs/img/ICONS/device_icons_preview.gif) + ![Raspberry Pi device details](/docs/img/ICONS/device-icon.png) - You can click into the `Icon` field or click the Pencil (2) icon in the above screenshot to enter any text. Only [free Font Awesome](https://fontawesome.com/search?o=r&m=free) icons in the following format will work: diff --git a/docs/img/ICONS/device_icons_preview.gif b/docs/img/ICONS/device_icons_preview.gif new file mode 100755 index 00000000..01929ff3 Binary files /dev/null and b/docs/img/ICONS/device_icons_preview.gif differ diff --git a/front/css/pialert.css b/front/css/pialert.css index a9d952f1..6b0aeb84 100755 --- a/front/css/pialert.css +++ b/front/css/pialert.css @@ -718,6 +718,10 @@ input[readonly] { } /* Devices */ +#txtIconFA { + min-width: 18px; +} + .drp-edit { cursor: pointer; @@ -795,7 +799,8 @@ input[readonly] { #networkTree .netPort { float:left; - display:inline; + display:inline; + text-align: center; } #networkTree .portBckgIcon @@ -816,7 +821,8 @@ input[readonly] { { width: 25px;; float:left; - display:inline; + display:inline; + text-align: center; } #networkTree .netCollapse { diff --git a/front/deviceDetails.php b/front/deviceDetails.php index d32a1f5c..cb12db29 100755 --- a/front/deviceDetails.php +++ b/front/deviceDetails.php @@ -149,7 +149,7 @@
-
+ @@ -196,9 +196,9 @@
- + + '> -
`; - }, +
+ ${devicePort} ${deviceIcon} + ${nodeData.data.name} + + ${collapseExpandHtml} +
+
`; + }, - onNodeClick: nodeData => { - console.log(this) - }, + onNodeClick: nodeData => { + console.log(this) + }, mainAxisNodeSpacing: 'auto', // mainAxisNodeSpacing: 3, secondaryAxisNodeSpacing: 0.3, - nodeHeight: nodeHeight.toString(), + nodeHeight: nodeHeight.toString(), marginTop: '5', hasZoom: false, hasPan: false, // marginLeft: '15', idKey: "id", - hasFlatData: false, + hasFlatData: false, linkWidth: (nodeData) => 3, linkColor: (nodeData) => "#ffcc80", onNodeClick: (nodeData) => handleNodeClick(nodeData), - relationnalField: "children", + relationnalField: "children", }); - console.log(myHierarchy) - - - myTree.refresh(myHierarchy); + + myTree.refresh(myHierarchy); } // --------------------------------------------------------------------------- diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index 2faee1ae..f1639a30 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -504,25 +504,13 @@ "Email_icon" : "", "REPORT_MAIL_name" : "Enable email", "REPORT_MAIL_description" : "If enabled an email is sent out with a list of changes you nove subscribed to. Please also fill out all remaining settings related to the SMTP setup below. If facing issues, set LOG_LEVEL to debug and check the error log.", - "SMTP_SERVER_name" : "SMTP server URL", - "SMTP_SERVER_description" : "The SMTP server host URL. For example smtp-relay.sendinblue.com. To use Gmail as an SMTP server follow this guide", - "SMTP_PORT_name" : "SMTP server PORT", - "SMTP_PORT_description" : "Port number used for the SMTP connection. Set to 0 if you do not want to use a port when connecting to the SMTP server.", - "SMTP_SKIP_LOGIN_name" : "Skip authentication", - "SMTP_SKIP_LOGIN_description" : "Do not use authentication when connecting to the SMTP server.", - "SMTP_USER_name" : "SMTP user", - "SMTP_USER_description" : "The user name used to login into the SMTP server (sometimes a full email address).", - "SMTP_PASS_name" : "SMTP password", - "SMTP_PASS_description" : "The SMTP server password. ", - "SMTP_SKIP_TLS_name" : "Do not use TLS", - "SMTP_SKIP_TLS_description" : "Disable TLS when connecting to your SMTP server.", - "SMTP_FORCE_SSL_name" : "Force SSL", - "SMTP_FORCE_SSL_description" : "Force SSL when connecting to your SMTP server.", - "SYSTEM_TITLE" : "System Information", - "REPORT_TO_name" : "Send email to", - "REPORT_TO_description" : "Email address to which the notification will be send to.", - "REPORT_FROM_name" : "Email subject", - "REPORT_FROM_description" : "Notification email subject line. Some SMTP servers need this to be an email.", + "SYSTEM_TITLE" : "System Information", + + "REPORT_TO_name" : "deprecated", + "REPORT_TO_description" : "deprecated", + "REPORT_FROM_name" : "deprecated", + "REPORT_FROM_description" : "deprecated", + "Webhooks_display_name" : "Webhooks", "Webhooks_icon" : "", "REPORT_WEBHOOK_name" : "Enable Webhooks", @@ -536,17 +524,7 @@ "WEBHOOK_SIZE_name" : "Max payload size", "WEBHOOK_SIZE_description" : "The maximum size of the webhook payload as number of characters in the passed string. If above limit, it will be truncated and a (text was truncated) message is appended.", "WEBHOOK_SECRET_name": "HMAC Secret", - "WEBHOOK_SECRET_description": "When set, use this secret to generate the SHA256-HMAC hex digest value of the request body, which will be passed as the X-Webhook-Signature header to the request. You can find more informations here.", - "Apprise_display_name" : "Apprise", - "Apprise_icon" : "", - "REPORT_APPRISE_name" : "Enable Apprise", - "REPORT_APPRISE_description" : "Enable sending notifications via Apprise.", - "APPRISE_HOST_name" : "Apprise host URL", - "APPRISE_HOST_description" : "Apprise host URL starting with http:// or https://. (do not forget to include /notify at the end)", - "APPRISE_URL_name" : "Apprise notification URL", - "APPRISE_URL_description" : "Apprise notification target URL. For example for Telegram it would be tgram://{bot_token}/{chat_id}.", - "APPRISE_SIZE_name" : "Max payload size", - "APPRISE_SIZE_description" : "The maximum size of the apprise payload as number of characters in the passed string. If above limit, it will be truncated and a (text was truncated) message is appended.", + "WEBHOOK_SECRET_description": "When set, use this secret to generate the SHA256-HMAC hex digest value of the request body, which will be passed as the X-Webhook-Signature header to the request. You can find more informations here.", "NTFY_display_name" : "NTFY", "NTFY_icon" : "", "REPORT_NTFY_name" : "Enable NTFY", @@ -564,9 +542,7 @@ "REPORT_PUSHSAFER_name" : "Enable Pushsafer", "REPORT_PUSHSAFER_description" : "Enable sending notifications via Pushsafer.", "PUSHSAFER_TOKEN_name" : "Pushsafer token", - "PUSHSAFER_TOKEN_description" : "Your secret Pushsafer API key (token).", - "APPRISE_PAYLOAD_name" : "Payload type", - "APPRISE_PAYLOAD_description" : "Select the payoad type sent to Apprise. For example html works well with emails, text with chat apps, such as Telegram.", + "PUSHSAFER_TOKEN_description" : "Your secret Pushsafer API key (token).", "MQTT_display_name" : "MQTT", "MQTT_icon" : "", "REPORT_TITLE" : "Report", diff --git a/front/plugins/README.md b/front/plugins/README.md index 69bf683a..9764f70d 100755 --- a/front/plugins/README.md +++ b/front/plugins/README.md @@ -542,8 +542,8 @@ Required attributes are: | `"name"` | Displayed on the Settings page. An array of localized strings. See Localized strings below. | | `"description"` | Displayed on the Settings page. An array of localized strings. See Localized strings below. | | (optional) `"events"` | Specifies whether to generate an execution button next to the input field of the setting. Supported values: | -| | - `test` | -| | - `run` | +| | - `"test"` - For notification plugins testing | +| | - `"run"` - Regular plugins testing | | (optional) `"override_value"` | Used to determine a user-defined override for the setting. Useful for template-based plugins, where you can choose to leave the current value or override it with the value defined in the setting. (Work in progress) | | (optional) `"events"` | Used to trigger the plugin. Usually used on the `RUN` setting. Not fully tested in all scenarios. Will show a play button next to the setting. After clicking, an event is generated for the backend in the `Parameters` database table to process the front-end event on the next run. | @@ -619,7 +619,8 @@ The UI will adjust how columns are displayed in the UI based on the resolvers de | Supported Types | Description | | -------------- | ----------- | | `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`. | | `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. | diff --git a/front/plugins/_publisher_apprise/README.md b/front/plugins/_publisher_apprise/README.md new file mode 100755 index 00000000..e32cc3f2 --- /dev/null +++ b/front/plugins/_publisher_apprise/README.md @@ -0,0 +1,8 @@ +## Overview + +[Apprise](front/plugins/arp_scan/README.md) is a notification gateway/publisher that allows you to push notifications to 80+ different services. + +### Usage + +- Go to settings and fill in relevant details. + diff --git a/front/plugins/_publisher_apprise/apprise.py b/front/plugins/_publisher_apprise/apprise.py index ac2fc7d0..08f7ebd9 100755 --- a/front/plugins/_publisher_apprise/apprise.py +++ b/front/plugins/_publisher_apprise/apprise.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# Based on the work of https://github.com/leiweibau/Pi.Alert import json import subprocess @@ -15,87 +14,115 @@ 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 +from helper import timeNowTZ, noti_obj, get_setting_value +from notification import Notification_obj +from database import DB CUR_PATH = str(pathlib.Path(__file__).parent.resolve()) RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log') +pluginName = 'APPRISE' + def main(): - mylog('verbose', ['[APPRISE](publisher) In script']) + mylog('verbose', [f'[{pluginName}](publisher) In script']) + + # Check if basic config settings supplied + if check_config() == False: + mylog('none', [f'[{pluginName}] Error: Publisher notification gateway not set up correctly. Check your pialert.conf {pluginName}_* variables.']) + return - parser = argparse.ArgumentParser(description='APPRISE publisher Plugin') - values = parser.parse_args() + # Create a database connection + db = DB() # instance of class DB + db.open() + # Initialize the Plugin obj output file plugin_objects = Plugin_Objects(RESULT_FILE) - speedtest_result = send() + # Create a Notification_obj instance + notifications = Notification_obj(db) - 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' - ) + # Retrieve new notifications + new_notifications = notifications.getNew() + + # Process the new notifications + for notification in new_notifications: + + # Send notification + result = send(notification["HTML"], notification["Text"]) + + # Log result + plugin_objects.add_object( + primaryId = pluginName, + secondaryId = timeNowTZ(), + watched1 = notification["GUID"], + watched2 = result, + 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.']) + if get_setting_value('APPRISE_URL') == '' or get_setting_value('APPRISE_HOST') == '': return False else: return True #------------------------------------------------------------------------------- -def send(msg: noti_struc): - html = msg.html - text = msg.text +def send(html, text): payloadData = '' + result = '' # limit = 1024 * 1024 # 1MB limit (1024 bytes * 1024 bytes = 1MB) - limit = conf.APPRISE_SIZE + limit = get_setting_value('APPRISE_SIZE') # truncate size - if conf.APPRISE_PAYLOAD == 'html': - if len(msg.html) > limit: - payloadData = msg.html[:limit] + "

(text was truncated)

" + if get_setting_value('APPRISE_PAYLOAD') == 'html': + if len(html) > limit: + payloadData = 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)" + payloadData = html + if get_setting_value('APPRISE_PAYLOAD') == 'text': + if len(text) > limit: + payloadData = text[:limit] + " (text was truncated)" else: - payloadData = msg.text + payloadData = text # Define Apprise compatible payload (https://github.com/caronc/apprise-api#stateless-solution) _json_payload = { - "urls": conf.APPRISE_URL, + "urls": get_setting_value('APPRISE_URL'), "title": "Pi.Alert Notifications", - "format": conf.APPRISE_PAYLOAD, + "format": get_setting_value('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) + p = subprocess.Popen(["curl","-i","-X", "POST" ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), get_setting_value('APPRISE_HOST')], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, stderr = p.communicate() - # write stdout and stderr into .log files for debugging if needed - + # 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 + mylog('debug', [stdout, stderr]) + + # log result + result = stdout + except subprocess.CalledProcessError as e: # An error occurred, handle it mylog('none', [e.output]) + # log result + result = e.output + + return result + if __name__ == '__main__': sys.exit(main()) diff --git a/front/plugins/_publisher_apprise/config.json b/front/plugins/_publisher_apprise/config.json index aaa42858..99b05b97 100755 --- a/front/plugins/_publisher_apprise/config.json +++ b/front/plugins/_publisher_apprise/config.json @@ -1,22 +1,30 @@ { - "code_name": "internet_speedtest", - "unique_prefix": "INTRSPD", + "code_name": "_publisher_apprise", + "unique_prefix": "APPRISE", "enabled": true, "data_source": "script", "show_ui": true, "localized": ["display_name", "description", "icon"], - "display_name" : [{ + "display_name" : [ + { "language_code": "en_us", - "string" : "Internet speedtest" - }], + "string" : "Apprise publisher" + }, + { + "language_code": "es_es", + "string" : "Habilitar Apprise" + } + ], "icon":[{ "language_code": "en_us", - "string" : "" + "string" : "" }], - "description": [{ + "description": [ + { "language_code": "en_us", - "string" : "A plugin to perform a scheduled internet speedtest." - }], + "string" : "A plugin to publish a notification via the Apprise gateway." + } + ], "params" : [], "database_column_definitions": [ @@ -94,7 +102,7 @@ "localized": ["name"], "name":[{ "language_code": "en_us", - "string" : "Test run on" + "string" : "Sent when" }] }, { @@ -118,60 +126,26 @@ "column": "Watched_Value1", "css_classes": "col-sm-2", "show": true, - "type": "threshold", + "type": "label", "default_value":"", - "options": [ - { - "maximum": 1, - "hexColor": "#D33115" - }, - { - "maximum": 5, - "hexColor": "#792D86" - }, - { - "maximum": 10, - "hexColor": "#7D862D" - }, - { - "maximum": 100, - "hexColor": "#05483C" - } - ], + "options": [], "localized": ["name"], "name":[{ "language_code": "en_us", - "string" : "Download" + "string" : "Notification GUID" }] }, { "column": "Watched_Value2", - "css_classes": "col-sm-2", + "css_classes": "col-sm-8", "show": true, - "type": "threshold", + "type": "textarea_readonly", "default_value":"", - "options": [ - { - "maximum": 1, - "hexColor": "#D33115" - }, - { - "maximum": 5, - "hexColor": "#792D86" - }, - { - "maximum": 10, - "hexColor": "#7D862D" - }, - { - "maximum": 100, - "hexColor": "#05483C" - } - ], + "options": [], "localized": ["name"], "name":[{ "language_code": "en_us", - "string" : "Upload" + "string" : "Result" }] }, { @@ -280,10 +254,10 @@ "settings":[ { "function": "RUN", - "events": ["run"], + "events": ["test"], "type": "text.select", "default_value":"disabled", - "options": ["disabled", "once", "schedule", "always_after_scan" ], + "options": ["disabled", "on_notification" ], "localized": ["name", "description"], "name" :[{ "language_code": "en_us", @@ -293,15 +267,21 @@ "language_code": "es_es", "string" : "Cuando ejecuta" }], - "description": [{ + "description": [ + { "language_code": "en_us", - "string" : "Enable a regular internet speedtest. If you select schedule the scheduling settings from below are applied. If you select once the scan is run only once on start of the application (container) for the time specified in INTRSPD_RUN_TIMEOUT setting." - }] + "string" : "Enable sending notifications via Apprise." + }, + { + "language_code": "es_es", + "string" : "Habilitar el envío de notificaciones a través de Apprise." + } + ] }, { "function": "CMD", "type": "readonly", - "default_value":"python3 /home/pi/pialert/front/plugins/internet_speedtest/script.py", + "default_value":"python3 /home/pi/pialert/front/plugins/_publisher_apprise/apprise.py", "options": [], "localized": ["name", "description"], "name" : [{ @@ -321,33 +301,10 @@ "string" : "Comando a ejecutar" }] }, - { - "function": "RUN_SCHD", - "type": "text", - "default_value":"*/30 * * * *", - "options": [], - "localized": ["name", "description"], - "name" : [{ - "language_code": "en_us", - "string" : "Schedule" - }, - { - "language_code": "es_es", - "string" : "Schedule" - }], - "description": [{ - "language_code": "en_us", - "string" : "Only enabled if you select schedule in the INTRSPD_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes." - }, - { - "language_code": "es_es", - "string": "Solo habilitado si selecciona schedule en la configuración INTRSPD_RUN. Asegúrese de ingresar el schedule en el formato similar a cron correcto (por ejemplo, valide en crontab.guru). Por ejemplo, ingrese 0 4 * * * ejecutará el escaneo después de las 4 am en el TIMEZONE que configuró arriba . Se ejecutará la PRÓXIMA vez que pase el tiempo." - }] - }, { "function": "RUN_TIMEOUT", "type": "integer", - "default_value":60, + "default_value": 10, "options": [], "localized": ["name", "description"], "name" : [{ @@ -372,46 +329,96 @@ }] }, { - "function": "WATCH", - "type": "text.multiselect", - "default_value":[], - "options": ["Watched_Value1","Watched_Value2","Watched_Value3","Watched_Value4"], + "function": "HOST", + "type": "text", + "default_value": "", + "options": [], "localized": ["name", "description"], - "name" :[{ + "name" : [{ "language_code": "en_us", - "string" : "Watched" + "string" : "Apprise host URL" }, { "language_code": "es_es", - "string" : "Visto" - }], - "description":[{ + "string" : "URL del host de Apprise" + }], + "description": [{ "language_code": "en_us", - "string" : "Send a notification if selected values change. Use CTRL + Click to select/deselect.
  • Watched_Value1 is Download speed (not recommended)
  • Watched_Value2 is Upload speed (not recommended)
  • Watched_Value3 unused
  • Watched_Value4 unused
" - }] + "string" : "Apprise host URL starting with http:// or https://. (do not forget to include /notify at the end)" + }, + { + "language_code": "es_es", + "string" : "URL del host de Apprise que comienza con http:// o https://. (no olvide incluir /notify al final)" + }] }, { - "function": "REPORT_ON", - "type": "text.multiselect", - "default_value":[], - "options": ["new","watched-changed","watched-not-changed", "missing-in-last-scan"], + "function": "URL", + "type": "text", + "default_value": "", + "options": [], "localized": ["name", "description"], - "name" :[{ + "name" : [{ "language_code": "en_us", - "string" : "Report on" + "string" : "Apprise notification URL" }, { "language_code": "es_es", - "string" : "Informar sobre" - }] , - "description":[{ + "string" : "URL de notificación de Apprise" + }], + "description": [{ "language_code": "en_us", - "string" : "Send a notification only on these statuses. new means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. watched-changed means that selected Watched_ValueN columns changed." + "string" : "Apprise notification target URL. For example for Telegram it would be tgram://{bot_token}/{chat_id}." }, { "language_code": "es_es", - "string" : "Envíe una notificación solo en estos estados. new significa que se descubrió un nuevo objeto único (combinación única de PrimaryId y SecondaryId). watched-changed significa que seleccionó Watched_ValueN Las columnas cambiaron." - }] - } + "string" : "Informar de la URL de destino de la notificación. Por ejemplo, para Telegram sería tgram://{bot_token}/{chat_id}." + }] + }, + { + "function": "PAYLOAD", + "type": "text.select", + "default_value": "html", + "options": ["html", "text"], + "localized": ["name", "description"], + "name" : [{ + "language_code": "en_us", + "string" : "Payload type" + }, + { + "language_code": "es_es", + "string" : "Tipo de carga" + }], + "description": [{ + "language_code": "en_us", + "string" : "Select the payoad type sent to Apprise. For example html works well with emails, text with chat apps, such as Telegram." + }, + { + "language_code": "es_es", + "string" : "Seleccione el tipo de carga útil enviada a Apprise. Por ejemplo, html funciona bien con correos electrónicos, text con aplicaciones de chat, como Telegram." + }] + }, + { + "function": "SIZE", + "type": "integer", + "default_value": 1024, + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code": "en_us", + "string" : "Max payload size" + }, + { + "language_code": "es_es", + "string" : "Tamaño máximo de carga útil" + }], + "description": [{ + "language_code": "en_us", + "string" : "The maximum size of the apprise payload as number of characters in the passed string. If above limit, it will be truncated and a (text was truncated) message is appended." + }, + { + "language_code": "es_es", + "string" : "El tamaño máximo de la carga útil de información como número de caracteres en la cadena pasada. Si supera el límite, se truncará y se agregará un mensaje (text was truncated)." + }] + } ] } diff --git a/front/plugins/_publisher_apprise/ignore_plugin b/front/plugins/_publisher_apprise/ignore_plugin deleted file mode 100755 index 77ffa1c1..00000000 --- a/front/plugins/_publisher_apprise/ignore_plugin +++ /dev/null @@ -1 +0,0 @@ -This plugin will not be loaded \ No newline at end of file diff --git a/front/plugins/_publisher_email/README.md b/front/plugins/_publisher_email/README.md new file mode 100755 index 00000000..e32cc3f2 --- /dev/null +++ b/front/plugins/_publisher_email/README.md @@ -0,0 +1,8 @@ +## Overview + +[Apprise](front/plugins/arp_scan/README.md) is a notification gateway/publisher that allows you to push notifications to 80+ different services. + +### Usage + +- Go to settings and fill in relevant details. + diff --git a/front/plugins/_publisher_email/config.json b/front/plugins/_publisher_email/config.json new file mode 100755 index 00000000..bc867b7e --- /dev/null +++ b/front/plugins/_publisher_email/config.json @@ -0,0 +1,683 @@ +{ + "code_name": "_publisher_email", + "unique_prefix": "SMTP", + "enabled": true, + "data_source": "script", + "show_ui": true, + "localized": [ + "display_name", + "description", + "icon" + ], + "display_name": [ + { + "language_code": "en_us", + "string": "Email publisher (SMTP)" + }, + { + "language_code": "es_es", + "string": "Habilitar email (SMTP)" + } + ], + "icon": [ + { + "language_code": "en_us", + "string": "" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "A plugin to publish a notification via Email (SMTP) gateway." + } + ], + "params": [], + "database_column_definitions": [ + { + "column": "Index", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + }, + { + "language_code": "es_es", + "string": "N/A" + } + ] + }, + { + "column": "Plugin", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + }, + { + "language_code": "es_es", + "string": "N/A" + } + ] + }, + { + "column": "Object_PrimaryID", + "css_classes": "col-sm-2", + "show": false, + "type": "url", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + } + ] + }, + { + "column": "Object_SecondaryID", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + }, + { + "language_code": "es_es", + "string": "N/A" + } + ] + }, + { + "column": "DateTimeCreated", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Sent when" + } + ] + }, + { + "column": "DateTimeChanged", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Changed" + }, + { + "language_code": "es_es", + "string": "Cambiado" + } + ] + }, + { + "column": "Watched_Value1", + "css_classes": "col-sm-2", + "show": true, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Notification GUID" + } + ] + }, + { + "column": "Watched_Value2", + "css_classes": "col-sm-8", + "show": true, + "type": "textarea_readonly", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Result" + } + ] + }, + { + "column": "Watched_Value3", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + }, + { + "language_code": "es_es", + "string": "N/A" + } + ] + }, + { + "column": "Watched_Value4", + "css_classes": "col-sm-2", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "N/A" + }, + { + "language_code": "es_es", + "string": "N/A" + } + ] + }, + { + "column": "UserData", + "css_classes": "col-sm-2", + "show": false, + "type": "textbox_save", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Comments" + }, + { + "language_code": "es_es", + "string": "Comentarios" + } + ] + }, + { + "column": "Status", + "css_classes": "col-sm-1", + "show": false, + "type": "replace", + "default_value": "", + "options": [ + { + "equals": "watched-not-changed", + "replacement": "
" + }, + { + "equals": "watched-changed", + "replacement": "
" + }, + { + "equals": "new", + "replacement": "
" + }, + { + "equals": "missing-in-last-scan", + "replacement": "
" + } + ], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Status" + }, + { + "language_code": "es_es", + "string": "Estado" + } + ] + }, + { + "column": "Extra", + "css_classes": "col-sm-3", + "show": false, + "type": "label", + "default_value": "", + "options": [], + "localized": [ + "name" + ], + "name": [ + { + "language_code": "en_us", + "string": "Extra" + }, + { + "language_code": "es_es", + "string": "Extra" + } + ] + } + ], + "settings": [ + { + "function": "RUN", + "events": [ + "test" + ], + "type": "text.select", + "default_value": "disabled", + "options": [ + "disabled", + "on_notification" + ], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "When to run" + }, + { + "language_code": "es_es", + "string": "Cuando ejecuta" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Enable sending notifications via the Email (SMTP) gateway." + }, + { + "language_code": "es_es", + "string": "Si está habilitado, se envía un correo electrónico con una lista de cambios a los que se ha suscrito. Complete también todas las configuraciones restantes relacionadas con la configuración de SMTP a continuación" + } + ] + }, + { + "function": "CMD", + "type": "readonly", + "default_value": "python3 /home/pi/pialert/front/plugins/_publisher_email/email_smtp.py", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Command" + }, + { + "language_code": "es_es", + "string": "Comando" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Command to run" + }, + { + "language_code": "es_es", + "string": "Comando a ejecutar" + } + ] + }, + { + "function": "RUN_TIMEOUT", + "type": "integer", + "default_value": 20, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Run timeout" + }, + { + "language_code": "es_es", + "string": "Tiempo de espera de ejecución" + }, + { + "language_code": "de_de", + "string": "Wartezeit" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted." + }, + { + "language_code": "es_es", + "string": "Tiempo máximo en segundos para esperar a que finalice el script. Si se supera este tiempo, el script se cancela." + } + ] + }, + { + "function": "SERVER", + "type": "text", + "default_value": "", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "SMTP server URL" + }, + { + "language_code": "es_es", + "string": "URL del servidor SMTP" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "The SMTP server host URL. For example smtp-relay.sendinblue.com. To use Gmail as an SMTP server follow this guide" + }, + { + "language_code": "es_es", + "string": "La URL del host del servidor SMTP. Por ejemplo, smtp-relay.sendinblue.com. Para utilizar Gmail como servidor SMTP siga esta guía" + } + ] + }, + { + "function": "PORT", + "type": "integer", + "default_value": 587, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "SMTP server PORT" + }, + { + "language_code": "es_es", + "string": "Puerto del servidor SMTP" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Port number used for the SMTP connection. Set to 0 if you do not want to use a port when connecting to the SMTP server." + }, + { + "language_code": "es_es", + "string": "Número de puerto utilizado para la conexión SMTP. Establézcalo en 0 si no desea utilizar un puerto al conectarse al servidor SMTP." + } + ] + }, + { + "function": "SKIP_LOGIN", + "type": "boolean", + "default_value": false, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Skip authentication" + }, + { + "language_code": "es_es", + "string": "Omitir autenticación" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Do not use authentication when connecting to the SMTP server." + }, + { + "language_code": "es_es", + "string": "No utilice la autenticación cuando se conecte al servidor SMTP." + } + ] + }, + { + "function": "USER", + "type": "text", + "default_value": "", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "SMTP user" + }, + { + "language_code": "es_es", + "string": "Nombre de usuario SMTP" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "The user name used to login into the SMTP server (sometimes a full email address)." + }, + { + "language_code": "es_es", + "string": "El nombre de usuario utilizado para iniciar sesión en el servidor SMTP (a veces, una dirección de correo electrónico completa)." + } + ] + }, + { + "function": "PASS", + "type": "password", + "default_value": "", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "SMTP password" + }, + { + "language_code": "es_es", + "string": "Contraseña de SMTP" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "The SMTP server password." + }, + { + "language_code": "es_es", + "string": "La contraseña del servidor SMTP." + } + ] + }, + { + "function": "SKIP_TLS", + "type": "boolean", + "default_value": false, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Do not use TLS" + }, + { + "language_code": "es_es", + "string": "No usar TLS" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Disable TLS when connecting to your SMTP server." + }, + { + "language_code": "es_es", + "string": "Deshabilite TLS cuando se conecte a su servidor SMTP." + } + ] + }, + { + "function": "FORCE_SSL", + "type": "boolean", + "default_value": false, + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Force SSL" + }, + { + "language_code": "es_es", + "string": "Forzar SSL" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Force SSL when connecting to your SMTP server." + }, + { + "language_code": "es_es", + "string": "Forzar SSL al conectarse a su servidor SMTP" + } + ] + }, + { + "function": "REPORT_TO", + "type": "text", + "default_value": "user@gmail.com", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Send email to" + }, + { + "language_code": "es_es", + "string": "Enviar el email a" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Email address to which the notification will be send to." + }, + { + "language_code": "es_es", + "string": "Dirección de correo electrónico a la que se enviará la notificación." + } + ] + }, + { + "function": "REPORT_FROM", + "type": "text", + "default_value": "Pi.Alert ", + "options": [], + "localized": [ + "name", + "description" + ], + "name": [ + { + "language_code": "en_us", + "string": "Email subject" + }, + { + "language_code": "es_es", + "string": "Asunto del email" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Notification email subject line. Some SMTP servers need this to be an email." + }, + { + "language_code": "es_es", + "string": "Asunto del correo electrónico de notificación." + } + ] + } + ] + } \ No newline at end of file diff --git a/front/plugins/_publisher_email/email_smtp.py b/front/plugins/_publisher_email/email_smtp.py new file mode 100755 index 00000000..dfb2d379 --- /dev/null +++ b/front/plugins/_publisher_email/email_smtp.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python +import json +import subprocess +import argparse +import os +import pathlib +import sys +from datetime import datetime +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import smtplib +import socket + +# Replace these paths with the actual paths to your Pi.Alert directories +sys.path.extend(["/home/pi/pialert/front/plugins", "/home/pi/pialert/pialert"]) + +# PiAlert modules +import conf +from plugin_helper import Plugin_Objects +from logger import mylog, append_line_to_file, print_log +from helper import timeNowTZ, noti_obj, get_setting_value, hide_email +from notification import Notification_obj +from database import DB + + +CUR_PATH = str(pathlib.Path(__file__).parent.resolve()) +RESULT_FILE = os.path.join(CUR_PATH, 'last_result.log') + +pluginName = 'SMTP' + +def main(): + + mylog('verbose', [f'[{pluginName}](publisher) In script']) + + # Check if basic config settings supplied + if check_config() == False: + mylog('none', [f'[{pluginName}] Error: Publisher notification gateway not set up correctly. Check your pialert.conf {pluginName}_* variables.']) + return + + # Create a database connection + db = DB() # instance of class DB + db.open() + + # Initialize the Plugin obj output file + plugin_objects = Plugin_Objects(RESULT_FILE) + + # Create a Notification_obj instance + notifications = Notification_obj(db) + + # Retrieve new notifications + new_notifications = notifications.getNew() + + # Process the new notifications + for notification in new_notifications: + + # Send notification + result = send(notification["HTML"], notification["Text"]) + + # Log result + plugin_objects.add_object( + primaryId = pluginName, + secondaryId = timeNowTZ(), + watched1 = notification["GUID"], + watched2 = result, + watched3 = 'null', + watched4 = 'null', + extra = 'null', + foreignKey = 'null' + ) + + plugin_objects.write_result_file() + +#------------------------------------------------------------------------------- +def check_config (): + if get_setting_value('SMTP_SERVER') == '' or get_setting_value('SMTP_REPORT_FROM') == '' or get_setting_value('SMTP_REPORT_TO') == '': + mylog('none', ['[Email Check Config] Error: Email service not set up correctly. Check your pialert.conf SMTP_*, SMTP_REPORT_FROM and SMTP_REPORT_TO variables.']) + return False + else: + return True + +#------------------------------------------------------------------------------- +def send(pHTML, pText): + + mylog('debug', [f'[{pluginName}] SMTP_REPORT_TO: {hide_email(str(get_setting_value("SMTP_REPORT_TO")))} SMTP_USER: {hide_email(str(get_setting_value("SMTP_USER")))}']) + + # Compose email + msg = MIMEMultipart('alternative') + msg['Subject'] = 'Pi.Alert Report' + msg['From'] = get_setting_value('SMTP_REPORT_FROM') + msg['To'] = get_setting_value('SMTP_REPORT_TO') + msg.attach (MIMEText (pText, 'plain')) + msg.attach (MIMEText (pHTML, 'html')) + + failedAt = '' + + failedAt = print_log ('SMTP try') + + try: + # Send mail + failedAt = print_log('Trying to open connection to ' + str(get_setting_value('SMTP_SERVER')) + ':' + str(get_setting_value('SMTP_PORT'))) + + # Set a timeout for the SMTP connection (in seconds) + smtp_timeout = 30 + + if get_setting_value('SMTP_FORCE_SSL'): + failedAt = print_log('SMTP_FORCE_SSL == True so using .SMTP_SSL()') + if get_setting_value('SMTP_PORT') == 0: + failedAt = print_log('SMTP_PORT == 0 so sending .SMTP_SSL(SMTP_SERVER)') + smtp_connection = smtplib.SMTP_SSL(get_setting_value('SMTP_SERVER')) + else: + failedAt = print_log('SMTP_PORT == 0 so sending .SMTP_SSL(SMTP_SERVER, SMTP_PORT)') + smtp_connection = smtplib.SMTP_SSL(get_setting_value('SMTP_SERVER'), get_setting_value('SMTP_PORT'), timeout=smtp_timeout) + + else: + failedAt = print_log('SMTP_FORCE_SSL == False so using .SMTP()') + if get_setting_value('SMTP_PORT') == 0: + failedAt = print_log('SMTP_PORT == 0 so sending .SMTP(SMTP_SERVER)') + smtp_connection = smtplib.SMTP (get_setting_value('SMTP_SERVER')) + else: + failedAt = print_log('SMTP_PORT == 0 so sending .SMTP(SMTP_SERVER, SMTP_PORT)') + smtp_connection = smtplib.SMTP (get_setting_value('SMTP_SERVER'), get_setting_value('SMTP_PORT')) + + failedAt = print_log('Setting SMTP debug level') + + # Log level set to debug of the communication between SMTP server and client + if get_setting_value('LOG_LEVEL') == 'debug': + smtp_connection.set_debuglevel(1) + + failedAt = print_log( 'Sending .ehlo()') + smtp_connection.ehlo() + + if not get_setting_value('SMTP_SKIP_TLS'): + failedAt = print_log('SMTP_SKIP_TLS == False so sending .starttls()') + smtp_connection.starttls() + failedAt = print_log('SMTP_SKIP_TLS == False so sending .ehlo()') + smtp_connection.ehlo() + if not get_setting_value('SMTP_SKIP_LOGIN'): + failedAt = print_log('SMTP_SKIP_LOGIN == False so sending .login()') + smtp_connection.login (get_setting_value('SMTP_USER'), get_setting_value('SMTP_PASS')) + + failedAt = print_log('Sending .sendmail()') + smtp_connection.sendmail (get_setting_value('SMTP_REPORT_FROM'), get_setting_value('SMTP_REPORT_TO'), msg.as_string()) + smtp_connection.quit() + except smtplib.SMTPAuthenticationError as e: + mylog('none', [' ERROR: Failed at - ', failedAt]) + mylog('none', [' ERROR: Couldn\'t connect to the SMTP server (SMTPAuthenticationError), skipping Email (enable LOG_LEVEL=debug for more logging)']) + mylog('none', [' ERROR: ', str(e)]) + except smtplib.SMTPServerDisconnected as e: + mylog('none', [' ERROR: Failed at - ', failedAt]) + mylog('none', [' ERROR: Couldn\'t connect to the SMTP server (SMTPServerDisconnected), skipping Email (enable LOG_LEVEL=debug for more logging)']) + mylog('none', [' ERROR: ', str(e)]) + except socket.gaierror as e: + mylog('none', [' ERROR: Failed at - ', failedAt]) + mylog('none', [' ERROR: Could not resolve hostname (socket.gaierror), skipping Email (enable LOG_LEVEL=debug for more logging)']) + mylog('none', [' ERROR: ', str(e)]) + + mylog('debug', [f'[{pluginName}] Last executed - {str(failedAt)}']) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/front/plugins/_publisher_email/ignore_plugin b/front/plugins/_publisher_email/ignore_plugin deleted file mode 100755 index 77ffa1c1..00000000 --- a/front/plugins/_publisher_email/ignore_plugin +++ /dev/null @@ -1 +0,0 @@ -This plugin will not be loaded \ No newline at end of file diff --git a/front/plugins/internet_speedtest/script.py b/front/plugins/internet_speedtest/script.py index 10b65cc0..8acadaba 100755 --- a/front/plugins/internet_speedtest/script.py +++ b/front/plugins/internet_speedtest/script.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# Based on the work of https://github.com/leiweibau/Pi.Alert import argparse import os diff --git a/front/pluginsCore.php b/front/pluginsCore.php index e30f986f..e0d6fdb6 100755 --- a/front/pluginsCore.php +++ b/front/pluginsCore.php @@ -89,6 +89,11 @@ function processColumnValue(dbColumnDef, value, index, type) { case 'label': value = `${value}`; break; + case 'textarea_readonly': + value = ``; + break; case 'textbox_save': value = value == 'null' ? '' : value; // hide 'null' values diff --git a/front/settings.php b/front/settings.php index d18cbcf1..a6dd7ee8 100755 --- a/front/settings.php +++ b/front/settings.php @@ -112,7 +112,7 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { const settingGroups = []; const settingKeyOfLists = []; // core groups are the ones not generated by plugins - const settingCoreGroups = ['General', 'Email', 'Webhooks', 'Apprise', 'NTFY', 'PUSHSAFER', 'MQTT', 'DynDNS', 'API']; + const settingCoreGroups = ['General']; // Loop through the settingsArray and collect unique settingGroups @@ -749,8 +749,7 @@ while ($row = $result -> fetchArray (SQLITE3_ASSOC)) { // ----------------------------------------------------------------------------- // --------------------------------------------------------- - // Show last time settings have been imported - // getParam("lastImportedTime", "Back_Settings_Imported", skipCache = true); + // Show last time settings have been imported handleLoadingDialog() diff --git a/pialert/__main__.py b/pialert/__main__.py index ae59a7d7..f801cc31 100755 --- a/pialert/__main__.py +++ b/pialert/__main__.py @@ -24,12 +24,13 @@ import multiprocessing import conf from const import * from logger import mylog -from helper import filePermissions, timeNowTZ, updateState, get_setting_value +from helper import filePermissions, timeNowTZ, updateState, get_setting_value, noti_obj from api import update_api from networkscan import process_scan from initialise import importConfigs -from database import DB, get_all_devices -from reporting import send_notifications +from database import DB +from reporting import get_notifications +from notification import Notification_obj from plugin import run_plugin_scripts, check_and_run_user_event @@ -146,8 +147,38 @@ def main (): # run all plugins registered to be run when new devices are found pluginsState = run_plugin_scripts(db, 'on_new_device', pluginsState) + # Notification handling + # ---------------------------------------- + # send all configured notifications - send_notifications(db) + notiStructure = get_notifications(db) + + # Write the notifications into the DB + notification = Notification_obj(db) + notificationObj = notification.create(notiStructure.json, notiStructure.text, notiStructure.html, "") + + # run all enabled publisher gateways + if notificationObj.HasNotifications: + pluginsState = run_plugin_scripts(db, 'on_notification', pluginsState) + notification.setAllProcessed() + + # Clean Pending Alert Events + sql.execute ("""UPDATE Devices SET dev_LastNotification = ? + WHERE dev_MAC IN ( + SELECT eve_MAC FROM Events + WHERE eve_PendingAlertEmail = 1 + ) + """, (timeNowTZ(),) ) + sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 + WHERE eve_PendingAlertEmail = 1""") + + # clear plugin events + sql.execute ("DELETE FROM Plugins_Events") + + # DEBUG - print number of rows updated + mylog('minimal', ['[Notification] Notifications changes: ', sql.rowcount]) + else: + mylog('verbose', ['[Notification] No changes to report']) # Commit SQL db.commitDB() diff --git a/pialert/api.py b/pialert/api.py index 13f21da1..58721fab 100755 --- a/pialert/api.py +++ b/pialert/api.py @@ -3,8 +3,7 @@ import json # pialert modules import conf -from const import (apiPath, sql_devices_all, sql_events_pending_alert, - sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings) +from const import (apiPath, sql_devices_all, sql_events_pending_alert, sql_settings, sql_plugins_events, sql_plugins_history, sql_plugins_objects,sql_language_strings, sql_notifications_all) from logger import mylog from helper import write_file @@ -19,8 +18,6 @@ def update_api(db, isNotification = False, updateOnlyDataSources = []): folder = apiPath - # update notifications moved to reporting send_api() - # Save plugins write_file(folder + 'plugins.json' , json.dumps({"data" : conf.plugins})) @@ -33,6 +30,7 @@ def update_api(db, isNotification = False, updateOnlyDataSources = []): ["plugins_history", sql_plugins_history], ["plugins_objects", sql_plugins_objects], ["plugins_language_strings", sql_language_strings], + ["notifications", sql_notifications_all], ["custom_endpoint", conf.API_CUSTOM_SQL], ] diff --git a/pialert/appevent.py b/pialert/appevent.py new file mode 100644 index 00000000..acbbe245 --- /dev/null +++ b/pialert/appevent.py @@ -0,0 +1,125 @@ +import datetime +import json +import uuid + +# PiAlert modules +import conf +import const +from const import pialertPath, logPath, apiPath +from logger import logResult, mylog, print_log +from helper import timeNowTZ + +#------------------------------------------------------------------------------- +# Execution object handling +#------------------------------------------------------------------------------- +class AppEvent_obj: + def __init__(self, db): + self.db = db + + # Create AppEvent table if missing + self.db.sql.execute("""CREATE TABLE IF NOT EXISTS "AppEvents" ( + "Index" INTEGER, + "GUID" TEXT UNIQUE, + "DateTimeCreated" TEXT, + "ObjectType" TEXT, -- ObjectType (Plugins, Notifications, Events) + "ObjectGUID" TEXT, + "ObjectPlugin" TEXT, + "ObjectMAC" TEXT, + "ObjectIP" TEXT, + "ObjectPrimaryID" TEXT, + "ObjectSecondaryID" TEXT, + "ObjectForeignKey" TEXT, + "ObjectIndex" TEXT, + "ObjectRowID" TEXT, + "ObjectStatusColumn" TEXT, -- Status (Notifications, Plugins), eve_EventType (Events) + "ObjectStatus" TEXT, -- new_devices, down_devices, events, new, watched-changed, watched-not-changed, missing-in-last-scan, Device down, New Device, IP Changed, Connected, Disconnected, VOIDED - Disconnected, VOIDED - Connected, + "AppEventStatus" TEXT, -- TBD "new", "used", "cleanup-next" + "Extra" TEXT, + PRIMARY KEY("Index" AUTOINCREMENT) + ); + """) + + self.save() + + # Create a new DB entry if new notifications are available, otherwise skip + def create(self, Extra="", **kwargs): + # Check if nothing to report, end + if not any(kwargs.values()): + return False + + # Continue and save into DB if notifications are available + self.GUID = str(uuid.uuid4()) + self.DateTimeCreated = timeNowTZ() + self.ObjectType = "Plugins" # Modify ObjectType as needed + + # Optional parameters + self.ObjectGUID = kwargs.get("ObjectGUID", "") + self.ObjectPlugin = kwargs.get("ObjectPlugin", "") + self.ObjectMAC = kwargs.get("ObjectMAC", "") + self.ObjectIP = kwargs.get("ObjectIP", "") + self.ObjectPrimaryID = kwargs.get("ObjectPrimaryID", "") + self.ObjectSecondaryID = kwargs.get("ObjectSecondaryID", "") + self.ObjectForeignKey = kwargs.get("ObjectForeignKey", "") + self.ObjectIndex = kwargs.get("ObjectIndex", "") + self.ObjectRowID = kwargs.get("ObjectRowID", "") + self.ObjectStatusColumn = kwargs.get("ObjectStatusColumn", "") + self.ObjectStatus = kwargs.get("ObjectStatus", "") + + self.AppEventStatus = "new" # Modify AppEventStatus as needed + self.Extra = Extra + + self.upsert() + + return True + + # Update the status of the entry + def updateStatus(self, newStatus): + self.ObjectStatus = newStatus + self.upsert() + + def upsert(self): + self.db.sql.execute(""" + INSERT OR REPLACE INTO AppEvents ( + "GUID", + "DateTimeCreated", + "ObjectType", + "ObjectGUID", + "ObjectPlugin", + "ObjectMAC", + "ObjectIP", + "ObjectPrimaryID", + "ObjectSecondaryID", + "ObjectForeignKey", + "ObjectIndex", + "ObjectRowID", + "ObjectStatusColumn", + "ObjectStatus", + "AppEventStatus", + "Extra" + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + self.GUID, + self.DateTimeCreated, + self.ObjectType, + self.ObjectGUID, + self.ObjectPlugin, + self.ObjectMAC, + self.ObjectIP, + self.ObjectPrimaryID, + self.ObjectSecondaryID, + self.ObjectForeignKey, + self.ObjectIndex, + self.ObjectRowID, + self.ObjectStatusColumn, + self.ObjectStatus, + self.AppEventStatus, + self.Extra + )) + + self.save() + + def save(self): + # Commit changes + self.db.commitDB() + diff --git a/pialert/conf.py b/pialert/conf.py index 266579cd..2078bf7d 100755 --- a/pialert/conf.py +++ b/pialert/conf.py @@ -70,12 +70,6 @@ WEBHOOK_PAYLOAD = 'json' WEBHOOK_REQUEST_METHOD = 'GET' WEBHOOK_SECRET = '' -# Apprise -REPORT_APPRISE = False -APPRISE_HOST = '' -APPRISE_URL = '' -APPRISE_PAYLOAD = 'html' - # NTFY REPORT_NTFY = False NTFY_HOST = 'https://ntfy.sh' diff --git a/pialert/const.py b/pialert/const.py index c5eb0771..cd6daef5 100755 --- a/pialert/const.py +++ b/pialert/const.py @@ -36,6 +36,7 @@ sql_events_pending_alert = "SELECT * FROM Events where eve_PendingAlertEmail is sql_settings = "SELECT * FROM Settings" sql_plugins_objects = "SELECT * FROM Plugins_Objects" sql_language_strings = "SELECT * FROM Plugins_Language_Strings" +sql_notifications_all = "SELECT * FROM Notifications" sql_plugins_events = "SELECT * FROM Plugins_Events" sql_plugins_history = "SELECT * FROM Plugins_History ORDER BY DateTimeChanged DESC" sql_new_devices = """SELECT * FROM ( diff --git a/pialert/database.py b/pialert/database.py index eab5fa05..a9e42e51 100755 --- a/pialert/database.py +++ b/pialert/database.py @@ -6,7 +6,7 @@ import sqlite3 from const import fullDbPath, sql_devices_stats, sql_devices_all from logger import mylog -from helper import json_struc, initOrSetParam, row_to_json, timeNowTZ #, updateState +from helper import json_obj, initOrSetParam, row_to_json, timeNowTZ #, updateState @@ -208,10 +208,7 @@ class DB(): "par_ID" TEXT PRIMARY KEY, "par_Value" TEXT ); - """) - - # Initialize Parameters if unavailable - initOrSetParam(self, 'Back_App_State','Initializing') + """) # ------------------------------------------------------------------------- # Nmap_Scan table setup DEPRECATED after 1/1/2024 @@ -384,7 +381,7 @@ class DB(): rows = self.sql.fetchall() except sqlite3.Error as e: mylog('none',[ '[Database] - SQL ERROR: ', e]) - return None + return json_obj({}, []) # return empty object result = {"data":[]} for row in rows: @@ -392,7 +389,7 @@ class DB(): result["data"].append(tmp) # mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames]) - return json_struc(result, columnNames) + return json_obj(result, columnNames) #------------------------------------------------------------------------------- # referece from here: https://codereview.stackexchange.com/questions/241043/interface-class-for-sqlite-databases diff --git a/pialert/device.py b/pialert/device.py index 580e52fa..5ed3d7ab 100755 --- a/pialert/device.py +++ b/pialert/device.py @@ -14,17 +14,10 @@ def save_scanned_devices (db): sql = db.sql #TO-DO - # #76 Add Local MAC of default local interface - # BUGFIX #106 - Device that pialert is running - # local_mac_cmd = ["bash -lc ifconfig `ip route list default | awk {'print $5'}` | grep ether | awk '{print $2}'"] - # local_mac_cmd = ["/sbin/ifconfig `ip route list default | sort -nk11 | head -1 | awk {'print $5'}` | grep ether | awk '{print $2}'"] + # Add Local MAC of default local interface local_mac_cmd = ["/sbin/ifconfig `ip -o route get 1 | sed 's/^.*dev \\([^ ]*\\).*$/\\1/;q'` | grep ether | awk '{print $2}'"] local_mac = subprocess.Popen (local_mac_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0].decode().strip() - # local_dev_cmd = ["ip -o route get 1 | sed 's/^.*dev \\([^ ]*\\).*$/\\1/;q'"] - # local_dev = subprocess.Popen (local_dev_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0].decode().strip() - - # local_ip_cmd = ["ip route list default | awk {'print $7'}"] local_ip_cmd = ["ip -o route get 1 | sed 's/^.*src \\([^ ]*\\).*$/\\1/;q'"] local_ip = subprocess.Popen (local_ip_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate()[0].decode().strip() @@ -149,7 +142,7 @@ def create_new_devices (db): FROM CurrentScan""" - # mylog('debug',f'[New Devices] 2 Create devices SQL: {sqlQuery}') + mylog('debug',f'[New Devices] 2 Create devices SQL: {sqlQuery}') sql.execute (sqlQuery, (startTime, startTime) ) diff --git a/pialert/helper.py b/pialert/helper.py index 676bc52b..4a36c8b5 100755 --- a/pialert/helper.py +++ b/pialert/helper.py @@ -243,36 +243,60 @@ def write_file(pPath, pText): #------------------------------------------------------------------------------- # Return whole setting touple def get_setting(key): - result = None - # index order: key, name, desc, inputtype, options, regex, result, group, events - for set in conf.mySettings: - if set[0] == key: - result = set - - if result is None: - mylog('minimal', [' Error - setting_missing - Setting not found for key: ', key]) - mylog('minimal', [' Error - logging the settings into file: ', logPath + '/setting_missing.json']) - write_file (logPath + '/setting_missing.json', json.dumps({ 'data' : conf.mySettings})) - return result + settingsFile = apiPath + 'table_settings.json' + + try: + with open(settingsFile, 'r') as json_file: + + data = json.load(json_file) + + for item in data.get("data",[]): + if item.get("Code_Name") == key: + return item + + mylog('debug', [f'[Settings] Error - setting_missing - Setting not found for key: {key} in file {settingsFile}']) + + return None + + 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 + mylog('none', [f'[Settings] Error - JSONDecodeError or FileNotFoundError for file {settingsFile}']) + + return None + + #------------------------------------------------------------------------------- # Return setting value 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[6] # setting value - setTyp = set[3] # setting type + set_value = setting["Value"] # Setting value + 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 '' + #------------------------------------------------------------------------------- # IP validation methods #------------------------------------------------------------------------------- @@ -613,24 +637,15 @@ def initOrSetParam(db, parID, parValue): db.commitDB() #------------------------------------------------------------------------------- -class json_struc: +class json_obj: def __init__(self, jsn, columnNames): self.json = jsn self.columnNames = columnNames #------------------------------------------------------------------------------- -class noti_struc: - def __init__(self, json, text, html, notificationType): +class noti_obj: + def __init__(self, json, text, html): self.json = json self.text = text 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/initialise.py b/pialert/initialise.py index 233bdc0c..83d24be7 100755 --- a/pialert/initialise.py +++ b/pialert/initialise.py @@ -6,6 +6,9 @@ from cron_converter import Cron from pathlib import Path import datetime import json +import shutil +import re + import conf from const import fullConfPath @@ -62,6 +65,9 @@ def importConfigs (db): # Only import file if the file was modifed since last import. # this avoids time zone issues as we just compare the previous timestamp to the current time stamp + # rename settings that have changed names due to code cleanup and migration to plugins + renameSettings(config_file) + fileModifiedTime = os.path.getmtime(config_file) mylog('debug', ['[Import Config] checking config file ']) @@ -114,18 +120,6 @@ def importConfigs (db): # Notification gateways # ---------------------------------------- - # Email - conf.REPORT_MAIL = ccd('REPORT_MAIL', False , c_d, 'Enable email', 'boolean', '', 'Email', ['test']) - conf.SMTP_SERVER = ccd('SMTP_SERVER', '' , c_d,'SMTP server URL', 'text', '', 'Email') - conf.SMTP_PORT = ccd('SMTP_PORT', 587 , c_d, 'SMTP port', 'integer', '', 'Email') - conf.REPORT_TO = ccd('REPORT_TO', 'user@gmail.com' , c_d, 'Email to', 'text', '', 'Email') - conf.REPORT_FROM = ccd('REPORT_FROM', 'Pi.Alert ' , c_d, 'Email Subject', 'text', '', 'Email') - conf.SMTP_SKIP_LOGIN = ccd('SMTP_SKIP_LOGIN', False , c_d, 'SMTP skip login', 'boolean', '', 'Email') - conf.SMTP_USER = ccd('SMTP_USER', '' , c_d, 'SMTP user', 'text', '', 'Email') - conf.SMTP_PASS = ccd('SMTP_PASS', '' , c_d, 'SMTP password', 'password', '', 'Email') - conf.SMTP_SKIP_TLS = ccd('SMTP_SKIP_TLS', False , c_d, 'SMTP skip TLS', 'boolean', '', 'Email') - conf.SMTP_FORCE_SSL = ccd('SMTP_FORCE_SSL', False , c_d, 'Force SSL', 'boolean', '', 'Email') - # Webhooks conf.REPORT_WEBHOOK = ccd('REPORT_WEBHOOK', False , c_d, 'Enable Webhooks', 'boolean', '', 'Webhooks', ['test']) conf.WEBHOOK_URL = ccd('WEBHOOK_URL', '' , c_d, 'Target URL', 'text', '', 'Webhooks') @@ -134,13 +128,6 @@ def importConfigs (db): conf.WEBHOOK_SIZE = ccd('WEBHOOK_SIZE', 1024 , c_d, 'Payload size', 'integer', '', 'Webhooks') conf.WEBHOOK_SECRET = ccd('WEBHOOK_SECRET', '' , c_d, 'Secret', 'text', '', 'Webhooks') - # Apprise - conf.REPORT_APPRISE = ccd('REPORT_APPRISE', False , c_d, 'Enable Apprise', 'boolean', '', 'Apprise', ['test']) - conf.APPRISE_HOST = ccd('APPRISE_HOST', '' , c_d, 'Apprise host URL', 'text', '', 'Apprise') - conf.APPRISE_URL = ccd('APPRISE_URL', '' , c_d, 'Apprise notification URL', 'text', '', 'Apprise') - conf.APPRISE_PAYLOAD = ccd('APPRISE_PAYLOAD', 'html' , c_d, 'Payload type', 'text.select', "['html', 'text']", 'Apprise') - conf.APPRISE_SIZE = ccd('APPRISE_SIZE', 1024 , c_d, 'Payload size', 'integer', '', 'Apprise') - # NTFY conf.REPORT_NTFY = ccd('REPORT_NTFY', False , c_d, 'Enable NTFY', 'boolean', '', 'NTFY', ['test']) conf.NTFY_HOST = ccd('NTFY_HOST', 'https://ntfy.sh' , c_d, 'NTFY host URL', 'text', '', 'NTFY') @@ -261,9 +248,6 @@ def importConfigs (db): sql.execute ("DELETE FROM Settings") sql.executemany ("""INSERT INTO Settings ("Code_Name", "Display_Name", "Description", "Type", "Options", "RegEx", "Value", "Group", "Events" ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", conf.mySettingsSQLsafe) - - # Is used to display a message in the UI when old (outdated) settings are loaded - initOrSetParam(db, "Back_Settings_Imported",(round(time.time() * 1000),) ) #commitDB(sql_connection) db.commitDB() @@ -293,4 +277,56 @@ def read_config_file(filename): code = compile(filename.read_text(), filename.name, "exec") confDict = {} # config dictionary exec(code, {"__builtins__": {}}, confDict) - return confDict \ No newline at end of file + return confDict + + +#------------------------------------------------------------------------------- +replacements = { + r'\bREPORT_TO\b': 'SMTP_REPORT_TO', + r'\bREPORT_FROM\b': 'SMTP_REPORT_FROM' +} + +def renameSettings(config_file): + # Check if the file contains any of the old setting code names + contains_old_settings = False + + # Open the original config_file for reading + with open(str(config_file), 'r') as original_file: # Convert config_file to a string + for line in original_file: + # Use regular expressions with word boundaries to check for the old setting code names + if any(re.search(key, line) for key in replacements.keys()): + contains_old_settings = True + break # Exit the loop if any old setting is found + + # If the file contains old settings, proceed with renaming and backup + if contains_old_settings: + # Create a backup file with the suffix "_old_setting_names" and timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + backup_file = f"{config_file}_old_setting_names_{timestamp}.bak" + + mylog('debug', f'[Config] Old setting names will be replaced and a backup ({backup_file}) of the config created.') + + shutil.copy(str(config_file), backup_file) # Convert config_file to a string + + # Open the original config_file for reading and create a temporary file for writing + with open(str(config_file), 'r') as original_file, open(str(config_file) + "_temp", 'w') as temp_file: # Convert config_file to a string + for line in original_file: + # Use regular expressions with word boundaries for replacements + for key, value in replacements.items(): + line = re.sub(key, value, line) + + # Write the modified line to the temporary file + temp_file.write(line) + + # Close both files + original_file.close() + temp_file.close() + + # Replace the original config_file with the temporary file + shutil.move(str(config_file) + "_temp", str(config_file)) # Convert config_file to a string + else: + mylog('debug', '[Config] No old setting names found in the file. No changes made.') + + + + \ No newline at end of file diff --git a/pialert/notification.py b/pialert/notification.py new file mode 100644 index 00000000..82c474a6 --- /dev/null +++ b/pialert/notification.py @@ -0,0 +1,114 @@ +import datetime +import json +import uuid + +# PiAlert modules +import conf +import const +from const import pialertPath, logPath, apiPath +from logger import logResult, mylog, print_log +from helper import timeNowTZ + +#------------------------------------------------------------------------------- +# Notification object handling +#------------------------------------------------------------------------------- +class Notification_obj: + 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() + + # Create a new DB entry if new notiifcations available, otherwise skip + def create(self, JSON, Text, HTML, Extra=""): + + # Check if nothing to report, end + if JSON["internet"] == [] and JSON["new_devices"] == [] and JSON["down_devices"] == [] and JSON["events"] == [] and JSON["plugins"] == []: + 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 = Text + self.HTML = HTML + self.PublishedVia = "" + self.Extra = Extra + + if self.HasNotifications: + 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() + + + + def save(self): + # Commit changes + self.db.commitDB() \ No newline at end of file diff --git a/pialert/plugin.py b/pialert/plugin.py index 6dea2247..e0a2f8f1 100755 --- a/pialert/plugin.py +++ b/pialert/plugin.py @@ -9,11 +9,12 @@ from collections import namedtuple # pialert modules import conf -from const import pluginsPath, logPath +from const import pluginsPath, logPath, pialertPath from logger import mylog from helper import timeNowTZ, updateState, get_file_content, write_file, get_setting, get_setting_value from api import update_api -from plugin_utils import logEventStatusCounts, get_plugin_string, get_plugin_setting, print_plugin_info, flatten_array, 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 #------------------------------------------------------------------------------- @@ -27,8 +28,8 @@ class plugin_param: inputValue = get_setting(param["value"]) if inputValue != None: - setVal = inputValue[6] # setting value - setTyp = inputValue[3] # setting type + setVal = inputValue["Value"] # setting value + setTyp = inputValue["Type"] # setting type noConversion = ['text', 'string', 'integer', 'boolean', 'password', 'readonly', 'integer.select', 'text.select', 'integer.checkbox' ] arrayConversion = ['text.multiselect', 'list', 'subnets'] @@ -45,11 +46,8 @@ class plugin_param: elif setTyp in arrayConversion: # make them safely passable to a python or linux script - resolved = flatten_array(setVal) + resolved = list_to_csv(setVal) - elif setTyp in arrayConversionBase64: - # make them safely passable to a python or linux script by converting them to a base64 string if necessary (if the arg contains spaces) - resolved = flatten_array(setVal) else: for item in jsonConversion: if setTyp.endswith(item): @@ -66,7 +64,7 @@ class plugin_param: paramValuesCount = len(inputValue) # make them safely passable to a python or linux script - resolved = flatten_array(inputValue) + resolved = list_to_csv(inputValue) mylog('debug', f'[Plugins] Resolved value: {resolved}') @@ -487,10 +485,7 @@ def process_plugin_events(db, plugin, pluginsState, plugEventsArr): history_to_insert = [] 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: # keep old createdTime time if the plugObj already was created before createdTime = plugObj.changed if plugObj.status == 'new' else plugObj.created @@ -508,6 +503,8 @@ def process_plugin_events(db, plugin, pluginsState, plugEventsArr): objects_to_update.append(values + (plugObj.index,)) # Include index for UPDATE # 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: events_to_insert.append(values) @@ -750,7 +747,7 @@ def check_and_run_user_event(db, pluginsState): return pluginsState if event == 'test': - handle_test(param) + pluginsState = handle_test(param, db, pluginsState) if event == 'run': pluginsState = handle_run(param, db, pluginsState) @@ -778,34 +775,26 @@ def handle_run(runType, db, pluginsState): #------------------------------------------------------------------------------- -def handle_test(testType): +def handle_test(runType, db, pluginsState): - mylog('minimal', ['[', timeNowTZ(), '] START Test: ', testType]) + mylog('minimal', ['[', timeNowTZ(), '] [Test] START Test: ', runType]) + + # 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"] + + # Create fake notification + notification = Notification_obj(db) + notificationObj = notification.create(sample_json, sample_txt, sample_html, "") - # Open text sample - sample_txt = get_file_content(pialertPath + '/back/report_sample.txt') + # Run test + pluginsState = handle_run(runType, db, pluginsState) - # Open html sample - sample_html = get_file_content(pialertPath + '/back/report_sample.html') + # Remove sample notification + notificationObj.remove(notificationObj.GUID) - # 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"] + mylog('minimal', ['[Test] END Test: ', runType]) - 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]) + return pluginsState diff --git a/pialert/plugin_utils.py b/pialert/plugin_utils.py index ed85cf8d..fc914353 100755 --- a/pialert/plugin_utils.py +++ b/pialert/plugin_utils.py @@ -71,30 +71,39 @@ def get_plugin_string(props, el): #------------------------------------------------------------------------------- -def flatten_array(arr): +# generates a comma separated list of values from a list (or a string representing a list) +def list_to_csv(arr): tmp = '' arrayItemStr = '' - mylog('debug', '[Plugins] Flattening the below array') - - mylog('debug', arr) + mylog('debug', '[Plugins] Flattening the below array') + mylog('debug', arr) + mylog('debug', f'[Plugins] isinstance(arr, list) : {isinstance(arr, list)} | isinstance(arr, str) : {isinstance(arr, str)}') - for arrayItem in arr: - # only one column flattening is supported - if isinstance(arrayItem, list): - arrayItemStr = str(arrayItem[0]).replace("'", '') # removing single quotes - not allowed - else: - # is string already - arrayItemStr = arrayItem + if isinstance(arr, str): + return arr.replace('[','').replace(']','').replace("'", '') # removing brackets and single quotes (not allowed) + + elif isinstance(arr, list): + for arrayItem in arr: + # only one column flattening is supported + if isinstance(arrayItem, list): + arrayItemStr = str(arrayItem[0]).replace("'", '') # removing single quotes - not allowed + else: + # is string already + arrayItemStr = arrayItem - tmp += f'{arrayItemStr},' + tmp += f'{arrayItemStr},' - tmp = tmp[:-1] # Remove last comma ',' + tmp = tmp[:-1] # Remove last comma ',' - mylog('debug', f'[Plugins] Flattened array: {tmp}') + mylog('debug', f'[Plugins] Flattened array: {tmp}') + + return tmp + + else: + mylog('none', f'[Plugins] ERROR Could not convert array: {arr}') - return tmp @@ -160,18 +169,6 @@ def get_plugins_configs(): 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): return namedtuple('X', pluginDict.keys())(*pluginDict.values()) diff --git a/pialert/publishers/apprise.py b/pialert/publishers/apprise.py deleted file mode 100755 index 0f2ed35c..00000000 --- a/pialert/publishers/apprise.py +++ /dev/null @@ -1,58 +0,0 @@ - -import json -import subprocess -import conf -from helper import noti_struc -from logger import logResult, mylog - -#------------------------------------------------------------------------------- -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 - logResult (stdout, stderr) # TO-DO should be changed to mylog - - # 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]) \ No newline at end of file diff --git a/pialert/publishers/email.py b/pialert/publishers/email.py deleted file mode 100755 index c957d006..00000000 --- a/pialert/publishers/email.py +++ /dev/null @@ -1,100 +0,0 @@ -""" Pi.Alert module to send notification emails """ - -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -import smtplib - - -import conf -import socket -from helper import hide_email, noti_struc -from logger import mylog, print_log - -#------------------------------------------------------------------------------- -def check_config (): - if conf.SMTP_SERVER == '' or conf.REPORT_FROM == '' or conf.REPORT_TO == '': - mylog('none', ['[Email Check Config] Error: Email service not set up correctly. Check your pialert.conf SMTP_*, REPORT_FROM and REPORT_TO variables.']) - return False - else: - return True - -#------------------------------------------------------------------------------- -def send (msg: noti_struc): - - pText = msg.text - pHTML = msg.html - - mylog('debug', '[Send Email] REPORT_TO: ' + hide_email(str(conf.REPORT_TO)) + ' SMTP_USER: ' + hide_email(str(conf.SMTP_USER))) - - # Compose email - msg = MIMEMultipart('alternative') - msg['Subject'] = 'Pi.Alert Report' - msg['From'] = conf.REPORT_FROM - msg['To'] = conf.REPORT_TO - msg.attach (MIMEText (pText, 'plain')) - msg.attach (MIMEText (pHTML, 'html')) - - failedAt = '' - - failedAt = print_log ('SMTP try') - - try: - # Send mail - failedAt = print_log('Trying to open connection to ' + str(conf.SMTP_SERVER) + ':' + str(conf.SMTP_PORT)) - - # Set a timeout for the SMTP connection (in seconds) - smtp_timeout = 30 - - if conf.SMTP_FORCE_SSL: - failedAt = print_log('SMTP_FORCE_SSL == True so using .SMTP_SSL()') - if conf.SMTP_PORT == 0: - failedAt = print_log('SMTP_PORT == 0 so sending .SMTP_SSL(SMTP_SERVER)') - smtp_connection = smtplib.SMTP_SSL(conf.SMTP_SERVER) - else: - failedAt = print_log('SMTP_PORT == 0 so sending .SMTP_SSL(SMTP_SERVER, SMTP_PORT)') - smtp_connection = smtplib.SMTP_SSL(conf.SMTP_SERVER, conf.SMTP_PORT, timeout=smtp_timeout) - - else: - failedAt = print_log('SMTP_FORCE_SSL == False so using .SMTP()') - if conf.SMTP_PORT == 0: - failedAt = print_log('SMTP_PORT == 0 so sending .SMTP(SMTP_SERVER)') - smtp_connection = smtplib.SMTP (conf.SMTP_SERVER) - else: - failedAt = print_log('SMTP_PORT == 0 so sending .SMTP(SMTP_SERVER, SMTP_PORT)') - smtp_connection = smtplib.SMTP (conf.SMTP_SERVER, conf.SMTP_PORT) - - failedAt = print_log('Setting SMTP debug level') - - # Log level set to debug of the communication between SMTP server and client - if conf.LOG_LEVEL == 'debug': - smtp_connection.set_debuglevel(1) - - failedAt = print_log( 'Sending .ehlo()') - smtp_connection.ehlo() - - if not conf.SMTP_SKIP_TLS: - failedAt = print_log('SMTP_SKIP_TLS == False so sending .starttls()') - smtp_connection.starttls() - failedAt = print_log('SMTP_SKIP_TLS == False so sending .ehlo()') - smtp_connection.ehlo() - if not conf.SMTP_SKIP_LOGIN: - failedAt = print_log('SMTP_SKIP_LOGIN == False so sending .login()') - smtp_connection.login (conf.SMTP_USER, conf.SMTP_PASS) - - failedAt = print_log('Sending .sendmail()') - smtp_connection.sendmail (conf.REPORT_FROM, conf.REPORT_TO, msg.as_string()) - smtp_connection.quit() - except smtplib.SMTPAuthenticationError as e: - mylog('none', [' ERROR: Failed at - ', failedAt]) - mylog('none', [' ERROR: Couldn\'t connect to the SMTP server (SMTPAuthenticationError), skipping Email (enable LOG_LEVEL=debug for more logging)']) - mylog('none', [' ERROR: ', str(e)]) - except smtplib.SMTPServerDisconnected as e: - mylog('none', [' ERROR: Failed at - ', failedAt]) - mylog('none', [' ERROR: Couldn\'t connect to the SMTP server (SMTPServerDisconnected), skipping Email (enable LOG_LEVEL=debug for more logging)']) - mylog('none', [' ERROR: ', str(e)]) - except socket.gaierror as e: - mylog('none', [' ERROR: Failed at - ', failedAt]) - mylog('none', [' ERROR: Could not resolve hostname (socket.gaierror), skipping Email (enable LOG_LEVEL=debug for more logging)']) - mylog('none', [' ERROR: ', str(e)]) - - mylog('debug', '[Send Email] Last executed - ' + str(failedAt)) \ No newline at end of file diff --git a/pialert/publishers/ntfy.py b/pialert/publishers/ntfy.py index 957657cf..fd613f78 100755 --- a/pialert/publishers/ntfy.py +++ b/pialert/publishers/ntfy.py @@ -4,7 +4,7 @@ import requests from base64 import b64encode from logger import mylog -from helper import noti_struc +from helper import noti_obj #------------------------------------------------------------------------------- def check_config(): @@ -15,7 +15,7 @@ def check_config(): return True #------------------------------------------------------------------------------- -def send (msg: noti_struc): +def send (msg: noti_obj): headers = { "Title": "Pi.Alert Notification", diff --git a/pialert/publishers/pushsafer.py b/pialert/publishers/pushsafer.py index b8252209..eaf97c57 100755 --- a/pialert/publishers/pushsafer.py +++ b/pialert/publishers/pushsafer.py @@ -3,7 +3,7 @@ import requests import conf -from helper import noti_struc +from helper import noti_obj from logger import mylog #------------------------------------------------------------------------------- @@ -15,7 +15,7 @@ def check_config(): return True #------------------------------------------------------------------------------- -def send ( msg:noti_struc ): +def send ( msg:noti_obj ): _Text = msg.text url = 'https://www.pushsafer.com/api' post_fields = { diff --git a/pialert/publishers/webhook.py b/pialert/publishers/webhook.py index d530ac7a..7397e714 100755 --- a/pialert/publishers/webhook.py +++ b/pialert/publishers/webhook.py @@ -5,7 +5,7 @@ import hmac import conf from const import logPath -from helper import noti_struc, write_file +from helper import noti_obj, write_file from logger import logResult, mylog #------------------------------------------------------------------------------- @@ -18,7 +18,7 @@ def check_config(): #------------------------------------------------------------------------------- -def send (msg: noti_struc): +def send (msg: noti_obj): # limit = 1024 * 1024 # 1MB limit (1024 bytes * 1024 bytes = 1MB) limit = conf.WEBHOOK_SIZE diff --git a/pialert/reporting.py b/pialert/reporting.py index 0129c425..510af85e 100755 --- a/pialert/reporting.py +++ b/pialert/reporting.py @@ -12,9 +12,7 @@ import datetime import json - import socket - import subprocess import requests from json2table import convert @@ -23,15 +21,11 @@ from json2table import convert import conf import const from const import pialertPath, logPath, apiPath -from helper import noti_struc, generate_mac_links, removeDuplicateNewLines, timeNowTZ, hide_email, updateState, get_file_content, write_file +from helper import noti_obj, generate_mac_links, removeDuplicateNewLines, timeNowTZ, hide_email, updateState, get_file_content, write_file from logger import logResult, mylog, print_log -from publishers.email import (check_config as email_check_config, - send as send_email ) from publishers.ntfy import (check_config as ntfy_check_config, send as send_ntfy ) -from publishers.apprise import (check_config as apprise_check_config, - send as send_apprise) from publishers.webhook import (check_config as webhook_check_config, send as send_webhook) from publishers.pushsafer import (check_config as pushsafer_check_config, @@ -50,10 +44,10 @@ json_final = [] #------------------------------------------------------------------------------- -def construct_notifications(db, sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None, notificationType=''): +def construct_notifications(db, sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None): if suppliedJsonStruct is None and sqlQuery == "": - return noti_struc("", "", "", notificationType) + return noti_obj("", "", "") 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' " @@ -63,11 +57,11 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied text_line = '{}\t{}\n' if suppliedJsonStruct is None: - json_struc = db.get_table_as_json(sqlQuery) + json_obj = db.get_table_as_json(sqlQuery) else: - json_struc = suppliedJsonStruct + json_obj = suppliedJsonStruct - jsn = json_struc.json + jsn = json_obj.json html = "" text = "" @@ -80,7 +74,7 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied # Cleanup the generated HTML table notification html = format_table(html, "data", headerProps, tableTitle).replace('
    ','
      ').replace("null", "") - headers = json_struc.columnNames + headers = json_obj.columnNames # prepare text-only message if skipText == False: @@ -97,7 +91,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, notificationType) + notiStruc = noti_obj(jsn, text, html) if not notiStruc.json['data'] and not notiStruc.text and not notiStruc.html: @@ -108,7 +102,7 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied return notiStruc -def send_notifications (db): +def get_notifications (db): sql = db.sql #TO-DO global mail_text, mail_html, json_final, partial_html, partial_txt, partial_json @@ -184,53 +178,50 @@ def send_notifications (db): if 'new_devices' in conf.INCLUDED_SECTIONS : # Compose New Devices Section - sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments, dev_Vendor as "Device Vendor" - FROM Events_Devices + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'New Device' ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "New devices", "new_devices") + notiStruc = construct_notifications(db, sqlQuery, "New devices") # collect "new_devices" for the webhook json json_new_devices = notiStruc.json["data"] - mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) mylog('verbose', ['[Notification] New Devices sections done.']) if 'down_devices' in conf.INCLUDED_SECTIONS : # Compose Devices Down Section - sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments, dev_Vendor as "Device Vendor" - FROM Events_Devices + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'Device Down' ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "Down devices", "down_Devices") + notiStruc = construct_notifications(db, sqlQuery, "Down devices") # collect "down_devices" for the webhook json json_down_devices = notiStruc.json["data"] - mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) mylog('verbose', ['[Notification] Down Devices sections done.']) if 'events' in conf.INCLUDED_SECTIONS : # Compose Events Section - sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments, dev_Vendor as "Device Vendor" - FROM Events_Devices + sqlQuery = """SELECT eve_MAC as MAC, eve_DateTime as Datetime, dev_LastIP as IP, eve_EventType as "Event Type", dev_Name as "Device name", dev_Comments as Comments FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType IN ('Connected','Disconnected', 'IP Changed') ORDER BY eve_DateTime""" - notiStruc = construct_notifications(db, sqlQuery, "Events", "events") + notiStruc = construct_notifications(db, sqlQuery, "Events") # collect "events" for the webhook json json_events = notiStruc.json["data"] - mail_text = mail_text.replace ('', notiStruc.text + '\n') + mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) mylog('verbose', ['[Notification] Events sections done.']) @@ -250,116 +241,67 @@ def send_notifications (db): plugins_report = len(json_plugins) > 0 mylog('verbose', ['[Notification] Plugins sections done.']) - json_final = { + final_json = { "internet": json_internet, "new_devices": json_new_devices, "down_devices": json_down_devices, - "events": json_events, - "ports": json_ports, + "events": json_events, "plugins": json_plugins, } - mail_text = removeDuplicateNewLines(mail_text) + final_text = removeDuplicateNewLines(mail_text) # Create clickable MAC links - mail_html = generate_mac_links (mail_html, deviceUrl) + final_html = generate_mac_links (mail_html, deviceUrl) # Write output emails for debug - write_file (logPath + '/report_output.json', json.dumps(json_final)) - write_file (logPath + '/report_output.txt', mail_text) - write_file (logPath + '/report_output.html', mail_html) + write_file (logPath + '/report_output.json', json.dumps(final_json)) + write_file (logPath + '/report_output.txt', final_text) + write_file (logPath + '/report_output.html', final_html) - # Write the notifications into the DB - # TODO + mylog('minimal', ['[Notification] Udating API files']) + send_api() - # 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, 'master') - - mylog('minimal', ['[Notification] Udating API files']) - send_api() - - if conf.REPORT_MAIL and check_config('email'): - updateState("Send: Email") - mylog('minimal', ['[Notification] Sending report by Email']) - send_email (msg ) - else : - 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'): - updateState("Send: Webhook") - mylog('minimal', ['[Notification] Sending report by Webhook']) - send_webhook (msg) - else : - mylog('verbose', ['[Notification] Skip webhook']) - if conf.REPORT_NTFY and check_config('ntfy'): - updateState("Send: NTFY") - mylog('minimal', ['[Notification] Sending report by NTFY']) - send_ntfy (msg) - else : - mylog('verbose', ['[Notification] Skip NTFY']) - if conf.REPORT_PUSHSAFER and check_config('pushsafer'): - updateState("Send: PUSHSAFER") - mylog('minimal', ['[Notification] Sending report by PUSHSAFER']) - send_pushsafer (msg) - else : - mylog('verbose', ['[Notification] Skip PUSHSAFER']) - # Update MQTT entities - if conf.REPORT_MQTT and check_config('mqtt'): - updateState("Send: MQTT") - mylog('minimal', ['[Notification] Establishing MQTT thread']) - mqtt_start(db) - else : - mylog('verbose', ['[Notification] Skip MQTT']) - else : - mylog('verbose', ['[Notification] No changes to report']) - - # Clean Pending Alert Events - sql.execute ("""UPDATE Devices SET dev_LastNotification = ? - WHERE dev_MAC IN (SELECT eve_MAC FROM Events - WHERE eve_PendingAlertEmail = 1) - """, (datetime.datetime.now(conf.tz),) ) - sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 - WHERE eve_PendingAlertEmail = 1""") - - # clear plugin events - sql.execute ("DELETE FROM Plugins_Events") - - # DEBUG - print number of rows updated - mylog('minimal', ['[Notification] Notifications changes: ', sql.rowcount]) - - # Commit changes - db.commitDB() + return noti_obj(final_json, final_text, final_html) -#------------------------------------------------------------------------------- -def check_config(service): - if service == 'email': - return email_check_config() + # if conf.REPORT_MAIL and check_config('email'): + # updateState("Send: Email") + # mylog('minimal', ['[Notification] Sending report by Email']) + # send_email (msg ) + # else : + # mylog('verbose', ['[Notification] Skip email']) + # - if service == 'apprise': - return apprise_check_config() - - if service == 'webhook': - return webhook_check_config() + # if conf.REPORT_WEBHOOK and check_config('webhook'): + # updateState("Send: Webhook") + # mylog('minimal', ['[Notification] Sending report by Webhook']) + # send_webhook (msg) + # else : + # mylog('verbose', ['[Notification] Skip webhook']) + # if conf.REPORT_NTFY and check_config('ntfy'): + # updateState("Send: NTFY") + # mylog('minimal', ['[Notification] Sending report by NTFY']) + # send_ntfy (msg) + # else : + # mylog('verbose', ['[Notification] Skip NTFY']) + # if conf.REPORT_PUSHSAFER and check_config('pushsafer'): + # updateState("Send: PUSHSAFER") + # mylog('minimal', ['[Notification] Sending report by PUSHSAFER']) + # send_pushsafer (msg) + # else : + # mylog('verbose', ['[Notification] Skip PUSHSAFER']) + # # Update MQTT entities + # if conf.REPORT_MQTT and check_config('mqtt'): + # updateState("Send: MQTT") + # mylog('minimal', ['[Notification] Establishing MQTT thread']) + # mqtt_start(db) + # else : + # mylog('verbose', ['[Notification] Skip MQTT']) + # else : + # mylog('verbose', ['[Notification] No changes to report']) - if service == 'ntfy': - return ntfy_check_config () - - if service == 'pushsafer': - return pushsafer_check_config() - - if service == 'mqtt': - return mqtt_check_config() #------------------------------------------------------------------------------- # Replacing table headers @@ -438,56 +380,6 @@ 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() - -