mirror of
				https://github.com/dgtlmoon/changedetection.io.git
				synced 2025-11-04 00:27:48 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					4b5b7f6c06 | ||
| 
						 | 
					8e36e9ddc4 | 
							
								
								
									
										62
									
								
								changedetectionio/api/Import.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								changedetectionio/api/Import.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
import os
 | 
			
		||||
from changedetectionio.strtobool import strtobool
 | 
			
		||||
from flask_restful import abort, Resource
 | 
			
		||||
from flask import request
 | 
			
		||||
import validators
 | 
			
		||||
from . import auth
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Import(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def post(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {post} /api/v1/import Import a list of watched URLs
 | 
			
		||||
        @apiDescription Accepts a line-feed separated list of URLs to import, additionally with ?tag_uuids=(tag  id), ?tag=(name), ?proxy={key}, ?dedupe=true (default true) one URL per line.
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/import --data-binary @list-of-sites.txt -H"x-api-key:8a111a21bc2f8f1dd9b9353bbd46049a"
 | 
			
		||||
        @apiName Import
 | 
			
		||||
        @apiGroup Watch
 | 
			
		||||
        @apiSuccess (200) {List} OK List of watch UUIDs added
 | 
			
		||||
        @apiSuccess (500) {String} ERR Some other error
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        extras = {}
 | 
			
		||||
 | 
			
		||||
        if request.args.get('proxy'):
 | 
			
		||||
            plist = self.datastore.proxy_list
 | 
			
		||||
            if not request.args.get('proxy') in plist:
 | 
			
		||||
                return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400
 | 
			
		||||
            else:
 | 
			
		||||
                extras['proxy'] = request.args.get('proxy')
 | 
			
		||||
 | 
			
		||||
        dedupe = strtobool(request.args.get('dedupe', 'true'))
 | 
			
		||||
 | 
			
		||||
        tags = request.args.get('tag')
 | 
			
		||||
        tag_uuids = request.args.get('tag_uuids')
 | 
			
		||||
 | 
			
		||||
        if tag_uuids:
 | 
			
		||||
            tag_uuids = tag_uuids.split(',')
 | 
			
		||||
 | 
			
		||||
        urls = request.get_data().decode('utf8').splitlines()
 | 
			
		||||
        added = []
 | 
			
		||||
        allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
 | 
			
		||||
        for url in urls:
 | 
			
		||||
            url = url.strip()
 | 
			
		||||
            if not len(url):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # If hosts that only contain alphanumerics are allowed ("localhost" for example)
 | 
			
		||||
            if not validators.url(url, simple_host=allow_simplehost):
 | 
			
		||||
                return f"Invalid or unsupported URL - {url}", 400
 | 
			
		||||
 | 
			
		||||
            if dedupe and self.datastore.url_exists(url):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            new_uuid = self.datastore.add_watch(url=url, extras=extras, tag=tags, tag_uuids=tag_uuids)
 | 
			
		||||
            added.append(new_uuid)
 | 
			
		||||
 | 
			
		||||
        return added
 | 
			
		||||
							
								
								
									
										54
									
								
								changedetectionio/api/SystemInfo.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								changedetectionio/api/SystemInfo.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
from flask_restful import Resource
 | 
			
		||||
from . import auth
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SystemInfo(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
        self.update_q = kwargs['update_q']
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def get(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {get} /api/v1/systeminfo Return system info
 | 
			
		||||
        @apiDescription Return some info about the current system state
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/systeminfo -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
            HTTP/1.0 200
 | 
			
		||||
            {
 | 
			
		||||
                'queue_size': 10 ,
 | 
			
		||||
                'overdue_watches': ["watch-uuid-list"],
 | 
			
		||||
                'uptime': 38344.55,
 | 
			
		||||
                'watch_count': 800,
 | 
			
		||||
                'version': "0.40.1"
 | 
			
		||||
            }
 | 
			
		||||
        @apiName Get Info
 | 
			
		||||
        @apiGroup System Information
 | 
			
		||||
        """
 | 
			
		||||
        import time
 | 
			
		||||
        overdue_watches = []
 | 
			
		||||
 | 
			
		||||
        # Check all watches and report which have not been checked but should have been
 | 
			
		||||
 | 
			
		||||
        for uuid, watch in self.datastore.data.get('watching', {}).items():
 | 
			
		||||
            # see if now - last_checked is greater than the time that should have been
 | 
			
		||||
            # this is not super accurate (maybe they just edited it) but better than nothing
 | 
			
		||||
            t = watch.threshold_seconds()
 | 
			
		||||
            if not t:
 | 
			
		||||
                # Use the system wide default
 | 
			
		||||
                t = self.datastore.threshold_seconds
 | 
			
		||||
 | 
			
		||||
            time_since_check = time.time() - watch.get('last_checked')
 | 
			
		||||
 | 
			
		||||
            # Allow 5 minutes of grace time before we decide it's overdue
 | 
			
		||||
            if time_since_check - (5 * 60) > t:
 | 
			
		||||
                overdue_watches.append(uuid)
 | 
			
		||||
        from changedetectionio import __version__ as main_version
 | 
			
		||||
        return {
 | 
			
		||||
                   'queue_size': self.update_q.qsize(),
 | 
			
		||||
                   'overdue_watches': overdue_watches,
 | 
			
		||||
                   'uptime': round(time.time() - self.datastore.start_time, 2),
 | 
			
		||||
                   'watch_count': len(self.datastore.data.get('watching', {})),
 | 
			
		||||
                   'version': main_version
 | 
			
		||||
               }, 200
 | 
			
		||||
							
								
								
									
										156
									
								
								changedetectionio/api/Tags.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								changedetectionio/api/Tags.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,156 @@
 | 
			
		||||
from flask_expects_json import expects_json
 | 
			
		||||
from flask_restful import abort, Resource
 | 
			
		||||
from flask import request
 | 
			
		||||
from . import auth
 | 
			
		||||
 | 
			
		||||
# Import schemas from __init__.py
 | 
			
		||||
from . import schema_tag, schema_create_tag, schema_update_tag
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Tag(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
 | 
			
		||||
    # Get information about a single tag
 | 
			
		||||
    # curl http://localhost:5000/api/v1/tag/<string:uuid>
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def get(self, uuid):
 | 
			
		||||
        """
 | 
			
		||||
        @api {get} /api/v1/tag/:uuid Single tag - get data or toggle notification muting.
 | 
			
		||||
        @apiDescription Retrieve tag information and set notification_muted status
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
            curl "http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091?muted=muted" -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
        @apiName Tag
 | 
			
		||||
        @apiGroup Tag
 | 
			
		||||
        @apiParam {uuid} uuid Tag unique ID.
 | 
			
		||||
        @apiQuery {String} [muted] =`muted` or =`unmuted` , Sets the MUTE NOTIFICATIONS state
 | 
			
		||||
        @apiSuccess (200) {String} OK When muted operation OR full JSON object of the tag
 | 
			
		||||
        @apiSuccess (200) {JSON} TagJSON JSON Full JSON object of the tag
 | 
			
		||||
        """
 | 
			
		||||
        from copy import deepcopy
 | 
			
		||||
        tag = deepcopy(self.datastore.data['settings']['application']['tags'].get(uuid))
 | 
			
		||||
        if not tag:
 | 
			
		||||
            abort(404, message=f'No tag exists with the UUID of {uuid}')
 | 
			
		||||
 | 
			
		||||
        if request.args.get('muted', '') == 'muted':
 | 
			
		||||
            self.datastore.data['settings']['application']['tags'][uuid]['notification_muted'] = True
 | 
			
		||||
            return "OK", 200
 | 
			
		||||
        elif request.args.get('muted', '') == 'unmuted':
 | 
			
		||||
            self.datastore.data['settings']['application']['tags'][uuid]['notification_muted'] = False
 | 
			
		||||
            return "OK", 200
 | 
			
		||||
 | 
			
		||||
        return tag
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def delete(self, uuid):
 | 
			
		||||
        """
 | 
			
		||||
        @api {delete} /api/v1/tag/:uuid Delete a tag and remove it from all watches
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -X DELETE -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
        @apiParam {uuid} uuid Tag unique ID.
 | 
			
		||||
        @apiName DeleteTag
 | 
			
		||||
        @apiGroup Tag
 | 
			
		||||
        @apiSuccess (200) {String} OK Was deleted
 | 
			
		||||
        """
 | 
			
		||||
        if not self.datastore.data['settings']['application']['tags'].get(uuid):
 | 
			
		||||
            abort(400, message='No tag exists with the UUID of {}'.format(uuid))
 | 
			
		||||
 | 
			
		||||
        # Delete the tag, and any tag reference
 | 
			
		||||
        del self.datastore.data['settings']['application']['tags'][uuid]
 | 
			
		||||
        
 | 
			
		||||
        # Remove tag from all watches
 | 
			
		||||
        for watch_uuid, watch in self.datastore.data['watching'].items():
 | 
			
		||||
            if watch.get('tags') and uuid in watch['tags']:
 | 
			
		||||
                watch['tags'].remove(uuid)
 | 
			
		||||
 | 
			
		||||
        return 'OK', 204
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    @expects_json(schema_update_tag)
 | 
			
		||||
    def put(self, uuid):
 | 
			
		||||
        """
 | 
			
		||||
        @api {put} /api/v1/tag/:uuid Update tag information
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            Update (PUT)
 | 
			
		||||
            curl http://localhost:5000/api/v1/tag/cc0cfffa-f449-477b-83ea-0caafd1dc091 -X PUT -H"x-api-key:813031b16330fe25e3780cf0325daa45" -H "Content-Type: application/json" -d '{"title": "New Tag Title"}'
 | 
			
		||||
 | 
			
		||||
        @apiDescription Updates an existing tag using JSON
 | 
			
		||||
        @apiParam {uuid} uuid Tag unique ID.
 | 
			
		||||
        @apiName UpdateTag
 | 
			
		||||
        @apiGroup Tag
 | 
			
		||||
        @apiSuccess (200) {String} OK Was updated
 | 
			
		||||
        @apiSuccess (500) {String} ERR Some other error
 | 
			
		||||
        """
 | 
			
		||||
        tag = self.datastore.data['settings']['application']['tags'].get(uuid)
 | 
			
		||||
        if not tag:
 | 
			
		||||
            abort(404, message='No tag exists with the UUID of {}'.format(uuid))
 | 
			
		||||
 | 
			
		||||
        tag.update(request.json)
 | 
			
		||||
        self.datastore.needs_write_urgent = True
 | 
			
		||||
 | 
			
		||||
        return "OK", 200
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    # Only cares for {'title': 'xxxx'}
 | 
			
		||||
    def post(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {post} /api/v1/watch Create a single tag
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/watch -H"x-api-key:813031b16330fe25e3780cf0325daa45" -H "Content-Type: application/json" -d '{"name": "Work related"}'
 | 
			
		||||
        @apiName Create
 | 
			
		||||
        @apiGroup Tag
 | 
			
		||||
        @apiSuccess (200) {String} OK Was created
 | 
			
		||||
        @apiSuccess (500) {String} ERR Some other error
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        json_data = request.get_json()
 | 
			
		||||
        title = json_data.get("title",'').strip()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        new_uuid = self.datastore.add_tag(title=title)
 | 
			
		||||
        if new_uuid:
 | 
			
		||||
            return {'uuid': new_uuid}, 201
 | 
			
		||||
        else:
 | 
			
		||||
            return "Invalid or unsupported tag", 400
 | 
			
		||||
 | 
			
		||||
class Tags(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def get(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {get} /api/v1/tags List tags
 | 
			
		||||
        @apiDescription Return list of available tags
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/tags -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
            {
 | 
			
		||||
                "cc0cfffa-f449-477b-83ea-0caafd1dc091": {
 | 
			
		||||
                    "title": "Tech News",
 | 
			
		||||
                    "notification_muted": false,
 | 
			
		||||
                    "date_created": 1677103794
 | 
			
		||||
                },
 | 
			
		||||
                "e6f5fd5c-dbfe-468b-b8f3-f9d6ff5ad69b": {
 | 
			
		||||
                    "title": "Shopping",
 | 
			
		||||
                    "notification_muted": true,
 | 
			
		||||
                    "date_created": 1676662819
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        @apiName ListTags
 | 
			
		||||
        @apiGroup Tag Management
 | 
			
		||||
        @apiSuccess (200) {String} OK JSON dict
 | 
			
		||||
        """
 | 
			
		||||
        result = {}
 | 
			
		||||
        for uuid, tag in self.datastore.data['settings']['application']['tags'].items():
 | 
			
		||||
            result[uuid] = {
 | 
			
		||||
                'date_created': tag.get('date_created', 0),
 | 
			
		||||
                'notification_muted': tag.get('notification_muted', False),
 | 
			
		||||
                'title': tag.get('title', ''),
 | 
			
		||||
                'uuid': tag.get('uuid')
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return result, 200
 | 
			
		||||
@@ -9,20 +9,9 @@ import validators
 | 
			
		||||
from . import auth
 | 
			
		||||
import copy
 | 
			
		||||
 | 
			
		||||
# See docs/README.md for rebuilding the docs/apidoc information
 | 
			
		||||
# Import schemas from __init__.py
 | 
			
		||||
from . import schema, schema_create_watch, schema_update_watch
 | 
			
		||||
 | 
			
		||||
from . import api_schema
 | 
			
		||||
from ..model import watch_base
 | 
			
		||||
 | 
			
		||||
# Build a JSON Schema atleast partially based on our Watch model
 | 
			
		||||
watch_base_config = watch_base()
 | 
			
		||||
schema = api_schema.build_watch_json_schema(watch_base_config)
 | 
			
		||||
 | 
			
		||||
schema_create_watch = copy.deepcopy(schema)
 | 
			
		||||
schema_create_watch['required'] = ['url']
 | 
			
		||||
 | 
			
		||||
schema_update_watch = copy.deepcopy(schema)
 | 
			
		||||
schema_update_watch['additionalProperties'] = False
 | 
			
		||||
 | 
			
		||||
class Watch(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
@@ -305,110 +294,4 @@ class CreateWatch(Resource):
 | 
			
		||||
                self.update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
 | 
			
		||||
            return {'status': "OK"}, 200
 | 
			
		||||
 | 
			
		||||
        return list, 200
 | 
			
		||||
 | 
			
		||||
class Import(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def post(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {post} /api/v1/import Import a list of watched URLs
 | 
			
		||||
        @apiDescription Accepts a line-feed separated list of URLs to import, additionally with ?tag_uuids=(tag  id), ?tag=(name), ?proxy={key}, ?dedupe=true (default true) one URL per line.
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/import --data-binary @list-of-sites.txt -H"x-api-key:8a111a21bc2f8f1dd9b9353bbd46049a"
 | 
			
		||||
        @apiName Import
 | 
			
		||||
        @apiGroup Watch
 | 
			
		||||
        @apiSuccess (200) {List} OK List of watch UUIDs added
 | 
			
		||||
        @apiSuccess (500) {String} ERR Some other error
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        extras = {}
 | 
			
		||||
 | 
			
		||||
        if request.args.get('proxy'):
 | 
			
		||||
            plist = self.datastore.proxy_list
 | 
			
		||||
            if not request.args.get('proxy') in plist:
 | 
			
		||||
                return "Invalid proxy choice, currently supported proxies are '{}'".format(', '.join(plist)), 400
 | 
			
		||||
            else:
 | 
			
		||||
                extras['proxy'] = request.args.get('proxy')
 | 
			
		||||
 | 
			
		||||
        dedupe = strtobool(request.args.get('dedupe', 'true'))
 | 
			
		||||
 | 
			
		||||
        tags = request.args.get('tag')
 | 
			
		||||
        tag_uuids = request.args.get('tag_uuids')
 | 
			
		||||
 | 
			
		||||
        if tag_uuids:
 | 
			
		||||
            tag_uuids = tag_uuids.split(',')
 | 
			
		||||
 | 
			
		||||
        urls = request.get_data().decode('utf8').splitlines()
 | 
			
		||||
        added = []
 | 
			
		||||
        allow_simplehost = not strtobool(os.getenv('BLOCK_SIMPLEHOSTS', 'False'))
 | 
			
		||||
        for url in urls:
 | 
			
		||||
            url = url.strip()
 | 
			
		||||
            if not len(url):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # If hosts that only contain alphanumerics are allowed ("localhost" for example)
 | 
			
		||||
            if not validators.url(url, simple_host=allow_simplehost):
 | 
			
		||||
                return f"Invalid or unsupported URL - {url}", 400
 | 
			
		||||
 | 
			
		||||
            if dedupe and self.datastore.url_exists(url):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            new_uuid = self.datastore.add_watch(url=url, extras=extras, tag=tags, tag_uuids=tag_uuids)
 | 
			
		||||
            added.append(new_uuid)
 | 
			
		||||
 | 
			
		||||
        return added
 | 
			
		||||
 | 
			
		||||
class SystemInfo(Resource):
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        # datastore is a black box dependency
 | 
			
		||||
        self.datastore = kwargs['datastore']
 | 
			
		||||
        self.update_q = kwargs['update_q']
 | 
			
		||||
 | 
			
		||||
    @auth.check_token
 | 
			
		||||
    def get(self):
 | 
			
		||||
        """
 | 
			
		||||
        @api {get} /api/v1/systeminfo Return system info
 | 
			
		||||
        @apiDescription Return some info about the current system state
 | 
			
		||||
        @apiExample {curl} Example usage:
 | 
			
		||||
            curl http://localhost:5000/api/v1/systeminfo -H"x-api-key:813031b16330fe25e3780cf0325daa45"
 | 
			
		||||
            HTTP/1.0 200
 | 
			
		||||
            {
 | 
			
		||||
                'queue_size': 10 ,
 | 
			
		||||
                'overdue_watches': ["watch-uuid-list"],
 | 
			
		||||
                'uptime': 38344.55,
 | 
			
		||||
                'watch_count': 800,
 | 
			
		||||
                'version': "0.40.1"
 | 
			
		||||
            }
 | 
			
		||||
        @apiName Get Info
 | 
			
		||||
        @apiGroup System Information
 | 
			
		||||
        """
 | 
			
		||||
        import time
 | 
			
		||||
        overdue_watches = []
 | 
			
		||||
 | 
			
		||||
        # Check all watches and report which have not been checked but should have been
 | 
			
		||||
 | 
			
		||||
        for uuid, watch in self.datastore.data.get('watching', {}).items():
 | 
			
		||||
            # see if now - last_checked is greater than the time that should have been
 | 
			
		||||
            # this is not super accurate (maybe they just edited it) but better than nothing
 | 
			
		||||
            t = watch.threshold_seconds()
 | 
			
		||||
            if not t:
 | 
			
		||||
                # Use the system wide default
 | 
			
		||||
                t = self.datastore.threshold_seconds
 | 
			
		||||
 | 
			
		||||
            time_since_check = time.time() - watch.get('last_checked')
 | 
			
		||||
 | 
			
		||||
            # Allow 5 minutes of grace time before we decide it's overdue
 | 
			
		||||
            if time_since_check - (5 * 60) > t:
 | 
			
		||||
                overdue_watches.append(uuid)
 | 
			
		||||
        from changedetectionio import __version__ as main_version
 | 
			
		||||
        return {
 | 
			
		||||
                   'queue_size': self.update_q.qsize(),
 | 
			
		||||
                   'overdue_watches': overdue_watches,
 | 
			
		||||
                   'uptime': round(time.time() - self.datastore.start_time, 2),
 | 
			
		||||
                   'watch_count': len(self.datastore.data.get('watching', {})),
 | 
			
		||||
                   'version': main_version
 | 
			
		||||
               }, 200
 | 
			
		||||
        return list, 200
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
import copy
 | 
			
		||||
from . import api_schema
 | 
			
		||||
from ..model import watch_base
 | 
			
		||||
 | 
			
		||||
# Build a JSON Schema atleast partially based on our Watch model
 | 
			
		||||
watch_base_config = watch_base()
 | 
			
		||||
schema = api_schema.build_watch_json_schema(watch_base_config)
 | 
			
		||||
 | 
			
		||||
schema_create_watch = copy.deepcopy(schema)
 | 
			
		||||
schema_create_watch['required'] = ['url']
 | 
			
		||||
 | 
			
		||||
schema_update_watch = copy.deepcopy(schema)
 | 
			
		||||
schema_update_watch['additionalProperties'] = False
 | 
			
		||||
 | 
			
		||||
# Tag schema is also based on watch_base since Tag inherits from it
 | 
			
		||||
schema_tag = copy.deepcopy(schema)
 | 
			
		||||
schema_create_tag = copy.deepcopy(schema_tag)
 | 
			
		||||
schema_create_tag['required'] = ['title']
 | 
			
		||||
schema_update_tag = copy.deepcopy(schema_tag)
 | 
			
		||||
schema_update_tag['additionalProperties'] = False
 | 
			
		||||
 | 
			
		||||
# Import all API resources
 | 
			
		||||
from .Watch import Watch, WatchHistory, WatchSingleHistory, CreateWatch
 | 
			
		||||
from .Tags import Tags, Tag
 | 
			
		||||
from .Import import Import
 | 
			
		||||
from .SystemInfo import SystemInfo
 | 
			
		||||
 
 | 
			
		||||
@@ -231,7 +231,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_updat
 | 
			
		||||
        elif (op == 'assign-tag'):
 | 
			
		||||
            op_extradata = request.form.get('op_extradata', '').strip()
 | 
			
		||||
            if op_extradata:
 | 
			
		||||
                tag_uuid = datastore.add_tag(name=op_extradata)
 | 
			
		||||
                tag_uuid = datastore.add_tag(title=op_extradata)
 | 
			
		||||
                if op_extradata and tag_uuid:
 | 
			
		||||
                    for uuid in uuids:
 | 
			
		||||
                        uuid = uuid.strip()
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
 | 
			
		||||
                    extra_update_obj['tags'] = form.data.get('tags')
 | 
			
		||||
                else:
 | 
			
		||||
                    for t in form.data.get('tags').split(','):
 | 
			
		||||
                        tag_uuids.append(datastore.add_tag(name=t))
 | 
			
		||||
                        tag_uuids.append(datastore.add_tag(title=t))
 | 
			
		||||
                    extra_update_obj['tags'] = tag_uuids
 | 
			
		||||
 | 
			
		||||
            datastore.data['watching'][uuid].update(form.data)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import flask_login
 | 
			
		||||
import locale
 | 
			
		||||
import os
 | 
			
		||||
@@ -34,7 +33,7 @@ from loguru import logger
 | 
			
		||||
 | 
			
		||||
from changedetectionio import __version__
 | 
			
		||||
from changedetectionio import queuedWatchMetaData
 | 
			
		||||
from changedetectionio.api import api_v1
 | 
			
		||||
from changedetectionio.api import Watch, WatchHistory, WatchSingleHistory, CreateWatch, Import, SystemInfo, Tag, Tags
 | 
			
		||||
from .time_handler import is_within_schedule
 | 
			
		||||
 | 
			
		||||
datastore = None
 | 
			
		||||
@@ -250,30 +249,32 @@ def changedetection_app(config=None, datastore_o=None):
 | 
			
		||||
                return login_manager.unauthorized()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.WatchSingleHistory,
 | 
			
		||||
    watch_api.add_resource(WatchSingleHistory,
 | 
			
		||||
                           '/api/v1/watch/<string:uuid>/history/<string:timestamp>',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.WatchHistory,
 | 
			
		||||
    watch_api.add_resource(WatchHistory,
 | 
			
		||||
                           '/api/v1/watch/<string:uuid>/history',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.CreateWatch, '/api/v1/watch',
 | 
			
		||||
    watch_api.add_resource(CreateWatch, '/api/v1/watch',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.Watch, '/api/v1/watch/<string:uuid>',
 | 
			
		||||
    watch_api.add_resource(Watch, '/api/v1/watch/<string:uuid>',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.SystemInfo, '/api/v1/systeminfo',
 | 
			
		||||
    watch_api.add_resource(SystemInfo, '/api/v1/systeminfo',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore, 'update_q': update_q})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(api_v1.Import,
 | 
			
		||||
    watch_api.add_resource(Import,
 | 
			
		||||
                           '/api/v1/import',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore})
 | 
			
		||||
 | 
			
		||||
    # Setup cors headers to allow all domains
 | 
			
		||||
    # https://flask-cors.readthedocs.io/en/latest/
 | 
			
		||||
    #    CORS(app)
 | 
			
		||||
    watch_api.add_resource(Tags, '/api/v1/tags',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore})
 | 
			
		||||
 | 
			
		||||
    watch_api.add_resource(Tag, '/api/v1/tag', '/api/v1/tag/<string:uuid>',
 | 
			
		||||
                           resource_class_kwargs={'datastore': datastore})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -571,16 +571,16 @@ class ChangeDetectionStore:
 | 
			
		||||
 | 
			
		||||
        return ret
 | 
			
		||||
 | 
			
		||||
    def add_tag(self, name):
 | 
			
		||||
    def add_tag(self, title):
 | 
			
		||||
        # If name exists, return that
 | 
			
		||||
        n = name.strip().lower()
 | 
			
		||||
        n = title.strip().lower()
 | 
			
		||||
        logger.debug(f">>> Adding new tag - '{n}'")
 | 
			
		||||
        if not n:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        for uuid, tag in self.__data['settings']['application'].get('tags', {}).items():
 | 
			
		||||
            if n == tag.get('title', '').lower().strip():
 | 
			
		||||
                logger.warning(f"Tag '{name}' already exists, skipping creation.")
 | 
			
		||||
                logger.warning(f"Tag '{title}' already exists, skipping creation.")
 | 
			
		||||
                return uuid
 | 
			
		||||
 | 
			
		||||
        # Eventually almost everything todo with a watch will apply as a Tag
 | 
			
		||||
@@ -588,7 +588,7 @@ class ChangeDetectionStore:
 | 
			
		||||
        with self.lock:
 | 
			
		||||
            from .model import Tag
 | 
			
		||||
            new_tag = Tag.model(datastore_path=self.datastore_path, default={
 | 
			
		||||
                'title': name.strip(),
 | 
			
		||||
                'title': title.strip(),
 | 
			
		||||
                'date_created': int(time.time())
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
@@ -847,7 +847,7 @@ class ChangeDetectionStore:
 | 
			
		||||
            if tag:
 | 
			
		||||
                tag_uuids = []
 | 
			
		||||
                for t in tag.split(','):
 | 
			
		||||
                    tag_uuids.append(self.add_tag(name=t))
 | 
			
		||||
                    tag_uuids.append(self.add_tag(title=t))
 | 
			
		||||
 | 
			
		||||
                self.data['watching'][uuid]['tags'] = tag_uuids
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								changedetectionio/tests/test_api_tags.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								changedetectionio/tests/test_api_tags.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
 | 
			
		||||
from flask import url_for
 | 
			
		||||
from .util import live_server_setup, wait_for_all_checks
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
def test_api_tags_listing(client, live_server, measure_memory_usage):
 | 
			
		||||
    live_server_setup(live_server)
 | 
			
		||||
    api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
 | 
			
		||||
    tag_title = 'Test Tag'
 | 
			
		||||
 | 
			
		||||
    # Get a listing
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tags"),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.text.strip() == "{}", "Should be empty list"
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("tag"),
 | 
			
		||||
        data=json.dumps({"title": tag_title}),
 | 
			
		||||
        headers={'content-type': 'application/json', 'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 201
 | 
			
		||||
 | 
			
		||||
    new_tag_uuid = res.json.get('uuid')
 | 
			
		||||
 | 
			
		||||
    # List tags - should include our new tag
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tags"),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert new_tag_uuid in res.text
 | 
			
		||||
    assert res.json[new_tag_uuid]['title'] == tag_title
 | 
			
		||||
    assert res.json[new_tag_uuid]['notification_muted'] == False
 | 
			
		||||
 | 
			
		||||
    # Get single tag
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert res.json['title'] == tag_title
 | 
			
		||||
 | 
			
		||||
    # Update tag
 | 
			
		||||
    res = client.put(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        data=json.dumps({"title": "Updated Tag"}),
 | 
			
		||||
        headers={'content-type': 'application/json', 'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert b'OK' in res.data
 | 
			
		||||
 | 
			
		||||
    # Verify update worked
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert res.json['title'] == 'Updated Tag'
 | 
			
		||||
 | 
			
		||||
    # Mute tag notifications
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid) + "?muted=muted",
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert b'OK' in res.data
 | 
			
		||||
 | 
			
		||||
    # Verify muted status
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert res.json['notification_muted'] == True
 | 
			
		||||
 | 
			
		||||
    # Unmute tag
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid) + "?muted=unmuted",
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert b'OK' in res.data
 | 
			
		||||
 | 
			
		||||
    # Verify unmuted status
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert res.json['notification_muted'] == False
 | 
			
		||||
 | 
			
		||||
    # Create a watch with the tag and check it matches UUID
 | 
			
		||||
    test_url = url_for('test_endpoint', _external=True)
 | 
			
		||||
    res = client.post(
 | 
			
		||||
        url_for("createwatch"),
 | 
			
		||||
        data=json.dumps({"url": test_url, "tag": "Updated Tag", "title": "Watch with tag"}),
 | 
			
		||||
        headers={'content-type': 'application/json', 'x-api-key': api_key},
 | 
			
		||||
        follow_redirects=True
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 201
 | 
			
		||||
    watch_uuid = res.json.get('uuid')
 | 
			
		||||
 | 
			
		||||
    # Verify tag is associated with watch by name if need be
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("watch", uuid=watch_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert new_tag_uuid in res.json.get('tags', [])
 | 
			
		||||
 | 
			
		||||
    # Delete tag
 | 
			
		||||
    res = client.delete(
 | 
			
		||||
        url_for("tag", uuid=new_tag_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 204
 | 
			
		||||
 | 
			
		||||
    # Verify tag is gone
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("tags"),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert new_tag_uuid not in res.text
 | 
			
		||||
 | 
			
		||||
    # Verify tag was removed from watch
 | 
			
		||||
    res = client.get(
 | 
			
		||||
        url_for("watch", uuid=watch_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key}
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 200
 | 
			
		||||
    assert new_tag_uuid not in res.json.get('tags', [])
 | 
			
		||||
 | 
			
		||||
    # Delete the watch
 | 
			
		||||
    res = client.delete(
 | 
			
		||||
        url_for("watch", uuid=watch_uuid),
 | 
			
		||||
        headers={'x-api-key': api_key},
 | 
			
		||||
    )
 | 
			
		||||
    assert res.status_code == 204
 | 
			
		||||
		Reference in New Issue
	
	Block a user