Compare commits

...

7 Commits

Author SHA1 Message Date
fiatjaf
3ee6320312 bunker: ignore duplicates caused by switch_relays. 2026-01-21 23:17:00 -03:00
fiatjaf
91474d65eb bunker: set default relays so switch_relays works. 2026-01-21 22:19:00 -03:00
fiatjaf
7d782737c4 git status: fix commit printing. 2026-01-21 14:36:25 -03:00
fiatjaf
9160c68cb5 bunker: using unix sockets. 2026-01-21 14:31:12 -03:00
fiatjaf
bf19f38996 nak bunker connect 'nostrconnect://...' working. 2026-01-21 12:44:40 -03:00
fiatjaf
4e2c136e45 nostrconnect:// beginnings. 2026-01-20 17:19:30 -03:00
fiatjaf
8cef1ed0ea group: publishing moderation actions. 2026-01-20 12:52:00 -03:00
6 changed files with 470 additions and 64 deletions

299
bunker.go
View File

@@ -4,13 +4,14 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"fiatjaf.com/nostr" "fiatjaf.com/nostr"
@@ -73,13 +74,7 @@ var bunker = &cli.Command{
}, },
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
// read config from file // read config from file
config := struct { config := BunkerConfig{}
AuthorizedKeys []nostr.PubKey `json:"authorized-keys"`
Secret plainOrEncryptedKey `json:"sec"`
Relays []string `json:"relays"`
}{
AuthorizedKeys: make([]nostr.PubKey, 0, 3),
}
baseRelaysUrls := appendUnique(c.Args().Slice(), c.StringSlice("relay")...) baseRelaysUrls := appendUnique(c.Args().Slice(), c.StringSlice("relay")...)
for i, url := range baseRelaysUrls { for i, url := range baseRelaysUrls {
baseRelaysUrls[i] = nostr.NormalizeURL(url) baseRelaysUrls[i] = nostr.NormalizeURL(url)
@@ -142,6 +137,15 @@ var bunker = &cli.Command{
if err := json.Unmarshal(b, &config); err != nil { if err := json.Unmarshal(b, &config); err != nil {
return err return err
} }
// convert from deprecated field
if len(config.AuthorizedKeys) > 0 {
config.Clients = make([]BunkerConfigClient, len(config.AuthorizedKeys))
for i := range config.AuthorizedKeys {
config.Clients[i] = BunkerConfigClient{PubKey: config.AuthorizedKeys[i]}
}
config.AuthorizedKeys = nil
persist()
}
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return err return err
} }
@@ -150,7 +154,11 @@ var bunker = &cli.Command{
config.Relays[i] = nostr.NormalizeURL(url) config.Relays[i] = nostr.NormalizeURL(url)
} }
config.Relays = appendUnique(config.Relays, baseRelaysUrls...) config.Relays = appendUnique(config.Relays, baseRelaysUrls...)
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, baseAuthorizedKeys...) for _, bak := range baseAuthorizedKeys {
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool { return c.PubKey == bak }) {
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: bak})
}
}
if config.Secret.Plain == nil && config.Secret.Encrypted == nil { if config.Secret.Plain == nil && config.Secret.Encrypted == nil {
// we don't have any secret key stored, so just use whatever was given via flags // we don't have any secret key stored, so just use whatever was given via flags
@@ -167,7 +175,9 @@ var bunker = &cli.Command{
} else { } else {
config.Secret = baseSecret config.Secret = baseSecret
config.Relays = baseRelaysUrls config.Relays = baseRelaysUrls
config.AuthorizedKeys = baseAuthorizedKeys for _, bak := range baseAuthorizedKeys {
config.Clients = append(config.Clients, BunkerConfigClient{PubKey: bak})
}
} }
// if we got here without any keys set (no flags, first time using a profile), use the default // if we got here without any keys set (no flags, first time using a profile), use the default
@@ -205,8 +215,17 @@ var bunker = &cli.Command{
// try to connect to the relays here // try to connect to the relays here
qs := url.Values{} qs := url.Values{}
relayURLs := make([]string, 0, len(config.Relays)) allRelays := make([]string, len(config.Relays), len(config.Relays)+5)
relays := connectToAllRelays(ctx, c, config.Relays, nil, nostr.PoolOptions{}) copy(allRelays, config.Relays)
for _, c := range config.Clients {
for _, url := range c.CustomRelays {
if !slices.ContainsFunc(allRelays, func(u string) bool { return u == url }) {
allRelays = append(allRelays, url)
}
}
}
relayURLs := make([]string, 0, len(allRelays))
relays := connectToAllRelays(ctx, c, allRelays, nil, nostr.PoolOptions{})
if len(relays) == 0 { if len(relays) == 0 {
log("failed to connect to any of the given relays.\n") log("failed to connect to any of the given relays.\n")
os.Exit(3) os.Exit(3)
@@ -236,10 +255,22 @@ var bunker = &cli.Command{
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode()) bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
authorizedKeysStr := "" authorizedKeysStr := ""
if len(config.AuthorizedKeys) != 0 { if len(config.Clients) != 0 {
authorizedKeysStr = "\n authorized keys:" authorizedKeysStr = "\n authorized clients:"
for _, pubkey := range config.AuthorizedKeys { for _, c := range config.Clients {
authorizedKeysStr += "\n - " + colors.italic(pubkey.Hex()) authorizedKeysStr += "\n - " + colors.italic(c.PubKey.Hex())
name := ""
if c.Name != "" {
name = c.Name
if c.URL != "" {
name += " " + colors.underline(c.URL)
}
} else if c.URL != "" {
name = colors.underline(c.URL)
}
if name != "" {
authorizedKeysStr += " (" + name + ")"
}
} }
} }
@@ -249,8 +280,8 @@ var bunker = &cli.Command{
} }
preauthorizedFlags := "" preauthorizedFlags := ""
for _, k := range config.AuthorizedKeys { for _, c := range config.Clients {
preauthorizedFlags += " -k " + k.Hex() preauthorizedFlags += " -k " + c.PubKey.Hex()
} }
for _, s := range authorizedSecrets { for _, s := range authorizedSecrets {
preauthorizedFlags += " -s " + s preauthorizedFlags += " -s " + s
@@ -314,28 +345,85 @@ var bunker = &cli.Command{
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}}, Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(), Since: nostr.Now(),
LimitZero: true, LimitZero: true,
}, nostr.SubscriptionOptions{ }, nostr.SubscriptionOptions{Label: "nak-bunker"})
Label: "nak-bunker",
})
signer := nip46.NewStaticKeySigner(sec) signer := nip46.NewStaticKeySigner(sec)
handlerWg := sync.WaitGroup{} signer.DefaultRelays = config.Relays
printLock := sync.Mutex{}
// unix socket nostrconnect:// handling
go func() {
for uri := range onSocketConnect(ctx, c) {
clientPublicKey, err := nostr.PubKeyFromHex(uri.Host)
if err != nil {
continue
}
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
relays := uri.Query()["relay"]
// pre-authorize this client since the user has explicitly added it
if !slices.ContainsFunc(config.Clients, func(c BunkerConfigClient) bool {
return c.PubKey == clientPublicKey
}) {
config.Clients = append(config.Clients, BunkerConfigClient{
PubKey: clientPublicKey,
Name: uri.Query().Get("name"),
URL: uri.Query().Get("url"),
Icon: uri.Query().Get("icon"),
CustomRelays: relays,
})
}
if persist != nil {
persist()
}
resp, eventResponse, err := signer.HandleNostrConnectURI(ctx, uri)
if err != nil {
log("* failed to handle: %s\n", err)
continue
}
go func() {
for event := range sys.Pool.SubscribeMany(ctx, relays, nostr.Filter{
Kinds: []nostr.Kind{nostr.KindNostrConnect},
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
Since: nostr.Now(),
LimitZero: true,
}, nostr.SubscriptionOptions{Label: "nak-bunker"}) {
events <- event
}
}()
time.Sleep(time.Millisecond * 25)
jresp, _ := json.MarshalIndent(resp, "", " ")
log("~ responding with %s\n", string(jresp))
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
if res.Error == nil {
log("* sent through %s\n", res.Relay.URL)
} else {
log("* failed to send through %s: %s\n", res.RelayURL, res.Error)
}
}
}
}()
// just a gimmick // just a gimmick
var cancelPreviousBunkerInfoPrint context.CancelFunc var cancelPreviousBunkerInfoPrint context.CancelFunc
_, cancel := context.WithCancel(ctx) _, cancel := context.WithCancel(ctx)
cancelPreviousBunkerInfoPrint = cancel cancelPreviousBunkerInfoPrint = cancel
// asking user for authorization
signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool { signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
if slices.Contains(config.AuthorizedKeys, from) || slices.Contains(authorizedSecrets, secret) { if slices.ContainsFunc(config.Clients, func(b BunkerConfigClient) bool { return b.PubKey == from }) {
return true
}
if slices.Contains(authorizedSecrets, secret) {
return true return true
} }
if secret == newSecret { if secret == newSecret {
// store this key // store this key
config.AuthorizedKeys = appendUnique(config.AuthorizedKeys, from) config.Clients = append(config.Clients, BunkerConfigClient{PubKey: from})
// discard this and generate a new secret // discard this and generate a new secret
newSecret = randString(12) newSecret = randString(12)
// print bunker info again after this // print bunker info again after this
@@ -358,34 +446,39 @@ var bunker = &cli.Command{
cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks cancelPreviousBunkerInfoPrint() // this prevents us from printing a million bunker info blocks
// handle the NIP-46 request event // handle the NIP-46 request event
from := ie.Event.PubKey
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event) req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
if err != nil { if err != nil {
log("< failed to handle request from %s: %s\n", ie.Event.PubKey, err.Error()) if errors.Is(err, nip46.AlreadyHandled) {
continue
}
log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
continue continue
} }
jreq, _ := json.MarshalIndent(req, "", " ") jreq, _ := json.MarshalIndent(req, "", " ")
log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(ie.Event.PubKey.Hex()), string(jreq)) log("- got request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(from.Hex()), string(jreq))
jresp, _ := json.MarshalIndent(resp, "", " ") jresp, _ := json.MarshalIndent(resp, "", " ")
log("~ responding with %s\n", string(jresp)) log("~ responding with %s\n", string(jresp))
handlerWg.Add(len(relayURLs)) // use custom relays if they are defined for this client
for _, relayURL := range relayURLs { // (normally if the initial connection came from a nostrconnect:// URL)
go func(relayURL string) { relays := relayURLs
defer handlerWg.Done() for _, c := range config.Clients {
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil { if c.PubKey == from && len(c.CustomRelays) > 0 {
err := relay.Publish(ctx, eventResponse) relays = c.CustomRelays
printLock.Lock() break
if err == nil { }
log("* sent response through %s\n", relay.URL) }
for res := range sys.Pool.PublishMany(ctx, relays, eventResponse) {
if res.Error == nil {
log("* sent response through %s\n", res.Relay.URL)
} else { } else {
log("* failed to send response: %s\n", err) log("* failed to send response through %s: %s\n", res.RelayURL, res.Error)
} }
printLock.Unlock()
} }
}(relayURL)
}
handlerWg.Wait()
// just after handling one request we trigger this // just after handling one request we trigger this
go func() { go func() {
@@ -410,22 +503,42 @@ var bunker = &cli.Command{
Name: "connect", Name: "connect",
Usage: "use the client-initiated NostrConnect flow of NIP46", Usage: "use the client-initiated NostrConnect flow of NIP46",
ArgsUsage: "<nostrconnect-uri>", ArgsUsage: "<nostrconnect-uri>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "profile",
Usage: "profile name of the bunker to connect to",
},
},
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
if c.Args().Len() != 1 { if c.Args().Len() != 1 {
return fmt.Errorf("must be called with a nostrconnect://... uri") return fmt.Errorf("must be called with a nostrconnect://... uri")
} }
uri, err := url.Parse(c.Args().First()) if err := sendToSocket(c, c.Args().First()); err != nil {
if err != nil || uri.Scheme != "nostrconnect" { return fmt.Errorf("failed to connect to running bunker: %w", err)
return fmt.Errorf("invalid uri")
} }
// TODO return nil
},
},
},
}
return fmt.Errorf("this is not implemented yet") type BunkerConfig struct {
}, Clients []BunkerConfigClient `json:"clients"`
}, Secret plainOrEncryptedKey `json:"sec"`
}, Relays []string `json:"relays"`
// deprecated
AuthorizedKeys []nostr.PubKey `json:"authorized-keys,omitempty"`
}
type BunkerConfigClient struct {
PubKey nostr.PubKey `json:"pubkey"`
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
Icon string `json:"icon,omitempty"`
CustomRelays []string `json:"custom_relays,omitempty"`
} }
type plainOrEncryptedKey struct { type plainOrEncryptedKey struct {
@@ -495,3 +608,89 @@ func (a plainOrEncryptedKey) equals(b plainOrEncryptedKey) bool {
return true return true
} }
func getSocketPath(c *cli.Command) string {
profile := "default"
if c.IsSet("profile") {
profile = c.String("profile")
}
return filepath.Join(c.String("config-path"), "bunkerconn", profile)
}
func onSocketConnect(ctx context.Context, c *cli.Command) chan *url.URL {
res := make(chan *url.URL)
socketPath := getSocketPath(c)
// ensure directory exists
if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil {
log(color.RedString("failed to create socket directory: %w\n", err))
return res
}
// delete existing socket file if it exists
if _, err := os.Stat(socketPath); err == nil {
if err := os.Remove(socketPath); err != nil {
log(color.RedString("failed to remove existing socket file: %w\n", err))
return res
}
}
listener, err := net.Listen("unix", socketPath)
if err != nil {
log(color.RedString("failed to listen on unix socket %s: %w\n", socketPath, err))
return res
}
go func() {
defer listener.Close()
defer os.Remove(socketPath) // cleanup socket file on exit
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-ctx.Done():
return
default:
continue
}
}
go func(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 4096)
for {
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buf)
if err != nil {
break
}
uri, err := url.Parse(string(buf[:n]))
if err == nil && uri.Scheme == "nostrconnect" {
res <- uri
}
}
}(conn)
}
}()
return res
}
func sendToSocket(c *cli.Command, value string) error {
socketPath := getSocketPath(c)
conn, err := net.DialTimeout("unix", socketPath, 5*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to bunker unix socket at %s: %w", socketPath, err)
}
defer conn.Close()
_, err = conn.Write([]byte(value))
if err != nil {
return fmt.Errorf("failed to send uri to bunker: %w", err)
}
return nil
}

