NAME

Net::Nostr::Group - NIP-29 relay-based groups

SYNOPSIS

use Net::Nostr::Group;
use Net::Nostr::Key;

my $key = Net::Nostr::Key->new;

# Format and parse a group identifier (naddr for kind 39000 metadata)
my $group_naddr = Net::Nostr::Group->format_id(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    relay    => 'wss://groups.nostr.com',
);

my $parsed = Net::Nostr::Group->parse_id($group_naddr);
# { group_id => 'pizza', pubkey => $relay_pubkey, kind => 39000, ... }

# Validate a group_id
Net::Nostr::Group->validate_group_id('my-group_1');  # 1
Net::Nostr::Group->validate_group_id('Pizza Fans');   # 1
Net::Nostr::Group->validate_group_id('');             # 0

# Join a group (kind 9021)
my $join = Net::Nostr::Group->join_request(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    reason   => 'I love pizza',
    code     => 'invite-abc',  # optional invite code
);
$key->sign_event($join);
$client->publish($join);

# Leave a group (kind 9022)
my $leave = Net::Nostr::Group->leave_request(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
);

# Add a user to a group (kind 9000, admin)
my $put = Net::Nostr::Group->put_user(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    target   => $user_pubkey,
    roles    => ['moderator'],
    reason   => 'promoted',
);

# Remove a user (kind 9001, admin)
my $rm = Net::Nostr::Group->remove_user(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    target   => $user_pubkey,
    reason   => 'spamming',
);

# Edit group metadata (kind 9002, admin)
my $edit = Net::Nostr::Group->edit_metadata(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    name     => 'Pizza Lovers',
    about    => 'We love pizza',
    private  => 1,
    closed   => 1,
);

# Delete an event from the group (kind 9005, admin)
my $del = Net::Nostr::Group->delete_event(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    event_id => $spam_event_id,
);

# Create a group (kind 9007)
my $create = Net::Nostr::Group->create_group(
    pubkey   => $key->pubkey_hex,
    group_id => 'new-group',
);

# Delete a group (kind 9008)
my $delete = Net::Nostr::Group->delete_group(
    pubkey   => $key->pubkey_hex,
    group_id => 'old-group',
);

# Create an invite code (kind 9009)
my $invite = Net::Nostr::Group->create_invite(
    pubkey   => $key->pubkey_hex,
    group_id => 'pizza',
    code     => 'secret-code-123',
);

# Generate group metadata (kind 39000, relay-generated)
my $meta = Net::Nostr::Group->metadata(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    name     => 'Pizza Lovers',
    picture  => 'https://pizza.com/pizza.png',
    about    => 'a group for pizza fans',
    private  => 1,
    closed   => 1,
    livekit  => 1,
    supported_kinds => [9, 11],
);

# Generate admin list (kind 39001, relay-generated)
my $admin_event = Net::Nostr::Group->admins(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    members  => [
        { pubkey => $admin_pk, roles => ['admin'] },
        { pubkey => $mod_pk,   roles => ['moderator'] },
    ],
);

# Generate member list (kind 39002, relay-generated)
my $member_event = Net::Nostr::Group->members(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    members  => [$pk1, $pk2, $pk3],
);

# Generate roles list (kind 39003, relay-generated)
my $role_event = Net::Nostr::Group->roles(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    roles    => [
        { name => 'admin', description => 'full control' },
        { name => 'moderator', description => 'can delete messages' },
    ],
);

# Generate LiveKit participant list (kind 39004, relay-generated)
my $participant_event = Net::Nostr::Group->participants(
    pubkey       => $relay_pubkey,
    group_id     => 'pizza',
    participants => [$pk1, $pk2],
);

# Parse received events
my $meta_info    = Net::Nostr::Group->metadata_from_event($event);
my $admin_info   = Net::Nostr::Group->admins_from_event($event);
my $member_info  = Net::Nostr::Group->members_from_event($event);
my $role_info    = Net::Nostr::Group->roles_from_event($event);
my $part_info    = Net::Nostr::Group->participants_from_event($event);
my $gid          = Net::Nostr::Group->group_id_from_event($event);

DESCRIPTION

Implements NIP-29 relay-based groups. Groups have arbitrary non-empty group_id strings in event h and d tags. Public group identifiers are naddr references to the group's kind 39000 metadata event. Group state is managed through moderation events (kinds 9000-9009) and user events (kinds 9021-9022). Relay-generated group state is published as addressable events (kinds 39000-39004) signed by the relay.

