Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/DirectMessageInterface.php
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;
}
67 changes: 67 additions & 0 deletions src/Event/Types/DirectMessageEvent.php
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;
}
}
109 changes: 109 additions & 0 deletions src/Event/Types/FileMessageEvent.php
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;
}
}
155 changes: 155 additions & 0 deletions src/Examples/nip17-private-direct-messages.php
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

@Sebastix Sebastix Apr 30, 2025

Copy link
Copy Markdown
Member

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.

Copy link
Copy Markdown
Collaborator Author

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

// 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;
}
Loading