From 97d38ee1a88f38b5022d5e2f10016ccefd072021 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Wed, 19 Apr 2023 22:28:53 +0800
Subject: [PATCH 1/5] Feat: Add custom body for Webhook Notif.

---
 package-lock.json                        | 28 +++++++++
 package.json                             |  1 +
 server/notification-providers/webhook.js | 26 +++++---
 src/components/notifications/Webhook.vue | 78 +++++++++++++++++-------
 src/lang/en.json                         |  3 +-
 5 files changed, 107 insertions(+), 29 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 8796b1774..2374eccc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,7 @@
                 "jsonwebtoken": "~9.0.0",
                 "jwt-decode": "~3.1.2",
                 "limiter": "~2.1.0",
+                "liquidjs": "^10.7.0",
                 "mongodb": "~4.14.0",
                 "mqtt": "~4.3.7",
                 "mssql": "~8.1.4",
@@ -13761,6 +13762,33 @@
             "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
             "dev": true
         },
+        "node_modules/liquidjs": {
+            "version": "10.7.1",
+            "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.7.1.tgz",
+            "integrity": "sha512-tl9nWBZrrKcC61yfih3lbtSjAn+k7e0HhwydPjQKI4+metLk927HYBfXfbf6yrCcYjnBnLzk8xMjUF83yknAQQ==",
+            "dependencies": {
+                "commander": "^10.0.0"
+            },
+            "bin": {
+                "liquid": "bin/liquid.js",
+                "liquidjs": "bin/liquid.js"
+            },
+            "engines": {
+                "node": ">=14"
+            },
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/liquidjs"
+            }
+        },
+        "node_modules/liquidjs/node_modules/commander": {
+            "version": "10.0.1",
+            "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+            "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+            "engines": {
+                "node": ">=14"
+            }
+        },
         "node_modules/listr2": {
             "version": "3.14.0",
             "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz",
diff --git a/package.json b/package.json
index c099b4d87..2740ea775 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
         "jsonwebtoken": "~9.0.0",
         "jwt-decode": "~3.1.2",
         "limiter": "~2.1.0",
+        "liquidjs": "^10.7.0",
         "mongodb": "~4.14.0",
         "mqtt": "~4.3.7",
         "mssql": "~8.1.4",
diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js
index 649746b8e..e5a47fbce 100644
--- a/server/notification-providers/webhook.js
+++ b/server/notification-providers/webhook.js
@@ -1,6 +1,7 @@
 const NotificationProvider = require("./notification-provider");
 const axios = require("axios");
 const FormData = require("form-data");
+const { Liquid } = require("liquidjs");
 
 class Webhook extends NotificationProvider {
 
@@ -15,17 +16,28 @@ class Webhook extends NotificationProvider {
                 monitor: monitorJSON,
                 msg,
             };
-            let finalData;
             let config = {
                 headers: {}
             };
 
             if (notification.webhookContentType === "form-data") {
-                finalData = new FormData();
-                finalData.append("data", JSON.stringify(data));
-                config.headers = finalData.getHeaders();
-            } else {
-                finalData = data;
+                const formData = new FormData();
+                formData.append("data", JSON.stringify(data));
+                config.headers = formData.getHeaders();
+                data = formData;
+            } else if (notification.webhookContentType === "custom") {
+                // Initialize LiquidJS and parse the custom Body Template
+                const engine = new Liquid();
+                const tpl = engine.parse(notification.webhookCustomBody);
+
+                // Insert templated values into Body
+                const parsedBody = await engine.render(tpl,
+                    {
+                        msg,
+                        heartbeatJSON,
+                        monitorJSON
+                    });
+                data = parsedBody;
             }
 
             if (notification.webhookAdditionalHeaders) {
@@ -39,7 +51,7 @@ class Webhook extends NotificationProvider {
                 }
             }
 
-            await axios.post(notification.webhookURL, finalData, config);
+            await axios.post(notification.webhookURL, data, config);
             return okMsg;
 
         } catch (error) {
diff --git a/src/components/notifications/Webhook.vue b/src/components/notifications/Webhook.vue
index 1b85a5409..34c90cb04 100644
--- a/src/components/notifications/Webhook.vue
+++ b/src/components/notifications/Webhook.vue
@@ -13,7 +13,7 @@
 
     <div class="mb-3">
         <label for="webhook-content-type" class="form-label">{{
-            $t("Content Type")
+            $t("Request Body")
         }}</label>
         <select
             id="webhook-content-type"
@@ -21,52 +21,88 @@
             class="form-select"
             required
         >
-            <option value="json">application/json</option>
-            <option value="form-data">multipart/form-data</option>
+            <option value="json">Preset - application/json</option>
+            <option value="form-data">Preset - multipart/form-data</option>
+            <option value="custom">Custom Body</option>
         </select>
 
         <div class="form-text">
-            <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
-            <i18n-t tag="p" keypath="webhookFormDataDesc">
-                <template #multipart>"multipart/form-data"</template>
-                <template #decodeFunction>
-                    <strong>json_decode($_POST['data'])</strong>
-                </template>
-            </i18n-t>
+            <div v-if="$parent.notification.webhookContentType == 'json'">
+                <p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
+            </div>
+            <div v-if="$parent.notification.webhookContentType == 'form-data'">
+                <i18n-t tag="p" keypath="webhookFormDataDesc">
+                    <template #multipart>multipart/form-data"</template>
+                    <template #decodeFunction>
+                        <strong>json_decode($_POST['data'])</strong>
+                    </template>
+                </i18n-t>
+            </div>
+            <div v-if="$parent.notification.webhookContentType == 'custom'">
+                <i18n-t tag="p" keypath="webhookCustomBodyDesc">
+                    <template #msg>
+                        <code>msg</code>
+                    </template>
+                    <template #heartbeat>
+                        <code>heartbeatJSON</code>
+                    </template>
+                    <template #monitor>
+                        <code>monitorJSON</code>
+                    </template>
+                </i18n-t>
+            </div>
         </div>
+
+        <textarea
+            v-if="$parent.notification.webhookContentType == 'custom'"
+            id="customBody"
+            v-model="$parent.notification.webhookCustomBody"
+            class="form-control"
+            :placeholder="customBodyPlaceholder"
+        ></textarea>
     </div>
 
     <div class="mb-3">
-        <i18n-t
-            tag="label"
-            class="form-label"
-            for="additionalHeaders"
-            keypath="webhookAdditionalHeadersTitle"
-        >
-        </i18n-t>
+        <div class="form-check form-switch">
+            <input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
+            <label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
+        </div>
+        <div class="form-text">
+            <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
+        </div>
         <textarea
+            v-if="showAdditionalHeadersField"
             id="additionalHeaders"
             v-model="$parent.notification.webhookAdditionalHeaders"
             class="form-control"
             :placeholder="headersPlaceholder"
         ></textarea>
-        <div class="form-text">
-            <i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
-        </div>
     </div>
 </template>
 
 <script>
 export default {
+    data() {
+        return {
+            showAdditionalHeadersField: this.$parent.notification.webhookAdditionalHeaders != null,
+        };
+    },
     computed: {
         headersPlaceholder() {
             return this.$t("Example:", [
                 `
 {
-    "HeaderName": "HeaderValue"
+    "Authorization": "Authorization Token"
 }`,
             ]);
         },
+        customBodyPlaceholder() {
+            return `Example:
+{
+    "Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
+    "Body": "{{ msg }}"
+}`;
+        }
     },
 };
 </script>
diff --git a/src/lang/en.json b/src/lang/en.json
index aecd34be8..7d3b7d2e8 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -195,8 +195,9 @@
     "Content Type": "Content Type",
     "webhookJsonDesc": "{0} is good for any modern HTTP servers such as Express.js",
     "webhookFormDataDesc": "{multipart} is good for PHP. The JSON will need to be parsed with {decodeFunction}",
+    "webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
     "webhookAdditionalHeadersTitle": "Additional Headers",
-    "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook.",
+    "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
     "Webhook URL": "Webhook URL",
     "Application Token": "Application Token",
     "Server URL": "Server URL",

From c9d6e576ab1b220882484b63d664ff9da5d84ad4 Mon Sep 17 00:00:00 2001
From: Nelson Chan <3271800+chakflying@users.noreply.github.com>
Date: Wed, 31 May 2023 09:33:01 +0800
Subject: [PATCH 2/5] Chore: Remove redundant assign

Co-authored-by: Frank Elsinga <frank@elsinga.de>
---
 server/notification-providers/webhook.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/server/notification-providers/webhook.js b/server/notification-providers/webhook.js
index e5a47fbce..9f5643b4e 100644
--- a/server/notification-providers/webhook.js
+++ b/server/notification-providers/webhook.js
@@ -31,13 +31,12 @@ class Webhook extends NotificationProvider {
                 const tpl = engine.parse(notification.webhookCustomBody);
 
                 // Insert templated values into Body
-                const parsedBody = await engine.render(tpl,
+                data = await engine.render(tpl,
                     {
                         msg,
                         heartbeatJSON,
                         monitorJSON
                     });
-                data = parsedBody;
             }
 
             if (notification.webhookAdditionalHeaders) {

From 7761e9a05ecbdaa3f7f54c899a2477c40541e963 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Wed, 31 May 2023 09:42:55 +0800
Subject: [PATCH 3/5] Chore: Add translations to options text

---
 src/components/notifications/Webhook.vue | 6 +++---
 src/lang/en.json                         | 2 ++
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/components/notifications/Webhook.vue b/src/components/notifications/Webhook.vue
index 34c90cb04..5ed0aa47a 100644
--- a/src/components/notifications/Webhook.vue
+++ b/src/components/notifications/Webhook.vue
@@ -21,9 +21,9 @@
             class="form-select"
             required
         >
-            <option value="json">Preset - application/json</option>
-            <option value="form-data">Preset - multipart/form-data</option>
-            <option value="custom">Custom Body</option>
+            <option value="json">{{ $t("webhookBodyPresetOption", ["application/json"]) }}</option>
+            <option value="form-data">{{ $t("webhookBodyPresetOption", ["multipart/form-data"]) }}</option>
+            <option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
         </select>
 
         <div class="form-text">
diff --git a/src/lang/en.json b/src/lang/en.json
index 7d3b7d2e8..d6ca64bae 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -198,6 +198,8 @@
     "webhookCustomBodyDesc": "Define a custom HTTP Body for the request. Template variables {msg}, {heartbeat}, {monitor} are accepted.",
     "webhookAdditionalHeadersTitle": "Additional Headers",
     "webhookAdditionalHeadersDesc": "Sets additional headers sent with the webhook. Each header should be defined as a JSON key/value.",
+    "webhookBodyPresetOption": "Preset - {0}",
+    "webhookBodyCustomOption": "Custom Body",
     "Webhook URL": "Webhook URL",
     "Application Token": "Application Token",
     "Server URL": "Server URL",

From 8955c3816b0a37039fe303f5bb26d496c3280ed0 Mon Sep 17 00:00:00 2001
From: Nelson Chan <chakflying@hotmail.com>
Date: Wed, 31 May 2023 22:07:48 +0800
Subject: [PATCH 4/5] Chore: Rename select ID & add translation

---
 src/components/notifications/Webhook.vue | 4 ++--
 src/lang/en.json                         | 3 ++-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/components/notifications/Webhook.vue b/src/components/notifications/Webhook.vue
index 5ed0aa47a..4262ad067 100644
--- a/src/components/notifications/Webhook.vue
+++ b/src/components/notifications/Webhook.vue
@@ -12,11 +12,11 @@
     </div>
 
     <div class="mb-3">
-        <label for="webhook-content-type" class="form-label">{{
+        <label for="webhook-request-body" class="form-label">{{
             $t("Request Body")
         }}</label>
         <select
-            id="webhook-content-type"
+            id="webhook-request-body"
             v-model="$parent.notification.webhookContentType"
             class="form-select"
             required
diff --git a/src/lang/en.json b/src/lang/en.json
index d6ca64bae..39c22505f 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -755,5 +755,6 @@
     "Group": "Group",
     "Monitor Group": "Monitor Group",
     "noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
-    "Close": "Close"
+    "Close": "Close",
+    "Request Body": "Request Body"
 }

From 293015ff35894b3972d77843d66b299caa7239c1 Mon Sep 17 00:00:00 2001
From: Louis Lam <louislam@users.noreply.github.com>
Date: Sun, 9 Jul 2023 18:53:57 +0800
Subject: [PATCH 5/5] Parse `x-www-form-urlencoded` for /test-webhook

---
 server/server.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/server/server.js b/server/server.js
index 976dca5de..c35af7fc1 100644
--- a/server/server.js
+++ b/server/server.js
@@ -208,6 +208,7 @@ let needSetup = false;
     });
 
     if (isDev) {
+        app.use(express.urlencoded({ extended: true }));
         app.post("/test-webhook", async (request, response) => {
             log.debug("test", request.headers);
             log.debug("test", request.body);