diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py index 6c0f53f6..93cd304e 100644 --- a/changedetectionio/notification.py +++ b/changedetectionio/notification.py @@ -46,6 +46,9 @@ from apprise.decorators import notify @notify(on="puts") def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs): import requests + from apprise.utils import parse_url as apprise_parse_url + from apprise.URLBase import URLBase + url = kwargs['meta'].get('url') if url.startswith('post'): @@ -68,16 +71,45 @@ def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs): url = url.replace('delete://', 'http://') url = url.replace('deletes://', 'https://') - # Try to auto-guess if it's JSON headers = {} + params = {} + auth = None + + # Convert /foobar?+some-header=hello to proper header dictionary + results = apprise_parse_url(url) + if results: + # Add our headers that the user can potentially over-ride if they wish + # to to our returned result set and tidy entries by unquoting them + headers = {URLBase.unquote(x): URLBase.unquote(y) + for x, y in results['qsd+'].items()} + + # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#get-parameter-manipulation + # In Apprise, it relies on prefixing each request arg with "-", because it uses say &method=update as a flag for apprise + # but here we are making straight requests, so we need todo convert this against apprise's logic + for k, v in results['qsd'].items(): + if not k.strip('+-') in results['qsd+'].keys(): + params[URLBase.unquote(k)] = URLBase.unquote(v) + + # Determine Authentication + auth = '' + if results.get('user') and results.get('password'): + auth = (URLBase.unquote(results.get('user')), URLBase.unquote(results.get('user'))) + elif results.get('user'): + auth = (URLBase.unquote(results.get('user'))) + + # Try to auto-guess if it's JSON try: json.loads(body) - headers = {'Content-Type': 'application/json; charset=utf-8'} + headers['Content-Type'] = 'application/json; charset=utf-8' except ValueError as e: pass - - r(url, headers=headers, data=body) + r(results.get('url'), + auth=auth, + data=body, + headers=headers, + params=params + ) def process_notification(n_object, datastore): diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja index cb27476e..fa113e17 100644 --- a/changedetectionio/templates/_common_fields.jinja +++ b/changedetectionio/templates/_common_fields.jinja @@ -16,7 +16,7 @@
  • discord:// (or https://discord.com/api/webhooks...)) only supports a maximum 2,000 characters of notification text, including the title.
  • tgram:// bots can't send messages to other bots, so you should specify chat ID of non-bot user.
  • tgram:// only supports very limited HTML and can fail when extra tags are sent, read more here (or use plaintext/markdown format)
  • -
  • gets://, posts://, puts://, deletes:// for direct API calls (or omit the "s" for non-SSL ie get://)
  • +
  • gets://, posts://, puts://, deletes:// for direct API calls (or omit the "s" for non-SSL ie get://) more help here
  • Accepts the {{ '{{token}}' }} placeholders listed below
  • diff --git a/changedetectionio/tests/conftest.py b/changedetectionio/tests/conftest.py index 754ec1fc..79ea5bc8 100644 --- a/changedetectionio/tests/conftest.py +++ b/changedetectionio/tests/conftest.py @@ -13,22 +13,17 @@ global app def cleanup(datastore_path): + import glob # Unlink test output files - files = [ - 'count.txt', - 'endpoint-content.txt' - 'headers.txt', - 'headers-testtag.txt', - 'notification.txt', - 'secret.txt', - 'url-watches.json', - 'output.txt', - ] - for file in files: - try: - os.unlink("{}/{}".format(datastore_path, file)) - except FileNotFoundError: - pass + + for g in ["*.txt", "*.json", "*.pdf"]: + files = glob.glob(os.path.join(datastore_path, g)) + for f in files: + if 'proxies.json' in f: + # Usually mounted by docker container during test time + continue + if os.path.isfile(f): + os.unlink(f) @pytest.fixture(scope='session') def app(request): diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index 7d0e5ff2..3c6674f8 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -281,7 +281,8 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server): # CUSTOM JSON BODY CHECK for POST:// set_original_response() - test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}" + # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#header-manipulation + test_notification_url = url_for('test_notification_endpoint', _external=True).replace('http://', 'post://')+"?xxx={{ watch_url }}&+custom-header=123" res = client.post( url_for("settings_page"), @@ -297,10 +298,7 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server): follow_redirects=True ) assert b'Settings updated' in res.data - client.get( - url_for("form_delete", uuid="all"), - follow_redirects=True - ) + # Add a watch and trigger a HTTP POST test_url = url_for('test_endpoint', _external=True) res = client.post( @@ -315,7 +313,9 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server): set_modified_response() client.get(url_for("form_watch_checknow"), follow_redirects=True) - time.sleep(2) + wait_for_all_checks(client) + + time.sleep(2) # plus extra delay for notifications to fire with open("test-datastore/notification.txt", 'r') as f: x = f.read() @@ -328,6 +328,13 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server): with open("test-datastore/notification-url.txt", 'r') as f: notification_url = f.read() assert 'xxx=http' in notification_url + # apprise style headers should be stripped + assert 'custom-header' not in notification_url + + with open("test-datastore/notification-headers.txt", 'r') as f: + notification_headers = f.read() + assert 'custom-header: 123' in notification_headers.lower() + # Should always be automatically detected as JSON content type even when we set it as 'Text' (default) assert os.path.isfile("test-datastore/notification-content-type.txt") @@ -335,3 +342,8 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server): assert 'application/json' in f.read() os.unlink("test-datastore/notification-url.txt") + + client.get( + url_for("form_delete", uuid="all"), + follow_redirects=True + ) diff --git a/changedetectionio/tests/util.py b/changedetectionio/tests/util.py index 904c1b62..ed1e424e 100644 --- a/changedetectionio/tests/util.py +++ b/changedetectionio/tests/util.py @@ -205,6 +205,9 @@ def live_server_setup(live_server): with open("test-datastore/notification-url.txt", "w") as f: f.write(request.url) + with open("test-datastore/notification-headers.txt", "w") as f: + f.write(str(request.headers)) + if request.content_type: with open("test-datastore/notification-content-type.txt", "w") as f: f.write(request.content_type)