Merge branch 'main' into feat/german-translation

This commit is contained in:
Markus Lorenz
2023-10-09 09:20:49 +02:00
38 changed files with 1685 additions and 793 deletions

View File

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

View File

@@ -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"
}
]
}
}
]
}
}
]

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -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
{

View File

@@ -149,7 +149,7 @@
<div class="input-group">
<input class="form-control" id="txtName" type="text" value="--">
<span class="input-group-addon"><i class="fa fa-pencil pointer" onclick="editDrp('txtName');"></i></span>
</div>
</div>
</div>
</div>
@@ -196,9 +196,9 @@
</label>
<div class="col-sm-9">
<div class="input-group">
<input class="form-control" id="txtIcon" type="text" value="--">
<span class="input-group-addon"><i class="fa" id="txtIconFA" onclick="editDrp('txtIcon');"></i></span>
<input class="form-control" id="txtIcon" type="text" value="--">
<span class="input-group-addon" title='<?= lang('DevDetail_button_OverwriteIcons_Tooltip');?>'><i class="fa fa-copy pointer" onclick="askOverwriteIconType();"></i></span>
<span class="input-group-addon"><i class="fa fa-pencil pointer" onclick="editDrp('txtIcon');"></i></span>
<div class="input-group-btn">
<button type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-caret-down"></span>
@@ -749,6 +749,11 @@ function main () {
}
});
// Show device icon as it changes
$('#txtIcon').on('change input', function() {
$('#txtIconFA').removeClass().addClass(`fa fa-${$(this).val()} pointer`)
});
});
});
@@ -1285,7 +1290,8 @@ function getDeviceData (readAllData=false) {
$('#txtOwner').val (deviceData['dev_Owner']);
$('#txtDeviceType').val (deviceData['dev_DeviceType']);
$('#txtVendor').val (deviceData['dev_Vendor']);
$('#txtIcon').val (initDefault(deviceData['dev_Icon'], 'laptop'));
$('#txtIcon').val (initDefault(deviceData['dev_Icon'], 'laptop'));
$('#txtIcon').trigger('change')
if (deviceData['dev_Favorite'] == 1) {$('#chkFavorite').iCheck('check');} else {$('#chkFavorite').iCheck('uncheck');}
$('#txtGroup').val (deviceData['dev_Group']);
@@ -1700,6 +1706,7 @@ function setTextValue (textElement, textValue) {
$('#'+textElement).attr ('data-myvalue', textValue);
$('#'+textElement).val (textValue);
}
$('#'+textElement).trigger('change')
}
// -----------------------------------------------------------------------------

View File

@@ -617,23 +617,20 @@
var sizeCoefficient = 1
function initTree(myHierarchy)
{
{
// calculate the font size of the leaf nodes to fit everything into the tree area
leafNodesCount == 0 ? 1 : leafNodesCount;
emSize = ((treeAreaHeight/(25*leafNodesCount)).toFixed(2));
emSize = emSize > 1 ? 1 : emSize;
emSize = ((treeAreaHeight/(25*leafNodesCount)).toFixed(2));
emSize = emSize > 1 ? 1 : emSize;
// nodeHeight = ((emSize*100*0.30).toFixed(0))
nodeHeight = ((emSize*100*0.30).toFixed(0))
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${$('.content-header').width()}px`)
console.log('here')
myTree = Treeviz.create({
htmlId: "networkTree",
renderNode: nodeData => {
renderNode: nodeData => {
var fontSize = "font-size:"+emSize+"em;";
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
@@ -641,52 +638,51 @@
(port == "" || port == 0 ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
// Build HTML for individual nodes in the network diagram
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ? "<div class='netIcon ' ><i class='fa fa-"+nodeData.data.icon +"'></i></div>" : "";
devicePort = `<div class='netPort ' style="width:${emSize*sizeCoefficient}em;height:${emSize*sizeCoefficient}em" >${port}</div> <div class="portBckgIcon" style="margin-left:-${emSize*sizeCoefficient}em;">${portBckgIcon}</div>`;
collapseExpandIcon = nodeData.data.hiddenChildren ? "square-plus" :"square-minus";
collapseExpandHtml = (nodeData.data.hasChildren) ? "<div class='netCollapse' style='font-size:"+emSize*sizeCoefficient+"em;' data-mytreepath='"+nodeData.data.path+"' data-mytreemac='"+nodeData.data.mac+"'><i class='fa fa-"+ collapseExpandIcon +" pointer'></i></div>" : "";
statusCss = " netStatus-" + nodeData.data.status;
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ? `<div class="netIcon"><i class="fa fa-${nodeData.data.icon}"></i></div>` : "";
devicePort = `<div class="netPort" style="width:${emSize*sizeCoefficient}em;height:${emSize*sizeCoefficient}em">${port}</div> <div class="portBckgIcon" style="margin-left:-${emSize*sizeCoefficient}em;">${portBckgIcon}</div>`;
collapseExpandIcon = nodeData.data.hiddenChildren ? "square-plus" : "square-minus";
collapseExpandHtml = nodeData.data.hasChildren ? `<div class="netCollapse" style="font-size:${emSize*sizeCoefficient}em;" data-mytreepath='${nodeData.data.path}" data-mytreemac="${nodeData.data.mac}"><i class='fa fa-${collapseExpandIcon} pointer"></i></div>` : "";
statusCss = ` netStatus-${nodeData.data.status}`;
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
highlightedCss = nodeData.data.mac == selectedNodeMac ? " highlightedNode" : "";
highlightedCss = nodeData.data.mac == selectedNodeMac ? " highlightedNode" : "";
return result = `<div class='box ${(nodeData.data.hasChildren)? "pointer":""} ${statusCss} ${highlightedCss}'
data-mytreemacmain='${nodeData.data.mac}'
style='height:${nodeData.settings.nodeHeight}px;${fontSize}
return result = `<div class="box ${nodeData.data.hasChildren ? "pointer":""} ${statusCss} ${highlightedCss}"
data-mytreemacmain="${nodeData.data.mac}"
style="height:${nodeData.settings.nodeHeight}px;${fontSize}"
>
<div class='netNodeText '>\
<strong>${devicePort} ${deviceIcon}
<span class='spanNetworkTree anonymizeDev'>${nodeData.data.name}</span>\
</strong>
${collapseExpandHtml}
</div></div>`;
},
<div class="netNodeText">
<strong>${devicePort} ${deviceIcon}
<span class="spanNetworkTree anonymizeDev">${nodeData.data.name}</span>
</strong>
${collapseExpandHtml}
</div>
</div>`;
},
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);
}
// ---------------------------------------------------------------------------

