Compare commits

...

45 Commits
0.29 ... 0.31

Author SHA1 Message Date
dgtlmoon
609a0a3aad Merge branch 'master' of github.com:dgtlmoon/changedetection.io 2021-06-03 10:51:18 +10:00
dgtlmoon
ad8065c072 Re #75 - Adding test to confirm watched URL appears in RSS feed 2021-06-03 10:50:59 +10:00
dgtlmoon
2346b42ef2 CSS selector filter (#73)
* Re #9 CSS Selector filtering,  Adding test for #9
2021-05-30 21:22:26 +10:00
dgtlmoon
1a0c3f1250 Fixing var name 2021-05-28 10:27:01 +10:00
dgtlmoon
91f69b92a2 Merge branch 'master' of github.com:dgtlmoon/changedetection.io 2021-05-28 10:20:53 +10:00
dgtlmoon
dd211d166c Include release metadata during github build 2021-05-28 10:20:42 +10:00
dgtlmoon
a6b0a23143 Update README.md 2021-05-28 00:03:17 +10:00
dgtlmoon
a03e53d826 Re #40 Ability to set individual timers (#72)
* Re #40 Ability to set individual timers
2021-05-27 23:55:05 +10:00
dgtlmoon
5d93009605 Update README.md 2021-05-27 20:37:56 +10:00
dgtlmoon
d4f3e744de Improvements for backup (#70)
* Remove previous backup files

* Backup - Add a text file containing only the URLs, with Windows+UNIX line-endings, for better portability.

* Fix filename on backup not being correct
2021-05-27 20:16:40 +10:00
dgtlmoon
13de31cf98 Update README.md 2021-05-26 21:26:35 +10:00
dgtlmoon
54ae82395a Disable image layer cache service 2021-05-25 16:46:13 +10:00
dgtlmoon
dba8944625 Re-enable ARM v6/v7 builds 2021-05-25 16:08:01 +10:00
dgtlmoon
270343b276 Install requirements, remove rust and dev packages that are no longer needed, hopefully for a smaller docker layer size 2021-05-25 15:06:35 +10:00
dgtlmoon
f3ce9b732c Remove rust build comments 2021-05-25 15:05:36 +10:00
dgtlmoon
baaee30499 Arm build fixes (#68)
* Add rustc compiler and remove when not needed (smaller docker layer)

* Using the magical ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1 to get around ARM issues
2021-05-25 15:04:33 +10:00
dgtlmoon
d50ff0b31c Re #65 - Append BASE_URL env var to the notification if it is set (#66)
* Re #65 - Append BASE_URL env var to the notification if it is set
2021-05-21 09:16:19 +10:00
dgtlmoon
395a6fca62 Update README.md 2021-05-19 13:09:41 +10:00
dgtlmoon
f582810ad0 Adding BTC support instructions 2021-05-18 23:34:56 +10:00
dgtlmoon
18b71edd6d Switch to just amd64 for now due to apprise not building on ARM 2021-05-15 21:23:05 +10:00
dgtlmoon
28f6af9153 Fixing syntax 2021-05-15 18:20:34 +10:00
dgtlmoon
63a3492547 Re #49 Re #60 - Adding more information about proxy setup to README.md 2021-05-15 18:13:00 +10:00
Unpublished
454fc26341 Add socks proxy support (#60)
* Add socks proxy support

* Add proxy config to README
2021-05-15 18:05:58 +10:00
KibosJ
e5409f8d16 Created docker-compose file (#55)
* Created docker-compose file, Removed version tag as per latest compose specification
2021-05-15 11:48:38 +10:00
dgtlmoon
1b736b3726 Re #58 - reduce to 1 minute (a small rewrite is required to change the backend to store in 'seconds' instead of minutes) 2021-05-13 22:33:33 +10:00
dgtlmoon
96f2b0d248 Merge branch 'master' of github.com:dgtlmoon/changedetection.io 2021-05-13 22:24:45 +10:00
dgtlmoon
308527f45e 56 - Fix notification test 2021-05-13 22:23:49 +10:00
dgtlmoon
70d766b647 Update README.md 2021-05-08 23:16:16 +10:00
dgtlmoon
40be9c615f Update README.md 2021-05-08 23:15:50 +10:00
dgtlmoon
f380754ff5 Adding rust compiler :( 2021-05-08 12:27:39 +10:00
dgtlmoon
bee6bd9fe0 trying without libssl and only libffi 2021-05-08 12:17:28 +10:00
dgtlmoon
fec2862ebe Adding extra libs required for build 2021-05-08 12:01:52 +10:00
dgtlmoon
969420e40b Cleanup docs 2021-05-08 11:44:43 +10:00
dgtlmoon
afba06dd1f Tweak workflow (tests) 2021-05-08 11:38:27 +10:00
dgtlmoon
1d66160e8c Security update 2021-05-08 11:33:46 +10:00
dgtlmoon
f877af75b9 Apprise notifications (#43)
* issue #4 Adding settings screen for apprise URLS
* Adding test notification mechanism

* Move Worker module to own class file

* Adding basic notification URL runner

* Tests for notifications

* Tweak readme with notification info

* Move notification test to main test_backend.py

* Fix spacing

* Adding notifications screenshot

* Cleanup more files from test

* Offer send notification test on individual edits and main/default

* Process global notifications

* All branches test

* Wrap worker notification process in try/catch, use global if nothing set

* Fix syntax

* Handle exception, increase wait time for liveserver to come up

* Fixing test setup

* remove debug

* Split tests into their own totally isolated setups, if you know a better way to make live_server() work, MR :)

* Tidying up lint/imports
2021-05-08 11:29:41 +10:00
dgtlmoon
b752690f89 Fixing security update 2021-05-08 10:19:49 +10:00
dgtlmoon
a10efa951b Also detect pytest in the environ (for local debug) 2021-05-03 11:20:11 +10:00
dgtlmoon
24a38f26f8 Prepend 'test-' when runnning under pytest to guid 2021-05-03 11:03:00 +10:00
dgtlmoon
1d0018dced - Relabel login button
- misc test cleanup
2021-05-01 11:55:24 +10:00
dgtlmoon
18c7a18be8 Re #46 - Add note to README.md about Javascript support 2021-05-01 10:02:43 +10:00
dgtlmoon
c11adcbe4a Bumping version 2021-05-01 01:20:56 +10:00
dgtlmoon
cd6ce89587 Re #45 - Set datastore path in app.config 2021-05-01 01:18:59 +10:00
dgtlmoon
4164ad29e3 Re #44 - Broke the menu by accident, adding tests and fixing. 2021-04-30 19:54:23 +10:00
dgtlmoon
4953e253e9 bump to 0.29 2021-04-30 17:17:23 +10:00
28 changed files with 781 additions and 196 deletions

View File

@@ -12,11 +12,6 @@
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '27 9 * * 4'

View File

@@ -2,7 +2,7 @@ name: Test, build and push to Docker Hub
on:
push:
branches: [ master ]
branches: [ master, arm-build ]
jobs:
build:
@@ -27,9 +27,17 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# 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
- name: Create release metadata
run: |
# COPY'ed by Dockerfile into backend/ of the image, then read by the server in store.py
echo ${{ github.sha }} > backend/source.txt
echo ${{ github.ref }} > backend/tag.txt
- name: Test with pytest
run: |
cd backend; pytest
# Each test is totally isolated and performs its own cleanup/reset
cd backend; ./run_all_tests.sh
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
@@ -49,6 +57,7 @@ jobs:
install: true
version: latest
driver-opts: image=moby/buildkit:master
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
@@ -60,18 +69,20 @@ jobs:
${{ secrets.DOCKER_HUB_USERNAME }}/changedetection.io:latest
# ${{ secrets.DOCKER_HUB_USERNAME }}:/changedetection.io:${{ env.RELEASE_VERSION }}
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
# platforms: linux/amd64
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
run: echo step SHA ${{ steps.vars.outputs.sha_short }} tag ${{steps.vars.outputs.tag}} branch ${{steps.vars.outputs.branch}} digest ${{ steps.docker_build.outputs.digest }}
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# failed: Cache service responded with 503
# - name: Cache Docker layers
# uses: actions/cache@v2
# with:
# path: /tmp/.buildx-cache
# key: ${{ runner.os }}-buildx-${{ github.sha }}
# restore-keys: |
# ${{ runner.os }}-buildx-

View File

@@ -1,10 +1,7 @@
name: Test only
name: ChangeDetection.io Test
on:
push:
branches:
- /refs/heads/*
- !master
# Triggers the workflow on push or pull request events
on: [push, pull_request]
jobs:
build:
@@ -31,5 +28,6 @@ jobs:
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
cd backend; pytest
# Each test is totally isolated and performs its own cleanup/reset
cd backend; ./run_all_tests.sh

View File

@@ -1,9 +1,13 @@
FROM python:3.8-slim
# rustc compiler would be needed on ARM type devices but theres an issue with some deps not building..
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
COPY requirements.txt /tmp/requirements.txt
RUN apt-get update && apt-get install -y gcc libc-dev libxslt-dev zlib1g-dev g++ --no-install-recommends && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
RUN pip3 install --no-cache-dir -r /tmp/requirements.txt
RUN apt-get update && apt-get install -y libssl-dev libffi-dev gcc libc-dev libxslt-dev zlib1g-dev rustc g++ --no-install-recommends && rm -rf /var/lib/apt/lists/* /var/cache/apt/*
# Update pip, install requirements, remove rust and dev packages that are no longer needed.
RUN pip3 install --upgrade pip && pip3 install --no-cache-dir -r /tmp/requirements.txt && apt-get remove rustc *-dev --purge -y
RUN [ ! -d "/app" ] && mkdir /app
RUN [ ! -d "/datastore" ] && mkdir /datastore
@@ -19,11 +23,6 @@ WORKDIR /app
# https://stackoverflow.com/questions/58701233/docker-logs-erroneously-appears-empty-until-container-stops
ENV PYTHONUNBUFFERED=1
# Attempt to store the triggered commit
ARG SOURCE_COMMIT
ARG SOURCE_BRANCH
RUN echo "commit: $SOURCE_COMMIT branch: $SOURCE_BRANCH" >/source.txt
CMD [ "python", "./changedetection.py" , "-d", "/datastore"]

View File

@@ -1,5 +1,5 @@
# changedetection.io
![changedetection.io](https://github.com/dgtlmoon/changedetection.io/actions/workflows/image.yml/badge.svg?branch=master)
![changedetection.io](https://github.com/dgtlmoon/changedetection.io/actions/workflows/test-only.yml/badge.svg?branch=master)
<a href="https://hub.docker.com/r/dgtlmoon/changedetection.io" target="_blank" title="Change detection docker hub">
<img src="https://img.shields.io/docker/pulls/dgtlmoon/changedetection.io" alt="Docker Pulls"/>
</a>
@@ -25,6 +25,7 @@ Know when ...
- New software releases, security advisories when you're not on their mailing list.
- Festivals with changes
- Realestate listing changes
- COVID related news from government websites
**Get monitoring now! super simple, one command!**
@@ -54,3 +55,61 @@ Examining differences in content.
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/screenshot-diff.png" style="max-width:100%;" alt="Self-hosted web page change monitoring context difference " title="Self-hosted web page change monitoring context difference " />
Please :star: star :star: this project and help it grow! https://github.com/dgtlmoon/changedetection.io/
### Notifications
ChangeDetection.io supports a massive amount of notifications (including email, office365, custom APIs, etc) when a web-page has a change detected thanks to the <a href="https://github.com/caronc/apprise">apprise</a> library.
Simply set one or more notification URL's in the _[edit]_ tab of that watch.
Just some examples
discord://webhook_id/webhook_token
flock://app_token/g:channel_id
gitter://token/room
gchat://workspace/key/token
msteams://TokenA/TokenB/TokenC/
o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
rocket://user:password@hostname/#Channel
mailto://user:pass@example.com?to=receivingAddress@example.com
json://someserver.com/custom-api
syslog://
<a href="https://github.com/caronc/apprise">And everything else in this list!</a>
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/screenshot-notifications.png" style="max-width:100%;" alt="Self-hosted web page change monitoring notifications" title="Self-hosted web page change monitoring notifications" />
### Proxy
A proxy for ChangeDectection.io can be configured by setting environment the
`HTTP_PROXY`, `HTTPS_PROXY` variables, examples are also in the `docker-compose.yml`
`NO_PROXY` exclude list can be specified by following `"localhost,192.168.0.0/24"`
as `docker run` with `-e`
```
docker run -d --restart always -e HTTPS_PROXY="socks5h://10.10.1.10:1080" -p "127.0.0.1:5000:5000" -v datastore-volume:/datastore --name changedetection.io dgtlmoon/changedetection.io
```
With `docker-compose`, see the `Proxy support example` in <a href="https://github.com/dgtlmoon/changedetection.io/blob/master/docker-compose.yml">docker-compose.yml</a>.
For more information see https://docs.python-requests.org/en/master/user/advanced/#proxies
This proxy support also extends to the notifications https://github.com/caronc/apprise/issues/387#issuecomment-841718867
### Notes
- Does not yet support Javascript
- Wont work with Cloudfare type "Please turn on javascript" protected pages
- You can use the 'headers' section to monitor password protected web page changes
### Support us
Do you use changedetection.io to make money? does it save you time or money? Does it make your life easier? less stressful? Remember, we write this software when we should be doing actual paid work, we have to buy food and pay rent just like you.
Please support us, even small amounts help a LOT.
BTC `1PLFN327GyUarpJd7nVe7Reqg9qHx5frNn`
<img src="https://raw.githubusercontent.com/dgtlmoon/changedetection.io/master/btc-support.png" style="max-width:50%;" alt="Support us!" />

View File

@@ -1 +0,0 @@
Note: run `pytest` from this directory.

View File

@@ -41,7 +41,9 @@ extra_stylesheets = []
update_q = queue.Queue()
app = Flask(__name__, static_url_path="/var/www/change-detection/backen/static")
notification_q = queue.Queue()
app = Flask(__name__, static_url_path="/var/www/change-detection/backend/static")
# Stop browser caching of assets
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
@@ -92,6 +94,7 @@ class User(flask_login.UserMixin):
def get_user(self, email="defaultuser@changedetection.io"):
return self
def is_authenticated(self):
return True
def is_active(self):
return True
@@ -331,6 +334,17 @@ def changedetection_app(conig=None, datastore_o=None):
url = request.form.get('url').strip()
tag = request.form.get('tag').strip()
minutes_recheck = request.form.get('minutes')
if minutes_recheck:
minutes = int(minutes_recheck.strip())
if minutes >= 1:
datastore.data['watching'][uuid]['minutes_between_check'] = minutes
else:
messages.append(
{'class': 'error', 'message': "Must be atleast 1 minute."})
# Extra headers
form_headers = request.form.get('headers').strip().split("\n")
extra_headers = {}
@@ -346,11 +360,22 @@ def changedetection_app(conig=None, datastore_o=None):
'headers': extra_headers
}
# Notification URLs
form_notification_text = request.form.get('notification_urls')
notification_urls = []
if form_notification_text:
for text in form_notification_text.strip().split("\n"):
text = text.strip()
if len(text):
notification_urls.append(text)
datastore.data['watching'][uuid]['notification_urls'] = notification_urls
# Ignore text
form_ignore_text = request.form.get('ignore-text').strip()
form_ignore_text = request.form.get('ignore-text')
ignore_text = []
if len(form_ignore_text):
for text in form_ignore_text.split("\n"):
if form_ignore_text:
for text in form_ignore_text.strip().split("\n"):
text = text.strip()
if len(text):
ignore_text.append(text)
@@ -361,12 +386,31 @@ def changedetection_app(conig=None, datastore_o=None):
if len(datastore.data['watching'][uuid]['history']):
update_obj['previous_md5'] = get_current_checksum_include_ignore_text(uuid=uuid)
# CSS Filter
css_filter = request.form.get('css_filter')
if css_filter:
datastore.data['watching'][uuid]['css_filter'] = css_filter.strip()
# Reset the previous_md5 so we process a new snapshot including stripping ignore text.
if len(datastore.data['watching'][uuid]['history']):
update_obj['previous_md5'] = get_current_checksum_include_ignore_text(uuid=uuid)
validators.url(url) # @todo switch to prop/attr/observer
datastore.data['watching'][uuid].update(update_obj)
datastore.needs_write = True
messages.append({'class': 'ok', 'message': 'Updated watch.'})
trigger_n = request.form.get('trigger-test-notification')
if trigger_n:
n_object = {'watch_url': url,
'notification_urls': notification_urls}
notification_q.put(n_object)
messages.append({'class': 'ok', 'message': 'Notifications queued.'})
return redirect(url_for('index'))
else:
@@ -380,6 +424,30 @@ def changedetection_app(conig=None, datastore_o=None):
global messages
if request.method == 'GET':
if request.values.get('notification-test'):
url_count = len(datastore.data['settings']['application']['notification_urls'])
if url_count:
import apprise
apobj = apprise.Apprise()
apobj.debug = True
# Add each notification
for n in datastore.data['settings']['application']['notification_urls']:
apobj.add(n)
outcome = apobj.notify(
body='Hello from the worlds best and simplest web page change detection and monitoring service!',
title='Changedetection.io Notification Test',
)
if outcome:
messages.append(
{'class': 'notice', 'message': "{} Notification URLs reached.".format(url_count)})
else:
messages.append(
{'class': 'error', 'message': "One or more Notification URLs failed"})
return redirect(url_for('settings_page'))
if request.values.get('removepassword'):
from pathlib import Path
@@ -413,19 +481,33 @@ def changedetection_app(conig=None, datastore_o=None):
messages.append({'class': 'error', 'message': "Invalid value given, use an integer."})
else:
if minutes >= 5:
if minutes >= 1:
datastore.data['settings']['requests']['minutes_between_check'] = minutes
datastore.needs_write = True
messages.append({'class': 'ok', 'message': "Updated"})
else:
messages.append(
{'class': 'error', 'message': "Must be atleast 5 minutes."})
{'class': 'error', 'message': "Must be atleast 1 minute."})
# 'validators' package doesnt work because its often a non-stanadard protocol. :(
datastore.data['settings']['application']['notification_urls'] = []
trigger_n = request.form.get('trigger-test-notification')
for n in request.values.get('notification_urls').strip().split("\n"):
url = n.strip()
datastore.data['settings']['application']['notification_urls'].append(url)
datastore.needs_write = True
if trigger_n:
n_object = {'watch_url': "Test from changedetection.io!",
'notification_urls': datastore.data['settings']['application']['notification_urls']}
notification_q.put(n_object)
messages.append({'class': 'ok', 'message': 'Notifications queued.'})
output = render_template("settings.html", messages=messages,
minutes=datastore.data['settings']['requests']['minutes_between_check'])
minutes=datastore.data['settings']['requests']['minutes_between_check'],
notification_urls="\r\n".join(
datastore.data['settings']['application']['notification_urls']))
messages = []
return output
@@ -573,6 +655,10 @@ def changedetection_app(conig=None, datastore_o=None):
import zipfile
from pathlib import Path
# Remove any existing backup file, for now we just keep one file
for previous_backup_filename in Path(app.config['datastore_path']).rglob('changedetection-backup-*.zip'):
os.unlink(previous_backup_filename)
# create a ZipFile object
backupname = "changedetection-backup-{}.zip".format(int(time.time()))
@@ -602,7 +688,20 @@ def changedetection_app(conig=None, datastore_o=None):
compress_type=zipfile.ZIP_DEFLATED,
compresslevel=8)
return send_from_directory(app.config['datastore_path'], backupname)
# Create a list file with just the URLs, so it's easier to port somewhere else in the future
list_file = os.path.join(app.config['datastore_path'], "url-list.txt")
with open(list_file, "w") as f:
for uuid in datastore.data['watching']:
url = datastore.data['watching'][uuid]['url']
f.write("{}\r\n".format(url))
# Add it to the Zip
zipObj.write(list_file,
arcname="url-list.txt",
compress_type=zipfile.ZIP_DEFLATED,
compresslevel=8)
return send_from_directory(app.config['datastore_path'], backupname, as_attachment=True)
@app.route("/static/<string:group>/<string:filename>", methods=['GET'])
def static_content(group, filename):
@@ -686,6 +785,8 @@ def changedetection_app(conig=None, datastore_o=None):
# @todo handle ctrl break
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
threading.Thread(target=notification_runner).start()
# Check for new release version
threading.Thread(target=check_for_new_version).start()
return app
@@ -717,54 +818,49 @@ def check_for_new_version():
# Check daily
app.config.exit.wait(86400)
def notification_runner():
# Requests for checking on the site use a pool of thread Workers managed by a Queue.
class Worker(threading.Thread):
current_uuid = None
while not app.config.exit.is_set():
try:
# At the moment only one thread runs (single runner)
n_object = notification_q.get(block=False)
except queue.Empty:
time.sleep(1)
pass
def __init__(self, q, *args, **kwargs):
self.q = q
super().__init__(*args, **kwargs)
def run(self):
from backend import fetch_site_status
update_handler = fetch_site_status.perform_site_check(datastore=datastore)
while not app.config.exit.is_set():
else:
import apprise
# Create an Apprise instance
try:
uuid = self.q.get(block=False)
except queue.Empty:
pass
apobj = apprise.Apprise()
for url in n_object['notification_urls']:
apobj.add(url.strip())
else:
self.current_uuid = uuid
n_body = n_object['watch_url']
if uuid in list(datastore.data['watching'].keys()):
try:
changed_detected, result, contents = update_handler.run(uuid)
# 65 - Append URL of instance to the notification if it is set.
base_url = os.getenv('BASE_URL')
if base_url != None:
n_body += "\n" + base_url
except PermissionError as s:
app.logger.error("File permission error updating", uuid, str(s))
else:
if result:
datastore.update_watch(uuid=uuid, update_obj=result)
if changed_detected:
# A change was detected
datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result)
apobj.notify(
body=n_body,
# @todo This should be configurable.
title="ChangeDetection.io Notification - {}".format(n_object['watch_url'])
)
self.current_uuid = None # Done
self.q.task_done()
app.config.exit.wait(1)
except Exception as e:
print("Watch URL: {} Error {}".format(n_object['watch_url'],e))
# Thread runner to check every minute, look for new watches to feed into the Queue.
def ticker_thread_check_time_launch_checks():
from backend import update_worker
# Spin up Workers.
for _ in range(datastore.data['settings']['requests']['workers']):
new_worker = Worker(update_q)
new_worker = update_worker.update_worker(update_q, notification_q, app, datastore)
running_update_threads.append(new_worker)
new_worker.start()
@@ -782,9 +878,16 @@ def ticker_thread_check_time_launch_checks():
threshold = time.time() - (minutes * 60)
for uuid, watch in datastore.data['watching'].items():
# If they supplied an individual entry minutes to recheck and its not the same as the default
if 'minutes_between_check' in watch and minutes != watch['minutes_between_check']:
threshold = time.time() - (watch['minutes_between_check'] * 60)
if not watch['paused'] and watch['last_checked'] <= threshold:
if not uuid in running_uuids and uuid not in update_q.queue:
update_q.put(uuid)
time.sleep(0.1)
# Should be low so we can break this out in testing
app.config.exit.wait(1)

View File

@@ -66,25 +66,36 @@ class perform_site_check():
timeout=timeout,
verify=False)
stripped_text_from_html = get_text(r.text)
# CSS Filter
css_filter = self.datastore.data['watching'][uuid]['css_filter']
if css_filter and len(css_filter.strip()):
from bs4 import BeautifulSoup
soup = BeautifulSoup(r.content, "html.parser")
stripped_text_from_html = ""
for item in soup.select(css_filter):
text = str(item.get_text())+"\n"
stripped_text_from_html += text
else:
stripped_text_from_html = get_text(r.text)
# Usually from networkIO/requests level
except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout) as e:
update_obj["last_error"] = str(e)
print(str(e))
except requests.exceptions.MissingSchema:
print("Skipping {} due to missing schema/bad url".format(uuid))
# Usually from html2text level
except UnicodeDecodeError as e:
except Exception as e:
# except UnicodeDecodeError as e:
update_obj["last_error"] = str(e)
print(str(e))
# figure out how to deal with this cleaner..
# 'utf-8' codec can't decode byte 0xe9 in position 480: invalid continuation byte
else:
# We rely on the actual text in the html output.. many sites have random script vars etc,
# in the future we'll implement other mechanisms.

View File

@@ -1,7 +1,7 @@
[pytest]
addopts = --no-start-live-server --live-server-port=5005
#testpaths = tests pytest_invenio
#live_server_scope = session
#live_server_scope = function
filterwarnings =
ignore::DeprecationWarning:urllib3.*:

19
backend/run_all_tests.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# live_server will throw errors even with live_server_scope=function if I have the live_server setup in different functions
# and I like to restart the server for each test (and have the test cleanup after each test)
# merge request welcome :)
# exit when any command fails
set -e
# Re #65 - Ability to include a link back to the installation, in the notification.
export BASE_URL="https://foobar.com"
find tests/test_*py -type f|while read test_name
do
echo "TEST RUNNING $test_name"
pytest $test_name
done

View File

@@ -39,7 +39,8 @@ class ChangeDetectionStore:
'workers': 10 # Number of threads, lower is better for slow connections
},
'application': {
'password': False
'password': False,
'notification_urls': [] # Apprise URL list
}
}
}
@@ -54,15 +55,18 @@ class ChangeDetectionStore:
'last_viewed': 0, # history key value of the last viewed via the [diff] link
'newest_history_key': "",
'title': None,
'minutes_between_check': 3 * 60, # Default 3 hours
'previous_md5': "",
'uuid': str(uuid_builder.uuid4()),
'headers': {}, # Extra headers to send
'history': {}, # Dict of timestamp and output stripped filename
'ignore_text': [] # List of text to ignore when calculating the comparison checksum
'ignore_text': [], # List of text to ignore when calculating the comparison checksum
'notification_urls': [], # List of URLs to add to the notification Queue (Usually AppRise)
'css_filter': "",
}
if path.isfile('/source.txt'):
with open('/source.txt') as f:
if path.isfile('backend/source.txt'):
with open('backend/source.txt') as f:
# Should be set in Dockerfile to look for /source.txt , this will give us the git commit #
# So when someone gives us a backup file to examine, we know exactly what code they were running.
self.__data['build_sha'] = f.read()
@@ -109,11 +113,15 @@ class ChangeDetectionStore:
self.add_watch(url='https://www.gov.uk/coronavirus', tag='Covid')
self.add_watch(url='https://changedetection.io', tag='Tech news')
self.__data['version_tag'] = "0.28"
self.__data['version_tag'] = "0.30"
if not 'app_guid' in self.__data:
self.__data['app_guid'] = str(uuid_builder.uuid4())
import sys
import os
if "pytest" in sys.modules or "PYTEST_CURRENT_TEST" in os.environ:
self.__data['app_guid'] = "test-" + str(uuid_builder.uuid4())
else:
self.__data['app_guid'] = str(uuid_builder.uuid4())
self.needs_write = True

View File

@@ -17,7 +17,7 @@
<div class="header">
<div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed">
{% if not current_user.is_authenticated %}
{% if has_password and not current_user.is_authenticated %}
<a class="pure-menu-heading" href="https://github.com/dgtlmoon/changedetection.io" rel="noopener"><strong>Change</strong>Detection.io</a>
{% else %}
<a class="pure-menu-heading" href="/"><strong>Change</strong>Detection.io</a>
@@ -31,7 +31,7 @@
{% endif %}
<ul class="pure-menu-list">
{% if current_user.is_authenticated %}
{% if current_user.is_authenticated or not has_password %}
<li class="pure-menu-item">
<a href="/backup" class="pure-menu-link">BACKUP</a>
</li>
@@ -47,9 +47,9 @@
</li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="pure-menu-item"><a href="/logout" class="pure-menu-link">LOG OUT</a></li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="pure-menu-item"><a href="/logout" class="pure-menu-link">LOG OUT</a></li>
{% endif %}
<li class="pure-menu-item"><a class="github-link" href="https://github.com/dgtlmoon/changedetection.io">
<svg class="octicon octicon-mark-github v-align-middle" height="32" viewBox="0 0 16 16"
version="1.1"
@@ -58,10 +58,6 @@
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>
</a></li>
<!--
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Tour</a></li>
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Sign Up</a></li>
-->
</ul>
</div>
</div>

View File

@@ -17,7 +17,20 @@
<input type="text" placeholder="tag" size="10" id="tag" name="tag" value="{{ watch.tag}}"/>
<span class="pure-form-message-inline">Grouping tags, can be a comma separated list.</span>
</div>
</br>
<div class="pure-control-group">
<label for="minutes">Maximum time in minutes until recheck.</label>
<input type="text" id="minutes" name="minutes" value="{{watch.minutes_between_check}}"
size="5"/>
<span class="pure-form-message-inline">Minimum 1 minute between recheck</span>
</div>
</br>
<div class="pure-control-group">
<label for="minutes">CSS Filter</label>
<input type="text" id="css_filter" name="css_filter" value="{{watch.css_filter}}"
size="25"/>
<span class="pure-form-message-inline">Limit text to this CSS rule, all matching CSS is included.</span>
</div>
<!-- @todo: move to tabs --->
<fieldset class="pure-group">
<label for="ignore-text">Ignore text</label>
@@ -49,8 +62,24 @@ User-Agent: wonderbra 1.0"
<br/>
</fieldset>
<div class="pure-control-group">
<label for="tag">Notification URLs</label>
<textarea id="notification_urls" name="notification_urls" class="pure-input-1-2" placeholder=""
style="width: 100%;
font-family:monospace;
white-space: pre;
overflow-wrap: normal;
overflow-x: scroll;" rows="5">{% for value in watch.notification_urls %}{{ value }}
{% endfor %}</textarea>
<span class="pure-form-message-inline">Use <a target=_new href="https://github.com/caronc/apprise">AppRise URLs</a> for notification to just about any service!</a> </span>
<br/>
<div class="pure-controls">
<span class="pure-form-message-inline"><label for="trigger-test-notification" class="pure-checkbox">
<input type="checkbox" id="trigger-test-notification" name="trigger-test-notification"> Send test notification on save.</label></span>
</div>
</div>
<br/>
<div class="pure-control-group">
<button type="submit" class="pure-button pure-button-primary">Save</button>
</div>

View File

@@ -11,7 +11,7 @@
<input type="hidden" id="email" name="email" value="defaultuser@changedetection.io" />
</div>
<div class="pure-control-group">
<button type="submit" class="pure-button pure-button-primary">Submit</button>
<button type="submit" class="pure-button pure-button-primary">Login</button>
</div>
</fieldset>
</form>

View File

@@ -10,9 +10,11 @@
<label for="minutes">Maximum time in minutes until recheck.</label>
<input type="text" id="minutes" required="" name="minutes" value="{{minutes}}"
size="5"/>
<span class="pure-form-message-inline">This is a required field.</span>
<span class="pure-form-message-inline">This is a required field.</span><br/>
<span class="pure-form-message-inline">Minimum 1 minute between recheck</span>
</div>
<br/>
<hr>
<div class="pure-control-group">
<label for="minutes">Password protection</label>
<input type="password" id="password" name="password" size="15"/>
@@ -21,6 +23,27 @@
{% endif %}
</div>
<br/>
<hr>
<div class="pure-control-group">
<label for="minutes">Global notification settings</label><br/>
Notification URLs <a href="https://github.com/caronc/apprise"> see Apprise examples</a>.
<textarea style="overflow-wrap: normal; overflow-x: scroll;" id="notification_urls" name="notification_urls" cols="80"
rows="6" wrap=off placeholder="Example:
Gitter - gitter://token/room
Office365 - o365://TenantID:AccountEmail/ClientID/ClientSecret/TargetEmail
AWS SNS - sns://AccessKeyID/AccessSecretKey/RegionName/+PhoneNo
SMTPS - mailtos://user:pass@mail.domain.com?to=receivingAddress@example.com
">{{notification_urls}}</textarea>
</div>
<div class="pure-controls">
<span class="pure-form-message-inline"><label for="trigger-test-notification" class="pure-checkbox">
<input type="checkbox" id="trigger-test-notification" name="trigger-test-notification"> Send test notification on save.</label></span>
</div>
<br/>
<div class="pure-control-group">
<button type="submit" class="pure-button pure-button-primary">Save</button>

View File

@@ -35,13 +35,12 @@ def app(request):
def teardown():
datastore.stop_thread = True
app.config.exit.set()
try:
os.unlink("{}/url-watches.json".format(datastore_path))
except FileNotFoundError:
# This is fine in the case of a failure.
pass
assert 1 == 1
for fname in ["url-watches.json", "count.txt", "output.txt"]:
try:
os.unlink("{}/{}".format(datastore_path, fname))
except FileNotFoundError:
# This is fine in the case of a failure.
pass
request.addfinalizer(teardown)
yield app

View File

@@ -0,0 +1,58 @@
from flask import url_for
def test_check_access_control(app, client):
# Still doesnt work, but this is closer.
return
with app.test_client() as c:
# Check we dont have any password protection enabled yet.
res = c.get(url_for("settings_page"))
assert b"Remove password" not in res.data
# Enable password check.
res = c.post(
url_for("settings_page"),
data={"password": "foobar"},
follow_redirects=True
)
assert b"Password protection enabled." in res.data
assert b"LOG OUT" not in res.data
print ("SESSION:", res.session)
# Check we hit the login
res = c.get(url_for("settings_page"), follow_redirects=True)
res = c.get(url_for("login"), follow_redirects=True)
assert b"Login" in res.data
print ("DEBUG >>>>>",res.data)
# Menu should not be available yet
assert b"SETTINGS" not in res.data
assert b"BACKUP" not in res.data
assert b"IMPORT" not in res.data
#defaultuser@changedetection.io is actually hardcoded for now, we only use a single password
res = c.post(
url_for("login"),
data={"password": "foobar", "email": "defaultuser@changedetection.io"},
follow_redirects=True
)
assert b"LOG OUT" in res.data
res = c.get(url_for("settings_page"))
# Menu should be available now
assert b"SETTINGS" in res.data
assert b"BACKUP" in res.data
assert b"IMPORT" in res.data
assert b"LOG OUT" in res.data
# Now remove the password so other tests function, @todo this should happen before each test automatically
c.get(url_for("settings_page", removepassword="true"))
c.get(url_for("import_page"))
assert b"LOG OUT" not in res.data

View File

@@ -3,55 +3,16 @@
import time
from flask import url_for
from urllib.request import urlopen
import pytest
from . util import set_original_response, set_modified_response, live_server_setup
sleep_time_for_fetch_thread = 3
def test_setup_liveserver(live_server):
@live_server.app.route('/test-endpoint')
def test_endpoint():
# Tried using a global var here but didn't seem to work, so reading from a file instead.
with open("test-datastore/output.txt", "r") as f:
return f.read()
live_server.start()
assert 1 == 1
def set_original_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>Which is across multiple lines</p>
</br>
So let's see what happens. </br>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
def set_modified_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>which has this one new line</p>
</br>
So let's see what happens. </br>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
def test_check_basic_change_detection_functionality(client, live_server):
set_original_response()
live_server_setup(live_server)
# Add our URL to the import page
res = client.post(
@@ -75,6 +36,12 @@ def test_check_basic_change_detection_functionality(client, live_server):
assert b'unviewed' not in res.data
assert b'test-endpoint' in res.data
# Default no password set, this stuff should be always available.
assert b"SETTINGS" in res.data
assert b"BACKUP" in res.data
assert b"IMPORT" in res.data
#####################
# Make a change
@@ -93,6 +60,12 @@ def test_check_basic_change_detection_functionality(client, live_server):
res = client.get(url_for("index"))
assert b'unviewed' in res.data
# #75, and it should be in the RSS feed
res = client.get(url_for("index", rss="true"))
expected_url = url_for('test_endpoint', _external=True)
assert b'<rss' in res.data
assert expected_url.encode('utf-8') in res.data
# Following the 'diff' link, it should no longer display as 'unviewed' even after we recheck it a few times
res = client.get(url_for("diff_history_page", uuid="first"))
assert b'Compare newest' in res.data
@@ -122,39 +95,3 @@ def test_check_basic_change_detection_functionality(client, live_server):
res = client.get(url_for("api_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data
def test_check_access_control(client):
return
# @note: does not seem to handle the last logout step correctly, we're still logged in.. but yet..
# pytest team keep telling us that we have a new context.. i'm lost :(
# Add our URL to the import page
res = client.post(
url_for("settings_page"),
data={"password": "foobar"},
follow_redirects=True
)
assert b"LOG OUT" not in res.data
client.get(url_for("import_page"), follow_redirects=True)
assert b"Password" in res.data
#defaultuser@changedetection.io is actually hardcoded for now, we only use a single password
res = client.post(
url_for("login"),
data={"password": "foobar", "email": "defaultuser@changedetection.io"},
follow_redirects=True
)
assert b"LOG OUT" in res.data
client.get(url_for("settings_page"), follow_redirects=True)
assert b"LOG OUT" in res.data
# Now remove the password so other tests function, @todo this should happen before each test automatically
print(res.data)
client.get(url_for("settings_page", removepassword="true"), follow_redirects=True)
client.get(url_for("import_page", removepassword="true"), follow_redirects=True)
assert b"LOG OUT" not in res.data

View File

@@ -0,0 +1,102 @@
#!/usr/bin/python3
import time
from flask import url_for
from . util import live_server_setup
def test_setup(live_server):
live_server_setup(live_server)
def set_original_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>Which is across multiple lines</p>
</br>
So let's see what happens. </br>
<div id="sametext">Some text thats the same</div>
<div id="changetext">Some text that will change</div>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
return None
def set_modified_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>which has this one new line</p>
</br>
So let's see what happens. </br>
<div id="sametext">Some text thats the same</div>
<div id="changetext">Some text that changes</div>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
return None
def test_check_markup_css_filter_restriction(client, live_server):
sleep_time_for_fetch_thread = 3
css_filter = "#sametext"
set_original_response()
# Give the endpoint time to spin up
time.sleep(1)
# Add our URL to the import page
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
# Trigger a check
client.get(url_for("api_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread)
# Goto the edit page, add our ignore text
# Add our URL to the import page
res = client.post(
url_for("edit_page", uuid="first"),
data={"css_filter": css_filter, "url": test_url, "tag": "", "headers": ""},
follow_redirects=True
)
assert b"Updated watch." in res.data
# Check it saved
res = client.get(
url_for("edit_page", uuid="first"),
)
assert bytes(css_filter.encode('utf-8')) in res.data
# Trigger a check
client.get(url_for("api_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread)
# Make a change
set_modified_response()
# Trigger a check
client.get(url_for("api_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread)
# It should have 'unviewed' still
# Because it should be looking at only that 'sametext' id
res = client.get(url_for("index"))
assert b'unviewed' in res.data

View File

@@ -2,9 +2,10 @@
import time
from flask import url_for
from urllib.request import urlopen
import pytest
from . util import live_server_setup
def test_setup(live_server):
live_server_setup(live_server)
# Unit test of the stripper
# Always we are dealing in utf-8

View File

@@ -0,0 +1,72 @@
import time
from flask import url_for
from . util import set_original_response, set_modified_response, live_server_setup
# Hard to just add more live server URLs when one test is already running (I think)
# So we add our test here (was in a different file)
def test_check_notification(client, live_server):
live_server_setup(live_server)
set_original_response()
# Give the endpoint time to spin up
time.sleep(3)
# Add our URL to the import page
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("import_page"),
data={"urls": test_url},
follow_redirects=True
)
assert b"1 Imported" in res.data
# Give the thread time to pick it up
time.sleep(3)
# Goto the edit page, add our ignore text
# Add our URL to the import page
url = url_for('test_notification_endpoint', _external=True)
notification_url = url.replace('http', 'json')
print (">>>> Notification URL: "+notification_url)
res = client.post(
url_for("edit_page", uuid="first"),
data={"notification_urls": notification_url, "url": test_url, "tag": "", "headers": ""},
follow_redirects=True
)
assert b"Updated watch." in res.data
# Hit the edit page, be sure that we saved it
res = client.get(
url_for("edit_page", uuid="first"))
assert bytes(notification_url.encode('utf-8')) in res.data
set_modified_response()
# Trigger a check
client.get(url_for("api_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up
time.sleep(3)
# Did the front end see it?
res = client.get(
url_for("index"))
assert bytes("just now".encode('utf-8')) in res.data
# Check it triggered
res = client.get(
url_for("test_notification_counter"),
)
assert bytes("we hit it".encode('utf-8')) in res.data
# Did we see the URL that had a change, in the notification?
assert bytes("test-endpoint".encode('utf-8')) in res.data
# Re #65 - did we see our foobar.com BASE_URL ?
assert bytes("https://foobar.com".encode('utf-8')) in res.data

67
backend/tests/util.py Normal file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/python3
def set_original_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>Which is across multiple lines</p>
</br>
So let's see what happens. </br>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
return None
def set_modified_response():
test_return_data = """<html>
<body>
Some initial text</br>
<p>which has this one new line</p>
</br>
So let's see what happens. </br>
</body>
</html>
"""
with open("test-datastore/output.txt", "w") as f:
f.write(test_return_data)
return None
def live_server_setup(live_server):
@live_server.app.route('/test-endpoint')
def test_endpoint():
# Tried using a global var here but didn't seem to work, so reading from a file instead.
with open("test-datastore/output.txt", "r") as f:
return f.read()
@live_server.app.route('/test_notification_endpoint', methods=['POST'])
def test_notification_endpoint():
from flask import request
with open("test-datastore/count.txt", "w") as f:
f.write("we hit it\n")
# Debug method, dump all POST to file also, used to prove #65
data = request.stream.read()
if data != None:
f.write(str(data))
print("\n>> Test notification endpoint was hit.\n")
return "Text was set"
# And this should return not zero.
@live_server.app.route('/test_notification_counter')
def test_notification_counter():
try:
with open("test-datastore/count.txt", "r") as f:
return f.read()
except FileNotFoundError:
return "nope :("
live_server.start()

67
backend/update_worker.py Normal file
View File

@@ -0,0 +1,67 @@
import threading
import queue
# Requests for checking on the site use a pool of thread Workers managed by a Queue.
class update_worker(threading.Thread):
current_uuid = None
def __init__(self, q, notification_q, app, datastore, *args, **kwargs):
self.q = q
self.app = app
self.notification_q = notification_q
self.datastore = datastore
super().__init__(*args, **kwargs)
def run(self):
from backend import fetch_site_status
update_handler = fetch_site_status.perform_site_check(datastore=self.datastore)
while not self.app.config.exit.is_set():
try:
uuid = self.q.get(block=False)
except queue.Empty:
pass
else:
self.current_uuid = uuid
if uuid in list(self.datastore.data['watching'].keys()):
try:
changed_detected, result, contents = update_handler.run(uuid)
except PermissionError as s:
self.app.logger.error("File permission error updating", uuid, str(s))
else:
if result:
try:
self.datastore.update_watch(uuid=uuid, update_obj=result)
if changed_detected:
# A change was detected
self.datastore.save_history_text(uuid=uuid, contents=contents, result_obj=result)
watch = self.datastore.data['watching'][uuid]
# Did it have any notification alerts to hit?
if len(watch['notification_urls']):
print("Processing notifications for UUID: {}".format(uuid))
n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'],
'notification_urls': watch['notification_urls']}
self.notification_q.put(n_object)
# No? maybe theres a global setting, queue them all
elif len(self.datastore.data['settings']['application']['notification_urls']):
print("Processing GLOBAL notifications for UUID: {}".format(uuid))
n_object = {'watch_url': self.datastore.data['watching'][uuid]['url'],
'notification_urls': self.datastore.data['settings']['application'][
'notification_urls']}
self.notification_q.put(n_object)
except Exception as e:
print("!!!! Exception in update_worker !!!\n", e)
self.current_uuid = None # Done
self.q.task_done()
self.app.config.exit.wait(1)

BIN
btc-support.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -66,15 +66,15 @@ def main(argv):
datastore = store.ChangeDetectionStore(datastore_path=app_config['datastore_path'])
app = backend.changedetection_app(app_config, datastore)
app.config['datastore_path'] = datastore_path
app.secret_key = init_app_secret(app_config['datastore_path'])
@app.context_processor
def inject_version():
return dict(version=datastore.data['version_tag'])
@app.context_processor
def inject_new_version_available():
return dict(new_version_available=app.config['NEW_VERSION_AVAILABLE'])
return dict(version=datastore.data['version_tag'],
new_version_available=app.config['NEW_VERSION_AVAILABLE'],
has_password=datastore.data['settings']['application']['password'] != False
)
if ssl_mode:
# @todo finalise SSL config, but this should get you in the right direction if you need it.

25
docker-compose.yml Normal file
View File

@@ -0,0 +1,25 @@
version: '2'
services:
changedetection.io:
image: dgtlmoon/changedetection.io
container_name: changedetection.io
hostname: changedetection.io
volumes:
- changedetection-data:/datastore
# environment:
# Proxy support example.
# - HTTP_PROXY="socks5h://10.10.1.10:1080"
# - HTTPS_PROXY="socks5h://10.10.1.10:1080"
# An exclude list (useful for notification URLs above) can be specified by with
# - NO_PROXY="localhost,192.168.0.0/24"
# Base URL of your changedetection.io install (Added to notification alert
# - BASE_URL="https://mysite.com"
ports:
- 5000:5000
restart: always
volumes:
changedetection-data:

View File

@@ -1,13 +1,20 @@
chardet==2.3.0
flask~= 1.0
pytest ~=6.2
pytest-flask ~=1.1
eventlet ~= 0.30
requests ~= 2.15
pytest-flask ~=1.2
eventlet>=0.31.0
requests[socks] ~= 2.15
validators
timeago ~=1.0
inscriptis ~= 1.1
feedgen ~= 0.9
flask-login ~= 0.5
pytz
urllib3
urllib3
# Notification library
apprise ~= 0.9
# Used for CSS filtering
bs4

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB