mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-09 17:18:50 +00:00
Compare commits
4 Commits
v0.17.2
...
789c6a3884
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
789c6a3884 | ||
|
|
eb6fdfdd39 | ||
|
|
2b189756d1 | ||
|
|
530b484662 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
nak
|
nak
|
||||||
mnt
|
mnt
|
||||||
nak.exe
|
nak.exe
|
||||||
|
qtbox
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -324,21 +324,6 @@ echo "#surely you're joking, mr npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn6
|
|||||||
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
|
ffmpeg -f alsa -i default -f webm -t 00:00:03 pipe:1 | nak blossom --server blossom.primal.net upload | jq -rc '{content: .url}' | nak event -k 1222 --sec 'bunker://urlgoeshere' pyramid.fiatjaf.com nostr.wine
|
||||||
```
|
```
|
||||||
|
|
||||||
### gift-wrap an event to a recipient and publish it somewhere
|
|
||||||
```shell
|
|
||||||
~> nak event -c 'secret message' | nak gift wrap --sec <my-secret-key> -p <recipient-public-key> | nak event wss://dmrelay.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### download a gift-wrap event and unwrap it
|
|
||||||
```shell
|
|
||||||
~> nak req -p <my-public-key> -k 1059 relay.com | nak gift unwrap --sec <my-secret-key> --from <sender-public-key>
|
|
||||||
```
|
|
||||||
|
|
||||||
### sync events between two relays using negentropy
|
|
||||||
```shell
|
|
||||||
~> nak sync relay1.com relay2.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### from a file with events get only those that have kind 1111 and were created by a given pubkey
|
### from a file with events get only those that have kind 1111 and were created by a given pubkey
|
||||||
```shell
|
```shell
|
||||||
~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl
|
~> cat all.jsonl | nak filter -k 1111 -a 117673e191b10fe1aedf1736ee74de4cffd4c132ca701960b70a5abad5870faa > filtered.jsonl
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ var encrypt = &cli.Command{
|
|||||||
defaultKeyFlags,
|
defaultKeyFlags,
|
||||||
&PubKeyFlag{
|
&PubKeyFlag{
|
||||||
Name: "recipient-pubkey",
|
Name: "recipient-pubkey",
|
||||||
Aliases: []string{"p", "tgt", "target", "pubkey", "to"},
|
Aliases: []string{"p", "tgt", "target", "pubkey"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -79,7 +79,7 @@ var decrypt = &cli.Command{
|
|||||||
defaultKeyFlags,
|
defaultKeyFlags,
|
||||||
&PubKeyFlag{
|
&PubKeyFlag{
|
||||||
Name: "sender-pubkey",
|
Name: "sender-pubkey",
|
||||||
Aliases: []string{"p", "src", "source", "pubkey", "from"},
|
Aliases: []string{"p", "src", "source", "pubkey"},
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
|
|||||||
192
gift.go
192
gift.go
@@ -1,192 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
525
git.go
525
git.go
@@ -8,7 +8,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip19"
|
"fiatjaf.com/nostr/nip19"
|
||||||
@@ -93,9 +92,6 @@ aside from those, there is also:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultOwner string
|
|
||||||
var defaultIdentifier string
|
|
||||||
|
|
||||||
// check if nip34.json already exists
|
// check if nip34.json already exists
|
||||||
existingConfig, err := readNip34ConfigFile("")
|
existingConfig, err := readNip34ConfigFile("")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -103,115 +99,44 @@ aside from those, there is also:
|
|||||||
if !c.Bool("force") && !c.Bool("interactive") {
|
if !c.Bool("force") && !c.Bool("interactive") {
|
||||||
return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update")
|
return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultIdentifier = existingConfig.Identifier
|
|
||||||
defaultOwner = existingConfig.Owner
|
|
||||||
} else {
|
|
||||||
// extract info from nostr:// git remotes (this is just for migrating from ngit)
|
|
||||||
if output, err := exec.Command("git", "remote", "-v").Output(); err == nil {
|
|
||||||
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
||||||
for _, remote := range remotes {
|
|
||||||
if strings.Contains(remote, "nostr://") {
|
|
||||||
parts := strings.Fields(remote)
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
nostrURL := parts[1]
|
|
||||||
// parse nostr://npub.../relay_hostname/identifier
|
|
||||||
if remoteOwner, remoteIdentifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 {
|
|
||||||
defaultIdentifier = remoteIdentifier
|
|
||||||
defaultOwner = nip19.EncodeNpub(remoteOwner)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get repository base directory name for defaults
|
// get repository base directory name for defaults
|
||||||
if defaultIdentifier == "" {
|
cwd, err := os.Getwd()
|
||||||
cwd, err := os.Getwd()
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to get current directory: %w", err)
|
||||||
return fmt.Errorf("failed to get current directory: %w", err)
|
}
|
||||||
|
baseName := filepath.Base(cwd)
|
||||||
|
|
||||||
|
// get earliest unique commit
|
||||||
|
var earliestCommit string
|
||||||
|
if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil {
|
||||||
|
earliest := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
if len(earliest) > 0 {
|
||||||
|
earliestCommit = earliest[0]
|
||||||
}
|
}
|
||||||
defaultIdentifier = filepath.Base(cwd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prompt for identifier first
|
// extract clone URLs from nostr:// git remotes
|
||||||
var identifier string
|
// (this is just for migrating from ngit)
|
||||||
if c.String("identifier") != "" {
|
var defaultCloneURLs []string
|
||||||
identifier = c.String("identifier")
|
if output, err := exec.Command("git", "remote", "-v").Output(); err == nil {
|
||||||
} else if c.Bool("interactive") {
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
if err := survey.AskOne(&survey.Input{
|
for _, remote := range remotes {
|
||||||
Message: "identifier",
|
if strings.Contains(remote, "nostr://") {
|
||||||
Default: defaultIdentifier,
|
parts := strings.Fields(remote)
|
||||||
}, &identifier); err != nil {
|
if len(parts) >= 2 {
|
||||||
return err
|
nostrURL := parts[1]
|
||||||
}
|
// parse nostr://npub.../relay_hostname/identifier
|
||||||
} else {
|
if owner, identifier, relays, err := parseRepositoryAddress(ctx, nostrURL); err == nil && len(relays) > 0 {
|
||||||
identifier = defaultIdentifier
|
relayURL := relays[0]
|
||||||
}
|
// convert to https://relay_hostname/npub.../identifier.git
|
||||||
|
cloneURL := fmt.Sprintf("http%s/%s/%s.git",
|
||||||
// prompt for owner pubkey
|
relayURL[2:], nip19.EncodeNpub(owner), identifier)
|
||||||
var owner nostr.PubKey
|
defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL)
|
||||||
var ownerStr string
|
}
|
||||||
if c.String("owner") != "" {
|
}
|
||||||
owner, err = parsePubKey(ownerStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid owner pubkey: %w", err)
|
|
||||||
}
|
|
||||||
ownerStr = nip19.EncodeNpub(owner)
|
|
||||||
} else if c.Bool("interactive") {
|
|
||||||
for {
|
|
||||||
if err := survey.AskOne(&survey.Input{
|
|
||||||
Message: "owner (npub or hex)",
|
|
||||||
Default: defaultOwner,
|
|
||||||
}, &ownerStr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
owner, err = parsePubKey(ownerStr)
|
|
||||||
if err == nil {
|
|
||||||
ownerStr = nip19.EncodeNpub(owner)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("owner pubkey is required (use --owner or --interactive)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to fetch existing repository announcement (kind 30617)
|
|
||||||
var fetchedRepo *nip34.Repository
|
|
||||||
if existingConfig.Identifier == "" {
|
|
||||||
log(" searching for existing events... ")
|
|
||||||
repo, _, _, err := fetchRepositoryAndState(ctx, owner, identifier, nil)
|
|
||||||
if err == nil && repo.Event.ID != nostr.ZeroID {
|
|
||||||
fetchedRepo = &repo
|
|
||||||
log("found one from %s.\n", repo.Event.CreatedAt.Time().Format(time.DateOnly))
|
|
||||||
} else {
|
|
||||||
log("none found.\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set config with fetched values or defaults
|
|
||||||
var config Nip34Config
|
|
||||||
if fetchedRepo != nil {
|
|
||||||
config = RepositoryToConfig(*fetchedRepo)
|
|
||||||
} else if existingConfig.Identifier != "" {
|
|
||||||
config = existingConfig
|
|
||||||
} else {
|
|
||||||
// get earliest unique commit
|
|
||||||
var earliestCommit string
|
|
||||||
if output, err := exec.Command("git", "rev-list", "--max-parents=0", "HEAD").Output(); err == nil {
|
|
||||||
earliestCommit = strings.TrimSpace(string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
config = Nip34Config{
|
|
||||||
Identifier: identifier,
|
|
||||||
Owner: ownerStr,
|
|
||||||
Name: identifier,
|
|
||||||
Description: "",
|
|
||||||
Web: []string{},
|
|
||||||
GraspServers: []string{"gitnostr.com", "relay.ngit.dev"},
|
|
||||||
EarliestUniqueCommit: earliestCommit,
|
|
||||||
Maintainers: []string{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,84 +161,21 @@ aside from those, there is also:
|
|||||||
return defaultVals
|
return defaultVals
|
||||||
}
|
}
|
||||||
|
|
||||||
// override with flags and existing config
|
config := Nip34Config{
|
||||||
config.Identifier = getValue(existingConfig.Identifier, c.String("identifier"), config.Identifier)
|
Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName),
|
||||||
config.Name = getValue(existingConfig.Name, c.String("name"), config.Name)
|
Name: getValue(existingConfig.Name, c.String("name"), baseName),
|
||||||
config.Description = getValue(existingConfig.Description, c.String("description"), config.Description)
|
Description: getValue(existingConfig.Description, c.String("description"), ""),
|
||||||
config.Web = getSliceValue(existingConfig.Web, c.StringSlice("web"), config.Web)
|
Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}),
|
||||||
config.Owner = getValue(existingConfig.Owner, c.String("owner"), config.Owner)
|
Owner: getValue(existingConfig.Owner, c.String("owner"), ""),
|
||||||
config.GraspServers = getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), config.GraspServers)
|
GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}),
|
||||||
config.EarliestUniqueCommit = getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), config.EarliestUniqueCommit)
|
EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit),
|
||||||
config.Maintainers = getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), config.Maintainers)
|
Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}),
|
||||||
|
}
|
||||||
|
|
||||||
if c.Bool("interactive") {
|
if c.Bool("interactive") {
|
||||||
// prompt for name
|
if err := promptForConfig(&config); err != nil {
|
||||||
if err := survey.AskOne(&survey.Input{
|
|
||||||
Message: "name",
|
|
||||||
Default: config.Name,
|
|
||||||
}, &config.Name); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// prompt for description
|
|
||||||
if err := survey.AskOne(&survey.Input{
|
|
||||||
Message: "description",
|
|
||||||
Default: config.Description,
|
|
||||||
}, &config.Description); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// prompt for grasp servers
|
|
||||||
graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{
|
|
||||||
"gitnostr.com",
|
|
||||||
"relay.ngit.dev",
|
|
||||||
"pyramid.fiatjaf.com",
|
|
||||||
"git.shakespeare.dyi",
|
|
||||||
}, graspServerHost, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
config.GraspServers = graspServers
|
|
||||||
|
|
||||||
// prompt for web URLs
|
|
||||||
webURLs, err := promptForStringList("web URLs", config.Web, []string{
|
|
||||||
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
|
|
||||||
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
|
|
||||||
config.Identifier,
|
|
||||||
),
|
|
||||||
}, func(s string) string {
|
|
||||||
return "http" + nostr.NormalizeURL(s)[2:]
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
config.Web = webURLs
|
|
||||||
|
|
||||||
// prompt for earliest unique commit
|
|
||||||
if err := survey.AskOne(&survey.Input{
|
|
||||||
Message: "earliest unique commit",
|
|
||||||
Default: config.EarliestUniqueCommit,
|
|
||||||
}, &config.EarliestUniqueCommit); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for maintainers
|
|
||||||
maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool {
|
|
||||||
pk, err := parsePubKey(s)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if pk.Hex() == config.Owner {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
config.Maintainers = maintainers
|
|
||||||
|
|
||||||
log("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
@@ -335,7 +197,7 @@ aside from those, there is also:
|
|||||||
|
|
||||||
log("edit %s if needed, then run %s to publish.\n",
|
log("edit %s if needed, then run %s to publish.\n",
|
||||||
color.CyanString("nip34.json"),
|
color.CyanString("nip34.json"),
|
||||||
color.CyanString("nak git sync"))
|
color.CyanString("nak git announce"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -367,7 +229,7 @@ aside from those, there is also:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository metadata and state
|
// fetch repository metadata and state
|
||||||
repo, _, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
repo, state, err := fetchRepositoryAndState(ctx, owner, identifier, relayHints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -404,7 +266,22 @@ aside from those, there is also:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write nip34.json inside cloned directory
|
// write nip34.json inside cloned directory
|
||||||
localConfig := RepositoryToConfig(repo)
|
localConfig := Nip34Config{
|
||||||
|
Identifier: repo.ID,
|
||||||
|
Name: repo.Name,
|
||||||
|
Description: repo.Description,
|
||||||
|
Web: repo.Web,
|
||||||
|
Owner: nip19.EncodeNpub(repo.Event.PubKey),
|
||||||
|
GraspServers: make([]string, 0, len(repo.Relays)),
|
||||||
|
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
|
||||||
|
Maintainers: make([]string, 0, len(repo.Maintainers)),
|
||||||
|
}
|
||||||
|
for _, r := range repo.Relays {
|
||||||
|
localConfig.GraspServers = append(localConfig.GraspServers, nostr.NormalizeURL(r))
|
||||||
|
}
|
||||||
|
for _, m := range repo.Maintainers {
|
||||||
|
localConfig.Maintainers = append(localConfig.Maintainers, nip19.EncodeNpub(m))
|
||||||
|
}
|
||||||
|
|
||||||
if err := localConfig.Validate(); err != nil {
|
if err := localConfig.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid config: %w", err)
|
return fmt.Errorf("invalid config: %w", err)
|
||||||
@@ -546,7 +423,8 @@ aside from those, there is also:
|
|||||||
pushSuccesses := 0
|
pushSuccesses := 0
|
||||||
for _, relay := range repo.Relays {
|
for _, relay := range repo.Relays {
|
||||||
relayURL := nostr.NormalizeURL(relay)
|
relayURL := nostr.NormalizeURL(relay)
|
||||||
remoteName := gitRemoteName(relayURL)
|
remoteName := "nip34/grasp/" + strings.TrimPrefix(relayURL, "wss://")
|
||||||
|
remoteName = strings.TrimPrefix(remoteName, "ws://")
|
||||||
|
|
||||||
log("pushing to %s...\n", color.CyanString(remoteName))
|
log("pushing to %s...\n", color.CyanString(remoteName))
|
||||||
pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)}
|
pushArgs := []string{"push", remoteName, fmt.Sprintf("%s:refs/heads/%s", localBranch, remoteBranch)}
|
||||||
@@ -555,7 +433,6 @@ aside from those, there is also:
|
|||||||
}
|
}
|
||||||
pushCmd := exec.Command("git", pushArgs...)
|
pushCmd := exec.Command("git", pushArgs...)
|
||||||
pushCmd.Stderr = os.Stderr
|
pushCmd.Stderr = os.Stderr
|
||||||
pushCmd.Stdout = os.Stdout
|
|
||||||
if err := pushCmd.Run(); err != nil {
|
if err := pushCmd.Run(); err != nil {
|
||||||
log("! failed to push to %s: %v\n", color.YellowString(remoteName), err)
|
log("! failed to push to %s: %v\n", color.YellowString(remoteName), err)
|
||||||
} else {
|
} else {
|
||||||
@@ -740,45 +617,50 @@ aside from those, there is also:
|
|||||||
|
|
||||||
func promptForStringList(
|
func promptForStringList(
|
||||||
name string,
|
name string,
|
||||||
|
existing []string,
|
||||||
defaults []string,
|
defaults []string,
|
||||||
alternatives []string,
|
|
||||||
normalize func(string) string,
|
normalize func(string) string,
|
||||||
validate func(string) bool,
|
validate func(string) bool,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
options := make([]string, 0, len(defaults)+len(alternatives)+1)
|
options := make([]string, 0, len(defaults)+len(existing)+1)
|
||||||
options = append(options, defaults...)
|
options = append(options, defaults...)
|
||||||
|
options = append(options, "add another")
|
||||||
|
|
||||||
// add existing not in options
|
// add existing not in options
|
||||||
for _, item := range alternatives {
|
for _, item := range existing {
|
||||||
if !slices.Contains(options, item) {
|
if !slices.Contains(options, item) {
|
||||||
options = append(options, item)
|
options = append(options, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options = append(options, "add another")
|
selected := make([]string, len(existing))
|
||||||
|
copy(selected, existing)
|
||||||
selected := make([]string, len(defaults))
|
|
||||||
copy(selected, defaults)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
newSelected := []string{}
|
prompt := &survey.MultiSelect{
|
||||||
if err := survey.AskOne(&survey.MultiSelect{
|
|
||||||
Message: name,
|
Message: name,
|
||||||
Options: options,
|
Options: options,
|
||||||
Default: selected,
|
Default: selected,
|
||||||
PageSize: 20,
|
PageSize: 20,
|
||||||
}, &newSelected); err != nil {
|
}
|
||||||
|
|
||||||
|
if err := survey.AskOne(prompt, &selected); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
selected = newSelected
|
|
||||||
|
|
||||||
if slices.Contains(selected, "add another") {
|
if slices.Contains(selected, "add another") {
|
||||||
selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" })
|
selected = slices.DeleteFunc(selected, func(s string) bool { return s == "add another" })
|
||||||
|
|
||||||
|
singular := name
|
||||||
|
if strings.HasSuffix(singular, "s") {
|
||||||
|
singular = singular[:len(singular)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
newPrompt := &survey.Input{
|
||||||
|
Message: fmt.Sprintf("enter new %s", singular),
|
||||||
|
}
|
||||||
var newItem string
|
var newItem string
|
||||||
if err := survey.AskOne(&survey.Input{
|
if err := survey.AskOne(newPrompt, &newItem); err != nil {
|
||||||
Message: fmt.Sprintf("enter new %s", strings.TrimSuffix(name, "s")),
|
|
||||||
}, &newItem); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,6 +690,97 @@ func promptForStringList(
|
|||||||
return selected, nil
|
return selected, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptForConfig(config *Nip34Config) error {
|
||||||
|
log("\nenter repository details (use arrow keys to navigate, space to select/deselect, enter to confirm):\n\n")
|
||||||
|
|
||||||
|
// prompt for identifier
|
||||||
|
identifierPrompt := &survey.Input{
|
||||||
|
Message: "identifier",
|
||||||
|
Default: config.Identifier,
|
||||||
|
}
|
||||||
|
if err := survey.AskOne(identifierPrompt, &config.Identifier); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prompt for name
|
||||||
|
namePrompt := &survey.Input{
|
||||||
|
Message: "name",
|
||||||
|
Default: config.Name,
|
||||||
|
}
|
||||||
|
if err := survey.AskOne(namePrompt, &config.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prompt for description
|
||||||
|
descPrompt := &survey.Input{
|
||||||
|
Message: "description",
|
||||||
|
Default: config.Description,
|
||||||
|
}
|
||||||
|
if err := survey.AskOne(descPrompt, &config.Description); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prompt for owner
|
||||||
|
for {
|
||||||
|
ownerPrompt := &survey.Input{
|
||||||
|
Message: "owner (npub or hex)",
|
||||||
|
Default: config.Owner,
|
||||||
|
}
|
||||||
|
if err := survey.AskOne(ownerPrompt, &config.Owner); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pubkey, err := parsePubKey(config.Owner); err == nil {
|
||||||
|
config.Owner = pubkey.Hex()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prompt for grasp servers
|
||||||
|
graspServers, err := promptForStringList("grasp servers", config.GraspServers, []string{
|
||||||
|
"gitnostr.com",
|
||||||
|
"relay.ngit.dev",
|
||||||
|
"pyramid.fiatjaf.com",
|
||||||
|
"git.shakespeare.dyi",
|
||||||
|
}, graspServerHost, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.GraspServers = graspServers
|
||||||
|
|
||||||
|
// prompt for web URLs
|
||||||
|
webURLs, err := promptForStringList("web URLs", config.Web, []string{
|
||||||
|
fmt.Sprintf("https://gitworkshop.dev/%s/%s",
|
||||||
|
nip19.EncodeNpub(nostr.MustPubKeyFromHex(config.Owner)),
|
||||||
|
config.Identifier,
|
||||||
|
),
|
||||||
|
}, func(s string) string {
|
||||||
|
return "http" + nostr.NormalizeURL(s)[2:]
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.Web = webURLs
|
||||||
|
|
||||||
|
// Prompt for maintainers
|
||||||
|
maintainers, err := promptForStringList("maintainers", config.Maintainers, []string{}, nil, func(s string) bool {
|
||||||
|
pk, err := parsePubKey(s)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pk.Hex() == config.Owner {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.Maintainers = maintainers
|
||||||
|
|
||||||
|
log("\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) {
|
func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.RepositoryState, error) {
|
||||||
// read current nip34.json
|
// read current nip34.json
|
||||||
localConfig, err := readNip34ConfigFile("")
|
localConfig, err := readNip34ConfigFile("")
|
||||||
@@ -822,26 +795,9 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository announcement and state from relays
|
// fetch repository announcement and state from relays
|
||||||
repo, upToDateRelays, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
repo, state, err := fetchRepositoryAndState(ctx, owner, localConfig.Identifier, localConfig.GraspServers)
|
||||||
notUpToDate := func(graspServer string) bool {
|
if err != nil && repo.Event.ID == nostr.ZeroID {
|
||||||
return !slices.Contains(upToDateRelays, nostr.NormalizeURL(graspServer))
|
log("couldn't fetch repository metadata (%s), will publish now\n", err)
|
||||||
}
|
|
||||||
if upToDateRelays == nil || slices.ContainsFunc(localConfig.GraspServers, notUpToDate) {
|
|
||||||
var relays []string
|
|
||||||
if upToDateRelays == nil {
|
|
||||||
// condition 1
|
|
||||||
relays = append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...)
|
|
||||||
log("couldn't fetch repository metadata (%s), will publish now\n", err)
|
|
||||||
} else {
|
|
||||||
// condition 2
|
|
||||||
relays = make([]string, 0, len(localConfig.GraspServers)-1)
|
|
||||||
for _, gs := range localConfig.GraspServers {
|
|
||||||
if notUpToDate(gs) {
|
|
||||||
relays = append(relays, graspServerHost(gs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("some grasp servers (%v) are not up-to-date, will publish to them\n", relays)
|
|
||||||
}
|
|
||||||
// create a local repository object from config and publish it
|
// create a local repository object from config and publish it
|
||||||
localRepo := localConfig.ToRepository()
|
localRepo := localConfig.ToRepository()
|
||||||
|
|
||||||
@@ -858,6 +814,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
return repo, state, fmt.Errorf("failed to sign announcement: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relays := append(sys.FetchOutboxRelays(ctx, owner, 3), localConfig.GraspServers...)
|
||||||
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
for res := range sys.Pool.PublishMany(ctx, relays, event) {
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
log("! error publishing to %s: %v\n", color.YellowString(res.RelayURL), res.Error)
|
||||||
@@ -957,7 +914,7 @@ func gitSync(ctx context.Context, signer nostr.Keyer) (nip34.Repository, *nip34.
|
|||||||
func fetchFromRemotes(ctx context.Context, targetDir string, repo nip34.Repository) {
|
func fetchFromRemotes(ctx context.Context, targetDir string, repo nip34.Repository) {
|
||||||
// fetch from each grasp remote
|
// fetch from each grasp remote
|
||||||
for _, grasp := range repo.Relays {
|
for _, grasp := range repo.Relays {
|
||||||
remoteName := gitRemoteName(grasp)
|
remoteName := "nip34/grasp/" + strings.Split(grasp, "/")[2]
|
||||||
|
|
||||||
logverbose("fetching from %s...\n", remoteName)
|
logverbose("fetching from %s...\n", remoteName)
|
||||||
fetchCmd := exec.Command("git", "fetch", remoteName)
|
fetchCmd := exec.Command("git", "fetch", remoteName)
|
||||||
@@ -983,69 +940,45 @@ func gitSetupRemotes(ctx context.Context, dir string, repo nip34.Repository) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete all nip34/grasp/ remotes that we don't have anymore in repo
|
// delete all nip34/grasp/ remotes
|
||||||
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
for i, remote := range remotes {
|
for i, remote := range remotes {
|
||||||
remote = strings.TrimSpace(remote)
|
remote = strings.TrimSpace(remote)
|
||||||
remotes[i] = remote
|
remotes[i] = remote
|
||||||
|
|
||||||
if strings.HasPrefix(remote, "nip34/grasp/") {
|
if strings.HasPrefix(remote, "nip34/grasp/") {
|
||||||
graspURL := rebuildGraspURLFromRemote(remote)
|
if !slices.Contains(repo.Relays, nostr.NormalizeURL(remote[12:])) {
|
||||||
|
delCmd := exec.Command("git", "remote", "remove", remote)
|
||||||
getUrlCmd := exec.Command("git", "remote", "get-url", remote)
|
if dir != "" {
|
||||||
if dir != "" {
|
delCmd.Dir = dir
|
||||||
getUrlCmd.Dir = dir
|
}
|
||||||
}
|
if err := delCmd.Run(); err != nil {
|
||||||
if output, err := getUrlCmd.Output(); err != nil {
|
logverbose("failed to remove remote %s: %v\n", remote, err)
|
||||||
panic(fmt.Errorf("failed to read remote (%s) url from git: %s", remote, err))
|
|
||||||
} else {
|
|
||||||
// check if the remote url is correct so we can update it if not
|
|
||||||
gitURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(graspURL)[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
|
|
||||||
if strings.TrimSpace(string(output)) != gitURL {
|
|
||||||
goto delete
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// check if this remote is not present in our grasp list anymore
|
|
||||||
if !slices.Contains(repo.Relays, nostr.NormalizeURL(graspURL)) {
|
|
||||||
goto delete
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
delete:
|
|
||||||
logverbose("deleting remote %s\n", remote)
|
|
||||||
delCmd := exec.Command("git", "remote", "remove", remote)
|
|
||||||
if dir != "" {
|
|
||||||
delCmd.Dir = dir
|
|
||||||
}
|
|
||||||
if err := delCmd.Run(); err != nil {
|
|
||||||
logverbose("failed to remove remote %s: %v\n", remote, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create new remotes for each grasp server
|
// create new remotes for each grasp server
|
||||||
remotes = strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
||||||
for _, relay := range repo.Relays {
|
for _, relay := range repo.Relays {
|
||||||
remote := gitRemoteName(relay)
|
remote := "nip34/grasp/" + strings.TrimPrefix(relay, "wss://")
|
||||||
gitURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(relay)[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
|
|
||||||
|
|
||||||
if slices.Contains(remotes, remote) {
|
if !slices.Contains(remotes, remote) {
|
||||||
continue
|
// construct the git URL
|
||||||
}
|
gitURL := fmt.Sprintf("http%s/%s/%s.git",
|
||||||
|
relay[2:], nip19.EncodeNpub(repo.PubKey), repo.ID)
|
||||||
|
|
||||||
logverbose("adding new remote for '%s'\n", relay)
|
addCmd := exec.Command("git", "remote", "add", remote, gitURL)
|
||||||
addCmd := exec.Command("git", "remote", "add", remote, gitURL)
|
if dir != "" {
|
||||||
if dir != "" {
|
addCmd.Dir = dir
|
||||||
addCmd.Dir = dir
|
}
|
||||||
}
|
if out, err := addCmd.Output(); err != nil {
|
||||||
if out, err := addCmd.Output(); err != nil {
|
var stderr string
|
||||||
var stderr string
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
stderr = string(exiterr.Stderr)
|
||||||
stderr = string(exiterr.Stderr)
|
}
|
||||||
|
logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out))
|
||||||
}
|
}
|
||||||
logverbose("failed to add remote %s: %s %s\n", remote, stderr, string(out))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1108,7 +1041,7 @@ func fetchRepositoryAndState(
|
|||||||
pubkey nostr.PubKey,
|
pubkey nostr.PubKey,
|
||||||
identifier string,
|
identifier string,
|
||||||
relayHints []string,
|
relayHints []string,
|
||||||
) (repo nip34.Repository, upToDateRelays []string, state *nip34.RepositoryState, err error) {
|
) (repo nip34.Repository, state *nip34.RepositoryState, err error) {
|
||||||
// fetch repository announcement (30617)
|
// fetch repository announcement (30617)
|
||||||
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
relays := appendUnique(relayHints, sys.FetchOutboxRelays(ctx, pubkey, 3)...)
|
||||||
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
for ie := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
@@ -1118,24 +1051,13 @@ func fetchRepositoryAndState(
|
|||||||
"d": []string{identifier},
|
"d": []string{identifier},
|
||||||
},
|
},
|
||||||
Limit: 2,
|
Limit: 2,
|
||||||
}, nostr.SubscriptionOptions{
|
}, nostr.SubscriptionOptions{Label: "nak-git"}) {
|
||||||
Label: "nak-git",
|
|
||||||
CheckDuplicate: func(id nostr.ID, relay string) bool {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
}) {
|
|
||||||
if ie.Event.CreatedAt > repo.CreatedAt {
|
if ie.Event.CreatedAt > repo.CreatedAt {
|
||||||
repo = nip34.ParseRepository(ie.Event)
|
repo = nip34.ParseRepository(ie.Event)
|
||||||
|
|
||||||
// reset this list as the previous was for relays with the older version
|
|
||||||
upToDateRelays = []string{ie.Relay.URL}
|
|
||||||
} else if ie.Event.CreatedAt == repo.CreatedAt {
|
|
||||||
// we discard this because it's the same, but this relay is up-to-date
|
|
||||||
upToDateRelays = append(upToDateRelays, ie.Relay.URL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if repo.Event.ID == nostr.ZeroID {
|
if repo.Event.ID == nostr.ZeroID {
|
||||||
return repo, upToDateRelays, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier)
|
return repo, state, fmt.Errorf("no repository announcement (kind 30617) found for %s", identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch repository state (30618)
|
// fetch repository state (30618)
|
||||||
@@ -1165,10 +1087,10 @@ func fetchRepositoryAndState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if stateErr != nil {
|
if stateErr != nil {
|
||||||
return repo, upToDateRelays, state, stateErr
|
return repo, state, stateErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo, upToDateRelays, state, nil
|
return repo, state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type StateErr struct{ string }
|
type StateErr struct{ string }
|
||||||
@@ -1445,6 +1367,8 @@ func figureOutBranches(c *cli.Command, refspec string, isPush bool) (
|
|||||||
return localBranch, remoteBranch, nil
|
return localBranch, remoteBranch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func graspServerHost(s string) string { return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2] }
|
||||||
|
|
||||||
type Nip34Config struct {
|
type Nip34Config struct {
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -1456,26 +1380,6 @@ type Nip34Config struct {
|
|||||||
Maintainers []string `json:"maintainers"`
|
Maintainers []string `json:"maintainers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func RepositoryToConfig(repo nip34.Repository) Nip34Config {
|
|
||||||
config := Nip34Config{
|
|
||||||
Identifier: repo.ID,
|
|
||||||
Name: repo.Name,
|
|
||||||
Description: repo.Description,
|
|
||||||
Web: repo.Web,
|
|
||||||
Owner: nip19.EncodeNpub(repo.Event.PubKey),
|
|
||||||
GraspServers: make([]string, 0, len(repo.Relays)),
|
|
||||||
EarliestUniqueCommit: repo.EarliestUniqueCommitID,
|
|
||||||
Maintainers: make([]string, 0, len(repo.Maintainers)),
|
|
||||||
}
|
|
||||||
for _, r := range repo.Relays {
|
|
||||||
config.GraspServers = append(config.GraspServers, graspServerHost(r))
|
|
||||||
}
|
|
||||||
for _, m := range repo.Maintainers {
|
|
||||||
config.Maintainers = append(config.Maintainers, nip19.EncodeNpub(m))
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (localConfig Nip34Config) Validate() error {
|
func (localConfig Nip34Config) Validate() error {
|
||||||
_, err := parsePubKey(localConfig.Owner)
|
_, err := parsePubKey(localConfig.Owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1526,18 +1430,3 @@ func (localConfig Nip34Config) ToRepository() nip34.Repository {
|
|||||||
|
|
||||||
return localRepo
|
return localRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitRemoteName(graspURL string) string {
|
|
||||||
host := graspServerHost(graspURL)
|
|
||||||
host = strings.Replace(host, ":", "__", 1)
|
|
||||||
return "nip34/grasp/" + host
|
|
||||||
}
|
|
||||||
|
|
||||||
func rebuildGraspURLFromRemote(remoteName string) string {
|
|
||||||
host := strings.TrimPrefix(remoteName, "nip34/grasp/")
|
|
||||||
return strings.Replace(host, "__", ":", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func graspServerHost(s string) string {
|
|
||||||
return strings.SplitN(nostr.NormalizeURL(s), "/", 3)[2]
|
|
||||||
}
|
|
||||||
|
|||||||
25
go.mod
25
go.mod
@@ -4,11 +4,10 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.1
|
fiatjaf.com/lib v0.3.1
|
||||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157
|
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
|
||||||
github.com/fatih/color v1.16.0
|
github.com/fatih/color v1.16.0
|
||||||
@@ -23,6 +22,7 @@ require (
|
|||||||
github.com/mdp/qrterminal/v3 v3.2.1
|
github.com/mdp/qrterminal/v3 v3.2.1
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d
|
||||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6
|
||||||
golang.org/x/sync v0.18.0
|
golang.org/x/sync v0.18.0
|
||||||
@@ -32,28 +32,18 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
github.com/FastFilter/xorfilter v0.2.1 // indirect
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // 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/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/bluekeyes/go-gitdiff v0.7.1 // indirect
|
||||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.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/logex v1.1.10 // indirect
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||||
github.com/dgraph-io/ristretto/v2 v2.3.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/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||||
github.com/elnosh/gonuts v0.4.2 // indirect
|
github.com/elnosh/gonuts v0.4.2 // indirect
|
||||||
@@ -61,7 +51,7 @@ require (
|
|||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/go-git/go-git/v5 v5.16.3 // indirect
|
github.com/go-git/go-git/v5 v5.16.3 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e // indirect
|
||||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||||
@@ -69,18 +59,12 @@ require (
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.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/magefile/mage v1.14.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // 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/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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
github.com/templexxx/cpu v0.0.1 // indirect
|
github.com/templexxx/cpu v0.0.1 // indirect
|
||||||
@@ -93,9 +77,6 @@ require (
|
|||||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // 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
|
go.etcd.io/bbolt v1.4.2 // indirect
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
|
|||||||
77
go.sum
77
go.sum
@@ -1,7 +1,7 @@
|
|||||||
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
fiatjaf.com/lib v0.3.1 h1:/oFQwNtFRfV+ukmOCxfBEAuayoLwXp4wu2/fz5iHpwA=
|
||||||
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
fiatjaf.com/lib v0.3.1/go.mod h1:Ycqq3+mJ9jAWu7XjbQI1cVr+OFgnHn79dQR5oTII47g=
|
||||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157 h1:14yLsO2HwpS2CLIKFvLMDp8tVEDahwdC8OeG6NGaL+M=
|
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606 h1:wQHJ0TFA0Fuq92p/6u6AbsBFq6ZVToSdxV6puXVIruI=
|
||||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
fiatjaf.com/nostr v0.0.0-20251126101225-44130595c606/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
@@ -13,20 +13,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
|
|||||||
github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg=
|
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/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/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 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
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 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
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 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/DWesQ=
|
||||||
@@ -61,22 +49,6 @@ 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 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
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 h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||||
@@ -102,8 +74,6 @@ github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXR
|
|||||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
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 h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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=
|
github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||||
@@ -137,8 +107,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e h1:XWcjeEtTFTOVA9Fs1w7n2XBftk5ib4oZrhzWk0B+3eA=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
@@ -146,8 +116,6 @@ 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/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 h1:SbJP1sUP+n1UF8NXBA14BuojmTez+mDgOk0bC057HQw=
|
||||||
github.com/hanwen/go-fuse/v2 v2.7.2/go.mod h1:ugNaD/iv5JYyS1Rcvi57Wz7/vrLQJo10mmketmoef48=
|
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 h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
@@ -165,6 +133,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
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 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
@@ -173,8 +143,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM=
|
||||||
github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk=
|
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 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
@@ -189,17 +157,12 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
|
|||||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q=
|
||||||
github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k=
|
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 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
|
||||||
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
|
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 h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
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 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
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=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -207,10 +170,6 @@ 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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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/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.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -226,17 +185,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
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/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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
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 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
@@ -249,6 +208,8 @@ github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b h1:XeDLE6c9mzHpdv3W
|
|||||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b/go.mod h1:7rwmCH0wC2fQvNEvPZ3sKXukhyCTyiaZ5VTZMQYpZKQ=
|
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 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||||
|
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o=
|
||||||
|
github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
@@ -271,20 +232,14 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e
|
|||||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
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-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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
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 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
@@ -294,7 +249,9 @@ golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWI
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
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-20180719180050-a680a1efc54d/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-20180906233101-161cd47e91fd/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/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-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-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-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
@@ -307,10 +264,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -336,6 +296,7 @@ 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 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -42,7 +42,6 @@ var app = &cli.Command{
|
|||||||
blossomCmd,
|
blossomCmd,
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
gift,
|
|
||||||
outbox,
|
outbox,
|
||||||
wallet,
|
wallet,
|
||||||
mcpServer,
|
mcpServer,
|
||||||
@@ -51,7 +50,6 @@ var app = &cli.Command{
|
|||||||
publish,
|
publish,
|
||||||
git,
|
git,
|
||||||
nip,
|
nip,
|
||||||
syncCmd,
|
|
||||||
},
|
},
|
||||||
Version: version,
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
|||||||
191
nip.go
191
nip.go
@@ -9,25 +9,30 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/glamour"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type nipInfo struct {
|
|
||||||
nip, desc, link string
|
|
||||||
}
|
|
||||||
|
|
||||||
var nip = &cli.Command{
|
var nip = &cli.Command{
|
||||||
Name: "nip",
|
Name: "nip",
|
||||||
Usage: "list NIPs or get the description of a NIP from its number",
|
Usage: "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.
|
Description: `fetches the NIPs README from GitHub and parses it to find the description of the given NIP number.
|
||||||
|
|
||||||
examples:
|
example:
|
||||||
nak nip # list all NIPs
|
nak nip 1
|
||||||
nak nip 29 # shows nip29 details
|
nak nip list
|
||||||
nak nip open 29 # opens nip29 in browser`,
|
nak nip open 1`,
|
||||||
ArgsUsage: "[NIP number]",
|
ArgsUsage: "<NIP number>",
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Usage: "list all NIPs",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
return iterateNips(func(nip, desc, link string) bool {
|
||||||
|
stdout(nip + ": " + desc)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "open",
|
Name: "open",
|
||||||
Usage: "open the NIP page in the browser",
|
Usage: "open the NIP page in the browser",
|
||||||
@@ -50,12 +55,17 @@ examples:
|
|||||||
reqNum = normalize(reqNum)
|
reqNum = normalize(reqNum)
|
||||||
|
|
||||||
foundLink := ""
|
foundLink := ""
|
||||||
for info := range listnips() {
|
err := iterateNips(func(nip, desc, link string) bool {
|
||||||
nipNum := normalize(info.nip)
|
nipNum := normalize(nip)
|
||||||
if nipNum == reqNum {
|
if nipNum == reqNum {
|
||||||
foundLink = info.link
|
foundLink = link
|
||||||
break
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if foundLink == "" {
|
if foundLink == "" {
|
||||||
@@ -82,11 +92,7 @@ examples:
|
|||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
reqNum := c.Args().First()
|
reqNum := c.Args().First()
|
||||||
if reqNum == "" {
|
if reqNum == "" {
|
||||||
// list all NIPs
|
return fmt.Errorf("missing NIP number")
|
||||||
for info := range listnips() {
|
|
||||||
stdout(info.nip + ": " + info.desc)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
normalize := func(s string) string {
|
normalize := func(s string) string {
|
||||||
@@ -101,101 +107,80 @@ examples:
|
|||||||
|
|
||||||
reqNum = normalize(reqNum)
|
reqNum = normalize(reqNum)
|
||||||
|
|
||||||
var foundLink string
|
found := false
|
||||||
for info := range listnips() {
|
err := iterateNips(func(nip, desc, link string) bool {
|
||||||
nipNum := normalize(info.nip)
|
nipNum := normalize(nip)
|
||||||
|
|
||||||
if nipNum == reqNum {
|
if nipNum == reqNum {
|
||||||
foundLink = info.link
|
stdout(strings.TrimSpace(desc))
|
||||||
break
|
found = true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if foundLink == "" {
|
if !found {
|
||||||
return fmt.Errorf("NIP-%s not found", strings.ToUpper(reqNum))
|
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
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func listnips() <-chan nipInfo {
|
func iterateNips(yield func(nip, desc, link string) bool) error {
|
||||||
ch := make(chan nipInfo)
|
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
|
||||||
go func() {
|
if err != nil {
|
||||||
defer close(ch)
|
return fmt.Errorf("failed to fetch NIPs README: %w", err)
|
||||||
resp, err := http.Get("https://raw.githubusercontent.com/nostr-protocol/nips/master/README.md")
|
}
|
||||||
if err != nil {
|
defer resp.Body.Close()
|
||||||
// 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)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("failed to read NIPs README: %w", err)
|
||||||
|
}
|
||||||
|
bodyStr := string(body)
|
||||||
|
epoch := strings.Index(bodyStr, "## List")
|
||||||
|
|
||||||
|
lines := strings.SplitSeq(bodyStr[epoch+8:], "\n")
|
||||||
|
for line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "##") {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
bodyStr := string(body)
|
if !strings.HasPrefix(line, "- [NIP-") {
|
||||||
epoch := strings.Index(bodyStr, "## List")
|
continue
|
||||||
if epoch == -1 {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.SplitSeq(bodyStr[epoch+8:], "\n")
|
start := strings.Index(line, "[")
|
||||||
for line := range lines {
|
end := strings.Index(line, "]")
|
||||||
line = strings.TrimSpace(line)
|
if start == -1 || end == -1 || end < start {
|
||||||
if strings.HasPrefix(line, "##") {
|
continue
|
||||||
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
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !yield(nipPart, strings.TrimSpace(descPart), link) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
464
sync.go
464
sync.go
@@ -1,464 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
5
view/.gitignore
vendored
Normal file
5
view/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
dist
|
||||||
|
view
|
||||||
|
*.json
|
||||||
|
deploy
|
||||||
|
qtbox
|
||||||
13
view/Makefile
Normal file
13
view/Makefile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
dist: deploy/linux/nakv deploy/windows/nakv.exe
|
||||||
|
mkdir -p dist
|
||||||
|
cd deploy/linux && tar -czvf nakv_linux.tar.gz nakv
|
||||||
|
mv deploy/linux/nakv_linux.tar.gz dist/
|
||||||
|
rm -f deploy/windows/nakv_windows.zip
|
||||||
|
cd deploy/windows && zip nakv_windows *.exe
|
||||||
|
mv deploy/windows/nakv_windows.zip dist/
|
||||||
|
|
||||||
|
deploy/linux/nakv: $(shell find . -name "*.go")
|
||||||
|
qtdeploy -ldflags="-s -w" -fast build desktop github.com/fiatjaf/nakv
|
||||||
|
|
||||||
|
deploy/windows/nakv.exe: $(shell find . -name "*.go")
|
||||||
|
qtdeploy -ldflags="-s -w" -docker build windows_64_static
|
||||||
201
view/event.go
Normal file
201
view/event.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/therecipe/qt/core"
|
||||||
|
"github.com/therecipe/qt/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
var event struct {
|
||||||
|
kindSpin *widgets.QSpinBox
|
||||||
|
kindNameLabel *widgets.QLabel
|
||||||
|
tagRows [][]*widgets.QLineEdit
|
||||||
|
contentEdit *widgets.QTextEdit
|
||||||
|
createdAtEdit *widgets.QDateTimeEdit
|
||||||
|
outputEdit *widgets.QTextEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEvent() {
|
||||||
|
kind := nostr.Kind(event.kindSpin.Value())
|
||||||
|
kindName := kind.Name()
|
||||||
|
if kindName != "unknown" {
|
||||||
|
event.kindNameLabel.SetText(kindName)
|
||||||
|
} else {
|
||||||
|
event.kindNameLabel.SetText("")
|
||||||
|
}
|
||||||
|
tags := make(nostr.Tags, 0, len(event.tagRows))
|
||||||
|
for y, tagItems := range event.tagRows {
|
||||||
|
if y == len(event.tagRows)-1 && strings.TrimSpace(tagItems[0].Text()) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := make(nostr.Tag, 0, len(tagItems))
|
||||||
|
for x, edit := range tagItems {
|
||||||
|
text := strings.TrimSpace(edit.Text())
|
||||||
|
if x == len(tagItems)-1 && text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
text = decodeTagValue(text)
|
||||||
|
tag = append(tag, text)
|
||||||
|
}
|
||||||
|
if len(tag) > 0 {
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := nostr.Event{
|
||||||
|
Kind: kind,
|
||||||
|
Content: event.contentEdit.ToPlainText(),
|
||||||
|
CreatedAt: nostr.Timestamp(event.createdAtEdit.DateTime().ToMSecsSinceEpoch() / 1000),
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize := func() {
|
||||||
|
jsonBytes, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
event.outputEdit.SetPlainText(string(jsonBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentKeyer != nil {
|
||||||
|
signAndFinalize := func() {
|
||||||
|
if currentKeyer != nil {
|
||||||
|
if err := currentKeyer.SignEvent(ctx, &result); err == nil {
|
||||||
|
finalize()
|
||||||
|
} else {
|
||||||
|
statusLabel.SetText("failed to sign: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSec == [32]byte{} {
|
||||||
|
// empty key, we must have a bunker
|
||||||
|
debounced.Call(signAndFinalize)
|
||||||
|
} else {
|
||||||
|
// we have a key, can sign immediately
|
||||||
|
signAndFinalize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupEventTab() *widgets.QWidget {
|
||||||
|
tab := widgets.NewQWidget(nil, 0)
|
||||||
|
|
||||||
|
// set up event tab
|
||||||
|
layout := widgets.NewQVBoxLayout()
|
||||||
|
tab.SetLayout(layout)
|
||||||
|
|
||||||
|
// kind input
|
||||||
|
kindHBox := widgets.NewQHBoxLayout()
|
||||||
|
layout.AddLayout(kindHBox, 0)
|
||||||
|
kindLabel := widgets.NewQLabel2("kind:", nil, 0)
|
||||||
|
kindHBox.AddWidget(kindLabel, 0, 0)
|
||||||
|
event.kindSpin = widgets.NewQSpinBox(nil)
|
||||||
|
event.kindSpin.SetValue(1)
|
||||||
|
event.kindSpin.SetMinimum(0)
|
||||||
|
event.kindSpin.SetMaximum(1<<16 - 1)
|
||||||
|
kindHBox.AddWidget(event.kindSpin, 0, 0)
|
||||||
|
event.kindSpin.ConnectValueChanged(func(int) {
|
||||||
|
updateEvent()
|
||||||
|
})
|
||||||
|
event.kindNameLabel = widgets.NewQLabel2("", nil, 0)
|
||||||
|
kindHBox.AddWidget(event.kindNameLabel, 0, 0)
|
||||||
|
|
||||||
|
// content input
|
||||||
|
contentLabel := widgets.NewQLabel2("content:", nil, 0)
|
||||||
|
layout.AddWidget(contentLabel, 0, 0)
|
||||||
|
event.contentEdit = widgets.NewQTextEdit(nil)
|
||||||
|
layout.AddWidget(event.contentEdit, 0, 0)
|
||||||
|
event.contentEdit.ConnectTextChanged(updateEvent)
|
||||||
|
|
||||||
|
// created_at input
|
||||||
|
createdAtLabel := widgets.NewQLabel2("created at:", nil, 0)
|
||||||
|
layout.AddWidget(createdAtLabel, 0, 0)
|
||||||
|
event.createdAtEdit = widgets.NewQDateTimeEdit(nil)
|
||||||
|
event.createdAtEdit.SetDateTime(core.QDateTime_CurrentDateTime())
|
||||||
|
layout.AddWidget(event.createdAtEdit, 0, 0)
|
||||||
|
event.createdAtEdit.ConnectDateTimeChanged(func(*core.QDateTime) {
|
||||||
|
updateEvent()
|
||||||
|
})
|
||||||
|
|
||||||
|
// tags input
|
||||||
|
tagsLabel := widgets.NewQLabel2("tags:", nil, 0)
|
||||||
|
layout.AddWidget(tagsLabel, 0, 0)
|
||||||
|
tagsLayout := widgets.NewQVBoxLayout()
|
||||||
|
tagRowHBoxes := make([]widgets.QLayout_ITF, 0, 2)
|
||||||
|
event.tagRows = make([][]*widgets.QLineEdit, 0, 2)
|
||||||
|
layout.AddLayout(tagsLayout, 0)
|
||||||
|
|
||||||
|
var addTagRow func()
|
||||||
|
addTagRow = func() {
|
||||||
|
hbox := widgets.NewQHBoxLayout()
|
||||||
|
tagRowHBoxes = append(tagRowHBoxes, hbox)
|
||||||
|
tagsLayout.AddLayout(hbox, 0)
|
||||||
|
tagItems := []*widgets.QLineEdit{}
|
||||||
|
y := len(event.tagRows)
|
||||||
|
event.tagRows = append(event.tagRows, tagItems)
|
||||||
|
|
||||||
|
var addItem func()
|
||||||
|
addItem = func() {
|
||||||
|
edit := widgets.NewQLineEdit(nil)
|
||||||
|
hbox.AddWidget(edit, 0, 0)
|
||||||
|
x := len(tagItems)
|
||||||
|
tagItems = append(tagItems, edit)
|
||||||
|
event.tagRows[y] = tagItems
|
||||||
|
edit.ConnectTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
// when an item input has been filled check if we have to show more
|
||||||
|
if y == len(event.tagRows)-1 {
|
||||||
|
addTagRow()
|
||||||
|
}
|
||||||
|
if x == len(tagItems)-1 {
|
||||||
|
addItem()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// do this when an item input has been emptied: check if we need to remove an item from this row
|
||||||
|
nItems := len(tagItems)
|
||||||
|
if nItems >= 2 && strings.TrimSpace(tagItems[nItems-1].Text()) == "" && strings.TrimSpace(tagItems[nItems-2].Text()) == "" {
|
||||||
|
// remove last item if the last 2 are empty
|
||||||
|
hbox.Layout().RemoveWidget(tagItems[nItems-1])
|
||||||
|
tagItems[nItems-1].DeleteLater()
|
||||||
|
tagItems = tagItems[0 : nItems-1]
|
||||||
|
event.tagRows[y] = tagItems
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we need to remove rows
|
||||||
|
nRows := len(event.tagRows)
|
||||||
|
itemIsFilled := func(edit *widgets.QLineEdit) bool { return strings.TrimSpace(edit.Text()) != "" }
|
||||||
|
if nRows >= 2 && !slices.ContainsFunc(event.tagRows[nRows-1], itemIsFilled) && !slices.ContainsFunc(event.tagRows[nRows-2], itemIsFilled) {
|
||||||
|
// remove the last row if the last 2 are empty
|
||||||
|
tagsLayout.RemoveItem(tagRowHBoxes[nRows-1])
|
||||||
|
for _, tagItem := range event.tagRows[nRows-1] {
|
||||||
|
tagItem.DeleteLater()
|
||||||
|
}
|
||||||
|
tagRowHBoxes[nRows-1].QLayout_PTR().DeleteLater()
|
||||||
|
event.tagRows = event.tagRows[0 : nRows-1]
|
||||||
|
tagRowHBoxes = tagRowHBoxes[0 : nRows-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateEvent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addItem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// first
|
||||||
|
addTagRow()
|
||||||
|
|
||||||
|
// output JSON
|
||||||
|
outputLabel := widgets.NewQLabel2("event:", nil, 0)
|
||||||
|
layout.AddWidget(outputLabel, 0, 0)
|
||||||
|
event.outputEdit = widgets.NewQTextEdit(nil)
|
||||||
|
event.outputEdit.SetReadOnly(true)
|
||||||
|
layout.AddWidget(event.outputEdit, 0, 0)
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
49
view/helpers.go
Normal file
49
view/helpers.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/keyer"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip46"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleSecretKeyOrBunker(sec string) (nostr.SecretKey, nostr.Keyer, error) {
|
||||||
|
if strings.HasPrefix(sec, "bunker://") {
|
||||||
|
// it's a bunker
|
||||||
|
bunkerURL := sec
|
||||||
|
clientKey := nostr.Generate()
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
bunker, err := nip46.ConnectBunker(ctx, clientKey, bunkerURL, nil, func(s string) {})
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("failed to connect to %s: %w", bunkerURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nostr.SecretKey{}, keyer.NewBunkerSignerFromBunkerClient(bunker), err
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, ski, err := nip19.Decode(sec); err == nil && prefix == "nsec" {
|
||||||
|
sk := ski.(nostr.SecretKey)
|
||||||
|
return sk, keyer.NewPlainKeySigner(sk), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sk, err := nostr.SecretKeyFromHex(sec)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.SecretKey{}, nil, fmt.Errorf("invalid secret key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sk, keyer.NewPlainKeySigner(sk), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
139
view/main.go
Normal file
139
view/main.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/lib/debouncer"
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip49"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/therecipe/qt/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentSec nostr.SecretKey
|
||||||
|
currentKeyer nostr.Keyer
|
||||||
|
statusLabel *widgets.QLabel
|
||||||
|
debounced = debouncer.New(800 * time.Millisecond)
|
||||||
|
sys = sdk.NewSystem()
|
||||||
|
ctx = context.Background()
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := widgets.NewQApplication(len(os.Args), os.Args)
|
||||||
|
|
||||||
|
window := widgets.NewQMainWindow(nil, 0)
|
||||||
|
window.SetMinimumSize2(800, 600)
|
||||||
|
window.SetWindowTitle("nakv")
|
||||||
|
|
||||||
|
centralWidget := widgets.NewQWidget(nil, 0)
|
||||||
|
window.SetCentralWidget(centralWidget)
|
||||||
|
|
||||||
|
mainLayout := widgets.NewQVBoxLayout()
|
||||||
|
centralWidget.SetLayout(mainLayout)
|
||||||
|
|
||||||
|
// private key input
|
||||||
|
secLabel := widgets.NewQLabel2("private key (hex or nsec):", nil, 0)
|
||||||
|
mainLayout.AddWidget(secLabel, 0, 0)
|
||||||
|
|
||||||
|
secHBox := widgets.NewQHBoxLayout()
|
||||||
|
mainLayout.AddLayout(secHBox, 0)
|
||||||
|
secEdit := widgets.NewQLineEdit(nil)
|
||||||
|
secHBox.AddWidget(secEdit, 0, 0)
|
||||||
|
generateButton := widgets.NewQPushButton2("generate", nil)
|
||||||
|
secHBox.AddWidget(generateButton, 0, 0)
|
||||||
|
|
||||||
|
// password input
|
||||||
|
passwordHBox := widgets.NewQHBoxLayout()
|
||||||
|
passwordWidget := widgets.NewQWidget(nil, 0)
|
||||||
|
passwordWidget.SetLayout(passwordHBox)
|
||||||
|
passwordWidget.SetVisible(false)
|
||||||
|
mainLayout.AddWidget(passwordWidget, 0, 0)
|
||||||
|
passwordLabel := widgets.NewQLabel2("password:", nil, 0)
|
||||||
|
passwordHBox.AddWidget(passwordLabel, 0, 0)
|
||||||
|
secPasswordEdit := widgets.NewQLineEdit(nil)
|
||||||
|
secPasswordEdit.SetEchoMode(widgets.QLineEdit__Password)
|
||||||
|
passwordHBox.AddWidget(secPasswordEdit, 0, 0)
|
||||||
|
keyChanged := func(text string) {
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
|
||||||
|
var sk nostr.SecretKey
|
||||||
|
var keyer nostr.Keyer
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
|
passwordWidget.SetVisible(false)
|
||||||
|
goto empty
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(text, "ncryptsec1") {
|
||||||
|
passwordWidget.SetVisible(true)
|
||||||
|
password := secPasswordEdit.Text()
|
||||||
|
if password != "" {
|
||||||
|
sk, err = nip49.Decrypt(text, password)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText("decryption failed: " + err.Error())
|
||||||
|
goto empty
|
||||||
|
}
|
||||||
|
text = hex.EncodeToString(sk[:])
|
||||||
|
} else {
|
||||||
|
goto empty
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
passwordWidget.SetVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
sk, keyer, err = handleSecretKeyOrBunker(text)
|
||||||
|
if err != nil {
|
||||||
|
statusLabel.SetText(err.Error())
|
||||||
|
currentSec = nostr.SecretKey{}
|
||||||
|
currentKeyer = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSec = sk
|
||||||
|
currentKeyer = keyer
|
||||||
|
statusLabel.SetText("")
|
||||||
|
updateEvent()
|
||||||
|
return
|
||||||
|
|
||||||
|
empty:
|
||||||
|
currentSec = nostr.SecretKey{}
|
||||||
|
currentKeyer = nil
|
||||||
|
statusLabel.SetText("")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
secEdit.ConnectTextChanged(keyChanged)
|
||||||
|
secPasswordEdit.ConnectTextChanged(keyChanged)
|
||||||
|
generateButton.ConnectClicked(func(bool) {
|
||||||
|
sk := nostr.Generate()
|
||||||
|
nsec := nip19.EncodeNsec(sk)
|
||||||
|
secEdit.SetText(nsec)
|
||||||
|
keyChanged(nsec)
|
||||||
|
})
|
||||||
|
|
||||||
|
tabWidget := widgets.NewQTabWidget(nil)
|
||||||
|
|
||||||
|
eventTab := setupEventTab()
|
||||||
|
reqTab := setupReqTab()
|
||||||
|
|
||||||
|
tabWidget.AddTab(eventTab, "event")
|
||||||
|
tabWidget.AddTab(reqTab, "req")
|
||||||
|
|
||||||
|
mainLayout.AddWidget(tabWidget, 0, 0)
|
||||||
|
|
||||||
|
statusLabel = widgets.NewQLabel2("", nil, 0)
|
||||||
|
mainLayout.AddWidget(statusLabel, 0, 0)
|
||||||
|
|
||||||
|
// initial render
|
||||||
|
updateEvent()
|
||||||
|
updateReq()
|
||||||
|
|
||||||
|
window.Show()
|
||||||
|
app.Exec()
|
||||||
|
}
|
||||||
353
view/req.go
Normal file
353
view/req.go
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"github.com/therecipe/qt/core"
|
||||||
|
"github.com/therecipe/qt/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type reqVars struct {
|
||||||
|
authorsEdits []*widgets.QLineEdit
|
||||||
|
idsEdits []*widgets.QLineEdit
|
||||||
|
kindsEdits []*widgets.QLineEdit
|
||||||
|
kindsLabels []*widgets.QLabel
|
||||||
|
relaysEdits []*widgets.QLineEdit
|
||||||
|
sinceEdit *widgets.QDateTimeEdit
|
||||||
|
untilEdit *widgets.QDateTimeEdit
|
||||||
|
limitSpin *widgets.QSpinBox
|
||||||
|
|
||||||
|
filter nostr.Filter
|
||||||
|
|
||||||
|
outputEdit *widgets.QTextEdit
|
||||||
|
resultsEdit *widgets.QTextEdit
|
||||||
|
}
|
||||||
|
|
||||||
|
var req = reqVars{}
|
||||||
|
|
||||||
|
func setupReqTab() *widgets.QWidget {
|
||||||
|
tab := widgets.NewQWidget(nil, 0)
|
||||||
|
layout := widgets.NewQVBoxLayout()
|
||||||
|
tab.SetLayout(layout)
|
||||||
|
|
||||||
|
// authors
|
||||||
|
authorsLabel := widgets.NewQLabel2("authors:", nil, 0)
|
||||||
|
layout.AddWidget(authorsLabel, 0, 0)
|
||||||
|
authorsVBox := widgets.NewQVBoxLayout()
|
||||||
|
layout.AddLayout(authorsVBox, 0)
|
||||||
|
req.authorsEdits = []*widgets.QLineEdit{}
|
||||||
|
var addAuthorEdit func()
|
||||||
|
addAuthorEdit = func() {
|
||||||
|
edit := widgets.NewQLineEdit(nil)
|
||||||
|
req.authorsEdits = append(req.authorsEdits, edit)
|
||||||
|
authorsVBox.AddWidget(edit, 0, 0)
|
||||||
|
edit.ConnectTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == req.authorsEdits[len(req.authorsEdits)-1] {
|
||||||
|
addAuthorEdit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(req.authorsEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(req.authorsEdits[n-1].Text()) == "" && strings.TrimSpace(req.authorsEdits[n-2].Text()) == "" {
|
||||||
|
authorsVBox.Layout().RemoveWidget(req.authorsEdits[n-1])
|
||||||
|
req.authorsEdits[n-1].DeleteLater()
|
||||||
|
req.authorsEdits = req.authorsEdits[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addAuthorEdit()
|
||||||
|
|
||||||
|
// ids
|
||||||
|
idsLabel := widgets.NewQLabel2("ids:", nil, 0)
|
||||||
|
layout.AddWidget(idsLabel, 0, 0)
|
||||||
|
idsVBox := widgets.NewQVBoxLayout()
|
||||||
|
layout.AddLayout(idsVBox, 0)
|
||||||
|
req.idsEdits = []*widgets.QLineEdit{}
|
||||||
|
var addIdEdit func()
|
||||||
|
addIdEdit = func() {
|
||||||
|
edit := widgets.NewQLineEdit(nil)
|
||||||
|
req.idsEdits = append(req.idsEdits, edit)
|
||||||
|
idsVBox.AddWidget(edit, 0, 0)
|
||||||
|
edit.ConnectTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == req.idsEdits[len(req.idsEdits)-1] {
|
||||||
|
addIdEdit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(req.idsEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(req.idsEdits[n-1].Text()) == "" && strings.TrimSpace(req.idsEdits[n-2].Text()) == "" {
|
||||||
|
idsVBox.Layout().RemoveWidget(req.idsEdits[n-1])
|
||||||
|
req.idsEdits[n-1].DeleteLater()
|
||||||
|
req.idsEdits = req.idsEdits[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addIdEdit()
|
||||||
|
|
||||||
|
// kinds
|
||||||
|
kindsLabel := widgets.NewQLabel2("kinds:", nil, 0)
|
||||||
|
layout.AddWidget(kindsLabel, 0, 0)
|
||||||
|
kindsVBox := widgets.NewQVBoxLayout()
|
||||||
|
layout.AddLayout(kindsVBox, 0)
|
||||||
|
req.kindsEdits = []*widgets.QLineEdit{}
|
||||||
|
req.kindsLabels = []*widgets.QLabel{}
|
||||||
|
var addKindEdit func()
|
||||||
|
addKindEdit = func() {
|
||||||
|
hbox := widgets.NewQHBoxLayout()
|
||||||
|
kindsVBox.AddLayout(hbox, 0)
|
||||||
|
edit := widgets.NewQLineEdit(nil)
|
||||||
|
req.kindsEdits = append(req.kindsEdits, edit)
|
||||||
|
hbox.AddWidget(edit, 0, 0)
|
||||||
|
label := widgets.NewQLabel2("", nil, 0)
|
||||||
|
req.kindsLabels = append(req.kindsLabels, label)
|
||||||
|
hbox.AddWidget(label, 0, 0)
|
||||||
|
edit.ConnectTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == req.kindsEdits[len(req.kindsEdits)-1] {
|
||||||
|
addKindEdit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(req.kindsEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(req.kindsEdits[n-1].Text()) == "" && strings.TrimSpace(req.kindsEdits[n-2].Text()) == "" {
|
||||||
|
lastItem := kindsVBox.ItemAt(kindsVBox.Count() - 1)
|
||||||
|
kindsVBox.RemoveItem(lastItem)
|
||||||
|
lastHBox := lastItem.Layout()
|
||||||
|
lastHBox.RemoveWidget(req.kindsEdits[n-1])
|
||||||
|
lastHBox.RemoveWidget(req.kindsLabels[n-1])
|
||||||
|
req.kindsEdits[n-1].DeleteLater()
|
||||||
|
req.kindsLabels[n-1].DeleteLater()
|
||||||
|
lastHBox.DeleteLater()
|
||||||
|
req.kindsEdits = req.kindsEdits[0 : n-1]
|
||||||
|
req.kindsLabels = req.kindsLabels[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addKindEdit()
|
||||||
|
|
||||||
|
// since
|
||||||
|
sinceHBox := widgets.NewQHBoxLayout()
|
||||||
|
layout.AddLayout(sinceHBox, 0)
|
||||||
|
sinceLabel := widgets.NewQLabel2("since:", nil, 0)
|
||||||
|
sinceHBox.AddWidget(sinceLabel, 0, 0)
|
||||||
|
req.sinceEdit = widgets.NewQDateTimeEdit(nil)
|
||||||
|
{
|
||||||
|
time := core.NewQDateTime3(core.NewQDate3(0, 0, 0), core.NewQTime3(0, 0, 0, 0), 0)
|
||||||
|
time.SetMSecsSinceEpoch(0)
|
||||||
|
req.sinceEdit.SetDateTime(time)
|
||||||
|
}
|
||||||
|
sinceHBox.AddWidget(req.sinceEdit, 0, 0)
|
||||||
|
req.sinceEdit.ConnectDateTimeChanged(func(*core.QDateTime) {
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
|
||||||
|
// until
|
||||||
|
untilHBox := widgets.NewQHBoxLayout()
|
||||||
|
layout.AddLayout(untilHBox, 0)
|
||||||
|
untilLabel := widgets.NewQLabel2("until:", nil, 0)
|
||||||
|
untilHBox.AddWidget(untilLabel, 0, 0)
|
||||||
|
req.untilEdit = widgets.NewQDateTimeEdit(nil)
|
||||||
|
{
|
||||||
|
time := core.NewQDateTime3(core.NewQDate3(0, 0, 0), core.NewQTime3(0, 0, 0, 0), 0)
|
||||||
|
time.SetMSecsSinceEpoch(0)
|
||||||
|
req.untilEdit.SetDateTime(time)
|
||||||
|
}
|
||||||
|
untilHBox.AddWidget(req.untilEdit, 0, 0)
|
||||||
|
req.untilEdit.ConnectDateTimeChanged(func(*core.QDateTime) {
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
|
||||||
|
// limit
|
||||||
|
limitHBox := widgets.NewQHBoxLayout()
|
||||||
|
layout.AddLayout(limitHBox, 0)
|
||||||
|
limitLabel := widgets.NewQLabel2("limit:", nil, 0)
|
||||||
|
limitHBox.AddWidget(limitLabel, 0, 0)
|
||||||
|
req.limitSpin = widgets.NewQSpinBox(nil)
|
||||||
|
req.limitSpin.SetMinimum(0)
|
||||||
|
req.limitSpin.SetMaximum(1000)
|
||||||
|
limitHBox.AddWidget(req.limitSpin, 0, 0)
|
||||||
|
req.limitSpin.ConnectValueChanged(func(int) {
|
||||||
|
updateReq()
|
||||||
|
})
|
||||||
|
|
||||||
|
// output
|
||||||
|
outputLabel := widgets.NewQLabel2("filter:", nil, 0)
|
||||||
|
layout.AddWidget(outputLabel, 0, 0)
|
||||||
|
req.outputEdit = widgets.NewQTextEdit(nil)
|
||||||
|
req.outputEdit.SetReadOnly(true)
|
||||||
|
layout.AddWidget(req.outputEdit, 0, 0)
|
||||||
|
|
||||||
|
// relays
|
||||||
|
relaysLabel := widgets.NewQLabel2("relays:", nil, 0)
|
||||||
|
layout.AddWidget(relaysLabel, 0, 0)
|
||||||
|
relaysVBox := widgets.NewQVBoxLayout()
|
||||||
|
layout.AddLayout(relaysVBox, 0)
|
||||||
|
req.relaysEdits = []*widgets.QLineEdit{}
|
||||||
|
var addRelayEdit func()
|
||||||
|
addRelayEdit = func() {
|
||||||
|
edit := widgets.NewQLineEdit(nil)
|
||||||
|
req.relaysEdits = append(req.relaysEdits, edit)
|
||||||
|
relaysVBox.AddWidget(edit, 0, 0)
|
||||||
|
edit.ConnectTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == req.relaysEdits[len(req.relaysEdits)-1] {
|
||||||
|
addRelayEdit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(req.relaysEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(req.relaysEdits[n-1].Text()) == "" && strings.TrimSpace(req.relaysEdits[n-2].Text()) == "" {
|
||||||
|
relaysVBox.Layout().RemoveWidget(req.relaysEdits[n-1])
|
||||||
|
req.relaysEdits[n-1].DeleteLater()
|
||||||
|
req.relaysEdits = req.relaysEdits[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addRelayEdit()
|
||||||
|
|
||||||
|
// send button
|
||||||
|
buttonHBox := widgets.NewQHBoxLayout()
|
||||||
|
layout.AddLayout(buttonHBox, 0)
|
||||||
|
sendButton := widgets.NewQPushButton2("send request", nil)
|
||||||
|
buttonHBox.AddWidget(sendButton, 0, 0)
|
||||||
|
buttonHBox.AddStretch(1)
|
||||||
|
|
||||||
|
// results
|
||||||
|
resultsLabel := widgets.NewQLabel2("results:", nil, 0)
|
||||||
|
layout.AddWidget(resultsLabel, 0, 0)
|
||||||
|
req.resultsEdit = widgets.NewQTextEdit(nil)
|
||||||
|
req.resultsEdit.SetReadOnly(true)
|
||||||
|
layout.AddWidget(req.resultsEdit, 0, 0)
|
||||||
|
|
||||||
|
sendButton.ConnectClicked(func(checked bool) {
|
||||||
|
req.subscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReq() {
|
||||||
|
req.filter = nostr.Filter{}
|
||||||
|
|
||||||
|
// collect authors
|
||||||
|
authors := []nostr.PubKey{}
|
||||||
|
for _, edit := range req.authorsEdits {
|
||||||
|
if pk, err := nostr.PubKeyFromHex(strings.TrimSpace(edit.Text())); err == nil {
|
||||||
|
authors = append(authors, pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(authors) > 0 {
|
||||||
|
req.filter.Authors = authors
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect ids
|
||||||
|
ids := []nostr.ID{}
|
||||||
|
for _, edit := range req.idsEdits {
|
||||||
|
if id, err := nostr.IDFromHex(strings.TrimSpace(edit.Text())); err == nil {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
req.filter.IDs = ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect kinds
|
||||||
|
kinds := []nostr.Kind{}
|
||||||
|
for _, edit := range req.kindsEdits {
|
||||||
|
text := strings.TrimSpace(edit.Text())
|
||||||
|
if k, err := strconv.Atoi(text); err == nil {
|
||||||
|
kinds = append(kinds, nostr.Kind(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(kinds) > 0 {
|
||||||
|
req.filter.Kinds = kinds
|
||||||
|
}
|
||||||
|
|
||||||
|
// update kind labels
|
||||||
|
for i, kind := range kinds {
|
||||||
|
if i < len(req.kindsLabels) {
|
||||||
|
name := kind.Name()
|
||||||
|
if name != "unknown" {
|
||||||
|
req.kindsLabels[i].SetText(name)
|
||||||
|
} else {
|
||||||
|
req.kindsLabels[i].SetText("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := len(kinds); i < len(req.kindsLabels); i++ {
|
||||||
|
req.kindsLabels[i].SetText("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// since
|
||||||
|
if req.sinceEdit.DateTime().IsValid() {
|
||||||
|
ts := nostr.Timestamp(req.sinceEdit.DateTime().ToMSecsSinceEpoch() / 1000)
|
||||||
|
req.filter.Since = ts
|
||||||
|
}
|
||||||
|
|
||||||
|
// until
|
||||||
|
if req.untilEdit.DateTime().IsValid() {
|
||||||
|
ts := nostr.Timestamp(req.untilEdit.DateTime().ToMSecsSinceEpoch() / 1000)
|
||||||
|
req.filter.Until = ts
|
||||||
|
}
|
||||||
|
|
||||||
|
// limit
|
||||||
|
if req.limitSpin.Value() > 0 {
|
||||||
|
req.filter.Limit = req.limitSpin.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, _ := json.Marshal(req.filter)
|
||||||
|
req.outputEdit.SetPlainText(string(jsonBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (req reqVars) subscribe() {
|
||||||
|
// collect relays
|
||||||
|
relays := []string{}
|
||||||
|
for _, edit := range req.relaysEdits {
|
||||||
|
url := strings.TrimSpace(edit.Text())
|
||||||
|
if url != "" {
|
||||||
|
relays = append(relays, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(relays) == 0 {
|
||||||
|
statusLabel.SetText("no relays specified")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribe
|
||||||
|
statusLabel.SetText("subscribed to " + strings.Join(relays, " "))
|
||||||
|
eoseChan := make(chan struct{})
|
||||||
|
eventsChan := sys.Pool.SubscribeManyNotifyEOSE(ctx, relays, req.filter, eoseChan, nostr.SubscriptionOptions{
|
||||||
|
Label: "nakv-req",
|
||||||
|
})
|
||||||
|
|
||||||
|
// collect events
|
||||||
|
go func() {
|
||||||
|
eosed := false
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ie, ok := <-eventsChan:
|
||||||
|
if !ok {
|
||||||
|
statusLabel.SetText("subscription ended")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, _ := json.Marshal(ie.Event)
|
||||||
|
if eosed {
|
||||||
|
req.resultsEdit.SetPlainText(string(jsonBytes) + "\n" + req.resultsEdit.ToPlainText())
|
||||||
|
} else {
|
||||||
|
req.resultsEdit.InsertPlainText("\n" + string(jsonBytes))
|
||||||
|
}
|
||||||
|
case <-eoseChan:
|
||||||
|
eosed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user