mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-22 01:16:12 +00:00
Compare commits
9 Commits
multiple-t
...
refactor-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d579e26cd6 | ||
|
|
f8fe5c8d41 | ||
|
|
bddf9af584 | ||
|
|
643a45b5e2 | ||
|
|
e9f1cc91c7 | ||
|
|
080526eb65 | ||
|
|
c9552d2319 | ||
|
|
3c14273021 | ||
|
|
547ab70ea3 |
221
.github/workflows/test-only.yml
vendored
221
.github/workflows/test-only.yml
vendored
@@ -4,10 +4,17 @@ name: ChangeDetection.io App Test
|
|||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-code:
|
test-application:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Mainly just for link/flake8
|
||||||
|
- name: Set up Python 3.11
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
- name: Lint with flake8
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
pip3 install flake8
|
pip3 install flake8
|
||||||
@@ -16,24 +23,202 @@ jobs:
|
|||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
test-application-3-10:
|
- name: Spin up ancillary testable services
|
||||||
needs: lint-code
|
run: |
|
||||||
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
|
||||||
with:
|
docker network create changedet-network
|
||||||
python-version: '3.10'
|
|
||||||
|
# Selenium
|
||||||
|
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
||||||
|
|
||||||
|
# SocketPuppetBrowser + Extra for custom browser test
|
||||||
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
||||||
|
|
||||||
|
- name: Build changedetection.io container for testing
|
||||||
|
run: |
|
||||||
|
# Build a changedetection.io container and start testing inside
|
||||||
|
docker build --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
|
||||||
|
# Debug info
|
||||||
|
docker run test-changedetectionio bash -c 'pip list'
|
||||||
|
|
||||||
|
- name: Spin up ancillary SMTP+Echo message test server
|
||||||
|
run: |
|
||||||
|
# Debug SMTP server/echo message back server
|
||||||
|
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'python changedetectionio/tests/smtp/smtp-test-server.py'
|
||||||
|
|
||||||
|
- name: Show docker container state and other debug info
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "Running processes in docker..."
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
- name: Test built container with Pytest (generally as requests/plaintext fetching)
|
||||||
|
run: |
|
||||||
|
# Unit tests
|
||||||
|
echo "run test with unittest"
|
||||||
|
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_watch_model'
|
||||||
|
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
|
||||||
|
|
||||||
|
# All tests
|
||||||
|
echo "run test with pytest"
|
||||||
|
# The default pytest logger_level is TRACE
|
||||||
|
# To change logger_level for pytest(test/conftest.py),
|
||||||
|
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
||||||
|
docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
||||||
|
|
||||||
|
# PLAYWRIGHT/NODE-> CDP
|
||||||
|
- name: Playwright and SocketPuppetBrowser - Specific tests in built container
|
||||||
|
run: |
|
||||||
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
|
# tests/visualselector/test_fetch_data.py will do browser steps
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
|
|
||||||
test-application-3-11:
|
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
||||||
needs: lint-code
|
run: |
|
||||||
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
||||||
with:
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
python-version: '3.11'
|
|
||||||
skip-pypuppeteer: true
|
|
||||||
|
|
||||||
test-application-3-12:
|
- name: Playwright and SocketPuppetBrowser - Restock detection
|
||||||
needs: lint-code
|
run: |
|
||||||
uses: ./.github/workflows/test-stack-reusable-workflow.yml
|
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
||||||
with:
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
||||||
python-version: '3.12'
|
|
||||||
skip-pypuppeteer: true
|
|
||||||
|
|
||||||
|
# STRAIGHT TO CDP
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
||||||
|
run: |
|
||||||
|
# Playwright via Sockpuppetbrowser fetch
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
||||||
|
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
||||||
|
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
||||||
|
run: |
|
||||||
|
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
- name: Pyppeteer and SocketPuppetBrowser - Restock detection
|
||||||
|
run: |
|
||||||
|
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
||||||
|
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
||||||
|
|
||||||
|
# SELENIUM
|
||||||
|
- name: Specific tests in built container for Selenium
|
||||||
|
run: |
|
||||||
|
# Selenium fetch
|
||||||
|
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
||||||
|
|
||||||
|
- name: Specific tests in built container for headers and requests checks with Selenium
|
||||||
|
run: |
|
||||||
|
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
||||||
|
|
||||||
|
# OTHER STUFF
|
||||||
|
- name: Test SMTP notification mime types
|
||||||
|
run: |
|
||||||
|
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
||||||
|
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
||||||
|
|
||||||
|
# @todo Add a test via playwright/puppeteer
|
||||||
|
# squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
|
||||||
|
- name: Test proxy squid style interaction
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_proxy_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test proxy SOCKS5 style interaction
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_socks_proxy_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test custom browser URL
|
||||||
|
run: |
|
||||||
|
cd changedetectionio
|
||||||
|
./run_custom_browser_url_tests.sh
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Test changedetection.io container starts+runs basically without error
|
||||||
|
run: |
|
||||||
|
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
# Should return 0 (no error) when grep finds it
|
||||||
|
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
||||||
|
|
||||||
|
# and IPv6
|
||||||
|
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
||||||
|
|
||||||
|
# Check whether TRACE log is enabled.
|
||||||
|
# Also, check whether TRACE is came from STDERR
|
||||||
|
docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
|
||||||
|
# Check whether DEBUG is came from STDOUT
|
||||||
|
docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
|
||||||
|
|
||||||
|
docker kill test-changedetectionio
|
||||||
|
|
||||||
|
- name: Test changedetection.io SIGTERM and SIGINT signal shutdown
|
||||||
|
run: |
|
||||||
|
|
||||||
|
echo SIGINT Shutdown request test
|
||||||
|
docker run --name sig-test -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
echo ">>> Sending SIGINT to sig-test container"
|
||||||
|
docker kill --signal=SIGINT sig-test
|
||||||
|
sleep 3
|
||||||
|
# invert the check (it should be not 0/not running)
|
||||||
|
docker ps
|
||||||
|
# check signal catch(STDERR) log. Because of
|
||||||
|
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
||||||
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
|
||||||
|
test -z "`docker ps|grep sig-test`"
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "Looks like container was running when it shouldnt be"
|
||||||
|
docker ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
||||||
|
docker rm sig-test
|
||||||
|
|
||||||
|
echo SIGTERM Shutdown request test
|
||||||
|
docker run --name sig-test -d test-changedetectionio
|
||||||
|
sleep 3
|
||||||
|
echo ">>> Sending SIGTERM to sig-test container"
|
||||||
|
docker kill --signal=SIGTERM sig-test
|
||||||
|
sleep 3
|
||||||
|
# invert the check (it should be not 0/not running)
|
||||||
|
docker ps
|
||||||
|
# check signal catch(STDERR) log. Because of
|
||||||
|
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
||||||
|
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
|
||||||
|
test -z "`docker ps|grep sig-test`"
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "Looks like container was running when it shouldnt be"
|
||||||
|
docker ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
||||||
|
docker rm sig-test
|
||||||
|
|
||||||
|
- name: Dump container log
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
mkdir output-logs
|
||||||
|
docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout.txt
|
||||||
|
docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr.txt
|
||||||
|
|
||||||
|
- name: Store container log
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-cdio-basic-tests-output
|
||||||
|
path: output-logs
|
||||||
|
|||||||
239
.github/workflows/test-stack-reusable-workflow.yml
vendored
239
.github/workflows/test-stack-reusable-workflow.yml
vendored
@@ -1,239 +0,0 @@
|
|||||||
name: ChangeDetection.io App Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
python-version:
|
|
||||||
description: 'Python version to use'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
default: '3.10'
|
|
||||||
skip-pypuppeteer:
|
|
||||||
description: 'Skip PyPuppeteer (not supported in 3.11/3.12)'
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-application:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Mainly just for link/flake8
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
|
||||||
|
|
||||||
- name: Build changedetection.io container for testing under Python ${{ env.PYTHON_VERSION }}
|
|
||||||
run: |
|
|
||||||
echo "---- Building for Python ${{ env.PYTHON_VERSION }} -----"
|
|
||||||
# Build a changedetection.io container and start testing inside
|
|
||||||
docker build --build-arg PYTHON_VERSION=${{ env.PYTHON_VERSION }} --build-arg LOGGER_LEVEL=TRACE -t test-changedetectionio .
|
|
||||||
# Debug info
|
|
||||||
docker run test-changedetectionio bash -c 'pip list'
|
|
||||||
|
|
||||||
- name: We should be Python ${{ env.PYTHON_VERSION }} ...
|
|
||||||
run: |
|
|
||||||
docker run test-changedetectionio bash -c 'python3 --version'
|
|
||||||
|
|
||||||
- name: Spin up ancillary testable services
|
|
||||||
run: |
|
|
||||||
|
|
||||||
docker network create changedet-network
|
|
||||||
|
|
||||||
# Selenium
|
|
||||||
docker run --network changedet-network -d --hostname selenium -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4
|
|
||||||
|
|
||||||
# SocketPuppetBrowser + Extra for custom browser test
|
|
||||||
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser --hostname sockpuppetbrowser --rm -p 3000:3000 dgtlmoon/sockpuppetbrowser:latest
|
|
||||||
docker run --network changedet-network -d -e "LOG_LEVEL=TRACE" --cap-add=SYS_ADMIN --name sockpuppetbrowser-custom-url --hostname sockpuppetbrowser-custom-url -p 3001:3000 --rm dgtlmoon/sockpuppetbrowser:latest
|
|
||||||
|
|
||||||
- name: Spin up ancillary SMTP+Echo message test server
|
|
||||||
run: |
|
|
||||||
# Debug SMTP server/echo message back server
|
|
||||||
docker run --network changedet-network -d -p 11025:11025 -p 11080:11080 --hostname mailserver test-changedetectionio bash -c 'pip3 install aiosmtpd && python changedetectionio/tests/smtp/smtp-test-server.py'
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
- name: Show docker container state and other debug info
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "Running processes in docker..."
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
- name: Test built container with Pytest (generally as requests/plaintext fetching)
|
|
||||||
run: |
|
|
||||||
# Unit tests
|
|
||||||
echo "run test with unittest"
|
|
||||||
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_watch_model'
|
|
||||||
docker run test-changedetectionio bash -c 'python3 -m unittest changedetectionio.tests.unit.test_jinja2_security'
|
|
||||||
|
|
||||||
# All tests
|
|
||||||
echo "run test with pytest"
|
|
||||||
# The default pytest logger_level is TRACE
|
|
||||||
# To change logger_level for pytest(test/conftest.py),
|
|
||||||
# append the docker option. e.g. '-e LOGGER_LEVEL=DEBUG'
|
|
||||||
docker run --name test-cdio-basic-tests --network changedet-network test-changedetectionio bash -c 'cd changedetectionio && ./run_basic_tests.sh'
|
|
||||||
|
|
||||||
# PLAYWRIGHT/NODE-> CDP
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Specific tests in built container
|
|
||||||
run: |
|
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
|
||||||
# tests/visualselector/test_fetch_data.py will do browser steps
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
|
||||||
|
|
||||||
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Headers and requests
|
|
||||||
run: |
|
|
||||||
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
- name: Playwright and SocketPuppetBrowser - Restock detection
|
|
||||||
run: |
|
|
||||||
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
|
||||||
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
||||||
|
|
||||||
# STRAIGHT TO CDP
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Specific tests in built container
|
|
||||||
if: ${{ inputs.skip-pypuppeteer == false }}
|
|
||||||
run: |
|
|
||||||
# Playwright via Sockpuppetbrowser fetch
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_content.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_errorhandling.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/visualselector/test_fetch_data.py'
|
|
||||||
docker run --rm -e "FLASK_SERVER_NAME=cdio" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network --hostname=cdio test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/fetchers/test_custom_js_before_content.py'
|
|
||||||
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Headers and requests checks
|
|
||||||
if: ${{ inputs.skip-pypuppeteer == false }}
|
|
||||||
run: |
|
|
||||||
# Settings headers playwright tests - Call back in from Sockpuppetbrowser, check headers
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "FLASK_SERVER_NAME=changedet" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000?dumpio=true" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
- name: Pyppeteer and SocketPuppetBrowser - Restock detection
|
|
||||||
if: ${{ inputs.skip-pypuppeteer == false }}
|
|
||||||
run: |
|
|
||||||
# restock detection via playwright - added name=changedet here so that playwright and sockpuppetbrowser can connect to it
|
|
||||||
docker run --rm --name "changedet" -e "FLASK_SERVER_NAME=changedet" -e "FAST_PUPPETEER_CHROME_FETCHER=True" -e "PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest --live-server-port=5004 --live-server-host=0.0.0.0 tests/restock/test_restock.py'
|
|
||||||
|
|
||||||
# SELENIUM
|
|
||||||
- name: Specific tests in built container for Selenium
|
|
||||||
run: |
|
|
||||||
# Selenium fetch
|
|
||||||
docker run --rm -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/fetchers/test_content.py && pytest tests/test_errorhandling.py'
|
|
||||||
|
|
||||||
- name: Specific tests in built container for headers and requests checks with Selenium
|
|
||||||
run: |
|
|
||||||
docker run --name "changedet" --hostname changedet --rm -e "FLASK_SERVER_NAME=changedet" -e "WEBDRIVER_URL=http://selenium:4444/wd/hub" --network changedet-network test-changedetectionio bash -c 'cd changedetectionio; pytest --live-server-host=0.0.0.0 --live-server-port=5004 tests/test_request.py'
|
|
||||||
|
|
||||||
# OTHER STUFF
|
|
||||||
- name: Test SMTP notification mime types
|
|
||||||
run: |
|
|
||||||
# SMTP content types - needs the 'Debug SMTP server/echo message back server' container from above
|
|
||||||
# "mailserver" hostname defined above
|
|
||||||
docker run --rm --network changedet-network test-changedetectionio bash -c 'cd changedetectionio;pytest tests/smtp/test_notification_smtp.py'
|
|
||||||
|
|
||||||
# @todo Add a test via playwright/puppeteer
|
|
||||||
# squid with auth is tested in run_proxy_tests.sh -> tests/proxy_list/test_select_custom_proxy.py
|
|
||||||
- name: Test proxy squid style interaction
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_proxy_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test proxy SOCKS5 style interaction
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_socks_proxy_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test custom browser URL
|
|
||||||
run: |
|
|
||||||
cd changedetectionio
|
|
||||||
./run_custom_browser_url_tests.sh
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
- name: Test changedetection.io container starts+runs basically without error
|
|
||||||
run: |
|
|
||||||
docker run --name test-changedetectionio -p 5556:5000 -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
# Should return 0 (no error) when grep finds it
|
|
||||||
curl --retry-connrefused --retry 6 -s http://localhost:5556 |grep -q checkbox-uuid
|
|
||||||
|
|
||||||
# and IPv6
|
|
||||||
curl --retry-connrefused --retry 6 -s -g -6 "http://[::1]:5556"|grep -q checkbox-uuid
|
|
||||||
|
|
||||||
# Check whether TRACE log is enabled.
|
|
||||||
# Also, check whether TRACE is came from STDERR
|
|
||||||
docker logs test-changedetectionio 2>&1 1>/dev/null | grep 'TRACE log is enabled' || exit 1
|
|
||||||
# Check whether DEBUG is came from STDOUT
|
|
||||||
docker logs test-changedetectionio 2>/dev/null | grep 'DEBUG' || exit 1
|
|
||||||
|
|
||||||
docker kill test-changedetectionio
|
|
||||||
|
|
||||||
- name: Test changedetection.io SIGTERM and SIGINT signal shutdown
|
|
||||||
run: |
|
|
||||||
|
|
||||||
echo SIGINT Shutdown request test
|
|
||||||
docker run --name sig-test -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
echo ">>> Sending SIGINT to sig-test container"
|
|
||||||
docker kill --signal=SIGINT sig-test
|
|
||||||
sleep 3
|
|
||||||
# invert the check (it should be not 0/not running)
|
|
||||||
docker ps
|
|
||||||
# check signal catch(STDERR) log. Because of
|
|
||||||
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
|
||||||
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGINT' || exit 1
|
|
||||||
test -z "`docker ps|grep sig-test`"
|
|
||||||
if [ $? -ne 0 ]
|
|
||||||
then
|
|
||||||
echo "Looks like container was running when it shouldnt be"
|
|
||||||
docker ps
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
|
||||||
docker rm sig-test
|
|
||||||
|
|
||||||
echo SIGTERM Shutdown request test
|
|
||||||
docker run --name sig-test -d test-changedetectionio
|
|
||||||
sleep 3
|
|
||||||
echo ">>> Sending SIGTERM to sig-test container"
|
|
||||||
docker kill --signal=SIGTERM sig-test
|
|
||||||
sleep 3
|
|
||||||
# invert the check (it should be not 0/not running)
|
|
||||||
docker ps
|
|
||||||
# check signal catch(STDERR) log. Because of
|
|
||||||
# changedetectionio/__init__.py: logger.add(sys.stderr, level=logger_level)
|
|
||||||
docker logs sig-test 2>&1 | grep 'Shutdown: Got Signal - SIGTERM' || exit 1
|
|
||||||
test -z "`docker ps|grep sig-test`"
|
|
||||||
if [ $? -ne 0 ]
|
|
||||||
then
|
|
||||||
echo "Looks like container was running when it shouldnt be"
|
|
||||||
docker ps
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# @todo - scan the container log to see the right "graceful shutdown" text exists
|
|
||||||
docker rm sig-test
|
|
||||||
|
|
||||||
- name: Dump container log
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
mkdir output-logs
|
|
||||||
docker logs test-cdio-basic-tests > output-logs/test-cdio-basic-tests-stdout-${{ env.PYTHON_VERSION }}.txt
|
|
||||||
docker logs test-cdio-basic-tests 2> output-logs/test-cdio-basic-tests-stderr-${{ env.PYTHON_VERSION }}.txt
|
|
||||||
|
|
||||||
- name: Store container log
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: test-cdio-basic-tests-output-py${{ env.PYTHON_VERSION }}
|
|
||||||
path: output-logs
|
|
||||||
@@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
# @NOTE! I would love to move to 3.11 but it breaks the async handler in changedetectionio/content_fetchers/puppeteer.py
|
# @NOTE! I would love to move to 3.11 but it breaks the async handler in changedetectionio/content_fetchers/puppeteer.py
|
||||||
# If you know how to fix it, please do! and test it for both 3.10 and 3.11
|
# If you know how to fix it, please do! and test it for both 3.10 and 3.11
|
||||||
|
FROM python:3.10-slim-bookworm as builder
|
||||||
ARG PYTHON_VERSION=3.10
|
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-slim-bookworm as builder
|
|
||||||
|
|
||||||
# See `cryptography` pin comment in requirements.txt
|
# See `cryptography` pin comment in requirements.txt
|
||||||
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
||||||
@@ -35,7 +32,7 @@ RUN pip install --target=/dependencies playwright~=1.41.2 \
|
|||||||
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
||||||
|
|
||||||
# Final image stage
|
# Final image stage
|
||||||
FROM python:${PYTHON_VERSION}-slim-bookworm
|
FROM python:3.10-slim-bookworm
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
libxslt1.1 \
|
libxslt1.1 \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
|
|
||||||
__version__ = '0.45.23'
|
__version__ = '0.45.22'
|
||||||
|
|
||||||
from changedetectionio.strtobool import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
@@ -175,7 +175,6 @@ def main():
|
|||||||
# proxy_set_header Host "localhost";
|
# proxy_set_header Host "localhost";
|
||||||
# proxy_set_header X-Forwarded-Prefix /app;
|
# proxy_set_header X-Forwarded-Prefix /app;
|
||||||
|
|
||||||
|
|
||||||
if os.getenv('USE_X_SETTINGS'):
|
if os.getenv('USE_X_SETTINGS'):
|
||||||
logger.info("USE_X_SETTINGS is ENABLED")
|
logger.info("USE_X_SETTINGS is ENABLED")
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|||||||
@@ -84,9 +84,7 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
# Tell Playwright to connect to Chrome and setup a new session via our stepper interface
|
# Tell Playwright to connect to Chrome and setup a new session via our stepper interface
|
||||||
browsersteps_start_session['browserstepper'] = browser_steps.browsersteps_live_ui(
|
browsersteps_start_session['browserstepper'] = browser_steps.browsersteps_live_ui(
|
||||||
playwright_browser=browsersteps_start_session['browser'],
|
playwright_browser=browsersteps_start_session['browser'],
|
||||||
proxy=proxy,
|
proxy=proxy)
|
||||||
start_url=datastore.data['watching'][watch_uuid].get('url')
|
|
||||||
)
|
|
||||||
|
|
||||||
# For test
|
# For test
|
||||||
#browsersteps_start_session['browserstepper'].action_goto_url(value="http://example.com?time="+str(time.time()))
|
#browsersteps_start_session['browserstepper'].action_goto_url(value="http://example.com?time="+str(time.time()))
|
||||||
@@ -169,6 +167,11 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
|||||||
step_n = int(request.form.get('step_n'))
|
step_n = int(request.form.get('step_n'))
|
||||||
is_last_step = strtobool(request.form.get('is_last_step'))
|
is_last_step = strtobool(request.form.get('is_last_step'))
|
||||||
|
|
||||||
|
if step_operation == 'Goto site':
|
||||||
|
step_operation = 'goto_url'
|
||||||
|
step_optional_value = datastore.data['watching'][uuid].get('url')
|
||||||
|
step_selector = None
|
||||||
|
|
||||||
# @todo try.. accept.. nice errors not popups..
|
# @todo try.. accept.. nice errors not popups..
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,6 @@ browser_step_ui_config = {'Choose one': '0 0',
|
|||||||
# ONLY Works in Playwright because we need the fullscreen screenshot
|
# ONLY Works in Playwright because we need the fullscreen screenshot
|
||||||
class steppable_browser_interface():
|
class steppable_browser_interface():
|
||||||
page = None
|
page = None
|
||||||
start_url = None
|
|
||||||
|
|
||||||
def __init__(self, start_url):
|
|
||||||
self.start_url = start_url
|
|
||||||
|
|
||||||
# Convert and perform "Click Button" for example
|
# Convert and perform "Click Button" for example
|
||||||
def call_action(self, action_name, selector=None, optional_value=None):
|
def call_action(self, action_name, selector=None, optional_value=None):
|
||||||
@@ -91,10 +87,6 @@ class steppable_browser_interface():
|
|||||||
logger.debug(f"Time to goto URL {time.time()-now:.2f}s")
|
logger.debug(f"Time to goto URL {time.time()-now:.2f}s")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Incase they request to go back to the start
|
|
||||||
def action_goto_site(self, selector=None, value=None):
|
|
||||||
return self.action_goto_url(value=self.start_url)
|
|
||||||
|
|
||||||
def action_click_element_containing_text(self, selector=None, value=''):
|
def action_click_element_containing_text(self, selector=None, value=''):
|
||||||
if not len(value.strip()):
|
if not len(value.strip()):
|
||||||
return
|
return
|
||||||
@@ -202,11 +194,10 @@ class browsersteps_live_ui(steppable_browser_interface):
|
|||||||
|
|
||||||
browser_type = os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').strip('"')
|
browser_type = os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').strip('"')
|
||||||
|
|
||||||
def __init__(self, playwright_browser, proxy=None, headers=None, start_url=None):
|
def __init__(self, playwright_browser, proxy=None, headers=None):
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
self.age_start = time.time()
|
self.age_start = time.time()
|
||||||
self.playwright_browser = playwright_browser
|
self.playwright_browser = playwright_browser
|
||||||
self.start_url = start_url
|
|
||||||
if self.context is None:
|
if self.context is None:
|
||||||
self.connect(proxy=proxy)
|
self.connect(proxy=proxy)
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def manage_user_agent(headers, current_ua=''):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# Ask it what the user agent is, if its obviously ChromeHeadless, switch it to the default
|
# Ask it what the user agent is, if its obviously ChromeHeadless, switch it to the default
|
||||||
ua_in_custom_headers = headers.get('User-Agent')
|
ua_in_custom_headers = next((v for k, v in headers.items() if k.lower() == "user-agent"), None)
|
||||||
if ua_in_custom_headers:
|
if ua_in_custom_headers:
|
||||||
return ua_in_custom_headers
|
return ua_in_custom_headers
|
||||||
|
|
||||||
@@ -112,26 +112,23 @@ class Fetcher():
|
|||||||
|
|
||||||
def browser_steps_get_valid_steps(self):
|
def browser_steps_get_valid_steps(self):
|
||||||
if self.browser_steps is not None and len(self.browser_steps):
|
if self.browser_steps is not None and len(self.browser_steps):
|
||||||
valid_steps = list(filter(
|
valid_steps = filter(
|
||||||
lambda s: (s['operation'] and len(s['operation']) and s['operation'] != 'Choose one'),
|
lambda s: (s['operation'] and len(s['operation']) and s['operation'] != 'Choose one' and s['operation'] != 'Goto site'),
|
||||||
self.browser_steps))
|
self.browser_steps)
|
||||||
|
|
||||||
# Just incase they selected Goto site by accident with older JS
|
|
||||||
if valid_steps and valid_steps[0]['operation'] == 'Goto site':
|
|
||||||
del(valid_steps[0])
|
|
||||||
|
|
||||||
return valid_steps
|
return valid_steps
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def iterate_browser_steps(self, start_url=None):
|
def iterate_browser_steps(self):
|
||||||
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
||||||
from playwright._impl._errors import TimeoutError, Error
|
from playwright._impl._errors import TimeoutError, Error
|
||||||
from changedetectionio.safe_jinja import render as jinja_render
|
from changedetectionio.safe_jinja import render as jinja_render
|
||||||
|
|
||||||
step_n = 0
|
step_n = 0
|
||||||
|
|
||||||
if self.browser_steps is not None and len(self.browser_steps):
|
if self.browser_steps is not None and len(self.browser_steps):
|
||||||
interface = steppable_browser_interface(start_url=start_url)
|
interface = steppable_browser_interface()
|
||||||
interface.page = self.page
|
interface.page = self.page
|
||||||
valid_steps = self.browser_steps_get_valid_steps()
|
valid_steps = self.browser_steps_get_valid_steps()
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class fetcher(Fetcher):
|
|||||||
|
|
||||||
# Re-use as much code from browser steps as possible so its the same
|
# Re-use as much code from browser steps as possible so its the same
|
||||||
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
from changedetectionio.blueprint.browser_steps.browser_steps import steppable_browser_interface
|
||||||
browsersteps_interface = steppable_browser_interface(start_url=url)
|
browsersteps_interface = steppable_browser_interface()
|
||||||
browsersteps_interface.page = self.page
|
browsersteps_interface.page = self.page
|
||||||
|
|
||||||
response = browsersteps_interface.action_goto_url(value=url)
|
response = browsersteps_interface.action_goto_url(value=url)
|
||||||
@@ -172,7 +172,7 @@ class fetcher(Fetcher):
|
|||||||
|
|
||||||
# Run Browser Steps here
|
# Run Browser Steps here
|
||||||
if self.browser_steps_get_valid_steps():
|
if self.browser_steps_get_valid_steps():
|
||||||
self.iterate_browser_steps(start_url=url)
|
self.iterate_browser_steps()
|
||||||
|
|
||||||
self.page.wait_for_timeout(extra_wait * 1000)
|
self.page.wait_for_timeout(extra_wait * 1000)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from loguru import logger
|
|||||||
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
|
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
|
||||||
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, BrowserFetchTimedOut, BrowserConnectError
|
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, BrowserFetchTimedOut, BrowserConnectError
|
||||||
|
|
||||||
|
|
||||||
class fetcher(Fetcher):
|
class fetcher(Fetcher):
|
||||||
fetcher_description = "Puppeteer/direct {}/Javascript".format(
|
fetcher_description = "Puppeteer/direct {}/Javascript".format(
|
||||||
os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').capitalize()
|
os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').capitalize()
|
||||||
@@ -92,38 +93,15 @@ class fetcher(Fetcher):
|
|||||||
ignoreHTTPSErrors=True
|
ignoreHTTPSErrors=True
|
||||||
)
|
)
|
||||||
except websockets.exceptions.InvalidStatusCode as e:
|
except websockets.exceptions.InvalidStatusCode as e:
|
||||||
raise BrowserConnectError(msg=f"Error while trying to connect the browser, Code {e.status_code} (check your access, whitelist IP, password etc)")
|
raise BrowserConnectError(msg=f"Error while trying to connect the browser, Code {e.status_code} (check your access)")
|
||||||
except websockets.exceptions.InvalidURI:
|
except websockets.exceptions.InvalidURI:
|
||||||
raise BrowserConnectError(msg=f"Error connecting to the browser, check your browser connection address (should be ws:// or wss://")
|
raise BrowserConnectError(msg=f"Error connecting to the browser, check your browser connection address (should be ws:// or wss://")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BrowserConnectError(msg=f"Error connecting to the browser {str(e)}")
|
raise BrowserConnectError(msg=f"Error connecting to the browser {str(e)}")
|
||||||
|
|
||||||
# Better is to launch chrome with the URL as arg
|
|
||||||
# non-headless - newPage() will launch an extra tab/window, .browser should already contain 1 page/tab
|
|
||||||
# headless - ask a new page
|
|
||||||
self.page = (pages := await browser.pages) and len(pages) or await browser.newPage()
|
|
||||||
|
|
||||||
try:
|
|
||||||
from pyppeteerstealth import inject_evasions_into_page
|
|
||||||
except ImportError:
|
|
||||||
logger.debug("pyppeteerstealth module not available, skipping")
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
# I tried hooking events via self.page.on(Events.Page.DOMContentLoaded, inject_evasions_requiring_obj_to_page)
|
self.page = await browser.newPage()
|
||||||
# But I could never get it to fire reliably, so we just inject it straight after
|
|
||||||
await inject_evasions_into_page(self.page)
|
|
||||||
|
|
||||||
# This user agent is similar to what was used when tweaking the evasions in inject_evasions_into_page(..)
|
await self.page.setUserAgent(manage_user_agent(headers=request_headers, current_ua=await self.page.evaluate('navigator.userAgent')))
|
||||||
user_agent = None
|
|
||||||
if request_headers and request_headers.get('User-Agent'):
|
|
||||||
# Request_headers should now be CaaseInsensitiveDict
|
|
||||||
# Remove it so it's not sent again with headers after
|
|
||||||
user_agent = request_headers.pop('User-Agent').strip()
|
|
||||||
await self.page.setUserAgent(user_agent)
|
|
||||||
|
|
||||||
if not user_agent:
|
|
||||||
# Attempt to strip 'HeadlessChrome' etc
|
|
||||||
await self.page.setUserAgent(manage_user_agent(headers=request_headers, current_ua=await self.page.evaluate('navigator.userAgent')))
|
|
||||||
|
|
||||||
await self.page.setBypassCSP(True)
|
await self.page.setBypassCSP(True)
|
||||||
if request_headers:
|
if request_headers:
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ class fetcher(Fetcher):
|
|||||||
if self.browser_steps_get_valid_steps():
|
if self.browser_steps_get_valid_steps():
|
||||||
raise BrowserStepsInUnsupportedFetcher(url=url)
|
raise BrowserStepsInUnsupportedFetcher(url=url)
|
||||||
|
|
||||||
|
# Make requests use a more modern looking user-agent
|
||||||
|
if not {k.lower(): v for k, v in request_headers.items()}.get('user-agent', None):
|
||||||
|
request_headers['User-Agent'] = os.getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT",
|
||||||
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36')
|
||||||
|
|
||||||
proxies = {}
|
proxies = {}
|
||||||
|
|
||||||
# Allows override the proxy on a per-request basis
|
# Allows override the proxy on a per-request basis
|
||||||
|
|||||||
@@ -124,10 +124,10 @@ def _jinja2_filter_datetime(watch_obj, format="%Y-%m-%d %H:%M:%S"):
|
|||||||
|
|
||||||
@app.template_filter('format_timestamp_timeago')
|
@app.template_filter('format_timestamp_timeago')
|
||||||
def _jinja2_filter_datetimestamp(timestamp, format="%Y-%m-%d %H:%M:%S"):
|
def _jinja2_filter_datetimestamp(timestamp, format="%Y-%m-%d %H:%M:%S"):
|
||||||
if not timestamp:
|
if timestamp == False:
|
||||||
return 'Not yet'
|
return 'Not yet'
|
||||||
|
|
||||||
return timeago.format(int(timestamp), time.time())
|
return timeago.format(timestamp, time.time())
|
||||||
|
|
||||||
|
|
||||||
@app.template_filter('pagination_slice')
|
@app.template_filter('pagination_slice')
|
||||||
@@ -338,11 +338,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
|
|
||||||
# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
|
# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
|
||||||
for uuid, watch in datastore.data['watching'].items():
|
for uuid, watch in datastore.data['watching'].items():
|
||||||
# @todo tag notification_muted skip also (improve Watch model)
|
|
||||||
if datastore.data['settings']['application'].get('rss_hide_muted_watches') and watch.get('notification_muted'):
|
|
||||||
continue
|
|
||||||
if limit_tag and not limit_tag in watch['tags']:
|
if limit_tag and not limit_tag in watch['tags']:
|
||||||
continue
|
continue
|
||||||
watch['uuid'] = uuid
|
watch['uuid'] = uuid
|
||||||
sorted_watches.append(watch)
|
sorted_watches.append(watch)
|
||||||
|
|
||||||
@@ -472,7 +469,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
# Don't link to hosting when we're on the hosting environment
|
# Don't link to hosting when we're on the hosting environment
|
||||||
active_tag=active_tag,
|
active_tag=active_tag,
|
||||||
active_tag_uuid=active_tag_uuid,
|
active_tag_uuid=active_tag_uuid,
|
||||||
app_rss_token=datastore.data['settings']['application'].get('rss_access_token'),
|
app_rss_token=datastore.data['settings']['application']['rss_access_token'],
|
||||||
datastore=datastore,
|
datastore=datastore,
|
||||||
errored_count=errored_count,
|
errored_count=errored_count,
|
||||||
form=form,
|
form=form,
|
||||||
@@ -771,7 +768,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
jq_support=jq_support,
|
jq_support=jq_support,
|
||||||
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
|
||||||
settings_application=datastore.data['settings']['application'],
|
settings_application=datastore.data['settings']['application'],
|
||||||
using_global_webdriver_wait=not default['webdriver_delay'],
|
using_global_webdriver_wait=default['webdriver_delay'] is None,
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
visualselector_enabled=visualselector_enabled,
|
visualselector_enabled=visualselector_enabled,
|
||||||
watch=watch
|
watch=watch
|
||||||
@@ -1066,8 +1063,6 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
content = []
|
content = []
|
||||||
ignored_line_numbers = []
|
ignored_line_numbers = []
|
||||||
trigger_line_numbers = []
|
trigger_line_numbers = []
|
||||||
versions = []
|
|
||||||
timestamp = None
|
|
||||||
|
|
||||||
# More for testing, possible to return the first/only
|
# More for testing, possible to return the first/only
|
||||||
if uuid == 'first':
|
if uuid == 'first':
|
||||||
@@ -1087,53 +1082,57 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
|
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
|
||||||
is_html_webdriver = True
|
is_html_webdriver = True
|
||||||
|
|
||||||
|
# Never requested successfully, but we detected a fetch error
|
||||||
if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
|
if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
|
||||||
flash("Preview unavailable - No fetch/check completed or triggers not reached", "error")
|
flash("Preview unavailable - No fetch/check completed or triggers not reached", "error")
|
||||||
else:
|
output = render_template("preview.html",
|
||||||
# So prepare the latest preview or not
|
content=content,
|
||||||
preferred_version = request.args.get('version')
|
history_n=watch.history_n,
|
||||||
versions = list(watch.history.keys())
|
extra_stylesheets=extra_stylesheets,
|
||||||
timestamp = versions[-1]
|
# current_diff_url=watch['url'],
|
||||||
if preferred_version and preferred_version in versions:
|
watch=watch,
|
||||||
timestamp = preferred_version
|
uuid=uuid,
|
||||||
|
is_html_webdriver=is_html_webdriver,
|
||||||
|
last_error=watch['last_error'],
|
||||||
|
last_error_text=watch.get_error_text(),
|
||||||
|
last_error_screenshot=watch.get_error_snapshot())
|
||||||
|
return output
|
||||||
|
|
||||||
try:
|
timestamp = list(watch.history.keys())[-1]
|
||||||
versions = list(watch.history.keys())
|
try:
|
||||||
tmp = watch.get_history_snapshot(timestamp).splitlines()
|
tmp = watch.get_history_snapshot(timestamp).splitlines()
|
||||||
|
|
||||||
# Get what needs to be highlighted
|
# Get what needs to be highlighted
|
||||||
ignore_rules = watch.get('ignore_text', []) + datastore.data['settings']['application']['global_ignore_text']
|
ignore_rules = watch.get('ignore_text', []) + datastore.data['settings']['application']['global_ignore_text']
|
||||||
|
|
||||||
# .readlines will keep the \n, but we will parse it here again, in the future tidy this up
|
# .readlines will keep the \n, but we will parse it here again, in the future tidy this up
|
||||||
ignored_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
|
ignored_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
|
||||||
wordlist=ignore_rules,
|
wordlist=ignore_rules,
|
||||||
mode='line numbers'
|
mode='line numbers'
|
||||||
)
|
)
|
||||||
|
|
||||||
trigger_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
|
trigger_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
|
||||||
wordlist=watch['trigger_text'],
|
wordlist=watch['trigger_text'],
|
||||||
mode='line numbers'
|
mode='line numbers'
|
||||||
)
|
)
|
||||||
# Prepare the classes and lines used in the template
|
# Prepare the classes and lines used in the template
|
||||||
i=0
|
i=0
|
||||||
for l in tmp:
|
for l in tmp:
|
||||||
classes=[]
|
classes=[]
|
||||||
i+=1
|
i+=1
|
||||||
if i in ignored_line_numbers:
|
if i in ignored_line_numbers:
|
||||||
classes.append('ignored')
|
classes.append('ignored')
|
||||||
if i in trigger_line_numbers:
|
if i in trigger_line_numbers:
|
||||||
classes.append('triggered')
|
classes.append('triggered')
|
||||||
content.append({'line': l, 'classes': ' '.join(classes)})
|
content.append({'line': l, 'classes': ' '.join(classes)})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})
|
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})
|
||||||
|
|
||||||
output = render_template("preview.html",
|
output = render_template("preview.html",
|
||||||
content=content,
|
content=content,
|
||||||
current_version=timestamp,
|
|
||||||
history_n=watch.history_n,
|
history_n=watch.history_n,
|
||||||
extra_stylesheets=extra_stylesheets,
|
extra_stylesheets=extra_stylesheets,
|
||||||
extra_title=f" - Diff - {watch.label} @ {timestamp}",
|
|
||||||
ignored_line_numbers=ignored_line_numbers,
|
ignored_line_numbers=ignored_line_numbers,
|
||||||
triggered_line_numbers=trigger_line_numbers,
|
triggered_line_numbers=trigger_line_numbers,
|
||||||
current_diff_url=watch['url'],
|
current_diff_url=watch['url'],
|
||||||
@@ -1143,10 +1142,7 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
is_html_webdriver=is_html_webdriver,
|
is_html_webdriver=is_html_webdriver,
|
||||||
last_error=watch['last_error'],
|
last_error=watch['last_error'],
|
||||||
last_error_text=watch.get_error_text(),
|
last_error_text=watch.get_error_text(),
|
||||||
last_error_screenshot=watch.get_error_snapshot(),
|
last_error_screenshot=watch.get_error_snapshot())
|
||||||
versions=versions
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
@@ -526,10 +526,6 @@ class SingleExtraBrowser(Form):
|
|||||||
browser_connection_url = StringField('Browser connection URL', [validators.Optional()], render_kw={"placeholder": "wss://brightdata... wss://oxylabs etc", "size":50})
|
browser_connection_url = StringField('Browser connection URL', [validators.Optional()], render_kw={"placeholder": "wss://brightdata... wss://oxylabs etc", "size":50})
|
||||||
# @todo do the validation here instead
|
# @todo do the validation here instead
|
||||||
|
|
||||||
class DefaultUAInputForm(Form):
|
|
||||||
html_requests = StringField('Plaintext requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})
|
|
||||||
if os.getenv("PLAYWRIGHT_DRIVER_URL") or os.getenv("WEBDRIVER_URL"):
|
|
||||||
html_webdriver = StringField('Chrome requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})
|
|
||||||
|
|
||||||
# datastore.data['settings']['requests']..
|
# datastore.data['settings']['requests']..
|
||||||
class globalSettingsRequestForm(Form):
|
class globalSettingsRequestForm(Form):
|
||||||
@@ -541,8 +537,6 @@ class globalSettingsRequestForm(Form):
|
|||||||
extra_proxies = FieldList(FormField(SingleExtraProxy), min_entries=5)
|
extra_proxies = FieldList(FormField(SingleExtraProxy), min_entries=5)
|
||||||
extra_browsers = FieldList(FormField(SingleExtraBrowser), min_entries=5)
|
extra_browsers = FieldList(FormField(SingleExtraBrowser), min_entries=5)
|
||||||
|
|
||||||
default_ua = FormField(DefaultUAInputForm, label="Default User-Agent overrides")
|
|
||||||
|
|
||||||
def validate_extra_proxies(self, extra_validators=None):
|
def validate_extra_proxies(self, extra_validators=None):
|
||||||
for e in self.data['extra_proxies']:
|
for e in self.data['extra_proxies']:
|
||||||
if e.get('proxy_name') or e.get('proxy_url'):
|
if e.get('proxy_name') or e.get('proxy_url'):
|
||||||
@@ -572,8 +566,6 @@ class globalSettingsApplicationForm(commonSettingsForm):
|
|||||||
removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"})
|
removepassword_button = SubmitField('Remove password', render_kw={"class": "pure-button pure-button-primary"})
|
||||||
render_anchor_tag_content = BooleanField('Render anchor tag content', default=False)
|
render_anchor_tag_content = BooleanField('Render anchor tag content', default=False)
|
||||||
shared_diff_access = BooleanField('Allow access to view diff page when password is enabled', default=False, validators=[validators.Optional()])
|
shared_diff_access = BooleanField('Allow access to view diff page when password is enabled', default=False, validators=[validators.Optional()])
|
||||||
rss_hide_muted_watches = BooleanField('Hide muted watches from RSS feed', default=True,
|
|
||||||
validators=[validators.Optional()])
|
|
||||||
filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification',
|
filter_failure_notification_threshold_attempts = IntegerField('Number of times the filter can be missing before sending a notification',
|
||||||
render_kw={"style": "width: 5em;"},
|
render_kw={"style": "width: 5em;"},
|
||||||
validators=[validators.NumberRange(min=0,
|
validators=[validators.NumberRange(min=0,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from changedetectionio.notification import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT = 6
|
_FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT = 6
|
||||||
DEFAULT_SETTINGS_HEADERS_USERAGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'
|
|
||||||
|
|
||||||
class model(dict):
|
class model(dict):
|
||||||
base_config = {
|
base_config = {
|
||||||
@@ -23,10 +22,6 @@ class model(dict):
|
|||||||
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
|
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
|
||||||
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
||||||
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")), # Number of threads, lower is better for slow connections
|
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")), # Number of threads, lower is better for slow connections
|
||||||
'default_ua': {
|
|
||||||
'html_requests': getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT", DEFAULT_SETTINGS_HEADERS_USERAGENT),
|
|
||||||
'html_webdriver': None,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'application': {
|
'application': {
|
||||||
# Custom notification content
|
# Custom notification content
|
||||||
@@ -46,8 +41,6 @@ class model(dict):
|
|||||||
'pager_size': 50,
|
'pager_size': 50,
|
||||||
'password': False,
|
'password': False,
|
||||||
'render_anchor_tag_content': False,
|
'render_anchor_tag_content': False,
|
||||||
'rss_access_token': None,
|
|
||||||
'rss_hide_muted_watches': True,
|
|
||||||
'schema_version' : 0,
|
'schema_version' : 0,
|
||||||
'shared_diff_access': False,
|
'shared_diff_access': False,
|
||||||
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
||||||
|
|||||||
@@ -333,9 +333,7 @@ class model(dict):
|
|||||||
# Small hack so that we sleep just enough to allow 1 second between history snapshots
|
# Small hack so that we sleep just enough to allow 1 second between history snapshots
|
||||||
# this is because history.txt indexes/keys snapshots by epoch seconds and we dont want dupe keys
|
# this is because history.txt indexes/keys snapshots by epoch seconds and we dont want dupe keys
|
||||||
if self.__newest_history_key and int(timestamp) == int(self.__newest_history_key):
|
if self.__newest_history_key and int(timestamp) == int(self.__newest_history_key):
|
||||||
logger.warning(f"Timestamp {timestamp} already exists, waiting 1 seconds so we have a unique key in history.txt")
|
time.sleep(timestamp - self.__newest_history_key)
|
||||||
timestamp = str(int(timestamp) + 1)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024))
|
threshold = int(os.getenv('SNAPSHOT_BROTLI_COMPRESSION_THRESHOLD', 1024))
|
||||||
skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False'))
|
skip_brotli = strtobool(os.getenv('DISABLE_BROTLI_TEXT_SNAPSHOT', 'False'))
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ from apprise.decorators import notify
|
|||||||
def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
|
def apprise_custom_api_call_wrapper(body, title, notify_type, *args, **kwargs):
|
||||||
import requests
|
import requests
|
||||||
from apprise.utils import parse_url as apprise_parse_url
|
from apprise.utils import parse_url as apprise_parse_url
|
||||||
from apprise import URLBase
|
from apprise.URLBase import URLBase
|
||||||
|
|
||||||
url = kwargs['meta'].get('url')
|
url = kwargs['meta'].get('url')
|
||||||
|
|
||||||
@@ -122,6 +122,10 @@ def process_notification(n_object, datastore):
|
|||||||
# Insert variables into the notification content
|
# Insert variables into the notification content
|
||||||
notification_parameters = create_notification_parameters(n_object, datastore)
|
notification_parameters = create_notification_parameters(n_object, datastore)
|
||||||
|
|
||||||
|
# Get the notification body from datastore
|
||||||
|
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
|
||||||
|
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
|
||||||
|
|
||||||
n_format = valid_notification_formats.get(
|
n_format = valid_notification_formats.get(
|
||||||
n_object.get('notification_format', default_notification_format),
|
n_object.get('notification_format', default_notification_format),
|
||||||
valid_notification_formats[default_notification_format],
|
valid_notification_formats[default_notification_format],
|
||||||
@@ -147,11 +151,6 @@ def process_notification(n_object, datastore):
|
|||||||
|
|
||||||
with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
|
with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
|
||||||
for url in n_object['notification_urls']:
|
for url in n_object['notification_urls']:
|
||||||
|
|
||||||
# Get the notification body from datastore
|
|
||||||
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
|
|
||||||
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
|
|
||||||
|
|
||||||
url = url.strip()
|
url = url.strip()
|
||||||
if not url:
|
if not url:
|
||||||
logger.warning(f"Process Notification: skipping empty notification URL.")
|
logger.warning(f"Process Notification: skipping empty notification URL.")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from changedetectionio.strtobool import strtobool
|
|
||||||
from copy import deepcopy
|
|
||||||
from loguru import logger
|
|
||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
from changedetectionio.strtobool import strtobool
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
class difference_detection_processor():
|
class difference_detection_processor():
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class difference_detection_processor():
|
|||||||
self.watch = deepcopy(self.datastore.data['watching'].get(watch_uuid))
|
self.watch = deepcopy(self.datastore.data['watching'].get(watch_uuid))
|
||||||
|
|
||||||
def call_browser(self):
|
def call_browser(self):
|
||||||
from requests.structures import CaseInsensitiveDict
|
|
||||||
# Protect against file:// access
|
# Protect against file:// access
|
||||||
if re.search(r'^file://', self.watch.get('url', '').strip(), re.IGNORECASE):
|
if re.search(r'^file://', self.watch.get('url', '').strip(), re.IGNORECASE):
|
||||||
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||||
@@ -93,13 +93,7 @@ class difference_detection_processor():
|
|||||||
self.fetcher.browser_steps_screenshot_path = os.path.join(self.datastore.datastore_path, self.watch.get('uuid'))
|
self.fetcher.browser_steps_screenshot_path = os.path.join(self.datastore.datastore_path, self.watch.get('uuid'))
|
||||||
|
|
||||||
# Tweak the base config with the per-watch ones
|
# Tweak the base config with the per-watch ones
|
||||||
request_headers = CaseInsensitiveDict()
|
request_headers = self.watch.get('headers', [])
|
||||||
|
|
||||||
ua = self.datastore.data['settings']['requests'].get('default_ua')
|
|
||||||
if ua and ua.get(prefer_fetch_backend):
|
|
||||||
request_headers.update({'User-Agent': ua.get(prefer_fetch_backend)})
|
|
||||||
|
|
||||||
request_headers.update(self.watch.get('headers', {}))
|
|
||||||
request_headers.update(self.datastore.get_all_base_headers())
|
request_headers.update(self.datastore.get_all_base_headers())
|
||||||
request_headers.update(self.datastore.get_all_headers_in_textfile_for_watch(uuid=self.watch.get('uuid')))
|
request_headers.update(self.datastore.get_all_headers_in_textfile_for_watch(uuid=self.watch.get('uuid')))
|
||||||
|
|
||||||
|
|||||||
BIN
changedetectionio/static/images/gradient-border.png
Normal file
BIN
changedetectionio/static/images/gradient-border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -26,8 +26,7 @@ $(document).ready(function () {
|
|||||||
set_scale();
|
set_scale();
|
||||||
});
|
});
|
||||||
// Should always be disabled
|
// Should always be disabled
|
||||||
$('#browser_steps-0-operation option[value="Goto site"]').prop("selected", "selected");
|
$('#browser_steps >li:first-child select').val('Goto site').attr('disabled', 'disabled');
|
||||||
$('#browser_steps-0-operation').attr('disabled', 'disabled');
|
|
||||||
|
|
||||||
$('#browsersteps-click-start').click(function () {
|
$('#browsersteps-click-start').click(function () {
|
||||||
$("#browsersteps-click-start").fadeOut();
|
$("#browsersteps-click-start").fadeOut();
|
||||||
|
|||||||
@@ -8,13 +8,6 @@ $(document).ready(function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$('.needs-localtime').each(function () {
|
|
||||||
for (var option of this.options) {
|
|
||||||
var dateObject = new Date(option.value * 1000);
|
|
||||||
option.label = dateObject.toLocaleString(undefined, {dateStyle: "full", timeStyle: "medium"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
@@ -79,7 +79,12 @@ $(document).ready(function () {
|
|||||||
$('#jump-next-diff').click();
|
$('#jump-next-diff').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('.needs-localtime').each(function () {
|
||||||
|
for (var option of this.options) {
|
||||||
|
var dateObject = new Date(option.value * 1000);
|
||||||
|
option.label = dateObject.toLocaleString(undefined, {dateStyle: "full", timeStyle: "medium"});
|
||||||
|
}
|
||||||
|
})
|
||||||
onDiffTypeChange(
|
onDiffTypeChange(
|
||||||
document.querySelector('#settings [name="diff_type"]:checked'),
|
document.querySelector('#settings [name="diff_type"]:checked'),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
function redirect_to_version(version) {
|
|
||||||
var currentUrl = window.location.href;
|
|
||||||
var baseUrl = currentUrl.split('?')[0]; // Base URL without query parameters
|
|
||||||
var anchor = '';
|
|
||||||
|
|
||||||
// Check if there is an anchor
|
|
||||||
if (baseUrl.indexOf('#') !== -1) {
|
|
||||||
anchor = baseUrl.substring(baseUrl.indexOf('#'));
|
|
||||||
baseUrl = baseUrl.substring(0, baseUrl.indexOf('#'));
|
|
||||||
}
|
|
||||||
window.location.href = baseUrl + '?version=' + version + anchor;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function (event) {
|
|
||||||
var selectElement = document.getElementById('preview-version');
|
|
||||||
if (selectElement) {
|
|
||||||
var selectedOption = selectElement.querySelector('option:checked');
|
|
||||||
if (selectedOption) {
|
|
||||||
if (event.key === 'ArrowLeft') {
|
|
||||||
if (selectedOption.previousElementSibling) {
|
|
||||||
redirect_to_version(selectedOption.previousElementSibling.value);
|
|
||||||
}
|
|
||||||
} else if (event.key === 'ArrowRight') {
|
|
||||||
if (selectedOption.nextElementSibling) {
|
|
||||||
redirect_to_version(selectedOption.nextElementSibling.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('preview-version').addEventListener('change', function () {
|
|
||||||
redirect_to_version(this.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
var selectElement = document.getElementById('preview-version');
|
|
||||||
if (selectElement) {
|
|
||||||
var selectedOption = selectElement.querySelector('option:checked');
|
|
||||||
if (selectedOption) {
|
|
||||||
if (selectedOption.previousElementSibling) {
|
|
||||||
document.getElementById('btn-previous').href = "?version=" + selectedOption.previousElementSibling.value;
|
|
||||||
} else {
|
|
||||||
document.getElementById('btn-previous').remove()
|
|
||||||
}
|
|
||||||
if (selectedOption.nextElementSibling) {
|
|
||||||
document.getElementById('btn-next').href = "?version=" + selectedOption.nextElementSibling.value;
|
|
||||||
} else {
|
|
||||||
document.getElementById('btn-next').remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -243,6 +243,7 @@ body::after {
|
|||||||
body::before {
|
body::before {
|
||||||
// background-image set in base.html so it works with reverse proxies etc
|
// background-image set in base.html so it works with reverse proxies etc
|
||||||
content: "";
|
content: "";
|
||||||
|
background-size: cover
|
||||||
}
|
}
|
||||||
|
|
||||||
body:after,
|
body:after,
|
||||||
@@ -1021,11 +1022,6 @@ ul {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
display: none;
|
display: none;
|
||||||
button {
|
|
||||||
/* some space if they wrap the page */
|
|
||||||
margin-bottom: 3px;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-uuid {
|
.checkbox-uuid {
|
||||||
@@ -1087,9 +1083,6 @@ ul {
|
|||||||
li {
|
li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
> * {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -574,7 +574,8 @@ body::after {
|
|||||||
opacity: 0.91; }
|
opacity: 0.91; }
|
||||||
|
|
||||||
body::before {
|
body::before {
|
||||||
content: ""; }
|
content: "";
|
||||||
|
background-size: cover; }
|
||||||
|
|
||||||
body:after,
|
body:after,
|
||||||
body:before {
|
body:before {
|
||||||
@@ -1127,10 +1128,6 @@ ul {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
display: none; }
|
display: none; }
|
||||||
#checkbox-operations button {
|
|
||||||
/* some space if they wrap the page */
|
|
||||||
margin-bottom: 3px;
|
|
||||||
margin-top: 3px; }
|
|
||||||
|
|
||||||
.checkbox-uuid > * {
|
.checkbox-uuid > * {
|
||||||
vertical-align: middle; }
|
vertical-align: middle; }
|
||||||
@@ -1176,8 +1173,6 @@ ul {
|
|||||||
#quick-watch-processor-type ul li {
|
#quick-watch-processor-type ul li {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
font-size: 0.8rem; }
|
font-size: 0.8rem; }
|
||||||
#quick-watch-processor-type ul li > * {
|
|
||||||
display: inline-block; }
|
|
||||||
|
|
||||||
.restock-label {
|
.restock-label {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
|||||||
@@ -124,12 +124,12 @@ class ChangeDetectionStore:
|
|||||||
self.__data['app_guid'] = str(uuid_builder.uuid4())
|
self.__data['app_guid'] = str(uuid_builder.uuid4())
|
||||||
|
|
||||||
# Generate the URL access token for RSS feeds
|
# Generate the URL access token for RSS feeds
|
||||||
if not self.__data['settings']['application'].get('rss_access_token'):
|
if not 'rss_access_token' in self.__data['settings']['application']:
|
||||||
secret = secrets.token_hex(16)
|
secret = secrets.token_hex(16)
|
||||||
self.__data['settings']['application']['rss_access_token'] = secret
|
self.__data['settings']['application']['rss_access_token'] = secret
|
||||||
|
|
||||||
# Generate the API access token
|
# Generate the API access token
|
||||||
if not self.__data['settings']['application'].get('api_access_token'):
|
if not 'api_access_token' in self.__data['settings']['application']:
|
||||||
secret = secrets.token_hex(16)
|
secret = secrets.token_hex(16)
|
||||||
self.__data['settings']['application']['api_access_token'] = secret
|
self.__data['settings']['application']['api_access_token'] = secret
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ class ChangeDetectionStore:
|
|||||||
@property
|
@property
|
||||||
def has_unviewed(self):
|
def has_unviewed(self):
|
||||||
for uuid, watch in self.__data['watching'].items():
|
for uuid, watch in self.__data['watching'].items():
|
||||||
if watch.history_n >= 2 and watch.viewed == False:
|
if watch.viewed == False:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -554,6 +554,7 @@ class ChangeDetectionStore:
|
|||||||
return os.path.isfile(filepath)
|
return os.path.isfile(filepath)
|
||||||
|
|
||||||
def get_all_base_headers(self):
|
def get_all_base_headers(self):
|
||||||
|
from .model.App import parse_headers_from_text_file
|
||||||
headers = {}
|
headers = {}
|
||||||
# Global app settings
|
# Global app settings
|
||||||
headers.update(self.data['settings'].get('headers', {}))
|
headers.update(self.data['settings'].get('headers', {}))
|
||||||
|
|||||||
@@ -6,9 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" >
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" >
|
||||||
<meta name="description" content="Self hosted website change detection." >
|
<meta name="description" content="Self hosted website change detection." >
|
||||||
<title>Change Detection{{extra_title}}</title>
|
<title>Change Detection{{extra_title}}</title>
|
||||||
{% if app_rss_token %}
|
<link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag %}- {{active_tag}}{% endif %}" href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}" >
|
||||||
<link rel="alternate" type="application/rss+xml" title="Changedetection.io » Feed{% if active_tag_uuid %}- {{active_tag.title}}{% endif %}" href="{{ url_for('rss', tag=active_tag_uuid , token=app_rss_token)}}" >
|
|
||||||
{% endif %}
|
|
||||||
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}" >
|
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='pure-min.css')}}" >
|
||||||
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}?v={{ get_css_version() }}" >
|
<link rel="stylesheet" href="{{url_for('static_content', group='styles', filename='styles.css')}}?v={{ get_css_version() }}" >
|
||||||
{% if extra_stylesheets %}
|
{% if extra_stylesheets %}
|
||||||
@@ -26,6 +24,12 @@
|
|||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<meta name="msapplication-config" content="favicons/browserconfig.xml">
|
<meta name="msapplication-config" content="favicons/browserconfig.xml">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body::before {
|
||||||
|
background-image: url({{url_for('static_content', group='images', filename='gradient-border.png') }});
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -85,8 +89,8 @@
|
|||||||
<li class="pure-menu-item pure-form" id="search-menu-item">
|
<li class="pure-menu-item pure-form" id="search-menu-item">
|
||||||
<!-- We use GET here so it offers people a chance to set bookmarks etc -->
|
<!-- We use GET here so it offers people a chance to set bookmarks etc -->
|
||||||
<form name="searchForm" action="" method="GET">
|
<form name="searchForm" action="" method="GET">
|
||||||
<input id="search-q" class="" name="q" placeholder="URL or Title {% if active_tag_uuid %}in '{{ active_tag.title }}'{% endif %}" required="" type="text" value="">
|
<input id="search-q" class="" name="q" placeholder="URL or Title {% if active_tag %}in '{{ active_tag }}'{% endif %}" required="" type="text" value="">
|
||||||
<input name="tags" type="hidden" value="{% if active_tag_uuid %}{{active_tag_uuid}}{% endif %}">
|
<input name="tags" type="hidden" value="{% if active_tag %}{{active_tag}}{% endif %}">
|
||||||
<button class="toggle-button " id="toggle-search" type="button" title="Search, or Use Alt+S Key" >
|
<button class="toggle-button " id="toggle-search" type="button" title="Search, or Use Alt+S Key" >
|
||||||
{% include "svgs/search-icon.svg" %}
|
{% include "svgs/search-icon.svg" %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -433,8 +433,7 @@ Unavailable") }}
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{% if visualselector_enabled %}
|
{% if visualselector_enabled %}
|
||||||
<span class="pure-form-message-inline">
|
<span class="pure-form-message-inline">
|
||||||
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection ‐ after the <i>Browser Steps</i> has completed.<br>
|
The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection ‐ after the <i>Browser Steps</i> has completed, this tool is a helper to manage filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab.
|
||||||
This tool is a helper to manage filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab.
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div id="selector-header">
|
<div id="selector-header">
|
||||||
|
|||||||
@@ -1,103 +1,72 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>
|
<script>
|
||||||
const screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}";
|
||||||
{% 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 %}
|
|
||||||
const highlight_submit_ignore_url = "{{url_for('highlight_submit_ignore_url', uuid=uuid)}}";
|
|
||||||
</script>
|
|
||||||
<script src="{{ url_for('static_content', group='js', filename='diff-overview.js') }}" defer></script>
|
|
||||||
<script src="{{ url_for('static_content', group='js', filename='preview.js') }}" defer></script>
|
|
||||||
<script src="{{ url_for('static_content', group='js', filename='tabs.js') }}" defer></script>
|
|
||||||
{% if versions|length >= 2 %}
|
|
||||||
<div id="settings" style="text-align: center;">
|
|
||||||
<form class="pure-form " action="" method="POST">
|
|
||||||
<fieldset>
|
|
||||||
<label for="preview-version">Select timestamp</label> <select id="preview-version"
|
|
||||||
name="from_version"
|
|
||||||
class="needs-localtime">
|
|
||||||
{% for version in versions|reverse %}
|
|
||||||
<option value="{{ version }}" {% if version == current_version %} selected="" {% endif %}>
|
|
||||||
{{ version }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="pure-button pure-button-primary">Go</button>
|
|
||||||
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
<br>
|
|
||||||
<strong>Keyboard: </strong><a href="" class="pure-button pure-button-primary" id="btn-previous">
|
|
||||||
← Previous</a> <a class="pure-button pure-button-primary" id="btn-next" href="">
|
|
||||||
→ Next</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
const highlight_submit_ignore_url="{{url_for('highlight_submit_ignore_url', uuid=uuid)}}";
|
||||||
|
</script>
|
||||||
|
<script src="{{url_for('static_content', group='js', filename='diff-overview.js')}}" defer></script>
|
||||||
|
|
||||||
<div class="tabs">
|
<script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script>
|
||||||
<ul>
|
<div class="tabs">
|
||||||
{% if last_error_text %}
|
<ul>
|
||||||
<li class="tab" id="error-text-tab"><a href="#error-text">Error Text</a></li> {% endif %}
|
{% if last_error_text %}<li class="tab" id="error-text-tab"><a href="#error-text">Error Text</a></li> {% endif %}
|
||||||
{% if last_error_screenshot %}
|
{% if last_error_screenshot %}<li class="tab" id="error-screenshot-tab"><a href="#error-screenshot">Error Screenshot</a></li> {% endif %}
|
||||||
<li class="tab" id="error-screenshot-tab"><a href="#error-screenshot">Error Screenshot</a>
|
{% if history_n > 0 %}
|
||||||
</li> {% endif %}
|
<li class="tab" id="text-tab"><a href="#text">Text</a></li>
|
||||||
{% if history_n > 0 %}
|
<li class="tab" id="screenshot-tab"><a href="#screenshot">Screenshot</a></li>
|
||||||
<li class="tab" id="text-tab"><a href="#text">Text</a></li>
|
{% endif %}
|
||||||
<li class="tab" id="screenshot-tab"><a href="#screenshot">Screenshot</a></li>
|
</ul>
|
||||||
{% endif %}
|
</div>
|
||||||
</ul>
|
<form><input type="hidden" name="csrf_token" value="{{ csrf_token() }}"></form>
|
||||||
</div>
|
<div id="diff-ui">
|
||||||
|
<div class="tab-pane-inner" id="error-text">
|
||||||
|
<div class="snapshot-age error">{{watch.error_text_ctime|format_seconds_ago}} seconds ago</div>
|
||||||
<div id="diff-ui">
|
<pre>
|
||||||
<div class="tab-pane-inner" id="error-text">
|
|
||||||
<div class="snapshot-age error">{{ watch.error_text_ctime|format_seconds_ago }} seconds ago</div>
|
|
||||||
<pre>
|
|
||||||
{{ last_error_text }}
|
{{ last_error_text }}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="error-screenshot">
|
|
||||||
<div class="snapshot-age error">{{ watch.snapshot_error_screenshot_ctime|format_seconds_ago }} seconds ago
|
|
||||||
</div>
|
|
||||||
<img id="error-screenshot-img" style="max-width: 80%"
|
|
||||||
alt="Current erroring screenshot from most recent request">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="text">
|
|
||||||
<div class="snapshot-age">{{ current_version|format_timestamp_timeago }}</div>
|
|
||||||
<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>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td id="diff-col" class="highlightable-filter">
|
|
||||||
{% for row in content %}
|
|
||||||
<div class="{{ row.classes }}">{{ row.line }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="screenshot">
|
|
||||||
<div class="tip">
|
|
||||||
For now, Differences are performed on text, not graphically, only the latest screenshot is available.
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
{% if is_html_webdriver %}
|
|
||||||
{% if screenshot %}
|
|
||||||
<div class="snapshot-age">{{ watch.snapshot_screenshot_ctime|format_timestamp_timeago }}</div>
|
|
||||||
<img style="max-width: 80%" id="screenshot-img" alt="Current screenshot from most recent request">
|
|
||||||
{% else %}
|
|
||||||
No screenshot available just yet! Try rechecking the page.
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<strong>Screenshot requires Playwright/WebDriver enabled</strong>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane-inner" id="error-screenshot">
|
||||||
|
<div class="snapshot-age error">{{watch.snapshot_error_screenshot_ctime|format_seconds_ago}} seconds ago</div>
|
||||||
|
<img id="error-screenshot-img" style="max-width: 80%" alt="Current erroring screenshot from most recent request" >
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane-inner" id="text">
|
||||||
|
<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="tip"><strong>Pro-tip</strong>: Highlight text to add to ignore filters</span>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td id="diff-col" class="highlightable-filter">
|
||||||
|
{% for row in content %}
|
||||||
|
<div class="{{row.classes}}">{{row.line}}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane-inner" id="screenshot">
|
||||||
|
<div class="tip">
|
||||||
|
For now, Differences are performed on text, not graphically, only the latest screenshot is available.
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
{% if is_html_webdriver %}
|
||||||
|
{% if screenshot %}
|
||||||
|
<div class="snapshot-age">{{watch.snapshot_screenshot_ctime|format_timestamp_timeago}}</div>
|
||||||
|
<img style="max-width: 80%" id="screenshot-img" alt="Current screenshot from most recent request" >
|
||||||
|
{% else %}
|
||||||
|
No screenshot available just yet! Try rechecking the page.
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<strong>Screenshot requires Playwright/WebDriver enabled</strong>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -62,9 +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_checkbox_field(form.application.form.rss_hide_muted_watches) }}
|
|
||||||
</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>
|
||||||
@@ -111,6 +108,8 @@
|
|||||||
<p>Use the <strong>Basic</strong> method (default) where your watched sites don't need Javascript to render.</p>
|
<p>Use the <strong>Basic</strong> method (default) where your watched sites don't need Javascript to render.</p>
|
||||||
<p>The <strong>Chrome/Javascript</strong> method requires a network connection to a running WebDriver+Chrome server, set by the ENV var 'WEBDRIVER_URL'. </p>
|
<p>The <strong>Chrome/Javascript</strong> method requires a network connection to a running WebDriver+Chrome server, set by the ENV var 'WEBDRIVER_URL'. </p>
|
||||||
</span>
|
</span>
|
||||||
|
<br>
|
||||||
|
Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a>
|
||||||
</div>
|
</div>
|
||||||
<fieldset class="pure-group" id="webdriver-override-options" data-visible-for="application-fetch_backend=html_webdriver">
|
<fieldset class="pure-group" id="webdriver-override-options" data-visible-for="application-fetch_backend=html_webdriver">
|
||||||
<div class="pure-form-message-inline">
|
<div class="pure-form-message-inline">
|
||||||
@@ -122,18 +121,6 @@
|
|||||||
{{ render_field(form.application.form.webdriver_delay) }}
|
{{ render_field(form.application.form.webdriver_delay) }}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="pure-control-group inline-radio">
|
|
||||||
{{ render_field(form.requests.form.default_ua) }}
|
|
||||||
<span class="pure-form-message-inline">
|
|
||||||
Applied to all requests.<br><br>
|
|
||||||
Note: Simply changing the User-Agent often does not defeat anti-robot technologies, it's important to consider <a href="https://changedetection.io/tutorial/what-are-main-types-anti-robot-mechanisms">all of the ways that the browser is detected</a>.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<br>
|
|
||||||
Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane-inner" id="filters">
|
<div class="tab-pane-inner" id="filters">
|
||||||
@@ -203,7 +190,7 @@ nav
|
|||||||
<a id="chrome-extension-link"
|
<a id="chrome-extension-link"
|
||||||
title="Try our new Chrome Extension!"
|
title="Try our new Chrome Extension!"
|
||||||
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
|
href="https://chromewebstore.google.com/detail/changedetectionio-website/kefcfmgmlhmankjmnbijimhofdjekbop">
|
||||||
<img src="{{ url_for('static_content', group='images', filename='Google-Chrome-icon.png') }}" alt="Chrome">
|
<img src="{{ url_for('static_content', group='images', filename='Google-Chrome-icon.png') }}">
|
||||||
Chrome Webstore
|
Chrome Webstore
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div id="watch-add-wrapper-zone">
|
<div id="watch-add-wrapper-zone">
|
||||||
|
|
||||||
{{ render_nolabel_field(form.url, placeholder="https://...", required=true) }}
|
{{ render_nolabel_field(form.url, placeholder="https://...", required=true) }}
|
||||||
{{ render_nolabel_field(form.tags, value=active_tag.title if active_tag_uuid else '', placeholder="watch label / tag") }}
|
{{ render_nolabel_field(form.tags, value=active_tag.title if active_tag else '', placeholder="watch label / tag") }}
|
||||||
{{ render_nolabel_field(form.watch_submit_button, title="Watch this URL!" ) }}
|
{{ render_nolabel_field(form.watch_submit_button, title="Watch this URL!" ) }}
|
||||||
{{ render_nolabel_field(form.edit_and_watch_submit_button, title="Edit first then Watch") }}
|
{{ render_nolabel_field(form.edit_and_watch_submit_button, title="Edit first then Watch") }}
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if search_q %}<div id="search-result-info">Searching "<strong><i>{{search_q}}</i></strong>"</div>{% endif %}
|
{% if search_q %}<div id="search-result-info">Searching "<strong><i>{{search_q}}</i></strong>"</div>{% endif %}
|
||||||
<div>
|
<div>
|
||||||
<a href="{{url_for('index')}}" class="pure-button button-tag {{'active' if not active_tag_uuid }}">All</a>
|
<a href="{{url_for('index')}}" class="pure-button button-tag {{'active' if not active_tag }}">All</a>
|
||||||
|
|
||||||
<!-- tag list -->
|
<!-- tag list -->
|
||||||
{% for uuid, tag in tags %}
|
{% for uuid, tag in tags %}
|
||||||
@@ -67,18 +67,18 @@
|
|||||||
<tr>
|
<tr>
|
||||||
{% set link_order = "desc" if sort_order == 'asc' else "asc" %}
|
{% set link_order = "desc" if sort_order == 'asc' else "asc" %}
|
||||||
{% set arrow_span = "" %}
|
{% set arrow_span = "" %}
|
||||||
<th><input style="vertical-align: middle" type="checkbox" id="check-all" > <a class="{{ 'active '+link_order if sort_attribute == 'date_created' else 'inactive' }}" href="{{url_for('index', sort='date_created', order=link_order, tag=active_tag_uuid)}}"># <span class='arrow {{link_order}}'></span></a></th>
|
<th><input style="vertical-align: middle" type="checkbox" id="check-all" > <a class="{{ 'active '+link_order if sort_attribute == 'date_created' else 'inactive' }}" href="{{url_for('index', sort='date_created', order=link_order, tag=active_tag)}}"># <span class='arrow {{link_order}}'></span></a></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'label' else 'inactive' }}" href="{{url_for('index', sort='label', order=link_order, tag=active_tag_uuid)}}">Website <span class='arrow {{link_order}}'></span></a></th>
|
<th><a class="{{ 'active '+link_order if sort_attribute == 'label' else 'inactive' }}" href="{{url_for('index', sort='label', order=link_order, tag=active_tag)}}">Website <span class='arrow {{link_order}}'></span></a></th>
|
||||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_checked' else 'inactive' }}" href="{{url_for('index', sort='last_checked', order=link_order, tag=active_tag_uuid)}}">Last Checked <span class='arrow {{link_order}}'></span></a></th>
|
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_checked' else 'inactive' }}" href="{{url_for('index', sort='last_checked', order=link_order, tag=active_tag)}}">Last Checked <span class='arrow {{link_order}}'></span></a></th>
|
||||||
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_changed' else 'inactive' }}" href="{{url_for('index', sort='last_changed', order=link_order, tag=active_tag_uuid)}}">Last Changed <span class='arrow {{link_order}}'></span></a></th>
|
<th><a class="{{ 'active '+link_order if sort_attribute == 'last_changed' else 'inactive' }}" href="{{url_for('index', sort='last_changed', order=link_order, tag=active_tag)}}">Last Changed <span class='arrow {{link_order}}'></span></a></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if not watches|length %}
|
{% if not watches|length %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" style="text-wrap: wrap;">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('import_page')}}" >import a list</a>.</td>
|
<td colspan="6">No website watches configured, please add a URL in the box above, or <a href="{{ url_for('import_page')}}" >import a list</a>.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
{% for watch in (watches|sort(attribute=sort_attribute, reverse=sort_order == 'asc'))|pagination_slice(skip=pagination.skip) %}
|
||||||
@@ -95,11 +95,11 @@
|
|||||||
<td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} " > <span>{{ loop.index+pagination.skip }}</span></td>
|
<td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} " > <span>{{ loop.index+pagination.skip }}</span></td>
|
||||||
<td class="inline watch-controls">
|
<td class="inline watch-controls">
|
||||||
{% if not watch.paused %}
|
{% if not watch.paused %}
|
||||||
<a class="state-off" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause" ></a>
|
<a class="state-off" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause" ></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
|
<a class="state-on" href="{{url_for('index', op='pause', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('index', op='mute', uuid=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notifications" title="Mute notifications" class="icon icon-mute" ></a>
|
||||||
</td>
|
</td>
|
||||||
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
||||||
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
all {% if active_tag_uuid %} in "{{active_tag.title}}"{%endif%}</a>
|
all {% if active_tag_uuid %} in "{{active_tag.title}}"{%endif%}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('rss', tag=active_tag_uuid, token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a>
|
<a href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{{ pagination.links }}
|
{{ pagination.links }}
|
||||||
|
|||||||
@@ -1,51 +1,42 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
import asyncio
|
import smtpd
|
||||||
from aiosmtpd.controller import Controller
|
import asyncore
|
||||||
from aiosmtpd.smtp import SMTP
|
|
||||||
|
|
||||||
# Accept a SMTP message and offer a way to retrieve the last message via TCP Socket
|
# Accept a SMTP message and offer a way to retrieve the last message via TCP Socket
|
||||||
|
|
||||||
last_received_message = b"Nothing"
|
last_received_message = b"Nothing"
|
||||||
|
|
||||||
|
|
||||||
class CustomSMTPHandler:
|
class CustomSMTPServer(smtpd.SMTPServer):
|
||||||
async def handle_DATA(self, server, session, envelope):
|
|
||||||
|
def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
|
||||||
global last_received_message
|
global last_received_message
|
||||||
last_received_message = envelope.content
|
last_received_message = data
|
||||||
print('Receiving message from:', session.peer)
|
print('Receiving message from:', peer)
|
||||||
print('Message addressed from:', envelope.mail_from)
|
print('Message addressed from:', mailfrom)
|
||||||
print('Message addressed to :', envelope.rcpt_tos)
|
print('Message addressed to :', rcpttos)
|
||||||
print('Message length :', len(envelope.content))
|
print('Message length :', len(data))
|
||||||
print(envelope.content.decode('utf8'))
|
print(data.decode('utf8'))
|
||||||
return '250 Message accepted for delivery'
|
return
|
||||||
|
|
||||||
|
|
||||||
class EchoServerProtocol(asyncio.Protocol):
|
# Just print out the last message received on plain TCP socket server
|
||||||
def connection_made(self, transport):
|
class EchoServer(asyncore.dispatcher):
|
||||||
|
|
||||||
|
def __init__(self, host, port):
|
||||||
|
asyncore.dispatcher.__init__(self)
|
||||||
|
self.create_socket()
|
||||||
|
self.set_reuse_addr()
|
||||||
|
self.bind((host, port))
|
||||||
|
self.listen(5)
|
||||||
|
|
||||||
|
def handle_accepted(self, sock, addr):
|
||||||
global last_received_message
|
global last_received_message
|
||||||
self.transport = transport
|
print('Incoming connection from %s' % repr(addr))
|
||||||
peername = transport.get_extra_info('peername')
|
sock.send(last_received_message)
|
||||||
print('Incoming connection from {}'.format(peername))
|
|
||||||
self.transport.write(last_received_message)
|
|
||||||
|
|
||||||
last_received_message = b''
|
last_received_message = b''
|
||||||
self.transport.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
server = CustomSMTPServer(('0.0.0.0', 11025), None) # SMTP mail goes here
|
||||||
# Start the SMTP server
|
server2 = EchoServer('0.0.0.0', 11080) # Echo back last message received
|
||||||
controller = Controller(CustomSMTPHandler(), hostname='0.0.0.0', port=11025)
|
asyncore.loop()
|
||||||
controller.start()
|
|
||||||
|
|
||||||
# Start the TCP Echo server
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
server = await loop.create_server(
|
|
||||||
lambda: EchoServerProtocol(),
|
|
||||||
'0.0.0.0', 11080
|
|
||||||
)
|
|
||||||
async with server:
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ def get_last_message_from_smtp_server():
|
|||||||
client_socket.connect((smtp_test_server, port)) # connect to the server
|
client_socket.connect((smtp_test_server, port)) # connect to the server
|
||||||
|
|
||||||
data = client_socket.recv(50024).decode() # receive response
|
data = client_socket.recv(50024).decode() # receive response
|
||||||
logging.info("get_last_message_from_smtp_server..")
|
|
||||||
logging.info(data)
|
|
||||||
client_socket.close() # close the connection
|
client_socket.close() # close the connection
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -73,8 +71,6 @@ def test_check_notification_email_formats_default_HTML(client, live_server):
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
set_longer_modified_response()
|
set_longer_modified_response()
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
@@ -85,7 +81,7 @@ def test_check_notification_email_formats_default_HTML(client, live_server):
|
|||||||
|
|
||||||
# The email should have two bodies, and the text/html part should be <br>
|
# The email should have two bodies, and the text/html part should be <br>
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n
|
assert '(added) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
||||||
assert 'Content-Type: text/html' in msg
|
assert 'Content-Type: text/html' in msg
|
||||||
assert '(added) So let\'s see what happens.<br>' in msg # the html part
|
assert '(added) So let\'s see what happens.<br>' in msg # the html part
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
@@ -139,7 +135,6 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
set_longer_modified_response()
|
set_longer_modified_response()
|
||||||
time.sleep(2)
|
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
@@ -152,7 +147,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
# The email should not have two bodies, should be TEXT only
|
# The email should not have two bodies, should be TEXT only
|
||||||
|
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n
|
assert '(added) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
||||||
|
|
||||||
set_original_response()
|
set_original_response()
|
||||||
# Now override as HTML format
|
# Now override as HTML format
|
||||||
@@ -173,7 +168,7 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv
|
|||||||
|
|
||||||
# The email should have two bodies, and the text/html part should be <br>
|
# The email should have two bodies, and the text/html part should be <br>
|
||||||
assert 'Content-Type: text/plain' in msg
|
assert 'Content-Type: text/plain' in msg
|
||||||
assert '(removed) So let\'s see what happens.\r\n' in msg # The plaintext part with \n
|
assert '(removed) So let\'s see what happens.\n' in msg # The plaintext part with \n
|
||||||
assert 'Content-Type: text/html' in msg
|
assert 'Content-Type: text/html' in msg
|
||||||
assert '(removed) So let\'s see what happens.<br>' in msg # the html part
|
assert '(removed) So let\'s see what happens.<br>' in msg # the html part
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ def test_check_ldjson_price_autodetect(client, live_server):
|
|||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
# Trigger a check
|
# Trigger a check
|
||||||
time.sleep(1)
|
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
# Offer should be gone
|
# Offer should be gone
|
||||||
|
|||||||
@@ -135,9 +135,6 @@ def test_check_basic_change_detection_functionality(client, live_server):
|
|||||||
# It should have picked up the <title>
|
# It should have picked up the <title>
|
||||||
assert b'head title' in res.data
|
assert b'head title' in res.data
|
||||||
|
|
||||||
# Be sure the last_viewed is going to be greater than the last snapshot
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# hit the mark all viewed link
|
# hit the mark all viewed link
|
||||||
res = client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
res = client.get(url_for("mark_all_viewed"), follow_redirects=True)
|
||||||
|
|
||||||
|
|||||||
@@ -479,9 +479,8 @@ def test_correct_header_detect(client, live_server):
|
|||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid="first"),
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
assert b'"world":' in res.data
|
||||||
assert b'"hello": 123,' in res.data
|
assert res.data.count(b'{') >= 2
|
||||||
assert b'"world": 123</div>' in res.data
|
|
||||||
|
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ def test_check_notification_error_handling(client, live_server):
|
|||||||
live_server_setup(live_server)
|
live_server_setup(live_server)
|
||||||
set_original_response()
|
set_original_response()
|
||||||
|
|
||||||
|
# Give the endpoint time to spin up
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# Set a URL and fetch it, then set a notification URL which is going to give errors
|
# Set a URL and fetch it, then set a notification URL which is going to give errors
|
||||||
test_url = url_for('test_endpoint', _external=True)
|
test_url = url_for('test_endpoint', _external=True)
|
||||||
res = client.post(
|
res = client.post(
|
||||||
|
|||||||
@@ -253,99 +253,15 @@ def test_method_in_request(client, live_server):
|
|||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
|
|
||||||
# Re #2408 - user-agent override test, also should handle case-insensitive header deduplication
|
|
||||||
def test_ua_global_override(client, live_server):
|
|
||||||
# live_server_setup(live_server)
|
|
||||||
test_url = url_for('test_headers', _external=True)
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("settings_page"),
|
|
||||||
data={
|
|
||||||
"application-fetch_backend": "html_requests",
|
|
||||||
"application-minutes_between_check": 180,
|
|
||||||
"requests-default_ua-html_requests": "html-requests-user-agent"
|
|
||||||
},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b'Settings updated' in res.data
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("import_page"),
|
|
||||||
data={"urls": test_url},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"1 Imported" in res.data
|
|
||||||
|
|
||||||
wait_for_all_checks(client)
|
|
||||||
res = client.get(
|
|
||||||
url_for("preview_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert b"html-requests-user-agent" in res.data
|
|
||||||
# default user-agent should have shown by now
|
|
||||||
# now add a custom one in the headers
|
|
||||||
|
|
||||||
|
|
||||||
# Add some headers to a request
|
|
||||||
res = client.post(
|
|
||||||
url_for("edit_page", uuid="first"),
|
|
||||||
data={
|
|
||||||
"url": test_url,
|
|
||||||
"tags": "testtag",
|
|
||||||
"fetch_backend": 'html_requests',
|
|
||||||
# Important - also test case-insensitive
|
|
||||||
"headers": "User-AGent: agent-from-watch"},
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"Updated watch." in res.data
|
|
||||||
wait_for_all_checks(client)
|
|
||||||
res = client.get(
|
|
||||||
url_for("preview_page", uuid="first"),
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b"agent-from-watch" in res.data
|
|
||||||
assert b"html-requests-user-agent" not in res.data
|
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
|
||||||
assert b'Deleted' in res.data
|
|
||||||
|
|
||||||
def test_headers_textfile_in_request(client, live_server):
|
def test_headers_textfile_in_request(client, live_server):
|
||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
# Add our URL to the import page
|
# Add our URL to the import page
|
||||||
|
|
||||||
webdriver_ua = "Hello fancy webdriver UA 1.0"
|
|
||||||
requests_ua = "Hello basic requests UA 1.1"
|
|
||||||
|
|
||||||
test_url = url_for('test_headers', _external=True)
|
test_url = url_for('test_headers', _external=True)
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
||||||
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
# Because its no longer calling back to localhost but from the browser container, set in test-only.yml
|
||||||
test_url = test_url.replace('localhost', 'cdio')
|
test_url = test_url.replace('localhost', 'cdio')
|
||||||
|
|
||||||
form_data = {
|
print ("TEST URL IS ",test_url)
|
||||||
"application-fetch_backend": "html_requests",
|
|
||||||
"application-minutes_between_check": 180,
|
|
||||||
"requests-default_ua-html_requests": requests_ua
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
|
||||||
form_data["requests-default_ua-html_webdriver"] = webdriver_ua
|
|
||||||
|
|
||||||
res = client.post(
|
|
||||||
url_for("settings_page"),
|
|
||||||
data=form_data,
|
|
||||||
follow_redirects=True
|
|
||||||
)
|
|
||||||
assert b'Settings updated' in res.data
|
|
||||||
|
|
||||||
res = client.get(url_for("settings_page"))
|
|
||||||
|
|
||||||
# Only when some kind of real browser is setup
|
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
|
||||||
assert b'requests-default_ua-html_webdriver' in res.data
|
|
||||||
|
|
||||||
# Field should always be there
|
|
||||||
assert b"requests-default_ua-html_requests" in res.data
|
|
||||||
|
|
||||||
# Add the test URL twice, we will check
|
# Add the test URL twice, we will check
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("import_page"),
|
url_for("import_page"),
|
||||||
@@ -356,14 +272,15 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
|
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
|
||||||
# Add some headers to a request
|
# Add some headers to a request
|
||||||
res = client.post(
|
res = client.post(
|
||||||
url_for("edit_page", uuid="first"),
|
url_for("edit_page", uuid="first"),
|
||||||
data={
|
data={
|
||||||
"url": test_url,
|
"url": test_url,
|
||||||
"tags": "testtag",
|
"tags": "testtag",
|
||||||
"fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests',
|
"fetch_backend": 'html_webdriver' if os.getenv('PLAYWRIGHT_DRIVER_URL') else 'html_requests',
|
||||||
"headers": "xxx:ooo\ncool:yeah\r\n"},
|
"headers": "xxx:ooo\ncool:yeah\r\n"},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b"Updated watch." in res.data
|
assert b"Updated watch." in res.data
|
||||||
@@ -375,7 +292,7 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
with open('test-datastore/headers.txt', 'w') as f:
|
with open('test-datastore/headers.txt', 'w') as f:
|
||||||
f.write("global-header: nice\r\nnext-global-header: nice")
|
f.write("global-header: nice\r\nnext-global-header: nice")
|
||||||
|
|
||||||
with open('test-datastore/' + extract_UUID_from_client(client) + '/headers.txt', 'w') as f:
|
with open('test-datastore/'+extract_UUID_from_client(client)+'/headers.txt', 'w') as f:
|
||||||
f.write("watch-header: nice")
|
f.write("watch-header: nice")
|
||||||
|
|
||||||
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
client.get(url_for("form_watch_checknow"), follow_redirects=True)
|
||||||
@@ -389,7 +306,7 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
# Not needed anymore
|
# Not needed anymore
|
||||||
os.unlink('test-datastore/headers.txt')
|
os.unlink('test-datastore/headers.txt')
|
||||||
os.unlink('test-datastore/headers-testtag.txt')
|
os.unlink('test-datastore/headers-testtag.txt')
|
||||||
|
os.unlink('test-datastore/'+extract_UUID_from_client(client)+'/headers.txt')
|
||||||
# The service should echo back the request verb
|
# The service should echo back the request verb
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid="first"),
|
||||||
@@ -402,12 +319,7 @@ def test_headers_textfile_in_request(client, live_server):
|
|||||||
assert b"Watch-Header:nice" in res.data
|
assert b"Watch-Header:nice" in res.data
|
||||||
assert b"Tag-Header:test" in res.data
|
assert b"Tag-Header:test" in res.data
|
||||||
|
|
||||||
# Check the custom UA from system settings page made it through
|
|
||||||
if os.getenv('PLAYWRIGHT_DRIVER_URL'):
|
|
||||||
assert "User-Agent:".encode('utf-8') + webdriver_ua.encode('utf-8') in res.data
|
|
||||||
else:
|
|
||||||
assert "User-Agent:".encode('utf-8') + requests_ua.encode('utf-8') in res.data
|
|
||||||
|
|
||||||
# unlink headers.txt on start/stop
|
#unlink headers.txt on start/stop
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
||||||
assert b'Deleted' in res.data
|
assert b'Deleted' in res.data
|
||||||
@@ -102,9 +102,10 @@ def test_basic_browserstep(client, live_server):
|
|||||||
"url": test_url,
|
"url": test_url,
|
||||||
"tags": "",
|
"tags": "",
|
||||||
'fetch_backend': "html_webdriver",
|
'fetch_backend': "html_webdriver",
|
||||||
'browser_steps-0-operation': 'Click element',
|
'browser_steps-0-operation': 'Goto site',
|
||||||
'browser_steps-0-selector': 'button[name=test-button]',
|
'browser_steps-1-operation': 'Click element',
|
||||||
'browser_steps-0-optional_value': '',
|
'browser_steps-1-selector': 'button[name=test-button]',
|
||||||
|
'browser_steps-1-optional_value': '',
|
||||||
# For now, cookies doesnt work in headers because it must be a full cookiejar object
|
# For now, cookies doesnt work in headers because it must be a full cookiejar object
|
||||||
'headers': "testheader: yes\buser-agent: MyCustomAgent",
|
'headers': "testheader: yes\buser-agent: MyCustomAgent",
|
||||||
},
|
},
|
||||||
@@ -140,9 +141,10 @@ def test_basic_browserstep(client, live_server):
|
|||||||
"url": four_o_four_url,
|
"url": four_o_four_url,
|
||||||
"tags": "",
|
"tags": "",
|
||||||
'fetch_backend': "html_webdriver",
|
'fetch_backend': "html_webdriver",
|
||||||
'browser_steps-0-operation': 'Click element',
|
'browser_steps-0-operation': 'Goto site',
|
||||||
'browser_steps-0-selector': 'button[name=test-button]',
|
'browser_steps-1-operation': 'Click element',
|
||||||
'browser_steps-0-optional_value': ''
|
'browser_steps-1-selector': 'button[name=test-button]',
|
||||||
|
'browser_steps-1-optional_value': ''
|
||||||
},
|
},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Used by Pyppeteer
|
# Used by Pyppeteer
|
||||||
pyee
|
pyee
|
||||||
|
|
||||||
eventlet==0.35.2 # related to dnspython fixes
|
eventlet==0.33.3 # related to dnspython fixes
|
||||||
feedgen~=0.9
|
feedgen~=0.9
|
||||||
flask-compress
|
flask-compress
|
||||||
# 0.6.3 included compatibility fix for werkzeug 3.x (2.x had deprecation of url handlers)
|
# 0.6.3 included compatibility fix for werkzeug 3.x (2.x had deprecation of url handlers)
|
||||||
@@ -29,12 +29,14 @@ chardet>2.3.0
|
|||||||
wtforms~=3.0
|
wtforms~=3.0
|
||||||
jsonpath-ng~=1.5.3
|
jsonpath-ng~=1.5.3
|
||||||
|
|
||||||
dnspython==2.6.1
|
# Pinned: module 'eventlet.green.select' has no attribute 'epoll'
|
||||||
|
# https://github.com/eventlet/eventlet/issues/805#issuecomment-1640463482
|
||||||
|
dnspython==2.3.0 # related to eventlet fixes
|
||||||
|
|
||||||
# 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.8.0
|
apprise~=1.7.4
|
||||||
|
|
||||||
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
# apprise mqtt https://github.com/dgtlmoon/changedetection.io/issues/315
|
||||||
# and 2.0.0 https://github.com/dgtlmoon/changedetection.io/issues/2241 not yet compatible
|
# and 2.0.0 https://github.com/dgtlmoon/changedetection.io/issues/2241 not yet compatible
|
||||||
@@ -50,10 +52,7 @@ cryptography~=3.4
|
|||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
|
||||||
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
# XPath filtering, lxml is required by bs4 anyway, but put it here to be safe.
|
||||||
# #2328 - 5.2.0 and 5.2.1 had extra CPU flag CFLAGS set which was not compatible on older hardware
|
lxml >=4.8.0,<6
|
||||||
# It could be advantageous to run its own pypi package here with those performance flags set
|
|
||||||
# https://bugs.launchpad.net/lxml/+bug/2059910/comments/16
|
|
||||||
lxml >=4.8.0,<6,!=5.2.0,!=5.2.1
|
|
||||||
|
|
||||||
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
# XPath 2.0-3.1 support - 4.2.0 broke something?
|
||||||
elementpath==4.1.5
|
elementpath==4.1.5
|
||||||
@@ -71,10 +70,12 @@ openpyxl
|
|||||||
jq~=1.3; python_version >= "3.8" and sys_platform == "darwin"
|
jq~=1.3; python_version >= "3.8" and sys_platform == "darwin"
|
||||||
jq~=1.3; python_version >= "3.8" and sys_platform == "linux"
|
jq~=1.3; python_version >= "3.8" and sys_platform == "linux"
|
||||||
|
|
||||||
|
# Any current modern version, required so far for screenshot PNG->JPEG conversion but will be used more in the future
|
||||||
|
pillow
|
||||||
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
# playwright is installed at Dockerfile build time because it's not available on all platforms
|
||||||
|
|
||||||
|
# experimental release
|
||||||
pyppeteer-ng==2.0.0rc5
|
pyppeteer-ng==2.0.0rc5
|
||||||
pyppeteerstealth>=0.0.4
|
|
||||||
|
|
||||||
# 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
|
||||||
@@ -84,5 +85,3 @@ pytest-flask ~=1.2
|
|||||||
jsonschema==4.17.3
|
jsonschema==4.17.3
|
||||||
|
|
||||||
loguru
|
loguru
|
||||||
# Needed for > 3.10, https://github.com/microsoft/playwright-python/issues/2096
|
|
||||||
greenlet >= 3.0.3
|
|
||||||
Reference in New Issue
Block a user