diff --git a/changedetectionio/conditions.py b/changedetectionio/conditions.py
new file mode 100644
index 000000000..9a28a9f9d
--- /dev/null
+++ b/changedetectionio/conditions.py
@@ -0,0 +1,95 @@
+from json_logic import jsonLogic
+from json_logic.builtins import BUILTINS
+import re
+
+# List of all supported JSON Logic operators
+operator_choices = [
+ (">", "Greater Than"),
+ ("<", "Less Than"),
+ (">=", "Greater Than or Equal To"),
+ ("<=", "Less Than or Equal To"),
+ ("==", "Equals"),
+ ("!=", "Not Equals"),
+ ("in", "Contains"),
+ ("!in", "Does Not Contain"),
+ ("contains_regex", "Text Matches Regex"),
+ ("!contains_regex", "Text Does NOT Match Regex"),
+ ("changed > minutes", "Changed more than X minutes ago"),
+# ("watch_uuid_changed", "Watch UUID had unviewed change"),
+# ("watch_uuid_not_changed", "Watch UUID did NOT have unviewed change"),
+# ("!!", "Is Truthy"),
+# ("!", "Is Falsy"),
+# ("and", "All Conditions Must Be True"),
+# ("or", "At Least One Condition Must Be True"),
+# ("max", "Maximum of Values"),
+# ("min", "Minimum of Values"),
+# ("+", "Addition"),
+# ("-", "Subtraction"),
+# ("*", "Multiplication"),
+# ("/", "Division"),
+# ("%", "Modulo"),
+# ("log", "Logarithm"),
+# ("if", "Conditional If-Else")
+]
+
+# Fields available in the rules
+field_choices = [
+ ("extracted_number", "Extracted Number"),
+ ("diff_removed", "Diff Removed"),
+ ("diff_added", "Diff Added"),
+ ("page_text", "Page Text"),
+ ("page_title", "Page Title"),
+ ("watch_uuid", "Watch UUID"),
+ ("watch_history_length", "History Length"),
+ ("watch_history", "All Watch Text History"),
+ ("watch_check_count", "Watch Check Count")
+]
+
+
+# ✅ Custom function for case-insensitive regex matching
+def contains_regex(_, text, pattern):
+ """Returns True if `text` contains `pattern` (case-insensitive regex match)."""
+ return bool(re.search(pattern, text, re.IGNORECASE))
+
+# ✅ Custom function for NOT matching case-insensitive regex
+def not_contains_regex(_, text, pattern):
+ """Returns True if `text` does NOT contain `pattern` (case-insensitive regex match)."""
+ return not bool(re.search(pattern, text, re.IGNORECASE))
+
+
+# ✅ Custom function to check if "watch_uuid" has changed
+def watch_uuid_changed(_, previous_uuid, current_uuid):
+ """Returns True if the watch UUID has changed."""
+ return previous_uuid != current_uuid
+
+# ✅ Custom function to check if "watch_uuid" has NOT changed
+def watch_uuid_not_changed(_, previous_uuid, current_uuid):
+ """Returns True if the watch UUID has NOT changed."""
+ return previous_uuid == current_uuid
+
+# Define the extended operations dictionary
+CUSTOM_OPERATIONS = {
+ **BUILTINS, # Include all standard operators
+ "watch_uuid_changed": watch_uuid_changed,
+ "watch_uuid_not_changed": watch_uuid_not_changed,
+ "contains_regex": contains_regex,
+ "!contains_regex": not_contains_regex
+}
+
+
+
+def run(ruleset, data):
+ """
+ Execute a JSON Logic rule against given data.
+
+ :param ruleset: JSON Logic rule dictionary.
+ :param data: Dictionary containing the facts.
+ :return: Boolean result of rule evaluation.
+ """
+
+
+ try:
+ return jsonLogic(ruleset, data, CUSTOM_OPERATIONS)
+ except Exception as e:
+ print(f"❌ Error evaluating JSON Logic: {e}")
+ return False
diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py
index f603e0121..713163fb6 100644
--- a/changedetectionio/flask_app.py
+++ b/changedetectionio/flask_app.py
@@ -758,6 +758,25 @@ def changedetection_app(config=None, datastore_o=None):
for p in datastore.proxy_list:
form.proxy.choices.append(tuple((p, datastore.proxy_list[p]['label'])))
+ # Example JSON Rule
+ DEFAULT_RULE = {
+ "and": [
+ {">": [{"var": "extracted_number"}, 5000]},
+ {"<": [{"var": "extracted_number"}, 80000]},
+ {"in": ["rock", {"var": "page_text"}]}
+ ]
+ }
+ form.conditions.pop_entry() # Remove the default empty row
+ for condition in DEFAULT_RULE["and"]:
+ operator, values = list(condition.items())[0]
+ field = values[0]["var"] if isinstance(values[0], dict) else values[1]["var"]
+ value = values[1] if isinstance(values[1], (str, int)) else values[0]
+
+ form.conditions.append_entry({
+ "operator": operator,
+ "field": field,
+ "value": value
+ })
if request.method == 'POST' and form.validate():
@@ -793,6 +812,19 @@ def changedetection_app(config=None, datastore_o=None):
extra_update_obj['filter_text_replaced'] = True
extra_update_obj['filter_text_removed'] = True
+ # Convert form input into JSON Logic format
+ extra_update_obj["conditions"] = {
+ "and": [
+ {
+ form.conditions[i].operator.data: [
+ {"var": form.conditions[i].field.data},
+ form.conditions[i].value.data
+ ]
+ }
+ for i in range(len(form.conditions))
+ ]
+ }
+
# Because wtforms doesn't support accessing other data in process_ , but we convert the CSV list of tags back to a list of UUIDs
tag_uuids = []
if form.data.get('tags'):
diff --git a/changedetectionio/forms.py b/changedetectionio/forms.py
index 11792d622..0a099e4ee 100644
--- a/changedetectionio/forms.py
+++ b/changedetectionio/forms.py
@@ -4,6 +4,10 @@ from loguru import logger
from wtforms.widgets.core import TimeInput
from changedetectionio.strtobool import strtobool
+from flask_wtf import FlaskForm
+from wtforms import SelectField, StringField, SubmitField, FieldList, FormField
+from wtforms.validators import DataRequired, URL
+from flask_wtf.file import FileField
from wtforms import (
BooleanField,
@@ -509,6 +513,23 @@ class quickWatchForm(Form):
edit_and_watch_submit_button = SubmitField('Edit > Watch', render_kw={"class": "pure-button pure-button-primary"})
+
+# Condition Rule Form (for each rule row)
+class ConditionForm(FlaskForm):
+ from .conditions import operator_choices, field_choices
+
+ operator = SelectField(
+ "Operator",
+ choices=operator_choices,
+ validators=[DataRequired()]
+ )
+ field = SelectField(
+ "Field",
+ choices=field_choices,
+ validators=[DataRequired()]
+ )
+ value = StringField("Value", validators=[DataRequired()])
+
# Common to a single watch and the global settings
class commonSettingsForm(Form):
from . import processors
@@ -596,6 +617,9 @@ class processor_text_json_diff_form(commonSettingsForm):
notification_muted = BooleanField('Notifications Muted / Off', default=False)
notification_screenshot = BooleanField('Attach screenshot to notification (where possible)', default=False)
+ conditions = FieldList(FormField(ConditionForm), min_entries=1) # Add rule logic here
+
+
def extra_tab_content(self):
return None
diff --git a/changedetectionio/templates/edit.html b/changedetectionio/templates/edit.html
index f970e608c..6ffe2a44a 100644
--- a/changedetectionio/templates/edit.html
+++ b/changedetectionio/templates/edit.html
@@ -6,6 +6,39 @@
+
+
+
+
+