diff options
Diffstat (limited to 'server/notification-providers/nostr.js')
-rw-r--r-- | server/notification-providers/nostr.js | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/server/notification-providers/nostr.js b/server/notification-providers/nostr.js new file mode 100644 index 0000000..8784738 --- /dev/null +++ b/server/notification-providers/nostr.js @@ -0,0 +1,122 @@ +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, "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"; + + /** + * @inheritdoc + */ + 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.`; + } + + /** + * Get the private key for the sender + * @param {string} sender Sender to retrieve key for + * @returns {nip19.DecodeResult} Private key + */ + 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}`); + } + } + + /** + * Get public keys for recipients + * @param {string} recipients Newline delimited list of recipients + * @returns {Promise<nip19.DecodeResult[]>} Public keys + */ + 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; |