thelounge-plugin-ntfy 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/docker-compose.yml +3 -0
- package/index.js +105 -8
- package/package.json +2 -2
- package/src/config.js +176 -12
- package/src/crypto.js +111 -0
- package/src/handler.js +10 -3
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# The Lounge ntfy Plugin
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
3
6
|
A plugin for [The Lounge](https://thelounge.chat/) that sends a message to an [ntfy](https://ntfy.sh/) server whenever you are mentioned in a chat.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
@@ -21,8 +24,10 @@ This plugin introduces the `/ntfy` command, subcommands are:
|
|
|
21
24
|
- `/ntfy status`: Show the ntfy listener status for this network
|
|
22
25
|
- `/ntfy test`: Send a test notification
|
|
23
26
|
- `/ntfy config`: Config commands
|
|
24
|
-
- `/ntfy config set <setting_key> <setting_value>`: Set a configuration setting
|
|
25
|
-
- `/ntfy config remove <setting_key>`: Set a configuration setting to null
|
|
27
|
+
- `/ntfy config set <setting_key> <setting_value>`: Set a global configuration setting
|
|
28
|
+
- `/ntfy config remove <setting_key>`: Set a global configuration setting to null
|
|
29
|
+
- `/ntfy config network set <setting_key> <setting_value>`: Set a per-network setting for this network
|
|
30
|
+
- `/ntfy config network remove <setting_key>`: Remove per-network setting for this network
|
|
26
31
|
- `/ntfy config print`: Print the current configuration with warnings if any
|
|
27
32
|
|
|
28
33
|
## Setup
|
|
@@ -43,12 +48,14 @@ To start/stop sending push notifications in the desired network, enter:
|
|
|
43
48
|
|
|
44
49
|
## Private Messages
|
|
45
50
|
|
|
46
|
-
By default, you will only be notified when you are mentioned, **this includes messages sent privately to you**. If you want to be notified of all private messages, enter this command and start the notifier like usual:
|
|
51
|
+
By default, you will only be notified when you are mentioned, **this includes messages sent privately to you**. If you want to be notified of all private messages on a specific network, enter this command while connected to that network and start the notifier like usual:
|
|
47
52
|
|
|
48
53
|
```
|
|
49
|
-
/ntfy config set config.notify_on_private_messages true
|
|
54
|
+
/ntfy config network set config.notify_on_private_messages true
|
|
50
55
|
```
|
|
51
56
|
|
|
57
|
+
This setting is per-network, so you can enable it for some networks and disable it for others.
|
|
58
|
+
|
|
52
59
|
## License
|
|
53
60
|
|
|
54
61
|
This plugin is licensed under [MIT](https://opensource.org/license/mit)
|
package/docker-compose.yml
CHANGED
|
@@ -12,3 +12,6 @@ services:
|
|
|
12
12
|
- $PWD/index.js:/var/opt/thelounge/packages/thelounge-plugin-ntfy/index.js
|
|
13
13
|
- $PWD/package.json:/var/opt/thelounge/packages/thelounge-plugin-ntfy/package.json
|
|
14
14
|
- $PWD/package-lock.json:/var/opt/thelounge/packages/thelounge-plugin-ntfy/package-lock.json
|
|
15
|
+
|
|
16
|
+
# Installation: su node -c "thelounge install file:/var/opt/thelounge/packages/thelounge-plugin-ntfy"
|
|
17
|
+
# Removal: su node -c "thelounge uninstall thelounge-plugin-ntfy"
|
package/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
setRootDir,
|
|
7
7
|
loadUserConfig,
|
|
8
8
|
saveUserSetting,
|
|
9
|
+
saveNetworkSetting,
|
|
10
|
+
PER_NETWORK_KEYS,
|
|
9
11
|
} = require("./src/config.js");
|
|
10
12
|
|
|
11
13
|
// user -> Map<network.uuid -> handler and client>
|
|
@@ -22,17 +24,23 @@ const ntfyCommand = {
|
|
|
22
24
|
say(`/${command} start - Start the ntfy listener for this network`);
|
|
23
25
|
say(`/${command} stop - Stop the ntfy listener for this network`);
|
|
24
26
|
say(
|
|
25
|
-
`/${command} status - Show the ntfy listener status for this network
|
|
27
|
+
`/${command} status - Show the ntfy listener status for this network`,
|
|
26
28
|
);
|
|
27
29
|
say(`/${command} test - Send a test notification`);
|
|
28
30
|
say(
|
|
29
|
-
`/${command} config set <setting_key> <setting_value> - Set a configuration setting
|
|
31
|
+
`/${command} config set <setting_key> <setting_value> - Set a global configuration setting`,
|
|
30
32
|
);
|
|
31
33
|
say(
|
|
32
|
-
`/${command} config remove <setting_key> - Set configuration setting to null
|
|
34
|
+
`/${command} config remove <setting_key> - Set global configuration setting to null`,
|
|
33
35
|
);
|
|
34
36
|
say(
|
|
35
|
-
`/${command} config
|
|
37
|
+
`/${command} config network set <setting_key> <setting_value> - Set a per-network setting for this network`,
|
|
38
|
+
);
|
|
39
|
+
say(
|
|
40
|
+
`/${command} config network remove <setting_key> - Remove per-network setting for this network`,
|
|
41
|
+
);
|
|
42
|
+
say(
|
|
43
|
+
`/${command} config print - Print the current configuration with warnings if any`,
|
|
36
44
|
);
|
|
37
45
|
};
|
|
38
46
|
|
|
@@ -197,7 +205,7 @@ const ntfyCommand = {
|
|
|
197
205
|
const response = saveUserSetting(
|
|
198
206
|
client.client.name,
|
|
199
207
|
settingKey,
|
|
200
|
-
settingValue
|
|
208
|
+
settingValue,
|
|
201
209
|
);
|
|
202
210
|
|
|
203
211
|
say(response);
|
|
@@ -217,7 +225,7 @@ const ntfyCommand = {
|
|
|
217
225
|
const response = saveUserSetting(
|
|
218
226
|
client.client.name,
|
|
219
227
|
settingKey,
|
|
220
|
-
null
|
|
228
|
+
null,
|
|
221
229
|
);
|
|
222
230
|
|
|
223
231
|
say(response);
|
|
@@ -228,13 +236,30 @@ const ntfyCommand = {
|
|
|
228
236
|
case "print": {
|
|
229
237
|
const [userConfig, errors] = loadUserConfig(client.client.name);
|
|
230
238
|
|
|
239
|
+
const sensitiveKeys = new Set(["ntfy.password", "ntfy.token"]);
|
|
240
|
+
const perNetworkKeys = new Set([
|
|
241
|
+
"config.notify_on_private_messages",
|
|
242
|
+
]);
|
|
243
|
+
|
|
231
244
|
const printConfig = (obj, parentKey = "") => {
|
|
232
245
|
for (const key in obj) {
|
|
233
246
|
const value = obj[key];
|
|
234
247
|
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
|
235
248
|
|
|
236
|
-
if (
|
|
249
|
+
if (perNetworkKeys.has(fullKey)) {
|
|
250
|
+
// Special handling for per-network settings
|
|
251
|
+
if (typeof value === "object" && value !== null) {
|
|
252
|
+
const networkValue = value[network.uuid];
|
|
253
|
+
say(
|
|
254
|
+
`${fullKey}=${networkValue !== undefined ? networkValue : "(not set for this network)"}`,
|
|
255
|
+
);
|
|
256
|
+
} else {
|
|
257
|
+
say(`${fullKey}=(not set for this network)`);
|
|
258
|
+
}
|
|
259
|
+
} else if (typeof value === "object" && value !== null) {
|
|
237
260
|
printConfig(value, fullKey);
|
|
261
|
+
} else if (sensitiveKeys.has(fullKey) && value) {
|
|
262
|
+
say(`${fullKey}=********`);
|
|
238
263
|
} else {
|
|
239
264
|
say(`${fullKey}=${value}`);
|
|
240
265
|
}
|
|
@@ -257,13 +282,85 @@ const ntfyCommand = {
|
|
|
257
282
|
userConfig.ntfy.password
|
|
258
283
|
) {
|
|
259
284
|
say(
|
|
260
|
-
"Warning: Both ntfy.token and ntfy.username/password are set, ntfy.token will be used for authentication"
|
|
285
|
+
"Warning: Both ntfy.token and ntfy.username/password are set, ntfy.token will be used for authentication",
|
|
261
286
|
);
|
|
262
287
|
}
|
|
263
288
|
|
|
264
289
|
break;
|
|
265
290
|
}
|
|
266
291
|
|
|
292
|
+
case "network": {
|
|
293
|
+
const networkArgs = subsubcommand.slice(1);
|
|
294
|
+
|
|
295
|
+
if (networkArgs.length === 0) {
|
|
296
|
+
helpMessage();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof networkArgs[0] !== "string") {
|
|
301
|
+
helpMessage();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
switch (networkArgs[0].toLowerCase()) {
|
|
306
|
+
case "set": {
|
|
307
|
+
const setArgs = networkArgs.slice(1);
|
|
308
|
+
|
|
309
|
+
if (setArgs.length < 2) {
|
|
310
|
+
say("Usage: /ntfy config network set <setting_key> <value>");
|
|
311
|
+
say(
|
|
312
|
+
`Available per-network settings: ${Array.from(PER_NETWORK_KEYS).join(", ")}`,
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const settingKey = setArgs[0];
|
|
318
|
+
const settingValue = setArgs.slice(1).join(" ");
|
|
319
|
+
const response = saveNetworkSetting(
|
|
320
|
+
client.client.name,
|
|
321
|
+
settingKey,
|
|
322
|
+
network.uuid,
|
|
323
|
+
settingValue,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
say(response);
|
|
327
|
+
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case "remove": {
|
|
332
|
+
const removeArgs = networkArgs.slice(1);
|
|
333
|
+
|
|
334
|
+
if (removeArgs.length < 1) {
|
|
335
|
+
say("Usage: /ntfy config network remove <setting_key>");
|
|
336
|
+
say(
|
|
337
|
+
`Available per-network settings: ${Array.from(PER_NETWORK_KEYS).join(", ")}`,
|
|
338
|
+
);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const settingKey = removeArgs[0];
|
|
343
|
+
const response = saveNetworkSetting(
|
|
344
|
+
client.client.name,
|
|
345
|
+
settingKey,
|
|
346
|
+
network.uuid,
|
|
347
|
+
null,
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
say(response);
|
|
351
|
+
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
default: {
|
|
356
|
+
helpMessage();
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
267
364
|
default: {
|
|
268
365
|
helpMessage();
|
|
269
366
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thelounge-plugin-ntfy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A plugin for The Lounge that sends push notifications via ntfy when highlighted",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"thelounge",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"ajv": "^8.17.1",
|
|
30
30
|
"ajv-errors": "^3.0.0",
|
|
31
31
|
"ajv-formats": "^3.0.1",
|
|
32
|
-
"ntfy": "
|
|
32
|
+
"ntfy": "~1.13.3",
|
|
33
33
|
"thelounge": "^4.4.3"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/config.js
CHANGED
|
@@ -5,6 +5,12 @@ const path = require("path");
|
|
|
5
5
|
const Ajv2019 = require("ajv/dist/2019").default;
|
|
6
6
|
const addFormats = require("ajv-formats");
|
|
7
7
|
const addErrors = require("ajv-errors");
|
|
8
|
+
const {
|
|
9
|
+
initEncryptionKey,
|
|
10
|
+
encrypt,
|
|
11
|
+
decrypt,
|
|
12
|
+
isEncrypted,
|
|
13
|
+
} = require("./crypto.js");
|
|
8
14
|
|
|
9
15
|
const DEFAULT_CONFIG = {
|
|
10
16
|
ntfy: {
|
|
@@ -15,20 +21,23 @@ const DEFAULT_CONFIG = {
|
|
|
15
21
|
token: null,
|
|
16
22
|
},
|
|
17
23
|
config: {
|
|
18
|
-
notify_on_private_messages: false
|
|
24
|
+
notify_on_private_messages: {}, // Per-network: { "network-uuid": true/false }
|
|
19
25
|
},
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
const
|
|
28
|
+
const GLOBAL_KEYS = new Set([
|
|
23
29
|
"ntfy.server",
|
|
24
30
|
"ntfy.topic",
|
|
25
31
|
"ntfy.username",
|
|
26
32
|
"ntfy.password",
|
|
27
33
|
"ntfy.token",
|
|
28
|
-
"config.notify_on_private_messages",
|
|
29
34
|
]);
|
|
35
|
+
const GLOBAL_BOOLEAN_KEYS = new Set([]);
|
|
30
36
|
|
|
31
|
-
const
|
|
37
|
+
const PER_NETWORK_KEYS = new Set(["config.notify_on_private_messages"]);
|
|
38
|
+
const PER_NETWORK_BOOLEAN_KEYS = new Set(["config.notify_on_private_messages"]);
|
|
39
|
+
|
|
40
|
+
const SENSITIVE_KEYS = new Set(["ntfy.password", "ntfy.token"]);
|
|
32
41
|
|
|
33
42
|
const userConfigSchema = {
|
|
34
43
|
type: "object",
|
|
@@ -102,8 +111,9 @@ const userConfigSchema = {
|
|
|
102
111
|
required: ["notify_on_private_messages"],
|
|
103
112
|
properties: {
|
|
104
113
|
notify_on_private_messages: {
|
|
105
|
-
type: "
|
|
106
|
-
|
|
114
|
+
type: "object",
|
|
115
|
+
additionalProperties: { type: ["boolean", "string"] },
|
|
116
|
+
default: {},
|
|
107
117
|
},
|
|
108
118
|
},
|
|
109
119
|
},
|
|
@@ -121,6 +131,38 @@ let rootDir = null;
|
|
|
121
131
|
|
|
122
132
|
function setRootDir(dir) {
|
|
123
133
|
rootDir = dir;
|
|
134
|
+
initEncryptionKey(dir);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function decryptSensitiveFields(config) {
|
|
138
|
+
const decrypted = JSON.parse(JSON.stringify(config)); // Deep clone
|
|
139
|
+
|
|
140
|
+
for (const key of SENSITIVE_KEYS) {
|
|
141
|
+
const keys = key.split(".");
|
|
142
|
+
let curr = decrypted;
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
145
|
+
if (curr[keys[i]]) {
|
|
146
|
+
curr = curr[keys[i]];
|
|
147
|
+
} else {
|
|
148
|
+
curr = null;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (curr) {
|
|
154
|
+
const finalKey = keys[keys.length - 1];
|
|
155
|
+
if (curr[finalKey] && isEncrypted(curr[finalKey])) {
|
|
156
|
+
try {
|
|
157
|
+
curr[finalKey] = decrypt(curr[finalKey]);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Leave as-is
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return decrypted;
|
|
124
166
|
}
|
|
125
167
|
|
|
126
168
|
function loadUserConfig(username) {
|
|
@@ -142,10 +184,12 @@ function loadUserConfig(username) {
|
|
|
142
184
|
userConfig = JSON.parse(fs.readFileSync(userConfigPath, "utf-8"));
|
|
143
185
|
} catch (e) {
|
|
144
186
|
throw new Error(
|
|
145
|
-
`Invalid JSON in user config for ${username}: ${e.message}
|
|
187
|
+
`Invalid JSON in user config for ${username}: ${e.message}`,
|
|
146
188
|
);
|
|
147
189
|
}
|
|
148
190
|
|
|
191
|
+
userConfig = decryptSensitiveFields(userConfig);
|
|
192
|
+
|
|
149
193
|
const validate = ajv.compile(userConfigSchema);
|
|
150
194
|
const valid = validate(userConfig);
|
|
151
195
|
|
|
@@ -153,12 +197,39 @@ function loadUserConfig(username) {
|
|
|
153
197
|
}
|
|
154
198
|
}
|
|
155
199
|
|
|
200
|
+
function encryptSensitiveFields(config) {
|
|
201
|
+
const encrypted = JSON.parse(JSON.stringify(config)); // Deep clone
|
|
202
|
+
|
|
203
|
+
for (const key of SENSITIVE_KEYS) {
|
|
204
|
+
const keys = key.split(".");
|
|
205
|
+
let curr = encrypted;
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
208
|
+
if (curr[keys[i]]) {
|
|
209
|
+
curr = curr[keys[i]];
|
|
210
|
+
} else {
|
|
211
|
+
curr = null;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (curr) {
|
|
217
|
+
const finalKey = keys[keys.length - 1];
|
|
218
|
+
if (curr[finalKey] && !isEncrypted(curr[finalKey])) {
|
|
219
|
+
curr[finalKey] = encrypt(curr[finalKey]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return encrypted;
|
|
225
|
+
}
|
|
226
|
+
|
|
156
227
|
function saveUserSetting(username, settingKey, settingValue) {
|
|
157
228
|
if (!rootDir) {
|
|
158
229
|
throw new Error("Root directory is not set");
|
|
159
230
|
}
|
|
160
231
|
|
|
161
|
-
if (
|
|
232
|
+
if (GLOBAL_KEYS.has(settingKey)) {
|
|
162
233
|
let userConfig = loadUserConfig(username)[0];
|
|
163
234
|
|
|
164
235
|
const keys = settingKey.split(".");
|
|
@@ -176,7 +247,7 @@ function saveUserSetting(username, settingKey, settingValue) {
|
|
|
176
247
|
return `Error: expected value to be a string`;
|
|
177
248
|
}
|
|
178
249
|
|
|
179
|
-
if (
|
|
250
|
+
if (GLOBAL_BOOLEAN_KEYS.has(settingKey)) {
|
|
180
251
|
try {
|
|
181
252
|
settingValue = settingValue
|
|
182
253
|
? JSON.parse(settingValue.toLowerCase())
|
|
@@ -192,25 +263,118 @@ function saveUserSetting(username, settingKey, settingValue) {
|
|
|
192
263
|
|
|
193
264
|
curr[keys[keys.length - 1]] = settingValue;
|
|
194
265
|
|
|
266
|
+
const configToSave = encryptSensitiveFields(userConfig);
|
|
267
|
+
|
|
195
268
|
const userConfigPath = path.join(rootDir, "config", `${username}.json`);
|
|
196
269
|
|
|
197
270
|
fs.mkdirSync(path.dirname(userConfigPath), { recursive: true });
|
|
198
271
|
fs.writeFileSync(
|
|
199
272
|
userConfigPath,
|
|
200
|
-
JSON.stringify(
|
|
201
|
-
"utf-8"
|
|
273
|
+
JSON.stringify(configToSave, null, 2),
|
|
274
|
+
"utf-8",
|
|
202
275
|
);
|
|
203
276
|
|
|
204
277
|
return "Success";
|
|
205
278
|
}
|
|
206
279
|
|
|
207
280
|
return `Invalid setting ${settingKey}, allowed settings are: ${Array.from(
|
|
208
|
-
|
|
281
|
+
GLOBAL_KEYS,
|
|
209
282
|
).join(", ")}`;
|
|
210
283
|
}
|
|
211
284
|
|
|
285
|
+
function saveNetworkSetting(username, settingKey, networkUuid, settingValue) {
|
|
286
|
+
if (!rootDir) {
|
|
287
|
+
throw new Error("Root directory is not set");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!PER_NETWORK_KEYS.has(settingKey)) {
|
|
291
|
+
return `Invalid per-network setting ${settingKey}, allowed settings are: ${Array.from(
|
|
292
|
+
PER_NETWORK_KEYS,
|
|
293
|
+
).join(", ")}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let userConfig = loadUserConfig(username)[0];
|
|
297
|
+
|
|
298
|
+
const keys = settingKey.split(".");
|
|
299
|
+
|
|
300
|
+
let curr = userConfig;
|
|
301
|
+
|
|
302
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
303
|
+
const key = keys[i];
|
|
304
|
+
if (key in curr) {
|
|
305
|
+
curr = curr[key];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const finalKey = keys[keys.length - 1];
|
|
310
|
+
|
|
311
|
+
if (!curr[finalKey] || typeof curr[finalKey] !== "object") {
|
|
312
|
+
curr[finalKey] = {};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (settingValue === null) {
|
|
316
|
+
delete curr[finalKey][networkUuid];
|
|
317
|
+
} else if (PER_NETWORK_BOOLEAN_KEYS.has(settingKey)) {
|
|
318
|
+
try {
|
|
319
|
+
const boolValue = JSON.parse(settingValue.toLowerCase());
|
|
320
|
+
|
|
321
|
+
if (typeof boolValue !== "boolean") {
|
|
322
|
+
return `Invalid value for ${settingKey}, expected a boolean`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
curr[finalKey][networkUuid] = boolValue;
|
|
326
|
+
} catch {
|
|
327
|
+
return `Invalid value for ${settingKey}, expected a boolean (true/false)`;
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
curr[finalKey][networkUuid] = settingValue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const configToSave = encryptSensitiveFields(userConfig);
|
|
334
|
+
|
|
335
|
+
const userConfigPath = path.join(rootDir, "config", `${username}.json`);
|
|
336
|
+
|
|
337
|
+
fs.mkdirSync(path.dirname(userConfigPath), { recursive: true });
|
|
338
|
+
fs.writeFileSync(
|
|
339
|
+
userConfigPath,
|
|
340
|
+
JSON.stringify(configToSave, null, 2),
|
|
341
|
+
"utf-8",
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return "Success";
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getNetworkSetting(
|
|
348
|
+
userConfig,
|
|
349
|
+
settingKey,
|
|
350
|
+
networkUuid,
|
|
351
|
+
defaultValue = false,
|
|
352
|
+
) {
|
|
353
|
+
const keys = settingKey.split(".");
|
|
354
|
+
|
|
355
|
+
let curr = userConfig;
|
|
356
|
+
|
|
357
|
+
for (const key of keys) {
|
|
358
|
+
if (curr && typeof curr === "object" && key in curr) {
|
|
359
|
+
curr = curr[key];
|
|
360
|
+
} else {
|
|
361
|
+
return defaultValue;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (curr && typeof curr === "object" && networkUuid in curr) {
|
|
366
|
+
return curr[networkUuid];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return defaultValue;
|
|
370
|
+
}
|
|
371
|
+
|
|
212
372
|
module.exports = {
|
|
213
373
|
setRootDir,
|
|
214
374
|
loadUserConfig,
|
|
215
375
|
saveUserSetting,
|
|
376
|
+
saveNetworkSetting,
|
|
377
|
+
getNetworkSetting,
|
|
378
|
+
PER_NETWORK_KEYS,
|
|
379
|
+
PER_NETWORK_BOOLEAN_KEYS,
|
|
216
380
|
};
|
package/src/crypto.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const ALGORITHM = "aes-256-gcm";
|
|
8
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
9
|
+
const IV_LENGTH = 16; // 128 bits
|
|
10
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
11
|
+
const ENCRYPTED_PREFIX = "enc:";
|
|
12
|
+
|
|
13
|
+
let encryptionKey = null;
|
|
14
|
+
|
|
15
|
+
function initEncryptionKey(configDir) {
|
|
16
|
+
const keyPath = path.join(configDir, ".ntfy_key");
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(keyPath)) {
|
|
19
|
+
const keyData = fs.readFileSync(keyPath, "utf-8").trim();
|
|
20
|
+
encryptionKey = Buffer.from(keyData, "hex");
|
|
21
|
+
|
|
22
|
+
if (encryptionKey.length !== KEY_LENGTH) {
|
|
23
|
+
throw new Error("Invalid encryption key length");
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
encryptionKey = crypto.randomBytes(KEY_LENGTH);
|
|
27
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(keyPath, encryptionKey.toString("hex"), {
|
|
29
|
+
encoding: "utf-8",
|
|
30
|
+
mode: 0o600, // R/W for owner only
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isInitialized() {
|
|
36
|
+
return encryptionKey !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function encrypt(plaintext) {
|
|
40
|
+
if (!encryptionKey) {
|
|
41
|
+
throw new Error("Encryption key not initialized");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!plaintext || typeof plaintext !== "string") {
|
|
45
|
+
return plaintext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (plaintext.startsWith(ENCRYPTED_PREFIX)) {
|
|
49
|
+
return plaintext;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
53
|
+
const cipher = crypto.createCipheriv(ALGORITHM, encryptionKey, iv, {
|
|
54
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
58
|
+
encrypted += cipher.final("hex");
|
|
59
|
+
|
|
60
|
+
const authTag = cipher.getAuthTag();
|
|
61
|
+
|
|
62
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function decrypt(encryptedText) {
|
|
66
|
+
if (!encryptionKey) {
|
|
67
|
+
throw new Error("Encryption key not initialized");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!encryptedText || typeof encryptedText !== "string") {
|
|
71
|
+
return encryptedText;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!encryptedText.startsWith(ENCRYPTED_PREFIX)) {
|
|
75
|
+
return encryptedText;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = encryptedText.slice(ENCRYPTED_PREFIX.length);
|
|
79
|
+
const parts = data.split(":");
|
|
80
|
+
|
|
81
|
+
if (parts.length !== 3) {
|
|
82
|
+
throw new Error("Invalid encrypted data format");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const [ivHex, authTagHex, encrypted] = parts;
|
|
86
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
87
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
88
|
+
|
|
89
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, encryptionKey, iv, {
|
|
90
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
91
|
+
});
|
|
92
|
+
decipher.setAuthTag(authTag);
|
|
93
|
+
|
|
94
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
95
|
+
decrypted += decipher.final("utf8");
|
|
96
|
+
|
|
97
|
+
return decrypted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isEncrypted(value) {
|
|
101
|
+
return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
initEncryptionKey,
|
|
106
|
+
isInitialized,
|
|
107
|
+
encrypt,
|
|
108
|
+
decrypt,
|
|
109
|
+
isEncrypted,
|
|
110
|
+
ENCRYPTED_PREFIX,
|
|
111
|
+
};
|
package/src/handler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const { loadUserConfig } = require("./config.js");
|
|
3
|
+
const { loadUserConfig, getNetworkSetting } = require("./config.js");
|
|
4
4
|
const { PluginLogger } = require("./logger.js");
|
|
5
5
|
|
|
6
6
|
function createHandler(client, network) {
|
|
@@ -23,7 +23,7 @@ function createHandler(client, network) {
|
|
|
23
23
|
// Mentions always notify
|
|
24
24
|
notify = true;
|
|
25
25
|
} else if (isPM) {
|
|
26
|
-
// PMs notify only if enabled in config
|
|
26
|
+
// PMs notify only if enabled in config for this network
|
|
27
27
|
const [uc, errors] = loadUserConfig(client.client.name);
|
|
28
28
|
|
|
29
29
|
if (errors.length > 0) {
|
|
@@ -32,7 +32,14 @@ function createHandler(client, network) {
|
|
|
32
32
|
|
|
33
33
|
userConfig = uc;
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
const notifyOnPMs = getNetworkSetting(
|
|
36
|
+
userConfig,
|
|
37
|
+
"config.notify_on_private_messages",
|
|
38
|
+
network.uuid,
|
|
39
|
+
false,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (notifyOnPMs) {
|
|
36
43
|
notify = true;
|
|
37
44
|
}
|
|
38
45
|
}
|