Nostr dm notifications (#3473)

* Add nostr DM notification provider

* require crypto for node 18 compatibility

* remove whitespace

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* move closer to where it is used

* simplify success or failure logic

* don't clobber the non-alert msg

* Update server/notification-providers/nostr.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* polyfills required for node <= 18

* resolve linter warnings

* missing comma

* better websocket polyfill, update deprecated function

* add conditional polyfills for node versions

* lint

* use correct npm for package-lock

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
zappityzap 2023-07-31 02:24:45 -07:00 committed by GitHub
parent db66195f7d
commit eb6167aaf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1378 additions and 2330 deletions

3546
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -100,6 +100,7 @@
"http-proxy-agent": "~5.0.0",
"https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3",
"isomorphic-ws": "^5.0.0",
"jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0",
@ -115,6 +116,7 @@
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nostr-tools": "^1.13.1",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
@ -132,7 +134,8 @@
"socks-proxy-agent": "6.1.1",
"tar": "~6.1.11",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2"
"thirty-two": "~1.0.2",
"ws": "^8.13.0"
},
"devDependencies": {
"@actions/github": "~5.0.1",

View File

@ -0,0 +1,119 @@
const { log } = require("../../src/util");
const NotificationProvider = require("./notification-provider");
const {
relayInit,
getPublicKey,
getEventHash,
getSignature,
nip04,
nip19
} = require("nostr-tools");
// polyfills for node versions
const semver = require("semver");
const nodeVersion = process.version;
if (semver.lt(nodeVersion, "16.0.0")) {
log.warn("monitor", "Node <= 16 is unsupported for nostr, sorry :(");
} else if (semver.lt(nodeVersion, "18.0.0")) {
// polyfills for node 16
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
if (typeof crypto !== "undefined" && !crypto.subtle && crypto.webcrypto) {
crypto.subtle = crypto.webcrypto.subtle;
}
} else if (semver.lt(nodeVersion, "20.0.0")) {
// polyfills for node 18
global.crypto = require("crypto");
global.WebSocket = require("isomorphic-ws");
} else {
// polyfills for node 20
global.WebSocket = require("isomorphic-ws");
}
class Nostr extends NotificationProvider {
name = "nostr";
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
// All DMs should have same timestamp
const createdAt = Math.floor(Date.now() / 1000);
const senderPrivateKey = await this.getPrivateKey(notification.sender);
const senderPublicKey = getPublicKey(senderPrivateKey);
const recipientsPublicKeys = await this.getPublicKeys(notification.recipients);
// Create NIP-04 encrypted direct message event for each recipient
const events = [];
for (const recipientPublicKey of recipientsPublicKeys) {
const ciphertext = await nip04.encrypt(senderPrivateKey, recipientPublicKey, msg);
let event = {
kind: 4,
pubkey: senderPublicKey,
created_at: createdAt,
tags: [[ "p", recipientPublicKey ]],
content: ciphertext,
};
event.id = getEventHash(event);
event.sig = getSignature(event, senderPrivateKey);
events.push(event);
}
// Publish events to each relay
const relays = notification.relays.split("\n");
let successfulRelays = 0;
// Connect to each relay
for (const relayUrl of relays) {
const relay = relayInit(relayUrl);
try {
await relay.connect();
successfulRelays++;
// Publish events
for (const event of events) {
relay.publish(event);
}
} catch (error) {
continue;
} finally {
relay.close();
}
}
// Report success or failure
if (successfulRelays === 0) {
throw Error("Failed to connect to any relays.");
}
return `${successfulRelays}/${relays.length} relays connected.`;
}
async getPrivateKey(sender) {
try {
const senderDecodeResult = await nip19.decode(sender);
const { data } = senderDecodeResult;
return data;
} catch (error) {
throw new Error(`Failed to get private key: ${error.message}`);
}
}
async getPublicKeys(recipients) {
const recipientsList = recipients.split("\n");
const publicKeys = [];
for (const recipient of recipientsList) {
try {
const recipientDecodeResult = await nip19.decode(recipient);
const { type, data } = recipientDecodeResult;
if (type === "npub") {
publicKeys.push(data);
} else {
throw new Error("not an npub");
}
} catch (error) {
throw new Error(`Error decoding recipient: ${error}`);
}
}
return publicKeys;
}
}
module.exports = Nostr;

View File

@ -21,6 +21,7 @@ const LineNotify = require("./notification-providers/linenotify");
const LunaSea = require("./notification-providers/lunasea");
const Matrix = require("./notification-providers/matrix");
const Mattermost = require("./notification-providers/mattermost");
const Nostr = require("./notification-providers/nostr");
const Ntfy = require("./notification-providers/ntfy");
const Octopush = require("./notification-providers/octopush");
const OneBot = require("./notification-providers/onebot");
@ -84,6 +85,7 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneBot(),

View File

@ -126,6 +126,7 @@ export default {
"lunasea": "LunaSea",
"matrix": "Matrix",
"mattermost": "Mattermost",
"nostr": "Nostr",
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",

View File

@ -0,0 +1,26 @@
<template>
<div class="mb-3">
<label for="nostr-relays" class="form-label">{{ $t("nostrRelays") }}<span style="color: red;"><sup>*</sup></span></label>
<textarea id="nostr-relays" v-model="$parent.notification.relays" class="form-control" :required="true" placeholder="wss://127.0.0.1:7777/"></textarea>
<small class="form-text text-muted">{{ $t("nostrRelaysHelp") }}</small>
</div>
<div class="mb-3">
<label for="nostr-sender" class="form-label">{{ $t("nostrSender") }}<span style="color: red;"><sup>*</sup></span></label>
<HiddenInput id="nostr-sender" v-model="$parent.notification.sender" autocomplete="new-password" :required="true"></HiddenInput>
</div>
<div class="mb-3">
<label for="nostr-recipients" class="form-label">{{ $t("nostrRecipients") }}<span style="color: red;"><sup>*</sup></span></label>
<textarea id="nostr-recipients" v-model="$parent.notification.recipients" class="form-control" :required="true" placeholder="npub123...&#10;npub789..."></textarea>
<small class="form-text text-muted">{{ $t("nostrRecipientsHelp") }}</small>
</div>
</template>
<script>
import HiddenInput from "../HiddenInput.vue";
export default {
components: {
HiddenInput,
},
};
</script>

View File

@ -19,6 +19,7 @@ import LineNotify from "./LineNotify.vue";
import LunaSea from "./LunaSea.vue";
import Matrix from "./Matrix.vue";
import Mattermost from "./Mattermost.vue";
import Nostr from "./Nostr.vue";
import Ntfy from "./Ntfy.vue";
import Octopush from "./Octopush.vue";
import OneBot from "./OneBot.vue";
@ -77,6 +78,7 @@ const NotificationFormList = {
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"nostr": Nostr,
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,

View File

@ -788,5 +788,10 @@
"Session Token": "Session Token",
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"Close": "Close",
"Request Body": "Request Body"
"Request Body": "Request Body",
"nostrRelays": "Nostr relays",
"nostrRelaysHelp": "One relay URL per line",
"nostrSender": "Sender Private Key (nsec)",
"nostrRecipients": "Recipients Public Keys (npub)",
"nostrRecipientsHelp": "npub format, one per line"
}