NAME

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

SYNOPSIS

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

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

# Parse a group identifier
my $parsed = Net::Nostr::Group->parse_id("groups.nostr.com'pizza");
# { host => 'groups.nostr.com', group_id => 'pizza' }

# Format a group identifier
my $id = Net::Nostr::Group->format_id(
    host     => 'groups.nostr.com',
    group_id => 'pizza',
);
# "groups.nostr.com'pizza"

# Validate a group_id
Net::Nostr::Group->validate_group_id('my-group_1');  # 1
Net::Nostr::Group->validate_group_id('INVALID');      # 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,
);

# 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' },
    ],
);

# 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 $gid          = Net::Nostr::Group->group_id_from_event($event);

DESCRIPTION

Implements NIP-29 relay-based groups. Groups are identified by a string restricted to a-z0-9-_ and belong to a specific relay. Group state is managed through moderation events (kinds 9000-9009) and user events (kinds 9021-9022). Group metadata is published as addressable events (kinds 39000-39003) 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);
$groups->add('group', "groups.nostr.com'pizza", '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("groups.nostr.com'pizza");
# { host => 'groups.nostr.com', group_id => 'pizza' }

my $parsed = Net::Nostr::Group->parse_id("groups.nostr.com");
# { host => 'groups.nostr.com', group_id => '_' }

Parses a group identifier string in <host>'<group-id> format. If only a host is given (no '), infers _ as the group id per NIP-29. Croaks if the group id contains invalid characters.

format_id

my $id = Net::Nostr::Group->format_id(
    host     => 'groups.nostr.com',
    group_id => 'pizza',
);
# "groups.nostr.com'pizza"

Formats a group identifier string from host and group_id components.

validate_group_id

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

Returns true if the group id matches the allowed character set a-z0-9-_.

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
);

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.

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.

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) are set to 1 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}";
}

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)