All user and moderation events MUST include an h tag with the group id. Group metadata events use a d tag instead.

To store a user's list of groups, use a kind 10009 Net::Nostr::List with group and r tags per NIP-51:

use Net::Nostr::List;

my $groups = Net::Nostr::List->new(kind => 10009);
my $group_id = Net::Nostr::Group->format_id(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    relay    => 'wss://groups.nostr.com',
);
$groups->add('group', $group_id, 'wss://groups.nostr.com', 'Pizza Lovers');
$groups->add('r', 'wss://groups.nostr.com');
my $event = $groups->to_event(pubkey => $key->pubkey_hex);

CLASS METHODS

parse_id

my $parsed = Net::Nostr::Group->parse_id($naddr);
# { group_id => 'pizza', pubkey => $relay_pubkey, kind => 39000, ... }

Parses a group identifier naddr that references a kind 39000 metadata event. Returns the raw group_id, relay pubkey, kind, relay hints, and the first relay hint as relay. Croaks for legacy host-based identifiers, invalid bech32 data, or naddr values that do not reference kind 39000.

format_id

my $id = Net::Nostr::Group->format_id(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    relay    => 'wss://groups.nostr.com',
);
# naddr1...

Formats a public group identifier as an naddr referencing the group's kind 39000 metadata event. pubkey is the relay's self pubkey. relay or relays may be supplied as relay hints. relays must be an arrayref.

validate_group_id

Net::Nostr::Group->validate_group_id('my-group');  # 1
Net::Nostr::Group->validate_group_id('Pizza Fans'); # 1
Net::Nostr::Group->validate_group_id('');           # 0

Returns true if the group id is a defined non-empty scalar. Current NIP-29 does not restrict group ids to a specific character set.

put_user

my $event = Net::Nostr::Group->put_user(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    target   => $user_pubkey,
    roles    => ['admin', 'moderator'],  # optional
    reason   => 'promoted',              # optional
    previous => ['abcd1234'],            # optional timeline refs
);

Creates a kind 9000 moderation event to add a user to the group or update their roles. The p tag contains the target pubkey followed by any role strings.

remove_user

my $event = Net::Nostr::Group->remove_user(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    target   => $user_pubkey,
    reason   => 'spamming',  # optional
);

Creates a kind 9001 moderation event to remove a user from the group.

All user and moderation event builders (put_user, remove_user, edit_metadata, delete_event, create_group, delete_group, create_invite, join_request, leave_request) accept an optional previous parameter for timeline references. See put_user for an example.

edit_metadata

my $event = Net::Nostr::Group->edit_metadata(
    pubkey       => $hex_pubkey,
    group_id     => 'pizza',
    name         => 'Pizza Lovers',       # optional
    picture      => 'https://pic.url',    # optional
    about        => 'description',        # optional
    private      => 1,                    # optional flag
    restricted   => 1,                    # optional flag
    hidden       => 1,                    # optional flag
    closed       => 1,                    # optional flag
    unrestricted => 1,                    # optional flag
    open         => 1,                    # optional flag
    visible      => 1,                    # optional flag
    public       => 1,                    # optional flag
);

Creates a kind 9002 moderation event to update group metadata. Metadata fields become tags. Boolean flags become single-element tags when true.

delete_event

my $event = Net::Nostr::Group->delete_event(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    event_id => $event_id_hex,
    reason   => 'spam content',  # optional
);

Creates a kind 9005 moderation event to delete an event from the group.

create_group

my $event = Net::Nostr::Group->create_group(
    pubkey   => $hex_pubkey,
    group_id => 'new-group',
);

Creates a kind 9007 event requesting the relay to create a new group.

delete_group

my $event = Net::Nostr::Group->delete_group(
    pubkey   => $hex_pubkey,
    group_id => 'old-group',
    reason   => 'inactive',  # optional
);

Creates a kind 9008 event requesting the relay to delete a group.

create_invite

my $event = Net::Nostr::Group->create_invite(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    code     => 'secret-code-123',
);

Creates a kind 9009 event with an invite code for the group.

join_request

my $event = Net::Nostr::Group->join_request(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    reason   => 'I love pizza',   # optional
    code     => 'invite-abc',     # optional invite code
);

Creates a kind 9021 join request event. The optional code tag can be used with invite codes created by create_invite.