2
git.go
View File

@@ -850,7 +850,7 @@ aside from those, there is also:
if commit == stateHEAD { if commit == stateHEAD {
row[2] = color.GreenString("repository synced with state") row[2] = color.GreenString("repository synced with state")
} else { } else {
row[2] = color.YellowString("mismatched HEAD state=%s, pushed=%s", state.HEAD, commit) row[2] = color.YellowString("mismatched HEAD state=%s, pushed=%s", stateHEAD[0:5], commit[0:5])
} }
} }
} }

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
go 1.25 go 1.25
require ( require (
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4 fiatjaf.com/nostr v0.0.0-20260122014616-241959d1e3f4
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
View File

@@ -1,7 +1,7 @@
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q= fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8= fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4 h1:/6AVjHIbbgyuiilcUuoFPMXGNXqialKGQM7uskF0b/0= fiatjaf.com/nostr v0.0.0-20260122014616-241959d1e3f4 h1:1KAEp9ktrnm7pB/o2QrdRT3dJyXYwei8N9RRRppiMFY=
fiatjaf.com/nostr v0.0.0-20260119010708-31af06f4c7c4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU= fiatjaf.com/nostr v0.0.0-20260122014616-241959d1e3f4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
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=

205
group.go
View File

@@ -295,7 +295,7 @@ var group = &cli.Command{
{ {
Name: "send", Name: "send",
Usage: "sends a message to the chat", Usage: "sends a message to the chat",
ArgsUsage: "<message>", ArgsUsage: "<relay>'<identifier> <message>",
Action: func(ctx context.Context, c *cli.Command) error { Action: func(ctx context.Context, c *cli.Command) error {
relay, identifier, err := parseGroupIdentifier(c) relay, identifier, err := parseGroupIdentifier(c)
if err != nil { if err != nil {
@@ -358,7 +358,210 @@ var group = &cli.Command{
return nil return nil
}, },
}, },
{
Name: "put-user",
Usage: "add a user to the group with optional roles",
ArgsUsage: "<relay>'<identifier>",
Flags: []cli.Flag{
&PubKeyFlag{
Name: "pubkey",
Required: true,
}, },
&cli.StringSliceFlag{
Name: "role",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
return createModerationEvent(ctx, c, 9000, func(evt *nostr.Event, args []string) error {
pubkey := getPubKey(c, "pubkey")
tag := nostr.Tag{"p", pubkey.Hex()}
tag = append(tag, c.StringSlice("role")...)
evt.Tags = append(evt.Tags, tag)
return nil
})
},
},
{
Name: "remove-user",
Usage: "remove a user from the group",
ArgsUsage: "<relay>'<identifier> <pubkey>",
Flags: []cli.Flag{
&PubKeyFlag{
Name: "pubkey",
Required: true,
},
},
Action: func(ctx context.Context, c *cli.Command) error {
return createModerationEvent(ctx, c, 9001, func(evt *nostr.Event, args []string) error {
pubkey := getPubKey(c, "pubkey")
evt.Tags = append(evt.Tags, nostr.Tag{"p", pubkey.Hex()})
return nil
})
},
},
{
Name: "edit-metadata",
Usage: "edits the group metadata",
ArgsUsage: "<relay>'<identifier>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
},
&cli.StringFlag{
Name: "about",
},
&cli.StringFlag{
Name: "picture",
},
&cli.BoolFlag{
Name: "restricted",
},
&cli.BoolFlag{
Name: "unrestricted",
},
&cli.BoolFlag{
Name: "closed",
},
&cli.BoolFlag{
Name: "open",
},
&cli.BoolFlag{
Name: "hidden",
},
&cli.BoolFlag{
Name: "visible",
},
&cli.BoolFlag{
Name: "private",
},
&cli.BoolFlag{
Name: "public",
},
},
Action: func(ctx context.Context, c *cli.Command) error {
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})
}
if picture := c.String("picture"); picture != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"picture", picture})
}
if about := c.String("about"); about != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"about", about})
}
if c.Bool("restricted") {
evt.Tags = append(evt.Tags, nostr.Tag{"restricted"})
} else if c.Bool("unrestricted") {
evt.Tags = append(evt.Tags, nostr.Tag{"unrestricted"})
}
if c.Bool("closed") {
evt.Tags = append(evt.Tags, nostr.Tag{"closed"})
} else if c.Bool("open") {
evt.Tags = append(evt.Tags, nostr.Tag{"open"})
}
if c.Bool("hidden") {
evt.Tags = append(evt.Tags, nostr.Tag{"hidden"})
} else if c.Bool("visible") {
evt.Tags = append(evt.Tags, nostr.Tag{"visible"})
}
if c.Bool("private") {
evt.Tags = append(evt.Tags, nostr.Tag{"private"})
} else if c.Bool("public") {
evt.Tags = append(evt.Tags, nostr.Tag{"public"})
}
return nil
})
},
},
{
Name: "delete-event",
Usage: "delete an event from the group",
ArgsUsage: "<relay>'<identifier>",
Flags: []cli.Flag{
&IDFlag{
Name: "event",
Required: true,
},
},
Action: func(ctx context.Context, c *cli.Command) error {
return createModerationEvent(ctx, c, 9005, func(evt *nostr.Event, args []string) error {
id := getID(c, "event")
evt.Tags = append(evt.Tags, nostr.Tag{"e", id.Hex()})
return nil
})
},
},
{
Name: "delete-group",
Usage: "deletes the group",
ArgsUsage: "<relay>'<identifier>",
Action: func(ctx context.Context, c *cli.Command) error {
return createModerationEvent(ctx, c, 9008, func(evt *nostr.Event, args []string) error {
return nil
})
},
},
{
Name: "create-invite",
Usage: "creates an invite code",
ArgsUsage: "<relay>'<identifier>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "code",
Required: true,
},
},
Action: func(ctx context.Context, c *cli.Command) error {
return createModerationEvent(ctx, c, 9009, func(evt *nostr.Event, args []string) error {
evt.Tags = append(evt.Tags, nostr.Tag{"code", c.String("code")})
return nil
})
},
},
},
}
func createModerationEvent(ctx context.Context, c *cli.Command, kind nostr.Kind, setupFunc func(*nostr.Event, []string) error) error {
args := c.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("requires group identifier")
}
relay, identifier, err := parseGroupIdentifier(c)
if err != nil {
return err
}
kr, _, err := gatherKeyerFromArguments(ctx, c)
if err != nil {
return err
}
evt := nostr.Event{
Kind: kind,
CreatedAt: nostr.Now(),
Content: "",
Tags: nostr.Tags{
{"h", identifier},
},
}
if err := setupFunc(&evt, args); err != nil {
return err
}
if err := kr.SignEvent(ctx, &evt); err != nil {
return fmt.Errorf("failed to sign event: %w", err)
}
stdout(evt.String())
r, err := sys.Pool.EnsureRelay(relay)
if err != nil {
return err
}
return r.Publish(ctx, evt)
} }
func cond(b bool, ifYes string, ifNo string) string { func cond(b bool, ifYes string, ifNo string) string {

View File

@@ -541,6 +541,8 @@ var colors = struct {
italicf func(string, ...any) string italicf func(string, ...any) string
bold func(...any) string bold func(...any) string
boldf func(string, ...any) string boldf func(string, ...any) string
underline func(...any) string
underlinef func(string, ...any) string
error func(...any) string error func(...any) string
errorf func(string, ...any) string errorf func(string, ...any) string
success func(...any) string success func(...any) string
@@ -551,6 +553,8 @@ var colors = struct {
color.New(color.Italic).Sprintf, color.New(color.Italic).Sprintf,
color.New(color.Bold).Sprint, color.New(color.Bold).Sprint,
color.New(color.Bold).Sprintf, color.New(color.Bold).Sprintf,
color.New(color.Underline).Sprint,
color.New(color.Underline).Sprintf,
color.New(color.Bold, color.FgHiRed).Sprint, color.New(color.Bold, color.FgHiRed).Sprint,
color.New(color.Bold, color.FgHiRed).Sprintf, color.New(color.Bold, color.FgHiRed).Sprintf,
color.New(color.Bold, color.FgHiGreen).Sprint, color.New(color.Bold, color.FgHiGreen).Sprint,