mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-01-29 10:26:01 +00:00
Multi-language / Translations Support (#3696) - Complete internationalization system implemented - Support for 7 languages: Czech (cs), German (de), French (fr), Italian (it), Korean (ko), Chinese Simplified (zh), Chinese Traditional (zh_TW) - Language selector with localized flags and theming - Flash message translations - Multiple translation fixes and improvements across all languages - Language setting preserved across redirects Pluggable Content Fetchers (#3653) - New architecture for extensible content fetcher system - Allows custom fetcher implementations Image / Screenshot Comparison Processor (#3680) - New processor for visual change detection (disabled for this release) - Supporting CSS/JS infrastructure added UI Improvements Design & Layout - Auto-generated tag color schemes - Simplified login form styling - Removed hard-coded CSS, moved to SCSS variables - Tag UI cleanup and improvements - Automatic tab wrapper functionality - Menu refactoring for better organization - Cleanup of offset settings - Hide sticky tabs on narrow viewports - Improved responsive layout (#3702) User Experience - Modal alerts/confirmations on delete/clear operations (#3693, #3598, #3382) - Auto-add https:// to URLs in quickwatch form if not present - Better redirect handling on login (#3699) - 'Recheck all' now returns to correct group/tag (#3673) - Language set redirect keeps hash fragment - More friendly human-readable text throughout UI Performance & Reliability Scheduler & Processing - Soft delays instead of blocking time.sleep() calls (#3710) - More resilient handling of same UUID being processed (#3700) - Better Puppeteer timeout handling - Improved Puppeteer shutdown/cleanup (#3692) - Requests cleanup now properly async History & Rendering - Faster server-side "difference" rendering on History page (#3442) - Show ignored/triggered rows in history - API: Retry watch data if watch dict changed (more reliable) API Improvements - Watch get endpoint: retry mechanism for changed watch data - WatchHistoryDiff API endpoint includes extra format args (#3703) Testing Improvements - Replace time.sleep with wait_for_notification_endpoint_output (#3716) - Test for mode switching (#3701) - Test for #3720 added (#3725) - Extract-text difference test fixes - Improved dev workflow Bug Fixes - Notification error text output (#3672, #3669, #3280) - HTML validation fixes (#3704) - Template discovery path fixes - Notification debug log now uses system locale for dates/times - Puppeteer spelling mistake in log output - Recalculation on anchor change - Queue bubble update disabled temporarily Dependency Updates - beautifulsoup4 updated (#3724) - psutil 7.1.0 → 7.2.1 (#3723) - python-engineio ~=4.12.3 → ~=4.13.0 (#3707) - python-socketio ~=5.14.3 → ~=5.16.0 (#3706) - flask-socketio ~=5.5.1 → ~=5.6.0 (#3691) - brotli ~=1.1 → ~=1.2 (#3687) - lxml updated (#3590) - pytest ~=7.2 → ~=9.0 (#3676) - jsonschema ~=4.0 → ~=4.25 (#3618) - pluggy ~=1.5 → ~=1.6 (#3616) - cryptography 44.0.1 → 46.0.3 (security) (#3589) Documentation - README updated with viewport size setup information Development Infrastructure - Dev container only built on dev branch - Improved dev workflow tooling
132 lines
7.3 KiB
Python
132 lines
7.3 KiB
Python
from flask import Blueprint, request, make_response
|
|
import random
|
|
from loguru import logger
|
|
|
|
from changedetectionio.store import ChangeDetectionStore
|
|
from changedetectionio.auth_decorator import login_optionally_required
|
|
|
|
def construct_blueprint(datastore: ChangeDetectionStore):
|
|
notification_blueprint = Blueprint('ui_notification', __name__, template_folder="../ui/templates")
|
|
|
|
# AJAX endpoint for sending a test
|
|
@notification_blueprint.route("/notification/send-test/<string:watch_uuid>", methods=['POST'])
|
|
@notification_blueprint.route("/notification/send-test", methods=['POST'])
|
|
@notification_blueprint.route("/notification/send-test/", methods=['POST'])
|
|
@login_optionally_required
|
|
def ajax_callback_send_notification_test(watch_uuid=None):
|
|
from changedetectionio.notification_service import NotificationContextData, set_basic_notification_vars
|
|
# Watch_uuid could be unset in the case it`s used in tag editor, global settings
|
|
import apprise
|
|
from changedetectionio.notification.handler import process_notification
|
|
from changedetectionio.notification.apprise_plugin.assets import apprise_asset
|
|
from changedetectionio.jinja2_custom import render as jinja_render
|
|
|
|
from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler
|
|
|
|
apobj = apprise.Apprise(asset=apprise_asset)
|
|
|
|
is_global_settings_form = request.args.get('mode', '') == 'global-settings'
|
|
is_group_settings_form = request.args.get('mode', '') == 'group-settings'
|
|
|
|
# Use an existing random one on the global/main settings form
|
|
if not watch_uuid and (is_global_settings_form or is_group_settings_form) \
|
|
and datastore.data.get('watching'):
|
|
logger.debug(f"Send test notification - Choosing random Watch {watch_uuid}")
|
|
watch_uuid = random.choice(list(datastore.data['watching'].keys()))
|
|
|
|
if not watch_uuid:
|
|
return make_response("Error: You must have atleast one watch configured for 'test notification' to work", 400)
|
|
|
|
watch = datastore.data['watching'].get(watch_uuid)
|
|
notification_urls = request.form.get('notification_urls','').strip().splitlines()
|
|
|
|
if not notification_urls:
|
|
logger.debug("Test notification - Trying by group/tag in the edit form if available")
|
|
# On an edit page, we should also fire off to the tags if they have notifications
|
|
if request.form.get('tags') and request.form['tags'].strip():
|
|
for k in request.form['tags'].split(','):
|
|
tag = datastore.tag_exists_by_name(k.strip())
|
|
notification_urls = tag.get('notifications_urls') if tag and tag.get('notifications_urls') else None
|
|
|
|
if not notification_urls and not is_global_settings_form and not is_group_settings_form:
|
|
# In the global settings, use only what is typed currently in the text box
|
|
logger.debug("Test notification - Trying by global system settings notifications")
|
|
if datastore.data['settings']['application'].get('notification_urls'):
|
|
notification_urls = datastore.data['settings']['application']['notification_urls']
|
|
|
|
if not notification_urls:
|
|
return 'Error: No Notification URLs set/found'
|
|
|
|
for n_url in notification_urls:
|
|
# We are ONLY validating the apprise:// part here, convert all tags to something so as not to break apprise URLs
|
|
generic_notification_context_data = NotificationContextData()
|
|
generic_notification_context_data.set_random_for_validation()
|
|
n_url = jinja_render(template_str=n_url, **generic_notification_context_data).strip()
|
|
if len(n_url.strip()):
|
|
if not apobj.add(n_url):
|
|
return f'Error: {n_url} is not a valid AppRise URL.'
|
|
|
|
try:
|
|
# use the same as when it is triggered, but then override it with the form test values
|
|
n_object = NotificationContextData({
|
|
'watch_url': request.form.get('window_url', "https://changedetection.io"),
|
|
'notification_urls': notification_urls
|
|
})
|
|
|
|
# Only use if present, if not set in n_object it should use the default system value
|
|
if 'notification_format' in request.form and request.form['notification_format'].strip():
|
|
n_object['notification_format'] = request.form.get('notification_format', '').strip()
|
|
else:
|
|
n_object['notification_format'] = datastore.data['settings']['application'].get('notification_format')
|
|
|
|
if 'notification_title' in request.form and request.form['notification_title'].strip():
|
|
n_object['notification_title'] = request.form.get('notification_title', '').strip()
|
|
elif datastore.data['settings']['application'].get('notification_title'):
|
|
n_object['notification_title'] = datastore.data['settings']['application'].get('notification_title')
|
|
else:
|
|
n_object['notification_title'] = "Test title"
|
|
|
|
if 'notification_body' in request.form and request.form['notification_body'].strip():
|
|
n_object['notification_body'] = request.form.get('notification_body', '').strip()
|
|
elif datastore.data['settings']['application'].get('notification_body'):
|
|
n_object['notification_body'] = datastore.data['settings']['application'].get('notification_body')
|
|
else:
|
|
n_object['notification_body'] = "Test body"
|
|
|
|
n_object['as_async'] = False
|
|
|
|
# Same like in notification service, should be refactored
|
|
dates = list(watch.history.keys())
|
|
trigger_text = ''
|
|
snapshot_contents = ''
|
|
|
|
# Could be called as a 'test notification' with only 1 snapshot available
|
|
prev_snapshot = "Example text: example test\nExample text: change detection is cool\nExample text: some more examples\n"
|
|
current_snapshot = "Example text: example test\nExample text: change detection is fantastic\nExample text: even more examples\nExample text: a lot more examples"
|
|
|
|
if len(dates) > 1:
|
|
prev_snapshot = watch.get_history_snapshot(timestamp=dates[-2])
|
|
current_snapshot = watch.get_history_snapshot(timestamp=dates[-1])
|
|
|
|
n_object.update(set_basic_notification_vars(current_snapshot=current_snapshot,
|
|
prev_snapshot=prev_snapshot,
|
|
watch=watch,
|
|
triggered_text=trigger_text,
|
|
timestamp_changed=dates[-1] if dates else None))
|
|
|
|
|
|
sent_obj = process_notification(n_object, datastore)
|
|
|
|
except Exception as e:
|
|
logger.error(e)
|
|
e_str = str(e)
|
|
# Remove this text which is not important and floods the container
|
|
e_str = e_str.replace(
|
|
"DEBUG - <class 'apprise.decorators.base.CustomNotifyPlugin.instantiate_plugin.<locals>.CustomNotifyPluginWrapper'>",
|
|
'')
|
|
|
|
return make_response(e_str, 400)
|
|
|
|
return 'OK - Sent test notifications'
|
|
|
|
return notification_blueprint |