-
Notifications
You must be signed in to change notification settings - Fork 16
Implement NIP-17 Private Direct Messages #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Sebastix
merged 14 commits into
nostrver-se:main
from
dsbaars:feature/nip17-private-direct-messaging
May 19, 2025
Merged
Changes from 3 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
8a6ff9c
feat: Implement NIP-17 Private Direct Messaging
dsbaars 6598f5c
fix: fix code style error
dsbaars e8ed694
Merge branch 'main' into nip17-private-direct-messaging
Sebastix e2c930b
fix: Also adapt tests to new names
dsbaars b8d4557
fix: fix typo in nip17 example
dsbaars df1fe8b
chore: Rename Event classes
dsbaars e6570e5
fix spacing
Sebastix 1a6072e
work in progress, refactoring code using the DmRelaysLists.php class …
Sebastix 2358685
phpcs fixes
Sebastix b6b96d5
refactored the part of getting the DM relays from a pubkey with a sep…
Sebastix 961f9c5
Merge branch 'main' into nip17-private-direct-messaging
Sebastix b014ee1
check to connect with relay in Request class when relay is disconnected
Sebastix 54cabbe
use new subscription and request message object for each relay to be …
Sebastix 44b56bd
finish up DmRelaysList class to fetch event kind 10050 of a npub
Sebastix File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace swentel\nostr; | ||
|
|
||
| interface DirectMessageInterface | ||
| { | ||
| /** | ||
| * Send a direct message to a recipient and create a copy for the sender | ||
| * | ||
| * @param string $senderPrivkey The sender's private key | ||
| * @param string $receiverPubkey The receiver's public key | ||
| * @param string $message The message content | ||
| * @param array $additionalTags Additional tags to include | ||
| * @param string|null $replyToId ID of message being replied to (optional) | ||
| * | ||
| * @return array An array containing both GiftWraps (receiver and sender copies) | ||
| */ | ||
| public function sendDirectMessage( | ||
| string $senderPrivkey, | ||
| string $receiverPubkey, | ||
| string $message, | ||
| array $additionalTags = [], | ||
| ?string $replyToId = null, | ||
| ): array; | ||
|
|
||
| /** | ||
| * Check if a pubkey has published a kind 10050 event with preferred relays | ||
| * | ||
| * @param string $pubkey The public key to check | ||
| * @return bool True if the pubkey has published a kind 10050 event | ||
| */ | ||
| public function hasPublishedRelayList(string $pubkey): bool; | ||
|
|
||
| /** | ||
| * Get preferred relays for a pubkey from kind 10050 events | ||
| * | ||
| * @param string $pubkey The public key to get relays for | ||
| * @return array Array of relay URLs | ||
| */ | ||
| public function getPreferredRelaysForPubkey(string $pubkey): array; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace swentel\nostr\Event\Types; | ||
|
|
||
| use swentel\nostr\Event\Event; | ||
|
|
||
| class DirectMessageEvent extends Event | ||
| { | ||
| /** | ||
| * Construct a Direct Message event (kind 14) | ||
| */ | ||
| public function __construct() | ||
| { | ||
| parent::__construct(); | ||
| $this->setKind(14); | ||
| } | ||
|
|
||
| /** | ||
| * Add a recipient to the direct message | ||
| * | ||
| * @param string $pubkey The recipient's public key | ||
| * @param string|null $relayUrl Optional relay URL for the recipient | ||
| * @return self | ||
| */ | ||
| public function addRecipient(string $pubkey, ?string $relayUrl = null): self | ||
| { | ||
| $tag = ['p', $pubkey]; | ||
| if ($relayUrl !== null) { | ||
| $tag[] = $relayUrl; | ||
| } | ||
|
|
||
| $this->addTag($tag); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the message as a reply to another message | ||
| * | ||
| * @param string $eventId The ID of the message being replied to | ||
| * @param string|null $relayUrl Optional relay URL where the original message can be found | ||
| * @return self | ||
| */ | ||
| public function setAsReplyTo(string $eventId, ?string $relayUrl = null): self | ||
| { | ||
| $tag = ['e', $eventId]; | ||
| if ($relayUrl !== null) { | ||
| $tag[] = $relayUrl; | ||
| } | ||
|
|
||
| $this->addTag($tag); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the sender's public key | ||
| * | ||
| * @param string $pubkey The sender's public key | ||
| * @return self | ||
| */ | ||
| public function setSenderPubkey(string $pubkey): self | ||
| { | ||
| $this->pubkey = $pubkey; | ||
| return $this; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace swentel\nostr\Event\Types; | ||
|
|
||
| use swentel\nostr\Event\Event; | ||
|
|
||
| class FileMessageEvent extends Event | ||
| { | ||
| /** | ||
| * Construct a File Message event (kind 15) | ||
| */ | ||
| public function __construct() | ||
| { | ||
| parent::__construct(); | ||
| $this->setKind(15); | ||
| } | ||
|
|
||
| /** | ||
| * Add a recipient to the file message | ||
| * | ||
| * @param string $pubkey The recipient's public key | ||
| * @param string|null $relayUrl Optional relay URL for the recipient | ||
| * @return self | ||
| */ | ||
| public function addRecipient(string $pubkey, ?string $relayUrl = null): self | ||
| { | ||
| $tag = ['p', $pubkey]; | ||
| if ($relayUrl !== null) { | ||
| $tag[] = $relayUrl; | ||
| } | ||
|
|
||
| $this->addTag($tag); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the file type | ||
| * | ||
| * @param string $mimeType The file MIME type | ||
| * @return self | ||
| */ | ||
| public function setFileType(string $mimeType): self | ||
| { | ||
| $this->addTag(['file-type', $mimeType]); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the encryption algorithm | ||
| * | ||
| * @param string $algorithm The encryption algorithm used | ||
| * @return self | ||
| */ | ||
| public function setEncryptionAlgorithm(string $algorithm): self | ||
| { | ||
| $this->addTag(['encryption-algorithm', $algorithm]); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the decryption key | ||
| * | ||
| * @param string $key The decryption key | ||
| * @return self | ||
| */ | ||
| public function setDecryptionKey(string $key): self | ||
| { | ||
| $this->addTag(['decryption-key', $key]); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the decryption nonce | ||
| * | ||
| * @param string $nonce The decryption nonce | ||
| * @return self | ||
| */ | ||
| public function setDecryptionNonce(string $nonce): self | ||
| { | ||
| $this->addTag(['decryption-nonce', $nonce]); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the file hash | ||
| * | ||
| * @param string $hash The SHA-256 hexencoded string of the file | ||
| * @return self | ||
| */ | ||
| public function setFileHash(string $hash): self | ||
| { | ||
| $this->addTag(['x', $hash]); | ||
| return $this; | ||
| } | ||
|
|
||
| /** | ||
| * Set the file URL in the content | ||
| * | ||
| * @param string $url URL where the file can be downloaded | ||
| * @return self | ||
| */ | ||
| public function setFileUrl(string $url): self | ||
| { | ||
| $this->setContent($url); | ||
| return $this; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use swentel\nostr\Key\Key; | ||
| use swentel\nostr\Nip17\DirectMessage; | ||
| use swentel\nostr\Nip59\GiftWrapService; | ||
| use swentel\nostr\Sign\Sign; | ||
|
|
||
| require __DIR__ . '/../../vendor/autoload.php'; | ||
|
|
||
| // Set up keys for Alice and Bob | ||
| $key = new Key(); | ||
| $sign = new Sign(); | ||
| $giftWrapService = new GiftWrapService($key, $sign); | ||
|
|
||
| try { | ||
| // Alice's private and public keys | ||
| $alicePrivKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; | ||
| $alicePubKey = $key->getPublicKey($alicePrivKey); | ||
| print "Alice's public key: " . $alicePubKey . PHP_EOL; | ||
|
|
||
| // Bob's private and public keys | ||
| $bobPrivKey = 'fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210'; | ||
| $bobPubKey = $key->getPublicKey($bobPrivKey); | ||
| print "Bob's public key: " . $bobPubKey . PHP_EOL; | ||
|
|
||
| // Create DirectMessage service | ||
| $directMessage = new DirectMessage($giftWrapService, $key); | ||
|
|
||
| print PHP_EOL . "===== SENDING A PRIVATE DIRECT MESSAGE =====" . PHP_EOL; | ||
|
|
||
| // Message from Alice to Bob | ||
| $messageText = "Hey Bob, this is a private message that only you can read!"; | ||
| print "Alice is sending message to Bob: '" . $messageText . "'" . PHP_EOL; | ||
|
|
||
| // Send the direct message, creating gift wraps for both sender and receiver | ||
| $result = $directMessage->sendDirectMessage( | ||
| $alicePrivKey, | ||
| $bobPubKey, | ||
| $messageText, | ||
| ); | ||
|
|
||
| // Display information about the created gift wraps | ||
| print PHP_EOL . "Two gift wraps were created:" . PHP_EOL; | ||
| print "1. Receiver gift wrap (for Bob):" . PHP_EOL; | ||
| print " - Kind: " . $result['receiver']->getKind() . PHP_EOL; | ||
| print " - ID: " . $result['receiver']->getId() . PHP_EOL; | ||
|
|
||
| print PHP_EOL . "2. Sender gift wrap (for Alice):" . PHP_EOL; | ||
| print " - Kind: " . $result['sender']->getKind() . PHP_EOL; | ||
| print " - ID: " . $result['sender']->getId() . PHP_EOL; | ||
|
|
||
| print PHP_EOL . "===== PUBLISHING GIFT WRAPS TO RELAYS =====" . PHP_EOL; | ||
|
|
||
| // Normally, you would get the relay list from the NIP-10050 event | ||
| // but for this example, we'll use hardcoded relay URLs | ||
| print "Publishing to receiver's relay: wss://relay.example.com" . PHP_EOL; | ||
|
|
||
| // In a real implementation, you would publish to relays from the result | ||
| // $receiverRelays = $result['receiver_relays']; | ||
| // $senderRelays = $result['sender_relays']; | ||
|
|
||
| print PHP_EOL . "===== SIMULATING GIFT WRAP RECEPTION AND DECRYPTION =====" . PHP_EOL; | ||
|
|
||
| // Simulate Bob receiving the gift wrap | ||
| print "Bob received a gift wrap with ID: " . $result['receiver']->getId() . PHP_EOL; | ||
|
|
||
| // In a real scenario, Bob would look for gift wraps addressed to him (p tag with his pubkey) | ||
| // For this example, we'll just use the gift wrap we already created | ||
|
|
||
| // Using the new static decryptDirectMessage helper method | ||
| print "Bob is decrypting the message using his private key and Alice's public key..." . PHP_EOL; | ||
| $decryptedEvent = DirectMessage::decryptDirectMessage( | ||
| $result['receiver'], // The gift wrap to decrypt | ||
| $bobPrivKey, // Bob's private key | ||
| true, // Verify the gift wrap is addressed to Bob | ||
| ); | ||
|
|
||
| if ($decryptedEvent) { | ||
| print "Decryption successful!" . PHP_EOL; | ||
|
|
||
| print PHP_EOL . "===== DECRYPTED MESSAGE DETAILS =====" . PHP_EOL; | ||
| print "Message kind: " . $decryptedEvent['kind'] . PHP_EOL; | ||
| print "Message content: '" . $decryptedEvent['content'] . "'" . PHP_EOL; | ||
|
|
||
| // Get the p tags to identify the participants | ||
| $tags = $decryptedEvent['tags'] ?? []; | ||
| $participants = []; | ||
|
|
||
| foreach ($tags as $tag) { | ||
| if ($tag[0] === 'p') { | ||
| $participants[] = $tag[1]; | ||
| } | ||
| } | ||
|
|
||
| print "Participants: " . implode(', ', $participants) . PHP_EOL; | ||
|
|
||
| // Bob can now reply to Alice using the same process | ||
| print PHP_EOL . "===== BOB REPLYING TO ALICE =====" . PHP_EOL; | ||
|
|
||
| // Create a reply message | ||
| $replyText = "Hey Alice, thanks for your message! This is a private reply."; | ||
| print "Bob is sending reply to Alice: '" . $replyText . "'" . PHP_EOL; | ||
|
|
||
| // Use the ID of the original message for reply | ||
| $originalMessageId = $decryptedEvent['id'] ?? null; | ||
|
|
||
| // Send the reply as a direct message | ||
| $replyResult = $directMessage->sendDirectMessage( | ||
| $bobPrivKey, | ||
| $alicePubKey, | ||
| $replyText, | ||
| [], | ||
| $originalMessageId, // Set as reply to the original message | ||
| ); | ||
|
|
||
| print PHP_EOL . "Reply gift wrap created with ID: " . $replyResult['receiver']->getId() . PHP_EOL; | ||
|
|
||
| // Alice can now decrypt Bob's reply | ||
| print PHP_EOL . "===== ALICE DECRYPTING BOB'S REPLY =====" . PHP_EOL; | ||
| print "Alice received a gift wrap with ID: " . $replyResult['receiver']->getId() . PHP_EOL; | ||
|
|
||
| $decryptedReply = DirectMessage::decryptDirectMessage( | ||
| $replyResult['receiver'], // The gift wrap to decrypt | ||
| $alicePrivKey, // Alice's private key | ||
| true, // Verify the gift wrap is addressed to Alice | ||
| ); | ||
|
|
||
| if ($decryptedReply) { | ||
| print "Alice successfully decrypted Bob's reply!" . PHP_EOL; | ||
| print "Reply content: '" . $decryptedReply['content'] . "'" . PHP_EOL; | ||
|
|
||
| // Check if it's a reply to the original message | ||
| $isReply = false; | ||
| foreach ($decryptedReply['tags'] as $tag) { | ||
| if ($tag[0] === 'e' && isset($tag[1]) && $tag[1] === $decryptedEvent['id']) { | ||
| $isReply = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if ($isReply) { | ||
| print "This is a reply to Alice's original message." . PHP_EOL; | ||
| } | ||
| } else { | ||
| print "Alice could not decrypt the message." . PHP_EOL; | ||
| } | ||
| } else { | ||
| print "Decryption failed! The message might not be for Bob or the encryption keys don't match." . PHP_EOL; | ||
| } | ||
|
|
||
| } catch (Exception $e) { | ||
| print 'Exception: ' . $e->getMessage() . PHP_EOL; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean NIP-65 here with event kind
10002? Or the relays returned by a NIP-05 lookup? NIP-10050 does not exist.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops... I meant kind 10050, as described in NIP-51