Repository: dgtlmoon/changedetection.io (Python)

Issue:
[feature] Use standard diff format
**Version and OS**
0.41.1, Linux, pip

**Is your feature request related to a problem? Please describe.**
Diff is displayed in a non-standard format :
```
(changed) foo
(into) bar
```

**Describe the solution you'd like**
Diff would be displayed in the standard format :
```
--- https://changedetection.io/settings#notifications	4/20/2023, 7:00:00 AM
+++ https://changedetection.io/settings#notifications	4/20/2023, 7:15:00 AM
@@ -1,1 +1,1 @@
- foo
+ bar
```

**Describe the use-case and give concrete real-world examples**
This would provide syntax highlighting on apps that supports said format, like Discord :
```diff
--- https://changedetection.io/settings#notifications	4/20/2023, 7:00:00 AM
+++ https://changedetection.io/settings#notifications	4/20/2023, 7:15:00 AM
@@ -1,1 +1,1 @@
- foo
+ bar
```

**Additional context**
None

---
Thanks


Discussion:
actually nice idea!
Would very much like the HTML diff in email notifications if possible, please.  
Could be a new token in the notification body like `{{ diff_patch }}`



Patch:
diff --git a/changedetectionio/diff.py b/changedetectionio/diff.py
index 9cb4c9fe57a..3371ad4de47 100644
--- a/changedetectionio/diff.py
+++ b/changedetectionio/diff.py
@@ -35,15 +35,19 @@ def customSequenceMatcher(before, after, include_equal=False, include_removed=Tr
 
 # only_differences - only return info about the differences, no context
 # line_feed_sep could be "<br>" or "<li>" or "\n" etc
-def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True):
+def render_diff(previous_version_file_contents, newest_version_file_contents, include_equal=False, include_removed=True, include_added=True, include_replaced=True, line_feed_sep="\n", include_change_type_prefix=True, patch_format=False):
 
     newest_version_file_contents = [line.rstrip() for line in newest_version_file_contents.splitlines()]
 
     if previous_version_file_contents:
-            previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()]
+        previous_version_file_contents = [line.rstrip() for line in previous_version_file_contents.splitlines()]
     else:
         previous_version_file_contents = ""
 
+    if patch_format:
+        patch = difflib.unified_diff(previous_version_file_contents, newest_version_file_contents)
+        return line_feed_sep.join(patch)
+
     rendered_diff = customSequenceMatcher(before=previous_version_file_contents,
                                           after=newest_version_file_contents,
                                           include_equal=include_equal,
diff --git a/changedetectionio/notification.py b/changedetectionio/notification.py
index d2beda013a2..27c7afa7195 100644
--- a/changedetectionio/notification.py
+++ b/changedetectionio/notification.py
@@ -9,6 +9,7 @@
     'diff': '',
     'diff_added': '',
     'diff_full': '',
+    'diff_patch': '',
     'diff_removed': '',
     'diff_url': '',
     'preview_url': '',
@@ -98,7 +99,7 @@ def process_notification(n_object, datastore):
         # Initially text or whatever
         n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format])
 
-    
+
     # 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
@@ -177,7 +178,7 @@ def process_notification(n_object, datastore):
                 log_value = logs.getvalue()
                 if log_value and 'WARNING' in log_value or 'ERROR' in log_value:
                     raise Exception(log_value)
