mirror of
https://github.com/fiatjaf/nak.git
synced 2026-04-10 15:36:54 +02:00
group: editing groups with livekit metadata and getting jwt from the server (can be used with meet.livekit.io for now).
This commit is contained in:
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83
|
||||
fiatjaf.com/nostr v0.0.0-20260306014620-163e59e1f19c
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,7 +1,7 @@
|
||||
fiatjaf.com/lib v0.3.6 h1:GRZNSxHI2EWdjSKVuzaT+c0aifLDtS16SzkeJaHyJfY=
|
||||
fiatjaf.com/lib v0.3.6/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83 h1:6pX4gFT6N7AoxeLcV3DzJAJCvAR1iPR2tcfInPV+7Ak=
|
||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83/go.mod h1:iRKV8eYKzePA30MdbaYBpAv8pYQ6to8rDr3W+R2hJzM=
|
||||
fiatjaf.com/nostr v0.0.0-20260306014620-163e59e1f19c h1:MybCUlYp81e6zdmn74cL0cRHtuQfIukjFWDcohGbah4=
|
||||
fiatjaf.com/nostr v0.0.0-20260306014620-163e59e1f19c/go.mod h1:iRKV8eYKzePA30MdbaYBpAv8pYQ6to8rDr3W+R2hJzM=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||
|
||||
200
group.go
200
group.go
@@ -2,12 +2,17 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
stdjson "encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip11"
|
||||
"fiatjaf.com/nostr/nip29"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -33,15 +38,9 @@ var group = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
group := nip29.Group{}
|
||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
|
||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
||||
if err := group.MergeInMetadataEvent(&ie.Event); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
group, err := fetchGroupMetadata(ctx, relay, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout("address:", color.HiBlueString(strings.SplitN(nostr.NormalizeURL(relay), "/", 3)[2]+"'"+identifier))
|
||||
@@ -68,6 +67,16 @@ var group = &cli.Command{
|
||||
", "+
|
||||
cond(group.Private, "group content is not accessible to non-members", "group content is public"),
|
||||
)
|
||||
stdout("livekit:",
|
||||
color.HiBlueString("%s", cond(group.Livekit, "yes", "no"))+
|
||||
", "+
|
||||
cond(group.Livekit, "group supports live audio/video with livekit", "group has no advertised live audio/video support"),
|
||||
)
|
||||
stdout("no-text:",
|
||||
color.HiBlueString("%s", cond(group.NoText, "yes", "no"))+
|
||||
", "+
|
||||
cond(group.NoText, "group is intended for live audio/video only", "text messages are expected to be supported"),
|
||||
)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -327,6 +336,35 @@ var group = &cli.Command{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "talk",
|
||||
Usage: "get livekit connection details",
|
||||
Description: "requests a livekit jwt for this group and prints the livekit server url.",
|
||||
ArgsUsage: "<relay>'<identifier>",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
relay, identifier, err := parseGroupIdentifier(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := fetchGroupMetadata(ctx, relay, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !group.Livekit {
|
||||
return fmt.Errorf("group doesn't advertise livekit support")
|
||||
}
|
||||
|
||||
serverURL, jwt, err := requestLivekitJWT(ctx, c, relay, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout("livekit:", color.HiBlueString(serverURL))
|
||||
stdout("jwt:", color.HiBlueString(jwt))
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "forum",
|
||||
Usage: "read group forum posts",
|
||||
@@ -436,8 +474,31 @@ var group = &cli.Command{
|
||||
&cli.BoolFlag{
|
||||
Name: "public",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "livekit",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-livekit",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "no-text",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "text",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
if c.Bool("livekit") || c.Bool("no-livekit") || c.Bool("no-text") || c.Bool("text") {
|
||||
relay, _, err := parseGroupIdentifier(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkRelayLivekitMetadataSupport(ctx, relay); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return createModerationEvent(ctx, c, 9002, func(evt *nostr.Event, args []string) error {
|
||||
if name := c.String("name"); name != "" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"name", name})
|
||||
@@ -468,6 +529,16 @@ var group = &cli.Command{
|
||||
} else if c.Bool("public") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"public"})
|
||||
}
|
||||
if c.Bool("livekit") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"livekit"})
|
||||
} else if c.Bool("no-livekit") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"no-livekit"})
|
||||
}
|
||||
if c.Bool("no-text") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"no-text"})
|
||||
} else if c.Bool("text") {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"text"})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -583,3 +654,114 @@ func parseGroupIdentifier(c *cli.Command) (relay string, identifier string, err
|
||||
|
||||
return strings.TrimSuffix(parts[0], "/"), parts[1], nil
|
||||
}
|
||||
|
||||
func fetchGroupMetadata(ctx context.Context, relay string, identifier string) (nip29.Group, error) {
|
||||
group := nip29.Group{}
|
||||
|
||||
filter := nostr.Filter{
|
||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
|
||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
||||
}
|
||||
|
||||
if info, err := nip11.Fetch(ctx, relay); err == nil {
|
||||
if info.Self != nil {
|
||||
filter.Authors = append(filter.Authors, *info.Self)
|
||||
} else if info.PubKey != nil {
|
||||
filter.Authors = append(filter.Authors, *info.PubKey)
|
||||
}
|
||||
}
|
||||
|
||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, filter, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
||||
if err := group.MergeInMetadataEvent(&ie.Event); err != nil {
|
||||
return group, err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func checkRelayLivekitMetadataSupport(ctx context.Context, relay string) error {
|
||||
url := "http" + nostr.NormalizeURL(relay)[2:] + "/.well-known/nip29/livekit"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create livekit support request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check relay livekit support: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("relay doesn't advertise livekit support at %s (expected 204, got %d)", url, resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func requestLivekitJWT(ctx context.Context, c *cli.Command, relay string, identifier string) (serverURL string, jwt string, err error) {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
url := "http" + nostr.NormalizeURL(relay)[2:] + "/.well-known/nip29/livekit/" + identifier
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create livekit token request: %w", err)
|
||||
}
|
||||
|
||||
tokenEvent := nostr.Event{
|
||||
Kind: 27235,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"u", url},
|
||||
{"method", "GET"},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||
return "", "", fmt.Errorf("failed to sign livekit auth token: %w", err)
|
||||
}
|
||||
|
||||
evtj, _ := stdjson.Marshal(tokenEvent)
|
||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("livekit token request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed reading livekit token response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
msg := strings.TrimSpace(string(body))
|
||||
if msg != "" {
|
||||
return "", "", fmt.Errorf("livekit token request failed with status %d: %s", resp.StatusCode, msg)
|
||||
}
|
||||
return "", "", fmt.Errorf("livekit token request failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
response := struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
ParticipantToken string `json:"participant_token"`
|
||||
}{}
|
||||
if err := stdjson.Unmarshal(body, &response); err != nil {
|
||||
return "", "", fmt.Errorf("invalid livekit token response: %w", err)
|
||||
}
|
||||
|
||||
serverURL = response.ServerURL
|
||||
jwt = response.ParticipantToken
|
||||
|
||||
if serverURL == "" || jwt == "" {
|
||||
return "", "", fmt.Errorf("livekit token response missing url or jwt")
|
||||
}
|
||||
|
||||
return serverURL, jwt, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user