From 5d132bff60ee8780b556a8033170c4306bd7d913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCnch?= Date: Thu, 26 Feb 2026 15:26:52 +0100 Subject: [PATCH] feat(custom-payload): add now datetime variables --- README.md | 4 +++ options/options.html | 19 +++++++++++-- options/options.js | 4 +++ tests/sendWebhook.test.js | 48 +++++++++++++++++++++++++++++++++ utils/utils.js | 56 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 125 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5a338cf..7101eeb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Easily manage and trigger webhooks directly from your browser! Compatible with F **๐Ÿงช Test a Webhook:** - When adding or editing a webhook, click the 'Test' button to send a test payload to your URL. +**๐Ÿ•’ DateTime Variables in Custom Payload:** +- You can use `{{now.*}}` placeholders in custom payload JSON, including UTC values like `{{now.iso}}`, `{{now.date}}`, `{{now.time}}`, `{{now.unix}}`, and local values like `{{now.local.iso}}`. +- Existing `{{triggeredAt}}` remains available as an alias for `{{now.iso}}` for backward compatibility. + **๐Ÿ—‚๏ธ Organize into Groups:** - Use the group management dialog to add, delete, rename, or reorder groups via drag-and-drop. diff --git a/options/options.html b/options/options.html index 79ce0e4..4c0f252 100644 --- a/options/options.html +++ b/options/options.html @@ -131,12 +131,27 @@