-                
+
                 sent_objs.append({'title': n_title,
                                   'body': n_body,
                                   'url' : url,
@@ -230,6 +231,7 @@ def create_notification_parameters(n_object, datastore):
             'diff': n_object.get('diff', ''),  # Null default in the case we use a test
             'diff_added': n_object.get('diff_added', ''),  # Null default in the case we use a test
             'diff_full': n_object.get('diff_full', ''),  # Null default in the case we use a test
+            'diff_patch': n_object.get('diff_patch', ''),  # Null default in the case we use a test
             'diff_removed': n_object.get('diff_removed', ''),  # Null default in the case we use a test
             'diff_url': diff_url,
             'preview_url': preview_url,
diff --git a/changedetectionio/templates/_common_fields.jinja b/changedetectionio/templates/_common_fields.jinja
index c8ffdb00c5f..6b6f6e05a64 100644
--- a/changedetectionio/templates/_common_fields.jinja
+++ b/changedetectionio/templates/_common_fields.jinja
@@ -99,6 +99,10 @@
                                         <td><code>{{ '{{diff_full}}' }}</code></td>
                                         <td>The diff output - full difference output</td>
                                     </tr>
+                                    <tr>
+                                        <td><code>{{ '{{diff_patch}}' }}</code></td>
+                                        <td>The diff output - patch in unified format</td>
+                                    </tr>
                                     <tr>
                                         <td><code>{{ '{{current_snapshot}}' }}</code></td>
                                         <td>The current snapshot value, useful when combined with JSON or CSS filters
diff --git a/changedetectionio/update_worker.py b/changedetectionio/update_worker.py
index 61b423942d7..63a0aab42fb 100644
--- a/changedetectionio/update_worker.py
+++ b/changedetectionio/update_worker.py
@@ -58,6 +58,7 @@ def queue_notification_for_watch(self, n_object, watch):
             'diff': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep),
             'diff_added': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_removed=False, line_feed_sep=line_feed_sep),
             'diff_full': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_equal=True, line_feed_sep=line_feed_sep),
+            'diff_patch': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), line_feed_sep=line_feed_sep, patch_format=True),
             'diff_removed': diff.render_diff(watch.get_history_snapshot(dates[-2]), watch.get_history_snapshot(dates[-1]), include_added=False, line_feed_sep=line_feed_sep),
             'screenshot': watch.get_screenshot() if watch.get('notification_screenshot') else None,
             'triggered_text': triggered_text,


Test patch:
diff --git a/changedetectionio/tests/test_filter_exist_changes.py b/changedetectionio/tests/test_filter_exist_changes.py
index 24f84455e7e..e57db579456 100644
--- a/changedetectionio/tests/test_filter_exist_changes.py
+++ b/changedetectionio/tests/test_filter_exist_changes.py
@@ -15,7 +15,7 @@ def set_response_without_filter():
      <p>Which is across multiple lines</p>
      <br>
      So let's see what happens.  <br>
-     <div id="nope-doesnt-exist">Some text thats the same</div>     
+     <div id="nope-doesnt-exist">Some text thats the same</div>
      </body>
      </html>
     """
@@ -32,7 +32,7 @@ def set_response_with_filter():
      <p>Which is across multiple lines</p>
      <br>
      So let's see what happens.  <br>
-     <div class="ticket-available">Ticket now on sale!</div>     
+     <div class="ticket-available">Ticket now on sale!</div>
      </body>
      </html>
     """
@@ -84,6 +84,7 @@ def test_filter_doesnt_exist_then_exists_should_get_notification(client, live_se
                                                    "Snapshot: {{current_snapshot}}\n"
                                                    "Diff: {{diff}}\n"
                                                    "Diff Full: {{diff_full}}\n"
+                                                   "Diff as Patch: {{diff_patch}}\n"
                                                    ":-)",
                               "notification_format": "Text"}
 
diff --git a/changedetectionio/tests/test_filter_failure_notification.py b/changedetectionio/tests/test_filter_failure_notification.py
index 3ec5bfb752c..b25931ea855 100644
--- a/changedetectionio/tests/test_filter_failure_notification.py
+++ b/changedetectionio/tests/test_filter_failure_notification.py
@@ -12,7 +12,7 @@ def set_response_with_filter():
      <p>Which is across multiple lines</p>
      <br>
      So let's see what happens.  <br>
-     <div id="nope-doesnt-exist">Some text thats the same</div>     
+     <div id="nope-doesnt-exist">Some text thats the same</div>
      </body>
      </html>
     """
@@ -66,6 +66,7 @@ def run_filter_test(client, content_filter):
                                                    "Snapshot: {{current_snapshot}}\n"
                                                    "Diff: {{diff}}\n"
                                                    "Diff Full: {{diff_full}}\n"
+                                                   "Diff as Patch: {{diff_patch}}\n"
                                                    ":-)",
                               "notification_format": "Text"}
 
diff --git a/changedetectionio/tests/test_group.py b/changedetectionio/tests/test_group.py
index 2bac9710934..ed38cb989b7 100644
--- a/changedetectionio/tests/test_group.py
+++ b/changedetectionio/tests/test_group.py
@@ -15,7 +15,7 @@ def set_original_response():
      Some initial text<br>
      <p id="only-this">Should be only this</p>
      <br>
-     <p id="not-this">And never this</p>     
+     <p id="not-this">And never this</p>
      </body>
      </html>
     """
