diff --git a/pialert/helper.py b/pialert/helper.py index e1d5881f..58006d15 100644 --- a/pialert/helper.py +++ b/pialert/helper.py @@ -13,7 +13,7 @@ import time from pathlib import Path import requests -import conf +import conf from const import * from logger import mylog, logResult @@ -27,29 +27,29 @@ def timeNowTZ(): return datetime.datetime.now(conf.tz).replace(microsecond=0) #------------------------------------------------------------------------------- -def updateState(db, newState): +def updateState(db, newState): # ?? Why is the state written to the DB? - + #sql = db.sql mylog('debug', '[updateState] changing state to: "' + newState +'"') - db.sql.execute ("UPDATE Parameters SET par_Value='"+ newState +"' WHERE par_ID='Back_App_State'") + db.sql.execute ("UPDATE Parameters SET par_Value='"+ newState +"' WHERE par_ID='Back_App_State'") db.commitDB() #------------------------------------------------------------------------------- def updateSubnets(scan_subnets): - subnets = [] + subnets = [] # multiple interfaces - if type(scan_subnets) is list: - for interface in scan_subnets : + if type(scan_subnets) is list: + for interface in scan_subnets : subnets.append(interface) # one interface only - else: - subnets.append(scan_subnets) + else: + subnets.append(scan_subnets) - return subnets + return subnets @@ -57,7 +57,7 @@ def updateSubnets(scan_subnets): # check RW access of DB and config file def checkPermissionsOK(): #global confR_access, confW_access, dbR_access, dbW_access - + confR_access = (os.access(fullConfPath, os.R_OK)) confW_access = (os.access(fullConfPath, os.W_OK)) dbR_access = (os.access(fullDbPath, os.R_OK)) @@ -72,14 +72,14 @@ def checkPermissionsOK(): mylog('none', [ " " , dbPath , " | " , " WRITE | " , dbW_access]) mylog('none', ['------------------------------------------------']) - #return dbR_access and dbW_access and confR_access and confW_access - return (confR_access, dbR_access) + #return dbR_access and dbW_access and confR_access and confW_access + return (confR_access, dbR_access) #------------------------------------------------------------------------------- def fixPermissions(): # Try fixing access rights if needed chmodCommands = [] - - chmodCommands.append(['sudo', 'chmod', 'a+rw', '-R', fullDbPath]) + + chmodCommands.append(['sudo', 'chmod', 'a+rw', '-R', fullDbPath]) chmodCommands.append(['sudo', 'chmod', 'a+rw', '-R', fullConfPath]) for com in chmodCommands: @@ -90,7 +90,7 @@ def fixPermissions(): result = subprocess.check_output (com, universal_newlines=True) except subprocess.CalledProcessError as e: # An error occured, handle it - mylog('none', ["[Setup] Fix Failed. Execute this command manually inside of the container: ", ' '.join(com)]) + mylog('none', ["[Setup] Fix Failed. Execute this command manually inside of the container: ", ' '.join(com)]) mylog('none', [e.output]) @@ -111,7 +111,7 @@ def initialiseFile(pathToCheck, defaultFile): # write stdout and stderr into .log files for debugging if needed logResult (stdout, stderr) # TO-DO should be changed to mylog - + except subprocess.CalledProcessError as e: # An error occured, handle it mylog('none', ["[Setup] Error copying ("+defaultFile+"). Make sure the app has Read & Write access to " + pathToCheck]) @@ -130,7 +130,7 @@ def filePermissions(): initialiseFile(fullDbPath, "/home/pi/pialert/back/pialert.db_bak") # last attempt - fixPermissions() + fixPermissions() #------------------------------------------------------------------------------- @@ -139,7 +139,7 @@ def bytes_to_string(value): # if value is of type bytes, convert to string if isinstance(value, bytes): value = value.decode('utf-8') - return value + return value #------------------------------------------------------------------------------- @@ -152,21 +152,15 @@ def if_byte_then_to_str(input): #------------------------------------------------------------------------------- def collect_lang_strings(db, json, pref): - for prop in json["localized"]: + for prop in json["localized"]: for language_string in json[prop]: - import_language_string(db, language_string["language_code"], pref + "_" + prop, language_string["string"]) + import_language_string(db, language_string["language_code"], pref + "_" + prop, language_string["string"]) - - - - - - #------------------------------------------------------------------------------- # Creates a JSON object from a DB row -def row_to_json(names, row): - +def row_to_json(names, row): + rowEntry = {} index = 0 @@ -179,7 +173,7 @@ def row_to_json(names, row): #------------------------------------------------------------------------------- def import_language_string(db, code, key, value, extra = ""): - db.sql.execute ("""INSERT INTO Plugins_Language_Strings ("Language_Code", "String_Key", "String_Value", "Extra") VALUES (?, ?, ?, ?)""", (str(code), str(key), str(value), str(extra))) + db.sql.execute ("""INSERT INTO Plugins_Language_Strings ("Language_Code", "String_Key", "String_Value", "Extra") VALUES (?, ?, ?, ?)""", (str(code), str(key), str(value), str(extra))) db.commitDB() @@ -198,13 +192,13 @@ def checkIPV4(ip): #------------------------------------------------------------------------------- -def isNewVersion(newVersion: bool): +def isNewVersion(newVersion: bool): - if newVersion == False: + if newVersion == False: - f = open(pialertPath + '/front/buildtimestamp.txt', 'r') + f = open(pialertPath + '/front/buildtimestamp.txt', 'r') buildTimestamp = int(f.read().strip()) - f.close() + f.close() data = "" @@ -213,19 +207,19 @@ def isNewVersion(newVersion: bool): text = url.text data = json.loads(text) except requests.exceptions.ConnectionError as e: - mylog('info', [" Couldn't check for new release."]) + mylog('info', [" Couldn't check for new release."]) data = "" - + # make sure we received a valid response and not an API rate limit exceeded message - if data != "" and len(data) > 0 and isinstance(data, list) and "published_at" in data[0]: + if data != "" and len(data) > 0 and isinstance(data, list) and "published_at" in data[0]: - dateTimeStr = data[0]["published_at"] + dateTimeStr = data[0]["published_at"] - realeaseTimestamp = int(datetime.datetime.strptime(dateTimeStr, '%Y-%m-%dT%H:%M:%SZ').strftime('%s')) + realeaseTimestamp = int(datetime.datetime.strptime(dateTimeStr, '%Y-%m-%dT%H:%M:%SZ').strftime('%s')) - if realeaseTimestamp > buildTimestamp + 600: + if realeaseTimestamp > buildTimestamp + 600: mylog('none', [" New version of the container available!"]) - newVersion = True + newVersion = True # updateState(db, 'Back_New_Version_Available', str(newVersionAvailable)) ## TO DO add this back in but avoid circular ref with database return newVersion @@ -237,7 +231,7 @@ def hide_email(email): if len(m) == 2: return f'{m[0][0]}{"*"*(len(m[0])-2)}{m[0][-1] if len(m[0]) > 1 else ""}@{m[1]}' - return email + return email #------------------------------------------------------------------------------- def removeDuplicateNewLines(text): @@ -250,14 +244,14 @@ def removeDuplicateNewLines(text): def add_json_list (row, list): new_row = [] - for column in row : + for column in row : column = bytes_to_string(column) new_row.append(column) - list.append(new_row) + list.append(new_row) - return list + return list #------------------------------------------------------------------------------- @@ -275,7 +269,7 @@ def generate_mac_links (html, deviceUrl): MACs = re.findall(p, html) - for mac in MACs: + for mac in MACs: html = html.replace('' + mac + '','' + mac + '') return html @@ -283,40 +277,47 @@ def generate_mac_links (html, deviceUrl): #------------------------------------------------------------------------------- -def initOrSetParam(db, parID, parValue): +def initOrSetParam(db, parID, parValue): sql = db.sql - sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'") + sql.execute ("INSERT INTO Parameters(par_ID, par_Value) VALUES('"+str(parID)+"', '"+str(parValue)+"') ON CONFLICT(par_ID) DO UPDATE SET par_Value='"+str(parValue)+"' where par_ID='"+str(parID)+"'") - db.commitDB() + db.commitDB() #------------------------------------------------------------------------------- class json_struc: - def __init__(self, jsn, columnNames): + def __init__(self, jsn, columnNames): self.json = jsn - self.columnNames = columnNames + self.columnNames = columnNames #------------------------------------------------------------------------------- def get_file_content(path): - f = open(path, 'r') - content = f.read() - f.close() + f = open(path, 'r') + content = f.read() + f.close() - return content + return content #------------------------------------------------------------------------------- def write_file (pPath, pText): # Write the text depending using the correct python version if sys.version_info < (3, 0): file = io.open (pPath , mode='w', encoding='utf-8') - file.write ( pText.decode('unicode_escape') ) - file.close() + file.write ( pText.decode('unicode_escape') ) + file.close() else: - file = open (pPath, 'w', encoding='utf-8') + file = open (pPath, 'w', encoding='utf-8') if pText is None: pText = "" - file.write (pText) - file.close() \ No newline at end of file + file.write (pText) + file.close() + +#------------------------------------------------------------------------------- +class noti_struc: + def __init__(self, json, text, html): + self.json = json + self.text = text + self.html = html \ No newline at end of file diff --git a/pialert/mqtt.py b/pialert/mqtt.py deleted file mode 100644 index 8843b1d5..00000000 --- a/pialert/mqtt.py +++ /dev/null @@ -1,244 +0,0 @@ - -import time -import re -from paho.mqtt import client as mqtt_client - -import conf -from logger import mylog -from database import get_all_devices, get_device_stats -from helper import bytes_to_string, sanitize_string - - - -#------------------------------------------------------------------------------- -# MQTT -#------------------------------------------------------------------------------- - -mqtt_connected_to_broker = False -mqtt_sensors = [] - -#------------------------------------------------------------------------------- -class sensor_config: - def __init__(self, deviceId, deviceName, sensorType, sensorName, icon): - self.deviceId = deviceId - self.deviceName = deviceName - self.sensorType = sensorType - self.sensorName = sensorName - self.icon = icon - self.hash = str(hash(str(deviceId) + str(deviceName)+ str(sensorType)+ str(sensorName)+ str(icon))) - -#------------------------------------------------------------------------------- - -def publish_mqtt(client, topic, message): - status = 1 - while status != 0: - result = client.publish( - topic=topic, - payload=message, - qos=conf.MQTT_QOS, - retain=True, - ) - - status = result[0] - - if status != 0: - mylog('info', ["Waiting to reconnect to MQTT broker"]) - time.sleep(0.1) - return True - -#------------------------------------------------------------------------------- -def create_generic_device(client): - - deviceName = 'PiAlert' - deviceId = 'pialert' - - create_sensor(client, deviceId, deviceName, 'sensor', 'online', 'wifi-check') - create_sensor(client, deviceId, deviceName, 'sensor', 'down', 'wifi-cancel') - create_sensor(client, deviceId, deviceName, 'sensor', 'all', 'wifi') - create_sensor(client, deviceId, deviceName, 'sensor', 'archived', 'wifi-lock') - create_sensor(client, deviceId, deviceName, 'sensor', 'new', 'wifi-plus') - create_sensor(client, deviceId, deviceName, 'sensor', 'unknown', 'wifi-alert') - - -#------------------------------------------------------------------------------- -def create_sensor(client, deviceId, deviceName, sensorType, sensorName, icon): - - new_sensor_config = sensor_config(deviceId, deviceName, sensorType, sensorName, icon) - - # check if config already in list and if not, add it, otherwise skip - global mqtt_sensors, uniqueSensorCount - - is_unique = True - - for sensor in mqtt_sensors: - if sensor.hash == new_sensor_config.hash: - is_unique = False - break - - # save if unique - if is_unique: - publish_sensor(client, new_sensor_config) - - - - -#------------------------------------------------------------------------------- -def publish_sensor(client, sensorConf): - - global mqtt_sensors - - message = '{ \ - "name":"'+ sensorConf.deviceName +' '+sensorConf.sensorName+'", \ - "state_topic":"system-sensors/'+sensorConf.sensorType+'/'+sensorConf.deviceId+'/state", \ - "value_template":"{{value_json.'+sensorConf.sensorName+'}}", \ - "unique_id":"'+sensorConf.deviceId+'_sensor_'+sensorConf.sensorName+'", \ - "device": \ - { \ - "identifiers": ["'+sensorConf.deviceId+'_sensor"], \ - "manufacturer": "PiAlert", \ - "name":"'+sensorConf.deviceName+'" \ - }, \ - "icon":"mdi:'+sensorConf.icon+'" \ - }' - - topic='homeassistant/'+sensorConf.sensorType+'/'+sensorConf.deviceId+'/'+sensorConf.sensorName+'/config' - - # add the sensor to the global list to keep track of succesfully added sensors - if publish_mqtt(client, topic, message): - # hack - delay adding to the queue in case the process is - time.sleep(conf.MQTT_DELAY_SEC) # restarted and previous publish processes aborted - # (it takes ~2s to update a sensor config on the broker) - mqtt_sensors.append(sensorConf) - -#------------------------------------------------------------------------------- -def mqtt_create_client(): - def on_disconnect(client, userdata, rc): - global mqtt_connected_to_broker - mqtt_connected_to_broker = False - - # not sure is below line is correct / necessary - # client = mqtt_create_client() - - def on_connect(client, userdata, flags, rc): - global mqtt_connected_to_broker - - if rc == 0: - mylog('verbose', [" Connected to broker"]) - mqtt_connected_to_broker = True # Signal connection - else: - mylog('none', [" Connection failed"]) - mqtt_connected_to_broker = False - - - client = mqtt_client.Client('PiAlert') # Set Connecting Client ID - client.username_pw_set(conf.MQTT_USER, conf.MQTT_PASSWORD) - client.on_connect = on_connect - client.on_disconnect = on_disconnect - client.connect(conf.MQTT_BROKER, conf.MQTT_PORT) - client.loop_start() - - return client - -#------------------------------------------------------------------------------- -def mqtt_start(): - - global client, mqtt_connected_to_broker - - if mqtt_connected_to_broker == False: - mqtt_connected_to_broker = True - client = mqtt_create_client() - - # General stats - - # Create a generic device for overal stats - create_generic_device(client) - - # Get the data - row = get_device_stats() - - columns = ["online","down","all","archived","new","unknown"] - - payload = "" - - # Update the values - for column in columns: - payload += '"'+column+'": ' + str(row[column]) +',' - - # Publish (warap into {} and remove last ',' from above) - publish_mqtt(client, "system-sensors/sensor/pialert/state", - '{ \ - '+ payload[:-1] +'\ - }' - ) - - - # Specific devices - - # Get all devices - devices = get_all_devices() - - sec_delay = len(devices) * int(conf.MQTT_DELAY_SEC)*5 - - mylog('info', [" Estimated delay: ", (sec_delay), 's ', '(', round(sec_delay/60,1) , 'min)' ]) - - for device in devices: - - # Create devices in Home Assistant - send config messages - deviceId = 'mac_' + device["dev_MAC"].replace(" ", "").replace(":", "_").lower() - deviceNameDisplay = re.sub('[^a-zA-Z0-9-_\s]', '', device["dev_Name"]) - - create_sensor(client, deviceId, deviceNameDisplay, 'sensor', 'last_ip', 'ip-network') - create_sensor(client, deviceId, deviceNameDisplay, 'binary_sensor', 'is_present', 'wifi') - create_sensor(client, deviceId, deviceNameDisplay, 'sensor', 'mac_address', 'folder-key-network') - create_sensor(client, deviceId, deviceNameDisplay, 'sensor', 'is_new', 'bell-alert-outline') - create_sensor(client, deviceId, deviceNameDisplay, 'sensor', 'vendor', 'cog') - - # update device sensors in home assistant - - publish_mqtt(client, 'system-sensors/sensor/'+deviceId+'/state', - '{ \ - "last_ip": "' + device["dev_LastIP"] +'", \ - "is_new": "' + str(device["dev_NewDevice"]) +'", \ - "vendor": "' + sanitize_string(device["dev_Vendor"]) +'", \ - "mac_address": "' + str(device["dev_MAC"]) +'" \ - }' - ) - - publish_mqtt(client, 'system-sensors/binary_sensor/'+deviceId+'/state', - '{ \ - "is_present": "' + to_binary_sensor(str(device["dev_PresentLastScan"])) +'"\ - }' - ) - - # delete device / topic - # homeassistant/sensor/mac_44_ef_bf_c4_b1_af/is_present/config - # client.publish( - # topic="homeassistant/sensor/"+deviceId+"/is_present/config", - # payload="", - # qos=1, - # retain=True, - # ) - # time.sleep(10) - - -#=============================================================================== -# Home Assistant UTILs -#=============================================================================== -def to_binary_sensor(input): - # In HA a binary sensor returns ON or OFF - result = "OFF" - - # bytestring - if isinstance(input, str): - if input == "1": - result = "ON" - elif isinstance(input, int): - if input == 1: - result = "ON" - elif isinstance(input, bool): - if input == True: - result = "ON" - elif isinstance(input, bytes): - if bytes_to_string(input) == "1": - result = "ON" - return result \ No newline at end of file diff --git a/pialert/publishers/__init__.py b/pialert/publishers/__init__.py new file mode 100644 index 00000000..52c36748 --- /dev/null +++ b/pialert/publishers/__init__.py @@ -0,0 +1,8 @@ +""" Publishers for Pi.Alert """ + +""" +each publisher exposes: + +- check_config () returning True / False +- send (message) returning True / Fasle +""" \ No newline at end of file diff --git a/pialert/publishers/apprise.py b/pialert/publishers/apprise.py new file mode 100644 index 00000000..9d065a3f --- /dev/null +++ b/pialert/publishers/apprise.py @@ -0,0 +1,42 @@ + +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 + + #Define Apprise compatible payload (https://github.com/caronc/apprise-api#stateless-solution) + payload = html + + if conf.APPRISE_PAYLOAD == 'text': + payload = text + + _json_payload={ + "urls": conf.APPRISE_URL, + "title": "Pi.Alert Notifications", + "format": conf.APPRISE_PAYLOAD, + "body": payload + } + + 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 + except subprocess.CalledProcessError as e: + # An error occured, handle it + mylog('none', [e.output]) \ No newline at end of file diff --git a/pialert/publishers/email.py b/pialert/publishers/email.py new file mode 100644 index 00000000..bae0ca1b --- /dev/null +++ b/pialert/publishers/email.py @@ -0,0 +1,88 @@ +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import smtplib + + +import conf +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)) + + 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) + + 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)']) + 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('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 new file mode 100644 index 00000000..2dc5318f --- /dev/null +++ b/pialert/publishers/ntfy.py @@ -0,0 +1,36 @@ + +import conf +import requests +from base64 import b64encode + +from logger import mylog, noti_struc + +#------------------------------------------------------------------------------- +def check_config(): + if conf.NTFY_HOST == '' or conf.NTFY_TOPIC == '': + mylog('none', ['[Check Config] Error: NTFY service not set up correctly. Check your pialert.conf NTFY_* variables.']) + return False + else: + return True + +#------------------------------------------------------------------------------- +def send (msg: noti_struc): + _Text = msg.html + headers = { + "Title": "Pi.Alert Notification", + "Actions": "view, Open Dashboard, "+ conf.REPORT_DASHBOARD_URL, + "Priority": "urgent", + "Tags": "warning" + } + # if username and password are set generate hash and update header + if conf.NTFY_USER != "" and conf.NTFY_PASSWORD != "": + # Generate hash for basic auth + # usernamepassword = "{}:{}".format(conf.NTFY_USER,conf.NTFY_PASSWORD) + basichash = b64encode(bytes(conf.NTFY_USER + ':' + conf.NTFY_PASSWORD, "utf-8")).decode("ascii") + + # add authorization header with hash + headers["Authorization"] = "Basic {}".format(basichash) + + requests.post("{}/{}".format( conf.NTFY_HOST, conf.NTFY_TOPIC), + data=_Text, + headers=headers) diff --git a/pialert/publishers/pushsafer.py b/pialert/publishers/pushsafer.py new file mode 100644 index 00000000..b8252209 --- /dev/null +++ b/pialert/publishers/pushsafer.py @@ -0,0 +1,33 @@ + +import requests + + +import conf +from helper import noti_struc +from logger import mylog + +#------------------------------------------------------------------------------- +def check_config(): + if conf.PUSHSAFER_TOKEN == 'ApiKey': + mylog('none', ['[Check Config] Error: Pushsafer service not set up correctly. Check your pialert.conf PUSHSAFER_TOKEN variable.']) + return False + else: + return True + +#------------------------------------------------------------------------------- +def send ( msg:noti_struc ): + _Text = msg.text + url = 'https://www.pushsafer.com/api' + post_fields = { + "t" : 'Pi.Alert Message', + "m" : _Text, + "s" : 11, + "v" : 3, + "i" : 148, + "c" : '#ef7f7f', + "d" : 'a', + "u" : conf.REPORT_DASHBOARD_URL, + "ut" : 'Open Pi.Alert', + "k" : conf.PUSHSAFER_TOKEN, + } + requests.post(url, data=post_fields) \ No newline at end of file diff --git a/pialert/publishers/webhook.py b/pialert/publishers/webhook.py new file mode 100644 index 00000000..850f5cc0 --- /dev/null +++ b/pialert/publishers/webhook.py @@ -0,0 +1,98 @@ +import json +import subprocess + +import conf +from const import logPath +from helper import noti_struc, write_file +from logger import logResult, mylog + +#------------------------------------------------------------------------------- +def check_config(): + if conf.WEBHOOK_URL == '': + mylog('none', ['[Check Config] Error: Webhook service not set up correctly. Check your pialert.conf WEBHOOK_* variables.']) + return False + else: + return True + +#------------------------------------------------------------------------------- + +def send_webhook (msg: noti_struc): + + # use data type based on specified payload type + if conf.WEBHOOK_PAYLOAD == 'json': + payloadData = msg.json + if conf.WEBHOOK_PAYLOAD == 'html': + payloadData = msg.html + if conf.WEBHOOK_PAYLOAD == 'text': + payloadData = to_text(msg.json) # TO DO can we just send msg.text? + + # Define slack-compatible payload + _json_payload = { "text": payloadData } if conf.WEBHOOK_PAYLOAD == 'text' else { + "username": "Pi.Alert", + "text": "There are new notifications", + "attachments": [{ + "title": "Pi.Alert Notifications", + "title_link": conf.REPORT_DASHBOARD_URL, + "text": payloadData + }] + } + + # DEBUG - Write the json payload into a log file for debugging + write_file (logPath + '/webhook_payload.json', json.dumps(_json_payload)) + + # Using the Slack-Compatible Webhook endpoint for Discord so that the same payload can be used for both + if(conf.WEBHOOK_URL.startswith('https://discord.com/api/webhooks/') and not conf.WEBHOOK_URL.endswith("/slack")): + _WEBHOOK_URL = f"{conf.WEBHOOK_URL}/slack" + curlParams = ["curl","-i","-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] + else: + _WEBHOOK_URL = conf.WEBHOOK_URL + curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] + + # execute CURL call + try: + # try runnning a subprocess + mylog('debug', '[send_webhook] curlParams: '+ curlParams) + p = subprocess.Popen(curlParams, 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 + except subprocess.CalledProcessError as e: + # An error occured, handle it + mylog('none', ['[send_webhook]', e.output]) + + + + + +#------------------------------------------------------------------------------- +def to_text(_json): + payloadData = "" + if len(_json['internet']) > 0 and 'internet' in conf.INCLUDED_SECTIONS: + payloadData += "INTERNET\n" + for event in _json['internet']: + payloadData += event[3] + ' on ' + event[2] + '. ' + event[4] + '. New address:' + event[1] + '\n' + + if len(_json['new_devices']) > 0 and 'new_devices' in conf.INCLUDED_SECTIONS: + payloadData += "NEW DEVICES:\n" + for event in _json['new_devices']: + if event[4] is None: + event[4] = event[11] + payloadData += event[1] + ' - ' + event[4] + '\n' + + if len(_json['down_devices']) > 0 and 'down_devices' in conf.INCLUDED_SECTIONS: + write_file (logPath + '/down_devices_example.log', _json['down_devices']) + payloadData += 'DOWN DEVICES:\n' + for event in _json['down_devices']: + if event[4] is None: + event[4] = event[11] + payloadData += event[1] + ' - ' + event[4] + '\n' + + if len(_json['events']) > 0 and 'events' in conf.INCLUDED_SECTIONS: + payloadData += "EVENTS:\n" + for event in _json['events']: + if event[8] != "Internet": + payloadData += event[8] + " on " + event[1] + " " + event[3] + " at " + event[2] + "\n" + + return payloadData \ No newline at end of file diff --git a/pialert/reporting.py b/pialert/reporting.py index 6a8186ea..0bb5e57a 100644 --- a/pialert/reporting.py +++ b/pialert/reporting.py @@ -1,40 +1,41 @@ -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText + import datetime import json -import smtplib + import socket -from base64 import b64encode + import subprocess import requests from json2table import convert # pialert modules -import conf +import conf from const import pialertPath, logPath, apiPath -from helper import generate_mac_links, removeDuplicateNewLines, timeNow, hide_email, json_struc, updateState, get_file_content, write_file +from helper import noti_struc, generate_mac_links, removeDuplicateNewLines, timeNow, hide_email, updateState, get_file_content, write_file from logger import logResult, mylog, print_log -from mqtt import mqtt_start - - +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, + send as send_pushsafer) +from publishers.mqtt import (check_config as mqtt_check_config, + mqtt_start ) #=============================================================================== # REPORTING #=============================================================================== -# create a json for webhook and mqtt notifications to provide further integration options +# create a json for webhook and mqtt notifications to provide further integration options json_final = [] -#------------------------------------------------------------------------------- -class noti_struc: - def __init__(self, json, text, html): - self.json = json - self.text = text - self.html = html - #------------------------------------------------------------------------------- def construct_notifications(db, sqlQuery, tableTitle, skipText = False, suppliedJsonStruct = None): @@ -55,7 +56,7 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied json_struc = suppliedJsonStruct jsn = json_struc.json - html = "" + html = "" text = "" if len(jsn["data"]) > 0: @@ -68,13 +69,13 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied # prepare text-only message if skipText == False: - + for device in jsn["data"]: for header in headers: padding = "" if len(header) < 4: padding = "\t" - text += text_line.format ( header + ': ' + padding, device[header]) + text += text_line.format ( header + ': ' + padding, device[header]) text += '\n' # Format HTML table headers @@ -86,7 +87,8 @@ def construct_notifications(db, sqlQuery, tableTitle, skipText = False, supplied -def send_notifications (db): +def send_notifications (db, INCLUDED_SECTIONS = conf.INCLUDED_SECTIONS): + sql = db.sql #TO-DO global mail_text, mail_html, json_final, changedPorts_json_struc, partial_html, partial_txt, partial_json @@ -94,7 +96,7 @@ def send_notifications (db): plugins_report = False # Reporting section - mylog('verbose', ['[Notification] Check if something to report']) + mylog('verbose', ['[Notification] Check if something to report']) # prepare variables for JSON construction json_internet = [] @@ -108,26 +110,26 @@ def send_notifications (db): sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 WHERE eve_PendingAlertEmail = 1 AND eve_EventType != 'Device Down' AND eve_MAC IN ( - SELECT dev_MAC FROM Devices WHERE dev_AlertEvents = 0 + SELECT dev_MAC FROM Devices WHERE dev_AlertEvents = 0 )""") sql.execute ("""UPDATE Events SET eve_PendingAlertEmail = 0 WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'Device Down' AND eve_MAC IN ( - SELECT dev_MAC FROM Devices WHERE dev_AlertDeviceDown = 0 + SELECT dev_MAC FROM Devices WHERE dev_AlertDeviceDown = 0 )""") # Open text Template - template_file = open(pialertPath + '/back/report_template.txt', 'r') - mail_text = template_file.read() - template_file.close() + template_file = open(pialertPath + '/back/report_template.txt', 'r') + mail_text = template_file.read() + template_file.close() # Open html Template - template_file = open(pialertPath + '/back/report_template.html', 'r') + template_file = open(pialertPath + '/back/report_template.html', 'r') if conf.newVersionAvailable : - template_file = open(pialertPath + '/back/report_template_new_version.html', 'r') + template_file = open(pialertPath + '/back/report_template_new_version.html', 'r') - mail_html = template_file.read() - template_file.close() + mail_html = template_file.read() + template_file.close() # Report Header & footer timeFormated = timeNow().strftime ('%Y-%m-%d %H:%M') @@ -137,7 +139,7 @@ def send_notifications (db): mail_text = mail_text.replace ('', socket.gethostname() ) mail_html = mail_html.replace ('', socket.gethostname() ) - if 'internet' in conf.INCLUDED_SECTIONS: + if 'internet' in INCLUDED_SECTIONS: # Compose Internet Section sqlQuery = """SELECT eve_MAC as MAC, eve_IP as IP, eve_DateTime as Datetime, eve_EventType as "Event Type", eve_AdditionalInfo as "More info" FROM Events WHERE eve_PendingAlertEmail = 1 AND eve_MAC = 'Internet' @@ -145,14 +147,14 @@ def send_notifications (db): notiStruc = construct_notifications(db, sqlQuery, "Internet IP change") - # collect "internet" (IP changes) for the webhook json + # collect "internet" (IP changes) for the webhook json json_internet = notiStruc.json["data"] mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) - if 'new_devices' in conf.INCLUDED_SECTIONS: - # Compose New Devices Section + if 'new_devices' in 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 FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'New Device' @@ -160,14 +162,14 @@ def send_notifications (db): notiStruc = construct_notifications(db, sqlQuery, "New devices") - # collect "new_devices" for the webhook json + # collect "new_devices" for the webhook json json_new_devices = notiStruc.json["data"] mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) - if 'down_devices' in conf.INCLUDED_SECTIONS: - # Compose Devices Down Section + if 'down_devices' in 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 FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType = 'Device Down' @@ -175,14 +177,14 @@ def send_notifications (db): notiStruc = construct_notifications(db, sqlQuery, "Down devices") - # collect "new_devices" for the webhook json + # collect "new_devices" for the webhook json json_down_devices = notiStruc.json["data"] mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) - if 'events' in conf.INCLUDED_SECTIONS: - # Compose Events Section + if 'events' in 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 FROM Events_Devices WHERE eve_PendingAlertEmail = 1 AND eve_EventType IN ('Connected','Disconnected', @@ -191,16 +193,16 @@ def send_notifications (db): notiStruc = construct_notifications(db, sqlQuery, "Events") - # collect "events" for the webhook json + # collect "events" for the webhook json json_events = notiStruc.json["data"] mail_text = mail_text.replace ('', notiStruc.text + '\n') mail_html = mail_html.replace ('', notiStruc.html) - - if 'ports' in conf.INCLUDED_SECTIONS: - # collect "ports" for the webhook json - if changedPorts_json_struc is not None: - json_ports = changedPorts_json_struc.json["data"] + + if 'ports' in INCLUDED_SECTIONS: + # collect "ports" for the webhook json + if changedPorts_json_struc is not None: + json_ports = changedPorts_json_struc.json["data"] notiStruc = construct_notifications(db, "", "Ports", True, changedPorts_json_struc) @@ -208,17 +210,17 @@ def send_notifications (db): portsTxt = "" if changedPorts_json_struc is not None: - portsTxt = "Ports \n---------\n Ports changed! Check PiAlert for details!\n" + portsTxt = "Ports \n---------\n Ports changed! Check PiAlert for details!\n" mail_text = mail_text.replace ('', portsTxt ) - if 'plugins' in conf.INCLUDED_SECTIONS and conf.ENABLE_PLUGINS: - # Compose Plugins Section + if 'plugins' in INCLUDED_SECTIONS and conf.ENABLE_PLUGINS: + # Compose Plugins Section sqlQuery = """SELECT Plugin, Object_PrimaryId, Object_SecondaryId, DateTimeChanged, Watched_Value1, Watched_Value2, Watched_Value3, Watched_Value4, Status from Plugins_Events""" notiStruc = construct_notifications(db, sqlQuery, "Plugins") - # collect "plugins" for the webhook json + # collect "plugins" for the webhook json json_plugins = notiStruc.json["data"] mail_text = mail_text.replace ('', notiStruc.text + '\n') @@ -229,42 +231,44 @@ def send_notifications (db): json_final = { - "internet": json_internet, + "internet": json_internet, "new_devices": json_new_devices, - "down_devices": json_down_devices, + "down_devices": json_down_devices, "events": json_events, "ports": json_ports, "plugins": json_plugins, - } + } mail_text = removeDuplicateNewLines(mail_text) - - # Create clickable MAC links + + # Create clickable MAC links mail_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 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) # Send Mail - if json_internet != [] or json_new_devices != [] or json_down_devices != [] or json_events != [] or json_ports != [] or conf.debug_force_notification or plugins_report: + if json_internet != [] or json_new_devices != [] or json_down_devices != [] or json_events != [] or json_ports != [] or conf.debug_force_notification or plugins_report: mylog('none', ['[Notification] Changes detected, sending reports']) + msg = noti_struc(json_final, mail_text, mail_html) + mylog('info', ['[Notification] Udateing API files']) send_api() - if conf.REPORT_MAIL and check_config('email'): + if conf.REPORT_MAIL and check_config('email'): updateState(db,"Send: Email") mylog('info', ['[Notification] Sending report by Email']) - send_email (mail_text, mail_html) + send_email (msg ) else : mylog('verbose', ['[Notification] Skip email']) if conf.REPORT_APPRISE and check_config('apprise'): updateState(db,"Send: Apprise") mylog('info', ['[Notification] Sending report by Apprise']) - send_apprise (mail_html, mail_text) + send_apprise (msg) else : mylog('verbose', ['[Notification] Skip Apprise']) if conf.REPORT_WEBHOOK and check_config('webhook'): @@ -276,20 +280,20 @@ def send_notifications (db): if conf.REPORT_NTFY and check_config('ntfy'): updateState(db,"Send: NTFY") mylog('info', ['[Notification] Sending report by NTFY']) - send_ntfy (mail_text) + send_ntfy (msg) else : mylog('verbose', ['[Notification] Skip NTFY']) if conf.REPORT_PUSHSAFER and check_config('pushsafer'): updateState(db,"Send: PUSHSAFER") mylog('info', ['[Notification] Sending report by PUSHSAFER']) - send_pushsafer (mail_text) + send_pushsafer (msg) else : mylog('verbose', ['[Notification] Skip PUSHSAFER']) # Update MQTT entities if conf.REPORT_MQTT and check_config('mqtt'): updateState(db,"Send: MQTT") - mylog('info', ['[Notification] Establishing MQTT thread']) - mqtt_start() + mylog('info', ['[Notification] Establishing MQTT thread']) + mqtt_start() else : mylog('verbose', ['[Notification] Skip MQTT']) else : @@ -305,13 +309,13 @@ def send_notifications (db): # clear plugin events sql.execute ("DELETE FROM Plugins_Events") - + changedPorts_json_struc = None - # DEBUG - print number of rows updated + # DEBUG - print number of rows updated mylog('info', ['[Notification] Notifications changes: ', sql.rowcount]) - # Commit changes + # Commit changes db.commitDB() @@ -319,53 +323,53 @@ def send_notifications (db): def check_config(service): if service == 'email': - if conf.SMTP_SERVER == '' or conf.REPORT_FROM == '' or conf.REPORT_TO == '': - mylog('none', ['[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 + return email_check_config() + + # if conf.SMTP_SERVER == '' or conf.REPORT_FROM == '' or conf.REPORT_TO == '': + # mylog('none', ['[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 if service == 'apprise': - 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 + return apprise_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 if service == 'webhook': - if conf.WEBHOOK_URL == '': - mylog('none', ['[Check Config] Error: Webhook service not set up correctly. Check your pialert.conf WEBHOOK_* variables.']) - return False - else: - return True + return webhook_check_config() + + # if conf.WEBHOOK_URL == '': + # mylog('none', ['[Check Config] Error: Webhook service not set up correctly. Check your pialert.conf WEBHOOK_* variables.']) + # return False + # else: + # return True if service == 'ntfy': - if conf.NTFY_HOST == '' or conf.NTFY_TOPIC == '': - mylog('none', ['[Check Config] Error: NTFY service not set up correctly. Check your pialert.conf NTFY_* variables.']) - return False - else: - return True + return ntfy_check_config () + # + # if conf.NTFY_HOST == '' or conf.NTFY_TOPIC == '': + # mylog('none', ['[Check Config] Error: NTFY service not set up correctly. Check your pialert.conf NTFY_* variables.']) + # return False + # else: + # return True if service == 'pushsafer': - if conf.PUSHSAFER_TOKEN == 'ApiKey': - mylog('none', ['[Check Config] Error: Pushsafer service not set up correctly. Check your pialert.conf PUSHSAFER_TOKEN variable.']) - return False - else: - return True + return pushsafer_check_config() if service == 'mqtt': - if conf.MQTT_BROKER == '' or conf.MQTT_PORT == '' or conf.MQTT_USER == '' or conf.MQTT_PASSWORD == '': - mylog('none', ['[Check Config] Error: MQTT service not set up correctly. Check your pialert.conf MQTT_* variables.']) - return False - else: - return True + return mqtt_check_config() #------------------------------------------------------------------------------- def format_table (html, thValue, props, newThValue = ''): if newThValue == '': newThValue = thValue - + return html.replace(""+thValue+"", ""+newThValue+"" ) #------------------------------------------------------------------------------- @@ -375,9 +379,9 @@ def format_report_section (pActive, pSection, pTable, pText, pHTML): # Replace section text if pActive : conf.mail_text = conf.mail_text.replace ('<'+ pTable +'>', pText) - conf.mail_html = conf.mail_html.replace ('<'+ pTable +'>', pHTML) + conf.mail_html = conf.mail_html.replace ('<'+ pTable +'>', pHTML) - conf.mail_text = remove_tag (conf.mail_text, pSection) + conf.mail_text = remove_tag (conf.mail_text, pSection) conf.mail_html = remove_tag (conf.mail_html, pSection) else: conf.mail_text = remove_section (conf.mail_text, pSection) @@ -387,7 +391,7 @@ def format_report_section (pActive, pSection, pTable, pText, pHTML): def remove_section (pText, pSection): # Search section into the text if pText.find ('<'+ pSection +'>') >=0 \ - and pText.find ('') >=0 : + and pText.find ('') >=0 : # return text without the section return pText[:pText.find ('<'+ pSection+'>')] + \ pText[pText.find ('') + len (pSection) +3:] @@ -402,215 +406,8 @@ def remove_tag (pText, pTag): #------------------------------------------------------------------------------- -# Reporting +# Reporting #------------------------------------------------------------------------------- -def send_email (pText, pHTML): - - 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)) - - 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) - - 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)']) - 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('debug', '[Send Email] Last executed - ' + str(failedAt)) - -#------------------------------------------------------------------------------- -def send_ntfy (_Text): - headers = { - "Title": "Pi.Alert Notification", - "Actions": "view, Open Dashboard, "+ conf.REPORT_DASHBOARD_URL, - "Priority": "urgent", - "Tags": "warning" - } - # if username and password are set generate hash and update header - if conf.NTFY_USER != "" and conf.NTFY_PASSWORD != "": - # Generate hash for basic auth - usernamepassword = "{}:{}".format(conf.NTFY_USER,conf.NTFY_PASSWORD) - basichash = b64encode(bytes(conf.NTFY_USER + ':' + conf.NTFY_PASSWORD, "utf-8")).decode("ascii") - - # add authorization header with hash - headers["Authorization"] = "Basic {}".format(basichash) - - requests.post("{}/{}".format( conf.NTFY_HOST, conf.NTFY_TOPIC), - data=_Text, - headers=headers) - -def send_pushsafer (_Text): - url = 'https://www.pushsafer.com/api' - post_fields = { - "t" : 'Pi.Alert Message', - "m" : _Text, - "s" : 11, - "v" : 3, - "i" : 148, - "c" : '#ef7f7f', - "d" : 'a', - "u" : conf.REPORT_DASHBOARD_URL, - "ut" : 'Open Pi.Alert', - "k" : conf.PUSHSAFER_TOKEN, - } - requests.post(url, data=post_fields) - -#------------------------------------------------------------------------------- -def send_webhook (_json, _html): - - # use data type based on specified payload type - if conf.WEBHOOK_PAYLOAD == 'json': - payloadData = _json - if conf.WEBHOOK_PAYLOAD == 'html': - payloadData = _html - if conf.WEBHOOK_PAYLOAD == 'text': - payloadData = to_text(_json) - - # Define slack-compatible payload - _json_payload = { "text": payloadData } if conf.WEBHOOK_PAYLOAD == 'text' else { - "username": "Pi.Alert", - "text": "There are new notifications", - "attachments": [{ - "title": "Pi.Alert Notifications", - "title_link": conf.REPORT_DASHBOARD_URL, - "text": payloadData - }] - } - - # DEBUG - Write the json payload into a log file for debugging - write_file (logPath + '/webhook_payload.json', json.dumps(_json_payload)) - - # Using the Slack-Compatible Webhook endpoint for Discord so that the same payload can be used for both - if(conf.WEBHOOK_URL.startswith('https://discord.com/api/webhooks/') and not conf.WEBHOOK_URL.endswith("/slack")): - _WEBHOOK_URL = f"{conf.WEBHOOK_URL}/slack" - curlParams = ["curl","-i","-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] - else: - _WEBHOOK_URL = conf.WEBHOOK_URL - curlParams = ["curl","-i","-X", conf.WEBHOOK_REQUEST_METHOD ,"-H", "Content-Type:application/json" ,"-d", json.dumps(_json_payload), _WEBHOOK_URL] - - # execute CURL call - try: - # try runnning a subprocess - mylog('debug', '[send_webhook] curlParams: '+ curlParams) - p = subprocess.Popen(curlParams, 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 - except subprocess.CalledProcessError as e: - # An error occured, handle it - mylog('none', ['[send_webhook]', e.output]) - -#------------------------------------------------------------------------------- -def send_apprise (html, text): - #Define Apprise compatible payload (https://github.com/caronc/apprise-api#stateless-solution) - payload = html - - if conf.APPRISE_PAYLOAD == 'text': - payload = text - - _json_payload={ - "urls": conf.APPRISE_URL, - "title": "Pi.Alert Notifications", - "format": conf.APPRISE_PAYLOAD, - "body": payload - } - - 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 - except subprocess.CalledProcessError as e: - # An error occured, handle it - mylog('none', [e.output]) - - -def to_text(_json): - payloadData = "" - if len(_json['internet']) > 0 and 'internet' in conf.INCLUDED_SECTIONS: - payloadData += "INTERNET\n" - for event in _json['internet']: - payloadData += event[3] + ' on ' + event[2] + '. ' + event[4] + '. New address:' + event[1] + '\n' - - if len(_json['new_devices']) > 0 and 'new_devices' in conf.INCLUDED_SECTIONS: - payloadData += "NEW DEVICES:\n" - for event in _json['new_devices']: - if event[4] is None: - event[4] = event[11] - payloadData += event[1] + ' - ' + event[4] + '\n' - - if len(_json['down_devices']) > 0 and 'down_devices' in conf.INCLUDED_SECTIONS: - write_file (logPath + '/down_devices_example.log', _json['down_devices']) - payloadData += 'DOWN DEVICES:\n' - for event in _json['down_devices']: - if event[4] is None: - event[4] = event[11] - payloadData += event[1] + ' - ' + event[4] + '\n' - - if len(_json['events']) > 0 and 'events' in conf.INCLUDED_SECTIONS: - payloadData += "EVENTS:\n" - for event in _json['events']: - if event[8] != "Internet": - payloadData += event[8] + " on " + event[1] + " " + event[3] + " at " + event[2] + "\n" - - return payloadData #------------------------------------------------------------------------------- def send_api(): @@ -618,11 +415,11 @@ def send_api(): write_file(apiPath + 'notification_text.txt' , mail_text) write_file(apiPath + 'notification_text.html' , mail_html) - write_file(apiPath + 'notification_json_final.json' , json.dumps(json_final)) + write_file(apiPath + 'notification_json_final.json' , json.dumps(json_final)) #------------------------------------------------------------------------------- -def skip_repeated_notifications (db): +def skip_repeated_notifications (db): # Skip repeated notifications # due strfime : Overflow --> use "strftime / 60" @@ -640,7 +437,7 @@ def skip_repeated_notifications (db): """ ) mylog('verbose','[Skip Repeated Notifications] Skip Repeated end') - db.commitDB() + db.commitDB() #=============================================================================== @@ -651,10 +448,10 @@ def skip_repeated_notifications (db): def check_and_run_event(db): sql = db.sql # TO-DO sql.execute(""" select * from Parameters where par_ID = "Front_Event" """) - rows = sql.fetchall() + rows = sql.fetchall() event, param = ['',''] - if len(rows) > 0 and rows[0]['par_Value'] != 'finished': + if len(rows) > 0 and rows[0]['par_Value'] != 'finished': event = rows[0]['par_Value'].split('|')[0] param = rows[0]['par_Value'].split('|')[1] else: @@ -666,45 +463,47 @@ def check_and_run_event(db): handle_run(param) # clear event execution flag - sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'") + sql.execute ("UPDATE Parameters SET par_Value='finished' WHERE par_ID='Front_Event'") - # commit to DB + # commit to DB db.commitDB() #------------------------------------------------------------------------------- def handle_run(runType): global last_network_scan - mylog('info', ['[', timeNow(), '] START Run: ', runType]) + mylog('info', ['[', timeNow(), '] START Run: ', runType]) if runType == 'ENABLE_ARPSCAN': - last_network_scan = conf.time_started - datetime.timedelta(hours = 24) + last_network_scan = conf.time_started - datetime.timedelta(hours = 24) mylog('info', ['[', timeNow(), '] END Run: ', runType]) #------------------------------------------------------------------------------- def handle_test(testType): - mylog('info', ['[', timeNow(), '] START Test: ', testType]) + mylog('info', ['[', timeNow(), '] START Test: ', testType]) - # Open text sample + # Open text sample sample_txt = get_file_content(pialertPath + '/back/report_sample.txt') - # Open html sample + # Open html sample sample_html = get_file_content(pialertPath + '/back/report_sample.html') - # Open json sample and get only the payload part - sample_json_payload = json.loads(get_file_content(pialertPath + '/back/webhook_json_sample.json'))[0]["body"]["attachments"][0]["text"] - - if testType == 'REPORT_MAIL': - send_email(sample_txt, sample_html) - if testType == 'REPORT_WEBHOOK': - send_webhook (sample_json_payload, sample_txt) - if testType == 'REPORT_APPRISE': - send_apprise (sample_html, sample_txt) - if testType == 'REPORT_NTFY': - send_ntfy (sample_txt) - if testType == 'REPORT_PUSHSAFER': - send_pushsafer (sample_txt) + # 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('info', ['[', timeNow(), '] END Test: ', testType]) \ No newline at end of file + sample_msg = noti_struc(sample_json_payload, sample_txt, sample_html ) + + if testType == 'REPORT_MAIL': + send_email(sample_msg) + if testType == 'REPORT_WEBHOOK': + send_webhook (sample_msg) + if testType == 'REPORT_APPRISE': + send_apprise (sample_msg) + if testType == 'REPORT_NTFY': + send_ntfy (sample_msg) + if testType == 'REPORT_PUSHSAFER': + send_pushsafer (sample_msg) + + mylog('info', ['[Test Publishers] END Test: ', testType]) \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..89c53da0 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +""" tests for Pi.Alert """ \ No newline at end of file diff --git a/test/test_helper.py b/test/test_helper.py new file mode 100644 index 00000000..ac31ee77 --- /dev/null +++ b/test/test_helper.py @@ -0,0 +1,29 @@ +import sys +import pathlib + +sys.path.append(str(pathlib.Path(__file__).parent.parent.resolve()) + "/pialert/") + + +import datetime + +from helper import timeNow, updateSubnets + + +# ------------------------------------------------------------------------------- +def test_helper(): + assert timeNow() == datetime.datetime.now().replace(microsecond=0) + + +# ------------------------------------------------------------------------------- +def test_updateSubnets(): + # test single subnet + subnet = "192.168.1.0/24 --interface=eth0" + result = updateSubnets(subnet) + assert type(result) is list + assert len(result) == 1 + + # test multip subnets + subnet = ["192.168.1.0/24 --interface=eth0", "192.168.2.0/24 --interface=eth1"] + result = updateSubnets(subnet) + assert type(result) is list + assert len(result) == 2