Files
vnak/paste.go
2026-01-09 00:26:55 -03:00

383 lines
10 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip05"
"fiatjaf.com/nostr/nip19"
"github.com/btcsuite/btcd/btcec/v2"
qt "github.com/mappu/miqt/qt6"
"github.com/mappu/miqt/qt6/mainthread"
)
type pasteVars struct {
inputEdit *qt.QTextEdit
outputVBox *qt.QVBoxLayout
nip05ctxCancel context.CancelFunc
nip05ctxAbort error
}
var paste = &pasteVars{
nip05ctxAbort: errors.New("aborted"),
}
func setupPasteTab() *qt.QWidget {
tab := qt.NewQWidget(window.QWidget)
layout := qt.NewQVBoxLayout2()
tab.SetLayout(layout.QLayout)
// input
inputLabel := qt.NewQLabel2()
inputLabel.SetText("paste an event, nevent, npub, nip05, filter, naddr or other things:")
layout.AddWidget(inputLabel.QWidget)
paste.inputEdit = qt.NewQTextEdit(tab)
layout.AddWidget(paste.inputEdit.QWidget)
paste.inputEdit.OnTextChanged(paste.updatePaste)
// output
paste.outputVBox = qt.NewQVBoxLayout2()
layout.AddLayout(paste.outputVBox.QLayout)
return tab
}
func deleteLayoutRecursively(layout *qt.QLayout) {
for layout.Count() > 0 {
layoutItem := layout.ItemAt(0)
if w := layoutItem.Widget(); w != nil {
w.DeleteLater()
} else if subLayout := layoutItem.Layout(); subLayout != nil {
deleteLayoutRecursively(subLayout)
}
layout.RemoveItem(layoutItem)
}
layout.DeleteLater()
}
func (paste *pasteVars) updatePaste() {
// clear previous output
for paste.outputVBox.Count() > 0 {
item := paste.outputVBox.ItemAt(0)
if widget := item.Widget(); widget != nil {
widget.DeleteLater()
} else if layout := item.Layout(); layout != nil {
deleteLayoutRecursively(layout)
}
paste.outputVBox.RemoveItem(item)
}
// process current
text := strings.TrimSpace(paste.inputEdit.ToPlainText())
if text == "" {
return
}
// try nip19 decode
if prefix, decoded, err := nip19.Decode(text); err == nil {
paste.displayNip19Decoded(prefix, decoded)
return
}
// try nip05
if nip05.IsValidIdentifier(text) {
debounced.Call(func() {
if nip05.IsValidIdentifier(text) {
paste.displayNip05(text)
}
})
return
}
// try JSON event
var event nostr.Event
if err := json.Unmarshal([]byte(text), &event); err == nil && (event.ID != nostr.ZeroID || event.Kind != 0 || event.CreatedAt != 0 || event.Content != "" || event.Tags != nil || event.PubKey != nostr.ZeroPK) {
paste.displayEventButton(event)
return
}
// try JSON filter
var filter nostr.Filter
if err := json.Unmarshal([]byte(text), &filter); err == nil {
paste.displayFilterButton(filter)
return
}
// if nothing worked, show error
errorLabel := qt.NewQLabel2()
errorLabel.SetText("could not decode input")
paste.outputVBox.AddWidget(errorLabel.QWidget)
}
func (p *pasteVars) displayNip19Decoded(prefix string, decoded interface{}) {
switch prefix {
case "nsec":
if sk, ok := decoded.(nostr.SecretKey); ok {
p.displayNsec(sk)
}
case "npub":
if pk, ok := decoded.(nostr.PubKey); ok {
p.displayPubKey(pk)
}
case "note":
if id, ok := decoded.(nostr.ID); ok {
p.displayEventID(id)
}
case "nevent":
if ep, ok := decoded.(nostr.EventPointer); ok {
p.displayEventPointer(ep)
p.displayPointerTag(ep)
}
case "nprofile":
if pp, ok := decoded.(nostr.ProfilePointer); ok {
p.displayProfilePointer(pp)
p.displayPointerTag(pp)
}
case "naddr":
if ap, ok := decoded.(nostr.EntityPointer); ok {
p.displayAddressPointer(ap)
p.displayPointerTag(ap)
}
default:
label := qt.NewQLabel2()
label.SetText(fmt.Sprintf("decoded %s: %v", prefix, decoded))
p.outputVBox.AddWidget(label.QWidget)
}
}
func (p *pasteVars) displayNsec(sk nostr.SecretKey) {
// nsec
nsecLabel := qt.NewQLabel2()
nsecLabel.SetText("nsec:")
p.outputVBox.AddWidget(nsecLabel.QWidget)
nsec := nip19.EncodeNsec(sk)
nsecEdit := qt.NewQLineEdit(window.QWidget)
nsecEdit.SetText(nsec)
nsecEdit.SetReadOnly(true)
p.outputVBox.AddWidget(nsecEdit.QWidget)
// hex
hexLabel := qt.NewQLabel2()
hexLabel.SetText("hex:")
p.outputVBox.AddWidget(hexLabel.QWidget)
hexEdit := qt.NewQLineEdit(window.QWidget)
hexEdit.SetText(sk.Hex())
hexEdit.SetReadOnly(true)
p.outputVBox.AddWidget(hexEdit.QWidget)
// npub
npubLabel := qt.NewQLabel2()
npubLabel.SetText("corresponding npub:")
p.outputVBox.AddWidget(npubLabel.QWidget)
_, pub := btcec.PrivKeyFromBytes(sk[:])
pk := nostr.PubKey(pub.SerializeCompressed()[1:])
npub := nip19.EncodeNpub(pk)
npubEdit := qt.NewQLineEdit(window.QWidget)
npubEdit.SetText(npub)
npubEdit.SetReadOnly(true)
p.outputVBox.AddWidget(npubEdit.QWidget)
}
func (p *pasteVars) displayPubKey(pk nostr.PubKey) {
npubLabel := qt.NewQLabel2()
npubLabel.SetText("npub:")
p.outputVBox.AddWidget(npubLabel.QWidget)
npub := nip19.EncodeNpub(pk)
npubEdit := qt.NewQLineEdit(window.QWidget)
npubEdit.SetText(npub)
npubEdit.SetReadOnly(true)
p.outputVBox.AddWidget(npubEdit.QWidget)
hexLabel := qt.NewQLabel2()
hexLabel.SetText("hex:")
p.outputVBox.AddWidget(hexLabel.QWidget)
hexEdit := qt.NewQLineEdit(window.QWidget)
hexEdit.SetText(pk.Hex())
hexEdit.SetReadOnly(true)
p.outputVBox.AddWidget(hexEdit.QWidget)
}
func (p *pasteVars) displayEventID(id nostr.ID) {
hexLabel := qt.NewQLabel2()
hexLabel.SetText("id (hex):")
p.outputVBox.AddWidget(hexLabel.QWidget)
hexEdit := qt.NewQLineEdit(window.QWidget)
hexEdit.SetText(id.Hex())
hexEdit.SetReadOnly(true)
p.outputVBox.AddWidget(hexEdit.QWidget)
}
func (p *pasteVars) displayPointerTag(pointer nostr.Pointer) {
tagLabel := qt.NewQLabel2()
tagLabel.SetText("tag reference:")
p.outputVBox.AddWidget(tagLabel.QWidget)
tagEdit := qt.NewQLineEdit(window.QWidget)
tagj, _ := json.Marshal(pointer.AsTag())
tagEdit.SetText(string(tagj))
tagEdit.SetReadOnly(true)
p.outputVBox.AddWidget(tagEdit.QWidget)
}
func (p *pasteVars) displayEventPointer(ep nostr.EventPointer) {
p.displayEventID(ep.ID)
if ep.Author != nostr.ZeroPK {
authorLabel := qt.NewQLabel2()
authorLabel.SetText("author:")
p.outputVBox.AddWidget(authorLabel.QWidget)
authorEdit := qt.NewQLineEdit(window.QWidget)
authorEdit.SetText(ep.Author.Hex())
authorEdit.SetReadOnly(true)
p.outputVBox.AddWidget(authorEdit.QWidget)
}
p.displayRelays(ep.Relays)
button := qt.NewQPushButton5("filter "+ARROW, window.QWidget)
button.OnClicked(func() {
filter := nostr.Filter{
IDs: []nostr.ID{ep.ID},
}
if ep.Author != nostr.ZeroPK {
filter.Authors = []nostr.PubKey{ep.Author}
}
req.populate(filter, ep.Relays)
tabWidget.SetCurrentIndex(tabs.req)
})
p.outputVBox.AddWidget(button.QWidget)
}
func (p *pasteVars) displayProfilePointer(pp nostr.ProfilePointer) {
p.displayPubKey(pp.PublicKey)
p.displayRelays(pp.Relays)
button := qt.NewQPushButton5("filter "+ARROW, window.QWidget)
button.OnClicked(func() {
filter := nostr.Filter{
Authors: []nostr.PubKey{pp.PublicKey},
}
req.populate(filter, pp.Relays)
tabWidget.SetCurrentIndex(tabs.req)
})
p.outputVBox.AddWidget(button.QWidget)
}
func (p *pasteVars) displayRelays(relays []string) {
if len(relays) > 0 {
relaysLabel := qt.NewQLabel2()
relaysLabel.SetText("relays:")
p.outputVBox.AddWidget(relaysLabel.QWidget)
relaysVBox := qt.NewQVBoxLayout2()
for i := 0; i < len(relays); i += 5 {
rowHBox := qt.NewQHBoxLayout2()
for j := 0; j < 5 && i+j < len(relays); j++ {
relayEdit := qt.NewQLineEdit(window.QWidget)
relayEdit.SetText(relays[i+j])
relayEdit.SetReadOnly(true)
rowHBox.AddWidget(relayEdit.QWidget)
}
relaysVBox.AddLayout(rowHBox.QLayout)
}
p.outputVBox.AddLayout(relaysVBox.QLayout)
}
}
func (p *pasteVars) displayAddressPointer(ap nostr.EntityPointer) {
kindLabel := qt.NewQLabel2()
kindLabel.SetText("kind:")
p.outputVBox.AddWidget(kindLabel.QWidget)
kindEdit := qt.NewQLineEdit(window.QWidget)
kindEdit.SetText(fmt.Sprintf("%d", ap.Kind))
kindEdit.SetReadOnly(true)
p.outputVBox.AddWidget(kindEdit.QWidget)
p.displayPubKey(ap.PublicKey)
identifierLabel := qt.NewQLabel2()
identifierLabel.SetText("identifier:")
p.outputVBox.AddWidget(identifierLabel.QWidget)
identifierEdit := qt.NewQLineEdit(window.QWidget)
identifierEdit.SetText(ap.Identifier)
identifierEdit.SetReadOnly(true)
p.outputVBox.AddWidget(identifierEdit.QWidget)
p.displayRelays(ap.Relays)
button := qt.NewQPushButton5("filter "+ARROW, window.QWidget)
button.OnClicked(func() {
filter := nostr.Filter{
Kinds: []nostr.Kind{ap.Kind},
Authors: []nostr.PubKey{ap.PublicKey},
Tags: map[string][]string{"d": {ap.Identifier}},
}
req.populate(filter, ap.Relays)
tabWidget.SetCurrentIndex(tabs.req)
})
p.outputVBox.AddWidget(button.QWidget)
}
func (p *pasteVars) displayNip05(identifier string) {
mainthread.Wait(func() {
label := qt.NewQLabel2()
label.SetText("nip05: " + identifier)
p.outputVBox.AddWidget(label.QWidget)
})
// try to query
if paste.nip05ctxCancel != nil {
paste.nip05ctxCancel()
}
nip05ctx, cancel := context.WithTimeoutCause(ctx, time.Second*3, paste.nip05ctxAbort)
paste.nip05ctxCancel = cancel
defer cancel()
pp, err := nip05.QueryIdentifier(nip05ctx, identifier)
if err != nil {
if err == paste.nip05ctxAbort || context.Cause(nip05ctx) == paste.nip05ctxAbort {
return
}
mainthread.Wait(func() {
errorLabel := qt.NewQLabel2()
errorLabel.SetText("failed to query nip05: " + err.Error())
p.outputVBox.AddWidget(errorLabel.QWidget)
})
return
}
mainthread.Wait(func() {
nprofileLabel := qt.NewQLabel2()
nprofileLabel.SetText("nprofile")
p.outputVBox.AddWidget(nprofileLabel.QWidget)
nprofileEdit := qt.NewQLineEdit(window.QWidget)
nprofileEdit.SetText(nip19.EncodeNprofile(pp.PublicKey, pp.Relays))
nprofileEdit.SetReadOnly(true)
p.outputVBox.AddWidget(nprofileEdit.QWidget)
p.displayProfilePointer(*pp)
p.displayPointerTag(*pp)
})
}
func (p *pasteVars) displayEventButton(evt nostr.Event) {
button := qt.NewQPushButton5("event "+ARROW, window.QWidget)
button.OnClicked(func() {
event.populate(evt)
// switch to event tab
tabWidget.SetCurrentIndex(tabs.event)
})
p.outputVBox.AddWidget(button.QWidget)
}
func (p *pasteVars) displayFilterButton(filter nostr.Filter) {
button := qt.NewQPushButton5("filter "+ARROW, window.QWidget)
button.OnClicked(func() {
req.populate(filter, []string{})
// switch to req tab
tabWidget.SetCurrentIndex(tabs.req)
})
p.outputVBox.AddWidget(button.QWidget)
}