Net-Nostr

 view release on metacpan or  search on metacpan

lib/Net/Nostr/LiveActivity.pm  view on Meta::CPAN

package Net::Nostr::LiveActivity;

use strictures 2;

use Carp qw(croak);
use Net::Nostr::Event;

use Class::Tiny qw(
    identifier
    title
    summary
    image
    streaming
    recording
    starts
    ends
    status
    current_participants
    total_participants
    hashtags
    participants
    relays
    pinned
    activity
    relay_hint
    reply_to
    room
    service
    endpoint
    space_ref
    hand
);

my %KINDS = (
    30311 => 'live_event',
    1311  => 'chat_message',
    30312 => 'meeting_space',
    30313 => 'meeting_room',
    10312 => 'room_presence',
);

sub new {
    my $class = shift;
    my %args = @_;
    $args{hashtags}     //= [];
    $args{participants} //= [];
    $args{relays}       //= [];
    $args{pinned}       //= [];
    my $self = bless \%args, $class;
    my %known; @known{Class::Tiny->get_all_attributes_for($class)} = ();
    my @unknown = grep { !exists $known{$_} } keys %$self;
    croak "unknown argument(s): " . join(', ', sort @unknown) if @unknown;
    return $self;
}

sub live_event {
    my ($class, %args) = @_;

    my $identifier = delete $args{identifier}
        // croak "live_event requires 'identifier'";
    my $title                = delete $args{title};
    my $summary              = delete $args{summary};
    my $image                = delete $args{image};
    my $streaming            = delete $args{streaming};
    my $recording            = delete $args{recording};
    my $starts               = delete $args{starts};
    my $ends                 = delete $args{ends};
    my $status               = delete $args{status};
    my $current_participants = delete $args{current_participants};
    my $total_participants   = delete $args{total_participants};
    my $hashtags             = delete $args{hashtags} // [];
    my $participants         = delete $args{participants} // [];
    my $relays               = delete $args{relays};
    my $pinned               = delete $args{pinned} // [];

    my @tags;
    push @tags, ['d', $identifier];
    push @tags, ['title', $title]         if defined $title;
    push @tags, ['summary', $summary]     if defined $summary;
    push @tags, ['image', $image]         if defined $image;
    push @tags, ['streaming', $streaming] if defined $streaming;
    push @tags, ['recording', $recording] if defined $recording;
    push @tags, ['starts', $starts]       if defined $starts;
    push @tags, ['ends', $ends]           if defined $ends;
    push @tags, ['status', $status]       if defined $status;
    push @tags, ['current_participants', $current_participants]
        if defined $current_participants;
    push @tags, ['total_participants', $total_participants]
        if defined $total_participants;
    push @tags, ['t', $_] for @$hashtags;
    push @tags, ['p', @$_] for @$participants;
    push @tags, ['relays', @$relays] if $relays;
    push @tags, ['pinned', $_] for @$pinned;

    return Net::Nostr::Event->new(
        %args,
        kind    => 30311,
        content => '',
        tags    => \@tags,
    );
}

sub chat_message {
    my ($class, %args) = @_;

    my $activity   = delete $args{activity}
        // croak "chat_message requires 'activity'";
    my $relay_hint = delete $args{relay_hint};
    my $reply_to   = delete $args{reply_to};
    my $content    = delete $args{content} // '';

    my @tags;
    if (defined $relay_hint) {
        push @tags, ['a', $activity, $relay_hint, 'root'];
    } else {
        push @tags, ['a', $activity];
    }
    push @tags, ['e', $reply_to] if defined $reply_to;

    return Net::Nostr::Event->new(
        %args,
        kind    => 1311,
        content => $content,
        tags    => \@tags,
    );
}

sub meeting_space {
    my ($class, %args) = @_;

    my $identifier = delete $args{identifier}
        // croak "meeting_space requires 'identifier'";
    my $room = delete $args{room}
        // croak "meeting_space requires 'room'";
    my $status = delete $args{status}
        // croak "meeting_space requires 'status'";
    my $service = delete $args{service}
        // croak "meeting_space requires 'service'";
    my $participants = delete $args{participants}
        // croak "meeting_space requires 'participants'";
    my $summary  = delete $args{summary};

lib/Net/Nostr/LiveActivity.pm  view on Meta::CPAN


    my @tags;
    push @tags, ['d', $identifier];
    push @tags, ['a', @$space_ref];
    push @tags, ['title', $title];
    push @tags, ['starts', $starts];
    push @tags, ['status', $status];
    push @tags, ['summary', $summary] if defined $summary;
    push @tags, ['image', $image]     if defined $image;
    push @tags, ['ends', $ends]       if defined $ends;
    push @tags, ['current_participants', $current_participants]
        if defined $current_participants;
    push @tags, ['total_participants', $total_participants]
        if defined $total_participants;
    push @tags, ['p', @$_] for @$participants;

    return Net::Nostr::Event->new(
        %args,
        kind    => 30313,
        content => '',
        tags    => \@tags,
    );
}

