]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: allow specifying JSON encoding for webhooks (#8799)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Sat, 18 Jan 2025 20:19:50 +0000 (12:19 -0800)
committerGitHub <noreply@github.com>
Sat, 18 Jan 2025 20:19:50 +0000 (12:19 -0800)
docs/usage.md
src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts
src-ui/src/app/data/workflow-action.ts
src/documents/migrations/1061_workflowactionwebhook_as_json.py [new file with mode: 0644]
src/documents/models.py
src/documents/serialisers.py
src/documents/signals/handlers.py
src/documents/tests/test_workflows.py

index 504aae9efd75b145508ccd229a124afffe325333..ab71f16a11291ec1cd06fc75083469fa50bce321 100644 (file)
@@ -419,6 +419,7 @@ The following workflow action types are available:
 
 -   The URL to send the request to
 -   The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
+-   Encoding for the request body, either JSON or form data
 -   The request headers as key-value pairs
 
 #### Workflow placeholders
index 5184dcd100f679fa6bee0e21b0f9dad4bc6add9b..add7878f483a12f5175b973fc7b2d92e01f52b57 100644 (file)
           <input type="hidden" formControlName="id" />
           <div class="col">
             <pngx-input-text i18n-title title="Webhook url" formControlName="url" [error]="error?.actions?.[i]?.url"></pngx-input-text>
-            <pngx-input-switch i18n-title title="Use parameters for webhook body" formControlName="use_params"></pngx-input-switch>
+            <div class="d-flex">
+              <pngx-input-switch i18n-title title="Use parameters for webhook body" formControlName="use_params" [horizontal]="true"></pngx-input-switch>
+              <pngx-input-switch i18n-title title="Send webhook payload as JSON" formControlName="as_json" [horizontal]="true" class="ms-5"></pngx-input-switch>
+            </div>
             @if (formGroup.get('webhook').value['use_params']) {
               <pngx-input-entries i18n-title title="Webhook params" formControlName="params" [error]="error?.actions?.[i]?.params"></pngx-input-entries>
             } @else {
index 3abdee09ae27d4b6164793158ff491769e1342c1..0908b69c043435d1818bbe5110e306a4927b0c95 100644 (file)
@@ -471,6 +471,7 @@ export class WorkflowEditDialogComponent
           id: new FormControl(action.webhook?.id),
           url: new FormControl(action.webhook?.url),
           use_params: new FormControl(action.webhook?.use_params),
+          as_json: new FormControl(action.webhook?.as_json),
           params: new FormControl(action.webhook?.params),
           body: new FormControl(action.webhook?.body),
           headers: new FormControl(action.webhook?.headers),
@@ -588,6 +589,7 @@ export class WorkflowEditDialogComponent
         id: null,
         url: null,
         use_params: true,
+        as_json: false,
         params: null,
         body: null,
         headers: null,
index b802d47b4495b5dfa6f802637a89413a8211a44e..0d8316ecbbd74cc384950be2d57062fa0675f6b7 100644 (file)
@@ -22,6 +22,8 @@ export interface WorkflowActionWebhook extends ObjectWithId {
 
   use_params?: boolean
 
+  as_json?: boolean
+
   params?: object
 
   body?: string
diff --git a/src/documents/migrations/1061_workflowactionwebhook_as_json.py b/src/documents/migrations/1061_workflowactionwebhook_as_json.py
new file mode 100644 (file)
index 0000000..f1945cf
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2025-01-18 19:35
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("documents", "1060_alter_customfieldinstance_value_select"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="workflowactionwebhook",
+            name="as_json",
+            field=models.BooleanField(default=False, verbose_name="send as JSON"),
+        ),
+    ]
index 88265a7da60bbdbe397c9df4d55a9ed4f7ca9cb5..79856b83701d4d1af8bb1687b58fe571ca05ccfd 100644 (file)
@@ -1209,6 +1209,11 @@ class WorkflowActionWebhook(models.Model):
         verbose_name=_("use parameters"),
     )
 
+    as_json = models.BooleanField(
+        default=False,
+        verbose_name=_("send as JSON"),
+    )
+
     params = models.JSONField(
         _("webhook parameters"),
         null=True,
index e051e00d6dd7ea58943a3d470b9254173aae2cce..eb1eba8f1473ae769e044adca9b187c8dbf67cb9 100644 (file)
@@ -1876,6 +1876,7 @@ class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
             "id",
             "url",
             "use_params",
+            "as_json",
             "params",
             "body",
             "headers",
index 1d21b962b2c8ad58362deac59337b5939af765ef..e60585a3720dd09cd292dc9c23c2a3863aabb770 100644 (file)
@@ -573,14 +573,29 @@ def run_workflows_updated(sender, document: Document, logging_group=None, **kwar
     max_retries=3,
     throws=(httpx.HTTPError,),
 )
-def send_webhook(url, data, headers, files):
+def send_webhook(
+    url: str,
+    data: str | dict,
+    headers: dict,
+    files: dict,
+    *,
+    as_json: bool = False,
+):
     try:
-        httpx.post(
-            url,
-            data=data,
-            files=files,
-            headers=headers,
-        ).raise_for_status()
+        if as_json:
+            httpx.post(
+                url,
+                json=data,
+                files=files,
+                headers=headers,
+            ).raise_for_status()
+        else:
+            httpx.post(
+                url,
+                data=data,
+                files=files,
+                headers=headers,
+            ).raise_for_status()
         logger.info(
             f"Webhook sent to {url}",
         )
@@ -1092,6 +1107,7 @@ def run_workflows(
                 data=data,
                 headers=headers,
                 files=files,
+                as_json=action.webhook.as_json,
             )
             logger.debug(
                 f"Webhook to {action.webhook.url} queued",
index 9f976bbfe49984fac1242996a3aa3caf9e674f61..cb5a132af1bd61cec7c66b4e6e25e5d7c84071f3 100644 (file)
@@ -11,6 +11,7 @@ from guardian.shortcuts import assign_perm
 from guardian.shortcuts import get_groups_with_perms
 from guardian.shortcuts import get_users_with_perms
 from httpx import HTTPStatusError
+from pytest_httpx import HTTPXMock
 from rest_framework.test import APITestCase
 
 from documents.signals.handlers import run_workflows
@@ -2407,6 +2408,7 @@ class TestWorkflows(
             data=f"Test message: http://localhost:8000/documents/{doc.id}/",
             headers={},
             files=None,
+            as_json=False,
         )
 
     @override_settings(
@@ -2468,6 +2470,7 @@ class TestWorkflows(
             data=f"Test message: http://localhost:8000/documents/{doc.id}/",
             headers={},
             files={"file": ("simple.pdf", mock.ANY, "application/pdf")},
+            as_json=False,
         )
 
     @override_settings(
@@ -2669,3 +2672,43 @@ class TestWorkflows(
                 )
 
         mock_post.assert_called_once()
+
+
+class TestWebhookSend:
+    def test_send_webhook_data_or_json(
+        self,
+        httpx_mock: HTTPXMock,
+    ):
+        """
+        GIVEN:
+            - Nothing
+        WHEN:
+            - send_webhook is called with data or dict
+        THEN:
+            - data is sent as form-encoded and json, respectively
+        """
+        httpx_mock.add_response(
+            content=b"ok",
+        )
+
+        send_webhook(
+            url="http://paperless-ngx.com",
+            data="Test message",
+            headers={},
+            files=None,
+            as_json=False,
+        )
+        assert httpx_mock.get_request().headers.get("Content-Type") is None
+        httpx_mock.reset()
+
+        httpx_mock.add_response(
+            json={"status": "ok"},
+        )
+        send_webhook(
+            url="http://paperless-ngx.com",
+            data={"message": "Test message"},
+            headers={},
+            files=None,
+            as_json=True,
+        )
+        assert httpx_mock.get_request().headers["Content-Type"] == "application/json"