__MSG_optionsAddWebhookHeader__

  • {{tab.audible}} - Is tab playing audio
  • {{tab.incognito}} - Is tab in incognito mode
  • {{tab.status}} - Tab loading status
  • -
  • {{platform.arch}} - Plaform architecture
  • +
  • {{platform.arch}} - Platform architecture
  • {{platform.os}} - Operating system
  • {{platform.version}} - Platform version
  • {{browser}} - Browser information
  • {{platform}} - Platform information
  • -
  • {{triggeredAt}} - Timestamp when triggered
  • +
  • {{now.iso}} - Current UTC time as ISO 8601
  • +
  • {{now.date}} - Current UTC date (YYYY-MM-DD)
  • +
  • {{now.time}} - Current UTC time (HH:mm:ss)
  • +
  • {{now.unix}} - Current UTC Unix timestamp in seconds
  • +
  • {{now.unix_ms}} - Current UTC Unix timestamp in milliseconds
  • +
  • {{now.year}} - Current UTC year
  • +
  • {{now.month}} - Current UTC month (1-12)
  • +
  • {{now.day}} - Current UTC day of month (1-31)
  • +
  • {{now.hour}} - Current UTC hour (0-23)
  • +
  • {{now.minute}} - Current UTC minute (0-59)
  • +
  • {{now.second}} - Current UTC second (0-59)
  • +
  • {{now.millisecond}} - Current UTC millisecond (0-999)
  • +
  • {{now.local.iso}} - Current local time as ISO 8601
  • +
  • {{now.local.date}} - Current local date (YYYY-MM-DD)
  • +
  • {{now.local.time}} - Current local time (HH:mm:ss)
  • +
  • {{triggeredAt}} - Alias for {{now.iso}}
  • {{identifier}} - Custom identifier
  • diff --git a/options/options.js b/options/options.js index 68c5bf6..40029a2 100644 --- a/options/options.js +++ b/options/options.js @@ -663,6 +663,10 @@ const availableVariables = [ "{{tab.index}}", "{{tab.pinned}}", "{{tab.audible}}", "{{tab.incognito}}", "{{tab.status}}", "{{browser}}", "{{platform}}", "{{triggeredAt}}", "{{identifier}}", "{{platform.arch}}", "{{platform.os}}", "{{platform.version}}", + "{{now.iso}}", "{{now.date}}", "{{now.time}}", "{{now.unix}}", "{{now.unix_ms}}", + "{{now.year}}", "{{now.month}}", "{{now.day}}", "{{now.hour}}", "{{now.minute}}", + "{{now.second}}", "{{now.millisecond}}", "{{now.local.iso}}", "{{now.local.date}}", + "{{now.local.time}}", ]; // Implement autocompletion for custom payload diff --git a/tests/sendWebhook.test.js b/tests/sendWebhook.test.js index f788d50..622e22e 100644 --- a/tests/sendWebhook.test.js +++ b/tests/sendWebhook.test.js @@ -50,6 +50,7 @@ describe('sendWebhook', () => { global.fetch = originalFetch; global.browser = originalBrowser; console.error = originalConsoleError; + jest.useRealTimers(); jest.clearAllMocks(); }); @@ -174,4 +175,51 @@ describe('sendWebhook', () => { await expect(sendWebhook(webhook, false)) .rejects.toThrow('popupErrorCustomPayloadJsonParseError'); }); + + test('should replace all new now datetime placeholders', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z')); + + const webhook = { + url: 'https://custom.com', + customPayload: '{"iso":"{{now.iso}}","date":"{{now.date}}","time":"{{now.time}}","unix":{{now.unix}},"unixMs":{{now.unix_ms}},"year":{{now.year}},"month":{{now.month}},"day":{{now.day}},"hour":{{now.hour}},"minute":{{now.minute}},"second":{{now.second}},"millisecond":{{now.millisecond}},"localIso":"{{now.local.iso}}","localDate":"{{now.local.date}}","localTime":"{{now.local.time}}"}' + }; + + await sendWebhook(webhook, false); + + const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body); + expect(fetchBody.iso).toBe('2025-08-07T10:20:30.123Z'); + expect(fetchBody.date).toBe('2025-08-07'); + expect(fetchBody.time).toBe('10:20:30'); + expect(fetchBody.unix).toBe(1754562030); + expect(fetchBody.unixMs).toBe(1754562030123); + expect(fetchBody.year).toBe(2025); + expect(fetchBody.month).toBe(8); + expect(fetchBody.day).toBe(7); + expect(fetchBody.hour).toBe(10); + expect(fetchBody.minute).toBe(20); + expect(fetchBody.second).toBe(30); + expect(fetchBody.millisecond).toBe(123); + expect(fetchBody.localDate).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(fetchBody.localTime).toMatch(/^\d{2}:\d{2}:\d{2}$/); + expect(fetchBody.localIso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/); + + }); + + test('should keep triggeredAt as alias for now.iso', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z')); + + const webhook = { + url: 'https://custom.com', + customPayload: '{"now":"{{now.iso}}","legacy":"{{triggeredAt}}"}' + }; + + await sendWebhook(webhook, false); + + const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body); + expect(fetchBody.now).toBe('2025-08-07T10:20:30.123Z'); + expect(fetchBody.legacy).toBe('2025-08-07T10:20:30.123Z'); + + }); }); diff --git a/utils/utils.js b/utils/utils.js index 61c24d6..64667b6 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -83,6 +83,52 @@ function replaceI18nPlaceholders() { } } +const padDatePart = (value, length = 2) => String(value).padStart(length, '0'); + +const toLocalIsoString = (date) => { + const year = date.getFullYear(); + const month = padDatePart(date.getMonth() + 1); + const day = padDatePart(date.getDate()); + const hour = padDatePart(date.getHours()); + const minute = padDatePart(date.getMinutes()); + const second = padDatePart(date.getSeconds()); + const millisecond = padDatePart(date.getMilliseconds(), 3); + const offsetMinutes = -date.getTimezoneOffset(); + const sign = offsetMinutes >= 0 ? '+' : '-'; + const absoluteOffset = Math.abs(offsetMinutes); + const offsetHour = padDatePart(Math.floor(absoluteOffset / 60)); + const offsetMinute = padDatePart(absoluteOffset % 60); + + return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}${sign}${offsetHour}:${offsetMinute}`; +}; + +const buildDateTimeVariables = (date = new Date()) => { + const nowIso = date.toISOString(); + const timestampMs = date.getTime(); + + return { + nowIso, + values: { + "{{now.iso}}": nowIso, + "{{now.date}}": nowIso.slice(0, 10), + "{{now.time}}": nowIso.slice(11, 19), + "{{now.unix}}": Math.floor(timestampMs / 1000), + "{{now.unix_ms}}": timestampMs, + "{{now.year}}": date.getUTCFullYear(), + "{{now.month}}": date.getUTCMonth() + 1, + "{{now.day}}": date.getUTCDate(), + "{{now.hour}}": date.getUTCHours(), + "{{now.minute}}": date.getUTCMinutes(), + "{{now.second}}": date.getUTCSeconds(), + "{{now.millisecond}}": date.getUTCMilliseconds(), + "{{now.local.iso}}": toLocalIsoString(date), + "{{now.local.date}}": `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`, + "{{now.local.time}}": `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}:${padDatePart(date.getSeconds())}`, + "{{triggeredAt}}": nowIso, + }, + }; +}; + async function sendWebhook(webhook, isTest = false) { const browserAPI = getBrowserAPI(); let selectors = Array.isArray(webhook?.selectors) ? [...webhook.selectors] : []; @@ -91,11 +137,13 @@ async function sendWebhook(webhook, isTest = false) { try { let payload; + const dateTimeVariables = buildDateTimeVariables(); + if (isTest) { payload = { url: "https://example.com", test: true, - triggeredAt: new Date().toISOString(), + triggeredAt: dateTimeVariables.nowIso, }; } else { // Get info about the active tab @@ -185,7 +233,7 @@ async function sendWebhook(webhook, isTest = false) { }, browser: browserInfo, platform: platformInfo, - triggeredAt: new Date().toISOString(), + triggeredAt: dateTimeVariables.nowIso, }; if (webhook && webhook.identifier) { @@ -212,9 +260,9 @@ async function sendWebhook(webhook, isTest = false) { "{{platform.arch}}": platformInfo.arch || "unknown", "{{platform.os}}": platformInfo.os || "unknown", "{{platform.version}}": platformInfo.version, - "{{triggeredAt}}": new Date().toISOString(), "{{identifier}}": webhook.identifier || "", - "{{selectorContent}}": selectorContent + "{{selectorContent}}": selectorContent, + ...dateTimeVariables.values, }; let customPayloadStr = webhook.customPayload;