@@ -30,7 +30,7 @@ def set_modified_response():
      Some initial text<br>
      <p id="only-this">Should be REALLY only this</p>
      <br>
-     <p id="not-this">And never this</p>     
+     <p id="not-this">And never this</p>
      </body>
      </html>
     """
@@ -189,6 +189,7 @@ def test_group_tag_notification(client, live_server):
                                                    "Diff Added: {{diff_added}}\n"
                                                    "Diff Removed: {{diff_removed}}\n"
                                                    "Diff Full: {{diff_full}}\n"
+                                                   "Diff as Patch: {{diff_patch}}\n"
                                                    ":-)",
                               "notification_screenshot": True,
                               "notification_format": "Text",
@@ -319,4 +320,4 @@ def test_clone_tag_on_quickwatchform_add(client, live_server):
     assert b'Deleted' in res.data
 
     res = client.get(url_for("tags.delete_all"), follow_redirects=True)
-    assert b'All tags deleted' in res.data
\ No newline at end of file
+    assert b'All tags deleted' in res.data
diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py
index 5f7b0582951..7d0e5ff2d19 100644
--- a/changedetectionio/tests/test_notification.py
+++ b/changedetectionio/tests/test_notification.py
@@ -98,6 +98,7 @@ def test_check_notification(client, live_server):
                                                    "Diff Added: {{diff_added}}\n"
                                                    "Diff Removed: {{diff_removed}}\n"
                                                    "Diff Full: {{diff_full}}\n"
+                                                   "Diff as Patch: {{diff_patch}}\n"
                                                    ":-)",
                               "notification_screenshot": True,
                               "notification_format": "Text"}
diff --git a/changedetectionio/tests/unit/test_notification_diff.py b/changedetectionio/tests/unit/test_notification_diff.py
index 6f32314655b..c7befc20f7f 100755
--- a/changedetectionio/tests/unit/test_notification_diff.py
+++ b/changedetectionio/tests/unit/test_notification_diff.py
@@ -36,7 +36,7 @@ def test_expected_diff_output(self):
         output = output.split("\n")
         self.assertIn('(removed) for having learned computerese,', output)
         self.assertIn('(removed) I continue to examine bits, bytes and words', output)
-        
+
         #diff_removed
         with open(base_dir + "/test-content/before.txt", 'r') as f:
             previous_version_file_contents = f.read()
@@ -49,7 +49,7 @@ def test_expected_diff_output(self):
         self.assertIn('(into) xok', output)
         self.assertIn('(into) next-x-ok', output)
         self.assertNotIn('(added) and something new', output)
-        
+
         #diff_removed
         with open(base_dir + "/test-content/after-2.txt", 'r') as f:
             newest_version_file_contents = f.read()
@@ -57,9 +57,25 @@ def test_expected_diff_output(self):
         output = output.split("\n")
         self.assertIn('(removed) for having learned computerese,', output)
         self.assertIn('(removed) I continue to examine bits, bytes and words', output)
-        
+
+    def test_expected_diff_patch_output(self):
+        base_dir = os.path.dirname(__file__)
+        with open(base_dir + "/test-content/before.txt", 'r') as f:
+            before = f.read()
+        with open(base_dir + "/test-content/after.txt", 'r') as f:
+            after = f.read()
+
+        output = diff.render_diff(previous_version_file_contents=before,
+                                  newest_version_file_contents=after,
+                                  patch_format=True)
+        output = output.split("\n")
+
+        self.assertIn('-ok', output)
+        self.assertIn('+xok', output)
+        self.assertIn('+next-x-ok', output)
+        self.assertIn('+and something new', output)
 
         # @todo test blocks of changed, blocks of added, blocks of removed
 
 if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+    unittest.main()