sub room_presence {
    my ($class, %args) = @_;

    my $activity   = delete $args{activity}
        // croak "room_presence requires 'activity'";
    my $relay_hint = delete $args{relay_hint};
    my $hand       = delete $args{hand};

    my @tags;
    push @tags, ['a', $activity, $relay_hint // '', 'root'];
    push @tags, ['hand', $hand] if defined $hand;

    return Net::Nostr::Event->new(
        %args,
        kind    => 10312,
        content => '',
        tags    => \@tags,
    );
}

sub from_event {
    my ($class, $event) = @_;
    my $kind = $event->kind;

    return undef unless exists $KINDS{$kind};

    my %attrs;
    my (@hashtags, @participants, @relays, @pinned);

    for my $tag (@{$event->tags}) {
        next unless @$tag >= 2;
        my $t = $tag->[0];
        if    ($t eq 'd')                    { $attrs{identifier} = $tag->[1] }
        elsif ($t eq 'title')                { $attrs{title} = $tag->[1] }
        elsif ($t eq 'summary')              { $attrs{summary} = $tag->[1] }
        elsif ($t eq 'image')                { $attrs{image} = $tag->[1] }
        elsif ($t eq 'streaming')            { $attrs{streaming} = $tag->[1] }
        elsif ($t eq 'recording')            { $attrs{recording} = $tag->[1] }
        elsif ($t eq 'starts')               { $attrs{starts} = $tag->[1] }
        elsif ($t eq 'ends')                 { $attrs{ends} = $tag->[1] }
        elsif ($t eq 'status')               { $attrs{status} = $tag->[1] }
        elsif ($t eq 'current_participants') { $attrs{current_participants} = $tag->[1] }
        elsif ($t eq 'total_participants')   { $attrs{total_participants} = $tag->[1] }
        elsif ($t eq 't')                    { push @hashtags, $tag->[1] }
        elsif ($t eq 'p')                    { push @participants, [@{$tag}[1 .. $#$tag]] }
        elsif ($t eq 'relays')               { @relays = @{$tag}[1 .. $#$tag] }
        elsif ($t eq 'pinned')               { push @pinned, $tag->[1] }
        elsif ($t eq 'a') {
            if ($kind == 30313) {
                $attrs{space_ref} = [@{$tag}[1 .. $#$tag]];
            } else {
                $attrs{activity}   = $tag->[1];
                $attrs{relay_hint} = $tag->[2] if @$tag > 2 && defined $tag->[2] && $tag->[2] ne '';
            }
        }
        elsif ($t eq 'e')        { $attrs{reply_to} = $tag->[1] }
        elsif ($t eq 'room')     { $attrs{room} = $tag->[1] }
        elsif ($t eq 'service')  { $attrs{service} = $tag->[1] }
        elsif ($t eq 'endpoint') { $attrs{endpoint} = $tag->[1] }
        elsif ($t eq 'hand')     { $attrs{hand} = $tag->[1] }
    }

    return $class->new(
        %attrs,
        hashtags     => \@hashtags,
        participants => \@participants,
        relays       => \@relays,
        pinned       => \@pinned,
    );
}

sub validate {
    my ($class, $event) = @_;
    my $kind = $event->kind;

    croak "live activity event MUST be kind 30311, 1311, 30312, 30313, or 10312"
        unless exists $KINDS{$kind};

    my %has;
    for my $tag (@{$event->tags}) {
        $has{$tag->[0]} = 1;
    }

    if ($kind == 30311) {
        croak "live event MUST have a 'd' tag" unless $has{d};
    }

    if ($kind == 1311) {
        croak "chat message MUST have an 'a' tag" unless $has{a};
    }

    if ($kind == 30312) {
        croak "meeting space MUST have a 'd' tag"       unless $has{d};
        croak "meeting space MUST have a 'room' tag"    unless $has{room};
        croak "meeting space MUST have a 'status' tag"  unless $has{status};
        croak "meeting space MUST have a 'service' tag" unless $has{service};
        croak "meeting space MUST have a 'p' tag"       unless $has{p};
    }

    if ($kind == 30313) {
        croak "meeting room MUST have a 'd' tag"      unless $has{d};
        croak "meeting room MUST have an 'a' tag"     unless $has{a};
        croak "meeting room MUST have a 'title' tag"  unless $has{title};
        croak "meeting room MUST have a 'starts' tag" unless $has{starts};
        croak "meeting room MUST have a 'status' tag" unless $has{status};
    }

    if ($kind == 10312) {
        croak "room presence MUST have an 'a' tag" unless $has{a};
    }

    return 1;
}

1;

__END__


=head1 NAME

Net::Nostr::LiveActivity - NIP-53 Live Activities

=head1 SYNOPSIS

    use Net::Nostr::LiveActivity;

    # Live streaming event (kind 30311)
    my $event = Net::Nostr::LiveActivity->live_event(
        pubkey     => $hex_pubkey,
        identifier => 'my-stream',
        title      => 'My Stream',
        status     => 'live',
    );

    # Live chat message (kind 1311)
    my $event = Net::Nostr::LiveActivity->chat_message(
        pubkey   => $hex_pubkey,
        activity => "30311:$author_pk:$d_id",
        content  => 'Hello!',
    );

    # Meeting space (kind 30312)
    my $event = Net::Nostr::LiveActivity->meeting_space(
        pubkey       => $hex_pubkey,
        identifier   => 'main-room',
        room         => 'Main Conference Hall',
        status       => 'open',
        service      => 'https://meet.example.com/room',
        participants => [[$host_pk, 'wss://relay.com/', 'Host']],
    );

    # Meeting room event (kind 30313)
    my $event = Net::Nostr::LiveActivity->meeting_room(
        pubkey     => $hex_pubkey,
        identifier => 'annual-meeting',
        space_ref  => ["30312:$space_pk:main-room", 'wss://relay.com'],
        title      => 'Annual Meeting',
        starts     => '1676262123',
        status     => 'planned',
    );

    # Room presence (kind 10312)
    my $event = Net::Nostr::LiveActivity->room_presence(
        pubkey   => $hex_pubkey,
        activity => "30312:$space_pk:main-room",
        hand     => '1',
    );

    # Parse any live activity event
    my $parsed = Net::Nostr::LiveActivity->from_event($event);

    # Validate
    Net::Nostr::LiveActivity->validate($event);

=head1 DESCRIPTION

Implements NIP-53 (Live Activities). Five event kinds are used:

=over 4

=item * B<Live Streaming Event> (kind 30311) - An addressable event
advertising a live stream. Contains tags for title, summary, image,
streaming URL, recording URL, start/end times, status, participant
counts, hashtags, participant roles, relay lists, and pinned chat
messages. Updated continuously as participants join and leave.

=item * B<Live Chat Message> (kind 1311) - A regular event for live
chat. MUST include an C<a> tag referencing the parent live activity.
MAY include an C<e> tag for replies.

=item * B<Meeting Space> (kind 30312) - An addressable event defining
a virtual interactive space. MUST have C<room>, C<status>, C<service>,
and at least one C<p> tag with a Host role.

=item * B<Meeting Room Event> (kind 30313) - An addressable event
representing a scheduled or ongoing meeting. MUST reference a parent
space via C<a> tag and have C<title>, C<starts>, and C<status>.

=item * B<Room Presence> (kind 10312) - A replaceable event signaling
a user's presence in a room. Contains an C<a> tag with the room
reference and an optional C<hand> tag for raised hand.

=back

=head1 CONSTRUCTOR

=head2 new

    my $la = Net::Nostr::LiveActivity->new(
        identifier => 'my-stream',
        status     => 'live',
    );

Creates a new C<Net::Nostr::LiveActivity> object. Croaks on unknown
arguments. Array fields (C<hashtags>, C<participants>, C<relays>,
C<pinned>) default to C<[]>.

=head1 CLASS METHODS

=head2 live_event

    my $event = Net::Nostr::LiveActivity->live_event(
        pubkey               => $hex_pubkey,       # required
        identifier           => $id,               # required (d tag)
        title                => $title,            # optional
        summary              => $summary,          # optional
        image                => $url,              # optional
        streaming            => $url,              # optional
        recording            => $url,              # optional
        starts               => $timestamp,        # optional
        ends                 => $timestamp,        # optional
        status               => $status,           # optional (planned/live/ended)
        current_participants => $count,            # optional
        total_participants   => $count,            # optional
        hashtags             => [$tag, ...],       # optional (t tags)
        participants         => [[$pk, $relay, $role, $proof], ...], # optional (p tags)
        relays               => [$url, ...],       # optional (relays tag)
        pinned               => [$event_id, ...],  # optional (pinned tags)
    );

Creates a kind 30311 live streaming L<Net::Nostr::Event>. Each C<p>
tag SHOULD have a displayable role name (e.g. C<Host>, C<Speaker>,
C<Participant>). The relay and proof fields in participant entries are
optional. Content defaults to C<''>.

=head2 chat_message

    my $event = Net::Nostr::LiveActivity->chat_message(
        pubkey     => $hex_pubkey,                 # required
        activity   => "30311:$pk:$d_id",           # required (a tag)
        relay_hint => $relay_url,                  # optional
        reply_to   => $event_id,                   # optional (e tag)
        content    => $message,                    # optional, defaults to ''
    );

Creates a kind 1311 live chat L<Net::Nostr::Event>. The C<a> tag
references the parent live activity. When a relay hint is provided,
the C<a> tag includes a C<root> marker.

=head2 meeting_space

    my $event = Net::Nostr::LiveActivity->meeting_space(
        pubkey       => $hex_pubkey,               # required
        identifier   => $id,                       # required (d tag)
        room         => $name,                     # required (room tag)
        status       => $status,                   # required (open/private/closed)
        service      => $url,                      # required (service tag)
        participants => [[$pk, $relay, $role, $proof], ...], # required (p tags; $proof optional)
        summary      => $summary,                  # optional
        image        => $url,                      # optional
        endpoint     => $url,                      # optional
        hashtags     => [$tag, ...],               # optional (t tags)
        relays       => [$url, ...],               # optional (relays tag)
    );

Creates a kind 30312 meeting space L<Net::Nostr::Event>. MUST have
at least one provider with a Host role. Status MUST be C<open>,
C<private>, or C<closed>. Content defaults to C<''>.

=head2 meeting_room

    my $event = Net::Nostr::LiveActivity->meeting_room(
        pubkey               => $hex_pubkey,       # required
        identifier           => $id,               # required (d tag)
        space_ref            => [$coord, $relay],   # required (a tag)
        title                => $title,            # required
        starts               => $timestamp,        # required
        status               => $status,           # required (planned/live/ended)
        summary              => $summary,          # optional
        image                => $url,              # optional
        ends                 => $timestamp,        # optional
        current_participants => $count,            # optional
        total_participants   => $count,            # optional
        participants         => [[$pk, $relay, $role], ...], # optional
    );

Creates a kind 30313 meeting room L<Net::Nostr::Event>. The C<a> tag
references the parent meeting space. Content defaults to C<''>.

=head2 room_presence

lib/Net/Nostr/LiveActivity.pm  view on Meta::CPAN

        activity   => $room_a_tag,                 # required (a tag)
        relay_hint => $relay_url,                  # optional
        hand       => '1',                         # optional (hand raised)
    );

Creates a kind 10312 room presence L<Net::Nostr::Event>. This is a
replaceable event, so presence can only be indicated in one room at a
time. The C<a> tag always includes a C<root> marker. When no relay
hint is provided, the relay field defaults to C<''>.

=head2 from_event

    my $la = Net::Nostr::LiveActivity->from_event($event);

Parses a kind 30311, 1311, 30312, 30313, or 10312 event into a
C<Net::Nostr::LiveActivity> object. Returns C<undef> for unrecognized
kinds.

=head2 validate

    Net::Nostr::LiveActivity->validate($event);

Validates a NIP-53 event. Croaks if:

=over

=item * Kind is not 30311, 1311, 30312, 30313, or 10312

=item * Kind 30311 missing C<d> tag

=item * Kind 1311 missing C<a> tag

=item * Kind 30312 missing C<d>, C<room>, C<status>, C<service>, or C<p> tag

=item * Kind 30313 missing C<d>, C<a>, C<title>, C<starts>, or C<status> tag

=item * Kind 10312 missing C<a> tag

=back

Returns 1 on success.

=head1 ACCESSORS

=head2 identifier

The C<d> tag value (kinds 30311, 30312, 30313).

=head2 title

Event title (kinds 30311, 30313).

=head2 summary

Event description.

=head2 image

Preview image URL.

=head2 streaming

Live stream URL (kind 30311).

=head2 recording

Recording URL (kind 30311).

=head2 starts

Start timestamp in seconds.

=head2 ends

End timestamp in seconds.

=head2 status

Event status. For live events: C<planned>, C<live>, or C<ended>. For
meeting spaces: C<open>, C<private>, or C<closed>.

=head2 current_participants

Current participant count string.

=head2 total_participants

Total participant count string.

=head2 hashtags

Arrayref of hashtag strings from C<t> tags. Defaults to C<[]>.

=head2 participants

Arrayref of arrayrefs from C<p> tags. Each contains
C<[$pubkey, $relay, $role]> and optionally a proof field.
Defaults to C<[]>.

=head2 relays

Arrayref of relay URL strings from C<relays> tag. Defaults to C<[]>.

=head2 pinned

Arrayref of pinned event IDs from C<pinned> tags. Defaults to C<[]>.

=head2 activity

The C<a> tag coordinate (kinds 1311, 10312).

=head2 relay_hint

Relay hint from C<a> tag.

=head2 reply_to

Parent event ID from C<e> tag (kind 1311).

=head2 room



( run in 0.649 second using v1.01-cache-2.11-cpan-140bd7fdf52 )