Skip to content

Commit 695995c

Browse files
feat: support DateTime specific variables
1 parent de6af2a commit 695995c

3 files changed

Lines changed: 98 additions & 26 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

popup/popup.js

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ document
158158
if (webhook && webhook.customPayload) {
159159
try {
160160
// Create variable replacements map
161+
const now = new Date();
162+
const nowLocal = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
163+
161164
const replacements = {
162165
"{{tab.title}}": activeTab.title,
163166
"{{tab.url}}": currentUrl,
@@ -172,30 +175,48 @@ document
172175
"{{platform.arch}}": platformInfo.arch || "unknown",
173176
"{{platform.os}}": platformInfo.os || "unknown",
174177
"{{platform.version}}": platformInfo.version,
175-
"{{triggeredAt}}": new Date().toISOString(),
176-
"{{identifier}}": webhook.identifier || ""
178+
"{{identifier}}": webhook.identifier || "",
179+
// Legacy variable
180+
"{{triggeredAt}}": now.toISOString(),
181+
// New DateTime variables (UTC)
182+
"{{now.iso}}": now.toISOString(),
183+
"{{now.date}}": now.toISOString().slice(0, 10),
184+
"{{now.time}}": now.toISOString().slice(11, 19),
185+
"{{now.unix}}": Math.floor(now.getTime() / 1000),
186+
"{{now.unix_ms}}": now.getTime(),
187+
"{{now.year}}": now.getUTCFullYear(),
188+
"{{now.month}}": now.getUTCMonth() + 1,
189+
"{{now.day}}": now.getUTCDate(),
190+
"{{now.hour}}": now.getUTCHours(),
191+
"{{now.minute}}": now.getUTCMinutes(),
192+
"{{now.second}}": now.getUTCSeconds(),
193+
"{{now.millisecond}}": now.getUTCMilliseconds(),
194+
// New DateTime variables (local)
195+
"{{now.local.iso}}": nowLocal.toISOString().slice(0, -1) + (now.getTimezoneOffset() > 0 ? "-" : "+") + ("0" + Math.abs(now.getTimezoneOffset() / 60)).slice(-2) + ":" + ("0" + Math.abs(now.getTimezoneOffset() % 60)).slice(-2),
196+
"{{now.local.date}}": nowLocal.toISOString().slice(0, 10),
197+
"{{now.local.time}}": nowLocal.toISOString().slice(11, 19),
177198
};
178199

179-
// Replace placeholders in custom payload
180-
let customPayloadStr = webhook.customPayload;
181-
Object.entries(replacements).forEach(([placeholder, value]) => {
182-
// Handle different types of values
183-
// For string values in JSON, we need to handle them differently based on context
184-
// If the placeholder is inside quotes in the JSON, we should not add quotes again
185-
const isPlaceholderInQuotes = customPayloadStr.match(new RegExp(`"[^"]*${placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^"]*"`, 'g'));
186-
187-
const replaceValue = typeof value === 'string'
188-
? (isPlaceholderInQuotes ? value.replace(/"/g, '\\"') : `"${value.replace(/"/g, '\\"')}"`)
189-
: (value === undefined ? 'null' : JSON.stringify(value));
200+
// Parse the custom payload as JSON
201+
let customPayload = JSON.parse(webhook.customPayload);
190202

191-
customPayloadStr = customPayloadStr.replace(
192-
new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
193-
replaceValue
194-
);
195-
});
203+
// Recursively replace placeholders
204+
const replacePlaceholders = (obj) => {
205+
for (const key in obj) {
206+
if (obj.hasOwnProperty(key)) {
207+
if (typeof obj[key] === 'string') {
208+
const placeholder = obj[key];
209+
if (replacements.hasOwnProperty(placeholder)) {
210+
obj[key] = replacements[placeholder];
211+
}
212+
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
213+
replacePlaceholders(obj[key]);
214+
}
215+
}
216+
}
217+
};
196218

197-
// Parse the resulting JSON
198-
const customPayload = JSON.parse(customPayloadStr);
219+
replacePlaceholders(customPayload);
199220

200221
// Use the custom payload instead of the default one
201222
payload = customPayload;

tests/popup.test.js

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,25 @@ describe('popup script', () => {
6969
expect(fetchMock.mock.calls[0][0]).toBe('https://hook.test');
7070
});
7171

72-
test('uses custom payload when available', async () => {
73-
const customPayload = '{"message": "Custom message with {{tab.title}}"}';
72+
test('uses custom payload with all datetime variables', async () => {
73+
const customPayload = JSON.stringify({
74+
"triggeredAt": "{{triggeredAt}}",
75+
"now.iso": "{{now.iso}}",
76+
"now.date": "{{now.date}}",
77+
"now.time": "{{now.time}}",
78+
"now.unix": "{{now.unix}}",
79+
"now.unix_ms": "{{now.unix_ms}}",
80+
"now.year": "{{now.year}}",
81+
"now.month": "{{now.month}}",
82+
"now.day": "{{now.day}}",
83+
"now.hour": "{{now.hour}}",
84+
"now.minute": "{{now.minute}}",
85+
"now.second": "{{now.second}}",
86+
"now.millisecond": "{{now.millisecond}}",
87+
"now.local.iso": "{{now.local.iso}}",
88+
"now.local.date": "{{now.local.date}}",
89+
"now.local.time": "{{now.local.time}}",
90+
});
7491
const hook = {
7592
id: '1',
7693
label: 'Send',
@@ -91,6 +108,18 @@ describe('popup script', () => {
91108
status: 'complete'
92109
}]);
93110

111+
// Mock Date
112+
const mockDate = new Date('2025-08-07T10:20:30.123Z');
113+
const OriginalDate = global.Date;
114+
global.Date = jest.fn((...args) => {
115+
if (args.length) {
116+
return new OriginalDate(...args);
117+
}
118+
return mockDate;
119+
});
120+
global.Date.now = jest.fn(() => mockDate.getTime());
121+
global.Date.toISOString = jest.fn(() => mockDate.toISOString());
122+
94123
require('../popup/popup.js');
95124
document.dispatchEvent(new dom.window.Event('DOMContentLoaded'));
96125
await new Promise(setImmediate);
@@ -102,10 +131,32 @@ describe('popup script', () => {
102131

103132
expect(fetchMock).toHaveBeenCalled();
104133

105-
// Check that the custom payload was used with the placeholder replaced
106134
const fetchOptions = fetchMock.mock.calls[0][1];
107135
const sentPayload = JSON.parse(fetchOptions.body);
108-
expect(sentPayload).toEqual({ message: 'Custom message with Test Page' });
136+
137+
const expectedPayload = {
138+
"triggeredAt": "2025-08-07T10:20:30.123Z",
139+
"now.iso": "2025-08-07T10:20:30.123Z",
140+
"now.date": "2025-08-07",
141+
"now.time": "10:20:30",
142+
"now.unix": Math.floor(mockDate.getTime() / 1000),
143+
"now.unix_ms": mockDate.getTime(),
144+
"now.year": 2025,
145+
"now.month": 8,
146+
"now.day": 7,
147+
"now.hour": 10,
148+
"now.minute": 20,
149+
"now.second": 30,
150+
"now.millisecond": 123,
151+
"now.local.iso": "2025-08-07T10:20:30.123+00:00",
152+
"now.local.date": "2025-08-07",
153+
"now.local.time": "10:20:30"
154+
};
155+
156+
expect(sentPayload).toEqual(expectedPayload);
157+
158+
// Restore Date mock
159+
global.Date = OriginalDate;
109160
});
110161

111162
test('filters webhooks based on urlFilter', async () => {

0 commit comments

Comments
 (0)