NAME
Net::Nostr::DirectMessage - NIP-17 private direct messages
SYNOPSIS
use Net::Nostr::DirectMessage;
use Net::Nostr::Key;
my $sender = Net::Nostr::Key->new;
my $recipient = Net::Nostr::Key->new;
# Create a chat message (kind 14 rumor)
my $msg = Net::Nostr::DirectMessage->create(
sender_pubkey => $sender->pubkey_hex,
content => 'Hola, que tal?',
recipients => [$recipient->pubkey_hex],
subject => 'Party',
);
# Wrap for all recipients + sender
my @wraps = Net::Nostr::DirectMessage->wrap_for_recipients(
rumor => $msg,
sender_key => $sender,
);
# Publish each wrap to recipient's kind 10050 relays
# Recipient unwraps
my $received = Net::Nostr::DirectMessage->receive(
event => $wraps[0],
recipient_key => $recipient,
);
say $received->content; # "Hola, que tal?"
# Create a DM relay list (kind 10050)
my $relay_list = Net::Nostr::DirectMessage->create_relay_list(
pubkey => $sender->pubkey_hex,
relays => ['wss://inbox.nostr.wine', 'wss://myrelay.nostr1.com'],
);
DESCRIPTION
Implements NIP-17 private direct messages using NIP-44 encryption and NIP-59 gift wrapping.
Messages are created as unsigned kind 14 (chat) or kind 15 (file) events, then sealed and gift-wrapped individually for each recipient and the sender. The gift wrap layer is designed to hide participant identities, timestamps, and message content from relays and third parties. However, metadata protection depends on proper usage: relay access patterns, network-level metadata, and the p tag on the outer wrap (needed for routing) are still visible to the relay.
Other event kinds (e.g. kind 7 reactions) MAY also be gift-wrapped and sent to chat participants using the same seal-and-wrap mechanism.
The set of pubkey + p tags defines a chat room. An optional subject tag sets the conversation topic.
CLASS METHODS
create
my $msg = Net::Nostr::DirectMessage->create(
sender_pubkey => $pubkey_hex,
content => 'Hello!',
recipients => [$recipient_pubkey],
subject => 'Optional topic', # optional
reply_to => $parent_event_id, # optional
quotes => [[$event_id, $relay, $pubkey]], # optional
);
Creates a kind 14 chat message as an unsigned rumor. content MUST be plain text. recipients is a non-empty arrayref of pubkey hex strings or [$pubkey, $relay_url] pairs for relay hints.
reply_to can be a plain event ID string or an arrayref [$event_id, $relay_url] for relay hints.
quotes is an optional arrayref of [$event_id, $relay_url, $pubkey] triples for citing other events via q tags.
# Simple recipients
my $msg = Net::Nostr::DirectMessage->create(
sender_pubkey => $key->pubkey_hex,
content => 'Hello!',
recipients => [$bob_pubkey, $carol_pubkey],
);
# With relay hints
my $msg = Net::Nostr::DirectMessage->create(
sender_pubkey => $key->pubkey_hex,
content => 'Hello!',
recipients => [[$bob_pubkey, 'wss://relay.example.com']],
reply_to => [$parent_id, 'wss://relay.example.com'],
);
# Quoting another event
my $msg = Net::Nostr::DirectMessage->create(
sender_pubkey => $key->pubkey_hex,
content => 'check this out',
recipients => [$recipient_pubkey],
quotes => [[$cited_id, 'wss://relay.example.com', $cited_pubkey]],
);
create_file
my $msg = Net::Nostr::DirectMessage->create_file(
sender_pubkey => $pubkey_hex,
content => 'https://example.com/encrypted-file.bin',
recipients => [$recipient_pubkey],
file_type => 'image/jpeg',
encryption_algorithm => 'aes-gcm',
decryption_key => $key,
decryption_nonce => $nonce,
x => $sha256_hex,
);
Creates a kind 15 file message as an unsigned rumor. The content is the file URL. Required tags: file-type, encryption-algorithm, decryption-key, decryption-nonce, x. recipients must contain at least one recipient. encryption-algorithm currently supports only aes-gcm. x and optional ox must be 64-character lowercase hex SHA-256 digests.
Optional tags: ox (SHA-256 of original file), size, dim, blurhash, thumb, fallback (arrayref of fallback URLs), subject (conversation topic).
recipients and reply_to accept relay hints in the same format as "create". For file replies, use an arrayref with the "reply" marker: [$event_id, $relay_url, 'reply'].
# With all optional tags
my $msg = Net::Nostr::DirectMessage->create_file(
sender_pubkey => $key->pubkey_hex,
content => 'https://example.com/encrypted-photo.bin',
recipients => [$recipient_pubkey],
file_type => 'image/jpeg',
encryption_algorithm => 'aes-gcm',
decryption_key => $key,
decryption_nonce => $nonce,
x => $sha256_hex,
ox => $original_sha256,
size => '2048000',
dim => '1920x1080',
blurhash => 'LEHV6nWB2yk8',
thumb => 'https://example.com/thumb.bin',
fallback => ['https://backup.example.com/photo.bin'],
subject => 'Vacation photos',
);
wrap_for_recipients
my @wraps = Net::Nostr::DirectMessage->wrap_for_recipients(
rumor => $msg,
sender_key => $key,
);
Seals and gift-wraps the rumor individually for each recipient (from p tags) and for the sender. Returns a list of kind 1059 events.
Optional arguments: expiration (add expiration tags for disappearing messages), skip_sender (omit the sender's copy).
# With expiration (disappearing messages)
my @wraps = Net::Nostr::DirectMessage->wrap_for_recipients(
rumor => $msg,
sender_key => $key,
expiration => time() + 3600,
);
# Skip the sender's self-copy
my @wraps = Net::Nostr::DirectMessage->wrap_for_recipients(
rumor => $msg,
sender_key => $key,
skip_sender => 1,
);
receive
my $rumor = Net::Nostr::DirectMessage->receive(
event => $gift_wrap,
recipient_key => $key,
);
say $rumor->kind; # 14
say $rumor->content; # "secret message"
say $rumor->pubkey; # sender's pubkey
Unwraps a kind 1059 gift wrap and returns the inner rumor (unsigned event). The returned rumor preserves all tags including subject. Verifies that the seal's pubkey matches the rumor's pubkey (required by NIP-17). Croaks with "sender pubkey mismatch" if the seal was created by a different key than the rumor claims, which prevents impersonation attacks.
Trust boundary: This method decrypts and parses the gift wrap structure and verifies the seal/rumor pubkey consistency. It does not verify the gift wrap's Schnorr signature or event ID hash. If you need full cryptographic verification of the outer event, call $relay->_validate_event or verify the signature separately before calling receive.
create_relay_list
my $event = Net::Nostr::DirectMessage->create_relay_list(
pubkey => $pubkey_hex,
relays => ['wss://inbox.nostr.wine', 'wss://myrelay.nostr1.com'],
);
Creates a kind 10050 replaceable event listing preferred DM relays. Tags use the relay tag name (not r). relays must be a non-empty arrayref of ws:// or wss:// relay URIs. Clients SHOULD keep lists small (1-3 relays).
chat_members
my @members = Net::Nostr::DirectMessage->chat_members($msg);
# @members = ($alice_pubkey, $bob_pubkey)
Returns the list of pubkeys in the chat room defined by the event: the event's pubkey (the sender) followed by all p-tagged pubkeys. The same set of members (regardless of order) defines the same chat room.