mirror of
https://github.com/fiatjaf/nak.git
synced 2025-12-23 06:58:51 +00:00
Compare commits
18 Commits
5ee7670ba8
...
v0.17.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
965a312b46 | ||
|
|
2e4079f92c | ||
|
|
5b64795015 | ||
|
|
5d4fe434c3 | ||
|
|
b95665d986 | ||
|
|
3be80c29df | ||
|
|
e01cfbde47 | ||
|
|
e91d4429ec | ||
|
|
21423b4a21 | ||
|
|
1b7f3162b5 | ||
|
|
8f38468103 | ||
|
|
9bf728d850 | ||
|
|
8396738fe2 | ||
|
|
c1d1682d6e | ||
|
|
6f00ff4c73 | ||
|
|
68bbece3db | ||
|
|
a83b23d76b | ||
|
|
a288cc47a4 |
@@ -366,6 +366,7 @@ var bunker = &cli.Command{
|
|||||||
handlerWg.Add(len(relayURLs))
|
handlerWg.Add(len(relayURLs))
|
||||||
for _, relayURL := range relayURLs {
|
for _, relayURL := range relayURLs {
|
||||||
go func(relayURL string) {
|
go func(relayURL string) {
|
||||||
|
defer handlerWg.Done()
|
||||||
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
|
if relay, _ := sys.Pool.EnsureRelay(relayURL); relay != nil {
|
||||||
err := relay.Publish(ctx, eventResponse)
|
err := relay.Publish(ctx, eventResponse)
|
||||||
printLock.Lock()
|
printLock.Lock()
|
||||||
@@ -375,7 +376,6 @@ var bunker = &cli.Command{
|
|||||||
log("* failed to send response: %s\n", err)
|
log("* failed to send response: %s\n", err)
|
||||||
}
|
}
|
||||||
printLock.Unlock()
|
printLock.Unlock()
|
||||||
handlerWg.Done()
|
|
||||||
}
|
}
|
||||||
}(relayURL)
|
}(relayURL)
|
||||||
}
|
}
|
||||||
|
|||||||
37
dekey.go
37
dekey.go
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/nip44"
|
"fiatjaf.com/nostr/nip44"
|
||||||
|
"github.com/fatih/color"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,11 +31,13 @@ var dekey = &cli.Command{
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
log(color.CyanString("gathering keyer from arguments...\n"))
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log(color.CyanString("getting user public key...\n"))
|
||||||
userPub, err := kr.GetPublicKey(ctx)
|
userPub, err := kr.GetPublicKey(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user public key: %w", err)
|
return fmt.Errorf("failed to get user public key: %w", err)
|
||||||
@@ -43,32 +46,40 @@ var dekey = &cli.Command{
|
|||||||
configPath := c.String("config-path")
|
configPath := c.String("config-path")
|
||||||
deviceName := c.String("device-name")
|
deviceName := c.String("device-name")
|
||||||
|
|
||||||
|
log(color.YellowString("handling device key for %s...\n"), deviceName)
|
||||||
// check if we already have a local-device secret key
|
// check if we already have a local-device secret key
|
||||||
deviceKeyPath := filepath.Join(configPath, "dekey", "device-key")
|
deviceKeyPath := filepath.Join(configPath, "dekey", "device-key")
|
||||||
var deviceSec nostr.SecretKey
|
var deviceSec nostr.SecretKey
|
||||||
if data, err := os.ReadFile(deviceKeyPath); err == nil {
|
if data, err := os.ReadFile(deviceKeyPath); err == nil {
|
||||||
|
log(color.GreenString("found existing device key\n"))
|
||||||
deviceSec, err = nostr.SecretKeyFromHex(string(data))
|
deviceSec, err = nostr.SecretKeyFromHex(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err)
|
return fmt.Errorf("invalid device key in %s: %w", deviceKeyPath, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log(color.YellowString("generating new device key...\n"))
|
||||||
// create one
|
// create one
|
||||||
deviceSec = nostr.Generate()
|
deviceSec = nostr.Generate()
|
||||||
os.MkdirAll(filepath.Dir(deviceKeyPath), 0700)
|
os.MkdirAll(filepath.Dir(deviceKeyPath), 0700)
|
||||||
if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil {
|
if err := os.WriteFile(deviceKeyPath, []byte(deviceSec.Hex()), 0600); err != nil {
|
||||||
return fmt.Errorf("failed to write device key: %w", err)
|
return fmt.Errorf("failed to write device key: %w", err)
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("device key generated and stored\n"))
|
||||||
}
|
}
|
||||||
devicePub := deviceSec.Public()
|
devicePub := deviceSec.Public()
|
||||||
|
|
||||||
// get relays for the user
|
// get relays for the user
|
||||||
|
log(color.CyanString("fetching write relays for user...\n"))
|
||||||
relays := sys.FetchWriteRelays(ctx, userPub)
|
relays := sys.FetchWriteRelays(ctx, userPub)
|
||||||
|
log(color.CyanString("connecting to %d relays...\n"), len(relays))
|
||||||
relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{})
|
relayList := connectToAllRelays(ctx, c, relays, nil, nostr.PoolOptions{})
|
||||||
if len(relayList) == 0 {
|
if len(relayList) == 0 {
|
||||||
return fmt.Errorf("no relays to use")
|
return fmt.Errorf("no relays to use")
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("connected to %d relays\n"), len(relayList))
|
||||||
|
|
||||||
// check if kind:4454 is already published
|
// check if kind:4454 is already published
|
||||||
|
log(color.CyanString("checking for existing device registration (kind:4454)...\n"))
|
||||||
events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
events := sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
Kinds: []nostr.Kind{4454},
|
Kinds: []nostr.Kind{4454},
|
||||||
Authors: []nostr.PubKey{userPub},
|
Authors: []nostr.PubKey{userPub},
|
||||||
@@ -77,6 +88,7 @@ var dekey = &cli.Command{
|
|||||||
},
|
},
|
||||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"})
|
||||||
if len(events) == 0 {
|
if len(events) == 0 {
|
||||||
|
log(color.YellowString("no device registration found, publishing kind:4454...\n"))
|
||||||
// publish kind:4454
|
// publish kind:4454
|
||||||
evt := nostr.Event{
|
evt := nostr.Event{
|
||||||
Kind: 4454,
|
Kind: 4454,
|
||||||
@@ -97,9 +109,13 @@ var dekey = &cli.Command{
|
|||||||
if err := publishFlow(ctx, c, kr, evt, relayList); err != nil {
|
if err := publishFlow(ctx, c, kr, evt, relayList); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("device registration published\n"))
|
||||||
|
} else {
|
||||||
|
log(color.GreenString("device already registered\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for kind:10044
|
// check for kind:10044
|
||||||
|
log(color.CyanString("checking for user encryption key (kind:10044)...\n"))
|
||||||
userKeyEventDate := nostr.Now()
|
userKeyEventDate := nostr.Now()
|
||||||
userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
userKeyResult := sys.Pool.FetchManyReplaceable(ctx, relays, nostr.Filter{
|
||||||
Kinds: []nostr.Kind{10044},
|
Kinds: []nostr.Kind{10044},
|
||||||
@@ -108,6 +124,7 @@ var dekey = &cli.Command{
|
|||||||
var eSec nostr.SecretKey
|
var eSec nostr.SecretKey
|
||||||
var ePub nostr.PubKey
|
var ePub nostr.PubKey
|
||||||
if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok {
|
if userKeyEvent, ok := userKeyResult.Load(nostr.ReplaceableKey{PubKey: userPub, D: ""}); !ok {
|
||||||
|
log(color.YellowString("no user encryption key found, generating new one...\n"))
|
||||||
// generate main secret key
|
// generate main secret key
|
||||||
eSec = nostr.Generate()
|
eSec = nostr.Generate()
|
||||||
ePub := eSec.Public()
|
ePub := eSec.Public()
|
||||||
@@ -118,8 +135,10 @@ var dekey = &cli.Command{
|
|||||||
if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil {
|
if err := os.WriteFile(eKeyPath, []byte(eSec.Hex()), 0600); err != nil {
|
||||||
return fmt.Errorf("failed to write user encryption key: %w", err)
|
return fmt.Errorf("failed to write user encryption key: %w", err)
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("user encryption key generated and stored\n"))
|
||||||
|
|
||||||
// publish kind:10044
|
// publish kind:10044
|
||||||
|
log(color.YellowString("publishing user encryption key (kind:10044)...\n"))
|
||||||
evt10044 := nostr.Event{
|
evt10044 := nostr.Event{
|
||||||
Kind: 10044,
|
Kind: 10044,
|
||||||
Content: "",
|
Content: "",
|
||||||
@@ -135,7 +154,9 @@ var dekey = &cli.Command{
|
|||||||
if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil {
|
if err := publishFlow(ctx, c, kr, evt10044, relayList); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("user encryption key published\n"))
|
||||||
} else {
|
} else {
|
||||||
|
log(color.GreenString("found existing user encryption key\n"))
|
||||||
userKeyEventDate = userKeyEvent.CreatedAt
|
userKeyEventDate = userKeyEvent.CreatedAt
|
||||||
|
|
||||||
// get the pub from the tag
|
// get the pub from the tag
|
||||||
@@ -152,6 +173,7 @@ var dekey = &cli.Command{
|
|||||||
// check if we have the key
|
// check if we have the key
|
||||||
eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex())
|
eKeyPath := filepath.Join(configPath, "dekey", "e", ePub.Hex())
|
||||||
if data, err := os.ReadFile(eKeyPath); err == nil {
|
if data, err := os.ReadFile(eKeyPath); err == nil {
|
||||||
|
log(color.GreenString("found stored user encryption key\n"))
|
||||||
eSec, err = nostr.SecretKeyFromHex(string(data))
|
eSec, err = nostr.SecretKeyFromHex(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid main key: %w", err)
|
return fmt.Errorf("invalid main key: %w", err)
|
||||||
@@ -160,6 +182,7 @@ var dekey = &cli.Command{
|
|||||||
return fmt.Errorf("stored user encryption key is corrupted: %w", err)
|
return fmt.Errorf("stored user encryption key is corrupted: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log(color.YellowString("user encryption key not stored locally, attempting to decrypt from other devices...\n"))
|
||||||
// try to decrypt from kind:4455
|
// try to decrypt from kind:4455
|
||||||
for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
for eKeyMsg := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
Kinds: []nostr.Kind{4455},
|
Kinds: []nostr.Kind{4455},
|
||||||
@@ -191,6 +214,7 @@ var dekey = &cli.Command{
|
|||||||
}
|
}
|
||||||
// check if it matches mainPub
|
// check if it matches mainPub
|
||||||
if eSec.Public() == ePub {
|
if eSec.Public() == ePub {
|
||||||
|
log(color.GreenString("successfully decrypted user encryption key from another device\n"))
|
||||||
// store it
|
// store it
|
||||||
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
os.MkdirAll(filepath.Dir(eKeyPath), 0700)
|
||||||
os.WriteFile(eKeyPath, []byte(eSecHex), 0600)
|
os.WriteFile(eKeyPath, []byte(eSecHex), 0600)
|
||||||
@@ -201,11 +225,13 @@ var dekey = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if eSec == [32]byte{} {
|
if eSec == [32]byte{} {
|
||||||
log("main secret key not available, must authorize on another device\n")
|
log(color.RedString("main secret key not available, must authorize on another device\n"))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
log(color.GreenString("user encryption key ready\n"))
|
||||||
|
|
||||||
// now we have mainSec, check for other kind:4454 events newer than the 10044
|
// now we have mainSec, check for other kind:4454 events newer than the 10044
|
||||||
|
log(color.CyanString("checking for other devices and key messages...\n"))
|
||||||
keyMsgs := make([]string, 0, 5)
|
keyMsgs := make([]string, 0, 5)
|
||||||
for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
for keyOrDeviceEvt := range sys.Pool.FetchMany(ctx, relays, nostr.Filter{
|
||||||
Kinds: []nostr.Kind{4454, 4455},
|
Kinds: []nostr.Kind{4454, 4455},
|
||||||
@@ -214,6 +240,7 @@ var dekey = &cli.Command{
|
|||||||
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
}, nostr.SubscriptionOptions{Label: "nak-nip4e"}) {
|
||||||
if keyOrDeviceEvt.Kind == 4455 {
|
if keyOrDeviceEvt.Kind == 4455 {
|
||||||
// key event
|
// key event
|
||||||
|
log(color.BlueString("received key message (kind:4455)\n"))
|
||||||
|
|
||||||
// skip ourselves
|
// skip ourselves
|
||||||
if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil {
|
if keyOrDeviceEvt.Tags.FindWithValue("p", devicePub.Hex()) != nil {
|
||||||
@@ -229,6 +256,7 @@ var dekey = &cli.Command{
|
|||||||
keyMsgs = append(keyMsgs, pubkeyTag[1])
|
keyMsgs = append(keyMsgs, pubkeyTag[1])
|
||||||
} else if keyOrDeviceEvt.Kind == 4454 {
|
} else if keyOrDeviceEvt.Kind == 4454 {
|
||||||
// device event
|
// device event
|
||||||
|
log(color.BlueString("received device registration (kind:4454)\n"))
|
||||||
|
|
||||||
// skip ourselves
|
// skip ourselves
|
||||||
if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil {
|
if keyOrDeviceEvt.Tags.FindWithValue("pubkey", devicePub.Hex()) != nil {
|
||||||
@@ -246,6 +274,7 @@ var dekey = &cli.Command{
|
|||||||
|
|
||||||
// here we know we're dealing with a deviceMsg without a corresponding keyMsg
|
// here we know we're dealing with a deviceMsg without a corresponding keyMsg
|
||||||
// so we have to build a keyMsg for them
|
// so we have to build a keyMsg for them
|
||||||
|
log(color.YellowString("sending encryption key to new device...\n"))
|
||||||
theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1])
|
theirDevice, err := nostr.PubKeyFromHex(pubkeyTag[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -273,7 +302,11 @@ var dekey = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
publishFlow(ctx, c, kr, evt4455, relayList)
|
if err := publishFlow(ctx, c, kr, evt4455, relayList); err != nil {
|
||||||
|
log(color.RedString("failed to publish key message: %v\n"), err)
|
||||||
|
} else {
|
||||||
|
log(color.GreenString("encryption key sent to device\n"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
2
event.go
2
event.go
@@ -24,7 +24,7 @@ const (
|
|||||||
CATEGORY_EXTRAS = "EXTRAS"
|
CATEGORY_EXTRAS = "EXTRAS"
|
||||||
)
|
)
|
||||||
|
|
||||||
var event = &cli.Command{
|
var eventCmd = &cli.Command{
|
||||||
Name: "event",
|
Name: "event",
|
||||||
Usage: "generates an encoded event and either prints it or sends it to a set of relays",
|
Usage: "generates an encoded event and either prints it or sends it to a set of relays",
|
||||||
Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays.
|
Description: `outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var filter = &cli.Command{
|
var filterCmd = &cli.Command{
|
||||||
Name: "filter",
|
Name: "filter",
|
||||||
Usage: "applies an event filter to an event to see if it matches.",
|
Usage: "applies an event filter to an event to see if it matches.",
|
||||||
Description: `
|
Description: `
|
||||||
|
|||||||
53
git.go
53
git.go
@@ -455,11 +455,17 @@ aside from those, there is also:
|
|||||||
{
|
{
|
||||||
Name: "push",
|
Name: "push",
|
||||||
Usage: "push git changes",
|
Usage: "push git changes",
|
||||||
Flags: append(defaultKeyFlags, &cli.BoolFlag{
|
Flags: append(defaultKeyFlags,
|
||||||
|
&cli.BoolFlag{
|
||||||
Name: "force",
|
Name: "force",
|
||||||
Aliases: []string{"f"},
|
Aliases: []string{"f"},
|
||||||
Usage: "force push to git remotes",
|
Usage: "force push to git remotes",
|
||||||
}),
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "tags",
|
||||||
|
Usage: "push all refs under refs/tags",
|
||||||
|
},
|
||||||
|
),
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
// setup signer
|
// setup signer
|
||||||
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
@@ -526,6 +532,40 @@ aside from those, there is also:
|
|||||||
log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch))
|
log("- setting HEAD to branch %s\n", color.CyanString(remoteBranch))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.Bool("tags") {
|
||||||
|
// add all refs/tags
|
||||||
|
output, err := exec.Command("git", "show-ref", "--tags").Output()
|
||||||
|
if err != nil && err.Error() != "exit status 1" {
|
||||||
|
// exit status 1 is returned when there are no tags, which should be ok for us
|
||||||
|
return fmt.Errorf("failed to get local tags: %s", err)
|
||||||
|
} else {
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
commitHash := parts[0]
|
||||||
|
ref := parts[1]
|
||||||
|
|
||||||
|
tagName := strings.TrimPrefix(ref, "refs/tags/")
|
||||||
|
|
||||||
|
if !c.Bool("force") {
|
||||||
|
// if --force is not passed then we can't overwrite tags
|
||||||
|
if existingHash, exists := state.Tags[tagName]; exists && existingHash != commitHash {
|
||||||
|
return fmt.Errorf("tag %s that is already published pointing to %s, call with --force to overwrite", tagName, existingHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.Tags[tagName] = commitHash
|
||||||
|
log("- setting tag %s to commit %s\n", color.CyanString(tagName), color.CyanString(commitHash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create and sign the new state event
|
// create and sign the new state event
|
||||||
newStateEvent := state.ToEvent()
|
newStateEvent := state.ToEvent()
|
||||||
err = kr.SignEvent(ctx, &newStateEvent)
|
err = kr.SignEvent(ctx, &newStateEvent)
|
||||||
@@ -553,6 +593,9 @@ aside from those, there is also:
|
|||||||
if c.Bool("force") {
|
if c.Bool("force") {
|
||||||
pushArgs = append(pushArgs, "--force")
|
pushArgs = append(pushArgs, "--force")
|
||||||
}
|
}
|
||||||
|
if c.Bool("tags") {
|
||||||
|
pushArgs = append(pushArgs, "--tags")
|
||||||
|
}
|
||||||
pushCmd := exec.Command("git", pushArgs...)
|
pushCmd := exec.Command("git", pushArgs...)
|
||||||
pushCmd.Stderr = os.Stderr
|
pushCmd.Stderr = os.Stderr
|
||||||
pushCmd.Stdout = os.Stdout
|
pushCmd.Stdout = os.Stdout
|
||||||
@@ -1061,7 +1104,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState)
|
|||||||
lines := strings.Split(string(output), "\n")
|
lines := strings.Split(string(output), "\n")
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) >= 2 && strings.Contains(parts[1], "refs/remotes/nip34/state/") {
|
if len(parts) >= 2 && strings.Contains(parts[1], "refs/heads/nip34/state/") {
|
||||||
delCmd := exec.Command("git", "update-ref", "-d", parts[1])
|
delCmd := exec.Command("git", "update-ref", "-d", parts[1])
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
delCmd.Dir = dir
|
delCmd.Dir = dir
|
||||||
@@ -1078,7 +1121,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState)
|
|||||||
branchName = "refs/heads/" + branchName
|
branchName = "refs/heads/" + branchName
|
||||||
}
|
}
|
||||||
|
|
||||||
refName := "refs/remotes/nip34/state/" + strings.TrimPrefix(branchName, "refs/heads/")
|
refName := "refs/heads/nip34/state/" + strings.TrimPrefix(branchName, "refs/heads/")
|
||||||
updateCmd := exec.Command("git", "update-ref", refName, commit)
|
updateCmd := exec.Command("git", "update-ref", refName, commit)
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
updateCmd.Dir = dir
|
updateCmd.Dir = dir
|
||||||
@@ -1091,7 +1134,7 @@ func gitUpdateRefs(ctx context.Context, dir string, state nip34.RepositoryState)
|
|||||||
// create ref for HEAD
|
// create ref for HEAD
|
||||||
if state.HEAD != "" {
|
if state.HEAD != "" {
|
||||||
if headCommit, ok := state.Branches[state.HEAD]; ok {
|
if headCommit, ok := state.Branches[state.HEAD]; ok {
|
||||||
headRefName := "refs/remotes/nip34/state/HEAD"
|
headRefName := "refs/heads/nip34/state/HEAD"
|
||||||
updateCmd := exec.Command("git", "update-ref", headRefName, headCommit)
|
updateCmd := exec.Command("git", "update-ref", headRefName, headCommit)
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
updateCmd.Dir = dir
|
updateCmd.Dir = dir
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -4,7 +4,7 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.1
|
fiatjaf.com/lib v0.3.1
|
||||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d
|
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb
|
||||||
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
|
||||||
@@ -32,6 +32,7 @@ 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/PowerDNS/lmdb-go v1.9.3 // indirect
|
||||||
github.com/alecthomas/chroma/v2 v2.14.0 // 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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
@@ -96,7 +97,6 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
||||||
go.etcd.io/bbolt v1.4.2 // indirect
|
|
||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,9 +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-20251222025842-099569ea4feb h1:GuqPn1g0JRD/dGxFRxEwEFxvbcT3vyvMjP3OoeLIIh0=
|
||||||
fiatjaf.com/nostr v0.0.0-20251201232830-91548fa0a157/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
fiatjaf.com/nostr v0.0.0-20251222025842-099569ea4feb/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d h1:xROmiuT7LrZk+/iGGeTqRI4liqJZrc87AWjsyHtbqDg=
|
|
||||||
fiatjaf.com/nostr v0.0.0-20251204122254-07061404918d/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 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=
|
||||||
@@ -283,8 +281,6 @@ 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 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 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
|
||||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
|
||||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
|||||||
43
lmdb.go
Normal file
43
lmdb.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//go:build linux && !riscv64 && !arm64
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr/eventstore/lmdb"
|
||||||
|
"fiatjaf.com/nostr/eventstore/nullstore"
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"fiatjaf.com/nostr/sdk/hints/lmdbh"
|
||||||
|
lmdbkv "fiatjaf.com/nostr/sdk/kvstore/lmdb"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupLocalDatabases(c *cli.Command, sys *sdk.System) {
|
||||||
|
configPath := c.String("config-path")
|
||||||
|
if configPath != "" {
|
||||||
|
hintsPath := filepath.Join(configPath, "outbox/hints")
|
||||||
|
os.MkdirAll(hintsPath, 0755)
|
||||||
|
_, err := lmdbh.NewLMDBHints(hintsPath)
|
||||||
|
if err != nil {
|
||||||
|
log("failed to create lmdb hints db at '%s': %s\n", hintsPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsPath := filepath.Join(configPath, "events")
|
||||||
|
os.MkdirAll(eventsPath, 0755)
|
||||||
|
sys.Store = &lmdb.LMDBBackend{Path: eventsPath}
|
||||||
|
if err := sys.Store.Init(); err != nil {
|
||||||
|
log("failed to create boltdb events db at '%s': %s\n", eventsPath, err)
|
||||||
|
sys.Store = &nullstore.NullStore{}
|
||||||
|
}
|
||||||
|
|
||||||
|
kvPath := filepath.Join(configPath, "kvstore")
|
||||||
|
os.MkdirAll(kvPath, 0755)
|
||||||
|
if kv, err := lmdbkv.NewStore(kvPath); err != nil {
|
||||||
|
log("failed to create boltdb kvstore db at '%s': %s\n", kvPath, err)
|
||||||
|
} else {
|
||||||
|
sys.KVStore = kv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
main.go
14
main.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,9 +25,9 @@ var app = &cli.Command{
|
|||||||
Usage: "the nostr army knife command-line tool",
|
Usage: "the nostr army knife command-line tool",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
event,
|
eventCmd,
|
||||||
req,
|
req,
|
||||||
filter,
|
filterCmd,
|
||||||
fetch,
|
fetch,
|
||||||
count,
|
count,
|
||||||
decode,
|
decode,
|
||||||
@@ -44,7 +43,7 @@ var app = &cli.Command{
|
|||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
gift,
|
gift,
|
||||||
outbox,
|
outboxCmd,
|
||||||
wallet,
|
wallet,
|
||||||
mcpServer,
|
mcpServer,
|
||||||
curl,
|
curl,
|
||||||
@@ -53,6 +52,7 @@ var app = &cli.Command{
|
|||||||
git,
|
git,
|
||||||
nip,
|
nip,
|
||||||
syncCmd,
|
syncCmd,
|
||||||
|
spell,
|
||||||
},
|
},
|
||||||
Version: version,
|
Version: version,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
@@ -63,7 +63,7 @@ var app = &cli.Command{
|
|||||||
if home, err := os.UserHomeDir(); err == nil {
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
return filepath.Join(home, ".config/nak")
|
return filepath.Join(home, ".config/nak")
|
||||||
} else {
|
} else {
|
||||||
return filepath.Join("/dev/null")
|
return ""
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
},
|
},
|
||||||
@@ -99,9 +99,7 @@ var app = &cli.Command{
|
|||||||
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||||
sys = sdk.NewSystem()
|
sys = sdk.NewSystem()
|
||||||
|
|
||||||
if err := initializeOutboxHintsDB(c, sys); err != nil {
|
setupLocalDatabases(c, sys)
|
||||||
return ctx, fmt.Errorf("failed to initialize outbox hints: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sys.Pool = nostr.NewPool(nostr.PoolOptions{
|
sys.Pool = nostr.NewPool(nostr.PoolOptions{
|
||||||
AuthorKindQueryMiddleware: sys.TrackQueryAttempts,
|
AuthorKindQueryMiddleware: sys.TrackQueryAttempts,
|
||||||
|
|||||||
11
non_lmdb.go
Normal file
11
non_lmdb.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !linux || riscv64 || arm64
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fiatjaf.com/nostr/sdk"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupLocalDatabases(c *cli.Command, sys *sdk.System) {
|
||||||
|
}
|
||||||
61
outbox.go
61
outbox.go
@@ -3,80 +3,21 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"fiatjaf.com/nostr/sdk"
|
|
||||||
"fiatjaf.com/nostr/sdk/hints/bbolth"
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var outboxCmd = &cli.Command{
|
||||||
hintsFilePath string
|
|
||||||
hintsFileExists bool
|
|
||||||
)
|
|
||||||
|
|
||||||
func initializeOutboxHintsDB(c *cli.Command, sys *sdk.System) error {
|
|
||||||
configPath := c.String("config-path")
|
|
||||||
if configPath != "" {
|
|
||||||
hintsFilePath = filepath.Join(configPath, "outbox/hints.db")
|
|
||||||
}
|
|
||||||
if hintsFilePath != "" {
|
|
||||||
if _, err := os.Stat(hintsFilePath); err == nil {
|
|
||||||
hintsFileExists = true
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hintsFileExists && hintsFilePath != "" {
|
|
||||||
hintsdb, err := bbolth.NewBoltHints(hintsFilePath)
|
|
||||||
if err == nil {
|
|
||||||
sys.Hints = hintsdb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var outbox = &cli.Command{
|
|
||||||
Name: "outbox",
|
Name: "outbox",
|
||||||
Usage: "manage outbox relay hints database",
|
Usage: "manage outbox relay hints database",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Commands: []*cli.Command{
|
Commands: []*cli.Command{
|
||||||
{
|
|
||||||
Name: "init",
|
|
||||||
Usage: "initialize the outbox hints database",
|
|
||||||
DisableSliceFlagSeparator: true,
|
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
|
||||||
if hintsFileExists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if hintsFilePath == "" {
|
|
||||||
return fmt.Errorf("couldn't find a place to store the hints, pass --config-path to fix.")
|
|
||||||
}
|
|
||||||
|
|
||||||
os.MkdirAll(hintsFilePath, 0755)
|
|
||||||
_, err := bbolth.NewBoltHints(hintsFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create bolt hints db at '%s': %w", hintsFilePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log("initialized hints database at %s\n", hintsFilePath)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "list",
|
Name: "list",
|
||||||
Usage: "list outbox relays for a given pubkey",
|
Usage: "list outbox relays for a given pubkey",
|
||||||
ArgsUsage: "<pubkey>",
|
ArgsUsage: "<pubkey>",
|
||||||
DisableSliceFlagSeparator: true,
|
DisableSliceFlagSeparator: true,
|
||||||
Action: func(ctx context.Context, c *cli.Command) error {
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
if !hintsFileExists {
|
|
||||||
log(color.YellowString("running with temporary fragile data.\n"))
|
|
||||||
log(color.YellowString("call `nak outbox init` to setup persistence.\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Args().Len() != 1 {
|
if c.Args().Len() != 1 {
|
||||||
return fmt.Errorf("expected exactly one argument (pubkey)")
|
return fmt.Errorf("expected exactly one argument (pubkey)")
|
||||||
}
|
}
|
||||||
|
|||||||
78
req.go
78
req.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore"
|
"fiatjaf.com/nostr/eventstore"
|
||||||
@@ -77,11 +78,6 @@ example:
|
|||||||
Name: "paginate-interval",
|
Name: "paginate-interval",
|
||||||
Usage: "time between queries when using --paginate",
|
Usage: "time between queries when using --paginate",
|
||||||
},
|
},
|
||||||
&cli.UintFlag{
|
|
||||||
Name: "paginate-global-limit",
|
|
||||||
Usage: "global limit at which --paginate should stop",
|
|
||||||
DefaultText: "uses the value given by --limit/-l or infinite",
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "bare",
|
Name: "bare",
|
||||||
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
Usage: "when printing the filter, print just the filter, not enveloped in a [\"REQ\", ...] array",
|
||||||
@@ -226,20 +222,51 @@ example:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
performReq(ctx, filter, relayUrls, c.Bool("stream"), c.Bool("outbox"), c.Uint("outbox-relays-per-pubkey"), c.Bool("paginate"), c.Duration("paginate-interval"), "nak-req")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no relays given, will just print the filter
|
||||||
|
var result string
|
||||||
|
if c.Bool("bare") {
|
||||||
|
result = filter.String()
|
||||||
|
} else {
|
||||||
|
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: []nostr.Filter{filter}})
|
||||||
|
result = string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitIfLineProcessingError(ctx)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func performReq(
|
||||||
|
ctx context.Context,
|
||||||
|
filter nostr.Filter,
|
||||||
|
relayUrls []string,
|
||||||
|
stream bool,
|
||||||
|
outbox bool,
|
||||||
|
outboxRelaysPerPubKey uint64,
|
||||||
|
paginate bool,
|
||||||
|
paginateInterval time.Duration,
|
||||||
|
label string,
|
||||||
|
) {
|
||||||
var results chan nostr.RelayEvent
|
var results chan nostr.RelayEvent
|
||||||
var closeds chan nostr.RelayClosed
|
var closeds chan nostr.RelayClosed
|
||||||
|
|
||||||
opts := nostr.SubscriptionOptions{
|
opts := nostr.SubscriptionOptions{
|
||||||
Label: "nak-req",
|
Label: label,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bool("paginate") {
|
if paginate {
|
||||||
paginator := sys.Pool.PaginatorWithInterval(c.Duration("paginate-interval"))
|
paginator := sys.Pool.PaginatorWithInterval(paginateInterval)
|
||||||
results = paginator(ctx, relayUrls, filter, opts)
|
results = paginator(ctx, relayUrls, filter, opts)
|
||||||
} else if c.Bool("outbox") {
|
} else if outbox {
|
||||||
defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2)
|
defs := make([]nostr.DirectedFilter, 0, len(filter.Authors)*2)
|
||||||
|
|
||||||
// hardcoded relays, if any
|
|
||||||
for _, relayUrl := range relayUrls {
|
for _, relayUrl := range relayUrls {
|
||||||
defs = append(defs, nostr.DirectedFilter{
|
defs = append(defs, nostr.DirectedFilter{
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
@@ -251,12 +278,13 @@ example:
|
|||||||
errg := errgroup.Group{}
|
errg := errgroup.Group{}
|
||||||
errg.SetLimit(16)
|
errg.SetLimit(16)
|
||||||
mu := sync.Mutex{}
|
mu := sync.Mutex{}
|
||||||
|
logverbose("gathering outbox relays for %d authors...\n", len(filter.Authors))
|
||||||
for _, pubkey := range filter.Authors {
|
for _, pubkey := range filter.Authors {
|
||||||
errg.Go(func() error {
|
errg.Go(func() error {
|
||||||
n := int(c.Uint("outbox-relays-per-pubkey"))
|
n := int(outboxRelaysPerPubKey)
|
||||||
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 specified globally, ignore
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !nostr.IsValidRelayURL(url) {
|
if !nostr.IsValidRelayURL(url) {
|
||||||
@@ -295,15 +323,19 @@ example:
|
|||||||
}
|
}
|
||||||
errg.Wait()
|
errg.Wait()
|
||||||
|
|
||||||
if c.Bool("stream") {
|
if stream {
|
||||||
|
logverbose("running subscription with %d directed filters...\n", len(defs))
|
||||||
results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts)
|
results, closeds = sys.Pool.BatchedSubscribeManyNotifyClosed(ctx, defs, opts)
|
||||||
} else {
|
} else {
|
||||||
|
logverbose("running query with %d directed filters...\n", len(defs))
|
||||||
results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts)
|
results, closeds = sys.Pool.BatchedQueryManyNotifyClosed(ctx, defs, opts)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if c.Bool("stream") {
|
if stream {
|
||||||
|
logverbose("running subscription to %d relays...\n", len(relayUrls))
|
||||||
results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts)
|
results, closeds = sys.Pool.SubscribeManyNotifyClosed(ctx, relayUrls, filter, opts)
|
||||||
} else {
|
} else {
|
||||||
|
logverbose("running query to %d relays...\n", len(relayUrls))
|
||||||
results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts)
|
results, closeds = sys.Pool.FetchManyNotifyClosed(ctx, relayUrls, filter, opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,24 +359,6 @@ example:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// no relays given, will just print the filter
|
|
||||||
var result string
|
|
||||||
if c.Bool("bare") {
|
|
||||||
result = filter.String()
|
|
||||||
} else {
|
|
||||||
j, _ := json.Marshal(nostr.ReqEnvelope{SubscriptionID: "nak", Filters: []nostr.Filter{filter}})
|
|
||||||
result = string(j)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdout(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var reqFilterFlags = []cli.Flag{
|
var reqFilterFlags = []cli.Flag{
|
||||||
&PubKeySliceFlag{
|
&PubKeySliceFlag{
|
||||||
|
|||||||
473
spell.go
Normal file
473
spell.go
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/sdk/hints"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/markusmobius/go-dateparser"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var spell = &cli.Command{
|
||||||
|
Name: "spell",
|
||||||
|
Usage: "downloads a spell event and executes its REQ request",
|
||||||
|
ArgsUsage: "[nevent_code]",
|
||||||
|
Description: `fetches a spell event (kind 777) and executes REQ command encoded in its tags.`,
|
||||||
|
Flags: append(defaultKeyFlags,
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "pub",
|
||||||
|
Usage: "public key to run spells in the context of (if you don't want to pass a --sec)",
|
||||||
|
},
|
||||||
|
&cli.UintFlag{
|
||||||
|
Name: "outbox-relays-per-pubkey",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Usage: "number of outbox relays to use for each pubkey",
|
||||||
|
Value: 3,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Action: func(ctx context.Context, c *cli.Command) error {
|
||||||
|
configPath := c.String("config-path")
|
||||||
|
os.MkdirAll(filepath.Join(configPath, "spells"), 0755)
|
||||||
|
|
||||||
|
// load history from file
|
||||||
|
var history []SpellHistoryEntry
|
||||||
|
historyPath := filepath.Join(configPath, "spells/history")
|
||||||
|
file, err := os.Open(historyPath)
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
var entry SpellHistoryEntry
|
||||||
|
if err := json.Unmarshal([]byte(scanner.Text()), &entry); err != nil {
|
||||||
|
continue // skip invalid entries
|
||||||
|
}
|
||||||
|
history = append(history, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Args().Len() == 0 {
|
||||||
|
// check if we have input from stdin
|
||||||
|
for stdinEvent := range getJsonsOrBlank() {
|
||||||
|
if stdinEvent == "{}" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var spell nostr.Event
|
||||||
|
if err := json.Unmarshal([]byte(stdinEvent), &spell); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse spell event from stdin: %w", err)
|
||||||
|
}
|
||||||
|
if spell.Kind != 777 {
|
||||||
|
return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runSpell(ctx, c, historyPath, history, nostr.EventPointer{ID: spell.ID}, spell)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no stdin input, show recent spells
|
||||||
|
log("recent spells:\n")
|
||||||
|
for i, entry := range history {
|
||||||
|
if i >= 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName := entry.Name
|
||||||
|
if displayName == "" {
|
||||||
|
displayName = entry.Content
|
||||||
|
if len(displayName) > 28 {
|
||||||
|
displayName = displayName[:27] + "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if displayName != "" {
|
||||||
|
displayName = color.HiMagentaString(displayName) + ": "
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := entry.Content
|
||||||
|
if len(desc) > 50 {
|
||||||
|
desc = desc[0:49] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUsed := entry.LastUsed.Format("2006-01-02 15:04")
|
||||||
|
stdout(fmt.Sprintf(" %s %s%s - %s",
|
||||||
|
color.BlueString(entry.Identifier),
|
||||||
|
displayName,
|
||||||
|
color.YellowString(lastUsed),
|
||||||
|
desc,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode nevent to get the spell event
|
||||||
|
var pointer nostr.EventPointer
|
||||||
|
identifier := c.Args().First()
|
||||||
|
prefix, value, err := nip19.Decode(identifier)
|
||||||
|
if err == nil {
|
||||||
|
if prefix != "nevent" {
|
||||||
|
return fmt.Errorf("expected nevent code, got %s", prefix)
|
||||||
|
}
|
||||||
|
pointer = value.(nostr.EventPointer)
|
||||||
|
} else {
|
||||||
|
// search our history
|
||||||
|
for _, entry := range history {
|
||||||
|
if entry.Identifier == identifier {
|
||||||
|
pointer = entry.Pointer
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pointer.ID == nostr.ZeroID {
|
||||||
|
return fmt.Errorf("invalid spell reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
// first try to fetch spell from sys.Store
|
||||||
|
var spell nostr.Event
|
||||||
|
found := false
|
||||||
|
for evt := range sys.Store.QueryEvents(nostr.Filter{IDs: []nostr.ID{pointer.ID}}, 1) {
|
||||||
|
spell = evt
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var relays []string
|
||||||
|
if !found {
|
||||||
|
// if not found in store, fetch from external relays
|
||||||
|
relays = pointer.Relays
|
||||||
|
if pointer.Author != nostr.ZeroPK {
|
||||||
|
for _, url := range relays {
|
||||||
|
sys.Hints.Save(pointer.Author, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now())
|
||||||
|
}
|
||||||
|
relays = append(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...)
|
||||||
|
}
|
||||||
|
result := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}},
|
||||||
|
nostr.SubscriptionOptions{Label: "nak-spell-f"})
|
||||||
|
if result == nil {
|
||||||
|
return fmt.Errorf("spell event not found")
|
||||||
|
}
|
||||||
|
spell = result.Event
|
||||||
|
}
|
||||||
|
if spell.Kind != 777 {
|
||||||
|
return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runSpell(ctx, c, historyPath, history, pointer, spell)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSpell(
|
||||||
|
ctx context.Context,
|
||||||
|
c *cli.Command,
|
||||||
|
historyPath string,
|
||||||
|
history []SpellHistoryEntry,
|
||||||
|
pointer nostr.EventPointer,
|
||||||
|
spell nostr.Event,
|
||||||
|
) error {
|
||||||
|
// parse spell tags to build REQ filter
|
||||||
|
spellFilter, err := buildSpellReq(ctx, c, spell.Tags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse spell tags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine relays to query
|
||||||
|
var spellRelays []string
|
||||||
|
var outbox bool
|
||||||
|
relaysTag := spell.Tags.Find("relays")
|
||||||
|
if relaysTag == nil {
|
||||||
|
// if this tag doesn't exist assume $outbox
|
||||||
|
relaysTag = nostr.Tag{"relays", "$outbox"}
|
||||||
|
}
|
||||||
|
for i := 1; i < len(relaysTag); i++ {
|
||||||
|
switch relaysTag[i] {
|
||||||
|
case "$outbox":
|
||||||
|
outbox = true
|
||||||
|
default:
|
||||||
|
spellRelays = append(spellRelays, relaysTag[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream := !spell.Tags.Has("close-on-eose")
|
||||||
|
|
||||||
|
// fill in the author if we didn't have it
|
||||||
|
pointer.Author = spell.PubKey
|
||||||
|
|
||||||
|
// save spell to sys.Store
|
||||||
|
if err := sys.Store.SaveEvent(spell); err != nil {
|
||||||
|
logverbose("failed to save spell to store: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to history before execution
|
||||||
|
{
|
||||||
|
idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK)
|
||||||
|
identifier := "spell" + idStr[len(idStr)-7:]
|
||||||
|
nameTag := spell.Tags.Find("name")
|
||||||
|
var name string
|
||||||
|
if nameTag != nil {
|
||||||
|
name = nameTag[1]
|
||||||
|
}
|
||||||
|
if len(history) > 100 {
|
||||||
|
history = history[:100]
|
||||||
|
}
|
||||||
|
// write back to file
|
||||||
|
file, err := os.Create(historyPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(SpellHistoryEntry{
|
||||||
|
Identifier: identifier,
|
||||||
|
Name: name,
|
||||||
|
Content: spell.Content,
|
||||||
|
LastUsed: time.Now(),
|
||||||
|
Pointer: pointer,
|
||||||
|
})
|
||||||
|
file.Write(data)
|
||||||
|
file.Write([]byte{'\n'})
|
||||||
|
for i, entry := range history {
|
||||||
|
if entry.Identifier == identifier {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(entry)
|
||||||
|
file.Write(data)
|
||||||
|
file.Write([]byte{'\n'})
|
||||||
|
|
||||||
|
// limit history size (keep last 100)
|
||||||
|
if i == 100 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.Close()
|
||||||
|
|
||||||
|
logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n",
|
||||||
|
identifier, spellFilter, spellRelays, outbox, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute
|
||||||
|
logSpellDetails(spell)
|
||||||
|
performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.Filter, error) {
|
||||||
|
filter := nostr.Filter{}
|
||||||
|
|
||||||
|
getMe := func() (nostr.PubKey, error) {
|
||||||
|
if !c.IsSet("sec") && !c.IsSet("prompt-sec") && c.IsSet("pub") {
|
||||||
|
return parsePubKey(c.String("pub"))
|
||||||
|
}
|
||||||
|
|
||||||
|
kr, _, err := gatherKeyerFromArguments(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("failed to get keyer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey, err := kr.GetPublicKey(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nostr.ZeroPK, fmt.Errorf("failed to get public key from keyer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubkey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if len(tag) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag[0] {
|
||||||
|
case "cmd":
|
||||||
|
if len(tag) < 2 || tag[1] != "REQ" {
|
||||||
|
return nostr.Filter{}, fmt.Errorf("only REQ commands are supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
case "k":
|
||||||
|
for i := 1; i < len(tag); i++ {
|
||||||
|
if kind, err := strconv.Atoi(tag[i]); err == nil {
|
||||||
|
filter.Kinds = append(filter.Kinds, nostr.Kind(kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "authors":
|
||||||
|
for i := 1; i < len(tag); i++ {
|
||||||
|
switch tag[i] {
|
||||||
|
case "$me":
|
||||||
|
me, err := getMe()
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, err
|
||||||
|
}
|
||||||
|
filter.Authors = append(filter.Authors, me)
|
||||||
|
case "$contacts":
|
||||||
|
me, err := getMe()
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, err
|
||||||
|
}
|
||||||
|
for _, f := range sys.FetchFollowList(ctx, me).Items {
|
||||||
|
filter.Authors = append(filter.Authors, f.Pubkey)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
pubkey, err := nostr.PubKeyFromHex(tag[i])
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, fmt.Errorf("invalid pubkey '%s' in 'authors': %w", tag[i], err)
|
||||||
|
}
|
||||||
|
filter.Authors = append(filter.Authors, pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ids":
|
||||||
|
for i := 1; i < len(tag); i++ {
|
||||||
|
id, err := nostr.IDFromHex(tag[i])
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, fmt.Errorf("invalid id '%s' in 'authors': %w", tag[i], err)
|
||||||
|
}
|
||||||
|
filter.IDs = append(filter.IDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "tag":
|
||||||
|
if len(tag) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tagName := tag[1]
|
||||||
|
if filter.Tags == nil {
|
||||||
|
filter.Tags = make(nostr.TagMap)
|
||||||
|
}
|
||||||
|
for i := 2; i < len(tag); i++ {
|
||||||
|
switch tag[i] {
|
||||||
|
case "$me":
|
||||||
|
me, err := getMe()
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, err
|
||||||
|
}
|
||||||
|
filter.Tags[tagName] = append(filter.Tags[tagName], me.Hex())
|
||||||
|
case "$contacts":
|
||||||
|
me, err := getMe()
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, err
|
||||||
|
}
|
||||||
|
for _, f := range sys.FetchFollowList(ctx, me).Items {
|
||||||
|
filter.Tags[tagName] = append(filter.Tags[tagName], f.Pubkey.Hex())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
filter.Tags[tagName] = append(filter.Tags[tagName], tag[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "limit":
|
||||||
|
if len(tag) >= 2 {
|
||||||
|
if limit, err := strconv.Atoi(tag[1]); err == nil {
|
||||||
|
filter.Limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "since":
|
||||||
|
if len(tag) >= 2 {
|
||||||
|
date, err := dateparser.Parse(&dateparser.Configuration{
|
||||||
|
DefaultTimezone: time.Local,
|
||||||
|
CurrentTime: time.Now(),
|
||||||
|
}, tag[1])
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, fmt.Errorf("invalid date %s: %w", tag[1], err)
|
||||||
|
}
|
||||||
|
filter.Since = nostr.Timestamp(date.Time.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
case "until":
|
||||||
|
if len(tag) >= 2 {
|
||||||
|
date, err := dateparser.Parse(&dateparser.Configuration{
|
||||||
|
DefaultTimezone: time.Local,
|
||||||
|
CurrentTime: time.Now(),
|
||||||
|
}, tag[1])
|
||||||
|
if err != nil {
|
||||||
|
return nostr.Filter{}, fmt.Errorf("invalid date %s: %w", tag[1], err)
|
||||||
|
}
|
||||||
|
filter.Until = nostr.Timestamp(date.Time.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
if len(tag) >= 2 {
|
||||||
|
filter.Search = tag[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRelativeTime(timeStr string) (nostr.Timestamp, error) {
|
||||||
|
// Handle special cases
|
||||||
|
switch timeStr {
|
||||||
|
case "now":
|
||||||
|
return nostr.Now(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as relative time (e.g., "7d", "1h", "30m")
|
||||||
|
if strings.HasSuffix(timeStr, "d") {
|
||||||
|
days := strings.TrimSuffix(timeStr, "d")
|
||||||
|
if daysInt, err := strconv.Atoi(days); err == nil {
|
||||||
|
return nostr.Now() - nostr.Timestamp(daysInt*24*60*60), nil
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(timeStr, "h") {
|
||||||
|
hours := strings.TrimSuffix(timeStr, "h")
|
||||||
|
if hoursInt, err := strconv.Atoi(hours); err == nil {
|
||||||
|
return nostr.Now() - nostr.Timestamp(hoursInt*60*60), nil
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(timeStr, "m") {
|
||||||
|
minutes := strings.TrimSuffix(timeStr, "m")
|
||||||
|
if minutesInt, err := strconv.Atoi(minutes); err == nil {
|
||||||
|
return nostr.Now() - nostr.Timestamp(minutesInt*60), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to parse as direct timestamp
|
||||||
|
if ts, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
|
||||||
|
return nostr.Timestamp(ts), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("invalid time format: %s", timeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpellHistoryEntry struct {
|
||||||
|
Identifier string `json:"_id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
LastUsed time.Time `json:"last_used"`
|
||||||
|
Pointer nostr.EventPointer `json:"pointer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func logSpellDetails(spell nostr.Event) {
|
||||||
|
nameTag := spell.Tags.Find("name")
|
||||||
|
name := ""
|
||||||
|
if nameTag != nil {
|
||||||
|
name = nameTag[1]
|
||||||
|
if len(name) > 28 {
|
||||||
|
name = name[:27] + "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
name = ": " + color.HiMagentaString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := spell.Content
|
||||||
|
if len(desc) > 50 {
|
||||||
|
desc = desc[0:49] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := nip19.EncodeNevent(spell.ID, nil, nostr.ZeroPK)
|
||||||
|
identifier := "spell" + idStr[len(idStr)-7:]
|
||||||
|
|
||||||
|
log("running %s%s - %s\n",
|
||||||
|
color.BlueString(identifier),
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user