mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
50 Commits
v0.15.4
...
a83b23d76b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a83b23d76b | ||
|
|
a288cc47a4 | ||
|
|
5ee7670ba8 | ||
|
|
b973b476bc | ||
|
|
252612b12f | ||
|
|
4b8b6bb3de | ||
|
|
df491be232 | ||
|
|
1dab81f77c | ||
|
|
11228d7082 | ||
|
|
a422b5f708 | ||
|
|
852fe6bdfb | ||
|
|
210cf66d5f | ||
|
|
f9335b0ab4 | ||
|
|
16916d7d95 | ||
|
|
3ff4dbe196 | ||
|
|
2de3ff78ee | ||
|
|
03c1bf832e | ||
|
|
8df130a822 | ||
|
|
e04861fcee | ||
|
|
73d80203a0 | ||
|
|
c3cb59a94a | ||
|
|
59edaba5b8 | ||
|
|
11a690b1c6 | ||
|
|
9f8679591e | ||
|
|
75c1a88333 | ||
|
|
26fc7c338a | ||
|
|
ddc009a391 | ||
|
|
68e49fa6e5 | ||
|
|
79c1a70683 | ||
|
|
77afab780b | ||
|
|
a4f53021f0 | ||
|
|
afa31a58fc | ||
|
|
26f9b33d53 | ||
|
|
51876f89c4 | ||
|
|
ae3cb7c108 | ||
|
|
bec821d3c0 | ||
|
|
5d7240b112 | ||
|
|
bbe1661096 | ||
|
|
ea4ad84aa0 | ||
|
|
85a04aa7ce | ||
|
|
e0ca768695 | ||
|
|
bef3739a67 | ||
|
|
210c0aa282 | ||
|
|
2758285d51 | ||
|
|
ecb7f8f195 | ||
|
|
9251702460 | ||
|
|
13452e6916 | ||
|
|
cdd64e340f | ||
|
|
3b4d6046cf | ||
|
|
bf1690a041 |
186
admin.go
Normal file
186
admin.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip86"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var admin = &cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "manage relays using the relay management API",
|
||||
Description: `examples:
|
||||
nak admin allowpubkey myrelay.com --pubkey 1234... --reason "good user"
|
||||
nak admin banpubkey myrelay.com --pubkey 1234... --reason "spam"
|
||||
nak admin listallowedpubkeys myrelay.com
|
||||
nak admin changerelayname myrelay.com --name "My Relay"`,
|
||||
ArgsUsage: "<relay-url>",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: defaultKeyFlags,
|
||||
Commands: (func() []*cli.Command {
|
||||
methods := []struct {
|
||||
method string
|
||||
args []string
|
||||
}{
|
||||
{"allowpubkey", []string{"pubkey", "reason"}},
|
||||
{"banpubkey", []string{"pubkey", "reason"}},
|
||||
{"listallowedpubkeys", nil},
|
||||
{"listbannedpubkeys", nil},
|
||||
{"listeventsneedingmoderation", nil},
|
||||
{"allowevent", []string{"id", "reason"}},
|
||||
{"banevent", []string{"id", "reason"}},
|
||||
{"listbannedevents", nil},
|
||||
{"changerelayname", []string{"name"}},
|
||||
{"changerelaydescription", []string{"description"}},
|
||||
{"changerelayicon", []string{"icon"}},
|
||||
{"allowkind", []string{"kind"}},
|
||||
{"disallowkind", []string{"kind"}},
|
||||
{"listallowedkinds", nil},
|
||||
{"blockip", []string{"ip", "reason"}},
|
||||
{"unblockip", []string{"ip", "reason"}},
|
||||
{"listblockedips", nil},
|
||||
}
|
||||
|
||||
commands := make([]*cli.Command, 0, len(methods))
|
||||
for _, def := range methods {
|
||||
def := def
|
||||
|
||||
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
||||
for i, argName := range def.args {
|
||||
flags[i] = declareFlag(argName)
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: def.method,
|
||||
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
||||
Description: fmt.Sprintf(
|
||||
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
||||
Flags: flags,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
params := make([]any, len(def.args))
|
||||
for i, argName := range def.args {
|
||||
params[i] = getArgument(c, argName)
|
||||
}
|
||||
req := nip86.Request{Method: def.method, Params: params}
|
||||
reqj, _ := json.Marshal(req)
|
||||
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) == 0 {
|
||||
stdout(string(reqj))
|
||||
return nil
|
||||
}
|
||||
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, relayUrl := range relayUrls {
|
||||
httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:]
|
||||
log("calling '%s' on %s... ", def.method, httpUrl)
|
||||
body := bytes.NewBuffer(nil)
|
||||
body.Write(reqj)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Authorization
|
||||
payloadHash := sha256.Sum256(reqj)
|
||||
tokenEvent := nostr.Event{
|
||||
Kind: 27235,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"u", httpUrl},
|
||||
{"method", "POST"},
|
||||
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||
return fmt.Errorf("failed to sign token event: %w", err)
|
||||
}
|
||||
evtj, _ := json.Marshal(tokenEvent)
|
||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||
|
||||
// Content-Type
|
||||
req.Header.Set("Content-Type", "application/nostr+json+rpc")
|
||||
|
||||
// make request to relay
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log("failed: %s\n", err)
|
||||
continue
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log("failed to read response: %s\n", err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode >= 300 {
|
||||
log("failed with status %d\n", resp.StatusCode)
|
||||
bodyPrintable := string(b)
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
continue
|
||||
}
|
||||
var response nip86.Response
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
log("bad json response: %s\n", err)
|
||||
bodyPrintable := string(b)
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// print the result
|
||||
log("\n")
|
||||
pretty, _ := json.MarshalIndent(response, "", " ")
|
||||
stdout(string(pretty))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
|
||||
return commands
|
||||
})(),
|
||||
}
|
||||
|
||||
func declareFlag(argName string) cli.Flag {
|
||||
usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information."
|
||||
switch argName {
|
||||
case "kind":
|
||||
return &cli.IntFlag{Name: argName, Required: true, Usage: usage}
|
||||
case "reason":
|
||||
return &cli.StringFlag{Name: argName, Usage: usage}
|
||||
default:
|
||||
return &cli.StringFlag{Name: argName, Required: true, Usage: usage}
|
||||
}
|
||||
}
|
||||
|
||||
func getArgument(c *cli.Command, argName string) any {
|
||||
switch argName {
|
||||
case "kind":
|
||||
return c.Int(argName)
|
||||
default:
|
||||
return c.String(argName)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/keyer"
|
||||
"fiatjaf.com/nostr/nipb0/blossom"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -38,11 +37,11 @@ var blossomCmd = &cli.Command{
|
||||
var client *blossom.Client
|
||||
pubkey := c.Args().First()
|
||||
if pubkey != "" {
|
||||
if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
|
||||
pk, err := parsePubKey(pubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
||||
} else {
|
||||
client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk))
|
||||
}
|
||||
client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk))
|
||||
} else {
|
||||
var err error
|
||||
client, err = getBlossomClient(ctx, c)
|
||||
|
||||
8
count.go
8
count.go
@@ -21,7 +21,7 @@ var count = &cli.Command{
|
||||
&PubKeySliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Usage: "only accept events from these authors",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
@@ -101,16 +101,16 @@ var count = &cli.Command{
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.SplitN(tagFlag, "=", 2)
|
||||
if len(spl) == 2 {
|
||||
tags = append(tags, spl)
|
||||
tags = append(tags, []string{spl[0], decodeTagValue(spl[1])})
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
tags = append(tags, []string{"e", decodeTagValue(etag)})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
tags = append(tags, []string{"p", decodeTagValue(ptag)})
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
filter.Tags = make(nostr.TagMap)
|
||||
|
||||
282
dekey.go
Normal file
282
dekey.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip44"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var dekey = &cli.Command{
|
||||
Name: "dekey",
|
||||
Usage: "handles NIP-4E decoupled encryption keys",
|
||||
Description: "maybe this picture will explain better than I can do here for now: https://cdn.azzamo.net/89c543d261ad0d665c1dea78f91e527c2e39e7fe503b440265a3c47e63c9139f.png",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "device-name",
|
||||
Usage: "name of this device that will be published and displayed on other clients",
|
||||
Value: func() string {
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
return "nak@" + hostname
|
||||
}
|
||||
return "nak@unknown"
|
||||
}(),
|
||||
},
|
||||
),
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userPub, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user public key: %w", err)
|
||||
}
|
||||
|
||||
configPath := c.String("config-path")
|
||||
deviceName := c.String("device-name")
|
||||
|
||||
// check if we already have a local-device secret key
|
||||
deviceKeyPath := filepath.Join(configPath, "dekey", "device-key")
|
||||
var deviceSec nostr.SecretKey
|
||||
if data, err := os.ReadFile(deviceKeyPath); err == nil {
|
||||
deviceSec, err = nostr.SecretKeyFromHex(string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err)
|
||||
}
|
||||
} else {
|
||||
// create one
|
||||
deviceSec = nostr.Generate()
|
||||
os.MkdirAll(filepath.Dir(deviceKeyPath), 0700)
|
||||
if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write device key: %w", err)
|
||||
}
|
||||
}
|
||||
devicePub := deviceSec.Public()
|
||||
|
||||
// get relays for the user
|
||||
relays := sys.FetchWriteRelays(ctx, userPub)
|
||||
relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{})
|
||||
if len(relayList) == 0 {
|
||||
return fmt.Errorf("no relays to use")
|
||||
}
|
||||
|
||||
// check if kind:4454 is already published
|
||||
events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{4454},
|
||||
Authors: []nostr.PubKey{userPub},
|
||||
Tags: nostr.TagMap{
|
||||
"pubkey": []string{devicePub.Hex()},
|
||||
},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
||||
if len(events) == 0 {
|
||||
// publish kind:4454
|
||||
evt := nostr.Event{
|
||||
Kind: 4454,
|
||||
Content: "",
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"client", deviceName},
|
||||
{"pubkey", devicePub.Hex()},
|
||||
},
|
||||
}
|
||||
|
||||
// sign with main key
|
||||
if err := kr.SignEvent(ctx, &evt); err != nil {
|
||||
return fmt.Errorf("failed to sign device event: %w", err)
|
||||
}
|
||||
|
||||
// publish
|
||||
if err := publishFlow(ctx, c, kr, evt, relayList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// check for kind:10044
|
||||
userKeyEventDate := nostr.Now()
|
||||
userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{10044},
|
||||
Authors: []nostr.PubKey{userPub},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
||||
var eSec nostr.SecretKey
|
||||
var ePub nostr.PubKey
|
||||
if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok {
|
||||
// generate main secret key
|
||||
eSec = nostr.Generate()
|
||||
ePub := eSec.Public()
|
||||
|
||||
// store it
|
||||
eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex())
|
||||
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
||||
if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write user encryption key: %w", err)
|
||||
}
|
||||
|
||||
// publish kind:10044
|
||||
evt10044 := nostr.Event{
|
||||
Kind: 10044,
|
||||
Content: "",
|
||||
CreatedAt: userKeyEventDate,
|
||||
Tags: nostr.Tags{
|
||||
{"n", ePub.Hex()},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &evt10044); err != nil {
|
||||
return fmt.Errorf("failed to sign kind:10044: %w", err)
|
||||
}
|
||||
|
||||
if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
userKeyEventDate = userKeyEvent.CreatedAt
|
||||
|
||||
// get the pub from the tag
|
||||
for _, tag := range userKeyEvent.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "n" {
|
||||
ePub, _ = nostr.PubKeyFromHex(tag[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
if ePub == nostr.ZeroPK {
|
||||
return fmt.Errorf("invalid kind:10044 event, no 'n' tag")
|
||||
}
|
||||
|
||||
// check if we have the key
|
||||
eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex())
|
||||
if data, err := os.ReadFile(eKeyPath); err == nil {
|
||||
eSec, err = nostr.SecretKeyFromHex(string(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid main key: %w", err)
|
||||
}
|
||||
if eSec.Public() != ePub {
|
||||
return fmt.Errorf("stored user encryption key is corrupted: %w", err)
|
||||
}
|
||||
} else {
|
||||
// try to decrypt from kind:4455
|
||||
for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{4455},
|
||||
Tags: nostr.TagMap{
|
||||
"p": []string{devicePub.Hex()},
|
||||
},
|
||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
||||
var senderPub nostr.PubKey
|
||||
for _, tag := range eKeyMsg.Tags {
|
||||
if len(tag) >= 2 && tag[0] == "P" {
|
||||
senderPub, _ = nostr.PubKeyFromHex(tag[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
if senderPub == nostr.ZeroPK {
|
||||
continue
|
||||
}
|
||||
ss, err := nip44.GenerateConversationKey(senderPub, deviceSec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eSecHex, err := nip44.Decrypt(eKeyMsg.Content, ss)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
eSec, err = nostr.SecretKeyFromHex(eSecHex)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// check if it matches mainPub
|
||||
if eSec.Public() == ePub {
|
||||
// store it
|
||||
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
||||
os.WriteFile(eKeyPath, []byte(eSecHex), 0600)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if eSec == [32]byte{} {
|
||||
log("main secret key not available, must authorize on another device\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// now we have mainSec, check for other kind:4454 events newer than the 10044
|
||||
keyMsgs := make([]string, 0, 5)
|
||||
for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||
Kinds: []nostr.Kind{4454, 4455},
|
||||
Authors: []nostr.PubKey{userPub},
|
||||
Since: userKeyEventDate,
|
||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
||||
if keyOrDeviceEvt.Kind == 4455 {
|
||||
// key event
|
||||
|
||||
// skip ourselves
|
||||
if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// assume a key msg will always come before its associated devicemsg
|
||||
// so just store them here:
|
||||
pubkeyTag := keyOrDeviceEvt.Tags.Find("p")
|
||||
if pubkeyTag == nil {
|
||||
continue
|
||||
}
|
||||
keyMsgs = append(keyMsgs, pubkeyTag[1])
|
||||
} else if keyOrDeviceEvt.Kind == 4454 {
|
||||
// device event
|
||||
|
||||
// skip ourselves
|
||||
if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if this already has a corresponding keyMsg then skip it
|
||||
pubkeyTag := keyOrDeviceEvt.Tags.Find("pubkey")
|
||||
if pubkeyTag == nil {
|
||||
continue
|
||||
}
|
||||
if slices.Contains(keyMsgs, pubkeyTag[1]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// here we know we're dealing with a deviceMsg without a corresponding keyMsg
|
||||
// so we have to build a keyMsg for them
|
||||
theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ss, err := nip44.GenerateConversationKey(theirDevice, deviceSec)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ciphertext, err := nip44.Encrypt(eSec.Hex(), ss)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
evt4455 := nostr.Event{
|
||||
Kind: 4455,
|
||||
Content: ciphertext,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"p", theirDevice.Hex()},
|
||||
{"P", devicePub.Hex()},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &evt4455); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
publishFlow(ctx, c, kr, evt4455, relayList)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -163,7 +163,7 @@ var encode = &cli.Command{
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||
id, err := nostr.IDFromHex(target)
|
||||
id, err := parseEventID(target)
|
||||
if err != nil {
|
||||
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
||||
continue
|
||||
|
||||
@@ -17,7 +17,7 @@ var encrypt = &cli.Command{
|
||||
defaultKeyFlags,
|
||||
&PubKeyFlag{
|
||||
Name: "recipient-pubkey",
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
@@ -79,7 +79,7 @@ var decrypt = &cli.Command{
|
||||
defaultKeyFlags,
|
||||
&PubKeyFlag{
|
||||
Name: "sender-pubkey",
|
||||
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||
Aliases: []string{"p", "src", "source", "pubkey", "from"},
|
||||
Required: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
@@ -111,7 +111,7 @@ var decrypt = &cli.Command{
|
||||
}
|
||||
plaintext, err := nip04.Decrypt(ciphertext, ss)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt as nip04: %w", err)
|
||||
return fmt.Errorf("failed to decrypt as nip04: %w", err)
|
||||
}
|
||||
stdout(plaintext)
|
||||
}
|
||||
|
||||
27
event.go
27
event.go
@@ -211,24 +211,30 @@ example:
|
||||
if found {
|
||||
// tags may also contain extra elements separated with a ";"
|
||||
tagValues := strings.Split(tagValue, ";")
|
||||
if len(tagValues) >= 1 {
|
||||
tagValues[0] = decodeTagValue(tagValues[0])
|
||||
}
|
||||
tag = append(tag, tagValues...)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
if tags.FindWithValue("e", etag) == nil {
|
||||
tags = append(tags, nostr.Tag{"e", etag})
|
||||
decodedEtag := decodeTagValue(etag)
|
||||
if tags.FindWithValue("e", decodedEtag) == nil {
|
||||
tags = append(tags, nostr.Tag{"e", decodedEtag})
|
||||
}
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
if tags.FindWithValue("p", ptag) == nil {
|
||||
tags = append(tags, nostr.Tag{"p", ptag})
|
||||
decodedPtag := decodeTagValue(ptag)
|
||||
if tags.FindWithValue("p", decodedPtag) == nil {
|
||||
tags = append(tags, nostr.Tag{"p", decodedPtag})
|
||||
}
|
||||
}
|
||||
for _, dtag := range c.StringSlice("d") {
|
||||
if tags.FindWithValue("d", dtag) == nil {
|
||||
tags = append(tags, nostr.Tag{"d", dtag})
|
||||
decodedDtag := decodeTagValue(dtag)
|
||||
if tags.FindWithValue("d", decodedDtag) == nil {
|
||||
tags = append(tags, nostr.Tag{"d", decodedDtag})
|
||||
}
|
||||
}
|
||||
if len(tags) > 0 {
|
||||
@@ -403,13 +409,20 @@ func publishFlow(ctx context.Context, c *cli.Command, kr nostr.Signer, evt nostr
|
||||
}
|
||||
} else {
|
||||
// normal dumb flow
|
||||
for _, relay := range relays {
|
||||
for i, relay := range relays {
|
||||
publish:
|
||||
cleanUrl, _ := strings.CutPrefix(relay.URL, "wss://")
|
||||
log("publishing to %s... ", color.CyanString(cleanUrl))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if !relay.IsConnected() {
|
||||
if new_, err := sys.Pool.EnsureRelay(relay.URL); err == nil {
|
||||
relays[i] = new_
|
||||
relay = new_
|
||||
}
|
||||
}
|
||||
|
||||
err := relay.Publish(ctx, evt)
|
||||
if err == nil {
|
||||
// published fine
|
||||
|
||||
8
flags.go
8
flags.go
@@ -96,8 +96,8 @@ func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.V
|
||||
func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() }
|
||||
|
||||
func (t *pubkeyValue) Set(value string) error {
|
||||
pk, err := nostr.PubKeyFromHex(value)
|
||||
t.pubkey = pk
|
||||
pubkey, err := parsePubKey(value)
|
||||
t.pubkey = pubkey
|
||||
t.hasBeenSet = true
|
||||
return err
|
||||
}
|
||||
@@ -147,8 +147,8 @@ func (t idValue) Create(val nostr.ID, p *nostr.ID, c struct{}) cli.Value {
|
||||
func (t idValue) ToString(b nostr.ID) string { return t.id.String() }
|
||||
|
||||
func (t *idValue) Set(value string) error {
|
||||
pk, err := nostr.IDFromHex(value)
|
||||
t.id = pk
|
||||
id, err := parseEventID(value)
|
||||
t.id = id
|
||||
t.hasBeenSet = true
|
||||
return err
|
||||
}
|
||||
|
||||
192
gift.go
Normal file
192
gift.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip44"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var gift = &cli.Command{
|
||||
Name: "gift",
|
||||
Usage: "gift-wraps (or unwraps) an event according to NIP-59",
|
||||
Description: `example:
|
||||
nak event | nak gift wrap --sec <sec-a> -p <sec-b> | nak gift unwrap --sec <sec-b> --from <pub-a>`,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "wrap",
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
&PubKeyFlag{
|
||||
Name: "recipient-pubkey",
|
||||
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
Usage: "turns an event into a rumor (unsigned) then gift-wraps it to the recipient",
|
||||
Description: `example:
|
||||
nak event -c 'hello' | nak gift wrap --sec <my-secret-key> -p <target-public-key>`,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
recipient := getPubKey(c, "recipient-pubkey")
|
||||
|
||||
// get sender pubkey
|
||||
sender, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get sender pubkey: %w", err)
|
||||
}
|
||||
|
||||
// read event from stdin
|
||||
for eventJSON := range getJsonsOrBlank() {
|
||||
if eventJSON == "{}" {
|
||||
continue
|
||||
}
|
||||
|
||||
var originalEvent nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(eventJSON), &originalEvent); err != nil {
|
||||
return fmt.Errorf("invalid event JSON: %w", err)
|
||||
}
|
||||
|
||||
// turn into rumor (unsigned event)
|
||||
rumor := originalEvent
|
||||
rumor.Sig = [64]byte{} // remove signature
|
||||
rumor.PubKey = sender
|
||||
rumor.ID = rumor.GetID() // compute ID
|
||||
|
||||
// create seal
|
||||
rumorJSON, _ := easyjson.Marshal(rumor)
|
||||
encryptedRumor, err := kr.Encrypt(ctx, string(rumorJSON), recipient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt rumor: %w", err)
|
||||
}
|
||||
seal := &nostr.Event{
|
||||
Kind: 13,
|
||||
Content: encryptedRumor,
|
||||
PubKey: sender,
|
||||
CreatedAt: randomNow(),
|
||||
Tags: nostr.Tags{},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, seal); err != nil {
|
||||
return fmt.Errorf("failed to sign seal: %w", err)
|
||||
}
|
||||
|
||||
// create gift wrap
|
||||
ephemeral := nostr.Generate()
|
||||
sealJSON, _ := easyjson.Marshal(seal)
|
||||
convkey, err := nip44.GenerateConversationKey(recipient, ephemeral)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate conversation key: %w", err)
|
||||
}
|
||||
encryptedSeal, err := nip44.Encrypt(string(sealJSON), convkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt seal: %w", err)
|
||||
}
|
||||
wrap := &nostr.Event{
|
||||
Kind: 1059,
|
||||
Content: encryptedSeal,
|
||||
CreatedAt: randomNow(),
|
||||
Tags: nostr.Tags{{"p", recipient.Hex()}},
|
||||
}
|
||||
wrap.Sign(ephemeral)
|
||||
|
||||
// print the gift-wrap
|
||||
wrapJSON, err := easyjson.Marshal(wrap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal gift wrap: %w", err)
|
||||
}
|
||||
stdout(string(wrapJSON))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "unwrap",
|
||||
Usage: "decrypts a gift-wrap event sent by the sender to us and exposes its internal rumor (unsigned event).",
|
||||
Description: `example:
|
||||
nak req -p <my-public-key> -k 1059 dmrelay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>`,
|
||||
Flags: append(
|
||||
defaultKeyFlags,
|
||||
&PubKeyFlag{
|
||||
Name: "sender-pubkey",
|
||||
Aliases: []string{"p", "src", "source", "pubkey", "from"},
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sender := getPubKey(c, "sender-pubkey")
|
||||
|
||||
// read gift-wrapped event from stdin
|
||||
for wrapJSON := range getJsonsOrBlank() {
|
||||
if wrapJSON == "{}" {
|
||||
continue
|
||||
}
|
||||
|
||||
var wrap nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(wrapJSON), &wrap); err != nil {
|
||||
return fmt.Errorf("invalid gift wrap JSON: %w", err)
|
||||
}
|
||||
|
||||
if wrap.Kind != 1059 {
|
||||
return fmt.Errorf("not a gift wrap event (kind %d)", wrap.Kind)
|
||||
}
|
||||
|
||||
ephemeralPubkey := wrap.PubKey
|
||||
|
||||
// decrypt seal
|
||||
sealJSON, err := kr.Decrypt(ctx, wrap.Content, ephemeralPubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt seal: %w", err)
|
||||
}
|
||||
|
||||
var seal nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(sealJSON), &seal); err != nil {
|
||||
return fmt.Errorf("invalid seal JSON: %w", err)
|
||||
}
|
||||
|
||||
if seal.Kind != 13 {
|
||||
return fmt.Errorf("not a seal event (kind %d)", seal.Kind)
|
||||
}
|
||||
|
||||
// decrypt rumor
|
||||
rumorJSON, err := kr.Decrypt(ctx, seal.Content, sender)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrypt rumor: %w", err)
|
||||
}
|
||||
|
||||
var rumor nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(rumorJSON), &rumor); err != nil {
|
||||
return fmt.Errorf("invalid rumor JSON: %w", err)
|
||||
}
|
||||
|
||||
// output the unwrapped event (rumor)
|
||||
stdout(rumorJSON)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func randomNow() nostr.Timestamp {
|
||||
const twoDays = 2 * 24 * 60 * 60
|
||||
now := time.Now().Unix()
|
||||
randomOffset := rand.Int63n(twoDays)
|
||||
return nostr.Timestamp(now - randomOffset)
|
||||
}
|
||||
64
go.mod
64
go.mod
@@ -1,84 +1,106 @@
|
||||
module github.com/fiatjaf/nak
|
||||
|
||||
go 1.24.1
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
fiatjaf.com/lib v0.3.1
|
||||
fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10
|
||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.5
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||
github.com/fatih/color v1.16.0
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/liamg/magic v0.0.1
|
||||
github.com/mailru/easyjson v0.9.0
|
||||
github.com/mailru/easyjson v0.9.1
|
||||
github.com/mark3labs/mcp-go v0.8.3
|
||||
github.com/markusmobius/go-dateparser v1.2.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-tty v0.0.7
|
||||
github.com/mdp/qrterminal/v3 v3.2.1
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
||||
golang.org/x/sync v0.18.0
|
||||
golang.org/x/term v0.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/bluekeyes/go-gitdiff v0.7.1 // indirect
|
||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/coder/websocket v1.8.13 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 // indirect
|
||||
github.com/dgraph-io/ristretto v1.0.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||
github.com/elnosh/gonuts v0.4.2 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/flatbuffers v24.12.23+incompatible // indirect
|
||||
github.com/go-git/go-git/v5 v5.16.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||
github.com/templexxx/cpu v0.0.1 // indirect
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.36.2 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||
go.etcd.io/bbolt v1.4.2 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
||||
240
go.sum
240
go.sum
@@ -1,22 +1,38 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||
fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab h1:zMp+G9Et5Z7ku/WUflZpmQzDIAB/Ah00Ms3cMtX9Pw4=
|
||||
fiatjaf.com/nostr v0.0.0-20250818235102-c8d5aa703fab/go.mod h1:j7AfnEAevFuLcpH4Y1RYM27sYJfshL3An6ZSAQNlUlY=
|
||||
fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10 h1:T1rQuoQgC6GwxJTLTDjli8ZauCtOa+BjybU3pxCN84w=
|
||||
fiatjaf.com/nostr v0.0.0-20250829192328-aa321f6e7f10/go.mod h1:92kdTV0aFobQ14aq5IMpkYy3lGI8RUabMwN3Cfv/qmg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 h1:14yLsO2HwpS2CLIKFvLMDp8tVEDahwdC8OeG6NGaL+M=
|
||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d h1:xROmiuT7LrZk+/iGGeTqRI4liqJZrc87AWjsyHtbqDg=
|
||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||
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=
|
||||
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
|
||||
github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
|
||||
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||
@@ -24,8 +40,8 @@ github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E=
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
|
||||
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
|
||||
@@ -43,21 +59,36 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -69,14 +100,12 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0 h1:TeJE3I1pIWLBjYhIYCA1+uxrjWEoJXImFBMEBVSm16g=
|
||||
github.com/dgraph-io/badger/v4 v4.5.0/go.mod h1:ysgYmIeG8dS/E8kwxT7xHyc7MkmwNYLRoYnFbr7387A=
|
||||
github.com/dgraph-io/ristretto v1.0.0 h1:SYG07bONKMlFDUYu5pEu3DGAh8c2OFNzKm6G9J4Si84=
|
||||
github.com/dgraph-io/ristretto v1.0.0/go.mod h1:jTi2FiYEhQ1NsMmA7DeBykizjOuY88NhKBkepyu1jPc=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
|
||||
github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
@@ -84,10 +113,6 @@ github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3p
|
||||
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||
github.com/elnosh/gonuts v0.4.2 h1:/WubPAWGxTE+okJ0WPvmtEzTzpi04RGxiTHAF1FYU+M=
|
||||
github.com/elnosh/gonuts v0.4.2/go.mod h1:vgZomh4YQk7R3w4ltZc0sHwCmndfHkuX6V4sga/8oNs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
|
||||
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
@@ -96,36 +121,26 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8=
|
||||
github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8=
|
||||
github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
@@ -133,6 +148,10 @@ github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||
@@ -143,29 +162,46 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
|
||||
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
|
||||
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
|
||||
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -173,6 +209,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -186,31 +226,36 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||
github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
|
||||
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3Wb1+pWBaWv/BlHK0ZYIu/KaL6eHg=
|
||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
@@ -228,46 +273,45 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e
|
||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 h1:vuCObX8mQzik1tfEcYxWZBuVsmQtD1IjxCyPKM18Bh4=
|
||||
golang.org/x/exp v0.0.0-20250717185816-542afb5b7346/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -275,47 +319,39 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -326,7 +362,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
|
||||
67
helpers.go
67
helpers.go
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip05"
|
||||
"fiatjaf.com/nostr/nip19"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
@@ -75,7 +76,7 @@ func getJsonsOrBlank() iter.Seq[string] {
|
||||
return true
|
||||
})
|
||||
|
||||
if !hasStdin {
|
||||
if !hasStdin && !isPiped() {
|
||||
yield("{}")
|
||||
}
|
||||
|
||||
@@ -464,6 +465,70 @@ func askConfirmation(msg string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func parsePubKey(value string) (nostr.PubKey, error) {
|
||||
// try nip05 first
|
||||
if nip05.IsValidIdentifier(value) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
pp, err := nip05.QueryIdentifier(ctx, value)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return pp.PublicKey, nil
|
||||
}
|
||||
// if nip05 fails, fall through to try as pubkey
|
||||
}
|
||||
|
||||
pk, err := nostr.PubKeyFromHex(value)
|
||||
if err == nil {
|
||||
return pk, nil
|
||||
}
|
||||
|
||||
if prefix, decoded, err := nip19.Decode(value); err == nil {
|
||||
switch prefix {
|
||||
case "npub":
|
||||
if pk, ok := decoded.(nostr.PubKey); ok {
|
||||
return pk, nil
|
||||
}
|
||||
case "nprofile":
|
||||
if profile, ok := decoded.(nostr.ProfilePointer); ok {
|
||||
return profile.PublicKey, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nostr.PubKey{}, fmt.Errorf("invalid pubkey (\"%s\"): expected hex, npub, or nprofile", value)
|
||||
}
|
||||
|
||||
func parseEventID(value string) (nostr.ID, error) {
|
||||
id, err := nostr.IDFromHex(value)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
if prefix, decoded, err := nip19.Decode(value); err == nil {
|
||||
switch prefix {
|
||||
case "note":
|
||||
if id, ok := decoded.(nostr.ID); ok {
|
||||
return id, nil
|
||||
}
|
||||
case "nevent":
|
||||
if event, ok := decoded.(nostr.EventPointer); ok {
|
||||
return event.ID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nostr.ID{}, fmt.Errorf("invalid event id (\"%s\"): expected hex, note, or nevent", value)
|
||||
}
|
||||
|
||||
func decodeTagValue(value string) string {
|
||||
if strings.HasPrefix(value, "npub1") || strings.HasPrefix(value, "nevent1") || strings.HasPrefix(value, "note1") || strings.HasPrefix(value, "nprofile1") || strings.HasPrefix(value, "naddr1") {
|
||||
if ptr, err := nip19.ToPointer(value); err == nil {
|
||||
return ptr.AsTagReference()
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
var colors = struct {
|
||||
reset func(...any) (int, error)
|
||||
italic func(...any) string
|
||||
|
||||
@@ -77,7 +77,7 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
|
||||
clientKey = nostr.Generate()
|
||||
}
|
||||
|
||||
logverbose("[nip46]: connecting to %s with client key %s", bunkerURL, clientKey.Hex())
|
||||
logverbose("[nip46]: connecting to %s with client key %s\n", bunkerURL, clientKey.Hex())
|
||||
|
||||
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {
|
||||
log(color.CyanString("[nip46]: open the following URL: %s"), s)
|
||||
|
||||
6
main.go
6
main.go
@@ -36,17 +36,23 @@ var app = &cli.Command{
|
||||
key,
|
||||
verify,
|
||||
relay,
|
||||
admin,
|
||||
bunker,
|
||||
serve,
|
||||
blossomCmd,
|
||||
dekey,
|
||||
encrypt,
|
||||
decrypt,
|
||||
gift,
|
||||
outbox,
|
||||
wallet,
|
||||
mcpServer,
|
||||
curl,
|
||||
fsCmd,
|
||||
publish,
|
||||
git,
|
||||
nip,
|
||||
syncCmd,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
|
||||
201
nip.go
Normal file
201
nip.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
type nipInfo struct {
|
||||
nip, desc, link string
|
||||
}
|
||||
|
||||
var nip = &cli.Command{
|
||||
Name: "nip",
|
||||
Usage: "list NIPs or get the description of a NIP from its number",
|
||||
Description: `lists NIPs, fetches and displays NIP text, or opens a NIP page in the browser.
|
||||
|
||||
examples:
|
||||
nak nip # list all NIPs
|
||||
nak nip 29 # shows nip29 details
|
||||
nak nip open 29 # opens nip29 in browser`,
|
||||
ArgsUsage: "[NIP number]",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "open",
|
||||
Usage: "open the NIP page in the browser",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
reqNum := c.Args().First()
|
||||
if reqNum == "" {
|
||||
return fmt.Errorf("missing NIP number")
|
||||
}
|
||||
|
||||
normalize := func(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = strings.TrimPrefix(s, "nip-")
|
||||
s = strings.TrimLeft(s, "0")
|
||||
if s == "" {
|
||||
s = "0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
reqNum = normalize(reqNum)
|
||||
|
||||
foundLink := ""
|
||||
for info := range listnips() {
|
||||
nipNum := normalize(info.nip)
|
||||
if nipNum == reqNum {
|
||||
foundLink = info.link
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundLink == "" {
|
||||
return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum))
|
||||
}
|
||||
|
||||
url := "https://github.com/nostr-protocol/nips/blob/master/" + foundLink
|
||||
fmt.Println("Opening " + url)
|
||||
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", url)
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "start", url)
|
||||
default:
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
}
|
||||
|
||||
return cmd.Start()
|
||||
},
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
reqNum := c.Args().First()
|
||||
if reqNum == "" {
|
||||
// list all NIPs
|
||||
for info := range listnips() {
|
||||
stdout(info.nip + ": " + info.desc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
normalize := func(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
s = strings.TrimPrefix(s, "nip-")
|
||||
s = strings.TrimLeft(s, "0")
|
||||
if s == "" {
|
||||
s = "0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
reqNum = normalize(reqNum)
|
||||
|
||||
var foundLink string
|
||||
for info := range listnips() {
|
||||
nipNum := normalize(info.nip)
|
||||
|
||||
if nipNum == reqNum {
|
||||
foundLink = info.link
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundLink == "" {
|
||||
return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum))
|
||||
}
|
||||
|
||||
// fetch the NIP markdown
|
||||
url := "https://raw.githubusercontent.com/nostr-protocol/nips/master/" + foundLink
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch NIP: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read NIP: %w", err)
|
||||
}
|
||||
|
||||
// render markdown
|
||||
rendered, err := glamour.Render(string(body), "auto")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render markdown: %w", err)
|
||||
}
|
||||
|
||||
fmt.Print(rendered)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func listnips() <-chan nipInfo {
|
||||
ch := make(chan nipInfo)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
|
||||
if err != nil {
|
||||
// TODO: handle error? but since chan, maybe send error somehow, but for now, just close
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyStr := string(body)
|
||||
epoch := strings.Index(bodyStr, "## List")
|
||||
if epoch == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.SplitSeq(bodyStr[epoch+8:], "\n")
|
||||
for line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "##") {
|
||||
break
|
||||
}
|
||||
if !strings.HasPrefix(line, "- [NIP-") {
|
||||
continue
|
||||
}
|
||||
|
||||
start := strings.Index(line, "[")
|
||||
end := strings.Index(line, "]")
|
||||
if start == -1 || end == -1 || end < start {
|
||||
continue
|
||||
}
|
||||
|
||||
content := line[start+1 : end]
|
||||
|
||||
parts := strings.SplitN(content, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
nipPart := parts[0]
|
||||
descPart := parts[1]
|
||||
|
||||
rest := line[end+1:]
|
||||
linkStart := strings.Index(rest, "(")
|
||||
linkEnd := strings.Index(rest, ")")
|
||||
link := ""
|
||||
if linkStart != -1 && linkEnd != -1 && linkEnd > linkStart {
|
||||
link = rest[linkStart+1 : linkEnd]
|
||||
}
|
||||
|
||||
ch <- nipInfo{nipPart, strings.TrimSpace(descPart), link}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
13
outbox.go
13
outbox.go
@@ -6,9 +6,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/sdk"
|
||||
"fiatjaf.com/nostr/sdk/hints/badgerh"
|
||||
"fiatjaf.com/nostr/sdk/hints/bbolth"
|
||||
"github.com/fatih/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -21,7 +20,7 @@ var (
|
||||
func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
||||
configPath := c.String("config-path")
|
||||
if configPath != "" {
|
||||
hintsFilePath = filepath.Join(configPath, "outbox/hints.bg")
|
||||
hintsFilePath = filepath.Join(configPath, "outbox/hints.db")
|
||||
}
|
||||
if hintsFilePath != "" {
|
||||
if _, err := os.Stat(hintsFilePath); err == nil {
|
||||
@@ -31,7 +30,7 @@ func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
||||
}
|
||||
}
|
||||
if hintsFileExists && hintsFilePath != "" {
|
||||
hintsdb, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||
hintsdb, err := bbolth.NewBoltHints(hintsFilePath)
|
||||
if err == nil {
|
||||
sys.Hints = hintsdb
|
||||
}
|
||||
@@ -58,9 +57,9 @@ var outbox = &cli.Command{
|
||||
}
|
||||
|
||||
os.MkdirAll(hintsFilePath, 0755)
|
||||
_, err := badgerh.NewBadgerHints(hintsFilePath)
|
||||
_, err := bbolth.NewBoltHints(hintsFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create badger hints db at '%s': %w", hintsFilePath, err)
|
||||
return fmt.Errorf("failed to create bolt hints db at '%s': %w", hintsFilePath, err)
|
||||
}
|
||||
|
||||
log("initialized hints database at %s\n", hintsFilePath)
|
||||
@@ -82,7 +81,7 @@ var outbox = &cli.Command{
|
||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||
}
|
||||
|
||||
pk, err := nostr.PubKeyFromHex(c.Args().First())
|
||||
pk, err := parsePubKey(c.Args().First())
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||
}
|
||||
|
||||
177
relay.go
177
relay.go
@@ -1,30 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip11"
|
||||
"fiatjaf.com/nostr/nip86"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var relay = &cli.Command{
|
||||
Name: "relay",
|
||||
Usage: "gets the relay information document for the given relay, as JSON -- or allows usage of the relay management API.",
|
||||
Description: `examples:
|
||||
fetching relay information:
|
||||
Usage: "gets the relay information document for the given relay, as JSON",
|
||||
Description: `
|
||||
nak relay nostr.wine
|
||||
|
||||
managing a relay
|
||||
nak relay nostr.wine banevent --sec 1234 --id 037eb3751073770ff17483b1b1ff125866cd5147668271975ef0a8a8e7ee184a --reason "I don't like it"`,
|
||||
`,
|
||||
ArgsUsage: "<relay-url>",
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
@@ -44,164 +33,4 @@ var relay = &cli.Command{
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Commands: (func() []*cli.Command {
|
||||
methods := []struct {
|
||||
method string
|
||||
args []string
|
||||
}{
|
||||
{"allowpubkey", []string{"pubkey", "reason"}},
|
||||
{"banpubkey", []string{"pubkey", "reason"}},
|
||||
{"listallowedpubkeys", nil},
|
||||
{"allowpubkey", []string{"pubkey", "reason"}},
|
||||
{"listallowedpubkeys", nil},
|
||||
{"listeventsneedingmoderation", nil},
|
||||
{"allowevent", []string{"id", "reason"}},
|
||||
{"banevent", []string{"id", "reason"}},
|
||||
{"listbannedevents", nil},
|
||||
{"changerelayname", []string{"name"}},
|
||||
{"changerelaydescription", []string{"description"}},
|
||||
{"changerelayicon", []string{"icon"}},
|
||||
{"allowkind", []string{"kind"}},
|
||||
{"disallowkind", []string{"kind"}},
|
||||
{"listallowedkinds", nil},
|
||||
{"blockip", []string{"ip", "reason"}},
|
||||
{"unblockip", []string{"ip", "reason"}},
|
||||
{"listblockedips", nil},
|
||||
}
|
||||
|
||||
commands := make([]*cli.Command, 0, len(methods))
|
||||
for _, def := range methods {
|
||||
def := def
|
||||
|
||||
flags := make([]cli.Flag, len(def.args), len(def.args)+4)
|
||||
for i, argName := range def.args {
|
||||
flags[i] = declareFlag(argName)
|
||||
}
|
||||
|
||||
flags = append(flags, defaultKeyFlags...)
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: def.method,
|
||||
Usage: fmt.Sprintf(`the "%s" relay management RPC call`, def.method),
|
||||
Description: fmt.Sprintf(
|
||||
`the "%s" management RPC call, see https://nips.nostr.com/86 for more information`, def.method),
|
||||
Flags: flags,
|
||||
DisableSliceFlagSeparator: true,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
params := make([]any, len(def.args))
|
||||
for i, argName := range def.args {
|
||||
params[i] = getArgument(c, argName)
|
||||
}
|
||||
req := nip86.Request{Method: def.method, Params: params}
|
||||
reqj, _ := json.Marshal(req)
|
||||
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) == 0 {
|
||||
stdout(string(reqj))
|
||||
return nil
|
||||
}
|
||||
|
||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, relayUrl := range relayUrls {
|
||||
httpUrl := "http" + nostr.NormalizeURL(relayUrl)[2:]
|
||||
log("calling '%s' on %s... ", def.method, httpUrl)
|
||||
body := bytes.NewBuffer(nil)
|
||||
body.Write(reqj)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", httpUrl, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Authorization
|
||||
payloadHash := sha256.Sum256(reqj)
|
||||
tokenEvent := nostr.Event{
|
||||
Kind: 27235,
|
||||
CreatedAt: nostr.Now(),
|
||||
Tags: nostr.Tags{
|
||||
{"u", httpUrl},
|
||||
{"method", "POST"},
|
||||
{"payload", hex.EncodeToString(payloadHash[:])},
|
||||
},
|
||||
}
|
||||
if err := kr.SignEvent(ctx, &tokenEvent); err != nil {
|
||||
return fmt.Errorf("failed to sign token event: %w", err)
|
||||
}
|
||||
evtj, _ := json.Marshal(tokenEvent)
|
||||
req.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evtj))
|
||||
|
||||
// Content-Type
|
||||
req.Header.Set("Content-Type", "application/nostr+json+rpc")
|
||||
|
||||
// make request to relay
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log("failed: %s\n", err)
|
||||
continue
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log("failed to read response: %s\n", err)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode >= 300 {
|
||||
log("failed with status %d\n", resp.StatusCode)
|
||||
bodyPrintable := string(b)
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
continue
|
||||
}
|
||||
var response nip86.Response
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
log("bad json response: %s\n", err)
|
||||
bodyPrintable := string(b)
|
||||
if len(bodyPrintable) > 300 {
|
||||
bodyPrintable = bodyPrintable[0:297] + "..."
|
||||
}
|
||||
log(bodyPrintable)
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// print the result
|
||||
log("\n")
|
||||
pretty, _ := json.MarshalIndent(response, "", " ")
|
||||
stdout(string(pretty))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
|
||||
return commands
|
||||
})(),
|
||||
}
|
||||
|
||||
func declareFlag(argName string) cli.Flag {
|
||||
usage := "parameter for this management RPC call, see https://nips.nostr.com/86 for more information."
|
||||
switch argName {
|
||||
case "kind":
|
||||
return &cli.IntFlag{Name: argName, Required: true, Usage: usage}
|
||||
case "reason":
|
||||
return &cli.StringFlag{Name: argName, Usage: usage}
|
||||
default:
|
||||
return &cli.StringFlag{Name: argName, Required: true, Usage: usage}
|
||||
}
|
||||
}
|
||||
|
||||
func getArgument(c *cli.Command, argName string) any {
|
||||
switch argName {
|
||||
case "kind":
|
||||
return c.Int(argName)
|
||||
default:
|
||||
return c.String(argName)
|
||||
}
|
||||
}
|
||||
|
||||
236
req.go
236
req.go
@@ -1,17 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||
"fiatjaf.com/nostr/eventstore/wrappers"
|
||||
"fiatjaf.com/nostr/nip42"
|
||||
"fiatjaf.com/nostr/nip77"
|
||||
"github.com/fatih/color"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,6 +43,11 @@ example:
|
||||
DisableSliceFlagSeparator: true,
|
||||
Flags: append(defaultKeyFlags,
|
||||
append(reqFilterFlags,
|
||||
&cli.StringFlag{
|
||||
Name: "only-missing",
|
||||
Usage: "use nip77 negentropy to only fetch events that aren't present in the given jsonl file",
|
||||
TakesFile: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ids-only",
|
||||
Usage: "use nip77 to fetch just a list of ids",
|
||||
@@ -44,6 +57,17 @@ example:
|
||||
Usage: "keep the subscription open, printing all events as they are returned",
|
||||
DefaultText: "false, will close on EOSE",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "outbox",
|
||||
Usage: "use outbox relays from specified public keys",
|
||||
DefaultText: "false, will only use manually-specified relays",
|
||||
},
|
||||
&cli.UintFlag{
|
||||
Name: "outbox-relays-per-pubkey",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "number of outbox relays to use for each pubkey",
|
||||
Value: 3,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "paginate",
|
||||
Usage: "make multiple REQs to the relay decreasing the value of 'until' until 'limit' or 'since' conditions are met",
|
||||
@@ -76,8 +100,23 @@ example:
|
||||
),
|
||||
ArgsUsage: "[relay...]",
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
negentropy := c.Bool("ids-only") || c.IsSet("only-missing")
|
||||
if negentropy {
|
||||
if c.Bool("paginate") || c.Bool("stream") || c.Bool("outbox") {
|
||||
return fmt.Errorf("negentropy is incompatible with --stream, --outbox or --paginate")
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("paginate") && c.Bool("stream") {
|
||||
return fmt.Errorf("incompatible flags --paginate and --stream")
|
||||
}
|
||||
|
||||
if c.Bool("paginate") && c.Bool("outbox") {
|
||||
return fmt.Errorf("incompatible flags --paginate and --outbox")
|
||||
}
|
||||
|
||||
relayUrls := c.Args().Slice()
|
||||
if len(relayUrls) > 0 {
|
||||
if len(relayUrls) > 0 && !negentropy {
|
||||
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
||||
// connect to all relays we expect to use in this call in parallel
|
||||
forcePreAuthSigner := authSigner
|
||||
@@ -129,35 +168,163 @@ example:
|
||||
return err
|
||||
}
|
||||
|
||||
if len(relayUrls) > 0 {
|
||||
if c.Bool("ids-only") {
|
||||
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
||||
for _, url := range relayUrls {
|
||||
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
||||
if len(relayUrls) > 0 || c.Bool("outbox") {
|
||||
if negentropy {
|
||||
store := &slicestore.SliceStore{}
|
||||
store.Init()
|
||||
|
||||
if syncFile := c.String("only-missing"); syncFile != "" {
|
||||
file, err := os.Open(syncFile)
|
||||
if err != nil {
|
||||
log("negentropy call to %s failed: %s", url, err)
|
||||
continue
|
||||
return fmt.Errorf("failed to open sync file: %w", err)
|
||||
}
|
||||
for id := range ch {
|
||||
if _, ok := seen[id]; ok {
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||
for scanner.Scan() {
|
||||
var evt nostr.Event
|
||||
if err := easyjson.Unmarshal([]byte(scanner.Text()), &evt); err != nil {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
stdout(id)
|
||||
if err := store.SaveEvent(evt); err != nil || err == eventstore.ErrDupEvent {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("failed to read sync file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
target := PrintingQuerierPublisher{
|
||||
QuerierPublisher: wrappers.StorePublisher{Store: store, MaxLimit: math.MaxInt},
|
||||
}
|
||||
|
||||
var source nostr.Querier = nil
|
||||
if c.IsSet("only-missing") {
|
||||
source = target
|
||||
}
|
||||
|
||||
handle := nip77.SyncEventsFromIDs
|
||||
|
||||
if c.Bool("ids-only") {
|
||||
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
||||
handle = func(ctx context.Context, dir nip77.Direction) {
|
||||
for id := range dir.Items {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
stdout(id.Hex())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, url := range relayUrls {
|
||||
err := nip77.NegentropySync(ctx, url, filter, source, target, handle)
|
||||
if err != nil {
|
||||
log("negentropy sync from %s failed: %s", url, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fn := sys.Pool.FetchMany
|
||||
if c.Bool("paginate") {
|
||||
fn = sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval"))
|
||||
} else if c.Bool("stream") {
|
||||
fn = sys.Pool.SubscribeMany
|
||||
var results chan nostr.RelayEvent
|
||||
var closeds chan nostr.RelayClosed
|
||||
|
||||
opts := nostr.SubscriptionOptions{
|
||||
Label: "nak-req",
|
||||
}
|
||||
|
||||
for ie := range fn(ctx, relayUrls, filter, nostr.SubscriptionOptions{
|
||||
Label: "nak-req",
|
||||
}) {
|
||||
stdout(ie.Event)
|
||||
if c.Bool("paginate") {
|
||||
paginator := sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval"))
|
||||
results = paginator(ctx, relayUrls, filter, opts)
|
||||
} else if c.Bool("outbox") {
|
||||
defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2)
|
||||
|
||||
// hardcoded relays, if any
|
||||
for _, relayUrl := range relayUrls {
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: relayUrl,
|
||||
})
|
||||
}
|
||||
|
||||
// relays for each pubkey
|
||||
errg := errgroup.Group{}
|
||||
errg.SetLimit(16)
|
||||
mu := sync.Mutex{}
|
||||
for _, pubkey := range filter.Authors {
|
||||
errg.Go(func() error {
|
||||
n := int(c.Uint("outbox-relays-per-pubkey"))
|
||||
for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) {
|
||||
if slices.Contains(relayUrls, url) {
|
||||
// already hardcoded, ignore
|
||||
continue
|
||||
}
|
||||
if !nostr.IsValidRelayURL(url) {
|
||||
continue
|
||||
}
|
||||
|
||||
matchUrl := func(def nostr.DirectedFilter) bool { return def.Relay == url }
|
||||
idx := slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
// new relay, add it
|
||||
mu.Lock()
|
||||
// check again after locking to prevent races
|
||||
idx = slices.IndexFunc(defs, matchUrl)
|
||||
if idx == -1 {
|
||||
// then add it
|
||||
filter := filter.Clone()
|
||||
filter.Authors = []nostr.PubKey{pubkey}
|
||||
defs = append(defs, nostr.DirectedFilter{
|
||||
Filter: filter,
|
||||
Relay: url,
|
||||
})
|
||||
mu.Unlock()
|
||||
continue // done with this relay url
|
||||
}
|
||||
|
||||
// otherwise we'll just use the idx
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// existing relay, add this pubkey
|
||||
defs[idx].Authors = append(defs[idx].Authors, pubkey)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
errg.Wait()
|
||||
|
||||
if c.Bool("stream") {
|
||||
results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts)
|
||||
} else {
|
||||
results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts)
|
||||
}
|
||||
} else {
|
||||
if c.Bool("stream") {
|
||||
results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts)
|
||||
} else {
|
||||
results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts)
|
||||
}
|
||||
}
|
||||
|
||||
readevents:
|
||||
for {
|
||||
select {
|
||||
case ie, ok := <-results:
|
||||
if !ok {
|
||||
break readevents
|
||||
}
|
||||
stdout(ie.Event)
|
||||
case closed := <-closeds:
|
||||
if closed.HandledAuth {
|
||||
logverbose("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason)
|
||||
} else {
|
||||
log("%s CLOSED: %s\n", closed.Relay.URL, closed.Reason)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
break readevents
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -183,13 +350,13 @@ var reqFilterFlags = []cli.Flag{
|
||||
&PubKeySliceFlag{
|
||||
Name: "author",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "only accept events from these authors (pubkey as hex)",
|
||||
Usage: "only accept events from these authors",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&IDSliceFlag{
|
||||
Name: "id",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "only accept events with these ids (hex)",
|
||||
Usage: "only accept events with these ids",
|
||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||
},
|
||||
&cli.IntSliceFlag{
|
||||
@@ -261,19 +428,19 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||
for _, tagFlag := range c.StringSlice("tag") {
|
||||
spl := strings.SplitN(tagFlag, "=", 2)
|
||||
if len(spl) == 2 {
|
||||
tags = append(tags, spl)
|
||||
tags = append(tags, []string{spl[0], decodeTagValue(spl[1])})
|
||||
} else {
|
||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||
}
|
||||
}
|
||||
for _, etag := range c.StringSlice("e") {
|
||||
tags = append(tags, []string{"e", etag})
|
||||
tags = append(tags, []string{"e", decodeTagValue(etag)})
|
||||
}
|
||||
for _, ptag := range c.StringSlice("p") {
|
||||
tags = append(tags, []string{"p", ptag})
|
||||
tags = append(tags, []string{"p", decodeTagValue(ptag)})
|
||||
}
|
||||
for _, dtag := range c.StringSlice("d") {
|
||||
tags = append(tags, []string{"d", dtag})
|
||||
tags = append(tags, []string{"d", decodeTagValue(dtag)})
|
||||
}
|
||||
|
||||
if len(tags) > 0 && filter.Tags == nil {
|
||||
@@ -302,3 +469,18 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PrintingQuerierPublisher struct {
|
||||
nostr.QuerierPublisher
|
||||
}
|
||||
|
||||
func (p PrintingQuerierPublisher) Publish(ctx context.Context, evt nostr.Event) error {
|
||||
if err := p.QuerierPublisher.Publish(ctx, evt); err == nil {
|
||||
stdout(evt)
|
||||
return nil
|
||||
} else if err == eventstore.ErrDupEvent {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
129
serve.go
129
serve.go
@@ -2,17 +2,24 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||
"fiatjaf.com/nostr/khatru"
|
||||
"fiatjaf.com/nostr/khatru/blossom"
|
||||
"fiatjaf.com/nostr/khatru/grasp"
|
||||
"github.com/bep/debounce"
|
||||
"github.com/fatih/color"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -36,10 +43,31 @@ var serve = &cli.Command{
|
||||
Usage: "file containing the initial batch of events that will be served by the relay as newline-separated JSON (jsonl)",
|
||||
DefaultText: "the relay will start empty",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "negentropy",
|
||||
Usage: "enable negentropy syncing",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "grasp",
|
||||
Usage: "enable grasp server",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "grasp-path",
|
||||
Usage: "where to store the repositories",
|
||||
TakesFile: true,
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "blossom",
|
||||
Usage: "enable blossom server",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
db := &slicestore.SliceStore{}
|
||||
|
||||
var blobStore *xsync.MapOf[string, []byte]
|
||||
var repoDir string
|
||||
|
||||
var scanner *bufio.Scanner
|
||||
if path := c.String("events"); path != "" {
|
||||
f, err := os.Open(path)
|
||||
@@ -71,7 +99,11 @@ var serve = &cli.Command{
|
||||
rl.Info.Software = "https://github.com/fiatjaf/nak"
|
||||
rl.Info.Version = version
|
||||
|
||||
rl.UseEventstore(db, 1_000_000)
|
||||
rl.UseEventstore(db, 500)
|
||||
|
||||
if c.Bool("negentropy") {
|
||||
rl.Negentropy = true
|
||||
}
|
||||
|
||||
started := make(chan bool)
|
||||
exited := make(chan error)
|
||||
@@ -79,16 +111,70 @@ var serve = &cli.Command{
|
||||
hostname := c.String("hostname")
|
||||
port := int(c.Uint("port"))
|
||||
|
||||
var printStatus func()
|
||||
|
||||
if c.Bool("blossom") {
|
||||
bs := blossom.New(rl, fmt.Sprintf("http://%s:%d", hostname, port))
|
||||
bs.Store = blossom.NewMemoryBlobIndex()
|
||||
|
||||
blobStore = xsync.NewMapOf[string, []byte]()
|
||||
bs.StoreBlob = func(ctx context.Context, sha256 string, ext string, body []byte) error {
|
||||
blobStore.Store(sha256+ext, body)
|
||||
log(" got %s %s\n", color.GreenString("blob stored"), sha256+ext)
|
||||
printStatus()
|
||||
return nil
|
||||
}
|
||||
bs.LoadBlob = func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, *url.URL, error) {
|
||||
if body, ok := blobStore.Load(sha256 + ext); ok {
|
||||
log(" got %s %s\n", color.BlueString("blob downloaded"), sha256+ext)
|
||||
printStatus()
|
||||
return bytes.NewReader(body), nil, nil
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
bs.DeleteBlob = func(ctx context.Context, sha256 string, ext string) error {
|
||||
blobStore.Delete(sha256 + ext)
|
||||
log(" got %s %s\n", color.RedString("blob deleted"), sha256+ext)
|
||||
printStatus()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("grasp") {
|
||||
repoDir = c.String("grasp-path")
|
||||
if repoDir == "" {
|
||||
var err error
|
||||
repoDir, err = os.MkdirTemp("", "nak-serve-grasp-repos-")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create grasp repos directory: %w", err)
|
||||
}
|
||||
}
|
||||
g := grasp.New(rl, repoDir)
|
||||
g.OnRead = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) {
|
||||
log(" got %s %s %s\n", color.CyanString("git read"), pubkey.Hex(), repo)
|
||||
printStatus()
|
||||
return false, ""
|
||||
}
|
||||
g.OnWrite = func(ctx context.Context, pubkey nostr.PubKey, repo string) (reject bool, reason string) {
|
||||
log(" got %s %s %s\n", color.YellowString("git write"), pubkey.Hex(), repo)
|
||||
printStatus()
|
||||
return false, ""
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := rl.Start(hostname, port, started)
|
||||
exited <- err
|
||||
}()
|
||||
|
||||
var printStatus func()
|
||||
|
||||
// relay logging
|
||||
rl.OnRequest = func(ctx context.Context, filter nostr.Filter) (reject bool, msg string) {
|
||||
log(" got %s %v\n", color.HiYellowString("request"), colors.italic(filter))
|
||||
negentropy := ""
|
||||
if khatru.IsNegentropySession(ctx) {
|
||||
negentropy = color.HiBlueString("negentropy ")
|
||||
}
|
||||
|
||||
log(" got %s%s %v\n", negentropy, color.HiYellowString("request"), colors.italic(filter))
|
||||
printStatus()
|
||||
return false, ""
|
||||
}
|
||||
@@ -123,9 +209,36 @@ var serve = &cli.Command{
|
||||
}
|
||||
subs := rl.GetListeningFilters()
|
||||
|
||||
log(" %s events: %s, connections: %s, subscriptions: %s\n",
|
||||
blossomMsg := ""
|
||||
if c.Bool("blossom") {
|
||||
blobsStored := blobStore.Size()
|
||||
blossomMsg = fmt.Sprintf("blobs: %s, ",
|
||||
color.HiMagentaString("%d", blobsStored),
|
||||
)
|
||||
}
|
||||
|
||||
graspMsg := ""
|
||||
if c.Bool("grasp") {
|
||||
gitAnnounced := 0
|
||||
gitStored := 0
|
||||
for evt := range db.QueryEvents(nostr.Filter{Kinds: []nostr.Kind{nostr.Kind(30617)}}, 500) {
|
||||
gitAnnounced++
|
||||
identifier := evt.Tags.GetD()
|
||||
if info, err := os.Stat(filepath.Join(repoDir, identifier)); err == nil && info.IsDir() {
|
||||
gitStored++
|
||||
}
|
||||
}
|
||||
graspMsg = fmt.Sprintf("git announced: %s, git stored: %s, ",
|
||||
color.HiMagentaString("%d", gitAnnounced),
|
||||
color.HiMagentaString("%d", gitStored),
|
||||
)
|
||||
}
|
||||
|
||||
log(" %s events: %s, %s%sconnections: %s, subscriptions: %s\n",
|
||||
color.HiMagentaString("•"),
|
||||
color.HiMagentaString("%d", totalEvents),
|
||||
blossomMsg,
|
||||
graspMsg,
|
||||
color.HiMagentaString("%d", totalConnections.Load()),
|
||||
color.HiMagentaString("%d", len(subs)),
|
||||
)
|
||||
@@ -133,7 +246,11 @@ var serve = &cli.Command{
|
||||
}
|
||||
|
||||
<-started
|
||||
log("%s relay running at %s\n", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port))
|
||||
log("%s relay running at %s", color.HiRedString(">"), colors.boldf("ws://%s:%d", hostname, port))
|
||||
if c.Bool("grasp") {
|
||||
log(" (grasp repos at %s)", repoDir)
|
||||
}
|
||||
log("\n")
|
||||
|
||||
return <-exited
|
||||
},
|
||||
|
||||
464
sync.go
Normal file
464
sync.go
Normal file
@@ -0,0 +1,464 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/nip77"
|
||||
"fiatjaf.com/nostr/nip77/negentropy"
|
||||
"fiatjaf.com/nostr/nip77/negentropy/storage"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var syncCmd = &cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "sync events between two relays using negentropy",
|
||||
Description: `uses nip77 negentropy to sync events between two relays`,
|
||||
ArgsUsage: "<relay1> <relay2>",
|
||||
Flags: reqFilterFlags,
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
args := c.Args().Slice()
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("need exactly two relay URLs: source and target")
|
||||
}
|
||||
|
||||
filter := nostr.Filter{}
|
||||
if err := applyFlagsToFilter(c, &filter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerA, err := NewRelayThirdPartyRemote(ctx, args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up %s: %w", args[0], err)
|
||||
}
|
||||
|
||||
peerB, err := NewRelayThirdPartyRemote(ctx, args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up %s: %w", args[1], err)
|
||||
}
|
||||
|
||||
tpn := NewThirdPartyNegentropy(
|
||||
peerA,
|
||||
peerB,
|
||||
filter,
|
||||
)
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
wg.Go(func() {
|
||||
err = tpn.Run(ctx)
|
||||
})
|
||||
|
||||
wg.Go(func() {
|
||||
type op struct {
|
||||
src *nostr.Relay
|
||||
dst *nostr.Relay
|
||||
ids []nostr.ID
|
||||
}
|
||||
|
||||
pending := []op{
|
||||
{peerA.relay, peerB.relay, make([]nostr.ID, 0, 30)},
|
||||
{peerB.relay, peerA.relay, make([]nostr.ID, 0, 30)},
|
||||
}
|
||||
|
||||
for delta := range tpn.Deltas {
|
||||
have := delta.Have.relay
|
||||
havenot := delta.HaveNot.relay
|
||||
logverbose("%s has %s, %s doesn't.\n", have.URL, delta.ID.Hex(), havenot.URL)
|
||||
|
||||
idx := 0 // peerA
|
||||
if have == peerB.relay {
|
||||
idx = 1 // peerB
|
||||
}
|
||||
pending[idx].ids = append(pending[idx].ids, delta.ID)
|
||||
|
||||
// every 30 ids do a fetch-and-publish
|
||||
if len(pending[idx].ids) == 30 {
|
||||
for evt := range pending[idx].src.QueryEvents(nostr.Filter{IDs: pending[idx].ids}) {
|
||||
pending[idx].dst.Publish(ctx, evt)
|
||||
}
|
||||
pending[idx].ids = pending[idx].ids[:0]
|
||||
}
|
||||
}
|
||||
|
||||
// do it for the remaining ids
|
||||
for _, op := range pending {
|
||||
if len(op.ids) > 0 {
|
||||
for evt := range op.src.QueryEvents(nostr.Filter{IDs: op.ids}) {
|
||||
op.dst.Publish(ctx, evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
type ThirdPartyNegentropy struct {
|
||||
PeerA *RelayThirdPartyRemote
|
||||
PeerB *RelayThirdPartyRemote
|
||||
Filter nostr.Filter
|
||||
|
||||
Deltas chan Delta
|
||||
}
|
||||
|
||||
type Delta struct {
|
||||
ID nostr.ID
|
||||
Have *RelayThirdPartyRemote
|
||||
HaveNot *RelayThirdPartyRemote
|
||||
}
|
||||
|
||||
type boundKey string
|
||||
|
||||
func getBoundKey(b negentropy.Bound) boundKey {
|
||||
return boundKey(fmt.Sprintf("%d:%x", b.Timestamp, b.IDPrefix))
|
||||
}
|
||||
|
||||
type RelayThirdPartyRemote struct {
|
||||
relay *nostr.Relay
|
||||
messages chan string
|
||||
err error
|
||||
}
|
||||
|
||||
func NewRelayThirdPartyRemote(ctx context.Context, url string) (*RelayThirdPartyRemote, error) {
|
||||
rtpr := &RelayThirdPartyRemote{
|
||||
messages: make(chan string, 3),
|
||||
}
|
||||
|
||||
var err error
|
||||
rtpr.relay, err = nostr.RelayConnect(ctx, url, nostr.RelayOptions{
|
||||
CustomHandler: func(data string) {
|
||||
envelope := nip77.ParseNegMessage(data)
|
||||
if envelope == nil {
|
||||
return
|
||||
}
|
||||
switch env := envelope.(type) {
|
||||
case *nip77.OpenEnvelope, *nip77.CloseEnvelope:
|
||||
rtpr.err = fmt.Errorf("unexpected %s received from relay", env.Label())
|
||||
return
|
||||
case *nip77.ErrorEnvelope:
|
||||
rtpr.err = fmt.Errorf("relay returned a %s: %s", env.Label(), env.Reason)
|
||||
return
|
||||
case *nip77.MessageEnvelope:
|
||||
rtpr.messages <- env.Message
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rtpr, nil
|
||||
}
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) SendInitialMessage(filter nostr.Filter, msg string) error {
|
||||
msgj, _ := json.Marshal(nip77.OpenEnvelope{
|
||||
SubscriptionID: "sync3",
|
||||
Filter: filter,
|
||||
Message: msg,
|
||||
})
|
||||
return rtpr.relay.WriteWithError(msgj)
|
||||
}
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) SendMessage(msg string) error {
|
||||
msgj, _ := json.Marshal(nip77.MessageEnvelope{
|
||||
SubscriptionID: "sync3",
|
||||
Message: msg,
|
||||
})
|
||||
return rtpr.relay.WriteWithError(msgj)
|
||||
}
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) SendClose() error {
|
||||
msgj, _ := json.Marshal(nip77.CloseEnvelope{
|
||||
SubscriptionID: "sync3",
|
||||
})
|
||||
return rtpr.relay.WriteWithError(msgj)
|
||||
}
|
||||
|
||||
var thirdPartyRemoteEndOfMessages = errors.New("the-end")
|
||||
|
||||
func (rtpr *RelayThirdPartyRemote) Receive() (string, error) {
|
||||
if rtpr.err != nil {
|
||||
return "", rtpr.err
|
||||
}
|
||||
if msg, ok := <-rtpr.messages; ok {
|
||||
return msg, nil
|
||||
}
|
||||
return "", thirdPartyRemoteEndOfMessages
|
||||
}
|
||||
|
||||
func NewThirdPartyNegentropy(peerA, peerB *RelayThirdPartyRemote, filter nostr.Filter) *ThirdPartyNegentropy {
|
||||
return &ThirdPartyNegentropy{
|
||||
PeerA: peerA,
|
||||
PeerB: peerB,
|
||||
Filter: filter,
|
||||
Deltas: make(chan Delta, 100),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *ThirdPartyNegentropy) Run(ctx context.Context) error {
|
||||
peerAIds := make(map[nostr.ID]struct{})
|
||||
peerBIds := make(map[nostr.ID]struct{})
|
||||
peerASkippedBounds := make(map[boundKey]struct{})
|
||||
peerBSkippedBounds := make(map[boundKey]struct{})
|
||||
|
||||
// send an empty message to A to start things up
|
||||
initialMsg := createInitialMessage()
|
||||
err := n.PeerA.SendInitialMessage(n.Filter, initialMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasSentInitialMessageToB := false
|
||||
|
||||
for {
|
||||
// receive message from A
|
||||
msgA, err := n.PeerA.Receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgAb, _ := nostr.HexDecodeString(msgA)
|
||||
if len(msgAb) == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
msgToB, err := parseMessageBuildNext(
|
||||
msgA,
|
||||
peerBSkippedBounds,
|
||||
func(id nostr.ID) {
|
||||
if _, exists := peerBIds[id]; exists {
|
||||
delete(peerBIds, id)
|
||||
} else {
|
||||
peerAIds[id] = struct{}{}
|
||||
}
|
||||
},
|
||||
func(boundKey boundKey) {
|
||||
peerASkippedBounds[boundKey] = struct{}{}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// emit deltas from B after receiving message from A
|
||||
for id := range peerBIds {
|
||||
select {
|
||||
case n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}:
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
delete(peerBIds, id)
|
||||
}
|
||||
|
||||
if len(msgToB) == 2 {
|
||||
// exit condition (no more messages to send)
|
||||
break
|
||||
}
|
||||
|
||||
// send message to B
|
||||
if hasSentInitialMessageToB {
|
||||
err = n.PeerB.SendMessage(msgToB)
|
||||
} else {
|
||||
err = n.PeerB.SendInitialMessage(n.Filter, msgToB)
|
||||
hasSentInitialMessageToB = true
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// receive message from B
|
||||
msgB, err := n.PeerB.Receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msgBb, _ := nostr.HexDecodeString(msgB)
|
||||
if len(msgBb) == 1 {
|
||||
break
|
||||
}
|
||||
|
||||
msgToA, err := parseMessageBuildNext(
|
||||
msgB,
|
||||
peerASkippedBounds,
|
||||
func(id nostr.ID) {
|
||||
if _, exists := peerAIds[id]; exists {
|
||||
delete(peerAIds, id)
|
||||
} else {
|
||||
peerBIds[id] = struct{}{}
|
||||
}
|
||||
},
|
||||
func(boundKey boundKey) {
|
||||
peerBSkippedBounds[boundKey] = struct{}{}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// emit deltas from A after receiving message from B
|
||||
for id := range peerAIds {
|
||||
select {
|
||||
case n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}:
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
delete(peerAIds, id)
|
||||
}
|
||||
|
||||
if len(msgToA) == 2 {
|
||||
// exit condition (no more messages to send)
|
||||
break
|
||||
}
|
||||
|
||||
// send message to A
|
||||
err = n.PeerA.SendMessage(msgToA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// emit remaining deltas before exit
|
||||
for id := range peerAIds {
|
||||
select {
|
||||
case n.Deltas <- Delta{ID: id, Have: n.PeerA, HaveNot: n.PeerB}:
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
for id := range peerBIds {
|
||||
select {
|
||||
case n.Deltas <- Delta{ID: id, Have: n.PeerB, HaveNot: n.PeerA}:
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
n.PeerA.SendClose()
|
||||
n.PeerB.SendClose()
|
||||
close(n.Deltas)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createInitialMessage() string {
|
||||
output := bytes.NewBuffer(make([]byte, 0, 64))
|
||||
output.WriteByte(negentropy.ProtocolVersion)
|
||||
|
||||
dummy := negentropy.BoundWriter{}
|
||||
dummy.WriteBound(output, negentropy.InfiniteBound)
|
||||
output.WriteByte(byte(negentropy.FingerprintMode))
|
||||
|
||||
// hardcoded random fingerprint
|
||||
fingerprint := [negentropy.FingerprintSize]byte{
|
||||
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||
}
|
||||
output.Write(fingerprint[:])
|
||||
|
||||
return nostr.HexEncodeToString(output.Bytes())
|
||||
}
|
||||
|
||||
func parseMessageBuildNext(
|
||||
msg string,
|
||||
skippedBounds map[boundKey]struct{},
|
||||
idCallback func(id nostr.ID),
|
||||
skipCallback func(boundKey boundKey),
|
||||
) (string, error) {
|
||||
msgb, err := nostr.HexDecodeString(msg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
br := &negentropy.BoundReader{}
|
||||
bw := &negentropy.BoundWriter{}
|
||||
|
||||
nextMsg := bytes.NewBuffer(make([]byte, 0, len(msgb)))
|
||||
acc := &storage.Accumulator{} // this will be used for building our own fingerprints and also as a placeholder
|
||||
|
||||
reader := bytes.NewReader(msgb)
|
||||
pv, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if pv != negentropy.ProtocolVersion {
|
||||
return "", fmt.Errorf("unsupported protocol version %v", pv)
|
||||
}
|
||||
|
||||
nextMsg.WriteByte(pv)
|
||||
|
||||
for reader.Len() > 0 {
|
||||
bound, err := br.ReadBound(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
modeVal, err := negentropy.ReadVarInt(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
mode := negentropy.Mode(modeVal)
|
||||
|
||||
switch mode {
|
||||
case negentropy.SkipMode:
|
||||
skipCallback(getBoundKey(bound))
|
||||
if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped {
|
||||
bw.WriteBound(nextMsg, bound)
|
||||
negentropy.WriteVarInt(nextMsg, int(negentropy.SkipMode))
|
||||
}
|
||||
|
||||
case negentropy.FingerprintMode:
|
||||
_, err = reader.Read(acc.Buf[0:negentropy.FingerprintSize] /* use this buffer as a dummy */)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped {
|
||||
bw.WriteBound(nextMsg, bound)
|
||||
negentropy.WriteVarInt(nextMsg, int(negentropy.FingerprintMode))
|
||||
nextMsg.Write(acc.Buf[0:negentropy.FingerprintSize] /* idem */)
|
||||
}
|
||||
case negentropy.IdListMode:
|
||||
// when receiving an idlist we will never send this bound again to this peer
|
||||
skipCallback(getBoundKey(bound))
|
||||
|
||||
// and instead of sending these ids to the other peer we'll send a fingerprint
|
||||
acc.Reset()
|
||||
|
||||
numIds, err := negentropy.ReadVarInt(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for range numIds {
|
||||
id := nostr.ID{}
|
||||
|
||||
_, err = reader.Read(id[:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
idCallback(id)
|
||||
|
||||
acc.AddBytes(id[:])
|
||||
}
|
||||
|
||||
if _, skipped := skippedBounds[getBoundKey(bound)]; !skipped {
|
||||
fingerprint := acc.GetFingerprint(numIds)
|
||||
|
||||
bw.WriteBound(nextMsg, bound)
|
||||
negentropy.WriteVarInt(nextMsg, int(negentropy.FingerprintMode))
|
||||
nextMsg.Write(fingerprint[:])
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unknown mode %v", mode)
|
||||
}
|
||||
}
|
||||
|
||||
return nostr.HexEncodeToString(nextMsg.Bytes()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user