From c685639f50f76f15e9e32b48bc2afd27c0cdea28 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 16 Aug 2026 14:17:40 -0300 Subject: [PATCH] group: replace legacy nip29 identifier with NIP-AD web address. --- README.md | 8 ++--- group.go | 105 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 8ab52b1..c8299f1 100644 --- a/README.md +++ b/README.md @@ -516,10 +516,10 @@ gitnostr.com... ok. ### interact with a NIP-29 group ```shell -~> nak group info "'" -~> nak group admin "'" -~> nak group chat "'" -~> nak group chat send "'" "" +~> nak group info wisp.chat/rootstock +~> nak group admins wisp.chat/rootstock +~> nak group chat wisp.chat/rootstock +~> nak group chat send wisp.chat/rootstock "hello" ``` ### figure out what is a given kind diff --git a/group.go b/group.go index 771c8e5..a2776e3 100644 --- a/group.go +++ b/group.go @@ -16,6 +16,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip11" "fiatjaf.com/nostr/nip29" + "fiatjaf.com/nostr/nipad" "github.com/fatih/color" "github.com/urfave/cli/v3" ) @@ -23,9 +24,9 @@ import ( var group = &cli.Command{ Name: "group", Usage: "nip29 group-related operations: info, chat, forum, members, admins, roles", - Description: `manage and interact with Nostr communities (NIP-29). Use "nak group '" where host.tld is the relay and identifier is the group identifier.`, + Description: `manage and interact with Nostr communities (NIP-29). Use "nak group " where is a NIP-AD web address like "wisp.chat/rootstock" (the domain hosts the group relay, the path is the group identifier).`, DisableSliceFlagSeparator: true, - ArgsUsage: " ' [flags]", + ArgsUsage: " [flags]", Flags: combineFlags([][]cli.Flag{}), Before: func(ctx context.Context, c *cli.Command) (context.Context, error) { return ctx, nil @@ -35,9 +36,9 @@ var group = &cli.Command{ Name: "info", Usage: "show group information", Description: "displays basic group metadata.", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -47,7 +48,6 @@ var group = &cli.Command{ return err } - stdout("address:", color.HiBlueString(strings.SplitN(nostr.NormalizeURL(relay), "/", 3)[2]+"'"+identifier)) stdout("name:", color.HiBlueString(group.Name)) stdout("picture:", color.HiBlueString(group.Picture)) stdout("about:", color.HiBlueString(group.About)) @@ -96,9 +96,9 @@ var group = &cli.Command{ Name: "members", Usage: "list and manage group members", Description: "view group membership information.", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -150,9 +150,9 @@ var group = &cli.Command{ Name: "admins", Usage: "manage group administrators", Description: "view and manage group admin permissions.", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -204,9 +204,9 @@ var group = &cli.Command{ Name: "roles", Usage: "manage group roles and permissions", Description: "configure custom roles and permissions within the group.", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -235,9 +235,9 @@ var group = &cli.Command{ Name: "chat", Usage: "send and read group chat messages", Description: "interact with group chat functionality.", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -299,9 +299,9 @@ var group = &cli.Command{ { Name: "send", Usage: "sends a message to the chat", - ArgsUsage: "' ", + ArgsUsage: " ", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -344,9 +344,9 @@ 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: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -376,9 +376,9 @@ var group = &cli.Command{ Name: "forum", Usage: "forum topic operations", Description: "when called directly, lists forum topics; with an id prefix, displays that topic with threaded comments.", - ArgsUsage: "' [id-prefix]", + ArgsUsage: " [id-prefix]", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -429,7 +429,7 @@ var group = &cli.Command{ Name: "create", Usage: "edit and send a forum topic event (kind 11)", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -456,7 +456,7 @@ var group = &cli.Command{ content, err := editWithDefaultEditor( "nak-group-forum/NOTES_EDITMSG", strings.TrimSpace(fmt.Sprintf(`# creating as '%s' ('%s') -# creating forum topic in group '%s' ('%s''%s') +# creating forum topic in group '%s' ('%s' at %s) # the first line will be used as the topic title topic title here @@ -464,7 +464,7 @@ topic title here write your forum post # lines starting with '#' are ignored -`, selfName, selfNpub, groupName, relay, identifier)), +`, selfName, selfNpub, groupName, identifier, relay)), true, ) if err != nil { @@ -508,9 +508,9 @@ write your forum post { Name: "comment", Usage: "comment on a forum topic with a NIP-22 comment event", - ArgsUsage: "' ", + ArgsUsage: " ", Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -556,7 +556,7 @@ write your forum post pm := sys.FetchProfileMetadata(ctx, topic.PubKey) headerLines := []string{ fmt.Sprintf("commenting as '%s' ('%s')", selfName, selfNpub), - fmt.Sprintf("commenting on forum topic '%s' '%s' by '%s' ('%s') in group '%s' ('%s''%s')", topic.ID.Hex()[:6], subject, pm.ShortName(), pm.NpubShort(), groupName, relay, identifier), + fmt.Sprintf("commenting on forum topic '%s' '%s' by '%s' ('%s') in group '%s' ('%s' at %s)", topic.ID.Hex()[:6], subject, pm.ShortName(), pm.NpubShort(), groupName, identifier, relay), } comments, err := fetchThreadComments(ctx, []string{relay}, topic.ID, nostr.TagMap{"h": []string{identifier}}) @@ -625,7 +625,7 @@ write your forum post { Name: "put-user", Usage: "add a user to the group with optional roles", - ArgsUsage: "'", + ArgsUsage: "", Flags: []cli.Flag{ &PubKeyFlag{ Name: "pubkey", @@ -648,7 +648,7 @@ write your forum post { Name: "remove-user", Usage: "remove a user from the group", - ArgsUsage: "' ", + ArgsUsage: " ", Flags: []cli.Flag{ &PubKeyFlag{ Name: "pubkey", @@ -666,7 +666,7 @@ write your forum post { Name: "create-group", Usage: "creates a new group", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { return publishModerationEvent(ctx, c, 9007, func(evt *nostr.Event, args []string) error { return nil @@ -676,7 +676,7 @@ write your forum post { Name: "edit-metadata", Usage: "edits the group metadata", - ArgsUsage: "'", + ArgsUsage: "", Flags: []cli.Flag{ &cli.StringFlag{ Name: "name", @@ -740,7 +740,7 @@ write your forum post }, }, Action: func(ctx context.Context, c *cli.Command) error { - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -857,7 +857,7 @@ write your forum post { Name: "delete-event", Usage: "delete an event from the group", - ArgsUsage: "'", + ArgsUsage: "", Flags: []cli.Flag{ &IDFlag{ Name: "event", @@ -875,7 +875,7 @@ write your forum post { Name: "delete-group", Usage: "deletes the group", - ArgsUsage: "'", + ArgsUsage: "", Action: func(ctx context.Context, c *cli.Command) error { return publishModerationEvent(ctx, c, 9008, func(evt *nostr.Event, args []string) error { return nil @@ -885,7 +885,7 @@ write your forum post { Name: "create-invite", Usage: "creates an invite code", - ArgsUsage: "'", + ArgsUsage: "", Flags: []cli.Flag{ &cli.StringFlag{ Name: "code", @@ -908,7 +908,7 @@ func publishModerationEvent(ctx context.Context, c *cli.Command, kind nostr.Kind return fmt.Errorf("requires group identifier") } - relay, identifier, err := parseGroupIdentifier(c) + relay, identifier, err := parseGroupIdentifier(ctx, c) if err != nil { return err } @@ -960,18 +960,39 @@ func cond(b bool, ifYes string, ifNo string) string { return ifNo } -func parseGroupIdentifier(c *cli.Command) (relay string, identifier string, err error) { - groupArg := c.Args().First() - if !strings.Contains(groupArg, "'") { - return "", "", fmt.Errorf("invalid group identifier format, expected '") +func parseGroupIdentifier(ctx context.Context, c *cli.Command) (relay string, identifier string, err error) { + groupArg := strings.TrimSpace(c.Args().First()) + + // deprecated, to remove in 2028: keep supporting the old "'" syntax. + // don't mention it in docs. + if strings.Contains(groupArg, "'") { + parts := strings.SplitN(groupArg, "'", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("invalid group identifier format, expected ") + } + + return strings.TrimSuffix(parts[0], "/"), parts[1], nil } - parts := strings.SplitN(groupArg, "'", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf("invalid group identifier format, expected '") + if !isWebAddress(groupArg) { + return "", "", fmt.Errorf("invalid group web address '%s'", groupArg) } - return strings.TrimSuffix(parts[0], "/"), parts[1], nil + webPath, err := nipad.Resolve(ctx, groupArg) + if err != nil { + return "", "", fmt.Errorf("failed to resolve group web address '%s': %w", groupArg, err) + } + + if len(webPath.Relays) == 0 { + return "", "", fmt.Errorf("no relays listed for group web address '%s'", groupArg) + } + + identifiers, ok := webPath.Filter.Tags["d"] + if !ok || len(identifiers) == 0 || identifiers[0] == "" { + return "", "", fmt.Errorf("no group identifier found for web address '%s'", groupArg) + } + + return nostr.NormalizeURL(webPath.Relays[0]), identifiers[0], nil } func fetchGroupMetadata(ctx context.Context, relay string, identifier string) (nip29.Group, error) {