mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-04 16:45:57 +00:00
Compare commits
48 Commits
1659-notif
...
0.45.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
460c724e51 | ||
|
|
dcf4bf37ed | ||
|
|
e3cf22fc27 | ||
|
|
d497db639e | ||
|
|
7355ac8d21 | ||
|
|
2f2d0ea0f2 | ||
|
|
a958e1fe20 | ||
|
|
5dc3b00ec6 | ||
|
|
8ac4757cd9 | ||
|
|
2180bb256d | ||
|
|
212f15ad5f | ||
|
|
22b2068208 | ||
|
|
4916043055 | ||
|
|
7bf13bad30 | ||
|
|
0aa2276afb | ||
|
|
3b875e5a6a | ||
|
|
8ec50294d2 | ||
|
|
e3c9255d9e | ||
|
|
3b03bdcb82 | ||
|
|
e25792bcec | ||
|
|
bf4168a2aa | ||
|
|
9d37eaa57b | ||
|
|
40d01acde9 | ||
|
|
d34832de73 | ||
|
|
ed4bafae63 | ||
|
|
3a5bceadfa | ||
|
|
6abdf2d332 | ||
|
|
dee23709a9 | ||
|
|
52df3b10e7 | ||
|
|
087d21c61e | ||
|
|
171faf465c | ||
|
|
a3d8bd0b1a | ||
|
|
6ef8a1c18f | ||
|
|
126f0fbf87 | ||
|
|
cfa712c88c | ||
|
|
6a6ba40b6a | ||
|
|
e7f726c057 | ||
|
|
df0cc7b585 | ||
|
|
76cd98b521 | ||
|
|
f84ba0fb31 | ||
|
|
c35cbd33d6 | ||
|
|
661f7fe32c | ||
|
|
7cb7eebbc5 | ||
|
|
aaceb4ebad | ||
|
|
56cf6e5ea5 | ||
|
|
1987e109e8 | ||
|
|
20d65cdd26 | ||
|
|
37ff5f6d37 |
4
.github/workflows/containers.yml
vendored
4
.github/workflows/containers.yml
vendored
@@ -95,7 +95,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:dev,ghcr.io/${{ github.repository }}:dev
|
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:dev,ghcr.io/${{ github.repository }}:dev
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
# Looks like this was disabled
|
# Looks like this was disabled
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
ghcr.io/dgtlmoon/changedetection.io:${{ github.event.release.tag_name }}
|
ghcr.io/dgtlmoon/changedetection.io:${{ github.event.release.tag_name }}
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:latest
|
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:latest
|
||||||
ghcr.io/dgtlmoon/changedetection.io:latest
|
ghcr.io/dgtlmoon/changedetection.io:latest
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
# Looks like this was disabled
|
# Looks like this was disabled
|
||||||
|
|||||||
2
.github/workflows/test-container-build.yml
vendored
2
.github/workflows/test-container-build.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
platforms: linux/arm/v7,linux/arm/v6,linux/amd64,linux/arm64,
|
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/arm/v8
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/test-only.yml
vendored
3
.github/workflows/test-only.yml
vendored
@@ -36,6 +36,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Build a changedetection.io container and start testing inside
|
# Build a changedetection.io container and start testing inside
|
||||||
docker build . -t test-changedetectionio
|
docker build . -t test-changedetectionio
|
||||||
|
# Debug info
|
||||||
|
docker run test-changedetectionio bash -c 'pip list'
|
||||||
|
|
||||||
- name: Spin up ancillary SMTP+Echo message test server
|
- name: Spin up ancillary SMTP+Echo message test server
|
||||||
run: |
|
run: |
|
||||||
@@ -44,7 +46,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Test built container with pytest
|
- name: Test built container with pytest
|
||||||
run: |
|
run: |
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_notification_diff'
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ include changedetection.py
|
|||||||
global-exclude *.pyc
|
global-exclude *.pyc
|
||||||
global-exclude node_modules
|
global-exclude node_modules
|
||||||
global-exclude venv
|
global-exclude venv
|
||||||
|
|
||||||
|
global-exclude test-datastore
|
||||||
|
global-exclude changedetection.io*dist-info
|
||||||
|
|||||||
@@ -54,5 +54,5 @@ $ changedetection.io -d /path/to/empty/data/dir -p 5000
|
|||||||
|
|
||||||
Then visit http://127.0.0.1:5000 , You should now be able to access the UI.
|
Then visit http://127.0.0.1:5000 , You should now be able to access the UI.
|
||||||
|
|
||||||
See https://github.com/dgtlmoon/changedetection.io for more information.
|
See https://changedetection.io for more information.
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -67,13 +67,14 @@ Requires Playwright to be enabled.
|
|||||||
- Get alerts when new job positions are open on Bamboo HR and other job platforms
|
- Get alerts when new job positions are open on Bamboo HR and other job platforms
|
||||||
- Website defacement monitoring
|
- Website defacement monitoring
|
||||||
- Pokémon Card Restock Tracker / Pokémon TCG Tracker
|
- Pokémon Card Restock Tracker / Pokémon TCG Tracker
|
||||||
|
- RegTech - stay ahead of regulatory changes, regulatory compliance
|
||||||
|
|
||||||
_Need an actual Chrome runner with Javascript support? We support fetching via WebDriver and Playwright!</a>_
|
_Need an actual Chrome runner with Javascript support? We support fetching via WebDriver and Playwright!</a>_
|
||||||
|
|
||||||
#### Key Features
|
#### Key Features
|
||||||
|
|
||||||
- Lots of trigger filters, such as "Trigger on text", "Remove text by selector", "Ignore text", "Extract text", also using regular-expressions!
|
- Lots of trigger filters, such as "Trigger on text", "Remove text by selector", "Ignore text", "Extract text", also using regular-expressions!
|
||||||
- Target elements with xPath and CSS Selectors, Easily monitor complex JSON with JSONPath or jq
|
- Target elements with xPath(1.0) and CSS Selectors, Easily monitor complex JSON with JSONPath or jq
|
||||||
- Switch between fast non-JS and Chrome JS based "fetchers"
|
- Switch between fast non-JS and Chrome JS based "fetchers"
|
||||||
- Track changes in PDF files (Monitor text changed in the PDF, Also monitor PDF filesize and checksums)
|
- Track changes in PDF files (Monitor text changed in the PDF, Also monitor PDF filesize and checksums)
|
||||||
- Easily specify how often a site should be checked
|
- Easily specify how often a site should be checked
|
||||||
@@ -85,6 +86,8 @@ _Need an actual Chrome runner with Javascript support? We support fetching via W
|
|||||||
|
|
||||||
We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link.
|
We [recommend and use Bright Data](https://brightdata.grsm.io/n0r16zf7eivq) global proxy services, Bright Data will match any first deposit up to $100 using our signup link.
|
||||||
|
|
||||||
|
[Oxylabs](https://oxylabs.go2cloud.org/SH2d) is also an excellent proxy provider and well worth using, they offer Residental, ISP, Rotating and many other proxy types to suit your project.
|
||||||
|
|
||||||
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
|
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -144,7 +147,7 @@ See the wiki for more information https://github.com/dgtlmoon/changedetection.io
|
|||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
|
|
||||||
XPath, JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools.
|
XPath(1.0), JSONPath, jq, and CSS support comes baked in! You can be as specific as you need, use XPath exported from various XPath element query creation tools.
|
||||||
(We support LXML `re:test`, `re:match` and `re:replace`.)
|
(We support LXML `re:test`, `re:match` and `re:replace`.)
|
||||||
|
|
||||||
## Notifications
|
## Notifications
|
||||||
@@ -183,7 +186,7 @@ This will re-parse the JSON and apply formatting to the text, making it super ea
|
|||||||
|
|
||||||
### JSONPath or jq?
|
### JSONPath or jq?
|
||||||
|
|
||||||
For more complex parsing, filtering, and modifying of JSON data, jq is recommended due to the built-in operators and functions. Refer to the [documentation](https://stedolan.github.io/jq/manual/) for more specifc information on jq.
|
For more complex parsing, filtering, and modifying of JSON data, jq is recommended due to the built-in operators and functions. Refer to the [documentation](https://stedolan.github.io/jq/manual/) for more specific information on jq.
|
||||||
|
|
||||||
One big advantage of `jq` is that you can use logic in your JSON filter, such as filters to only show items that have a value greater than/less than etc.
|
One big advantage of `jq` is that you can use logic in your JSON filter, such as filters to only show items that have a value greater than/less than etc.
|
||||||
|
|
||||||
@@ -223,7 +226,7 @@ The application also supports notifying you that it can follow this information
|
|||||||
|
|
||||||
## Proxy Configuration
|
## Proxy Configuration
|
||||||
|
|
||||||
See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration , we also support using [BrightData proxy services where possible]( https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support)
|
See the wiki https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration , we also support using [Bright Data proxy services where possible]( https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support)
|
||||||
|
|
||||||
## Raspberry Pi support?
|
## Raspberry Pi support?
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ from flask_paginate import Pagination, get_page_parameter
|
|||||||
from changedetectionio import html_tools
|
from changedetectionio import html_tools
|
||||||
from changedetectionio.api import api_v1
|
from changedetectionio.api import api_v1
|
||||||
|
|
||||||
__version__ = '0.43.1'
|
__version__ = '0.45.2'
|
||||||
|
|
||||||
|
from changedetectionio.store import BASE_URL_NOT_SET_TEXT
|
||||||
|
|
||||||
datastore = None
|
datastore = None
|
||||||
|
|
||||||
@@ -355,11 +357,11 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
# Include a link to the diff page, they will have to login here to see if password protection is enabled.
|
# Include a link to the diff page, they will have to login here to see if password protection is enabled.
|
||||||
# Description is the page you watch, link takes you to the diff JS UI page
|
# Description is the page you watch, link takes you to the diff JS UI page
|
||||||
base_url = datastore.data['settings']['application']['base_url']
|
# Dict val base_url will get overriden with the env var if it is set.
|
||||||
if base_url == '':
|
ext_base_url = datastore.data['settings']['application'].get('active_base_url')
|
||||||
base_url = "<base-url-env-var-not-set>"
|
|
||||||
|
|
||||||
diff_link = {'href': "{}{}".format(base_url, url_for('diff_history_page', uuid=watch['uuid']))}
|
# Because we are called via whatever web server, flask should figure out the right path (
|
||||||
|
diff_link = {'href': url_for('diff_history_page', uuid=watch['uuid'], _external=True)}
|
||||||
|
|
||||||
fe.link(link=diff_link)
|
fe.link(link=diff_link)
|
||||||
|
|
||||||
@@ -712,7 +714,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
output = render_template("edit.html",
|
output = render_template("edit.html",
|
||||||
available_processors=processors.available_processors(),
|
available_processors=processors.available_processors(),
|
||||||
browser_steps_config=browser_step_ui_config,
|
browser_steps_config=browser_step_ui_config,
|
||||||
current_base_url=datastore.data['settings']['application']['base_url'],
|
|
||||||
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
||||||
form=form,
|
form=form,
|
||||||
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
has_default_notification_urls=True if len(datastore.data['settings']['application']['notification_urls']) else False,
|
||||||
@@ -802,7 +803,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
output = render_template("settings.html",
|
output = render_template("settings.html",
|
||||||
form=form,
|
form=form,
|
||||||
current_base_url = datastore.data['settings']['application']['base_url'],
|
|
||||||
hide_remove_pass=os.getenv("SALTED_PASS", False),
|
hide_remove_pass=os.getenv("SALTED_PASS", False),
|
||||||
api_key=datastore.data['settings']['application'].get('api_access_token'),
|
api_key=datastore.data['settings']['application'].get('api_access_token'),
|
||||||
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
emailprefix=os.getenv('NOTIFICATION_MAIL_BUTTON_PREFIX', False),
|
||||||
@@ -1268,10 +1268,10 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
|
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid, 'skip_when_checksum_same': False}))
|
||||||
i = 1
|
i = 1
|
||||||
|
|
||||||
elif tag != None:
|
elif tag:
|
||||||
# Items that have this current tag
|
# Items that have this current tag
|
||||||
for watch_uuid, watch in datastore.data['watching'].items():
|
for watch_uuid, watch in datastore.data['watching'].items():
|
||||||
if (tag != None and tag in watch.get('tags', {})):
|
if tag in watch.get('tags', {}):
|
||||||
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
|
if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']:
|
||||||
update_q.put(
|
update_q.put(
|
||||||
queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False})
|
queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': watch_uuid, 'skip_when_checksum_same': False})
|
||||||
@@ -1430,6 +1430,27 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# paste in etc
|
# paste in etc
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@app.route("/highlight_submit_ignore_url", methods=['POST'])
|
||||||
|
def highlight_submit_ignore_url():
|
||||||
|
import re
|
||||||
|
mode = request.form.get('mode')
|
||||||
|
selection = request.form.get('selection')
|
||||||
|
|
||||||
|
uuid = request.args.get('uuid','')
|
||||||
|
if datastore.data["watching"].get(uuid):
|
||||||
|
if mode == 'exact':
|
||||||
|
for l in selection.splitlines():
|
||||||
|
datastore.data["watching"][uuid]['ignore_text'].append(l.strip())
|
||||||
|
elif mode == 'digit-regex':
|
||||||
|
for l in selection.splitlines():
|
||||||
|
# Replace any series of numbers with a regex
|
||||||
|
s = re.escape(l.strip())
|
||||||
|
s = re.sub(r'[0-9]+', r'\\d+', s)
|
||||||
|
datastore.data["watching"][uuid]['ignore_text'].append('/' + s + '/')
|
||||||
|
|
||||||
|
return f"<a href={url_for('preview_page', uuid=uuid)}>Click to preview</a>"
|
||||||
|
|
||||||
|
|
||||||
import changedetectionio.blueprint.browser_steps as browser_steps
|
import changedetectionio.blueprint.browser_steps as browser_steps
|
||||||
app.register_blueprint(browser_steps.construct_blueprint(datastore), url_prefix='/browser-steps')
|
app.register_blueprint(browser_steps.construct_blueprint(datastore), url_prefix='/browser-steps')
|
||||||
|
|
||||||
@@ -1439,6 +1460,10 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
import changedetectionio.blueprint.tags as tags
|
import changedetectionio.blueprint.tags as tags
|
||||||
app.register_blueprint(tags.construct_blueprint(datastore), url_prefix='/tags')
|
app.register_blueprint(tags.construct_blueprint(datastore), url_prefix='/tags')
|
||||||
|
|
||||||
|
import changedetectionio.blueprint.check_proxies as check_proxies
|
||||||
|
app.register_blueprint(check_proxies.construct_blueprint(datastore=datastore), url_prefix='/check_proxy')
|
||||||
|
|
||||||
|
|
||||||
# @todo handle ctrl break
|
# @todo handle ctrl break
|
||||||
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
|
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
|
||||||
threading.Thread(target=notification_runner).start()
|
threading.Thread(target=notification_runner).start()
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import os
|
||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from flask_expects_json import expects_json
|
from flask_expects_json import expects_json
|
||||||
from changedetectionio import queuedWatchMetaData
|
from changedetectionio import queuedWatchMetaData
|
||||||
from flask_restful import abort, Resource
|
from flask_restful import abort, Resource
|
||||||
@@ -33,7 +36,7 @@ class Watch(Resource):
|
|||||||
@auth.check_token
|
@auth.check_token
|
||||||
def get(self, uuid):
|
def get(self, uuid):
|
||||||
"""
|
"""
|
||||||
@api {get} /api/v1/watch/:uuid Get a single watch data
|
@api {get} /api/v1/watch/:uuid Single watch - get data, recheck, pause, mute.
|
||||||
@apiDescription Retrieve watch information and set muted/paused status
|
@apiDescription Retrieve watch information and set muted/paused status
|
||||||
@apiExample {curl} Example usage:
|
@apiExample {curl} Example usage:
|
||||||
curl http://localhost:4000/api/v1/watch/cc0cfffa-f449-477b-83ea-0caafd1dc091 -H"x-api-key:813031b16330fe25e3780cf0325daa45"
|
curl http://localhost:4000/api/v1/watch/cc0cfffa-f449-477b-83ea-0caafd1dc091 -H"x-api-key:813031b16330fe25e3780cf0325daa45"
|
||||||
@@ -209,7 +212,9 @@ class CreateWatch(Resource):
|
|||||||
json_data = request.get_json()
|
json_data = request.get_json()
|
||||||
url = json_data['url'].strip()
|
url = json_data['url'].strip()
|
||||||
|
|
||||||
if not validators.url(json_data['url'].strip()):
|
# If hosts that only contain alphanumerics are allowed ("localhost" for example)
|
||||||
|
allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
|
||||||
|
if not validators.url(url, simple_host=allow_simplehost):
|
||||||
return "Invalid or unsupported URL", 400
|
return "Invalid or unsupported URL", 400
|
||||||
|
|
||||||
if json_data.get('proxy'):
|
if json_data.get('proxy'):
|
||||||
|
|||||||
116
changedetectionio/blueprint/check_proxies/__init__.py
Normal file
116
changedetectionio/blueprint/check_proxies/__init__.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import Blueprint
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from changedetectionio.processors import text_json_diff
|
||||||
|
from changedetectionio.store import ChangeDetectionStore
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_CHECKING = 0
|
||||||
|
STATUS_FAILED = 1
|
||||||
|
STATUS_OK = 2
|
||||||
|
THREADPOOL_MAX_WORKERS = 3
|
||||||
|
_DEFAULT_POOL = ThreadPoolExecutor(max_workers=THREADPOOL_MAX_WORKERS)
|
||||||
|
|
||||||
|
|
||||||
|
# Maybe use fetch-time if its >5 to show some expected load time?
|
||||||
|
def threadpool(f, executor=None):
|
||||||
|
@wraps(f)
|
||||||
|
def wrap(*args, **kwargs):
|
||||||
|
return (executor or _DEFAULT_POOL).submit(f, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(datastore: ChangeDetectionStore):
|
||||||
|
check_proxies_blueprint = Blueprint('check_proxies', __name__)
|
||||||
|
checks_in_progress = {}
|
||||||
|
|
||||||
|
@threadpool
|
||||||
|
def long_task(uuid, preferred_proxy):
|
||||||
|
import time
|
||||||
|
from changedetectionio import content_fetcher
|
||||||
|
|
||||||
|
status = {'status': '', 'length': 0, 'text': ''}
|
||||||
|
from jinja2 import Environment, BaseLoader
|
||||||
|
|
||||||
|
contents = ''
|
||||||
|
now = time.time()
|
||||||
|
try:
|
||||||
|
update_handler = text_json_diff.perform_site_check(datastore=datastore)
|
||||||
|
changed_detected, update_obj, contents = update_handler.run(uuid, preferred_proxy=preferred_proxy, skip_when_checksum_same=False)
|
||||||
|
# title, size is len contents not len xfer
|
||||||
|
except content_fetcher.Non200ErrorCodeReceived as e:
|
||||||
|
if e.status_code == 404:
|
||||||
|
status.update({'status': 'OK', 'length': len(contents), 'text': f"OK but 404 (page not found)"})
|
||||||
|
elif e.status_code == 403 or e.status_code == 401:
|
||||||
|
status.update({'status': 'ERROR', 'length': len(contents), 'text': f"{e.status_code} - Access denied"})
|
||||||
|
else:
|
||||||
|
status.update({'status': 'ERROR', 'length': len(contents), 'text': f"Status code: {e.status_code}"})
|
||||||
|
except text_json_diff.FilterNotFoundInResponse:
|
||||||
|
status.update({'status': 'OK', 'length': len(contents), 'text': f"OK but CSS/xPath filter not found (page changed layout?)"})
|
||||||
|
except content_fetcher.EmptyReply as e:
|
||||||
|
if e.status_code == 403 or e.status_code == 401:
|
||||||
|
status.update({'status': 'ERROR OTHER', 'length': len(contents), 'text': f"Got empty reply with code {e.status_code} - Access denied"})
|
||||||
|
else:
|
||||||
|
status.update({'status': 'ERROR OTHER', 'length': len(contents) if contents else 0, 'text': f"Empty reply with code {e.status_code}, needs chrome?"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
status.update({'status': 'ERROR OTHER', 'length': len(contents) if contents else 0, 'text': 'Error: '+str(e)})
|
||||||
|
else:
|
||||||
|
status.update({'status': 'OK', 'length': len(contents), 'text': ''})
|
||||||
|
|
||||||
|
if status.get('text'):
|
||||||
|
status['text'] = Environment(loader=BaseLoader()).from_string('{{text|e}}').render({'text': status['text']})
|
||||||
|
|
||||||
|
status['time'] = "{:.2f}s".format(time.time() - now)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def _recalc_check_status(uuid):
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for k, v in checks_in_progress.get(uuid, {}).items():
|
||||||
|
try:
|
||||||
|
r_1 = v.result(timeout=0.05)
|
||||||
|
except Exception as e:
|
||||||
|
# If timeout error?
|
||||||
|
results[k] = {'status': 'RUNNING'}
|
||||||
|
|
||||||
|
else:
|
||||||
|
results[k] = r_1
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@check_proxies_blueprint.route("/<string:uuid>/status", methods=['GET'])
|
||||||
|
def get_recheck_status(uuid):
|
||||||
|
results = _recalc_check_status(uuid=uuid)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@check_proxies_blueprint.route("/<string:uuid>/start", methods=['GET'])
|
||||||
|
def start_check(uuid):
|
||||||
|
|
||||||
|
if not datastore.proxy_list:
|
||||||
|
return
|
||||||
|
|
||||||
|
if checks_in_progress.get(uuid):
|
||||||
|
state = _recalc_check_status(uuid=uuid)
|
||||||
|
for proxy_key, v in state.items():
|
||||||
|
if v.get('status') == 'RUNNING':
|
||||||
|
return state
|
||||||
|
else:
|
||||||
|
checks_in_progress[uuid] = {}
|
||||||
|
|
||||||
|
for k, v in datastore.proxy_list.items():
|
||||||
|
if not checks_in_progress[uuid].get(k):
|
||||||
|
checks_in_progress[uuid][k] = long_task(uuid=uuid, preferred_proxy=k)
|
||||||
|
|
||||||
|
results = _recalc_check_status(uuid=uuid)
|
||||||
|
return results
|
||||||
|
|
||||||
|
return check_proxies_blueprint
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
{% from '_helpers.jinja' import render_field, render_checkbox_field, render_button %}
|
||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.jinja' import render_common_settings_form %}
|
||||||
|
<script>
|
||||||
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test')}}";
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ class Fetcher():
|
|||||||
dest = os.path.join(self.browser_steps_screenshot_path, 'step_*.jpeg')
|
dest = os.path.join(self.browser_steps_screenshot_path, 'step_*.jpeg')
|
||||||
files = glob.glob(dest)
|
files = glob.glob(dest)
|
||||||
for f in files:
|
for f in files:
|
||||||
os.unlink(f)
|
if os.path.isfile(f):
|
||||||
|
os.unlink(f)
|
||||||
|
|
||||||
|
|
||||||
# Maybe for the future, each fetcher provides its own diff output, could be used for text, image
|
# Maybe for the future, each fetcher provides its own diff output, could be used for text, image
|
||||||
|
|||||||
@@ -35,15 +35,19 @@ def customSequenceMatcher(before, after, include_equal=False, include_removed=Tr
|
|||||||
|
|
||||||
# only_differences - only return info about the differences, no context
|
# only_differences - only return info about the differences, no context
|
||||||
# line_feed_sep could be "<br>" or "<li>" or "\n" etc
|
# line_feed_sep could be "<br>" or "<li>" or "\n" etc
|
||||||
def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True):
|
def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True, patch_format=False):
|
||||||
|
|
||||||
newest_version_file_contents = [line.rstrip() for line in newest_version_file_contents.splitlines()]
|
newest_version_file_contents = [line.rstrip() for line in newest_version_file_contents.splitlines()]
|
||||||
|
|
||||||
if previous_version_file_contents:
|
if previous_version_file_contents:
|
||||||
previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()]
|
previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()]
|
||||||
else:
|
else:
|
||||||
previous_version_file_contents = ""
|
previous_version_file_contents = ""
|
||||||
|
|
||||||
|
if patch_format:
|
||||||
|
patch = difflib.unified_diff(previous_version_file_contents, newest_version_file_contents)
|
||||||
|
return line_feed_sep.join(patch)
|
||||||
|
|
||||||
rendered_diff = customSequenceMatcher(before=previous_version_file_contents,
|
rendered_diff = customSequenceMatcher(before=previous_version_file_contents,
|
||||||
after=newest_version_file_contents,
|
after=newest_version_file_contents,
|
||||||
include_equal=include_equal,
|
include_equal=include_equal,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from wtforms import (
|
from wtforms import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
@@ -257,9 +258,10 @@ class validateURL(object):
|
|||||||
|
|
||||||
def __call__(self, form, field):
|
def __call__(self, form, field):
|
||||||
import validators
|
import validators
|
||||||
|
# If hosts that only contain alphanumerics are allowed ("localhost" for example)
|
||||||
|
allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
|
||||||
try:
|
try:
|
||||||
validators.url(field.data.strip())
|
validators.url(field.data.strip(), simple_host=allow_simplehost)
|
||||||
except validators.ValidationFailure:
|
except validators.ValidationFailure:
|
||||||
message = field.gettext('\'%s\' is not a valid URL.' % (field.data.strip()))
|
message = field.gettext('\'%s\' is not a valid URL.' % (field.data.strip()))
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
@@ -500,7 +502,10 @@ class globalSettingsRequestForm(Form):
|
|||||||
class globalSettingsApplicationForm(commonSettingsForm):
|
class globalSettingsApplicationForm(commonSettingsForm):
|
||||||
|
|
||||||
api_access_token_enabled = BooleanField('API access token security check enabled', default=True, validators=[validators.Optional()])
|
api_access_token_enabled = BooleanField('API access token security check enabled', default=True, validators=[validators.Optional()])
|
||||||
base_url = StringField('Base URL', validators=[validators.Optional()])
|
base_url = StringField('Notification base URL override',
|
||||||
|
validators=[validators.Optional()],
|
||||||
|
render_kw={"placeholder": os.getenv('BASE_URL', 'Not set')}
|
||||||
|
)
|
||||||
empty_pages_are_a_change = BooleanField('Treat empty pages as a change?', default=False)
|
empty_pages_are_a_change = BooleanField('Treat empty pages as a change?', default=False)
|
||||||
fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
|
fetch_backend = RadioField('Fetch Method', default="html_requests", choices=content_fetcher.available_fetchers(), validators=[ValidateContentFetcherIsReady()])
|
||||||
global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()])
|
global_ignore_text = StringListField('Ignore Text', [ValidateListRegex()])
|
||||||
|
|||||||
@@ -191,42 +191,50 @@ def extract_json_as_string(content, json_filter, ensure_is_ldjson_info_type=None
|
|||||||
#
|
#
|
||||||
# wordlist - list of regex's (str) or words (str)
|
# wordlist - list of regex's (str) or words (str)
|
||||||
def strip_ignore_text(content, wordlist, mode="content"):
|
def strip_ignore_text(content, wordlist, mode="content"):
|
||||||
ignore = []
|
|
||||||
ignore_regex = []
|
|
||||||
|
|
||||||
# @todo check this runs case insensitive
|
|
||||||
for k in wordlist:
|
|
||||||
|
|
||||||
# Is it a regex?
|
|
||||||
if k[0] == '/':
|
|
||||||
ignore_regex.append(k.strip(" /"))
|
|
||||||
else:
|
|
||||||
ignore.append(k)
|
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
output = []
|
output = []
|
||||||
|
ignore_text = []
|
||||||
|
ignore_regex = []
|
||||||
|
|
||||||
ignored_line_numbers = []
|
ignored_line_numbers = []
|
||||||
|
|
||||||
|
for k in wordlist:
|
||||||
|
# Is it a regex?
|
||||||
|
x = re.search('^\/(.*)\/(.*)', k.strip())
|
||||||
|
if x:
|
||||||
|
# Starts with / but doesn't look like a regex
|
||||||
|
p = x.group(1)
|
||||||
|
try:
|
||||||
|
# @Todo python regex options can go before the regex str, but not really many of the options apply on a per-line basis
|
||||||
|
ignore_regex.append(re.compile(rf"{p}", re.IGNORECASE))
|
||||||
|
except Exception as e:
|
||||||
|
# Badly formed regex, treat as text
|
||||||
|
ignore_text.append(k.strip())
|
||||||
|
else:
|
||||||
|
# Had a / but doesn't work as regex
|
||||||
|
ignore_text.append(k.strip())
|
||||||
|
|
||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
i += 1
|
i += 1
|
||||||
# Always ignore blank lines in this mode. (when this function gets called)
|
# Always ignore blank lines in this mode. (when this function gets called)
|
||||||
|
got_match = False
|
||||||
if len(line.strip()):
|
if len(line.strip()):
|
||||||
regex_matches = False
|
for l in ignore_text:
|
||||||
|
if l.lower() in line.lower():
|
||||||
|
got_match = True
|
||||||
|
|
||||||
# if any of these match, skip
|
if not got_match:
|
||||||
for regex in ignore_regex:
|
for r in ignore_regex:
|
||||||
try:
|
if r.search(line):
|
||||||
if re.search(regex, line, re.IGNORECASE):
|
got_match = True
|
||||||
regex_matches = True
|
|
||||||
except Exception as e:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not regex_matches and not any(skip_text.lower() in line.lower() for skip_text in ignore):
|
if not got_match:
|
||||||
|
# Not ignored
|
||||||
output.append(line.encode('utf8'))
|
output.append(line.encode('utf8'))
|
||||||
else:
|
else:
|
||||||
ignored_line_numbers.append(i)
|
ignored_line_numbers.append(i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Used for finding out what to highlight
|
# Used for finding out what to highlight
|
||||||
if mode == "line numbers":
|
if mode == "line numbers":
|
||||||
return ignored_line_numbers
|
return ignored_line_numbers
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ valid_tokens = {
|
|||||||
'diff': '',
|
'diff': '',
|
||||||
'diff_added': '',
|
'diff_added': '',
|
||||||
'diff_full': '',
|
'diff_full': '',
|
||||||
|
'diff_patch': '',
|
||||||
'diff_removed': '',
|
'diff_removed': '',
|
||||||
'diff_url': '',
|
'diff_url': '',
|
||||||
'preview_url': '',
|
'preview_url': '',
|
||||||
@@ -98,7 +99,7 @@ def process_notification(n_object, datastore):
|
|||||||
# Initially text or whatever
|
# Initially text or whatever
|
||||||
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
|
n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
|
||||||
|
|
||||||
|
|
||||||
# https://github.com/caronc/apprise/wiki/Development_LogCapture
|
# https://github.com/caronc/apprise/wiki/Development_LogCapture
|
||||||
# Anything higher than or equal to WARNING (which covers things like Connection errors)
|
# Anything higher than or equal to WARNING (which covers things like Connection errors)
|
||||||
# raise it as an exception
|
# raise it as an exception
|
||||||
@@ -177,7 +178,7 @@ def process_notification(n_object, datastore):
|
|||||||
log_value = logs.getvalue()
|
log_value = logs.getvalue()
|
||||||
if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
|
if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
|
||||||
raise Exception(log_value)
|
raise Exception(log_value)
|
||||||
|
|
||||||
sent_objs.append({'title': n_title,
|
sent_objs.append({'title': n_title,
|
||||||
'body': n_body,
|
'body': n_body,
|
||||||
'url' : url,
|
'url' : url,
|
||||||
@@ -207,15 +208,11 @@ def create_notification_parameters(n_object, datastore):
|
|||||||
watch_tag = ''
|
watch_tag = ''
|
||||||
|
|
||||||
# Create URLs to customise the notification with
|
# Create URLs to customise the notification with
|
||||||
base_url = datastore.data['settings']['application']['base_url']
|
# active_base_url - set in store.py data property
|
||||||
|
base_url = datastore.data['settings']['application'].get('active_base_url')
|
||||||
|
|
||||||
watch_url = n_object['watch_url']
|
watch_url = n_object['watch_url']
|
||||||
|
|
||||||
# Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services
|
|
||||||
# like 'Join', so it's always best to atleast set something obvious so that they are not broken.
|
|
||||||
if base_url == '':
|
|
||||||
base_url = "<base-url-env-var-not-set>"
|
|
||||||
|
|
||||||
diff_url = "{}/diff/{}".format(base_url, uuid)
|
diff_url = "{}/diff/{}".format(base_url, uuid)
|
||||||
preview_url = "{}/preview/{}".format(base_url, uuid)
|
preview_url = "{}/preview/{}".format(base_url, uuid)
|
||||||
|
|
||||||
@@ -225,11 +222,12 @@ def create_notification_parameters(n_object, datastore):
|
|||||||
# Valid_tokens also used as a field validator
|
# Valid_tokens also used as a field validator
|
||||||
tokens.update(
|
tokens.update(
|
||||||
{
|
{
|
||||||
'base_url': base_url if base_url is not None else '',
|
'base_url': base_url,
|
||||||
'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '',
|
'current_snapshot': n_object['current_snapshot'] if 'current_snapshot' in n_object else '',
|
||||||
'diff': n_object.get('diff', ''), # Null default in the case we use a test
|
'diff': n_object.get('diff', ''), # Null default in the case we use a test
|
||||||
'diff_added': n_object.get('diff_added', ''), # Null default in the case we use a test
|
'diff_added': n_object.get('diff_added', ''), # Null default in the case we use a test
|
||||||
'diff_full': n_object.get('diff_full', ''), # Null default in the case we use a test
|
'diff_full': n_object.get('diff_full', ''), # Null default in the case we use a test
|
||||||
|
'diff_patch': n_object.get('diff_patch', ''), # Null default in the case we use a test
|
||||||
'diff_removed': n_object.get('diff_removed', ''), # Null default in the case we use a test
|
'diff_removed': n_object.get('diff_removed', ''), # Null default in the case we use a test
|
||||||
'diff_url': diff_url,
|
'diff_url': diff_url,
|
||||||
'preview_url': preview_url,
|
'preview_url': preview_url,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class difference_detection_processor():
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def run(self, uuid, skip_when_checksum_same=True):
|
def run(self, uuid, skip_when_checksum_same=True, preferred_proxy=None):
|
||||||
update_obj = {'last_notification_error': False, 'last_error': False}
|
update_obj = {'last_notification_error': False, 'last_error': False}
|
||||||
some_data = 'xxxxx'
|
some_data = 'xxxxx'
|
||||||
update_obj["previous_md5"] = hashlib.md5(some_data.encode('utf-8')).hexdigest()
|
update_obj["previous_md5"] = hashlib.md5(some_data.encode('utf-8')).hexdigest()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class perform_site_check(difference_detection_processor):
|
|||||||
|
|
||||||
return regex
|
return regex
|
||||||
|
|
||||||
def run(self, uuid, skip_when_checksum_same=True):
|
def run(self, uuid, skip_when_checksum_same=True, preferred_proxy=None):
|
||||||
changed_detected = False
|
changed_detected = False
|
||||||
screenshot = False # as bytes
|
screenshot = False # as bytes
|
||||||
stripped_text_from_html = ""
|
stripped_text_from_html = ""
|
||||||
@@ -105,7 +105,11 @@ class perform_site_check(difference_detection_processor):
|
|||||||
# If the klass doesnt exist, just use a default
|
# If the klass doesnt exist, just use a default
|
||||||
klass = getattr(content_fetcher, "html_requests")
|
klass = getattr(content_fetcher, "html_requests")
|
||||||
|
|
||||||
proxy_id = self.datastore.get_preferred_proxy_for_watch(uuid=uuid)
|
if preferred_proxy:
|
||||||
|
proxy_id = preferred_proxy
|
||||||
|
else:
|
||||||
|
proxy_id = self.datastore.get_preferred_proxy_for_watch(uuid=uuid)
|
||||||
|
|
||||||
proxy_url = None
|
proxy_url = None
|
||||||
if proxy_id:
|
if proxy_id:
|
||||||
proxy_url = self.datastore.proxy_list.get(proxy_id).get('url')
|
proxy_url = self.datastore.proxy_list.get(proxy_id).get('url')
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# exit when any command fails
|
# exit when any command fails
|
||||||
set -e
|
set -e
|
||||||
|
# enable debug
|
||||||
|
set -x
|
||||||
|
|
||||||
# Test proxy list handling, starting two squids on different ports
|
# Test proxy list handling, starting two squids on different ports
|
||||||
# Each squid adds a different header to the response, which is the main thing we test for.
|
# Each squid adds a different header to the response, which is the main thing we test for.
|
||||||
@@ -19,7 +21,6 @@ docker run --network changedet-network -d \
|
|||||||
|
|
||||||
|
|
||||||
## 2nd test actually choose the preferred proxy from proxies.json
|
## 2nd test actually choose the preferred proxy from proxies.json
|
||||||
|
|
||||||
docker run --network changedet-network \
|
docker run --network changedet-network \
|
||||||
-v `pwd`/tests/proxy_list/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \
|
-v `pwd`/tests/proxy_list/proxies.json-example:/app/changedetectionio/test-datastore/proxies.json \
|
||||||
test-changedetectionio \
|
test-changedetectionio \
|
||||||
@@ -44,7 +45,6 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
# Test the UI configurable proxies
|
# Test the UI configurable proxies
|
||||||
|
|
||||||
docker run --network changedet-network \
|
docker run --network changedet-network \
|
||||||
test-changedetectionio \
|
test-changedetectionio \
|
||||||
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py'
|
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py'
|
||||||
@@ -58,4 +58,25 @@ then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Test "no-proxy" option
|
||||||
|
docker run --network changedet-network \
|
||||||
|
test-changedetectionio \
|
||||||
|
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_noproxy.py'
|
||||||
|
|
||||||
|
# We need to handle grep returning 1
|
||||||
|
set +e
|
||||||
|
# Check request was never seen in any container
|
||||||
|
for c in $(echo "squid-one squid-two squid-custom"); do
|
||||||
|
echo Checking $c
|
||||||
|
docker logs $c &> $c.txt
|
||||||
|
grep noproxy $c.txt
|
||||||
|
if [ $? -ne 1 ]
|
||||||
|
then
|
||||||
|
echo "Saw request for noproxy in $c container"
|
||||||
|
cat $c.txt
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
docker kill squid-one squid-two squid-custom
|
docker kill squid-one squid-two squid-custom
|
||||||
|
|||||||
58
changedetectionio/static/images/brightdata.svg
Normal file
58
changedetectionio/static/images/brightdata.svg
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
height="59.553207"
|
||||||
|
viewBox="-0.36 95.21 25.082135 59.553208"
|
||||||
|
width="249.99138"
|
||||||
|
version="1.1"
|
||||||
|
id="svg12"
|
||||||
|
sodipodi:docname="brightdata.svg"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs16" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview14"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="0.9464"
|
||||||
|
inkscape:cx="22.189349"
|
||||||
|
inkscape:cy="-90.870668"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1051"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg12" />
|
||||||
|
<path
|
||||||
|
d="m -34.416031,129.28 c -3.97,-2.43 -5.1,-6.09 -4.32,-10.35 0.81,-4.4 3.95,-6.75 8.04,-7.75 4.23,-1.04 8.44,-0.86 12.3,1.5 0.63,0.39 0.93,0.03 1.31,-0.29 1.5,-1.26 3.27,-1.72 5.189999,-1.83 0.79,-0.05 1.04,0.24 1.01,1.01 -0.05,1.31 -0.04,2.63 0,3.95 0.02,0.65 -0.19,0.93 -0.87,0.89 -0.889999,-0.04 -1.789999,0.03 -2.669999,-0.02 -0.82,-0.04 -1.08,0.1 -0.88,1.04 0.83,3.9 -0.06,7.37 -3.1,10.06 -2.76,2.44 -6.13,3.15 -9.72,3.04 -0.51,-0.02 -1.03,-0.02 -1.52,-0.13 -1.22,-0.25 -1.96,0.14 -2.19,1.41 -0.28,1.54 0.16,2.62 1.37,3.07 0.84,0.31 1.74,0.35 2.63,0.39 2.97,0.13 5.95,-0.18 8.91,0.21 2.93,0.39 5.69,1.16 6.85,4.25 1.269999,3.38 0.809999,6.62 -1.48,9.47 -2.73,3.39 -6.52,4.78 -10.66,5.33 -3.53,0.48 -7.04,0.27 -10.39,-1.11 -3.89,-1.6 -5.75,-4.95 -4.84,-8.72 0.51,-2.11 1.85,-3.58 3.69,-4.65 0.38,-0.22 0.93,-0.32 0.28,-0.96 -2.91,-2.83 -2.85,-6.16 0.1,-8.95 0.28,-0.26 0.6,-0.53 0.96,-0.86 z m 8.07,21.5 c 0.95,0.04 1.87,-0.13 2.78,-0.33 1.89,-0.42 3.51,-1.3 4.49,-3.06 1.82,-3.25 0.24,-6.2 -3.37,-6.58 -2.88,-0.3 -5.76,0.24 -8.63,-0.13 -0.53,-0.07 -0.75,0.34 -0.95,0.71 -1.16,2.24 -1.08,4.53 0,6.73 1.15,2.34 3.46,2.48 5.68,2.66 z m -5,-30.61 c -0.03,1.67 0.08,3.19 0.74,4.61 0.76,1.62 2.17,2.42 4.03,2.31 1.62,-0.1 2.9,-1.12 3.36,-2.84 0.66,-2.46 0.69,-4.95 0.01,-7.42 -0.49,-1.76 -1.7,-2.64 -3.56,-2.7 -2.08,-0.07 -3.37,0.7 -4.04,2.42 -0.47,1.21 -0.6,2.47 -0.54,3.62 z m 32.9399993,6.56 c 0,2.59 0.05,5.18 -0.02,7.77 -0.03,1.03 0.31,1.46 1.32,1.52 0.65,0.04 1.61,-0.09 1.82,0.57 0.26,0.81 0.11,1.76 0.06,2.65 -0.03,0.48 -0.81,0.39 -0.81,0.39 l -11.47,0.01 c 0,0 -0.95,-0.21 -0.88,-0.88 0.03,-0.29 0.04,-0.6 0,-0.89 -0.19,-1.24 0.21,-1.92 1.58,-1.9 0.99,0.01 1.28,-0.52 1.28,-1.53 -0.05,-8.75 -0.05,-17.49 0,-26.24 0.01,-1.15 -0.36,-1.62 -1.44,-1.67 -0.17,-0.01 -0.34,-0.04 -0.5,-0.07 -1.43,-0.22 -2.12,-1.57 -1.53,-2.91 0.15,-0.35 0.43,-0.36 0.72,-0.4 2.94,-0.41 5.88,-0.81 8.82000002,-1.23 0.81999998,-0.12 0.99999998,0.27 0.98999998,1.01 -0.02,3.35 0,6.71 0.02,10.06 0,0.35 -0.23,0.84 0.18,1.03 0.38,0.17 0.69,-0.25 0.99,-0.45 2.56,-1.74 5.33,-2.73 8.4900007,-2.56 3.51005,0.19 5.65005,1.95 6.35005,5.46 0.42,2.09 0.52,4.21 0.51,6.33 -0.02,3.86 0.05,7.73 -0.04,11.59 -0.02,1.12 0.37,1.5 1.39,1.6 0.61,0.05 1.55,-0.13 1.74,0.47 0.26,0.85 0.12,1.84 0.1,2.77 -0.01,0.41 -0.69,0.37 -0.69,0.37 l -11.4700504,0.01 c 0,0 -0.81,-0.29 -0.8,-0.85 0.01,-0.38 0.04,-0.77 -0.01,-1.15 -0.13,-1.01 0.32,-1.52 1.31,-1.56 1.0600004,-0.05 1.3800004,-0.55 1.3500004,-1.63 -0.14,-4.84 0.16,-9.68 -0.18,-14.51 -0.26,-3.66 -2.1100004,-4.95 -5.6700007,-3.99 -0.25,0.07 -0.49,0.15 -0.73,0.22 -2.57,0.8 -2.79,1.09 -2.79,3.71 0.01,2.3 0.01,4.59 0.01,6.88 z M -109.26603,122.56 c 0,-4.75 -0.02,-9.51 0.02,-14.26 0.01,-0.92 -0.17,-1.47 -1.19,-1.45 -0.16,0 -0.33,-0.07 -0.5,-0.1 -1.56,-0.27 -2.24,-1.47 -1.69,-2.92 0.14,-0.37 0.41,-0.38 0.7,-0.42 2.98,-0.41 5.97,-0.81 8.94,-1.24 0.85,-0.12 0.88,0.33 0.88,0.96 -0.01,3.01 -0.01,6.03 0,9.04 0,0.4 -0.18,0.96 0.27,1.16 0.36,0.16 0.66,-0.3 0.96,-0.52 4.729999,-3.51 12.459999,-2.61 14.889999,4.48 1.89,5.51 1.91,11.06 -0.96,16.28 -2.37,4.31 -6.19,6.49 -11.15,6.59 -3.379999,0.07 -6.679999,-0.3 -9.909999,-1.37 -0.93,-0.31 -1.3,-0.78 -1.28,-1.83 0.05,-4.81 0.02,-9.6 0.02,-14.4 z m 7.15,3.89 c 0,2.76 0.02,5.52 -0.01,8.28 -0.01,0.76 0.18,1.29 0.91,1.64 1.899999,0.9 4.299999,0.5 5.759999,-1.01 0.97,-1 1.56,-2.21 1.96,-3.52 1.03,-3.36 0.97,-6.78 0.61,-10.22 a 9.991,9.991 0 0 0 -0.93,-3.29 c -1.47,-3.06 -4.67,-3.85 -7.439999,-1.86 -0.6,0.43 -0.88,0.93 -0.87,1.7 0.04,2.76 0.01,5.52 0.01,8.28 z"
|
||||||
|
fill="#4280f6"
|
||||||
|
id="path2" />
|
||||||
|
<path
|
||||||
|
d="m 68.644019,137.2 c -1.62,1.46 -3.41,2.56 -5.62,2.96 -4.4,0.8 -8.7,-1.39 -10.49,-5.49 -2.31,-5.31 -2.3,-10.67 -0.1,-15.98 2.31,-5.58 8.29,-8.65 14.24,-7.46 1.71,0.34 1.9,0.18 1.9,-1.55 0,-0.68 -0.05,-1.36 0.01,-2.04 0.09,-1.02 -0.25,-1.54 -1.34,-1.43 -0.64,0.06 -1.26,-0.1 -1.88,-0.21 -1.32,-0.24 -1.6,-0.62 -1.37,-1.97 0.07,-0.41 0.25,-0.57 0.65,-0.62 2.63,-0.33 5.27,-0.66 7.9,-1.02 1.04,-0.14 1.17,0.37 1.17,1.25 -0.02,10.23 -0.02,20.45 -0.01,30.68 v 1.02 c 0.02,0.99 0.35,1.6 1.52,1.47 0.52,-0.06 1.35,-0.27 1.25,0.73 -0.08,0.8 0.58,1.93 -0.94,2.18 -1.29,0.22 -2.51,0.69 -3.86,0.65 -2.04,-0.06 -2.3,-0.23 -2.76,-2.19 -0.09,-0.3 0.06,-0.67 -0.27,-0.98 z m -0.07,-12.46 c 0,-2.8 -0.04,-5.6 0.02,-8.39 0.02,-0.9 -0.28,-1.47 -1.05,-1.81 -3.18,-1.4 -7.54,-0.8 -9.3,2.87 -0.83,1.74 -1.31,3.54 -1.49,5.46 -0.28,2.93 -0.38,5.83 0.61,8.65 0.73,2.09 1.81,3.9 4.11,4.67 2.49,0.83 4.55,-0.04 6.5,-1.48 0.54,-0.4 0.62,-0.95 0.61,-1.57 -0.02,-2.8 -0.01,-5.6 -0.01,-8.4 z m 28.79,2.53 c 0,3.24 0.04,5.83 -0.02,8.41 -0.02,1 0.19,1.49 1.309998,1.41 0.55,-0.04 1.460003,-0.46 1.520003,0.73 0.05,1.02 0.1,1.89 -1.330003,2.08 -1.289998,0.17 -2.559998,0.51 -3.889998,0.48 -1.88,-0.05 -2.15,-0.26 -2.42,-2.15 -0.04,-0.27 0.14,-0.65 -0.22,-0.79 -0.34,-0.13 -0.5,0.24 -0.72,0.42 -3.61,3 -8.15,3.4 -11.64,1.08 -1.61,-1.07 -2.49,-2.63 -2.67,-4.43 -0.51,-5.13 0.77,-7.91 6.3,-10.22 2.44,-1.02 5.07,-1.27 7.68,-1.49 0.77,-0.07 1.03,-0.28 1.02,-1.05 -0.03,-1.48 -0.05,-2.94 -0.64,-4.36 -0.59,-1.42 -1.67,-1.92 -3.08,-2.03 -3.04,-0.24 -5.88,0.5 -8.63,1.71 -0.51,0.23 -1.19,0.75 -1.48,-0.13 -0.26,-0.77 -1.35,-1.61 0.05,-2.47 3.27,-2 6.7,-3.44 10.61,-3.42 1.44,0.01 2.88,0.27 4.21,0.81 2.67,1.08 3.44,3.4 3.8,5.99 0.46,3.37 0.1,6.73 0.24,9.42 z m -5.09,2.9 c 0,-1.23 -0.01,-2.46 0,-3.69 0,-0.52 -0.06,-0.98 -0.75,-0.84 -1.45,0.3 -2.93,0.28 -4.37,0.69 -3.71,1.04 -5.46,4.48 -3.97,8.03 0.51,1.22 1.48,1.98 2.79,2.16 2.01,0.28 3.86,-0.29 5.6,-1.28 0.54,-0.31 0.73,-0.76 0.72,-1.37 -0.05,-1.23 -0.02,-2.47 -0.02,-3.7 z m 43.060001,-2.89 c 0,2.72 0.01,5.43 -0.01,8.15 0,0.66 0.02,1.21 0.91,1.12 0.54,-0.06 0.99,0.12 0.86,0.75 -0.15,0.71 0.56,1.7 -0.58,2.09 -1.55,0.52 -3.16,0.59 -4.77,0.4 -0.99,-0.12 -1.12,-1.01 -1.18,-1.73 -0.08,-1.15 -0.16,-1.45 -1.24,-0.54 -3.41,2.87 -8.05,3.17 -11.43,0.88 -1.75,-1.18 -2.49,-2.91 -2.7,-4.94 -0.64,-6.24 3.16,-8.74 7.83,-10.17 2.04,-0.62 4.14,-0.8 6.24,-0.99 0.81,-0.07 1,-0.36 0.98,-1.09 -0.04,-1.31 0.04,-2.62 -0.42,-3.89 -0.57,-1.57 -1.53,-2.34 -3.18,-2.45 -3.03,-0.21 -5.88,0.46 -8.64,1.66 -0.6,0.26 -1.25,0.81 -1.68,-0.2 -0.34,-0.8 -1.08,-1.61 0.16,-2.36 4.12,-2.5 8.44,-4.16 13.36,-3.07 3.21,0.71 4.89,2.91 5.26,6.34 0.18,1.69 0.22,3.37 0.22,5.07 0.01,1.66 0.01,3.32 0.01,4.97 z m -5.09,2.54 c 0,-1.27 -0.03,-2.54 0.01,-3.81 0.02,-0.74 -0.27,-1.02 -0.98,-0.92 -1.21,0.17 -2.43,0.28 -3.62,0.55 -3.72,0.83 -5.47,3.48 -4.82,7.21 0.29,1.66 1.57,2.94 3.21,3.16 2.02,0.27 3.85,-0.34 5.57,-1.34 0.49,-0.29 0.64,-0.73 0.63,-1.29 -0.02,-1.18 0,-2.37 0,-3.56 z"
|
||||||
|
fill="#c8dbfb"
|
||||||
|
id="path4" />
|
||||||
|
<path
|
||||||
|
d="m 26.314019,125.77 c 0,-2.89 -0.05,-5.77 0.02,-8.66 0.03,-1.04 -0.33,-1.39 -1.31,-1.24 a 0.7,0.7 0 0 1 -0.25,0 c -0.57,-0.18 -1.44,0.48 -1.68,-0.58 -0.35,-1.48 -0.02,-2.3 1.21,-2.7 1.3,-0.43 2.16,-1.26 2.76,-2.46 0.78,-1.56 1.44,-3.17 1.91,-4.84 0.18,-0.63 0.47,-0.86 1.15,-0.88 3.28,-0.09 3.27,-0.11 3.32,3.17 0.01,1.06 0.09,2.12 0.09,3.18 -0.01,0.67 0.27,0.89 0.91,0.88 1.61,-0.02 3.23,0.03 4.84,-0.02 0.77,-0.02 1.01,0.23 1.03,1.01 0.08,3.27 0.1,3.27 -3.09,3.27 -0.93,0 -1.87,0.03 -2.8,-0.01 -0.67,-0.02 -0.89,0.26 -0.88,0.91 0.04,5.43 0.04,10.86 0.12,16.29 0.02,1.7 0.75,2.26 2.46,2.1 1.1,-0.1 2.19,-0.26 3.23,-0.65 0.59,-0.22 0.89,-0.09 1.14,0.53 0.93,2.29 0.92,2.37 -1.32,3.52 -2.54,1.3 -5.22,1.99 -8.1,1.79 -2.27,-0.16 -3.68,-1.27 -4.35,-3.45 -0.3,-0.98 -0.41,-1.99 -0.41,-3.01 z m -97.67005,-8.99 c 0.57,-0.84 1.11,-1.74 1.76,-2.55 1.68,-2.09 3.68,-3.62 6.54,-3.66 1.08,-0.01 1.63,0.28 1.57,1.52 -0.1,2.08 -0.05,4.16 -0.02,6.24 0.01,0.74 -0.17,0.96 -0.96,0.76 -2.36,-0.59 -4.71,-0.42 -7.03,0.28 -0.8,0.24 -1.16,0.62 -1.15,1.52 0.05,4.5 0.04,9 0,13.5 -0.01,0.89 0.29,1.16 1.15,1.2 1.23,0.06 2.44,0.32 3.67,0.39 0.75,0.05 0.91,0.38 0.89,1.04 -0.06,2.86 0.29,2.28 -2.25,2.3 -4.2,0.04 -8.41,-0.02 -12.61,0.03 -0.91,0.01 -1.39,-0.18 -1.22,-1.18 0.02,-0.12 0,-0.25 0,-0.38 0.02,-2.1 -0.24,-1.88 1.77,-2.04 1.33,-0.11 1.6,-0.67 1.58,-1.9 -0.07,-5.35 -0.04,-10.7 -0.02,-16.05 0,-0.78 -0.17,-1.2 -1,-1.46 -2.21,-0.68 -2.7,-1.69 -2.22,-3.99 0.11,-0.52 0.45,-0.56 0.82,-0.62 2.22,-0.34 4.44,-0.7 6.67,-0.99 0.99,-0.13 1.82,0.7 1.84,1.76 0.03,1.4 0.03,2.8 0.04,4.2 -0.01,0.02 0.06,0.04 0.18,0.08 z m 25.24,6.59 c 0,3.69 0.04,7.38 -0.03,11.07 -0.02,1.04 0.31,1.48 1.32,1.49 0.29,0 0.59,0.12 0.88,0.13 0.93,0.01 1.18,0.47 1.16,1.37 -0.05,2.19 0,2.19 -2.24,2.19 -3.48,0 -6.96,-0.04 -10.44,0.03 -1.09,0.02 -1.47,-0.33 -1.3,-1.36 0.02,-0.12 0.02,-0.26 0,-0.38 -0.28,-1.39 0.39,-1.96 1.7,-1.9 1.36,0.06 1.76,-0.51 1.74,-1.88 -0.09,-5.17 -0.08,-10.35 0,-15.53 0.02,-1.22 -0.32,-1.87 -1.52,-2.17 -0.57,-0.14 -1.47,-0.11 -1.57,-0.85 -0.15,-1.04 -0.05,-2.11 0.01,-3.17 0.02,-0.34 0.44,-0.35 0.73,-0.39 2.81,-0.39 5.63,-0.77 8.44,-1.18 0.92,-0.14 1.15,0.2 1.14,1.09 -0.04,3.8 -0.02,7.62 -0.02,11.44 z"
|
||||||
|
fill="#4280f6"
|
||||||
|
id="path6" />
|
||||||
|
<path
|
||||||
|
d="m 101.44402,125.64 c 0,-3.18 -0.03,-6.37 0.02,-9.55 0.02,-0.94 -0.26,-1.36 -1.22,-1.22 -0.21,0.03 -0.430003,0.04 -0.630003,0 -0.51,-0.12 -1.35,0.39 -1.44,-0.55 -0.08,-0.85 -0.429998,-1.87 0.93,-2.24 2.080003,-0.57 2.720003,-2.39 3.350003,-4.17 0.31,-0.88 0.62,-1.76 0.87,-2.66 0.18,-0.64 0.52,-0.85 1.19,-0.84 2.46,0.05 2,-0.15 2.04,2.04 0.02,1.1 0.08,2.21 -0.02,3.31 -0.11,1.16 0.46,1.52 1.46,1.53 1.78,0.01 3.57,0.04 5.35,-0.01 0.82,-0.02 1.12,0.23 1.11,1.08 -0.05,2.86 0.19,2.49 -2.42,2.51 -1.53,0.01 -3.06,0.02 -4.59,-0.01 -0.65,-0.01 -0.9,0.22 -0.9,0.89 0.02,5.52 0,11.04 0.03,16.56 0,0.67 0.14,1.34 0.25,2.01 0.17,1.04 1.17,1.62 2.59,1.42 1.29,-0.19 2.57,-0.49 3.86,-0.69 0.43,-0.07 1.05,-0.47 1.19,0.4 0.12,0.75 1.05,1.61 -0.09,2.24 -2.09,1.16 -4.28,2.07 -6.71,2.16 -1.05,0.04 -2.13,0.2 -3.16,-0.14 -1.92,-0.65 -3.03,-2.28 -3.05,-4.51 -0.02,-3.19 -0.01,-6.37 -0.01,-9.56 z"
|
||||||
|
fill="#c8dbfb"
|
||||||
|
id="path8" />
|
||||||
|
<path
|
||||||
|
d="m -50.816031,95.21 c 0.19,2.160002 1.85,3.240002 2.82,4.740002 0.25,0.379998 0.48,0.109998 0.67,-0.16 0.21,-0.31 0.6,-1.21 1.15,-1.28 -0.35,1.38 -0.04,3.149998 0.16,4.449998 0.49,3.05 -1.22,5.64 -4.07,6.18 -3.38,0.65 -6.22,-2.21 -5.6,-5.62 0.23,-1.24 1.37,-2.5 0.77,-3.699998 -0.85,-1.7 0.54,-0.52 0.79,-0.22 1.04,1.199998 1.21,0.09 1.45,-0.55 0.24,-0.63 0.31,-1.31 0.47,-1.97 0.19,-0.770002 0.55,-1.400002 1.39,-1.870002 z"
|
||||||
|
fill="#4280f6"
|
||||||
|
id="path10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
57
changedetectionio/static/images/oxylabs.svg
Normal file
57
changedetectionio/static/images/oxylabs.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.7 KiB |
@@ -1,4 +1,13 @@
|
|||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
var csrftoken = $('input[name=csrf_token]').val();
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function (xhr, settings) {
|
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrftoken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Load it when the #screenshot tab is in use, so we dont give a slow experience when waiting for the text diff to load
|
// Load it when the #screenshot tab is in use, so we dont give a slow experience when waiting for the text diff to load
|
||||||
window.addEventListener('hashchange', function (e) {
|
window.addEventListener('hashchange', function (e) {
|
||||||
toggle(location.hash);
|
toggle(location.hash);
|
||||||
@@ -15,11 +24,71 @@ $(document).ready(function () {
|
|||||||
$("#settings").hide();
|
$("#settings").hide();
|
||||||
} else if (hash_name === '#extract') {
|
} else if (hash_name === '#extract') {
|
||||||
$("#settings").hide();
|
$("#settings").hide();
|
||||||
}
|
} else {
|
||||||
|
|
||||||
|
|
||||||
else {
|
|
||||||
$("#settings").show();
|
$("#settings").show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const article = $('.highlightable-filter')[0];
|
||||||
|
|
||||||
|
// We could also add the 'touchend' event for touch devices, but since
|
||||||
|
// most iOS/Android browsers already show a dialog when you select
|
||||||
|
// text (often with a Share option) we'll skip that
|
||||||
|
article.addEventListener('mouseup', dragTextHandler, false);
|
||||||
|
article.addEventListener('mousedown', clean, false);
|
||||||
|
|
||||||
|
function clean(event) {
|
||||||
|
$("#highlightSnippet").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dragTextHandler(event) {
|
||||||
|
console.log('mouseupped');
|
||||||
|
|
||||||
|
// Check if any text was selected
|
||||||
|
if (window.getSelection().toString().length > 0) {
|
||||||
|
|
||||||
|
// Find out how much (if any) user has scrolled
|
||||||
|
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
|
||||||
|
|
||||||
|
// Get cursor position
|
||||||
|
const posX = event.clientX;
|
||||||
|
const posY = event.clientY + 20 + scrollTop;
|
||||||
|
|
||||||
|
// Append HTML to the body, create the "Tweet Selection" dialog
|
||||||
|
document.body.insertAdjacentHTML('beforeend', '<div id="highlightSnippet" style="position: absolute; top: ' + posY + 'px; left: ' + posX + 'px;"><div class="pure-form-message-inline" style="font-size: 70%">Ignore any change on any line which contains the selected text.</div><br><a data-mode="exact" href="javascript:void(0);" class="pure-button button-secondary button-xsmall">Ignore exact text</a> </div>');
|
||||||
|
|
||||||
|
if (/\d/.test(window.getSelection().toString())) {
|
||||||
|
// Offer regex replacement
|
||||||
|
document.getElementById("highlightSnippet").insertAdjacentHTML('beforeend', '<a data-mode="digit-regex" href="javascript:void(0);" class="pure-button button-secondary button-xsmall">Ignore text including number changes</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#highlightSnippet a').bind('click', function (e) {
|
||||||
|
if(!window.getSelection().toString().trim().length) {
|
||||||
|
alert('Oops no text selected!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: highlight_submit_ignore_url,
|
||||||
|
data: {'mode': $(this).data('mode'), 'selection': window.getSelection().toString()},
|
||||||
|
statusCode: {
|
||||||
|
400: function () {
|
||||||
|
// More than likely the CSRF token was lost when the server restarted
|
||||||
|
alert("There was a problem processing the request, please reload the page.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).done(function (data) {
|
||||||
|
$("#highlightSnippet").html(data)
|
||||||
|
}).fail(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
alert('There was an error communicating with the server.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,5 +32,10 @@ $(document).ready(function () {
|
|||||||
window.getSelection().removeAllRanges();
|
window.getSelection().removeAllRanges();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#notification-token-toggle").click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#notification-tokens-info').toggle();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
87
changedetectionio/static/js/recheck-proxy.js
Normal file
87
changedetectionio/static/js/recheck-proxy.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
$(function () {
|
||||||
|
/* add container before each proxy location to show status */
|
||||||
|
|
||||||
|
var option_li = $('.fetch-backend-proxy li').filter(function() {
|
||||||
|
return $("input",this)[0].value.length >0;
|
||||||
|
});
|
||||||
|
|
||||||
|
//var option_li = $('.fetch-backend-proxy li');
|
||||||
|
var isActive = false;
|
||||||
|
$(option_li).prepend('<div class="proxy-status"></div>');
|
||||||
|
$(option_li).append('<div class="proxy-timing"></div><div class="proxy-check-details"></div>');
|
||||||
|
|
||||||
|
function set_proxy_check_status(proxy_key, state) {
|
||||||
|
// select input by value name
|
||||||
|
const proxy_li = $('input[value="' + proxy_key + '" ]').parent();
|
||||||
|
if (state['status'] === 'RUNNING') {
|
||||||
|
$('.proxy-status', proxy_li).html('<span class="spinner"></span>');
|
||||||
|
}
|
||||||
|
if (state['status'] === 'OK') {
|
||||||
|
$('.proxy-status', proxy_li).html('<span style="color: green; font-weight: bold" >OK</span>');
|
||||||
|
$('.proxy-check-details', proxy_li).html(state['text']);
|
||||||
|
}
|
||||||
|
if (state['status'] === 'ERROR' || state['status'] === 'ERROR OTHER') {
|
||||||
|
$('.proxy-status', proxy_li).html('<span style="color: red; font-weight: bold" >X</span>');
|
||||||
|
$('.proxy-check-details', proxy_li).html(state['text']);
|
||||||
|
}
|
||||||
|
$('.proxy-timing', proxy_li).html(state['time']);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function pollServer() {
|
||||||
|
if (isActive) {
|
||||||
|
window.setTimeout(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: proxy_recheck_status_url,
|
||||||
|
success: function (data) {
|
||||||
|
var all_done = true;
|
||||||
|
$.each(data, function (proxy_key, state) {
|
||||||
|
set_proxy_check_status(proxy_key, state);
|
||||||
|
if (state['status'] === 'RUNNING') {
|
||||||
|
all_done = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (all_done) {
|
||||||
|
console.log("Shutting down poller, all done.")
|
||||||
|
isActive = false;
|
||||||
|
} else {
|
||||||
|
pollServer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
//ERROR HANDLING
|
||||||
|
pollServer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#check-all-proxies').click(function (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
$('body').addClass('proxy-check-active');
|
||||||
|
$('.proxy-check-details').html('');
|
||||||
|
$('.proxy-status').html('<span class="spinner"></span>').fadeIn();
|
||||||
|
$('.proxy-timing').html('');
|
||||||
|
|
||||||
|
// Request start, needs CSRF?
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: recheck_proxy_start_url,
|
||||||
|
}).done(function (data) {
|
||||||
|
$.each(data, function (proxy_key, state) {
|
||||||
|
set_proxy_check_status(proxy_key, state['status'])
|
||||||
|
});
|
||||||
|
isActive = true;
|
||||||
|
pollServer();
|
||||||
|
|
||||||
|
}).fail(function (data) {
|
||||||
|
console.log(data);
|
||||||
|
alert('There was an error communicating with the server.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@@ -42,4 +42,8 @@ $(document).ready(function () {
|
|||||||
$('#notification_urls').val('');
|
$('#notification_urls').val('');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
$("#notification-token-toggle").click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#notification-tokens-info').toggle();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -218,3 +218,10 @@ td#diff-col div {
|
|||||||
text-align: center; }
|
text-align: center; }
|
||||||
.tab-pane-inner#screenshot img {
|
.tab-pane-inner#screenshot img {
|
||||||
max-width: 99%; }
|
max-width: 99%; }
|
||||||
|
|
||||||
|
#highlightSnippet {
|
||||||
|
background: var(--color-background);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 1px 1px 4px var(--color-shadow-jump); }
|
||||||
|
|||||||
@@ -119,3 +119,11 @@ td#diff-col div {
|
|||||||
max-width: 99%;
|
max-width: 99%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#highlightSnippet {
|
||||||
|
background: var(--color-background);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--color-background);
|
||||||
|
box-shadow: 1px 1px 4px var(--color-shadow-jump);
|
||||||
|
}
|
||||||
|
|||||||
25
changedetectionio/static/styles/scss/parts/_darkmode.scss
Normal file
25
changedetectionio/static/styles/scss/parts/_darkmode.scss
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
#toggle-light-mode {
|
||||||
|
width: 3rem;
|
||||||
|
/* default */
|
||||||
|
.icon-dark {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-darkmode="true"] {
|
||||||
|
#toggle-light-mode {
|
||||||
|
.icon-light {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dark {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ ul#requests-extra_proxies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* each proxy entry is a `table` */
|
/* each proxy entry is a `table` */
|
||||||
table {
|
table {
|
||||||
tr {
|
tr {
|
||||||
@@ -15,3 +16,47 @@ ul#requests-extra_proxies {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#request {
|
||||||
|
/* Auto proxy scan/checker */
|
||||||
|
label[for=proxy] {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.proxy-check-active {
|
||||||
|
#request {
|
||||||
|
.proxy-status {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-check-details {
|
||||||
|
font-size: 80%;
|
||||||
|
color: #555;
|
||||||
|
display: block;
|
||||||
|
padding-left: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proxy-timing {
|
||||||
|
font-size: 80%;
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: var(--color-link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#recommended-proxy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
border: 1px #aaa solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
@import "parts/_pagination";
|
@import "parts/_pagination";
|
||||||
@import "parts/_spinners";
|
@import "parts/_spinners";
|
||||||
@import "parts/_variables";
|
@import "parts/_variables";
|
||||||
|
@import "parts/_darkmode";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -54,22 +55,6 @@ a.github-link {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#toggle-light-mode {
|
|
||||||
width: 3rem;
|
|
||||||
.icon-dark {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
.icon-light {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-dark {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#toggle-search {
|
#toggle-search {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
|
|||||||
@@ -95,6 +95,37 @@ ul#requests-extra_proxies {
|
|||||||
ul#requests-extra_proxies table tr {
|
ul#requests-extra_proxies table tr {
|
||||||
display: inline; }
|
display: inline; }
|
||||||
|
|
||||||
|
#request {
|
||||||
|
/* Auto proxy scan/checker */ }
|
||||||
|
#request label[for=proxy] {
|
||||||
|
display: inline-block; }
|
||||||
|
|
||||||
|
body.proxy-check-active #request .proxy-status {
|
||||||
|
width: 2em; }
|
||||||
|
|
||||||
|
body.proxy-check-active #request .proxy-check-details {
|
||||||
|
font-size: 80%;
|
||||||
|
color: #555;
|
||||||
|
display: block;
|
||||||
|
padding-left: 4em; }
|
||||||
|
|
||||||
|
body.proxy-check-active #request .proxy-timing {
|
||||||
|
font-size: 80%;
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: var(--color-link); }
|
||||||
|
|
||||||
|
#recommended-proxy {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
padding-bottom: 1em; }
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
#recommended-proxy {
|
||||||
|
grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
#recommended-proxy > div {
|
||||||
|
border: 1px #aaa solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 1em; }
|
||||||
|
|
||||||
.pagination-page-info {
|
.pagination-page-info {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -283,10 +314,6 @@ html[data-darkmode="true"] {
|
|||||||
--color-icon-github-hover: var(--color-grey-700);
|
--color-icon-github-hover: var(--color-grey-700);
|
||||||
--color-watch-table-error: var(--color-light-red);
|
--color-watch-table-error: var(--color-light-red);
|
||||||
--color-watch-table-row-text: var(--color-grey-800); }
|
--color-watch-table-row-text: var(--color-grey-800); }
|
||||||
html[data-darkmode="true"] #toggle-light-mode .icon-light {
|
|
||||||
display: none; }
|
|
||||||
html[data-darkmode="true"] #toggle-light-mode .icon-dark {
|
|
||||||
display: block; }
|
|
||||||
html[data-darkmode="true"] .icon-spread {
|
html[data-darkmode="true"] .icon-spread {
|
||||||
filter: hue-rotate(-10deg) brightness(1.5); }
|
filter: hue-rotate(-10deg) brightness(1.5); }
|
||||||
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
html[data-darkmode="true"] .watch-table .title-col a[target="_blank"]::after,
|
||||||
@@ -301,6 +328,18 @@ html[data-darkmode="true"] {
|
|||||||
html[data-darkmode="true"] .watch-table .unviewed.error {
|
html[data-darkmode="true"] .watch-table .unviewed.error {
|
||||||
color: var(--color-watch-table-error); }
|
color: var(--color-watch-table-error); }
|
||||||
|
|
||||||
|
#toggle-light-mode {
|
||||||
|
width: 3rem;
|
||||||
|
/* default */ }
|
||||||
|
#toggle-light-mode .icon-dark {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
html[data-darkmode="true"] #toggle-light-mode .icon-light {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
html[data-darkmode="true"] #toggle-light-mode .icon-dark {
|
||||||
|
display: block; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-background-page); }
|
background: var(--color-background-page); }
|
||||||
@@ -335,11 +374,6 @@ a.github-link {
|
|||||||
a.github-link:hover {
|
a.github-link:hover {
|
||||||
color: var(--color-icon-github-hover); }
|
color: var(--color-icon-github-hover); }
|
||||||
|
|
||||||
#toggle-light-mode {
|
|
||||||
width: 3rem; }
|
|
||||||
#toggle-light-mode .icon-dark {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
#toggle-search {
|
#toggle-search {
|
||||||
width: 2rem; }
|
width: 2rem; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
flash
|
flash
|
||||||
)
|
)
|
||||||
@@ -16,6 +18,9 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import uuid as uuid_builder
|
import uuid as uuid_builder
|
||||||
|
|
||||||
|
# Because the server will run as a daemon and wont know the URL for notification links when firing off a notification
|
||||||
|
BASE_URL_NOT_SET_TEXT = '("Base URL" not set - see settings - notifications)'
|
||||||
|
|
||||||
dictfilt = lambda x, y: dict([ (i,x[i]) for i in x if i in set(y) ])
|
dictfilt = lambda x, y: dict([ (i,x[i]) for i in x if i in set(y) ])
|
||||||
|
|
||||||
# Is there an existing library to ensure some data store (JSON etc) is in sync with CRUD methods?
|
# Is there an existing library to ensure some data store (JSON etc) is in sync with CRUD methods?
|
||||||
@@ -173,12 +178,21 @@ class ChangeDetectionStore:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
# Re #152, Return env base_url if not overriden, @todo also prefer the proxy pass url
|
# Re #152, Return env base_url if not overriden
|
||||||
env_base_url = os.getenv('BASE_URL','')
|
# Re #148 - Some people have just {{ base_url }} in the body or title, but this may break some notification services
|
||||||
if not self.__data['settings']['application']['base_url']:
|
# like 'Join', so it's always best to atleast set something obvious so that they are not broken.
|
||||||
self.__data['settings']['application']['base_url'] = env_base_url.strip('" ')
|
|
||||||
|
|
||||||
return self.__data
|
active_base_url = BASE_URL_NOT_SET_TEXT
|
||||||
|
if self.__data['settings']['application'].get('base_url'):
|
||||||
|
active_base_url = self.__data['settings']['application'].get('base_url')
|
||||||
|
elif os.getenv('BASE_URL'):
|
||||||
|
active_base_url = os.getenv('BASE_URL')
|
||||||
|
|
||||||
|
# I looked at various ways todo the following, but in the end just copying the dict seemed simplest/most reliable
|
||||||
|
# even given the memory tradeoff - if you know a better way.. maybe return d|self.__data.. or something
|
||||||
|
d = self.__data
|
||||||
|
d['settings']['application']['active_base_url'] = active_base_url.strip('" ')
|
||||||
|
return d
|
||||||
|
|
||||||
# Delete a single watch by UUID
|
# Delete a single watch by UUID
|
||||||
def delete(self, uuid):
|
def delete(self, uuid):
|
||||||
@@ -325,6 +339,9 @@ class ChangeDetectionStore:
|
|||||||
if k in apply_extras:
|
if k in apply_extras:
|
||||||
del apply_extras[k]
|
del apply_extras[k]
|
||||||
|
|
||||||
|
if not apply_extras.get('date_created'):
|
||||||
|
apply_extras['date_created'] = int(time.time())
|
||||||
|
|
||||||
new_watch.update(apply_extras)
|
new_watch.update(apply_extras)
|
||||||
new_watch.ensure_data_dir_exists()
|
new_watch.ensure_data_dir_exists()
|
||||||
self.__data['watching'][new_uuid] = new_watch
|
self.__data['watching'][new_uuid] = new_watch
|
||||||
@@ -468,6 +485,8 @@ class ChangeDetectionStore:
|
|||||||
k = "ui-" + str(i) + proxy.get('proxy_name')
|
k = "ui-" + str(i) + proxy.get('proxy_name')
|
||||||
proxy_list[k] = {'label': proxy.get('proxy_name'), 'url': proxy.get('proxy_url')}
|
proxy_list[k] = {'label': proxy.get('proxy_name'), 'url': proxy.get('proxy_url')}
|
||||||
|
|
||||||
|
if proxy_list and strtobool(os.getenv('ENABLE_NO_PROXY_OPTION', 'True')):
|
||||||
|
proxy_list["no-proxy"] = {'label': "No proxy", 'url': ''}
|
||||||
|
|
||||||
return proxy_list if len(proxy_list) else None
|
return proxy_list if len(proxy_list) else None
|
||||||
|
|
||||||
@@ -485,6 +504,9 @@ class ChangeDetectionStore:
|
|||||||
# If it's a valid one
|
# If it's a valid one
|
||||||
watch = self.data['watching'].get(uuid)
|
watch = self.data['watching'].get(uuid)
|
||||||
|
|
||||||
|
if strtobool(os.getenv('ENABLE_NO_PROXY_OPTION', 'True')) and watch.get('proxy') == "no-proxy":
|
||||||
|
return None
|
||||||
|
|
||||||
if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()):
|
if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()):
|
||||||
return watch.get('proxy')
|
return watch.get('proxy')
|
||||||
|
|
||||||
@@ -778,15 +800,6 @@ class ChangeDetectionStore:
|
|||||||
continue
|
continue
|
||||||
return
|
return
|
||||||
|
|
||||||
# We don't know when the date_created was in the past until now, so just add an index number for now.
|
|
||||||
def update_11(self):
|
|
||||||
i = 0
|
|
||||||
for uuid, watch in self.data['watching'].items():
|
|
||||||
if not watch.get('date_created'):
|
|
||||||
watch['date_created'] = i
|
|
||||||
i+=1
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create tag objects and their references from existing tag text
|
# Create tag objects and their references from existing tag text
|
||||||
def update_12(self):
|
def update_12(self):
|
||||||
i = 0
|
i = 0
|
||||||
@@ -800,3 +813,11 @@ class ChangeDetectionStore:
|
|||||||
|
|
||||||
self.data['watching'][uuid]['tags'] = tag_uuids
|
self.data['watching'][uuid]['tags'] = tag_uuids
|
||||||
|
|
||||||
|
# #1775 - Update 11 did not update the records correctly when adding 'date_created' values for sorting
|
||||||
|
def update_13(self):
|
||||||
|
i = 0
|
||||||
|
for uuid, watch in self.data['watching'].items():
|
||||||
|
if not watch.get('date_created'):
|
||||||
|
self.data['watching'][uuid]['date_created'] = i
|
||||||
|
i+=1
|
||||||
|
return
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
<div class="pure-form-message-inline">
|
<div class="pure-form-message-inline">
|
||||||
<ul>
|
<ul>
|
||||||
<li>Use <a target=_new href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service! <i><a target=_new href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.</li>
|
<li>Use <a target=_new href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service! <i><a target=_new href="https://github.com/dgtlmoon/changedetection.io/wiki/Notification-configuration-notes">Please read the notification services wiki here for important configuration notes</a></i>.</li>
|
||||||
<li><code>discord://</code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li>
|
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_discord">discord://</a></code> (or <code>https://discord.com/api/webhooks...</code>)) </code> only supports a maximum <strong>2,000 characters</strong> of notification text, including the title.</li>
|
||||||
<li><code>tgram://</code> bots cant send messages to other bots, so you should specify chat ID of non-bot user.</li>
|
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> bots can't send messages to other bots, so you should specify chat ID of non-bot user.</li>
|
||||||
<li><code>tgram://</code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li>
|
<li><code><a target=_new href="https://github.com/caronc/apprise/wiki/Notify_telegram">tgram://</a></code> only supports very limited HTML and can fail when extra tags are sent, <a href="https://core.telegram.org/bots/api#html-style">read more here</a> (or use plaintext/markdown format)</li>
|
||||||
<li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>)</li>
|
<li><code>gets://</code>, <code>posts://</code>, <code>puts://</code>, <code>deletes://</code> for direct API calls (or omit the "<code>s</code>" for non-SSL ie <code>get://</code>)</li>
|
||||||
<li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li>
|
<li>Accepts the <code>{{ '{{token}}' }}</code> placeholders listed below</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -35,18 +35,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
|
{{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }}
|
||||||
<span class="pure-form-message-inline">Body for all notifications</span>
|
<span class="pure-form-message-inline">Body for all notifications ‐ You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL, and tokens from below.
|
||||||
</div>
|
</span>
|
||||||
<div class="pure-control-group">
|
|
||||||
<!-- unsure -->
|
|
||||||
{{ render_field(form.notification_format , class="notification-format") }}
|
|
||||||
<span class="pure-form-message-inline">Format for all notifications</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-controls">
|
<div class="pure-controls">
|
||||||
<p class="pure-form-message-inline">
|
<div id="notification-token-toggle" class="pure-button button-tag button-xsmall">Show token/placeholders</div>
|
||||||
You can use <a target="_new" href="https://jinja.palletsprojects.com/en/3.0.x/templates/">Jinja2</a> templating in the notification title, body and URL.
|
</div>
|
||||||
</p>
|
<div class="pure-controls" style="display: none;" id="notification-tokens-info">
|
||||||
|
|
||||||
<table class="pure-table" id="token-table">
|
<table class="pure-table" id="token-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -99,6 +95,10 @@
|
|||||||
<td><code>{{ '{{diff_full}}' }}</code></td>
|
<td><code>{{ '{{diff_full}}' }}</code></td>
|
||||||
<td>The diff output - full difference output</td>
|
<td>The diff output - full difference output</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ '{{diff_patch}}' }}</code></td>
|
||||||
|
<td>The diff output - patch in unified format</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>{{ '{{current_snapshot}}' }}</code></td>
|
<td><code>{{ '{{current_snapshot}}' }}</code></td>
|
||||||
<td>The current snapshot value, useful when combined with JSON or CSS filters
|
<td>The current snapshot value, useful when combined with JSON or CSS filters
|
||||||
@@ -111,12 +111,15 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="pure-form-message-inline">
|
<div class="pure-form-message-inline">
|
||||||
<br>
|
<p>
|
||||||
URLs generated by changedetection.io (such as <code>{{ '{{diff_url}}' }}</code>) require the <code>BASE_URL</code> environment variable set.<br>
|
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. <br>
|
||||||
Your <code>BASE_URL</code> var is currently "{{settings_application['current_base_url']}}"
|
For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
||||||
<br>
|
</p>
|
||||||
Warning: Contents of <code>{{ '{{diff}}' }}</code>, <code>{{ '{{diff_removed}}' }}</code>, and <code>{{ '{{diff_added}}' }}</code> depend on how the difference algorithm perceives the change. For example, an addition or removal could be perceived as a change in some cases. <a target="_new" href="https://github.com/dgtlmoon/changedetection.io/wiki/Using-the-%7B%7Bdiff%7D%7D,-%7B%7Bdiff_added%7D%7D,-and-%7B%7Bdiff_removed%7D%7D-notification-tokens">More Here</a> <br>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
{{ render_field(form.notification_format , class="notification-format") }}
|
||||||
|
<span class="pure-form-message-inline">Format for all notifications</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{% macro render_field(field) %}
|
{% macro render_field(field) %}
|
||||||
<div {% if field.errors %} class="error" {% endif %}>{{ field(**kwargs)|safe }}
|
|
||||||
<div {% if field.errors %} class="error" {% endif %}>{{ field.label }}</div>
|
<div {% if field.errors %} class="error" {% endif %}>{{ field.label }}</div>
|
||||||
|
<div {% if field.errors %} class="error" {% endif %}>{{ field(**kwargs)|safe }}
|
||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<ul class=errors>
|
<ul class=errors>
|
||||||
{% for error in field.errors %}
|
{% for error in field.errors %}
|
||||||
@@ -25,18 +24,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_field(field) %}
|
|
||||||
<div {% if field.errors %} class="error" {% endif %}>{{ field.label }}</div>
|
|
||||||
<div {% if field.errors %} class="error" {% endif %}>{{ field(**kwargs)|safe }}
|
|
||||||
{% if field.errors %}
|
|
||||||
<ul class=errors>
|
|
||||||
{% for error in field.errors %}
|
|
||||||
<li>{{ error }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro render_simple_field(field) %}
|
{% macro render_simple_field(field) %}
|
||||||
<span class="label {% if field.errors %}error{% endif %}">{{ field.label }}</span>
|
<span class="label {% if field.errors %}error{% endif %}">{{ field.label }}</span>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed" id="nav-menu">
|
<div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed" id="nav-menu">
|
||||||
{% if has_password and not current_user.is_authenticated %}
|
{% if has_password and not current_user.is_authenticated %}
|
||||||
<a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener">
|
<a class="pure-menu-heading" href="https://changedetection.io" rel="noopener">
|
||||||
<strong>Change</strong>Detection.io</a>
|
<strong>Change</strong>Detection.io</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="pure-menu-heading" href="{{url_for('index')}}">
|
<a class="pure-menu-heading" href="{{url_for('index')}}">
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% if new_version_available and not(has_password and not current_user.is_authenticated) %}
|
{% if new_version_available and not(has_password and not current_user.is_authenticated) %}
|
||||||
<span id="new-version-text" class="pure-menu-heading">
|
<span id="new-version-text" class="pure-menu-heading">
|
||||||
<a href="https://github.com/dgtlmoon/changedetection.io">A new version is available</a>
|
<a href="https://changedetection.io">A new version is available</a>
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="pure-menu-item">
|
<li class="pure-menu-item">
|
||||||
<a class="pure-menu-link" href="https://github.com/dgtlmoon/changedetection.io">Website Change Detection and Notification.</a>
|
<a class="pure-menu-link" href="https://changedetection.io">Website Change Detection and Notification.</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
{% if last_error_screenshot %}
|
{% if last_error_screenshot %}
|
||||||
const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
|
const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
const highlight_submit_ignore_url="{{url_for('highlight_submit_ignore_url', uuid=uuid)}}";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="text">
|
<div class="tab-pane-inner" id="text">
|
||||||
<div class="tip">Pro-tip: Use <strong>show current snapshot</strong> tab to visualise what will be ignored.</div>
|
<div class="tip">Pro-tip: Use <strong>show current snapshot</strong> tab to visualise what will be ignored, highlight text to add to ignore filters</div>
|
||||||
|
|
||||||
{% if password_enabled_and_share_is_off %}
|
{% if password_enabled_and_share_is_off %}
|
||||||
<div class="tip">Pro-tip: You can enable <strong>"share access when password is enabled"</strong> from settings</div>
|
<div class="tip">Pro-tip: You can enable <strong>"share access when password is enabled"</strong> from settings</div>
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
<td id="a" style="display: none;">{{previous}}</td>
|
<td id="a" style="display: none;">{{previous}}</td>
|
||||||
<td id="b" style="display: none;">{{newest}}</td>
|
<td id="b" style="display: none;">{{newest}}</td>
|
||||||
<td id="diff-col">
|
<td id="diff-col">
|
||||||
<span id="result"></span>
|
<span id="result" class="highlightable-filter"></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -4,18 +4,19 @@
|
|||||||
{% from '_common_fields.jinja' import render_common_settings_form %}
|
{% from '_common_fields.jinja' import render_common_settings_form %}
|
||||||
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
<script>
|
<script>
|
||||||
const notification_base_url="{{url_for('ajax_callback_send_notification_test')}}";
|
|
||||||
const watch_visual_selector_data_url="{{url_for('static_content', group='visual_selector_data', filename=uuid)}}";
|
|
||||||
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
|
||||||
const playwright_enabled={% if playwright_enabled %} true {% else %} false {% endif %};
|
|
||||||
|
|
||||||
{% if emailprefix %}
|
|
||||||
const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}');
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}');
|
||||||
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}";
|
||||||
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}";
|
||||||
|
{% if emailprefix %}
|
||||||
|
const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}');
|
||||||
|
{% endif %}
|
||||||
|
const notification_base_url="{{url_for('ajax_callback_send_notification_test')}}";
|
||||||
|
const playwright_enabled={% if playwright_enabled %} true {% else %} false {% endif %};
|
||||||
|
const recheck_proxy_start_url="{{url_for('check_proxies.start_check', uuid=uuid)}}";
|
||||||
|
const proxy_recheck_status_url="{{url_for('check_proxies.get_recheck_status', uuid=uuid)}}";
|
||||||
|
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
||||||
|
const watch_visual_selector_data_url="{{url_for('static_content', group='visual_selector_data', filename=uuid)}}";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script>
|
||||||
|
|
||||||
<div class="edit-form monospaced-textarea">
|
<div class="edit-form monospaced-textarea">
|
||||||
|
|
||||||
<div class="tabs collapsable">
|
<div class="tabs collapsable">
|
||||||
@@ -111,7 +114,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if form.proxy %}
|
{% if form.proxy %}
|
||||||
<div class="pure-control-group inline-radio">
|
<div class="pure-control-group inline-radio">
|
||||||
{{ render_field(form.proxy, class="fetch-backend-proxy") }}
|
<div>{{ form.proxy.label }} <a href="" id="check-all-proxies" class="pure-button button-secondary button-xsmall" >Check/Scan all</a></div>
|
||||||
|
<div>{{ form.proxy(class="fetch-backend-proxy") }}</div>
|
||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
Choose a proxy for this watch
|
Choose a proxy for this watch
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
{% if last_error_screenshot %}
|
{% if last_error_screenshot %}
|
||||||
const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
|
const error_screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
const highlight_submit_ignore_url="{{url_for('highlight_submit_ignore_url', uuid=uuid)}}";
|
||||||
</script>
|
</script>
|
||||||
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<form><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"></form>
|
||||||
<div id="diff-ui">
|
<div id="diff-ui">
|
||||||
<div class="tab-pane-inner" id="error-text">
|
<div class="tab-pane-inner" id="error-text">
|
||||||
<div class="snapshot-age error">{{watch.error_text_ctime|format_seconds_ago}} seconds ago</div>
|
<div class="snapshot-age error">{{watch.error_text_ctime|format_seconds_ago}} seconds ago</div>
|
||||||
@@ -36,11 +37,12 @@
|
|||||||
|
|
||||||
<div class="tab-pane-inner" id="text">
|
<div class="tab-pane-inner" id="text">
|
||||||
<div class="snapshot-age">{{watch.snapshot_text_ctime|format_timestamp_timeago}}</div>
|
<div class="snapshot-age">{{watch.snapshot_text_ctime|format_timestamp_timeago}}</div>
|
||||||
<span class="ignored">Grey lines are ignored</span> <span class="triggered">Blue lines are triggers</span>
|
<span class="ignored">Grey lines are ignored</span> <span class="triggered">Blue lines are triggers</span> <span class="tip"><strong>Pro-tip</strong>: Highlight text to add to ignore filters</span>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td id="diff-col">
|
<td id="diff-col" class="highlightable-filter">
|
||||||
{% for row in content %}
|
{% for row in content %}
|
||||||
<div class="{{row.classes}}">{{row.line}}</div>
|
<div class="{{row.classes}}">{{row.line}}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -62,14 +62,6 @@
|
|||||||
<span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page)
|
<span class="pure-form-message-inline">Allow access to view watch diff page when password is enabled (Good for sharing the diff page)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-control-group">
|
|
||||||
{{ render_field(form.application.form.base_url, placeholder="http://yoursite.com:5000/",
|
|
||||||
class="m-d") }}
|
|
||||||
<span class="pure-form-message-inline">
|
|
||||||
Base URL used for the <code>{{ '{{ base_url }}' }}</code> token in notifications and RSS links.<br>Default value is the ENV var 'BASE_URL' (Currently "{{settings_application['current_base_url']}}"),
|
|
||||||
<a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.application.form.pager_size) }}
|
{{ render_field(form.application.form.pager_size) }}
|
||||||
<span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span>
|
<span class="pure-form-message-inline">Number of items per page in the watch overview list, 0 to disable.</span>
|
||||||
@@ -100,6 +92,13 @@
|
|||||||
{{ render_common_settings_form(form.application.form, emailprefix, settings_application) }}
|
{{ render_common_settings_form(form.application.form, emailprefix, settings_application) }}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<div class="pure-control-group" id="notification-base-url">
|
||||||
|
{{ render_field(form.application.form.base_url, class="m-d") }}
|
||||||
|
<span class="pure-form-message-inline">
|
||||||
|
Base URL used for the <code>{{ '{{ base_url }}' }}</code> token in notification links.<br>
|
||||||
|
Default value is the system environment variable '<code>BASE_URL</code>' - <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Configurable-BASE_URL-setting">read more here</a>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="fetching">
|
<div class="tab-pane-inner" id="fetching">
|
||||||
@@ -181,20 +180,56 @@ nav
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane-inner" id="proxies">
|
<div class="tab-pane-inner" id="proxies">
|
||||||
|
<div id="recommended-proxy">
|
||||||
|
<div>
|
||||||
|
<img style="height: 2em;" src="{{url_for('static_content', group='images', filename='brightdata.svg')}}" alt="BrightData Proxy Provider">
|
||||||
|
<p>BrightData offer world-class proxy services, "Data Center" proxies are a very affordable way to proxy your requests, whilst <strong><a href="https://brightdata.grsm.io/n0r16zf7eivq">WebUnlocker</a></strong> can help solve most CAPTCHAs.</p>
|
||||||
|
<p>
|
||||||
|
BrightData offer many <a href="https://brightdata.com/proxy-types" target="new">many different types of proxies</a>, it is worth reading about what is best for your use-case.
|
||||||
|
</p>
|
||||||
|
|
||||||
<p><strong>Tip</strong>: You can connect to websites using <a href="https://brightdata.grsm.io/n0r16zf7eivq">BrightData</a> proxies, their service <strong>WebUnlocker</strong> will solve most CAPTCHAs, whilst their <strong>Residential Proxies</strong> may help to avoid CAPTCHA altogether. </p>
|
<p>
|
||||||
<p>It may be easier to try <strong>WebUnlocker</strong> first, WebUnlocker also supports country selection.</p>
|
When you have <a href="https://brightdata.grsm.io/n0r16zf7eivq">registered</a>, enabled the required services, visit the <A href="https://brightdata.com/cp/api_example?">API example page</A>, then select <strong>Python</strong>, set the country you wish to use, then copy+paste the access Proxy URL into the "Extra Proxies" boxes below.<br>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The Proxy URL with BrightData should start with <code>http://brd-customer...</code>
|
||||||
|
</p>
|
||||||
|
<p>When you sign up using <a href="https://brightdata.grsm.io/n0r16zf7eivq">https://brightdata.grsm.io/n0r16zf7eivq</a> BrightData will match any first deposit up to $150</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<img style="height: 2em;"
|
||||||
|
src="{{url_for('static_content', group='images', filename='oxylabs.svg')}}"
|
||||||
|
alt="Oxylabs Proxy Provider">
|
||||||
|
<p>
|
||||||
|
Collect public data at scale with industry-leading web scraping solutions and the world’s
|
||||||
|
largest ethical proxy network.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Oxylabs also provide a <a href="https://oxylabs.io/products/web-unblocker"><strong>WebUnlocker</strong></a>
|
||||||
|
proxy that bypasses sophisticated anti-bot systems, so you don’t have to.<br>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Serve over <a href="https://oxylabs.io/location-proxy">195 countries</a>, providing <a
|
||||||
|
href="https://oxylabs.io/products/residential-proxy-pool">Residential</a>, <a
|
||||||
|
href="https://oxylabs.io/products/mobile-proxies">Mobile</a> and <a
|
||||||
|
href="https://oxylabs.io/products/rotating-isp-proxies">ISP proxies</a> and much more.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Use the promo code <strong>boost35</strong> with this link <a href="https://oxylabs.go2cloud.org/SH2d">https://oxylabs.go2cloud.org/SH2d</a> for 35% off Residential, Mobile proxies, Web Unblocker, and Scraper APIs. Built-in proxies enable you to access data from all around the world and help overcome anti-bot solutions.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
When you have <a href="https://brightdata.grsm.io/n0r16zf7eivq">registered</a>, enabled the required services, visit the <A href="https://brightdata.com/cp/api_example?">API example page</A>, then select <strong>Python</strong>, set the country you wish to use, then copy+paste the example URL below<br>
|
Your proxy provider may need to whitelist our IP of <code>204.15.192.195</code>
|
||||||
The Proxy URL with BrightData should start with <code>http://brd-customer...</code>
|
|
||||||
</p>
|
</p>
|
||||||
|
<p><strong>Tip</strong>: "Residential" and "Mobile" proxy type can be more successfull than "Data Center" for blocked websites.
|
||||||
<p>When you sign up using <a href="https://brightdata.grsm.io/n0r16zf7eivq">https://brightdata.grsm.io/n0r16zf7eivq</a> BrightData will match any first deposit up to $150</p>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_field(form.requests.form.extra_proxies) }}
|
{{ render_field(form.requests.form.extra_proxies) }}
|
||||||
<span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span>
|
<span class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="actions">
|
<div id="actions">
|
||||||
|
|||||||
77
changedetectionio/tests/proxy_list/test_noproxy.py
Normal file
77
changedetectionio/tests/proxy_list/test_noproxy.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import time
|
||||||
|
from flask import url_for
|
||||||
|
from ..util import live_server_setup, wait_for_all_checks, extract_UUID_from_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_noproxy_option(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
# Run by run_proxy_tests.sh
|
||||||
|
# Call this URL then scan the containers that it never went through them
|
||||||
|
url = "http://noproxy.changedetection.io"
|
||||||
|
|
||||||
|
# Should only be available when a proxy is setup
|
||||||
|
res = client.get(
|
||||||
|
url_for("edit_page", uuid="first", unpause_on_save=1))
|
||||||
|
assert b'No proxy' not in res.data
|
||||||
|
|
||||||
|
# Setup a proxy
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings_page"),
|
||||||
|
data={
|
||||||
|
"requests-time_between_check-minutes": 180,
|
||||||
|
"application-ignore_whitespace": "y",
|
||||||
|
"application-fetch_backend": "html_requests",
|
||||||
|
"requests-extra_proxies-0-proxy_name": "custom-one-proxy",
|
||||||
|
"requests-extra_proxies-0-proxy_url": "http://test:awesome@squid-one:3128",
|
||||||
|
"requests-extra_proxies-1-proxy_name": "custom-two-proxy",
|
||||||
|
"requests-extra_proxies-1-proxy_url": "http://test:awesome@squid-two:3128",
|
||||||
|
"requests-extra_proxies-2-proxy_name": "custom-proxy",
|
||||||
|
"requests-extra_proxies-2-proxy_url": "http://test:awesome@squid-custom:3128",
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"Settings updated." in res.data
|
||||||
|
|
||||||
|
# Should be available as an option
|
||||||
|
res = client.get(
|
||||||
|
url_for("settings_page", unpause_on_save=1))
|
||||||
|
assert b'No proxy' in res.data
|
||||||
|
|
||||||
|
|
||||||
|
# This will add it paused
|
||||||
|
res = client.post(
|
||||||
|
url_for("form_quick_watch_add"),
|
||||||
|
data={"url": url, "tags": '', 'edit_and_watch_submit_button': 'Edit > Watch'},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"Watch added in Paused state, saving will unpause" in res.data
|
||||||
|
uuid = extract_UUID_from_client(client)
|
||||||
|
res = client.get(
|
||||||
|
url_for("edit_page", uuid=uuid, unpause_on_save=1))
|
||||||
|
assert b'No proxy' in res.data
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("edit_page", uuid=uuid, unpause_on_save=1),
|
||||||
|
data={
|
||||||
|
"include_filters": "",
|
||||||
|
"fetch_backend": "html_requests",
|
||||||
|
"headers": "",
|
||||||
|
"proxy": "no-proxy",
|
||||||
|
"tags": "",
|
||||||
|
"url": url,
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"unpaused" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
# Now the request should NOT appear in the second-squid logs (handled by the run_test_proxies.sh script)
|
||||||
|
|
||||||
|
# Prove that it actually checked
|
||||||
|
|
||||||
|
assert live_server.app.config['DATASTORE'].data['watching'][uuid]['last_checked'] != 0
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from .util import set_original_response, set_modified_response, live_server_setup
|
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@@ -19,12 +19,12 @@ def test_backup(client, live_server):
|
|||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("import_page"),
|
url_for("import_page"),
|
||||||
data={"urls": url_for('test_endpoint', _external=True)},
|
data={"urls": url_for('test_endpoint', _external=True)+"?somechar=őőőőőőőő"},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|
||||||
assert b"1 Imported" in res.data
|
assert b"1 Imported" in res.data
|
||||||
time.sleep(3)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("get_backup"),
|
url_for("get_backup"),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def set_response_without_filter():
|
|||||||
<p>Which is across multiple lines</p>
|
<p>Which is across multiple lines</p>
|
||||||
<br>
|
<br>
|
||||||
So let's see what happens. <br>
|
So let's see what happens. <br>
|
||||||
<div id="nope-doesnt-exist">Some text thats the same</div>
|
<div id="nope-doesnt-exist">Some text thats the same</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -32,7 +32,7 @@ def set_response_with_filter():
|
|||||||
<p>Which is across multiple lines</p>
|
<p>Which is across multiple lines</p>
|
||||||
<br>
|
<br>
|
||||||
So let's see what happens. <br>
|
So let's see what happens. <br>
|
||||||
<div class="ticket-available">Ticket now on sale!</div>
|
<div class="ticket-available">Ticket now on sale!</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -84,6 +84,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
|
|||||||
"Snapshot: {{current_snapshot}}\n"
|
"Snapshot: {{current_snapshot}}\n"
|
||||||
"Diff: {{diff}}\n"
|
"Diff: {{diff}}\n"
|
||||||
"Diff Full: {{diff_full}}\n"
|
"Diff Full: {{diff_full}}\n"
|
||||||
|
"Diff as Patch: {{diff_patch}}\n"
|
||||||
":-)",
|
":-)",
|
||||||
"notification_format": "Text"}
|
"notification_format": "Text"}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def set_response_with_filter():
|
|||||||
<p>Which is across multiple lines</p>
|
<p>Which is across multiple lines</p>
|
||||||
<br>
|
<br>
|
||||||
So let's see what happens. <br>
|
So let's see what happens. <br>
|
||||||
<div id="nope-doesnt-exist">Some text thats the same</div>
|
<div id="nope-doesnt-exist">Some text thats the same</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -66,6 +66,7 @@ def run_filter_test(client, content_filter):
|
|||||||
"Snapshot: {{current_snapshot}}\n"
|
"Snapshot: {{current_snapshot}}\n"
|
||||||
"Diff: {{diff}}\n"
|
"Diff: {{diff}}\n"
|
||||||
"Diff Full: {{diff_full}}\n"
|
"Diff Full: {{diff_full}}\n"
|
||||||
|
"Diff as Patch: {{diff_patch}}\n"
|
||||||
":-)",
|
":-)",
|
||||||
"notification_format": "Text"}
|
"notification_format": "Text"}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def set_original_response():
|
|||||||
Some initial text<br>
|
Some initial text<br>
|
||||||
<p id="only-this">Should be only this</p>
|
<p id="only-this">Should be only this</p>
|
||||||
<br>
|
<br>
|
||||||
<p id="not-this">And never this</p>
|
<p id="not-this">And never this</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -30,7 +30,7 @@ def set_modified_response():
|
|||||||
Some initial text<br>
|
Some initial text<br>
|
||||||
<p id="only-this">Should be REALLY only this</p>
|
<p id="only-this">Should be REALLY only this</p>
|
||||||
<br>
|
<br>
|
||||||
<p id="not-this">And never this</p>
|
<p id="not-this">And never this</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
@@ -189,6 +189,7 @@ def test_group_tag_notification(client, live_server):
|
|||||||
"Diff Added: {{diff_added}}\n"
|
"Diff Added: {{diff_added}}\n"
|
||||||
"Diff Removed: {{diff_removed}}\n"
|
"Diff Removed: {{diff_removed}}\n"
|
||||||
"Diff Full: {{diff_full}}\n"
|
"Diff Full: {{diff_full}}\n"
|
||||||
|
"Diff as Patch: {{diff_patch}}\n"
|
||||||
":-)",
|
":-)",
|
||||||
"notification_screenshot": True,
|
"notification_screenshot": True,
|
||||||
"notification_format": "Text",
|
"notification_format": "Text",
|
||||||
@@ -319,4 +320,4 @@ def test_clone_tag_on_quickwatchform_add(client, live_server):
|
|||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
res = client.get(url_for("tags.delete_all"), follow_redirects=True)
|
res = client.get(url_for("tags.delete_all"), follow_redirects=True)
|
||||||
assert b'All tags deleted' in res.data
|
assert b'All tags deleted' in res.data
|
||||||
|
|||||||
@@ -15,11 +15,24 @@ def test_strip_regex_text_func():
|
|||||||
but sometimes we want to remove the lines.
|
but sometimes we want to remove the lines.
|
||||||
|
|
||||||
but 1 lines
|
but 1 lines
|
||||||
|
skip 5 lines
|
||||||
|
really? yes man
|
||||||
|
#/not this tries weirdly formed regex or just strings starting with /
|
||||||
|
/not this
|
||||||
but including 1234 lines
|
but including 1234 lines
|
||||||
igNORe-cAse text we dont want to keep
|
igNORe-cAse text we dont want to keep
|
||||||
but not always."""
|
but not always."""
|
||||||
|
|
||||||
ignore_lines = ["sometimes", "/\s\d{2,3}\s/", "/ignore-case text/"]
|
|
||||||
|
ignore_lines = [
|
||||||
|
"sometimes",
|
||||||
|
"/\s\d{2,3}\s/",
|
||||||
|
"/ignore-case text/",
|
||||||
|
"really?",
|
||||||
|
"/skip \d lines/i",
|
||||||
|
"/not"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
fetcher = fetch_site_status.perform_site_check(datastore=False)
|
fetcher = fetch_site_status.perform_site_check(datastore=False)
|
||||||
stripped_content = html_tools.strip_ignore_text(test_content, ignore_lines)
|
stripped_content = html_tools.strip_ignore_text(test_content, ignore_lines)
|
||||||
@@ -27,4 +40,10 @@ def test_strip_regex_text_func():
|
|||||||
assert b"but 1 lines" in stripped_content
|
assert b"but 1 lines" in stripped_content
|
||||||
assert b"igNORe-cAse text" not in stripped_content
|
assert b"igNORe-cAse text" not in stripped_content
|
||||||
assert b"but 1234 lines" not in stripped_content
|
assert b"but 1234 lines" not in stripped_content
|
||||||
|
assert b"really" not in stripped_content
|
||||||
|
assert b"not this" not in stripped_content
|
||||||
|
|
||||||
|
# Check line number reporting
|
||||||
|
stripped_content = html_tools.strip_ignore_text(test_content, ignore_lines, mode="line numbers")
|
||||||
|
assert stripped_content == [2, 5, 6, 7, 8, 10]
|
||||||
|
|
||||||
|
|||||||
57
changedetectionio/tests/test_ignorehighlighter.py
Normal file
57
changedetectionio/tests/test_ignorehighlighter.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import time
|
||||||
|
from flask import url_for
|
||||||
|
from .util import live_server_setup, wait_for_all_checks
|
||||||
|
from changedetectionio import html_tools
|
||||||
|
from . util import extract_UUID_from_client
|
||||||
|
|
||||||
|
def set_original_ignore_response():
|
||||||
|
test_return_data = """<html>
|
||||||
|
<body>
|
||||||
|
Some initial text<br>
|
||||||
|
<p>Which is across multiple lines</p>
|
||||||
|
<br>
|
||||||
|
So let's see what happens. <br>
|
||||||
|
<p>oh yeah 456</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open("test-datastore/endpoint-content.txt", "w") as f:
|
||||||
|
f.write(test_return_data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_highlight_ignore(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
set_original_ignore_response()
|
||||||
|
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
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
uuid = extract_UUID_from_client(client)
|
||||||
|
# use the highlighter endpoint
|
||||||
|
res = client.post(
|
||||||
|
url_for("highlight_submit_ignore_url", uuid=uuid),
|
||||||
|
data={"mode": 'digit-regex', 'selection': 'oh yeah 123'},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
res = client.get(url_for("edit_page", uuid=uuid))
|
||||||
|
|
||||||
|
# should be a regex now
|
||||||
|
assert b'/oh\ yeah\ \d+/' in res.data
|
||||||
|
|
||||||
|
# Should return a link
|
||||||
|
assert b'href' in res.data
|
||||||
|
|
||||||
|
# And it should register in the preview page
|
||||||
|
res = client.get(url_for("preview_page", uuid=uuid))
|
||||||
|
assert b'<div class="ignored">oh yeah 456' in res.data
|
||||||
@@ -98,6 +98,7 @@ def test_check_notification(client, live_server):
|
|||||||
"Diff Added: {{diff_added}}\n"
|
"Diff Added: {{diff_added}}\n"
|
||||||
"Diff Removed: {{diff_removed}}\n"
|
"Diff Removed: {{diff_removed}}\n"
|
||||||
"Diff Full: {{diff_full}}\n"
|
"Diff Full: {{diff_full}}\n"
|
||||||
|
"Diff as Patch: {{diff_patch}}\n"
|
||||||
":-)",
|
":-)",
|
||||||
"notification_screenshot": True,
|
"notification_screenshot": True,
|
||||||
"notification_format": "Text"}
|
"notification_format": "Text"}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class TestDiffBuilder(unittest.TestCase):
|
|||||||
output = output.split("\n")
|
output = output.split("\n")
|
||||||
self.assertIn('(removed) for having learned computerese,', output)
|
self.assertIn('(removed) for having learned computerese,', output)
|
||||||
self.assertIn('(removed) I continue to examine bits, bytes and words', output)
|
self.assertIn('(removed) I continue to examine bits, bytes and words', output)
|
||||||
|
|
||||||
#diff_removed
|
#diff_removed
|
||||||
with open(base_dir + "/test-content/before.txt", 'r') as f:
|
with open(base_dir + "/test-content/before.txt", 'r') as f:
|
||||||
previous_version_file_contents = f.read()
|
previous_version_file_contents = f.read()
|
||||||
@@ -49,7 +49,7 @@ class TestDiffBuilder(unittest.TestCase):
|
|||||||
self.assertIn('(into) xok', output)
|
self.assertIn('(into) xok', output)
|
||||||
self.assertIn('(into) next-x-ok', output)
|
self.assertIn('(into) next-x-ok', output)
|
||||||
self.assertNotIn('(added) and something new', output)
|
self.assertNotIn('(added) and something new', output)
|
||||||
|
|
||||||
#diff_removed
|
#diff_removed
|
||||||
with open(base_dir + "/test-content/after-2.txt", 'r') as f:
|
with open(base_dir + "/test-content/after-2.txt", 'r') as f:
|
||||||
newest_version_file_contents = f.read()
|
newest_version_file_contents = f.read()
|
||||||
@@ -57,9 +57,25 @@ class TestDiffBuilder(unittest.TestCase):
|
|||||||
output = output.split("\n")
|
output = output.split("\n")
|
||||||
self.assertIn('(removed) for having learned computerese,', output)
|
self.assertIn('(removed) for having learned computerese,', output)
|
||||||
self.assertIn('(removed) I continue to examine bits, bytes and words', output)
|
self.assertIn('(removed) I continue to examine bits, bytes and words', output)
|
||||||
|
|
||||||
|
def test_expected_diff_patch_output(self):
|
||||||
|
base_dir = os.path.dirname(__file__)
|
||||||
|
with open(base_dir + "/test-content/before.txt", 'r') as f:
|
||||||
|
before = f.read()
|
||||||
|
with open(base_dir + "/test-content/after.txt", 'r') as f:
|
||||||
|
after = f.read()
|
||||||
|
|
||||||
|
output = diff.render_diff(previous_version_file_contents=before,
|
||||||
|
newest_version_file_contents=after,
|
||||||
|
patch_format=True)
|
||||||
|
output = output.split("\n")
|
||||||
|
|
||||||
|
self.assertIn('-ok', output)
|
||||||
|
self.assertIn('+xok', output)
|
||||||
|
self.assertIn('+next-x-ok', output)
|
||||||
|
self.assertIn('+and something new', output)
|
||||||
|
|
||||||
# @todo test blocks of changed, blocks of added, blocks of removed
|
# @todo test blocks of changed, blocks of added, blocks of removed
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class update_worker(threading.Thread):
|
|||||||
'diff': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep),
|
'diff': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep),
|
||||||
'diff_added': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_removed=False, line_feed_sep=line_feed_sep),
|
'diff_added': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_removed=False, line_feed_sep=line_feed_sep),
|
||||||
'diff_full': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_equal=True, line_feed_sep=line_feed_sep),
|
'diff_full': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_equal=True, line_feed_sep=line_feed_sep),
|
||||||
|
'diff_patch': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep, patch_format=True),
|
||||||
'diff_removed': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_added=False, line_feed_sep=line_feed_sep),
|
'diff_removed': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_added=False, line_feed_sep=line_feed_sep),
|
||||||
'screenshot': watch.get_screenshot() if watch.get('notification_screenshot') else None,
|
'screenshot': watch.get_screenshot() if watch.get('notification_screenshot') else None,
|
||||||
'triggered_text': triggered_text,
|
'triggered_text': triggered_text,
|
||||||
@@ -379,6 +380,9 @@ class update_worker(threading.Thread):
|
|||||||
if not self.datastore.data['watching'][uuid].get('ignore_status_codes'):
|
if not self.datastore.data['watching'][uuid].get('ignore_status_codes'):
|
||||||
update_obj['consecutive_filter_failures'] = 0
|
update_obj['consecutive_filter_failures'] = 0
|
||||||
|
|
||||||
|
# Everything ran OK, clean off any previous error
|
||||||
|
update_obj['last_error'] = False
|
||||||
|
|
||||||
self.cleanup_error_artifacts(uuid)
|
self.cleanup_error_artifacts(uuid)
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ services:
|
|||||||
#
|
#
|
||||||
# Hides the `Referer` header so that monitored websites can't see the changedetection.io hostname.
|
# Hides the `Referer` header so that monitored websites can't see the changedetection.io hostname.
|
||||||
# - HIDE_REFERER=true
|
# - HIDE_REFERER=true
|
||||||
|
#
|
||||||
|
# Default number of parallel/concurrent fetchers
|
||||||
|
# - FETCH_WORKERS=10
|
||||||
|
|
||||||
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
# Comment out ports: when using behind a reverse proxy , enable networks: etc.
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ flask~=2.0
|
|||||||
inscriptis~=2.2
|
inscriptis~=2.2
|
||||||
pytz
|
pytz
|
||||||
timeago~=1.0
|
timeago~=1.0
|
||||||
validators
|
validators~=0.21
|
||||||
|
|
||||||
|
|
||||||
# Set these versions together to avoid a RequestsDependencyWarning
|
# Set these versions together to avoid a RequestsDependencyWarning
|
||||||
# >= 2.26 also adds Brotli support if brotli is installed
|
# >= 2.26 also adds Brotli support if brotli is installed
|
||||||
@@ -32,7 +33,7 @@ dnspython<2.3.0
|
|||||||
# jq not available on Windows so must be installed manually
|
# jq not available on Windows so must be installed manually
|
||||||
|
|
||||||
# Notification library
|
# Notification library
|
||||||
apprise~=1.3.0
|
apprise~=1.5.0
|
||||||
|
|
||||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
||||||
paho-mqtt
|
paho-mqtt
|
||||||
@@ -71,3 +72,6 @@ pillow
|
|||||||
# Include pytest, so if theres a support issue we can ask them to run these tests on their setup
|
# Include pytest, so if theres a support issue we can ask them to run these tests on their setup
|
||||||
pytest ~=7.2
|
pytest ~=7.2
|
||||||
pytest-flask ~=1.2
|
pytest-flask ~=1.2
|
||||||
|
|
||||||
|
# Pin jsonschema version to prevent build errors on armv6 while rpds-py wheels aren't available (1708)
|
||||||
|
jsonschema==4.17.3
|
||||||
|
|||||||
Reference in New Issue
Block a user