Skip to content

Commit

Permalink
Nostr dm notifications (louislam#3051)
Browse files Browse the repository at this point in the history
* 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

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>
  • Loading branch information
zappityzap and CommanderStorm authored Jun 27, 2023
1 parent 4f60358 commit b8bcb6f
Show file tree
Hide file tree
Showing 8 changed files with 427 additions and 6 deletions.
290 changes: 286 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"node-cloudflared-tunnel": "~1.0.9",
"node-radius-client": "~1.0.0",
"nodemailer": "~6.6.5",
"nostr-tools": "^1.8.1",
"notp": "~2.0.3",
"password-hash": "~1.2.2",
"pg": "~8.8.0",
Expand All @@ -124,7 +125,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",
"websocket-polyfill": "^0.0.3"
},
"devDependencies": {
"@actions/github": "~5.0.1",
Expand Down
101 changes: 101 additions & 0 deletions server/notification-providers/nostr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const NotificationProvider = require("./notification-provider");
// polyfill for node <= 18
global.crypto = require("crypto");
const {
relayInit,
getPublicKey,
getEventHash,
signEvent,
nip04,
nip19
} = require("nostr-tools");
// polyfill for node <= 18
require("websocket-polyfill");

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 = signEvent(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;
2 changes: 2 additions & 0 deletions server/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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");
Expand Down Expand Up @@ -82,6 +83,7 @@ class Notification {
new LunaSea(),
new Matrix(),
new Mattermost(),
new Nostr(),
new Ntfy(),
new Octopush(),
new OneBot(),
Expand Down
1 change: 1 addition & 0 deletions src/components/NotificationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export default {
"lunasea": "LunaSea",
"matrix": "Matrix",
"mattermost": "Mattermost",
"nostr": "Nostr",
"ntfy": "Ntfy",
"octopush": "Octopush",
"OneBot": "OneBot",
Expand Down
26 changes: 26 additions & 0 deletions src/components/notifications/Nostr.vue
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 2 additions & 0 deletions src/components/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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";
Expand Down Expand Up @@ -75,6 +76,7 @@ const NotificationFormList = {
"lunasea": LunaSea,
"matrix": Matrix,
"mattermost": Mattermost,
"nostr": Nostr,
"ntfy": Ntfy,
"octopush": Octopush,
"OneBot": OneBot,
Expand Down
7 changes: 6 additions & 1 deletion src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -755,5 +755,10 @@
"Group": "Group",
"Monitor Group": "Monitor Group",
"noGroupMonitorMsg": "Not Available. Create a Group Monitor First.",
"Close": "Close"
"Close": "Close",
"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"
}

0 comments on commit b8bcb6f

Please sign in to comment.