mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-08 16:48:51 +00:00
Compare commits
4 Commits
210c0aa282
...
1dbe8ef2e5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dbe8ef2e5 | ||
|
|
85a04aa7ce | ||
|
|
e0ca768695 | ||
|
|
bef3739a67 |
@@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/keyer"
|
"fiatjaf.com/nostr/keyer"
|
||||||
"fiatjaf.com/nostr/nipb0/blossom"
|
"fiatjaf.com/nostr/nipb0/blossom"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -38,11 +37,11 @@ var blossomCmd = &cli.Command{
|
|||||||
var client *blossom.Client
|
var client *blossom.Client
|
||||||
pubkey := c.Args().First()
|
pubkey := c.Args().First()
|
||||||
if pubkey != "" {
|
if pubkey != "" {
|
||||||
if pk, err := nostr.PubKeyFromHex(pubkey); err != nil {
|
pk, err := parsePubKey(pubkey)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
return fmt.Errorf("invalid public key '%s': %w", pubkey, err)
|
||||||
} else {
|
|
||||||
client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk))
|
|
||||||
}
|
}
|
||||||
|
client = blossom.NewClient(c.String("server"), keyer.NewReadOnlySigner(pk))
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
client, err = getBlossomClient(ctx, c)
|
client, err = getBlossomClient(ctx, c)
|
||||||
|
|||||||
8
count.go
8
count.go
@@ -21,7 +21,7 @@ var count = &cli.Command{
|
|||||||
&PubKeySliceFlag{
|
&PubKeySliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "only accept events from these authors (pubkey as hex)",
|
Usage: "only accept events from these authors",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.IntSliceFlag{
|
&cli.IntSliceFlag{
|
||||||
@@ -101,16 +101,16 @@ var count = &cli.Command{
|
|||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.SplitN(tagFlag, "=", 2)
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, []string{spl[0], decodeTagValue(spl[1])})
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
tags = append(tags, []string{"e", etag})
|
tags = append(tags, []string{"e", decodeTagValue(etag)})
|
||||||
}
|
}
|
||||||
for _, ptag := range c.StringSlice("p") {
|
for _, ptag := range c.StringSlice("p") {
|
||||||
tags = append(tags, []string{"p", ptag})
|
tags = append(tags, []string{"p", decodeTagValue(ptag)})
|
||||||
}
|
}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
filter.Tags = make(nostr.TagMap)
|
filter.Tags = make(nostr.TagMap)
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ var encode = &cli.Command{
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
for target := range getStdinLinesOrArguments(c.Args()) {
|
for target := range getStdinLinesOrArguments(c.Args()) {
|
||||||
id, err := nostr.IDFromHex(target)
|
id, err := parseEventID(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
ctx = lineProcessingError(ctx, "invalid event id: %s", target)
|
||||||
continue
|
continue
|
||||||
|
|||||||
18
event.go
18
event.go
@@ -211,24 +211,30 @@ example:
|
|||||||
if found {
|
if found {
|
||||||
// tags may also contain extra elements separated with a ";"
|
// tags may also contain extra elements separated with a ";"
|
||||||
tagValues := strings.Split(tagValue, ";")
|
tagValues := strings.Split(tagValue, ";")
|
||||||
|
if len(tagValues) >= 1 {
|
||||||
|
tagValues[0] = decodeTagValue(tagValues[0])
|
||||||
|
}
|
||||||
tag = append(tag, tagValues...)
|
tag = append(tag, tagValues...)
|
||||||
}
|
}
|
||||||
tags = append(tags, tag)
|
tags = append(tags, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
if tags.FindWithValue("e", etag) == nil {
|
decodedEtag := decodeTagValue(etag)
|
||||||
tags = append(tags, nostr.Tag{"e", etag})
|
if tags.FindWithValue("e", decodedEtag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"e", decodedEtag})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, ptag := range c.StringSlice("p") {
|
for _, ptag := range c.StringSlice("p") {
|
||||||
if tags.FindWithValue("p", ptag) == nil {
|
decodedPtag := decodeTagValue(ptag)
|
||||||
tags = append(tags, nostr.Tag{"p", ptag})
|
if tags.FindWithValue("p", decodedPtag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"p", decodedPtag})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, dtag := range c.StringSlice("d") {
|
for _, dtag := range c.StringSlice("d") {
|
||||||
if tags.FindWithValue("d", dtag) == nil {
|
decodedDtag := decodeTagValue(dtag)
|
||||||
tags = append(tags, nostr.Tag{"d", dtag})
|
if tags.FindWithValue("d", decodedDtag) == nil {
|
||||||
|
tags = append(tags, nostr.Tag{"d", decodedDtag})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
|
|||||||
8
flags.go
8
flags.go
@@ -96,8 +96,8 @@ func (t pubkeyValue) Create(val nostr.PubKey, p *nostr.PubKey, c struct{}) cli.V
|
|||||||
func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() }
|
func (t pubkeyValue) ToString(b nostr.PubKey) string { return t.pubkey.String() }
|
||||||
|
|
||||||
func (t *pubkeyValue) Set(value string) error {
|
func (t *pubkeyValue) Set(value string) error {
|
||||||
pk, err := nostr.PubKeyFromHex(value)
|
pubkey, err := parsePubKey(value)
|
||||||
t.pubkey = pk
|
t.pubkey = pubkey
|
||||||
t.hasBeenSet = true
|
t.hasBeenSet = true
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -147,8 +147,8 @@ func (t idValue) Create(val nostr.ID, p *nostr.ID, c struct{}) cli.Value {
|
|||||||
func (t idValue) ToString(b nostr.ID) string { return t.id.String() }
|
func (t idValue) ToString(b nostr.ID) string { return t.id.String() }
|
||||||
|
|
||||||
func (t *idValue) Set(value string) error {
|
func (t *idValue) Set(value string) error {
|
||||||
pk, err := nostr.IDFromHex(value)
|
id, err := parseEventID(value)
|
||||||
t.id = pk
|
t.id = id
|
||||||
t.hasBeenSet = true
|
t.hasBeenSet = true
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
737
git.go
Normal file
737
git.go
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip34"
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Nip34Config struct {
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Web []string `json:"web"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
GraspServers []string `json:"grasp-servers"`
|
||||||
|
EarliestUniqueCommit string `json:"earliest-unique-commit"`
|
||||||
|
Maintainers []string `json:"maintainers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var git = &cli.Command{
|
||||||
|
Name: "git",
|
||||||
|
Usage: "git-related operations",
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
gitInit,
|
||||||
|
gitPush,
|
||||||
|
gitPull,
|
||||||
|
gitFetch,
|
||||||
|
gitAnnounce,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitInit = &cli.Command{
|
||||||
|
Name: "init",
|
||||||
|
Usage: "initialize a NIP-34 repository configuration",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "interactive",
|
||||||
|
Aliases: []string{"i"},
|
||||||
|
Usage: "prompt for repository details interactively",
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Aliases: []string{"f"},
|
||||||
|
Usage: "overwrite existing nip34.json file",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "identifier",
|
||||||
|
Usage: "unique identifier for the repository",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Usage: "repository name",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "description",
|
||||||
|
Usage: "repository description",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "web",
|
||||||
|
Usage: "web URLs for the repository (can be used multiple times)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "owner",
|
||||||
|
Usage: "owner public key",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "grasp-servers",
|
||||||
|
Usage: "grasp servers (can be used multiple times)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "relays",
|
||||||
|
Usage: "relay URLs to publish to (can be used multiple times)",
|
||||||
|
},
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "maintainers",
|
||||||
|
Usage: "maintainer public keys as npub or hex (can be used multiple times)",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "earliest-unique-commit",
|
||||||
|
Usage: "earliest unique commit of the repository",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// check if current directory is a git repository
|
||||||
|
cmd := exec.Command("git", "rev-parse", "--git-dir")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("current directory is not a git repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if nip34.json already exists
|
||||||
|
configPath := "nip34.json"
|
||||||
|
var existingConfig Nip34Config
|
||||||
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
|
// file exists, read it
|
||||||
|
if !c.Bool("force") && !c.Bool("interactive") {
|
||||||
|
return fmt.Errorf("nip34.json already exists, use --force to overwrite or --interactive to update")
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &existingConfig); err != nil {
|
||||||
|
log("Warning: could not parse existing nip34.json: %v\n", err)
|
||||||
|
existingConfig = Nip34Config{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get repository base directory name for defaults
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract clone URLs from nostr:// git remotes
|
||||||
|
var defaultCloneURLs []string
|
||||||
|
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 strings.HasPrefix(nostrURL, "nostr://") {
|
||||||
|
urlParts := strings.TrimPrefix(nostrURL, "nostr://")
|
||||||
|
components := strings.Split(urlParts, "/")
|
||||||
|
if len(components) == 3 {
|
||||||
|
npub := components[0]
|
||||||
|
relayHostname := components[1]
|
||||||
|
identifier := components[2]
|
||||||
|
// convert to https://relay_hostname/npub.../identifier.git
|
||||||
|
cloneURL := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(relayHostname)[2:], npub, identifier)
|
||||||
|
defaultCloneURLs = appendUnique(defaultCloneURLs, cloneURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper to get value from flags, existing config, or default
|
||||||
|
getValue := func(existingVal, flagVal, defaultVal string) string {
|
||||||
|
if flagVal != "" {
|
||||||
|
return flagVal
|
||||||
|
}
|
||||||
|
if existingVal != "" {
|
||||||
|
return existingVal
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
getSliceValue := func(existingVals, flagVals, defaultVals []string) []string {
|
||||||
|
if len(flagVals) > 0 {
|
||||||
|
return flagVals
|
||||||
|
}
|
||||||
|
if len(existingVals) > 0 {
|
||||||
|
return existingVals
|
||||||
|
}
|
||||||
|
return defaultVals
|
||||||
|
}
|
||||||
|
|
||||||
|
config := Nip34Config{
|
||||||
|
Identifier: getValue(existingConfig.Identifier, c.String("identifier"), baseName),
|
||||||
|
Name: getValue(existingConfig.Name, c.String("name"), baseName),
|
||||||
|
Description: getValue(existingConfig.Description, c.String("description"), ""),
|
||||||
|
Web: getSliceValue(existingConfig.Web, c.StringSlice("web"), []string{}),
|
||||||
|
Owner: getValue(existingConfig.Owner, c.String("owner"), ""),
|
||||||
|
GraspServers: getSliceValue(existingConfig.GraspServers, c.StringSlice("grasp-servers"), []string{"gitnostr.com", "relay.ngit.dev"}),
|
||||||
|
EarliestUniqueCommit: getValue(existingConfig.EarliestUniqueCommit, c.String("earliest-unique-commit"), earliestCommit),
|
||||||
|
Maintainers: getSliceValue(existingConfig.Maintainers, c.StringSlice("maintainers"), []string{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Bool("interactive") {
|
||||||
|
if err := promptForConfig(&config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write config file
|
||||||
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write nip34.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Created %s\n", color.GreenString(configPath))
|
||||||
|
|
||||||
|
// parse owner to npub
|
||||||
|
pk, err := parsePubKey(config.Owner)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid owner public key: %w", err)
|
||||||
|
}
|
||||||
|
ownerNpub := nip19.EncodeNpub(pk)
|
||||||
|
|
||||||
|
// check existing git remotes
|
||||||
|
cmd = exec.Command("git", "remote", "-v")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get git remotes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
var nostrRemote string
|
||||||
|
for _, remote := range remotes {
|
||||||
|
if strings.Contains(remote, "nostr://") {
|
||||||
|
parts := strings.Fields(remote)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
nostrRemote = parts[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nostrRemote == "" {
|
||||||
|
// set the remote
|
||||||
|
remoteURL := fmt.Sprintf("nostr://%s/%s/%s", ownerNpub, config.GraspServers[0], config.Identifier)
|
||||||
|
cmd = exec.Command("git", "remote", "add", "origin", remoteURL)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to add git remote: %w", err)
|
||||||
|
}
|
||||||
|
log("Added git remote: %s\n", remoteURL)
|
||||||
|
} else {
|
||||||
|
// validate existing remote
|
||||||
|
if !strings.HasPrefix(nostrRemote, "nostr://") {
|
||||||
|
return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote)
|
||||||
|
}
|
||||||
|
urlParts := strings.TrimPrefix(nostrRemote, "nostr://")
|
||||||
|
parts := strings.Split(urlParts, "/")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return fmt.Errorf("invalid nostr URL format, expected nostr://<npub>/<relay_hostname>/<identifier>, got: %s", nostrRemote)
|
||||||
|
}
|
||||||
|
repoNpub := parts[0]
|
||||||
|
relayHostname := parts[1]
|
||||||
|
identifier := parts[2]
|
||||||
|
if repoNpub != ownerNpub {
|
||||||
|
return fmt.Errorf("git remote npub '%s' does not match owner '%s'", repoNpub, ownerNpub)
|
||||||
|
}
|
||||||
|
if !slices.Contains(config.GraspServers, relayHostname) {
|
||||||
|
return fmt.Errorf("git remote relay '%s' not in grasp servers %v", relayHostname, config.GraspServers)
|
||||||
|
}
|
||||||
|
if identifier != config.Identifier {
|
||||||
|
return fmt.Errorf("git remote identifier '%s' does not match config '%s'", identifier, config.Identifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitignore it
|
||||||
|
excludePath := ".git/info/exclude"
|
||||||
|
excludeContent, err := os.ReadFile(excludePath)
|
||||||
|
if err != nil {
|
||||||
|
// file doesn't exist, create it
|
||||||
|
excludeContent = []byte("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if nip34.json is already in exclude
|
||||||
|
if !strings.Contains(string(excludeContent), "nip34.json") {
|
||||||
|
newContent := string(excludeContent)
|
||||||
|
if len(newContent) > 0 && !strings.HasSuffix(newContent, "\n") {
|
||||||
|
newContent += "\n"
|
||||||
|
}
|
||||||
|
newContent += "nip34.json\n"
|
||||||
|
if err := os.WriteFile(excludePath, []byte(newContent), 0644); err != nil {
|
||||||
|
log(color.YellowString("failed to add nip34.json to .git/info/exclude: %v\n", err))
|
||||||
|
} else {
|
||||||
|
log("added nip34.json to %s\n", color.GreenString(".git/info/exclude"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log("edit %s if needed, then run %s to publish.\n",
|
||||||
|
color.CyanString("nip34.json"),
|
||||||
|
color.CyanString("nak git announce"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func repositoriesEqual(a, b nip34.Repository) bool {
|
||||||
|
if a.ID != b.ID || a.Name != b.Name || a.Description != b.Description {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a.EarliestUniqueCommitID != b.EarliestUniqueCommitID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a.Web) != len(b.Web) || len(a.Clone) != len(b.Clone) ||
|
||||||
|
len(a.Relays) != len(b.Relays) || len(a.Maintainers) != len(b.Maintainers) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a.Web {
|
||||||
|
if a.Web[i] != b.Web[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range a.Clone {
|
||||||
|
if a.Clone[i] != b.Clone[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range a.Relays {
|
||||||
|
if a.Relays[i] != b.Relays[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range a.Maintainers {
|
||||||
|
if a.Maintainers[i] != b.Maintainers[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptForConfig(config *Nip34Config) error {
|
||||||
|
rlConfig := &readline.Config{
|
||||||
|
Stdout: os.Stderr,
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
rl, err := readline.NewEx(rlConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rl.Close()
|
||||||
|
|
||||||
|
promptString := func(currentVal *string, prompt string) error {
|
||||||
|
rl.SetPrompt(color.YellowString("%s [%s]: ", prompt, *currentVal))
|
||||||
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if answer != "" {
|
||||||
|
*currentVal = answer
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
promptSlice := func(currentVal *[]string, prompt string) error {
|
||||||
|
defaultStr := strings.Join(*currentVal, ", ")
|
||||||
|
rl.SetPrompt(color.YellowString("%s (comma-separated) [%s]: ", prompt, defaultStr))
|
||||||
|
answer, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
answer = strings.TrimSpace(answer)
|
||||||
|
if answer != "" {
|
||||||
|
parts := strings.Split(answer, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if trimmed := strings.TrimSpace(p); trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*currentVal = result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log("\nenter repository details (press Enter to keep default):\n\n")
|
||||||
|
|
||||||
|
if err := promptString(&config.Identifier, "identifier"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptString(&config.Name, "name"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptString(&config.Description, "description"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptString(&config.Owner, "owner (npub or hex)"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptSlice(&config.GraspServers, "grasp servers"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptSlice(&config.Web, "web URLs"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := promptSlice(&config.Maintainers, "other maintainers"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log("\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitSanityCheck(localConfig Nip34Config, nostrRemote string) (nostr.PubKey, error) {
|
||||||
|
urlParts := strings.TrimPrefix(nostrRemote, "nostr://")
|
||||||
|
parts := strings.Split(urlParts, "/")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("invalid nostr URL format, expected nostr://<npub>/<relay_hostname>/<identifier>, got: %s", nostrRemote)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteNpub := parts[0]
|
||||||
|
remoteHostname := parts[1]
|
||||||
|
remoteIdentifier := parts[2]
|
||||||
|
|
||||||
|
ownerPk, err := parsePubKey(localConfig.Owner)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("invalid owner public key: %w", err)
|
||||||
|
}
|
||||||
|
if nip19.EncodeNpub(ownerPk) != remoteNpub {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("owner in nip34.json does not match git remote npub")
|
||||||
|
}
|
||||||
|
if remoteIdentifier != localConfig.Identifier {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("git remote identifier '%s' differs from nip34.json identifier '%s'", remoteIdentifier, localConfig.Identifier)
|
||||||
|
}
|
||||||
|
if !slices.Contains(localConfig.GraspServers, remoteHostname) {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("git remote relay '%s' not in grasp servers %v", remoteHostname, localConfig.GraspServers)
|
||||||
|
}
|
||||||
|
return ownerPk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitPush = &cli.Command{
|
||||||
|
Name: "push",
|
||||||
|
Usage: "push git changes",
|
||||||
|
Flags: defaultKeyFlags,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// setup signer
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to gather keyer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// log publishing as npub
|
||||||
|
currentPk, _ := kr.GetPublicKey(ctx)
|
||||||
|
currentNpub := nip19.EncodeNpub(currentPk)
|
||||||
|
log("publishing as %s\n", color.CyanString(currentNpub))
|
||||||
|
|
||||||
|
// read nip34.json configuration
|
||||||
|
configPath := "nip34.json"
|
||||||
|
var localConfig Nip34Config
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &localConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse nip34.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get git remotes
|
||||||
|
cmd := exec.Command("git", "remote", "-v")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get git remotes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
var nostrRemote string
|
||||||
|
for _, remote := range remotes {
|
||||||
|
if strings.Contains(remote, "nostr://") {
|
||||||
|
parts := strings.Fields(remote)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
nostrRemote = parts[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nostrRemote == "" {
|
||||||
|
return fmt.Errorf("no nostr:// remote found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the URL: nostr://<npub>/<relay_hostname>/<identifier>
|
||||||
|
if !strings.HasPrefix(nostrRemote, "nostr://") {
|
||||||
|
return fmt.Errorf("invalid nostr remote URL: %s", nostrRemote)
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerPk, err := gitSanityCheck(localConfig, nostrRemote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch repository announcement (30617) and state (30618) events
|
||||||
|
var repo nip34.Repository
|
||||||
|
var state nip34.RepositoryState
|
||||||
|
relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...)
|
||||||
|
results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{30617, 30618},
|
||||||
|
Tags: nostr.TagMap{
|
||||||
|
"d": []string{localConfig.Identifier},
|
||||||
|
},
|
||||||
|
Limit: 2,
|
||||||
|
}, nostr.SubscriptionOptions{
|
||||||
|
Label: "nak-git-push",
|
||||||
|
})
|
||||||
|
for ie := range results {
|
||||||
|
if ie.Event.Kind == 30617 {
|
||||||
|
repo = nip34.ParseRepository(ie.Event)
|
||||||
|
} else if ie.Event.Kind == 30618 {
|
||||||
|
state = nip34.ParseRepositoryState(ie.Event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Event.ID == nostr.ZeroID {
|
||||||
|
return fmt.Errorf("no existing repository announcement found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if signer matches owner or is in maintainers
|
||||||
|
if currentPk != ownerPk && !slices.Contains(repo.Maintainers, currentPk) {
|
||||||
|
return fmt.Errorf("current user is not allowed to push")
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.Event.ID != nostr.ZeroID {
|
||||||
|
log("found state event: %s\n", state.Event.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current branch and commit
|
||||||
|
res, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current branch: %w", err)
|
||||||
|
}
|
||||||
|
currentBranch := strings.TrimSpace(string(res))
|
||||||
|
|
||||||
|
res, err = exec.Command("git", "rev-parse", "HEAD").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get current commit: %w", err)
|
||||||
|
}
|
||||||
|
currentCommit := strings.TrimSpace(string(res))
|
||||||
|
|
||||||
|
log("Current branch: %s, commit: %s\n", currentBranch, currentCommit)
|
||||||
|
|
||||||
|
// create a new state if we didn't find any
|
||||||
|
if state.Event.ID == nostr.ZeroID {
|
||||||
|
state = nip34.RepositoryState{
|
||||||
|
ID: repo.ID,
|
||||||
|
Branches: make(map[string]string),
|
||||||
|
Tags: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the branch
|
||||||
|
state.Branches[currentBranch] = currentCommit
|
||||||
|
log("> setting branch %s to commit %s\n", currentBranch, currentCommit)
|
||||||
|
|
||||||
|
// set the HEAD to the current branch if none is set
|
||||||
|
if state.HEAD == "" {
|
||||||
|
state.HEAD = currentBranch
|
||||||
|
log("> setting HEAD to branch %s\n", currentBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create and sign the new state event
|
||||||
|
newStateEvent := state.ToEvent()
|
||||||
|
err = kr.SignEvent(ctx, &newStateEvent)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error signing state event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("> publishing updated repository state %s\n", newStateEvent.ID)
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, newStateEvent) {
|
||||||
|
if res.Error != nil {
|
||||||
|
log("(!) error publishing event to relay %s: %v\n", res.RelayURL, res.Error)
|
||||||
|
} else {
|
||||||
|
log("> published to relay %s\n", res.RelayURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// push to git clone URLs
|
||||||
|
for _, cloneURL := range repo.Clone {
|
||||||
|
log("> pushing to: %s\n", cloneURL)
|
||||||
|
cmd := exec.Command("git", "push", cloneURL, fmt.Sprintf("refs/heads/%s:refs/heads/%s", currentBranch, currentBranch))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log("(!) failed to push to %s: %v\n%s\n", cloneURL, err, string(output))
|
||||||
|
} else {
|
||||||
|
log("> successfully pushed to %s\n", cloneURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitPull = &cli.Command{
|
||||||
|
Name: "pull",
|
||||||
|
Usage: "pull git changes",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
return fmt.Errorf("git pull not implemented yet")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitFetch = &cli.Command{
|
||||||
|
Name: "fetch",
|
||||||
|
Usage: "fetch git data",
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
return fmt.Errorf("git fetch not implemented yet")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var gitAnnounce = &cli.Command{
|
||||||
|
Name: "announce",
|
||||||
|
Usage: "announce repository to Nostr",
|
||||||
|
Flags: defaultKeyFlags,
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
// check if current directory is a git repository
|
||||||
|
cmd := exec.Command("git", "rev-parse", "--git-dir")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("current directory is not a git repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read nip34.json configuration
|
||||||
|
configPath := "nip34.json"
|
||||||
|
var localConfig Nip34Config
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read nip34.json: %w (run 'nak git init' first)", err)
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &localConfig); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse nip34.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get git remotes
|
||||||
|
cmd = exec.Command("git", "remote", "-v")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get git remotes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotes := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
var nostrRemote string
|
||||||
|
for _, remote := range remotes {
|
||||||
|
if strings.Contains(remote, "nostr://") {
|
||||||
|
parts := strings.Fields(remote)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
nostrRemote = parts[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nostrRemote == "" {
|
||||||
|
return fmt.Errorf("no nostr:// remote found")
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerPk, err := gitSanityCheck(localConfig, nostrRemote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup signer
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to gather keyer: %w", err)
|
||||||
|
}
|
||||||
|
currentPk, _ := kr.GetPublicKey(ctx)
|
||||||
|
|
||||||
|
// current signer must match owner otherwise we can't announce
|
||||||
|
if currentPk != ownerPk {
|
||||||
|
return fmt.Errorf("current user is not the owner of this repository, can't announce")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert local config to nip34.Repository
|
||||||
|
localRepo := nip34.Repository{
|
||||||
|
ID: localConfig.Identifier,
|
||||||
|
Name: localConfig.Name,
|
||||||
|
Description: localConfig.Description,
|
||||||
|
Web: localConfig.Web,
|
||||||
|
Relays: localConfig.GraspServers,
|
||||||
|
EarliestUniqueCommitID: localConfig.EarliestUniqueCommit,
|
||||||
|
Maintainers: []nostr.PubKey{},
|
||||||
|
}
|
||||||
|
for _, server := range localConfig.GraspServers {
|
||||||
|
url := fmt.Sprintf("http%s/%s/%s.git", nostr.NormalizeURL(server)[2:], nip19.EncodeNpub(ownerPk), localConfig.Identifier)
|
||||||
|
localRepo.Clone = append(localRepo.Clone, url)
|
||||||
|
}
|
||||||
|
for _, maintainer := range localConfig.Maintainers {
|
||||||
|
if pk, err := parsePubKey(maintainer); err == nil {
|
||||||
|
localRepo.Maintainers = append(localRepo.Maintainers, pk)
|
||||||
|
} else {
|
||||||
|
log(color.YellowString("invalid maintainer pubkey '%s': %v\n", maintainer, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch repository announcement (30617) events
|
||||||
|
var repo nip34.Repository
|
||||||
|
relays := append(sys.FetchOutboxRelays(ctx, ownerPk, 3), localConfig.GraspServers...)
|
||||||
|
results := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{30617},
|
||||||
|
Tags: nostr.TagMap{
|
||||||
|
"d": []string{localConfig.Identifier},
|
||||||
|
},
|
||||||
|
Limit: 1,
|
||||||
|
}, nostr.SubscriptionOptions{
|
||||||
|
Label: "nak-git-announce",
|
||||||
|
})
|
||||||
|
for ie := range results {
|
||||||
|
repo = nip34.ParseRepository(ie.Event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// publish repository announcement if needed
|
||||||
|
var needsAnnouncement bool
|
||||||
|
if repo.Event.ID == nostr.ZeroID {
|
||||||
|
log("no existing repository announcement found, will create one\n")
|
||||||
|
needsAnnouncement = true
|
||||||
|
} else if !repositoriesEqual(repo, localRepo) {
|
||||||
|
log("Local repository config differs from published announcement, will update\n")
|
||||||
|
needsAnnouncement = true
|
||||||
|
}
|
||||||
|
if needsAnnouncement {
|
||||||
|
announcementEvent := localRepo.ToEvent()
|
||||||
|
if err := kr.SignEvent(ctx, &announcementEvent); err != nil {
|
||||||
|
return fmt.Errorf("failed to sign announcement event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log("> publishing repository announcement %s\n", announcementEvent.ID)
|
||||||
|
for res := range sys.Pool.PublishMany(ctx, relays, announcementEvent) {
|
||||||
|
if res.Error != nil {
|
||||||
|
log("(!) error publishing announcement to relay %s: %v\n", res.RelayURL, res.Error)
|
||||||
|
} else {
|
||||||
|
log("> published announcement to relay %s\n", res.RelayURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log("Repository announcement is up to date\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -4,7 +4,7 @@ go 1.24.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.1
|
fiatjaf.com/lib v0.3.1
|
||||||
fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954
|
fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643
|
||||||
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/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||||
|
|||||||
4
go.sum
4
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-20251104112613-38a6ca92b954 h1:CMD8D3TgEjGhuIBNMnvZ0EXOW0JR9O3w8AI6Yuzt8Ec=
|
fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643 h1:GcoAN1FQV+rayCIklvj+mIB/ZR3Oni98C3bS/M+vzts=
|
||||||
fiatjaf.com/nostr v0.0.0-20251104112613-38a6ca92b954/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss=
|
fiatjaf.com/nostr v0.0.0-20251112024900-1c43f0d66643/go.mod h1:Nq86Jjsd0OmsOEImUg0iCcLuqM5B67Nj2eu/2dP74Ss=
|
||||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
github.com/FastFilter/xorfilter v0.2.1/go.mod h1:aumvdkhscz6YBZF9ZA/6O4fIoNod4YR50kIVGGZ7l9I=
|
||||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||||
|
|||||||
53
helpers.go
53
helpers.go
@@ -464,6 +464,59 @@ func askConfirmation(msg string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsePubKey(value string) (nostr.PubKey, error) {
|
||||||
|
pk, err := nostr.PubKeyFromHex(value)
|
||||||
|
if err == nil {
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, decoded, err := nip19.Decode(value); err == nil {
|
||||||
|
switch prefix {
|
||||||
|
case "npub":
|
||||||
|
if pk, ok := decoded.(nostr.PubKey); ok {
|
||||||
|
return pk, nil
|
||||||
|
}
|
||||||
|
case "nprofile":
|
||||||
|
if profile, ok := decoded.(nostr.ProfilePointer); ok {
|
||||||
|
return profile.PublicKey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nostr.PubKey{}, fmt.Errorf("invalid pubkey (\"%s\"): expected hex, npub, or nprofile", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEventID(value string) (nostr.ID, error) {
|
||||||
|
id, err := nostr.IDFromHex(value)
|
||||||
|
if err == nil {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix, decoded, err := nip19.Decode(value); err == nil {
|
||||||
|
switch prefix {
|
||||||
|
case "note":
|
||||||
|
if id, ok := decoded.(nostr.ID); ok {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
case "nevent":
|
||||||
|
if event, ok := decoded.(nostr.EventPointer); ok {
|
||||||
|
return event.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nostr.ID{}, fmt.Errorf("invalid event id (\"%s\"): expected hex, note, or nevent", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeTagValue(value string) string {
|
||||||
|
if strings.HasPrefix(value, "npub1") || strings.HasPrefix(value, "nevent1") || strings.HasPrefix(value, "note1") || strings.HasPrefix(value, "nprofile1") || strings.HasPrefix(value, "naddr1") {
|
||||||
|
if ptr, err := nip19.ToPointer(value); err == nil {
|
||||||
|
return ptr.AsTagReference()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
var colors = struct {
|
var colors = struct {
|
||||||
reset func(...any) (int, error)
|
reset func(...any) (int, error)
|
||||||
italic func(...any) string
|
italic func(...any) string
|
||||||
|
|||||||
1
main.go
1
main.go
@@ -48,6 +48,7 @@ var app = &cli.Command{
|
|||||||
curl,
|
curl,
|
||||||
fsCmd,
|
fsCmd,
|
||||||
publish,
|
publish,
|
||||||
|
git,
|
||||||
},
|
},
|
||||||
Version: version,
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
|
||||||
"fiatjaf.com/nostr/sdk"
|
"fiatjaf.com/nostr/sdk"
|
||||||
"fiatjaf.com/nostr/sdk/hints/bbolth"
|
"fiatjaf.com/nostr/sdk/hints/bbolth"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
@@ -82,7 +81,7 @@ var outbox = &cli.Command{
|
|||||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
}
|
}
|
||||||
|
|
||||||
pk, err := nostr.PubKeyFromHex(c.Args().First())
|
pk, err := parsePubKey(c.Args().First())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
return fmt.Errorf("invalid public key '%s': %w", c.Args().First(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
110
req.go
110
req.go
@@ -1,14 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/eventstore"
|
||||||
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
|
"fiatjaf.com/nostr/eventstore/wrappers"
|
||||||
"fiatjaf.com/nostr/nip42"
|
"fiatjaf.com/nostr/nip42"
|
||||||
"fiatjaf.com/nostr/nip77"
|
"fiatjaf.com/nostr/nip77"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
@@ -38,6 +43,11 @@ example:
|
|||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Flags: append(defaultKeyFlags,
|
Flags: append(defaultKeyFlags,
|
||||||
append(reqFilterFlags,
|
append(reqFilterFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "only-missing",
|
||||||
|
Usage: "use nip77 negentropy to only fetch events that aren't present in the given jsonl file",
|
||||||
|
TakesFile: true,
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "ids-only",
|
Name: "ids-only",
|
||||||
Usage: "use nip77 to fetch just a list of ids",
|
Usage: "use nip77 to fetch just a list of ids",
|
||||||
@@ -53,7 +63,7 @@ example:
|
|||||||
DefaultText: "false, will only use manually-specified relays",
|
DefaultText: "false, will only use manually-specified relays",
|
||||||
},
|
},
|
||||||
&cli.UintFlag{
|
&cli.UintFlag{
|
||||||
Name: "outbox-relays-number",
|
Name: "outbox-relays-per-pubkey",
|
||||||
Aliases: []string{"n"},
|
Aliases: []string{"n"},
|
||||||
Usage: "number of outbox relays to use for each pubkey",
|
Usage: "number of outbox relays to use for each pubkey",
|
||||||
Value: 3,
|
Value: 3,
|
||||||
@@ -90,6 +100,13 @@ example:
|
|||||||
),
|
),
|
||||||
ArgsUsage: "[relay...]",
|
ArgsUsage: "[relay...]",
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
negentropy := c.Bool("ids-only") || c.IsSet("only-missing")
|
||||||
|
if negentropy {
|
||||||
|
if c.Bool("paginate") || c.Bool("stream") || c.Bool("outbox") {
|
||||||
|
return fmt.Errorf("negentropy is incompatible with --stream, --outbox or --paginate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c.Bool("paginate") && c.Bool("stream") {
|
if c.Bool("paginate") && c.Bool("stream") {
|
||||||
return fmt.Errorf("incompatible flags --paginate and --stream")
|
return fmt.Errorf("incompatible flags --paginate and --stream")
|
||||||
}
|
}
|
||||||
@@ -99,7 +116,7 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
relayUrls := c.Args().Slice()
|
relayUrls := c.Args().Slice()
|
||||||
if len(relayUrls) > 0 {
|
if len(relayUrls) > 0 && !negentropy {
|
||||||
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
// this is used both for the normal AUTH (after "auth-required:" is received) or forced pre-auth
|
||||||
// connect to all relays we expect to use in this call in parallel
|
// connect to all relays we expect to use in this call in parallel
|
||||||
forcePreAuthSigner := authSigner
|
forcePreAuthSigner := authSigner
|
||||||
@@ -152,20 +169,60 @@ example:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(relayUrls) > 0 || c.Bool("outbox") {
|
if len(relayUrls) > 0 || c.Bool("outbox") {
|
||||||
if c.Bool("ids-only") {
|
if negentropy {
|
||||||
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
store := &slicestore.SliceStore{}
|
||||||
for _, url := range relayUrls {
|
store.Init()
|
||||||
ch, err := nip77.FetchIDsOnly(ctx, url, filter)
|
|
||||||
|
if syncFile := c.String("only-missing"); syncFile != "" {
|
||||||
|
file, err := os.Open(syncFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("negentropy call to %s failed: %s", url, err)
|
return fmt.Errorf("failed to open sync file: %w", err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
for id := range ch {
|
defer file.Close()
|
||||||
if _, ok := seen[id]; ok {
|
scanner := bufio.NewScanner(file)
|
||||||
|
scanner.Buffer(make([]byte, 16*1024*1024), 256*1024*1024)
|
||||||
|
for scanner.Scan() {
|
||||||
|
var evt nostr.Event
|
||||||
|
if err := easyjson.Unmarshal([]byte(scanner.Text()), &evt); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[id] = struct{}{}
|
if err := store.SaveEvent(evt); err != nil || err == eventstore.ErrDupEvent {
|
||||||
stdout(id)
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("failed to read sync file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
target := PrintingQuerierPublisher{
|
||||||
|
QuerierPublisher: wrappers.StorePublisher{Store: store, MaxLimit: math.MaxInt},
|
||||||
|
}
|
||||||
|
|
||||||
|
var source nostr.Querier = nil
|
||||||
|
if c.IsSet("only-missing") {
|
||||||
|
source = target
|
||||||
|
}
|
||||||
|
|
||||||
|
handle := nip77.SyncEventsFromIDs
|
||||||
|
|
||||||
|
if c.Bool("ids-only") {
|
||||||
|
seen := make(map[nostr.ID]struct{}, max(500, filter.Limit))
|
||||||
|
handle = func(ctx context.Context, dir nip77.Direction) {
|
||||||
|
for id := range dir.Items {
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
stdout(id.Hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, url := range relayUrls {
|
||||||
|
err := nip77.NegentropySync(ctx, url, filter, source, target, handle)
|
||||||
|
if err != nil {
|
||||||
|
log("negentropy sync from %s failed: %s", url, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -194,7 +251,7 @@ example:
|
|||||||
mu := sync.Mutex{}
|
mu := sync.Mutex{}
|
||||||
for _, pubkey := range filter.Authors {
|
for _, pubkey := range filter.Authors {
|
||||||
errg.Go(func() error {
|
errg.Go(func() error {
|
||||||
n := int(c.Uint("outbox-relays-number"))
|
n := int(c.Uint("outbox-relays-per-pubkey"))
|
||||||
for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) {
|
for _, url := range sys.FetchOutboxRelays(ctx, pubkey, n) {
|
||||||
if slices.Contains(relayUrls, url) {
|
if slices.Contains(relayUrls, url) {
|
||||||
// already hardcoded, ignore
|
// already hardcoded, ignore
|
||||||
@@ -276,13 +333,13 @@ var reqFilterFlags = []cli.Flag{
|
|||||||
&PubKeySliceFlag{
|
&PubKeySliceFlag{
|
||||||
Name: "author",
|
Name: "author",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "only accept events from these authors (pubkey as hex)",
|
Usage: "only accept events from these authors",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&IDSliceFlag{
|
&IDSliceFlag{
|
||||||
Name: "id",
|
Name: "id",
|
||||||
Aliases: []string{"i"},
|
Aliases: []string{"i"},
|
||||||
Usage: "only accept events with these ids (hex)",
|
Usage: "only accept events with these ids",
|
||||||
Category: CATEGORY_FILTER_ATTRIBUTES,
|
Category: CATEGORY_FILTER_ATTRIBUTES,
|
||||||
},
|
},
|
||||||
&cli.IntSliceFlag{
|
&cli.IntSliceFlag{
|
||||||
@@ -354,19 +411,19 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
|||||||
for _, tagFlag := range c.StringSlice("tag") {
|
for _, tagFlag := range c.StringSlice("tag") {
|
||||||
spl := strings.SplitN(tagFlag, "=", 2)
|
spl := strings.SplitN(tagFlag, "=", 2)
|
||||||
if len(spl) == 2 {
|
if len(spl) == 2 {
|
||||||
tags = append(tags, spl)
|
tags = append(tags, []string{spl[0], decodeTagValue(spl[1])})
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
return fmt.Errorf("invalid --tag '%s'", tagFlag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, etag := range c.StringSlice("e") {
|
for _, etag := range c.StringSlice("e") {
|
||||||
tags = append(tags, []string{"e", etag})
|
tags = append(tags, []string{"e", decodeTagValue(etag)})
|
||||||
}
|
}
|
||||||
for _, ptag := range c.StringSlice("p") {
|
for _, ptag := range c.StringSlice("p") {
|
||||||
tags = append(tags, []string{"p", ptag})
|
tags = append(tags, []string{"p", decodeTagValue(ptag)})
|
||||||
}
|
}
|
||||||
for _, dtag := range c.StringSlice("d") {
|
for _, dtag := range c.StringSlice("d") {
|
||||||
tags = append(tags, []string{"d", dtag})
|
tags = append(tags, []string{"d", decodeTagValue(dtag)})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tags) > 0 && filter.Tags == nil {
|
if len(tags) > 0 && filter.Tags == nil {
|
||||||
@@ -395,3 +452,18 @@ func applyFlagsToFilter(c *cli.Command, filter *nostr.Filter) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PrintingQuerierPublisher struct {
|
||||||
|
nostr.QuerierPublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p PrintingQuerierPublisher) Publish(ctx context.Context, evt nostr.Event) error {
|
||||||
|
if err := p.QuerierPublisher.Publish(ctx, evt); err == nil {
|
||||||
|
stdout(evt)
|
||||||
|
return nil
|
||||||
|
} else if err == eventstore.ErrDupEvent {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user