kopia lustrzana https://github.com/dgtlmoon/changedetection.io
				
				
				
			
		
			
				
	
	
		
			194 wiersze
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			194 wiersze
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Python
		
	
	
| 
 | |
| import time
 | |
| import apprise
 | |
| from loguru import logger
 | |
| from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL
 | |
| 
 | |
| def process_notification(n_object, datastore):
 | |
|     from changedetectionio.jinja2_custom import render as jinja_render
 | |
|     from . import default_notification_format_for_watch, default_notification_format, valid_notification_formats
 | |
|     # be sure its registered
 | |
|     from .apprise_plugin.custom_handlers import apprise_http_custom_handler
 | |
| 
 | |
|     now = time.time()
 | |
|     if n_object.get('notification_timestamp'):
 | |
|         logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s")
 | |
|     # Insert variables into the notification content
 | |
|     notification_parameters = create_notification_parameters(n_object, datastore)
 | |
| 
 | |
|     n_format = valid_notification_formats.get(
 | |
|         n_object.get('notification_format', default_notification_format),
 | |
|         valid_notification_formats[default_notification_format],
 | |
|     )
 | |
| 
 | |
|     # If we arrived with 'System default' then look it up
 | |
|     if n_format == default_notification_format_for_watch and datastore.data['settings']['application'].get('notification_format') != default_notification_format_for_watch:
 | |
|         # Initially text or whatever
 | |
|         n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
 | |
| 
 | |
|     logger.trace(f"Complete notification body including Jinja and placeholders calculated in  {time.time() - now:.2f}s")
 | |
| 
 | |
|     # https://github.com/caronc/apprise/wiki/Development_LogCapture
 | |
|     # Anything higher than or equal to WARNING (which covers things like Connection errors)
 | |
|     # raise it as an exception
 | |
| 
 | |
|     sent_objs = []
 | |
| 
 | |
|     if 'as_async' in n_object:
 | |
|         apprise_asset.async_mode = n_object.get('as_async')
 | |
| 
 | |
|     apobj = apprise.Apprise(debug=True, asset=apprise_asset)
 | |
| 
 | |
|     if not n_object.get('notification_urls'):
 | |
|         return None
 | |
| 
 | |
|     with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
 | |
|         for url in n_object['notification_urls']:
 | |
| 
 | |
|             # Get the notification body from datastore
 | |
|             n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
 | |
|             if n_object.get('notification_format', '').startswith('HTML'):
 | |
|                 n_body = n_body.replace("\n", '<br>')
 | |
| 
 | |
|             n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)
 | |
| 
 | |
|             url = url.strip()
 | |
|             if url.startswith('#'):
 | |
|                 logger.trace(f"Skipping commented out notification URL - {url}")
 | |
|                 continue
 | |
| 
 | |
|             if not url:
 | |
|                 logger.warning(f"Process Notification: skipping empty notification URL.")
 | |
|                 continue
 | |
| 
 | |
|             logger.info(f">> Process Notification: AppRise notifying {url}")
 | |
|             url = jinja_render(template_str=url, **notification_parameters)
 | |
| 
 | |
|             # Re 323 - Limit discord length to their 2000 char limit total or it wont send.
 | |
|             # Because different notifications may require different pre-processing, run each sequentially :(
 | |
|             # 2000 bytes minus -
 | |
|             #     200 bytes for the overhead of the _entire_ json payload, 200 bytes for {tts, wait, content} etc headers
 | |
|             #     Length of URL - Incase they specify a longer custom avatar_url
 | |
| 
 | |
|             # So if no avatar_url is specified, add one so it can be correctly calculated into the total payload
 | |
|             k = '?' if not '?' in url else '&'
 | |
|             if not 'avatar_url' in url \
 | |
|                     and not url.startswith('mail') \
 | |
|                     and not url.startswith('post') \
 | |
|                     and not url.startswith('get') \
 | |
|                     and not url.startswith('delete') \
 | |
|                     and not url.startswith('put'):
 | |
|                 url += k + f"avatar_url={APPRISE_AVATAR_URL}"
 | |
| 
 | |
|             if url.startswith('tgram://'):
 | |
|                 # Telegram only supports a limit subset of HTML, remove the '<br>' we place in.
 | |
|                 # re https://github.com/dgtlmoon/changedetection.io/issues/555
 | |
|                 # @todo re-use an existing library we have already imported to strip all non-allowed tags
 | |
|                 n_body = n_body.replace('<br>', '\n')
 | |
|                 n_body = n_body.replace('</br>', '\n')
 | |
|                 # real limit is 4096, but minus some for extra metadata
 | |
|                 payload_max_size = 3600
 | |
|                 body_limit = max(0, payload_max_size - len(n_title))
 | |
