spell: store spells locally and add stdin spell to history.

This commit is contained in:
fiatjaf
2025-12-22 11:59:17 -03:00
parent 3be80c29df
commit b95665d986

220
spell.go
View File

@@ -65,38 +65,8 @@ var spell = &cli.Command{
if spell.Kind != 777 { if spell.Kind != 777 {
return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind)
} }
// 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") return runSpell(ctx, c, historyPath, history, nostr.EventPointer{ID: spell.ID}, spell)
logverbose("executing spell from stdin: %s relays=%v outbox=%v stream=%v\n",
spellFilter, spellRelays, outbox, stream)
// execute without adding to history
logSpellDetails(spell)
performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell")
return nil
} }
// no stdin input, show recent spells // no stdin input, show recent spells
@@ -123,13 +93,14 @@ var spell = &cli.Command{
} }
lastUsed := entry.LastUsed.Format("2006-01-02 15:04") lastUsed := entry.LastUsed.Format("2006-01-02 15:04")
stdout(fmt.Sprintf(" %s %s%s - %s\n", stdout(fmt.Sprintf(" %s %s%s - %s",
color.BlueString(entry.Identifier), color.BlueString(entry.Identifier),
displayName, displayName,
color.YellowString(lastUsed), color.YellowString(lastUsed),
desc, desc,
)) ))
} }
return nil return nil
} }
@@ -156,99 +127,132 @@ var spell = &cli.Command{
return fmt.Errorf("invalid spell reference") return fmt.Errorf("invalid spell reference")
} }
// fetch spell // first try to fetch spell from sys.Store
relays := pointer.Relays var spell nostr.Event
if pointer.Author != nostr.ZeroPK { found := false
for _, url := range relays { for evt := range sys.Store.QueryEvents(nostr.Filter{IDs: []nostr.ID{pointer.ID}}, 1) {
sys.Hints.Save(pointer.Author, nostr.NormalizeURL(url), hints.LastInHint, nostr.Now()) spell = evt
} found = true
relays = append(relays, sys.FetchOutboxRelays(ctx, pointer.Author, 3)...) break
} }
spell := sys.Pool.QuerySingle(ctx, relays, nostr.Filter{IDs: []nostr.ID{pointer.ID}},
nostr.SubscriptionOptions{Label: "nak-spell-f"}) var relays []string
if spell == nil { if !found {
return fmt.Errorf("spell event not 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 { if spell.Kind != 777 {
return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind) return fmt.Errorf("event is not a spell (expected kind 777, got %d)", spell.Kind)
} }
// parse spell tags to build REQ filter return runSpell(ctx, c, historyPath, history, pointer, spell)
spellFilter, err := buildSpellReq(ctx, c, spell.Tags) },
}
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 { if err != nil {
return fmt.Errorf("failed to parse spell tags: %w", err) return err
} }
data, _ := json.Marshal(SpellHistoryEntry{
// determine relays to query Identifier: identifier,
var spellRelays []string Name: name,
var outbox bool Content: spell.Content,
relaysTag := spell.Event.Tags.Find("relays") LastUsed: time.Now(),
if relaysTag == nil { Pointer: pointer,
// if this tag doesn't exist assume $outbox })
relaysTag = nostr.Tag{"relays", "$outbox"} file.Write(data)
} file.Write([]byte{'\n'})
for i := 1; i < len(relaysTag); i++ { for i, entry := range history {
switch relaysTag[i] { if entry.Identifier == identifier {
case "$outbox": continue
outbox = true
default:
relays = append(relays, relaysTag[i])
} }
}
stream := !spell.Tags.Has("close-on-eose") data, _ := json.Marshal(entry)
// fill in the author if we didn't have it
pointer.Author = spell.PubKey
// 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(data)
file.Write([]byte{'\n'}) file.Write([]byte{'\n'})
for i, entry := range history {
// limit history size (keep last 100)
if i == 100 {
break
}
data, _ := json.Marshal(entry) // limit history size (keep last 100)
file.Write(data) if i == 100 {
file.Write([]byte{'\n'}) break
} }
file.Close()
logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n",
identifier, spellFilter, spellRelays, outbox, stream)
} }
file.Close()
// execute logverbose("executing %s: %s relays=%v outbox=%v stream=%v\n",
logSpellDetails(spell.Event) identifier, spellFilter, spellRelays, outbox, stream)
performReq(ctx, spellFilter, spellRelays, stream, outbox, c.Uint("outbox-relays-per-pubkey"), false, 0, "nak-spell") }
return nil // 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) { func buildSpellReq(ctx context.Context, c *cli.Command, tags nostr.Tags) (nostr.Filter, error) {