mirror of
https://github.com/fiatjaf/nak.git
synced 2026-06-04 17:51:15 +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
|
go 1.25
|
||||||
|
|
||||||
require (
|
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/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
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 h1:GRZNSxHI2EWdjSKVuzaT+c0aifLDtS16SzkeJaHyJfY=
|
||||||
fiatjaf.com/lib v0.3.6/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
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-20260306014620-163e59e1f19c h1:MybCUlYp81e6zdmn74cL0cRHtuQfIukjFWDcohGbah4=
|
||||||
fiatjaf.com/nostr v0.0.0-20260304030104-1d14e6bebe83/go.mod h1:iRKV8eYKzePA30MdbaYBpAv8pYQ6to8rDr3W+R2hJzM=
|
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 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
|
|||||||
200
group.go
200
group.go
@@ -2,12 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
stdjson "encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip11"
|
||||||
"fiatjaf.com/nostr/nip29"
|
"fiatjaf.com/nostr/nip29"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -33,15 +38,9 @@ var group = &cli.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
group := nip29.Group{}
|
group, err := fetchGroupMetadata(ctx, relay, identifier)
|
||||||
for ie := range sys.Pool.FetchMany(ctx, []string{relay}, nostr.Filter{
|
if err != nil {
|
||||||
Kinds: []nostr.Kind{nostr.KindSimpleGroupMetadata},
|
return err
|
||||||
Tags: nostr.TagMap{"d": []string{identifier}},
|
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip29"}) {
|
|
||||||
if err := group.MergeInMetadataEvent(&ie.Event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout("address:", color.HiBlueString(strings.SplitN(nostr.NormalizeURL(relay), "/", 3)[2]+"'"+identifier))
|
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"),
|
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
|
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",
|
Name: "forum",
|
||||||
Usage: "read group forum posts",
|
Usage: "read group forum posts",
|
||||||
@@ -436,8 +474,31 @@ var group = &cli.Command{
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "public",
|
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 {
|
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 {
|
return createModerationEvent(ctx, c, 9002, func(evt *nostr.Event, args []string) error {
|
||||||
if name := c.String("name"); name != "" {
|
if name := c.String("name"); name != "" {
|
||||||
evt.Tags = append(evt.Tags, nostr.Tag{"name", name})
|
evt.Tags = append(evt.Tags, nostr.Tag{"name", name})
|
||||||
@@ -468,6 +529,16 @@ var group = &cli.Command{
|
|||||||
} else if c.Bool("public") {
|
} else if c.Bool("public") {
|
||||||
evt.Tags = append(evt.Tags, nostr.Tag{"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
|
return nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -583,3 +654,114 @@ func parseGroupIdentifier(c *cli.Command) (relay string, identifier string, err
|
|||||||
|
|
||||||
return strings.TrimSuffix(parts[0], "/"), parts[1], nil
|
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