|                 n_title = n_title[0:payload_max_size]
 | |
|                 n_body = n_body[0:body_limit]
 | |
| 
 | |
|             elif url.startswith('discord://') or url.startswith('https://discordapp.com/api/webhooks') or url.startswith(
 | |
|                     'https://discord.com/api'):
 | |
|                 # real limit is 2000, but minus some for extra metadata
 | |
|                 payload_max_size = 1700
 | |
|                 body_limit = max(0, payload_max_size - len(n_title))
 | |
|                 n_title = n_title[0:payload_max_size]
 | |
|                 n_body = n_body[0:body_limit]
 | |
| 
 | |
|             elif url.startswith('mailto'):
 | |
|                 # Apprise will default to HTML, so we need to override it
 | |
|                 # So that whats' generated in n_body is in line with what is going to be sent.
 | |
|                 # https://github.com/caronc/apprise/issues/633#issuecomment-1191449321
 | |
|                 if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'):
 | |
|                     prefix = '?' if not '?' in url else '&'
 | |
|                     # Apprise format is lowercase text https://github.com/caronc/apprise/issues/633
 | |
|                     n_format = n_format.lower()
 | |
|                     url = f"{url}{prefix}format={n_format}"
 | |
|                 # If n_format == HTML, then apprise email should default to text/html and we should be sending HTML only
 | |
| 
 | |
|             apobj.add(url)
 | |
| 
 | |
|             sent_objs.append({'title': n_title,
 | |
|                               'body': n_body,
 | |
|                               'url': url,
 | |
|                               'body_format': n_format})
 | |
| 
 | |
|         # Blast off the notifications tht are set in .add()
 | |
|         apobj.notify(
 | |
|             title=n_title,
 | |
|             body=n_body,
 | |
|             body_format=n_format,
 | |
|             # False is not an option for AppRise, must be type None
 | |
|             attach=n_object.get('screenshot', None)
 | |
|         )
 | |
| 
 | |
| 
 | |
|         # Returns empty string if nothing found, multi-line string otherwise
 | |
|         log_value = logs.getvalue()
 | |
| 
 | |
|         if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
 | |
|             logger.critical(log_value)
 | |
|             raise Exception(log_value)
 | |
| 
 | |
|     # Return what was sent for better logging - after the for loop
 | |
|     return sent_objs
 | |
| 
 | |
| 
 | |
| # Notification title + body content parameters get created here.
 | |
| # ( Where we prepare the tokens in the notification to be replaced with actual values )
 | |
| def create_notification_parameters(n_object, datastore):
 | |
|     from copy import deepcopy
 | |
|     from . import valid_tokens
 | |
| 
 | |
|     # in the case we send a test notification from the main settings, there is no UUID.
 | |
|     uuid = n_object['uuid'] if 'uuid' in n_object else ''
 | |
| 
 | |
|     if uuid:
 | |
|         watch_title = datastore.data['watching'][uuid].label
 | |
|         tag_list = []
 | |
|         tags = datastore.get_all_tags_for_watch(uuid)
 | |
|         if tags:
 | |
|             for tag_uuid, tag in tags.items():
 | |
|                 tag_list.append(tag.get('title'))
 | |
|         watch_tag = ', '.join(tag_list)
 | |
|     else:
 | |
|         watch_title = 'Change Detection'
 | |
|         watch_tag = ''
 | |
| 
 | |
|     # Create URLs to customise the notification with
 | |
|     # active_base_url - set in store.py data property
 | |
|     base_url = datastore.data['settings']['application'].get('active_base_url')
 | |
| 
 | |
|     watch_url = n_object['watch_url']
 | |
| 
 | |
|     diff_url = "{}/diff/{}".format(base_url, uuid)
 | |
|     preview_url = "{}/preview/{}".format(base_url, uuid)
 | |
| 
 | |
|     # Not sure deepcopy is needed here, but why not
 | |
|     tokens = deepcopy(valid_tokens)
 | |
| 
 | |
|     # Valid_tokens also used as a field validator
 | |
|     tokens.update(
 | |
|         {
 | |
|             'base_url': base_url,
 | |
|             'diff_url': diff_url,
 | |
|             'preview_url': preview_url,
 | |
|             'watch_tag': watch_tag if watch_tag is not None else '',
 | |
|             'watch_title': watch_title if watch_title is not None else '',
 | |
|             'watch_url': watch_url,
 | |
|             'watch_uuid': uuid,
 | |
|         })
 | |
| 
 | |
|     # n_object will contain diff, diff_added etc etc
 | |
|     tokens.update(n_object)
 | |
| 
 | |
|     if uuid:
 | |
|         tokens.update(datastore.data['watching'].get(uuid).extra_notification_token_values())
 | |
| 
 | |
|     return tokens
 |