Compare commits

..

6 Commits

Author SHA1 Message Date
dgtlmoon
5d4ebff235 little for apprise type urls that are converted ot post:// 2023-11-30 13:15:39 +01:00
dgtlmoon
74d4c580cf Merge branch 'fixing-post-headers' into api-import 2023-11-30 12:16:32 +01:00
dgtlmoon
b899579ca8 properly handle user/pass auth and clean URL without token 2023-11-30 12:15:22 +01:00
dgtlmoon
1f7f1e2bfa Fixing support for headers in custom post, posts etc notifications 2023-11-30 11:34:19 +01:00
dgtlmoon
0df773a12c Fixing support for headers in custom post, posts etc notifications 2023-11-30 11:33:36 +01:00
dgtlmoon
6a5566e771 Add import by list 2023-11-30 10:14:33 +01:00
9 changed files with 149 additions and 32 deletions

View File

@@ -38,7 +38,7 @@ from flask_paginate import Pagination, get_page_parameter
from changedetectionio import html_tools
from changedetectionio.api import api_v1
__version__ = '0.45.8.1'
__version__ = '0.45.8'
from changedetectionio.store import BASE_URL_NOT_SET_TEXT
@@ -240,6 +240,10 @@ def changedetection_app(config=None, datastore_o=None):
watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
watch_api.add_resource(api_v1.Import,
'/api/v1/import',
resource_class_kwargs={'datastore': datastore})
watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo',
resource_class_kwargs={'datastore': datastore, 'update_q': update_q})

View File

@@ -296,6 +296,62 @@ class CreateWatch(Resource):
return list, 200
class Import(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency
self.datastore = kwargs['datastore']
@auth.check_token
def post(self):
"""
@api {post} /api/v1/import - Import a list of watched URLs
@apiDescription Accepts a line-feed separated list of URLs to import, additionally with ?tag_uuids=(tag id), ?tag=(name), ?proxy={key}, ?dedupe=true (default true) one URL per line.
@apiExample {curl} Example usage:
curl http://localhost:5000/api/v1/import --data-binary @list-of-sites.txt -H"x-api-key:8a111a21bc2f8f1dd9b9353bbd46049a"
@apiName Import
@apiGroup Watch
@apiSuccess (200) {List} OK List of watch UUIDs added
@apiSuccess (500) {String} ERR Some other error
"""
extras = {}
if request.args.get('proxy'):
plist = self.datastore.proxy_list
if not request.args.get('proxy') in plist:
return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400
else:
extras['proxy'] = request.args.get('proxy')
dedupe = strtobool(request.args.get('dedupe', 'true'))
tags = request.args.get('tag')
tag_uuids = request.args.get('tag_uuids')
if tag_uuids:
tag_uuids = tag_uuids.split(',')
urls = request.get_data().decode('utf8').splitlines()
added = []
allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
for url in urls:
url = url.strip()
if not len(url):
continue
# If hosts that only contain alphanumerics are allowed ("localhost" for example)
if not validators.url(url, simple_host=allow_simplehost):
return f"Invalid or unsupported URL - {url}", 400
if dedupe and self.datastore.url_exists(url):
continue
new_uuid = self.datastore.add_watch(url=url, extras=extras, tag=tags, tag_uuids=tag_uuids)
added.append(new_uuid)
return added
class SystemInfo(Resource):
def __init__(self, **kwargs):
# datastore is a black box dependency

View File

@@ -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,46 @@ 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'),
headers=headers,
data=body,
params=params,
auth=auth
)
def process_notification(n_object, datastore):

View File

@@ -52,11 +52,6 @@ class difference_detection_processor():
prefer_fetch_backend = 'base_html_playwright'
browser_connection_url = connection[0].get('browser_connection_url')
# PDF should be html_requests because playwright will serve it up (so far) in a embedded page
# @todo https://github.com/dgtlmoon/changedetection.io/issues/2019
# @todo needs test to or a fix
if self.watch.is_pdf:
prefer_fetch_backend = "html_requests"
# Grab the right kind of 'fetcher', (playwright, requests, etc)
if hasattr(content_fetcher, prefer_fetch_backend):

View File