leave_request

my $event = Net::Nostr::Group->leave_request(
    pubkey   => $hex_pubkey,
    group_id => 'pizza',
    reason   => 'moving on',  # optional
);

Creates a kind 9022 leave request event.

metadata

my $event = Net::Nostr::Group->metadata(
    pubkey     => $relay_pubkey,
    group_id   => 'pizza',
    name       => 'Pizza Lovers',
    picture    => 'https://pizza.com/pizza.png',
    about      => 'a group for pizza fans',
    private    => 1,    # only members can read
    restricted => 1,    # only members can write
    hidden     => 1,    # hide metadata from non-members
    closed     => 1,    # ignore join requests
    livekit    => 1,    # group supports LiveKit A/V rooms
    supported_kinds => [9, 11], # text event kinds supported by the group
);

Creates a kind 39000 addressable event describing group metadata. This event should be signed by the relay's master key. Uses a d tag (not h) with the group id. supported_kinds, when supplied, must be an arrayref.

admins

my $event = Net::Nostr::Group->admins(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    content  => 'admin list',  # optional
    members  => [
        { pubkey => $pk, roles => ['admin'] },
    ],
);

Creates a kind 39001 addressable event listing group admins with roles.

members

my $event = Net::Nostr::Group->members(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    content  => 'member list',  # optional
    members  => [$pk1, $pk2],
);

Creates a kind 39002 addressable event listing group members.

roles

my $event = Net::Nostr::Group->roles(
    pubkey   => $relay_pubkey,
    group_id => 'pizza',
    content  => 'role definitions',  # optional
    roles    => [
        { name => 'admin', description => 'full control' },
        { name => 'moderator' },
    ],
);

Creates a kind 39003 addressable event listing supported roles.

participants

my $event = Net::Nostr::Group->participants(
    pubkey       => $relay_pubkey,
    group_id     => 'pizza',
    content      => 'participant list',  # optional
    participants => [$pk1, $pk2],
);

Creates a kind 39004 addressable event listing current LiveKit participants. participants must be an arrayref. This event should be signed by the relay's master key.

metadata_from_event

my $meta = Net::Nostr::Group->metadata_from_event($event);
# { group_id => '...', name => '...', picture => '...',
#   about => '...', private => 1, closed => 1 }

Parses a kind 39000 event. Returns a hashref with group metadata. Boolean flags (private, restricted, hidden, closed, livekit) are set to 1 when present. supported_kinds is returned as an arrayref when present. Croaks if the event is not kind 39000.

admins_from_event

my $result = Net::Nostr::Group->admins_from_event($event);
# { group_id => '...', admins => [{ pubkey => '...', roles => [...] }] }

Parses a kind 39001 event. Returns a hashref with group_id and an arrayref of admin entries. Croaks if the event is not kind 39001.

for my $admin (@{$result->{admins}}) {
    say "$admin->{pubkey}: " . join(', ', @{$admin->{roles}});
}

members_from_event

my $result = Net::Nostr::Group->members_from_event($event);
# { group_id => '...', members => [$pk1, $pk2] }

Parses a kind 39002 event. Returns a hashref with group_id and an arrayref of member pubkeys. Croaks if the event is not kind 39002.

roles_from_event

my $result = Net::Nostr::Group->roles_from_event($event);
# { group_id => '...', roles => [{ name => '...', description => '...' }] }

Parses a kind 39003 event. Returns a hashref with group_id and an arrayref of role definitions. Croaks if the event is not kind 39003.

for my $role (@{$result->{roles}}) {
    say "$role->{name}: $role->{description}";
}

participants_from_event

my $result = Net::Nostr::Group->participants_from_event($event);
# { group_id => '...', participants => [$pk1, $pk2] }

Parses a kind 39004 event. Returns a hashref with group_id and an arrayref of participant pubkeys. Croaks if the event is not kind 39004.

group_id_from_event

my $gid = Net::Nostr::Group->group_id_from_event($event);

Extracts the group id from an event's h tag (user/moderation events) or d tag (metadata events). If both tags are present, the h tag takes priority. Returns undef if neither is found.

my $gid = Net::Nostr::Group->group_id_from_event($join_event);
say "Group: $gid";

SEE ALSO

NIP-29, Net::Nostr, Net::Nostr::Event, Net::Nostr::List (kind 10009 group storage)