From f877af75b92c352aa4b3e05a2f54e8a193cd9ba8 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Sat, 8 May 2021 11:29:41 +1000 Subject: [PATCH] Apprise notifications (#43) * issue #4 Adding settings screen for apprise URLS * Adding test notification mechanism * Move Worker module to own class file * Adding basic notification URL runner * Tests for notifications * Tweak readme with notification info * Move notification test to main test_backend.py * Fix spacing * Adding notifications screenshot * Cleanup more files from test * Offer send notification test on individual edits and main/default * Process global notifications * All branches test * Wrap worker notification process in try/catch, use global if nothing set * Fix syntax * Handle exception, increase wait time for liveserver to come up * Fixing test setup * remove debug * Split tests into their own totally isolated setups, if you know a better way to make live_server() work, MR :) * Tidying up lint/imports --- .github/workflows/test-only.yml | 10 +- README.md | 23 +++++ backend/__init__.py | 135 ++++++++++++++++++--------- backend/pytest.ini | 2 +- backend/run_all_tests.sh | 16 ++++ backend/store.py | 8 +- backend/templates/edit.html | 18 +++- backend/templates/settings.html | 22 +++++ backend/tests/conftest.py | 13 ++- backend/tests/test_access_control.py | 58 ++++++++++++ backend/tests/test_backend.py | 99 +------------------- backend/tests/test_ignore_text.py | 5 +- backend/tests/test_notification.py | 66 +++++++++++++ backend/tests/util.py | 60 ++++++++++++ backend/update_worker.py | 67 +++++++++++++ requirements.txt | 7 +- screenshot-notifications.png | Bin 0 -> 27536 bytes 17 files changed, 446 insertions(+), 163 deletions(-) create mode 100755 backend/run_all_tests.sh create mode 100644 backend/tests/test_access_control.py create mode 100644 backend/tests/test_notification.py create mode 100644 backend/tests/util.py create mode 100644 backend/update_worker.py create mode 100644 screenshot-notifications.png diff --git a/.github/workflows/test-only.yml b/.github/workflows/test-only.yml index eb1a1805..5b9005da 100644 --- a/.github/workflows/test-only.yml +++ b/.github/workflows/test-only.yml @@ -1,10 +1,7 @@ name: Test only -on: - push: - branches: - - /refs/heads/* - - !master +# Triggers the workflow on push or pull request events +on: [push, pull_request] jobs: build: @@ -31,5 +28,6 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - cd backend; pytest + # Each test is totally isolated and performs its own cleanup/reset + cd backend; ./run_all_tests.sh diff --git a/README.md b/README.md index acfa0c65..d02e711c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,29 @@ Examining differences in content. Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/ +### Notifications + +ChangeDetection.io supports a massive amount of notifications (including email, office365, custom APIs, etc) when a web-page has a change detected thanks to the apprise library. +Simply set one or more notification URL's in the _[edit]_ tab of that watch. + +Just some examples + + discord://webhook_id/webhook_token + flock://app_token/g:channel_id + gitter://token/room + gchat://workspace/key/token + msteams://TokenA/TokenB/TokenC/ + o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail + rocket://user:password@hostname/#Channel + mailto://user:pass@example.com?to=receivingAddress@example.com + json://someserver.com/custom-api + syslog:// + +And everything else in this list! + +Self-hosted web page change monitoring notifications + + ### Notes - Does not yet support Javascript diff --git a/backend/__init__.py b/backend/__init__.py index da5975db..8efd5df1 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -41,7 +41,9 @@ extra_stylesheets = [] update_q = queue.Queue() -app = Flask(__name__, static_url_path="/var/www/change-detection/backen/static") +notification_q = queue.Queue() + +app = Flask(__name__, static_url_path="/var/www/change-detection/backend/static") # Stop browser caching of assets app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 @@ -347,11 +349,22 @@ def changedetection_app(conig=None, datastore_o=None): 'headers': extra_headers } + # Notification URLs + form_notification_text = request.form.get('notification_urls') + notification_urls = [] + if form_notification_text: + for text in form_notification_text.strip().split("\n"): + text = text.strip() + if len(text): + notification_urls.append(text) + + datastore.data['watching'][uuid]['notification_urls'] = notification_urls + # Ignore text - form_ignore_text = request.form.get('ignore-text').strip() + form_ignore_text = request.form.get('ignore-text') ignore_text = [] - if len(form_ignore_text): - for text in form_ignore_text.split("\n"): + if form_ignore_text: + for text in form_ignore_text.strip().split("\n"): text = text.strip() if len(text): ignore_text.append(text) @@ -368,6 +381,14 @@ def changedetection_app(conig=None, datastore_o=None): messages.append({'class': 'ok', 'message': 'Updated watch.'}) + trigger_n = request.form.get('trigger-test-notification') + if trigger_n: + n_object = {'watch_url': url, + 'notification_urls': datastore.data['settings']['application']['notification_urls']} + notification_q.put(n_object) + + messages.append({'class': 'ok', 'message': 'Notifications queued.'}) + return redirect(url_for('index')) else: @@ -381,6 +402,30 @@ def changedetection_app(conig=None, datastore_o=None): global messages if request.method == 'GET': + if request.values.get('notification-test'): + url_count = len(datastore.data['settings']['application']['notification_urls']) + if url_count: + import apprise + apobj = apprise.Apprise() + apobj.debug = True + + # Add each notification + for n in datastore.data['settings']['application']['notification_urls']: + apobj.add(n) + outcome = apobj.notify( + body='Hello from the worlds best and simplest web page change detection and monitoring service!', + title='Changedetection.io Notification Test', + ) + + if outcome: + messages.append( + {'class': 'notice', 'message': "{} Notification URLs reached.".format(url_count)}) + else: + messages.append( + {'class': 'error', 'message': "One or more Notification URLs failed"}) + + return redirect(url_for('settings_page')) + if request.values.get('removepassword'): from pathlib import Path @@ -417,16 +462,30 @@ def changedetection_app(conig=None, datastore_o=None): if minutes >= 5: datastore.data['settings']['requests']['minutes_between_check'] = minutes datastore.needs_write = True - - messages.append({'class': 'ok', 'message': "Updated"}) else: messages.append( {'class': 'error', 'message': "Must be atleast 5 minutes."}) + # 'validators' package doesnt work because its often a non-stanadard protocol. :( + datastore.data['settings']['application']['notification_urls'] = [] + trigger_n = request.form.get('trigger-test-notification') + for n in request.values.get('notification_urls').strip().split("\n"): + url = n.strip() + datastore.data['settings']['application']['notification_urls'].append(url) + datastore.needs_write = True + + if trigger_n: + n_object = {'watch_url': "Test from changedetection.io!", + 'notification_urls': datastore.data['settings']['application']['notification_urls']} + notification_q.put(n_object) + + messages.append({'class': 'ok', 'message': 'Notifications queued.'}) output = render_template("settings.html", messages=messages, - minutes=datastore.data['settings']['requests']['minutes_between_check']) + minutes=datastore.data['settings']['requests']['minutes_between_check'], + notification_urls="\r\n".join( + datastore.data['settings']['application']['notification_urls'])) messages = [] return output @@ -687,6 +746,8 @@ def changedetection_app(conig=None, datastore_o=None): # @todo handle ctrl break ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start() + threading.Thread(target=notification_runner).start() + # Check for new release version threading.Thread(target=check_for_new_version).start() return app @@ -718,54 +779,42 @@ def check_for_new_version(): # Check daily app.config.exit.wait(86400) +def notification_runner(): -# Requests for checking on the site use a pool of thread Workers managed by a Queue. -class Worker(threading.Thread): - current_uuid = None - - def __init__(self, q, *args, **kwargs): - self.q = q - super().__init__(*args, **kwargs) - - def run(self): - from backend import fetch_site_status - - update_handler = fetch_site_status.perform_site_check(datastore=datastore) + while not app.config.exit.is_set(): + try: + # At the moment only one thread runs (single runner) + n_object = notification_q.get(block=False) + except queue.Empty: + time.sleep(1) + pass - while not app.config.exit.is_set(): + else: + import apprise + # Create an Apprise instance try: - uuid = self.q.get(block=False) - except queue.Empty: - pass - - else: - self.current_uuid = uuid + apobj = apprise.Apprise() + for url in n_object['notification_urls']: + apobj.add(url.strip()) - if uuid in list(datastore.data['watching'].keys()): - try: - changed_detected, result, contents = update_handler.run(uuid) + apobj.notify( + body=n_object['watch_url'], + # @todo This should be configurable. + title="ChangeDetection.io Notification - {}".format(n_object['watch_url']) + ) - except PermissionError as s: - app.logger.error("File permission error updating", uuid, str(s)) - else: - if result: - datastore.update_watch(uuid=uuid, update_obj=result) - if changed_detected: - # A change was detected - datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result) - - self.current_uuid = None # Done - self.q.task_done() - - app.config.exit.wait(1) + except Exception as e: + print("Watch URL: {} Error {}".format(n_object['watch_url'],e)) # Thread runner to check every minute, look for new watches to feed into the Queue. def ticker_thread_check_time_launch_checks(): + from backend import update_worker + # Spin up Workers. for _ in range(datastore.data['settings']['requests']['workers']): - new_worker = Worker(update_q) + new_worker = update_worker.update_worker(update_q, notification_q, app, datastore) running_update_threads.append(new_worker) new_worker.start() diff --git a/backend/pytest.ini b/backend/pytest.ini index 883439b1..af2b409a 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,7 +1,7 @@ [pytest] addopts = --no-start-live-server --live-server-port=5005 #testpaths = tests pytest_invenio -#live_server_scope = session +#live_server_scope = function filterwarnings = ignore::DeprecationWarning:urllib3.*: diff --git a/backend/run_all_tests.sh b/backend/run_all_tests.sh new file mode 100755 index 00000000..43c487ce --- /dev/null +++ b/backend/run_all_tests.sh @@ -0,0 +1,16 @@ +#!/bin/bash + + +# live_server will throw errors even with live_server_scope=function if I have the live_server setup in different functions +# and I like to restart the server for each test (and have the test cleanup after each test) +# merge request welcome :) + + +# exit when any command fails +set -e + +find tests/test_*py -type f|while read test_name +do + echo "TEST RUNNING $test_name" + pytest $test_name +done diff --git a/backend/store.py b/backend/store.py index d14d8086..34e4938b 100644 --- a/backend/store.py +++ b/backend/store.py @@ -39,7 +39,8 @@ class ChangeDetectionStore: 'workers': 10 # Number of threads, lower is better for slow connections }, 'application': { - 'password': False + 'password': False, + 'notification_urls': [] # Apprise URL list } } } @@ -58,7 +59,8 @@ class ChangeDetectionStore: 'uuid': str(uuid_builder.uuid4()), 'headers': {}, # Extra headers to send 'history': {}, # Dict of timestamp and output stripped filename - 'ignore_text': [] # List of text to ignore when calculating the comparison checksum + 'ignore_text': [], # List of text to ignore when calculating the comparison checksum + 'notification_urls': [] # List of URLs to add to the notification Queue (Usually AppRise) } if path.isfile('/source.txt'): @@ -109,7 +111,7 @@ class ChangeDetectionStore: self.add_watch(url='https://www.gov.uk/coronavirus', tag='Covid') self.add_watch(url='https://changedetection.io', tag='Tech news') - self.__data['version_tag'] = "0.292" + self.__data['version_tag'] = "0.30" if not 'app_guid' in self.__data: import sys diff --git a/backend/templates/edit.html b/backend/templates/edit.html index 3b032acb..f889cf8c 100644 --- a/backend/templates/edit.html +++ b/backend/templates/edit.html @@ -49,8 +49,24 @@ User-Agent: wonderbra 1.0"
+
+ + + Use AppRise URLs for notification to just about any service! +
+
+ - +
+
+
diff --git a/backend/templates/settings.html b/backend/templates/settings.html index 7417f36c..fec14a4b 100644 --- a/backend/templates/settings.html +++ b/backend/templates/settings.html @@ -13,6 +13,7 @@ This is a required field.
+
@@ -21,6 +22,27 @@ {% endif %}
+
+
+ +
+
+ Notification URLs see Apprise examples. + +
+
+ + +
+
diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 836e0fd1..1daf7d74 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -35,13 +35,12 @@ def app(request): def teardown(): datastore.stop_thread = True app.config.exit.set() - try: - os.unlink("{}/url-watches.json".format(datastore_path)) - except FileNotFoundError: - # This is fine in the case of a failure. - pass - - assert 1 == 1 + for fname in ["url-watches.json", "count.txt", "output.txt"]: + try: + os.unlink("{}/{}".format(datastore_path, fname)) + except FileNotFoundError: + # This is fine in the case of a failure. + pass request.addfinalizer(teardown) yield app diff --git a/backend/tests/test_access_control.py b/backend/tests/test_access_control.py new file mode 100644 index 00000000..b615a2c0 --- /dev/null +++ b/backend/tests/test_access_control.py @@ -0,0 +1,58 @@ +from flask import url_for + +def test_check_access_control(app, client): + # Still doesnt work, but this is closer. + return + with app.test_client() as c: + + # Check we dont have any password protection enabled yet. + res = c.get(url_for("settings_page")) + assert b"Remove password" not in res.data + + # Enable password check. + res = c.post( + url_for("settings_page"), + data={"password": "foobar"}, + follow_redirects=True + ) + assert b"Password protection enabled." in res.data + assert b"LOG OUT" not in res.data + print ("SESSION:", res.session) + # Check we hit the login + + res = c.get(url_for("settings_page"), follow_redirects=True) + res = c.get(url_for("login"), follow_redirects=True) + + assert b"Login" in res.data + + print ("DEBUG >>>>>",res.data) + # Menu should not be available yet + assert b"SETTINGS" not in res.data + assert b"BACKUP" not in res.data + assert b"IMPORT" not in res.data + + + + #defaultuser@changedetection.io is actually hardcoded for now, we only use a single password + res = c.post( + url_for("login"), + data={"password": "foobar", "email": "defaultuser@changedetection.io"}, + follow_redirects=True + ) + + assert b"LOG OUT" in res.data + res = c.get(url_for("settings_page")) + + # Menu should be available now + assert b"SETTINGS" in res.data + assert b"BACKUP" in res.data + assert b"IMPORT" in res.data + + assert b"LOG OUT" in res.data + + # Now remove the password so other tests function, @todo this should happen before each test automatically + + c.get(url_for("settings_page", removepassword="true")) + c.get(url_for("import_page")) + assert b"LOG OUT" not in res.data + diff --git a/backend/tests/test_backend.py b/backend/tests/test_backend.py index 42291a0f..b832f94b 100644 --- a/backend/tests/test_backend.py +++ b/backend/tests/test_backend.py @@ -3,55 +3,16 @@ import time from flask import url_for from urllib.request import urlopen -import pytest +from . util import set_original_response, set_modified_response, live_server_setup sleep_time_for_fetch_thread = 3 -def test_setup_liveserver(live_server): - @live_server.app.route('/test-endpoint') - def test_endpoint(): - # Tried using a global var here but didn't seem to work, so reading from a file instead. - with open("test-datastore/output.txt", "r") as f: - return f.read() - - live_server.start() - - assert 1 == 1 - - -def set_original_response(): - test_return_data = """ - - Some initial text
-

Which is across multiple lines

-
- So let's see what happens.
- - - """ - - with open("test-datastore/output.txt", "w") as f: - f.write(test_return_data) - - -def set_modified_response(): - test_return_data = """ - - Some initial text
-

which has this one new line

-
- So let's see what happens.
- - - """ - - with open("test-datastore/output.txt", "w") as f: - f.write(test_return_data) def test_check_basic_change_detection_functionality(client, live_server): set_original_response() + live_server_setup(live_server) # Add our URL to the import page res = client.post( @@ -128,59 +89,3 @@ def test_check_basic_change_detection_functionality(client, live_server): res = client.get(url_for("api_delete", uuid="all"), follow_redirects=True) assert b'Deleted' in res.data - -def test_check_access_control(app, client): - # Still doesnt work, but this is closer. - return - with app.test_client() as c: - - # Check we dont have any password protection enabled yet. - res = c.get(url_for("settings_page")) - assert b"Remove password" not in res.data - - # Enable password check. - res = c.post( - url_for("settings_page"), - data={"password": "foobar"}, - follow_redirects=True - ) - assert b"Password protection enabled." in res.data - assert b"LOG OUT" not in res.data - print ("SESSION:", res.session) - # Check we hit the login - - res = c.get(url_for("settings_page"), follow_redirects=True) - res = c.get(url_for("login"), follow_redirects=True) - - assert b"Login" in res.data - - print ("DEBUG >>>>>",res.data) - # Menu should not be available yet - assert b"SETTINGS" not in res.data - assert b"BACKUP" not in res.data - assert b"IMPORT" not in res.data - - - - #defaultuser@changedetection.io is actually hardcoded for now, we only use a single password - res = c.post( - url_for("login"), - data={"password": "foobar", "email": "defaultuser@changedetection.io"}, - follow_redirects=True - ) - - assert b"LOG OUT" in res.data - res = c.get(url_for("settings_page")) - - # Menu should be available now - assert b"SETTINGS" in res.data - assert b"BACKUP" in res.data - assert b"IMPORT" in res.data - - assert b"LOG OUT" in res.data - - # Now remove the password so other tests function, @todo this should happen before each test automatically - - c.get(url_for("settings_page", removepassword="true")) - c.get(url_for("import_page")) - assert b"LOG OUT" not in res.data \ No newline at end of file diff --git a/backend/tests/test_ignore_text.py b/backend/tests/test_ignore_text.py index 29ad7d06..77ab7c4d 100644 --- a/backend/tests/test_ignore_text.py +++ b/backend/tests/test_ignore_text.py @@ -2,9 +2,10 @@ import time from flask import url_for -from urllib.request import urlopen -import pytest +from . util import live_server_setup +def test_setup(live_server): + live_server_setup(live_server) # Unit test of the stripper # Always we are dealing in utf-8 diff --git a/backend/tests/test_notification.py b/backend/tests/test_notification.py new file mode 100644 index 00000000..e079865b --- /dev/null +++ b/backend/tests/test_notification.py @@ -0,0 +1,66 @@ + +import time +from flask import url_for +from . util import set_original_response, set_modified_response, live_server_setup + +# Hard to just add more live server URLs when one test is already running (I think) +# So we add our test here (was in a different file) +def test_check_notification(client, live_server): + + live_server_setup(live_server) + set_original_response() + + # Give the endpoint time to spin up + time.sleep(3) + + # Add our URL to the import page + test_url = url_for('test_endpoint', _external=True) + res = client.post( + url_for("import_page"), + data={"urls": test_url}, + follow_redirects=True + ) + assert b"1 Imported" in res.data + + # Give the thread time to pick it up + time.sleep(3) + + # Goto the edit page, add our ignore text + # Add our URL to the import page + url = url_for('test_notification_endpoint', _external=True) + notification_url = url.replace('http', 'json') + + print (">>>> Notification URL: "+notification_url) + res = client.post( + url_for("edit_page", uuid="first"), + data={"notification_urls": notification_url, "url": test_url, "tag": "", "headers": ""}, + follow_redirects=True + ) + assert b"Updated watch." in res.data + + # Hit the edit page, be sure that we saved it + res = client.get( + url_for("edit_page", uuid="first")) + assert bytes(notification_url.encode('utf-8')) in res.data + + set_modified_response() + + # Trigger a check + client.get(url_for("api_watch_checknow"), follow_redirects=True) + + # Give the thread time to pick it up + time.sleep(3) + + # Did the front end see it? + res = client.get( + url_for("index")) + assert bytes("just now".encode('utf-8')) in res.data + + + # Check it triggered + res = client.get( + url_for("test_notification_counter"), + ) + print (res.data) + + assert bytes("we hit it".encode('utf-8')) in res.data diff --git a/backend/tests/util.py b/backend/tests/util.py new file mode 100644 index 00000000..6f655991 --- /dev/null +++ b/backend/tests/util.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 + + +def set_original_response(): + test_return_data = """ + + Some initial text
+

Which is across multiple lines

+
+ So let's see what happens.
+ + + """ + + with open("test-datastore/output.txt", "w") as f: + f.write(test_return_data) + return None + +def set_modified_response(): + test_return_data = """ + + Some initial text
+

which has this one new line

+
+ So let's see what happens.
+ + + """ + + with open("test-datastore/output.txt", "w") as f: + f.write(test_return_data) + + return None + + +def live_server_setup(live_server): + + @live_server.app.route('/test-endpoint') + def test_endpoint(): + # Tried using a global var here but didn't seem to work, so reading from a file instead. + with open("test-datastore/output.txt", "r") as f: + return f.read() + + @live_server.app.route('/test_notification_endpoint', methods=['POST']) + def test_notification_endpoint(): + with open("test-datastore/count.txt", "w") as f: + f.write("we hit it") + print("\n>> Test notification endpoint was hit.\n") + return "Text was set" + + # And this should return not zero. + @live_server.app.route('/test_notification_counter') + def test_notification_counter(): + try: + with open("test-datastore/count.txt", "r") as f: + return f.read() + except FileNotFoundError: + return "nope :(" + + live_server.start() \ No newline at end of file diff --git a/backend/update_worker.py b/backend/update_worker.py new file mode 100644 index 00000000..5d1db851 --- /dev/null +++ b/backend/update_worker.py @@ -0,0 +1,67 @@ +import threading +import queue + +# Requests for checking on the site use a pool of thread Workers managed by a Queue. +class update_worker(threading.Thread): + current_uuid = None + + def __init__(self, q, notification_q, app, datastore, *args, **kwargs): + self.q = q + self.app = app + self.notification_q = notification_q + self.datastore = datastore + super().__init__(*args, **kwargs) + + def run(self): + from backend import fetch_site_status + + update_handler = fetch_site_status.perform_site_check(datastore=self.datastore) + + while not self.app.config.exit.is_set(): + + try: + uuid = self.q.get(block=False) + except queue.Empty: + pass + + else: + self.current_uuid = uuid + + if uuid in list(self.datastore.data['watching'].keys()): + try: + changed_detected, result, contents = update_handler.run(uuid) + + except PermissionError as s: + self.app.logger.error("File permission error updating", uuid, str(s)) + else: + if result: + try: + self.datastore.update_watch(uuid=uuid, update_obj=result) + if changed_detected: + # A change was detected + self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result) + + watch = self.datastore.data['watching'][uuid] + + # Did it have any notification alerts to hit? + if len(watch['notification_urls']): + print("Processing notifications for UUID: {}".format(uuid)) + n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], + 'notification_urls': watch['notification_urls']} + self.notification_q.put(n_object) + + + # No? maybe theres a global setting, queue them all + elif len(self.datastore.data['settings']['application']['notification_urls']): + print("Processing GLOBAL notifications for UUID: {}".format(uuid)) + n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'], + 'notification_urls': self.datastore.data['settings']['application'][ + 'notification_urls']} + self.notification_q.put(n_object) + except Exception as e: + print("!!!! Exception in update_worker !!!\n", e) + + self.current_uuid = None # Done + self.q.task_done() + + self.app.config.exit.wait(1) diff --git a/requirements.txt b/requirements.txt index 958473e0..ff04dbb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ chardet==2.3.0 flask~= 1.0 pytest ~=6.2 -pytest-flask ~=1.1 -eventlet>=0.31.0 +pytest-flask ~=1.2 +eventlet ~= 0.30 requests ~= 2.15 validators timeago ~=1.0 @@ -10,4 +10,5 @@ inscriptis ~= 1.1 feedgen ~= 0.9 flask-login ~= 0.5 pytz -urllib3 \ No newline at end of file +urllib3 +apprise ~= 0.9 diff --git a/screenshot-notifications.png b/screenshot-notifications.png new file mode 100644 index 0000000000000000000000000000000000000000..ad9e2be9530004199a0f65210ac50cc83ce654fa GIT binary patch literal 27536 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