NAME

Net::Nostr::GroupDiscovery - NIP-29 migration and fork discovery

SYNOPSIS

use Net::Nostr::GroupDiscovery;

# $discovery_client is a dedicated, already connected Net::Nostr::Client.
# Restore $relay_pubkey and $admin_pubkey from the trusted local cache.
my @candidates;
my $lookup_number = 0;
my $watch = Net::Nostr::GroupDiscovery->new(
    group_id => 'pizza', relay => 'wss://old.example',
    relay_pubkey => $relay_pubkey, admins => [$admin_pubkey],
    lookup => sub {
        my ($filter, $complete) = @_;
        my $sub_id = 'group-discovery-' . ++$lookup_number;
        my @events;
        $discovery_client->on(event => sub {
            push @events, $_[1] if $_[0] eq $sub_id;
        });
        $discovery_client->on(eose => sub {
            return unless $_[0] eq $sub_id;
            $discovery_client->close($sub_id);
            $complete->(\@events, undef);
        });
        $discovery_client->subscribe($sub_id, $filter);
    },
    on_candidate => sub { push @candidates, $_[0] },
);
$watch->primary_unreachable;

DESCRIPTION

Coordinates lookup of kind 10009 group lists using cached administrator keys and trusted friends. Reports alternate relay hints without switching relays or publishing a list. Identical group IDs on different relays are separate instances and may represent intentional forks.

The application supplies a lookup transport callback, typically querying its already connected discovery relays with Net::Nostr::Client. It receives a Net::Nostr::Filter and a completion callback. Gather matching stored events through EOSE from those relays, then invoke completion once as $complete->(\@events, undef) or $complete->(undef, $error). Use relays independent of the group's primary relay so lookup works offline. Returned events are authenticated again before they influence discovery. The SYNOPSIS is a minimal single-author, single-relay lookup. A production transport must account for relay caps, pagination hints, authentication, CLOSED replies, and disconnects before treating a batch as complete. Each lookup needs a fresh subscription ID so late messages cannot complete a later request. The watcher's timeout permits retries but does not close transport subscriptions; the application must clean those up.

Call primary_unreachable when the primary connection fails or becomes unreachable: NIP-29 requires a lookup in that situation. Call start to enable recommended periodic checks. Persist the admins snapshot in the application's local storage, and refresh it with cache_admins while the primary relay is available. This helper does not monitor network reachability or persist the cache on disk itself.

Show on_candidate results to the user and offer to fetch the group's metadata and history from the candidate relay. A hint is not proof that a relay hosts the desired community: inspect its own signed metadata and administrators. Only after the user chooses a migration should the application build and publish migration_event, switch its primary connection, and construct a new watcher with that relay's key and admin cache. Live history replication remains an application responsibility.

METHODS

new

Strict constructor accepting named arguments as either a flat list or a single hash reference. Required arguments are group_id, relay (strict ws/wss URL), relay_pubkey (64 lowercase hex), admins (array of public keys), lookup and on_candidate (callbacks). Optional trusted_friends defaults to an empty array. At least one admin or friend is required. Arrays are validated, deduplicated, sorted, and copied. interval defaults to 300 positive seconds; timeout defaults to 10. Optional on_error receives lookup errors; otherwise they produce warnings. Invalid values and unknown options croak. Construction starts no network work.

relay

Returns the configured primary relay URL.

admins

Returns a defensive array copy of cached administrator public keys.

cache_admins

Authenticates a kind 39001 event from the configured relay key and requires this group's ID. Validates every administrator key, exactly one group tag, unique administrator entries, and non-empty role labels. Discovery completions use the current cache, so a removed admin cannot redirect a lookup already in progress. Replaces the cache only with a newer event (lower event ID wins timestamp ties). Returns one on replacement or zero for an older or duplicate event. Malformed, forged, or wrong-relay metadata croaks. Persist the returned admins snapshot locally.

check

Starts a lookup using the cached admins and trusted friends. Returns one if started, or zero if a lookup is already in progress or no authors remain. Accepts only valid signed kind 10009 events from those authors. Validates group IDs and relay hints, retains the newest event per author, and calls on_candidate for newly observed alternate relays. The callback receives group_id, relay, and an advertised_by array of public keys in a hashref. Unknown authors are ignored. Malformed lookup batches are rejected without changing discovery state. Repeated completion calls are ignored. Timeout or lookup exceptions reach on_error; later checks may retry. Group IDs are arbitrary non-empty strings. Valid kind-39000 naddr references are also resolved, but strings merely starting with naddr1 remain valid raw IDs. An exact match to the configured raw group ID takes precedence.

primary_unreachable

Immediately calls check, even when periodic checking has not been started. An already pending lookup also satisfies this request without overlapping it.

start

Checks immediately and installs an AnyEvent timer using interval. Repeated calls do not create additional timers. The application's event loop must run.

stop

Cancels periodic checks. An already pending lookup may still complete.

migration_event

Strict builder taking relay, event (the user's signed kind 10009 list), and key (its private author key). Validates and authenticates the list, updates references to this group on the configured primary relay, and retains other groups and same-ID forks. Updates both public and encrypted private items, adds the new relay hint, and returns a newly signed event with a newer timestamp. Existing relay hints are retained because other groups may use them. A migrated naddr reference becomes a raw group ID, avoiding reuse of the old relay's signing key. If no existing entry matches, adds a public entry. Does not publish, mutate the input list, or change the watcher's relay.

SEE ALSO

NIP-29, NIP-51, Net::Nostr::Client, Net::Nostr::Group, Net::Nostr::List