Files
didactyl/plans/nip17_messaging.md

6.9 KiB

NIP-17 Messaging Implementation Plan

Background

Didactyl now supports both NIP-04 and NIP-17 DM flows (including receive path for kind 1059 and protocol-aware auto-reply routing). Remaining work in this plan tracks spec-hardening and bootstrap/discovery details.

The nostr_core_lib already has full NIP-17/NIP-59 support including:

  • nostr_nip17_create_chat_event() — create kind 14 rumor
  • nostr_nip17_send_dm() — seal + gift wrap (creates wraps for both recipient AND sender)
  • nostr_nip17_receive_dm() — unwrap gift wrap, unseal rumor, return kind 14
  • nostr_nip17_extract_dm_relays() — parse kind 10050 relay lists

Config-Driven Protocol Selection

New Config Field

Add a dm_protocol field to the top-level config:

{
  "dm_protocol": "nip04",
  ...
}

Valid values:

  • "nip04" — NIP-04 only (current behavior, default for backward compatibility)
  • "nip17" — NIP-17 only (subscribe to kind 1059, send via gift wrap)
  • "both" — Subscribe to both kind 4 and kind 1059; reply using whichever protocol the message arrived on

Config Struct Change

In config.h, add to didactyl_config_t:

typedef enum {
    DM_PROTOCOL_NIP04 = 0,
    DM_PROTOCOL_NIP17 = 1,
    DM_PROTOCOL_BOTH  = 2
} dm_protocol_t;

Add dm_protocol_t dm_protocol; to the config struct.

Implementation Steps

1. Add dm_protocol config parsing

Files: config.h, config.c

  • Add dm_protocol_t enum and field to didactyl_config_t
  • Parse "dm_protocol" string from JSON in config_load()
  • Default to DM_PROTOCOL_NIP04 if not specified

2. Add kind 1059 subscription

File: nostr_handler.cnostr_handler_subscribe_dms()

  • When dm_protocol is NIP17 or BOTH, add kind: 1059 to the subscription filter
  • When dm_protocol is NIP04 or BOTH, keep kind: 4 in the filter
  • The subscription filter becomes kinds: [4, 1059] for BOTH mode

3. Add NIP-17 receive handling in on_event()

File: nostr_handler.con_event()

Currently on_event() rejects anything that is not kind 4. Update to:

  • If kind == 1059: unwrap gift wrap via nostr_nip17_receive_dm(), extract sender pubkey from the kind 14 rumor, extract message content, determine sender tier, fire g_dm_callback
  • If kind == 4: existing NIP-04 decrypt path (unchanged)
  • Track which protocol was used per sender pubkey for reply routing

4. Track protocol per sender for reply routing

File: nostr_handler.c

Add a small cache that maps sender_pubkey_hex -> last_protocol_used:

typedef struct {
    char pubkey_hex[65];
    dm_protocol_t protocol;
} sender_protocol_entry_t;

#define SENDER_PROTOCOL_CACHE_SIZE 64
static sender_protocol_entry_t g_sender_protocol_cache[SENDER_PROTOCOL_CACHE_SIZE];

When a DM arrives via kind 4, record NIP04 for that sender. When via kind 1059, record NIP17.

5. Add protocol-aware send function

File: nostr_handler.c

Add a new function that routes based on config + sender history:

int nostr_handler_send_dm_auto(const char* recipient_pubkey_hex, const char* message);

Logic:

  • If dm_protocol == NIP04: always use nostr_handler_send_dm()
  • If dm_protocol == NIP17: always use nostr_handler_send_dm_nip17()
  • If dm_protocol == BOTH: check sender protocol cache; use matching protocol, default to NIP-04

Expose in nostr_handler.h.

6. Update agent.c to use auto-routing

File: agent.c

Replace all nostr_handler_send_dm() calls with nostr_handler_send_dm_auto(). This is a mechanical find-and-replace across ~15 call sites.

7. Add kind 10050 startup event support

Status: Implemented

Files:

  • src/default_events.h (default startup kinds now include 10050)
  • src/setup_wizard.c (builds kind 10050 tags from current configured relays)
  • genesis.jsonc.example (includes kind 10050 startup event)
  • genesis.lt.didactyl.jsonc (includes kind 10050 startup event)

Implementation detail: kind 10050 reuses the same relay set as kind 10002/current config relays, with relay tags as required by NIP-17.

8. Update startup DM to use auto-routing

Status: Implemented

main.c uses nostr_handler_send_dm_auto() for startup DM flow.

9. Add NIP-17 seal/rumor pubkey verification

Status: Implemented (in core lib)

File: nostr_core_lib/nostr_core/nip017.c

nostr_nip17_receive_dm() now verifies that seal pubkey matches rumor pubkey and rejects mismatches to prevent sender impersonation.

10. Recipient kind-10050 relay targeting for sends

Status: ⏸ Deferred

Current NIP-17 send path publishes to connected configured relays. Querying and targeting recipient-advertised kind 10050 relays is kept as a future enhancement while debugging current interoperability.

Architecture Diagram

flowchart TD
    subgraph Config
        CFG[dm_protocol setting]
        CFG -->|nip04| NIP04_MODE[NIP-04 only]
        CFG -->|nip17| NIP17_MODE[NIP-17 only]
        CFG -->|both| BOTH_MODE[Both protocols]
    end

    subgraph Subscription
        SUB[nostr_handler_subscribe_dms]
        NIP04_MODE --> SUB_K4[Subscribe kind 4]
        NIP17_MODE --> SUB_K1059[Subscribe kind 1059]
        BOTH_MODE --> SUB_BOTH[Subscribe kind 4 + 1059]
    end

    subgraph Receive Path
        EVT[on_event callback]
        EVT -->|kind 4| DEC4[NIP-04 decrypt]
        EVT -->|kind 1059| DEC17[NIP-17 unwrap + unseal]
        DEC4 --> CACHE[Record sender protocol]
        DEC17 --> CACHE
        CACHE --> CB[Fire dm_callback]
    end

    subgraph Send Path
        SEND[nostr_handler_send_dm_auto]
        SEND -->|config=nip04| S4[nostr_handler_send_dm - kind 4]
        SEND -->|config=nip17| S17[nostr_handler_send_dm_nip17 - kind 1059]
        SEND -->|config=both| LOOKUP[Check sender protocol cache]
        LOOKUP -->|sender used nip04| S4
        LOOKUP -->|sender used nip17| S17
        LOOKUP -->|unknown| S4
    end

File Change Summary

File Changes
config.h Add dm_protocol_t enum and field
config.c Parse dm_protocol from JSON
nostr_handler.h Add nostr_handler_send_dm_auto() declaration
nostr_handler.c Add kind 1059 subscription, NIP-17 receive in on_event(), sender protocol cache, nostr_handler_send_dm_auto()
agent.c Replace nostr_handler_send_dm() with nostr_handler_send_dm_auto()
main.c Replace startup DM send with nostr_handler_send_dm_auto()
config.json.example Add dm_protocol field and kind 10050 startup event example

Testing Strategy

  1. NIP-04 mode (default): Verify existing behavior is unchanged
  2. NIP-17 mode: Send a NIP-17 DM from a client like Amethyst/0xchat, verify Didactyl receives and replies via NIP-17
  3. Both mode: Send NIP-04 DM, verify NIP-04 reply; send NIP-17 DM, verify NIP-17 reply
  4. Kind 10050: Verify the relay list event is published on startup and discoverable by NIP-17 clients