diff --git a/README.md b/README.md
index cc3c566dc..defa0a5f3 100644
--- a/README.md
+++ b/README.md
@@ -170,9 +170,12 @@ Raspberry Pi and linux/arm/v6 linux/arm/v7 arm64 devices are supported! See the
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`
+Firstly, consider taking out a [change detection monthly subscription - unlimited checks and watches](https://lemonade.changedetection.io/start) , even if you don't use it, you still get the warm fuzzy feeling of helping out the project. (And who knows, you might just use it!)
+
+Or directly donate an amount PayPal [](https://www.paypal.com/donate/?hosted_button_id=7CP6HR9ZCNDYJ)
+
+Or BTC `1PLFN327GyUarpJd7nVe7Reqg9qHx5frNn`
diff --git a/changedetectionio/__init__.py b/changedetectionio/__init__.py
index 07c8a819d..525e86886 100644
--- a/changedetectionio/__init__.py
+++ b/changedetectionio/__init__.py
@@ -32,6 +32,7 @@ from flask import (
render_template,
request,
send_from_directory,
+ session,
url_for,
)
from flask_login import login_required
@@ -393,7 +394,8 @@ def changedetection_app(config=None, datastore_o=None):
hosted_sticky=os.getenv("SALTED_PASS", False) == False,
guid=datastore.data['app_guid'],
queued_uuids=update_q.queue)
-
+ if session.get('share-link'):
+ del(session['share-link'])
return output
@@ -688,12 +690,14 @@ def changedetection_app(config=None, datastore_o=None):
# Up to 5000 per batch so we dont flood the server
if len(url) and validators.url(url.replace('source:', '')) and good < 5000:
new_uuid = datastore.add_watch(url=url.strip(), tag=" ".join(tags), write_to_disk_now=False)
- # Straight into the queue.
- update_q.put(new_uuid)
- good += 1
- else:
- if len(url):
- remaining_urls.append(url)
+ if new_uuid:
+ # Straight into the queue.
+ update_q.put(new_uuid)
+ good += 1
+ continue
+
+ if len(url.strip()):
+ remaining_urls.append(url)
flash("{} Imported in {:.2f}s, {} Skipped.".format(good, time.time()-now,len(remaining_urls)))
datastore.needs_write = True
@@ -1019,29 +1023,35 @@ def changedetection_app(config=None, datastore_o=None):
from changedetectionio import forms
form = forms.quickWatchForm(request.form)
- if form.validate():
-
- url = request.form.get('url').strip()
- if datastore.url_exists(url):
- flash('The URL {} already exists'.format(url), "error")
- return redirect(url_for('index'))
-
- # @todo add_watch should throw a custom Exception for validation etc
- new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip())
- # Straight into the queue.
- update_q.put(new_uuid)
-
- flash("Watch added.")
- return redirect(url_for('index'))
- else:
+ if not form.validate():
flash("Error")
return redirect(url_for('index'))
+ url = request.form.get('url').strip()
+ if datastore.url_exists(url):
+ flash('The URL {} already exists'.format(url), "error")
+ return redirect(url_for('index'))
+
+ # @todo add_watch should throw a custom Exception for validation etc
+ new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip())
+ if new_uuid:
+ # Straight into the queue.
+ update_q.put(new_uuid)
+ flash("Watch added.")
+
+ return redirect(url_for('index'))
+
+
@app.route("/api/delete", methods=['GET'])
@login_required
def api_delete():
uuid = request.args.get('uuid')
+
+ if uuid != 'all' and not uuid in datastore.data['watching'].keys():
+ flash('The watch by UUID {} does not exist.'.format(uuid), 'error')
+ return redirect(url_for('index'))
+
# More for testing, possible to return the first/only
if uuid == 'first':
uuid = list(datastore.data['watching'].keys()).pop()
@@ -1101,6 +1111,57 @@ def changedetection_app(config=None, datastore_o=None):
flash("{} watches are queued for rechecking.".format(i))
return redirect(url_for('index', tag=tag))
+ @app.route("/api/share-url", methods=['GET'])
+ @login_required
+ def api_share_put_watch():
+ """Given a watch UUID, upload the info and return a share-link
+ the share-link can be imported/added"""
+ import requests
+ import json
+ tag = request.args.get('tag')
+ uuid = request.args.get('uuid')
+
+ # more for testing
+ if uuid == 'first':
+ uuid = list(datastore.data['watching'].keys()).pop()
+
+ # copy it to memory as trim off what we dont need (history)
+ watch = deepcopy(datastore.data['watching'][uuid])
+ if (watch.get('history')):
+ del (watch['history'])
+
+ # for safety/privacy
+ for k in list(watch.keys()):
+ if k.startswith('notification_'):
+ del watch[k]
+
+ for r in['uuid', 'last_checked', 'last_changed']:
+ if watch.get(r):
+ del (watch[r])
+
+ # Add the global stuff which may have an impact
+ watch['ignore_text'] += datastore.data['settings']['application']['global_ignore_text']
+ watch['subtractive_selectors'] += datastore.data['settings']['application']['global_subtractive_selectors']
+
+ watch_json = json.dumps(watch)
+
+ try:
+ r = requests.request(method="POST",
+ data={'watch': watch_json},
+ url="https://changedetection.io/share/share",
+ headers={'App-Guid': datastore.data['app_guid']})
+ res = r.json()
+
+ session['share-link'] = "https://changedetection.io/share/{}".format(res['share_key'])
+
+
+ except Exception as e:
+ flash("Could not share, something went wrong while communicating with the share server.", 'error')
+
+ # https://changedetection.io/share/VrMv05wpXyQa
+ # in the browser - should give you a nice info page - wtf
+ # paste in etc
+ return redirect(url_for('index'))
# @todo handle ctrl break
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()
diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py
index b148f584e..c03138682 100644
--- a/changedetectionio/model/Watch.py
+++ b/changedetectionio/model/Watch.py
@@ -22,7 +22,8 @@ class model(dict):
'newest_history_key': 0,
'title': None,
'previous_md5': False,
- 'uuid': str(uuid_builder.uuid4()),
+# UUID not needed, should be generated only as a key
+# 'uuid':
'headers': {}, # Extra headers to send
'body': None,
'method': 'GET',
diff --git a/changedetectionio/static/images/copy.svg b/changedetectionio/static/images/copy.svg
new file mode 100644
index 000000000..b14994ab8
--- /dev/null
+++ b/changedetectionio/static/images/copy.svg
@@ -0,0 +1,40 @@
+
+
diff --git a/changedetectionio/static/images/spread.svg b/changedetectionio/static/images/spread.svg
new file mode 100644
index 000000000..757cb6311
--- /dev/null
+++ b/changedetectionio/static/images/spread.svg
@@ -0,0 +1,46 @@
+
+
diff --git a/changedetectionio/static/js/watch-overview.js b/changedetectionio/static/js/watch-overview.js
index fb2b917d3..a06034b13 100644
--- a/changedetectionio/static/js/watch-overview.js
+++ b/changedetectionio/static/js/watch-overview.js
@@ -4,4 +4,23 @@ $(function () {
$(this).closest('.unviewed').removeClass('unviewed');
});
+
+ $('.with-share-link > *').click(function () {
+ $("#copied-clipboard").remove();
+
+ var range = document.createRange();
+ var n=$("#share-link")[0];
+ range.selectNode(n);
+ window.getSelection().removeAllRanges();
+ window.getSelection().addRange(range);
+ document.execCommand("copy");
+ window.getSelection().removeAllRanges();
+
+ $('.with-share-link').append('Copied to clipboard');
+ $("#copied-clipboard").fadeOut(2500, function() {
+ $(this).remove();
+ });
+ });
+
});
+
diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css
index ed6896a70..a10bab9b2 100644
--- a/changedetectionio/static/styles/styles.css
+++ b/changedetectionio/static/styles/styles.css
@@ -180,6 +180,9 @@ body:after, body:before {
.messages li.notice {
background: rgba(255, 255, 255, 0.5); }
+.messages.with-share-link > *:hover {
+ cursor: pointer; }
+
#notification-customisation {
border: 1px solid #ccc;
padding: 0.5rem;
diff --git a/changedetectionio/static/styles/styles.scss b/changedetectionio/static/styles/styles.scss
index 118eb314c..a7e47a7c5 100644
--- a/changedetectionio/static/styles/styles.scss
+++ b/changedetectionio/static/styles/styles.scss
@@ -237,6 +237,11 @@ body:after, body:before {
background: rgba(255, 255, 255, .5);
}
}
+ &.with-share-link {
+ > *:hover {
+ cursor:pointer;
+ }
+ }
}
#notification-customisation {
diff --git a/changedetectionio/store.py b/changedetectionio/store.py
index 8ca3fde25..c65f17fa5 100644
--- a/changedetectionio/store.py
+++ b/changedetectionio/store.py
@@ -1,3 +1,6 @@
+from flask import (
+ flash
+)
import json
import logging
import os
@@ -8,6 +11,7 @@ from copy import deepcopy
from os import mkdir, path, unlink
from threading import Lock
import re
+import requests
from changedetectionio.model import Watch, App
@@ -34,7 +38,8 @@ class ChangeDetectionStore:
self.__data = App.model()
# Base definition for all watchers
- self.generic_definition = Watch.model()
+ # deepcopy part of #569 - not sure why its needed exactly
+ self.generic_definition = deepcopy(Watch.model())
if path.isfile('changedetectionio/source.txt'):
with open('changedetectionio/source.txt') as f:
@@ -227,7 +232,7 @@ class ChangeDetectionStore:
del self.data['watching'][uuid]
- self.needs_write = True
+ self.needs_write_urgent = True
# Clone a watch by UUID
def clone(self, uuid):
@@ -295,17 +300,46 @@ class ChangeDetectionStore:
def add_watch(self, url, tag="", extras=None, write_to_disk_now=True):
if extras is None:
extras = {}
+ # Incase these are copied across, assume it's a reference and deepcopy()
+ apply_extras = deepcopy(extras)
+
+ # Was it a share link? try to fetch the data
+ if (url.startswith("https://changedetection.io/share/")):
+ try:
+ r = requests.request(method="GET",
+ url=url,
+ # So we know to return the JSON instead of the human-friendly "help" page
+ headers={'App-Guid': self.__data['app_guid']})
+ res = r.json()
+
+ # List of permisable stuff we accept from the wild internet
+ for k in ['url', 'tag',
+ 'paused', 'title',
+ 'previous_md5', 'headers',
+ 'body', 'method',
+ 'ignore_text', 'css_filter',
+ 'subtractive_selectors', 'trigger_text',
+ 'extract_title_as_title']:
+ if res.get(k):
+ apply_extras[k] = res[k]
+
+ except Exception as e:
+ logging.error("Error fetching metadata for shared watch link", url, str(e))
+ flash("Error fetching metadata for {}".format(url), 'error')
+ return False
with self.lock:
# @todo use a common generic version of this
new_uuid = str(uuid_builder.uuid4())
- new_watch = Watch.model({
+ # #Re 569
+ # Not sure why deepcopy was needed here, sometimes new watches would appear to already have 'history' set
+ # I assumed this would instantiate a new object but somehow an existing dict was getting used
+ new_watch = deepcopy(Watch.model({
'url': url,
'tag': tag
- })
+ }))
+
- # Incase these are copied across, assume it's a reference and deepcopy()
- apply_extras = deepcopy(extras)
for k in ['uuid', 'history', 'last_checked', 'last_changed', 'newest_history_key', 'previous_md5', 'viewed']:
if k in apply_extras:
del apply_extras[k]
diff --git a/changedetectionio/templates/base.html b/changedetectionio/templates/base.html
index 41105f16f..9a9c65414 100644
--- a/changedetectionio/templates/base.html
+++ b/changedetectionio/templates/base.html
@@ -94,6 +94,13 @@
{% endif %}
{% endwith %}
+
+ {% if session['share-link'] %}
+