View File

@@ -504,25 +504,13 @@
"Email_icon" : "<i class=\"fa fa-at\"></i>",
"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 <code>LOG_LEVEL</code> to <code>debug</code> and check the <a href=\"/maintenance.php#tab_Logging\">error log</a>.",
"SMTP_SERVER_name" : "SMTP server URL",
"SMTP_SERVER_description" : "The SMTP server host URL. For example <code>smtp-relay.sendinblue.com</code>. To use Gmail as an SMTP server <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP.md\">follow this guide</a>",
"SMTP_PORT_name" : "SMTP server PORT",
"SMTP_PORT_description" : "Port number used for the SMTP connection. Set to <code>0</code> 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" : "<i class=\"fa fa-circle-nodes\"></i>",
"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 <code>(text was truncated)</code> 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 <code>X-Webhook-Signature</code> header to the request. You can find more informations <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_SECRET.md\">here</a>.",
"Apprise_display_name" : "Apprise",
"Apprise_icon" : "<i class=\"fa fa-bullhorn\"></i>",
"REPORT_APPRISE_name" : "Enable Apprise",
"REPORT_APPRISE_description" : "Enable sending notifications via <a target=\"_blank\" href=\"https://hub.docker.com/r/caronc/apprise\">Apprise</a>.",
"APPRISE_HOST_name" : "Apprise host URL",
"APPRISE_HOST_description" : "Apprise host URL starting with <code>http://</code> or <code>https://</code>. (do not forget to include <code>/notify</code> at the end)",
"APPRISE_URL_name" : "Apprise notification URL",
"APPRISE_URL_description" : "Apprise notification target URL. For example for Telegram it would be <code>tgram://{bot_token}/{chat_id}</code>.",
"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 <code>(text was truncated)</code> 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 <code>X-Webhook-Signature</code> header to the request. You can find more informations <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/WEBHOOK_SECRET.md\">here</a>.",
"NTFY_display_name" : "NTFY",
"NTFY_icon" : "<i class=\"fa fa-terminal\"></i>",
"REPORT_NTFY_name" : "Enable NTFY",
@@ -564,9 +542,7 @@
"REPORT_PUSHSAFER_name" : "Enable Pushsafer",
"REPORT_PUSHSAFER_description" : "Enable sending notifications via <a target=\"_blank\" href=\"https://www.pushsafer.com/\">Pushsafer</a>.",
"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 <code>html</code> works well with emails, <code>text</code> with chat apps, such as Telegram.",
"PUSHSAFER_TOKEN_description" : "Your secret Pushsafer API key (token).",
"MQTT_display_name" : "MQTT",
"MQTT_icon" : "<i class=\"fa fa-square-rss\"></i>",
"REPORT_TITLE" : "Report",

View File

@@ -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. |

View File

@@ -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.

View File

@@ -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] + " <h1> (text was truncated)</h1>"
if get_setting_value('APPRISE_PAYLOAD') == 'html':
if len(html) > limit:
payloadData = html[:limit] + "<h1>(text was truncated)</h1>"
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())

View File

@@ -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" : "<i class=\"fa-solid fa-gauge-high\"></i>"
"string" : "<i class=\"fa-solid fa-bullhorn\"></i>"
}],
"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 <code>schedule</code> the scheduling settings from below are applied. If you select <code>once</code> the scan is run only once on start of the application (container) for the time specified in <a href=\"#INTRSPD_RUN_TIMEOUT\"><code>INTRSPD_RUN_TIMEOUT</code> setting</a>."
}]
"string" : "Enable sending notifications via <a target=\"_blank\" href=\"https://hub.docker.com/r/caronc/apprise\">Apprise</a>."
},
{
"language_code": "es_es",
"string" : "Habilitar el envío de notificaciones a través de <a target=\"_blank\" href=\"https://hub.docker.com/r/caronc/apprise\">Apprise</a>."
}
]
},
{
"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 <code>schedule</code> in the <a href=\"#INTRSPD_RUN\"><code>INTRSPD_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
},
{
"language_code": "es_es",
"string": "Solo habilitado si selecciona <code>schedule</code> en la configuración <a href=\"#INTRSPD_RUN\"><code>INTRSPD_RUN</code></a>. Asegúrese de ingresar el schedule en el formato similar a cron correcto (por ejemplo, valide en <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). Por ejemplo, ingrese <code >0 4 * * *</code> ejecutará el escaneo después de las 4 am en el <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> que configuró arriba </a>. 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 <code>CTRL + Click</code> to select/deselect. <ul> <li><code>Watched_Value1</code> is Download speed (not recommended)</li><li><code>Watched_Value2</code> is Upload speed (not recommended)</li><li><code>Watched_Value3</code> unused </li><li><code>Watched_Value4</code> unused </li></ul>"
}]
"string" : "Apprise host URL starting with <code>http://</code> or <code>https://</code>. (do not forget to include <code>/notify</code> at the end)"
},
{
"language_code": "es_es",
"string" : "URL del host de Apprise que comienza con <code>http://</code> o <code>https://</code>. (no olvide incluir <code>/notify</code> 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. <code>new</code> means a new unique (unique combination of PrimaryId and SecondaryId) object was discovered. <code>watched-changed</code> means that selected <code>Watched_ValueN</code> columns changed."
"string" : "Apprise notification target URL. For example for Telegram it would be <code>tgram://{bot_token}/{chat_id}</code>."
},
{
"language_code": "es_es",
"string" : "Envíe una notificación solo en estos estados. <code>new</code> significa que se descubrió un nuevo objeto único (combinación única de PrimaryId y SecondaryId). <code>watched-changed</code> significa que seleccionó <code>Watched_ValueN Las columnas </code> cambiaron."
}]
}
"string" : "Informar de la URL de destino de la notificación. Por ejemplo, para Telegram sería <code>tgram://{bot_token}/{chat_id}</code>."
}]
},
{
"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 <code>html</code> works well with emails, <code>text</code> with chat apps, such as Telegram."
},
{
"language_code": "es_es",
"string" : "Seleccione el tipo de carga útil enviada a Apprise. Por ejemplo, <code>html</code> funciona bien con correos electrónicos, <code>text</code> 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 <code>(text was truncated)</code> 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 <code>(text was truncated)</code>."
}]
}
]
}

View File

@@ -1 +0,0 @@
This plugin will not be loaded

View File

@@ -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.

View File

@@ -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": "<i class=\"fa-solid fa-at\"></i>"
}
],
"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": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"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 <code>smtp-relay.sendinblue.com</code>. To use Gmail as an SMTP server <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP.md\">follow this guide</a>"
},
{
"language_code": "es_es",
"string": "La URL del host del servidor SMTP. Por ejemplo, <code>smtp-relay.sendinblue.com</code>. Para utilizar Gmail como servidor SMTP <a target=\"_blank\" href=\"https://github.com/jokob-sk/Pi.Alert/blob/main/docs/SMTP.md\">siga esta guía</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 <code>0</code> 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 <code>0</code> 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 <user@gmail.com>",
"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."
}
]
}
]
}

View File

@@ -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())

View File

@@ -1 +0,0 @@
This plugin will not be loaded

View File

@@ -1,5 +1,4 @@
#!/usr/bin/env python
# Based on the work of https://github.com/leiweibau/Pi.Alert
import argparse
import os

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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],
]

125
pialert/appevent.py Normal file
View File

@@ -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, <missing event>
"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()

View File

@@ -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'

View File

@@ -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 (

View File

@@ -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

View File

@@ -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) )

View File

@@ -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)

View File

@@ -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 <user@gmail.com>' , 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
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.')

114
pialert/notification.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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())

View File

@@ -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] + " <h1> (text was truncated)</h1>"
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])

View File

@@ -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))

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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('<ul>','<ul style="list-style:none;padding-left:0">').replace("<td>null</td>", "<td></td>")
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 ('<SECTION_NEW_DEVICES>', notiStruc.text + '\n')
mail_text = mail_text.replace ('<NEW_DEVICES_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<NEW_DEVICES_TABLE>', notiStruc.html)
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 ('<SECTION_DEVICES_DOWN>', notiStruc.text + '\n')
mail_text = mail_text.replace ('<DOWN_DEVICES_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<DOWN_DEVICES_TABLE>', notiStruc.html)
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 ('<SECTION_EVENTS>', notiStruc.text + '\n')
mail_text = mail_text.replace ('<EVENTS_TABLE>', notiStruc.text + '\n')
mail_html = mail_html.replace ('<EVENTS_TABLE>', 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()