@@ -2,28 +2,21 @@ $(document).ready(function () {
// Lazy Hide/Show elements mechanism
$('[data-visible-for]').hide();
function show_related_elem(e) {
var n = $(e).attr('name') + "=" + $(e).val();
$(':radio').on('keyup keypress blur change click', function (e) {
$('[data-visible-for]').hide();
$('.advanced-options').hide();
var n = $(this).attr('name') + "=" + $(this).val();
if (n === 'fetch_backend=system') {
n = "fetch_backend=" + default_system_fetch_backend;
}
$(`[data-visible-for~="${n}"]`).show();
}
$(':radio').on('keyup keypress blur change click', function (e) {
$(`[data-visible-for]`).hide();
$('.advanced-options').hide();
show_related_elem(this);
});
$(':radio:checked').each(function (e) {
show_related_elem(this);
})
});
$(':radio:checked').change();
// Show advanced
$('.show-advanced').click(function (e) {
$(this).closest('.tab-pane-inner').find('.advanced-options').each(function (e) {
$(this).toggle();
})
$(this).closest('.tab-pane-inner').find('.advanced-options').toggle();
});
});

View File

@@ -1,4 +1,18 @@
$(document).ready(function () {
// Lazy Hide/Show elements mechanism
$('[data-visible-for]').hide();
$(':radio').on('keyup keypress blur change click', function (e){
$('[data-visible-for]').hide();
var n = $(this).attr('name') + "=" + $(this).val();
if (n === 'fetch_backend=system') {
n = "fetch_backend=" + default_system_fetch_backend;
}
$(`[data-visible-for~="${n}"]`).show();
});
$(':radio:checked').change();
$('#notification-setting-reset-to-default').click(function (e) {
$('#notification_title').val('');
$('#notification_body').val('');

View File

@@ -234,7 +234,7 @@ class ChangeDetectionStore:
# Probably their should be dict...
for watch in self.data['watching'].values():
if watch['url'] == url:
if watch['url'].lower() == url.lower():
return True
return False
@@ -333,7 +333,8 @@ class ChangeDetectionStore:
# Or if UUIDs given directly
if tag_uuids:
apply_extras['tags'] = list(set(apply_extras['tags'] + tag_uuids))
for t in tag_uuids:
apply_extras['tags'] = list(set(apply_extras['tags'] + [t.strip()]))
# Make any uuids unique
if apply_extras.get('tags'):

View File

@@ -127,7 +127,7 @@
{% endif %}
<!-- webdriver always -->
<fieldset data-visible-for="fetch_backend=html_webdriver" style="display: none;">
<fieldset data-visible-for="fetch_backend=html_webdriver">
<div class="pure-control-group">
{{ render_field(form.webdriver_delay) }}
<div class="pure-form-message-inline">
@@ -153,15 +153,15 @@
</div>
</fieldset>
<!-- html requests always -->
<fieldset data-visible-for="fetch_backend=html_requests">
<fieldset data-visible-for="fetch_backend=html_requests" style="display: none;">
<div class="pure-control-group">
<a class="pure-button button-secondary button-xsmall show-advanced">Show advanced options</a>
</div>
<div class="advanced-options" style="display: none;">
<div class="pure-control-group" id="request-method">
<div class="pure-control-group advanced-options" id="request-method" style="display: none;">
{{ render_field(form.method) }}
</div>
<div id="request-body">
<div class="advanced-options" id="request-body">
{{ render_field(form.body, rows=5, placeholder="Example
{
\"name\":\"John\",
@@ -187,8 +187,8 @@ User-Agent: wonderbra 1.0") }}
(Not supported by Selenium browser)
</div>
</div>
<fieldset data-visible-for="fetch_backend=html_requests fetch_backend=html_webdriver" >
<div class="pure-control-group inline-radio advanced-options" style="display: none;">
<fieldset data-visible-for="fetch_backend=html_requests fetch_backend=html_webdriver" style="display: none;">
<div class="pure-control-group inline-radio advanced-options" style="display: none;">
{{ render_checkbox_field(form.ignore_status_codes) }}
</div>
</fieldset>

View File

@@ -357,3 +357,24 @@ def test_api_watch_PUT_update(client, live_server):
# Cleanup everything
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data
def test_api_import(client, live_server):
api_key = extract_api_key_from_UI(client)
res = client.post(
url_for("import") + "?tag=import-test",
data='https://website1.com\r\nhttps://website2.com',
headers={'x-api-key': api_key},
follow_redirects=True
)
assert res.status_code == 200
assert len(res.json) == 2
res = client.get(url_for("index"))
assert b"https://website1.com" in res.data
assert b"https://website2.com" in res.data
# Should see the new tag in the tag/groups list
res = client.get(url_for('tags.tags_overview_page'))
assert b'import-test' in res.data