mirror of
https://github.com/fiatjaf/vnak.git
synced 2026-10-05 15:38:23 +00:00
bunker and build (work-in-progress).
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr/schema"
|
||||||
|
qt "github.com/mappu/miqt/qt6"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buildVars struct {
|
||||||
|
kindsCombo *qt.QComboBox
|
||||||
|
}
|
||||||
|
|
||||||
|
type kindItem struct {
|
||||||
|
kind uint16
|
||||||
|
schema.KindSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
var build = &buildVars{}
|
||||||
|
|
||||||
|
func setupBuildTab() *qt.QWidget {
|
||||||
|
tab := qt.NewQWidget(window.QWidget)
|
||||||
|
layout := qt.NewQVBoxLayout2()
|
||||||
|
tab.SetLayout(layout.QLayout)
|
||||||
|
|
||||||
|
// label
|
||||||
|
label := qt.NewQLabel2()
|
||||||
|
label.SetText("select a kind to build an event:")
|
||||||
|
layout.AddWidget(label.QWidget)
|
||||||
|
|
||||||
|
// dropdown
|
||||||
|
build.kindsCombo = qt.NewQComboBox(tab)
|
||||||
|
layout.AddWidget(build.kindsCombo.QWidget)
|
||||||
|
|
||||||
|
// load kinds
|
||||||
|
loadKinds()
|
||||||
|
|
||||||
|
return tab
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadKinds() {
|
||||||
|
sch, err := schema.FetchSchemaFromURL(schema.DefaultSchemaURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error loading schema:", err)
|
||||||
|
setStatus(tabs.build, "error loading schema: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]kindItem, 0, len(sch.Kinds))
|
||||||
|
for k, v := range sch.Kinds {
|
||||||
|
num := -1
|
||||||
|
fmt.Sscanf(k, "%d", &num)
|
||||||
|
items = append(items, kindItem{kind: uint16(num), KindSchema: v})
|
||||||
|
}
|
||||||
|
slices.SortFunc(items, func(a, b kindItem) int { return int(a.kind - b.kind) })
|
||||||
|
|
||||||
|
model := qt.NewQStandardItemModel2(len(items)+2, 2)
|
||||||
|
inUseCategory := qt.NewQStandardItem2("common")
|
||||||
|
inUseCategory.SetFlags(inUseCategory.Flags() ^ qt.ItemIsSelectable) // remove selectable flag
|
||||||
|
inUseCategory.SetData(qt.NewQVariant(), int(qt.FontRole)) // bold font
|
||||||
|
model.AppendRowWithItem(inUseCategory)
|
||||||
|
|
||||||
|
notInUseCategory := qt.NewQStandardItem2("weird")
|
||||||
|
notInUseCategory.SetFlags(inUseCategory.Flags() ^ qt.ItemIsSelectable) // remove selectable flag
|
||||||
|
notInUseCategory.SetData(qt.NewQVariant(), int(qt.FontRole)) // bold font
|
||||||
|
model.AppendRowWithItem(notInUseCategory)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
qitem := []*qt.QStandardItem{
|
||||||
|
qt.NewQStandardItem2(strconv.Itoa(int(item.kind))),
|
||||||
|
qt.NewQStandardItem2(item.Description),
|
||||||
|
}
|
||||||
|
if item.InUse {
|
||||||
|
inUseCategory.AppendRow(qitem)
|
||||||
|
} else {
|
||||||
|
notInUseCategory.AppendRow(qitem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := qt.NewQSortFilterProxyModel()
|
||||||
|
proxy.SetSourceModel(model.QAbstractItemModel)
|
||||||
|
proxy.SetFilterCaseSensitivity(qt.CaseInsensitive)
|
||||||
|
build.kindsCombo.SetModel(proxy.QAbstractItemModel)
|
||||||
|
build.kindsCombo.SetEditable(true)
|
||||||
|
|
||||||
|
tableView := qt.NewQAbstractItemView2()
|
||||||
|
tableView.SetSelectionBehavior(qt.QAbstractItemView__SelectRows)
|
||||||
|
tableView.SetSelectionMode(qt.QAbstractItemView__SingleSelection)
|
||||||
|
build.kindsCombo.SetView(tableView)
|
||||||
|
|
||||||
|
build.kindsCombo.OnCurrentIndexChanged(func(index int) {
|
||||||
|
sourceIndex := proxy.MapToSource(proxy.Index(index, 0, qt.NewQModelIndex()))
|
||||||
|
actualIndex := sourceIndex.Row()
|
||||||
|
selectedItem := items[actualIndex]
|
||||||
|
fmt.Println("selected", selectedItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
build.kindsCombo.OnEditTextChanged(func(text string) {
|
||||||
|
fmt.Println("########", text)
|
||||||
|
|
||||||
|
proxy.SetFilterFixedString(text)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip19"
|
||||||
|
"fiatjaf.com/nostr/nip46"
|
||||||
|
qt "github.com/mappu/miqt/qt6"
|
||||||
|
"github.com/mappu/miqt/qt6/mainthread"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bunkerVars struct {
|
||||||
|
tab *qt.QWidget
|
||||||
|
|
||||||
|
loadProfileButton *qt.QPushButton
|
||||||
|
startButton *qt.QPushButton
|
||||||
|
stopButton *qt.QPushButton
|
||||||
|
|
||||||
|
authorizedKeysList *qt.QListWidget
|
||||||
|
logsList *qt.QListWidget
|
||||||
|
bunkerURIInput *qt.QLineEdit
|
||||||
|
|
||||||
|
relaysHBox *qt.QHBoxLayout
|
||||||
|
relaysEdits []*qt.QLineEdit
|
||||||
|
|
||||||
|
config struct {
|
||||||
|
AuthorizedKeys []nostr.PubKey `json:"authorized-keys"`
|
||||||
|
Secret plainOrEncryptedKey `json:"sec"`
|
||||||
|
Relays []string `json:"relays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
relays []*nostr.Relay
|
||||||
|
signer nip46.StaticKeySigner
|
||||||
|
handlerWg sync.WaitGroup
|
||||||
|
cancel context.CancelFunc
|
||||||
|
newSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
var bunkerTab = &bunkerVars{}
|
||||||
|
|
||||||
|
type plainOrEncryptedKey struct {
|
||||||
|
Plain *nostr.SecretKey
|
||||||
|
Encrypted *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe plainOrEncryptedKey) MarshalJSON() ([]byte, error) {
|
||||||
|
if pe.Plain != nil {
|
||||||
|
res := make([]byte, 66)
|
||||||
|
hex.Encode(res[1:], (*pe.Plain)[:])
|
||||||
|
res[0] = '"'
|
||||||
|
res[65] = '"'
|
||||||
|
return res, nil
|
||||||
|
} else if pe.Encrypted != nil {
|
||||||
|
return json.Marshal(*pe.Encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no key to marshal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe *plainOrEncryptedKey) UnmarshalJSON(buf []byte) error {
|
||||||
|
if len(buf) == 66 {
|
||||||
|
sk, err := nostr.SecretKeyFromHex(string(buf[1 : 1+64]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pe.Plain = &sk
|
||||||
|
return nil
|
||||||
|
} else if bytes.HasPrefix(buf, []byte("\"nsec")) {
|
||||||
|
_, v, err := nip19.Decode(string(buf[1 : len(buf)-1]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sk := v.(nostr.SecretKey)
|
||||||
|
pe.Plain = &sk
|
||||||
|
return nil
|
||||||
|
} else if bytes.HasPrefix(buf, []byte("\"ncryptsec1")) {
|
||||||
|
ncryptsec := string(buf[1 : len(buf)-1])
|
||||||
|
pe.Encrypted = &ncryptsec
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unrecognized key format '%s'", string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupBunkerTab() *qt.QWidget {
|
||||||
|
bunkerTab.tab = qt.NewQWidget(window.QWidget)
|
||||||
|
layout := qt.NewQVBoxLayout2()
|
||||||
|
bunkerTab.tab.SetLayout(layout.QLayout)
|
||||||
|
|
||||||
|
// buttons row
|
||||||
|
buttonsHBox := qt.NewQHBoxLayout2()
|
||||||
|
layout.AddLayout(buttonsHBox.QLayout)
|
||||||
|
|
||||||
|
bunkerTab.loadProfileButton = qt.NewQPushButton5("load profile", bunkerTab.tab)
|
||||||
|
buttonsHBox.AddWidget(bunkerTab.loadProfileButton.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.startButton = qt.NewQPushButton5("start", bunkerTab.tab)
|
||||||
|
buttonsHBox.AddWidget(bunkerTab.startButton.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.stopButton = qt.NewQPushButton5("stop", bunkerTab.tab)
|
||||||
|
bunkerTab.stopButton.SetEnabled(false)
|
||||||
|
buttonsHBox.AddWidget(bunkerTab.stopButton.QWidget)
|
||||||
|
|
||||||
|
// bunker URI input
|
||||||
|
uriHBox := qt.NewQHBoxLayout2()
|
||||||
|
layout.AddLayout(uriHBox.QLayout)
|
||||||
|
|
||||||
|
uriLabel := qt.NewQLabel2()
|
||||||
|
uriLabel.SetText("bunker URI:")
|
||||||
|
uriHBox.AddWidget(uriLabel.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.bunkerURIInput = qt.NewQLineEdit(bunkerTab.tab)
|
||||||
|
bunkerTab.bunkerURIInput.SetReadOnly(true)
|
||||||
|
uriHBox.AddWidget(bunkerTab.bunkerURIInput.QWidget)
|
||||||
|
|
||||||
|
// relays
|
||||||
|
relaysHBox := qt.NewQHBoxLayout2()
|
||||||
|
layout.AddLayout(relaysHBox.QLayout)
|
||||||
|
bunkerTab.relaysHBox = relaysHBox
|
||||||
|
relaysLabel := qt.NewQLabel2()
|
||||||
|
relaysLabel.SetText("relays:")
|
||||||
|
relaysHBox.AddWidget(relaysLabel.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.relaysEdits = []*qt.QLineEdit{}
|
||||||
|
bunkerTab.addRelay("")
|
||||||
|
|
||||||
|
// tables
|
||||||
|
tablesHBox := qt.NewQHBoxLayout2()
|
||||||
|
layout.AddLayout(tablesHBox.QLayout)
|
||||||
|
|
||||||
|
// authorized keys list
|
||||||
|
keysVBox := qt.NewQVBoxLayout2()
|
||||||
|
keysLabel := qt.NewQLabel2()
|
||||||
|
keysLabel.SetText("authorized keys:")
|
||||||
|
keysVBox.AddWidget(keysLabel.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.authorizedKeysList = qt.NewQListWidget(bunkerTab.tab)
|
||||||
|
keysVBox.AddWidget(bunkerTab.authorizedKeysList.QWidget)
|
||||||
|
tablesHBox.AddLayout(keysVBox.QLayout)
|
||||||
|
|
||||||
|
// logs list
|
||||||
|
logsVBox := qt.NewQVBoxLayout2()
|
||||||
|
logsLabel := qt.NewQLabel2()
|
||||||
|
logsLabel.SetText("logs:")
|
||||||
|
logsVBox.AddWidget(logsLabel.QWidget)
|
||||||
|
|
||||||
|
bunkerTab.logsList = qt.NewQListWidget(bunkerTab.tab)
|
||||||
|
logsVBox.AddWidget(bunkerTab.logsList.QWidget)
|
||||||
|
tablesHBox.AddLayout(logsVBox.QLayout)
|
||||||
|
|
||||||
|
// connect signals
|
||||||
|
bunkerTab.loadProfileButton.OnClicked(bunkerTab.loadProfile)
|
||||||
|
bunkerTab.startButton.OnClicked(bunkerTab.startBunker)
|
||||||
|
bunkerTab.stopButton.OnClicked(bunkerTab.stopBunker)
|
||||||
|
|
||||||
|
return bunkerTab.tab
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) loadProfile() {
|
||||||
|
fileDialog := qt.NewQFileDialog(window.QWidget)
|
||||||
|
fileDialog.SetFileMode(qt.QFileDialog__ExistingFile)
|
||||||
|
fileDialog.SetNameFilter("JSON files (*.json)")
|
||||||
|
fileDialog.SetWindowTitle("select bunker profile")
|
||||||
|
|
||||||
|
if fileDialog.Exec() == int(qt.QDialog__Accepted) {
|
||||||
|
selectedFiles := fileDialog.SelectedFiles()
|
||||||
|
if len(selectedFiles) > 0 {
|
||||||
|
filename := selectedFiles[0]
|
||||||
|
b.loadProfileFromFile(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) loadProfileFromFile(filename string) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
setStatus(tabs.bunker, "failed to read profile: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &b.config); err != nil {
|
||||||
|
setStatus(tabs.bunker, "failed to parse profile: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize relays
|
||||||
|
for i, url := range b.config.Relays {
|
||||||
|
b.config.Relays[i] = nostr.NormalizeURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set currentSec and currentKeyer if secret is available
|
||||||
|
if b.config.Secret.Plain != nil {
|
||||||
|
secEdit.SetText(b.config.Secret.Plain.Hex())
|
||||||
|
} else if b.config.Secret.Encrypted != nil {
|
||||||
|
secEdit.SetText(*b.config.Secret.Encrypted)
|
||||||
|
}
|
||||||
|
keyChanged(secEdit.Text())
|
||||||
|
|
||||||
|
b.updateAuthorizedKeysTable()
|
||||||
|
b.updateRelaysUI()
|
||||||
|
setStatus(tabs.bunker, "loaded profile from %s", filepath.Base(filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) updateAuthorizedKeysTable() {
|
||||||
|
mainthread.Start(func() {
|
||||||
|
b.authorizedKeysList.Clear()
|
||||||
|
for _, pubkey := range b.config.AuthorizedKeys {
|
||||||
|
npub := nip19.EncodeNpub(pubkey)
|
||||||
|
item := qt.NewQListWidgetItem2(fmt.Sprintf("%s %s", pubkey.Hex(), npub))
|
||||||
|
b.authorizedKeysList.AddItemWithItem(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) addRelay(value string) {
|
||||||
|
edit := qt.NewQLineEdit(b.tab)
|
||||||
|
edit.SetText(value)
|
||||||
|
b.relaysEdits = append(b.relaysEdits, edit)
|
||||||
|
b.relaysHBox.AddWidget(edit.QWidget)
|
||||||
|
edit.OnTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == b.relaysEdits[len(b.relaysEdits)-1] {
|
||||||
|
b.addRelay("")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(b.relaysEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(b.relaysEdits[n-1].Text()) == "" && strings.TrimSpace(b.relaysEdits[n-2].Text()) == "" {
|
||||||
|
b.relaysHBox.RemoveWidget(b.relaysEdits[n-1].QWidget)
|
||||||
|
b.relaysEdits[n-1].DeleteLater()
|
||||||
|
b.relaysEdits = b.relaysEdits[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.updateConfigRelays()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) updateConfigRelays() {
|
||||||
|
b.config.Relays = []string{}
|
||||||
|
for _, edit := range b.relaysEdits {
|
||||||
|
text := strings.TrimSpace(edit.Text())
|
||||||
|
if text != "" {
|
||||||
|
b.config.Relays = append(b.config.Relays, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bunkerVars) updateRelaysUI() {
|
||||||
|
mainthread.Start(func() {
|
||||||
|
// clear existing
|
||||||
|
for _, edit := range b.relaysEdits {
|
||||||
|
b.relaysHBox.RemoveWidget(edit.QWidget)
|
||||||
|
edit.DeleteLater()
|
||||||
|
}
|
||||||
|
b.relaysEdits = []*qt.QLineEdit{}
|
||||||
|
// add from config
|
||||||
|
for _, relay := range b.config.Relays {
|
||||||
|
b.addRelay(relay)
|
||||||
|
}
|
||||||
|
b.addRelay("") // extra
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bunker *bunkerVars) log(message string, args ...any) {
|
||||||
|
msg := fmt.Sprintf(message, args...)
|
||||||
|
timestamp := time.Now().Format("15:04:05")
|
||||||
|
mainthread.Start(func() {
|
||||||
|
pos := bunker.logsList.VerticalScrollBar().SliderPosition()
|
||||||
|
bunker.logsList.InsertItem(0, qt.NewQListWidgetItem2(fmt.Sprintf("%s %s", timestamp, msg)))
|
||||||
|
if pos == 0 {
|
||||||
|
bunker.logsList.ScrollToTop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bunker *bunkerVars) startBunker() {
|
||||||
|
if len(bunker.config.Relays) == 0 {
|
||||||
|
setStatus(tabs.bunker, "no relays configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSec == [32]byte{} {
|
||||||
|
setStatus(tabs.bunker, "no secret key configured")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bunker.startButton.SetEnabled(false)
|
||||||
|
bunker.stopButton.SetEnabled(true)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
bunker.cancel = cancel
|
||||||
|
|
||||||
|
// generate new secret for bunker URI
|
||||||
|
bunker.newSecret = randString(12)
|
||||||
|
|
||||||
|
// connect to relays
|
||||||
|
bunker.relays = connectToAllRelays(ctx, bunker, bunker.config.Relays, nil, nostr.PoolOptions{})
|
||||||
|
if len(bunker.relays) == 0 {
|
||||||
|
setStatus(tabs.bunker, "failed to connect to any relays")
|
||||||
|
bunker.stopBunker()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// update bunker URI
|
||||||
|
bunker.updateBunkerURI()
|
||||||
|
|
||||||
|
// setup signer
|
||||||
|
bunker.signer = nip46.NewStaticKeySigner(currentSec)
|
||||||
|
|
||||||
|
// setup authorization
|
||||||
|
bunker.signer.AuthorizeRequest = func(harmless bool, from nostr.PubKey, secret string) bool {
|
||||||
|
if secret == bunker.newSecret {
|
||||||
|
// store this key
|
||||||
|
bunker.config.AuthorizedKeys = appendUnique(bunker.config.AuthorizedKeys, from)
|
||||||
|
// discard this and generate a new secret
|
||||||
|
bunker.newSecret = randString(12)
|
||||||
|
// update URI
|
||||||
|
bunker.updateBunkerURI()
|
||||||
|
// update table
|
||||||
|
bunker.updateAuthorizedKeysTable()
|
||||||
|
bunker.log("new client authorized: %s", from.Hex())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
authorized := slices.Contains(bunker.config.AuthorizedKeys, from) || slices.Contains([]string{}, secret) // TODO: authorized secrets
|
||||||
|
if authorized {
|
||||||
|
if harmless {
|
||||||
|
bunker.log("harmless request from %s", from.Hex())
|
||||||
|
} else {
|
||||||
|
bunker.log("signing request from %s", from.Hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return authorized
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribe to requests
|
||||||
|
pubkey := currentSec.Public()
|
||||||
|
relayURLs := make([]string, len(bunker.relays))
|
||||||
|
for i, relay := range bunker.relays {
|
||||||
|
relayURLs[i] = relay.URL
|
||||||
|
}
|
||||||
|
events := sys.Pool.SubscribeMany(ctx, relayURLs, nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{nostr.KindNostrConnect},
|
||||||
|
Tags: nostr.TagMap{"p": []string{pubkey.Hex()}},
|
||||||
|
Since: nostr.Now(),
|
||||||
|
LimitZero: true,
|
||||||
|
}, nostr.SubscriptionOptions{
|
||||||
|
Label: "vnak-bunker",
|
||||||
|
})
|
||||||
|
|
||||||
|
bunker.log("bunker started")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for ie := range events {
|
||||||
|
_, _, eventResponse, err := bunker.signer.HandleRequest(ctx, ie.Event)
|
||||||
|
if err != nil {
|
||||||
|
bunker.log("failed to handle request from %s: %s", ie.Event.PubKey, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bunker.log("request from %s", ie.Event.PubKey.Hex())
|
||||||
|
|
||||||
|
bunker.handlerWg.Add(len(bunker.relays))
|
||||||
|
for _, relay := range bunker.relays {
|
||||||
|
go func(relay *nostr.Relay) {
|
||||||
|
defer bunker.handlerWg.Done()
|
||||||
|
if relay != nil {
|
||||||
|
err := relay.Publish(ctx, eventResponse)
|
||||||
|
if err != nil {
|
||||||
|
bunker.log("failed to send response: %s", err)
|
||||||
|
} else {
|
||||||
|
bunker.log("sent response through %s", relay.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(relay)
|
||||||
|
}
|
||||||
|
bunker.handlerWg.Wait()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bunker *bunkerVars) stopBunker() {
|
||||||
|
if bunker.cancel != nil {
|
||||||
|
bunker.cancel()
|
||||||
|
}
|
||||||
|
bunker.startButton.SetEnabled(true)
|
||||||
|
bunker.stopButton.SetEnabled(false)
|
||||||
|
bunker.bunkerURIInput.SetText("")
|
||||||
|
bunker.log("bunker stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bunker *bunkerVars) updateBunkerURI() {
|
||||||
|
if bunker.config.Secret.Plain == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey := bunker.config.Secret.Plain.Public()
|
||||||
|
qs := url.Values{}
|
||||||
|
for _, relay := range bunker.relays {
|
||||||
|
qs.Add("relay", relay.URL)
|
||||||
|
}
|
||||||
|
qs.Set("secret", bunker.newSecret)
|
||||||
|
bunkerURI := fmt.Sprintf("bunker://%s?%s", pubkey.Hex(), qs.Encode())
|
||||||
|
|
||||||
|
mainthread.Start(func() {
|
||||||
|
bunker.bunkerURIInput.SetText(bunkerURI)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToAllRelays(ctx context.Context, b *bunkerVars, relayURLs []string, onConnect func(*nostr.Relay), opts nostr.PoolOptions) []*nostr.Relay {
|
||||||
|
relays := make([]*nostr.Relay, 0, len(relayURLs))
|
||||||
|
for _, relayURL := range relayURLs {
|
||||||
|
relay, err := sys.Pool.EnsureRelay(relayURL)
|
||||||
|
if err != nil {
|
||||||
|
b.log("failed to connect to %s: %v", relayURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relays = append(relays, relay)
|
||||||
|
if onConnect != nil {
|
||||||
|
onConnect(relay)
|
||||||
|
}
|
||||||
|
b.log("connected to %s", relayURL)
|
||||||
|
}
|
||||||
|
return relays
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ func setupEventTab() *qt.QWidget {
|
|||||||
copyButton := qt.NewQPushButton5("➡️", event.tab)
|
copyButton := qt.NewQPushButton5("➡️", event.tab)
|
||||||
outputHBox.AddWidget(copyButton.QWidget)
|
outputHBox.AddWidget(copyButton.QWidget)
|
||||||
copyButton.OnClicked(func() {
|
copyButton.OnClicked(func() {
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.paste)
|
tabWidget.SetCurrentIndex(tabs.paste)
|
||||||
paste.inputEdit.SetPlainText(event.outputEdit.ToPlainText())
|
paste.inputEdit.SetPlainText(event.outputEdit.ToPlainText())
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ func setupEventTab() *qt.QWidget {
|
|||||||
|
|
||||||
sendButton.OnClicked(func() {
|
sendButton.OnClicked(func() {
|
||||||
if event.currentEvent == nil {
|
if event.currentEvent == nil {
|
||||||
statusLabel.SetText("no event to publish")
|
setStatus(tabs.event, "no event to publish")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ func setupEventTab() *qt.QWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
statusLabel.SetText("no relays specified")
|
setStatus(tabs.event, "no relays specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ func (event *eventVars) updateEvent() {
|
|||||||
if err := currentKeyer.SignEvent(ctx, &result); err == nil {
|
if err := currentKeyer.SignEvent(ctx, &result); err == nil {
|
||||||
finalize()
|
finalize()
|
||||||
} else {
|
} else {
|
||||||
statusLabel.SetText("failed to sign: " + err.Error())
|
setStatus(tabs.event, "failed to sign: "+err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module github.com/fiatjaf/vnak
|
module github.com/fiatjaf/vnak
|
||||||
|
|
||||||
go 1.25.4
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/lib v0.3.2
|
fiatjaf.com/lib v0.3.2
|
||||||
fiatjaf.com/nostr v0.0.0-20251126120447-7261a4b515ed
|
fiatjaf.com/nostr v0.0.0-20251126120447-7261a4b515ed
|
||||||
github.com/bep/debounce v1.2.1
|
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
github.com/btcsuite/btcd/btcec/v2 v2.3.4
|
||||||
|
github.com/mailru/easyjson v0.9.0
|
||||||
github.com/mappu/miqt v0.12.0
|
github.com/mappu/miqt v0.12.0
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394
|
||||||
@@ -16,6 +16,7 @@ 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/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/bluekeyes/go-gitdiff v0.7.1 // indirect
|
github.com/bluekeyes/go-gitdiff v0.7.1 // indirect
|
||||||
github.com/btcsuite/btcd v0.24.2 // indirect
|
github.com/btcsuite/btcd v0.24.2 // indirect
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
github.com/btcsuite/btcd/btcutil v1.1.5 // indirect
|
||||||
@@ -34,11 +35,11 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/liamg/magic v0.0.1 // indirect
|
github.com/liamg/magic v0.0.1 // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/rs/cors v1.11.1 // indirect
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
github.com/templexxx/cpu v0.0.1 // indirect
|
github.com/templexxx/cpu v0.0.1 // indirect
|
||||||
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
|
github.com/templexxx/xhex v0.0.0-20200614015412-aed53437177b // indirect
|
||||||
github.com/tidwall/gjson v1.18.0 // indirect
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
@@ -52,4 +53,7 @@ require (
|
|||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace fiatjaf.com/nostr => ../nostrlib
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
||||||
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||||
fiatjaf.com/nostr v0.0.0-20251126120447-7261a4b515ed h1:dip4Bxlfj4/N2oIIGCZpcYRrvfMObcXRcyBM5uYswnM=
|
|
||||||
fiatjaf.com/nostr v0.0.0-20251126120447-7261a4b515ed/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
|
||||||
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=
|
||||||
@@ -141,8 +139,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
|
github.com/templexxx/cpu v0.0.1 h1:hY4WdLOgKdc8y13EYklu9OUTXik80BkxHoWvTO6MQQY=
|
||||||
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
github.com/templexxx/cpu v0.0.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||||
|
|||||||
+20
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -116,3 +117,22 @@ func niceRelayURLs(urls []string) []string {
|
|||||||
}
|
}
|
||||||
return nices
|
return nices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func randString(n int) string {
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
|
||||||
|
time.Sleep(1 * time.Nanosecond) // ensure different values
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUnique[T comparable](slice []T, items ...T) []T {
|
||||||
|
for _, item := range items {
|
||||||
|
if !slices.Contains(slice, item) {
|
||||||
|
slice = append(slice, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,15 +23,19 @@ var (
|
|||||||
window *qt.QMainWindow
|
window *qt.QMainWindow
|
||||||
tabWidget *qt.QTabWidget
|
tabWidget *qt.QTabWidget
|
||||||
|
|
||||||
|
keyChanged func(string)
|
||||||
|
secEdit *qt.QLineEdit
|
||||||
currentSec nostr.SecretKey
|
currentSec nostr.SecretKey
|
||||||
currentKeyer nostr.Keyer
|
currentKeyer nostr.Keyer
|
||||||
tabIndexes struct {
|
|
||||||
event int
|
tabs struct {
|
||||||
req int
|
event int
|
||||||
paste int
|
req int
|
||||||
serve int
|
paste int
|
||||||
|
serve int
|
||||||
|
bunker int
|
||||||
|
build int
|
||||||
}
|
}
|
||||||
statusLabel *qt.QLabel
|
|
||||||
|
|
||||||
debounced = debouncer.New(950 * time.Millisecond)
|
debounced = debouncer.New(950 * time.Millisecond)
|
||||||
sys = sdk.NewSystem()
|
sys = sdk.NewSystem()
|
||||||
@@ -92,7 +96,7 @@ func main() {
|
|||||||
|
|
||||||
secHBox := qt.NewQHBoxLayout2()
|
secHBox := qt.NewQHBoxLayout2()
|
||||||
mainLayout.AddLayout(secHBox.QLayout)
|
mainLayout.AddLayout(secHBox.QLayout)
|
||||||
secEdit := qt.NewQLineEdit(centralWidget)
|
secEdit = qt.NewQLineEdit(centralWidget)
|
||||||
secHBox.AddWidget(secEdit.QWidget)
|
secHBox.AddWidget(secEdit.QWidget)
|
||||||
generateButton := qt.NewQPushButton5("generate", centralWidget)
|
generateButton := qt.NewQPushButton5("generate", centralWidget)
|
||||||
secHBox.AddWidget(generateButton.QWidget)
|
secHBox.AddWidget(generateButton.QWidget)
|
||||||
@@ -109,7 +113,7 @@ func main() {
|
|||||||
secPasswordEdit := qt.NewQLineEdit(passwordWidget)
|
secPasswordEdit := qt.NewQLineEdit(passwordWidget)
|
||||||
secPasswordEdit.SetEchoMode(qt.QLineEdit__Password)
|
secPasswordEdit.SetEchoMode(qt.QLineEdit__Password)
|
||||||
passwordHBox.AddWidget(secPasswordEdit.QWidget)
|
passwordHBox.AddWidget(secPasswordEdit.QWidget)
|
||||||
keyChanged := func(text string) {
|
keyChanged = func(text string) {
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
|
|
||||||
var sk nostr.SecretKey
|
var sk nostr.SecretKey
|
||||||
@@ -168,33 +172,48 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
tabWidget = qt.NewQTabWidget(centralWidget)
|
tabWidget = qt.NewQTabWidget(centralWidget)
|
||||||
|
tabWidget.OnCurrentChanged(func(index int) {
|
||||||
|
maybeSetStatusOnTab(index)
|
||||||
|
})
|
||||||
|
|
||||||
eventTab := setupEventTab()
|
eventTab := setupEventTab()
|
||||||
reqTab := setupReqTab()
|
reqTab := setupReqTab()
|
||||||
pasteTab := setupPasteTab()
|
pasteTab := setupPasteTab()
|
||||||
serveTab := setupServeTab()
|
serveTab := setupServeTab()
|
||||||
|
bunkerTab := setupBunkerTab()
|
||||||
|
buildTab := setupBuildTab()
|
||||||
|
|
||||||
tabWidget.AddTab(eventTab, "event")
|
tabWidget.AddTab(eventTab, "event")
|
||||||
tabIndexes.event = 0
|
tabs.event = 0
|
||||||
|
|
||||||
tabWidget.AddTab(reqTab, "req")
|
tabWidget.AddTab(reqTab, "req")
|
||||||
tabIndexes.req = 1
|
tabs.req = 1
|
||||||
|
|
||||||
tabWidget.AddTab(pasteTab, "paste")
|
tabWidget.AddTab(pasteTab, "paste")
|
||||||
tabIndexes.paste = 2
|
tabs.paste = 2
|
||||||
|
|
||||||
tabWidget.AddTab(serveTab, "serve")
|
tabWidget.AddTab(serveTab, "serve")
|
||||||
tabIndexes.serve = 3
|
tabs.serve = 3
|
||||||
|
|
||||||
|
tabWidget.AddTab(bunkerTab, "bunker")
|
||||||
|
tabs.bunker = 4
|
||||||
|
|
||||||
|
tabWidget.AddTab(buildTab, "build")
|
||||||
|
tabs.build = 5
|
||||||
|
|
||||||
switch *initialTab {
|
switch *initialTab {
|
||||||
case "event":
|
case "event":
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.event)
|
tabWidget.SetCurrentIndex(tabs.event)
|
||||||
case "req":
|
case "req":
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.req)
|
tabWidget.SetCurrentIndex(tabs.req)
|
||||||
case "paste":
|
case "paste":
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.paste)
|
tabWidget.SetCurrentIndex(tabs.paste)
|
||||||
case "serve":
|
case "serve":
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.serve)
|
tabWidget.SetCurrentIndex(tabs.serve)
|
||||||
|
case "bunker":
|
||||||
|
tabWidget.SetCurrentIndex(tabs.bunker)
|
||||||
|
case "build":
|
||||||
|
tabWidget.SetCurrentIndex(tabs.build)
|
||||||
default:
|
default:
|
||||||
tabWidget.SetCurrentIndex(0)
|
tabWidget.SetCurrentIndex(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,11 +236,34 @@ func (p *pasteVars) displayEventPointer(ep nostr.EventPointer) {
|
|||||||
p.outputVBox.AddWidget(authorEdit.QWidget)
|
p.outputVBox.AddWidget(authorEdit.QWidget)
|
||||||
}
|
}
|
||||||
p.displayRelays(ep.Relays)
|
p.displayRelays(ep.Relays)
|
||||||
|
|
||||||
|
button := qt.NewQPushButton5("filter ➡️", 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) {
|
func (p *pasteVars) displayProfilePointer(pp nostr.ProfilePointer) {
|
||||||
p.displayPubKey(pp.PublicKey)
|
p.displayPubKey(pp.PublicKey)
|
||||||
p.displayRelays(pp.Relays)
|
p.displayRelays(pp.Relays)
|
||||||
|
|
||||||
|
button := qt.NewQPushButton5("filter ➡️", 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) {
|
func (p *pasteVars) displayRelays(relays []string) {
|
||||||
@@ -283,6 +306,18 @@ func (p *pasteVars) displayAddressPointer(ap nostr.EntityPointer) {
|
|||||||
p.outputVBox.AddWidget(identifierEdit.QWidget)
|
p.outputVBox.AddWidget(identifierEdit.QWidget)
|
||||||
|
|
||||||
p.displayRelays(ap.Relays)
|
p.displayRelays(ap.Relays)
|
||||||
|
|
||||||
|
button := qt.NewQPushButton5("filter ➡️", window.QWidget)
|
||||||
|
button.OnClicked(func() {
|
||||||
|
filter := nostr.Filter{
|
||||||
|
Kinds: []nostr.Kind{ap.Kind},
|
||||||
|
Authors: []nostr.PubKey{ap.PublicKey},
|
||||||
|
Tags: map[string][]string{"d": []string{ap.Identifier}},
|
||||||
|
}
|
||||||
|
req.populate(filter, ap.Relays)
|
||||||
|
tabWidget.SetCurrentIndex(tabs.req)
|
||||||
|
})
|
||||||
|
p.outputVBox.AddWidget(button.QWidget)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pasteVars) displayNip05(identifier string) {
|
func (p *pasteVars) displayNip05(identifier string) {
|
||||||
@@ -328,7 +363,7 @@ func (p *pasteVars) displayEventButton(evt nostr.Event) {
|
|||||||
button.OnClicked(func() {
|
button.OnClicked(func() {
|
||||||
event.populate(evt)
|
event.populate(evt)
|
||||||
// switch to event tab
|
// switch to event tab
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.event)
|
tabWidget.SetCurrentIndex(tabs.event)
|
||||||
})
|
})
|
||||||
p.outputVBox.AddWidget(button.QWidget)
|
p.outputVBox.AddWidget(button.QWidget)
|
||||||
}
|
}
|
||||||
@@ -336,9 +371,9 @@ func (p *pasteVars) displayEventButton(evt nostr.Event) {
|
|||||||
func (p *pasteVars) displayFilterButton(filter nostr.Filter) {
|
func (p *pasteVars) displayFilterButton(filter nostr.Filter) {
|
||||||
button := qt.NewQPushButton5("filter ➡️", window.QWidget)
|
button := qt.NewQPushButton5("filter ➡️", window.QWidget)
|
||||||
button.OnClicked(func() {
|
button.OnClicked(func() {
|
||||||
req.populate(filter)
|
req.populate(filter, []string{})
|
||||||
// switch to req tab
|
// switch to req tab
|
||||||
tabWidget.SetCurrentIndex(tabIndexes.req)
|
tabWidget.SetCurrentIndex(tabs.req)
|
||||||
})
|
})
|
||||||
p.outputVBox.AddWidget(button.QWidget)
|
p.outputVBox.AddWidget(button.QWidget)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,6 +25,7 @@ type reqVars struct {
|
|||||||
tagsVBox *qt.QVBoxLayout
|
tagsVBox *qt.QVBoxLayout
|
||||||
tagRows []reqTagRow
|
tagRows []reqTagRow
|
||||||
tagsLayout *qt.QVBoxLayout
|
tagsLayout *qt.QVBoxLayout
|
||||||
|
relaysHBox *qt.QHBoxLayout
|
||||||
relaysEdits []*qt.QLineEdit
|
relaysEdits []*qt.QLineEdit
|
||||||
sinceEdit *qt.QDateTimeEdit
|
sinceEdit *qt.QDateTimeEdit
|
||||||
sinceCheck *qt.QCheckBox
|
sinceCheck *qt.QCheckBox
|
||||||
@@ -55,6 +55,29 @@ type reqTagRow struct {
|
|||||||
|
|
||||||
var req = &reqVars{}
|
var req = &reqVars{}
|
||||||
|
|
||||||
|
func (req *reqVars) addRelayEdit() {
|
||||||
|
edit := qt.NewQLineEdit(req.tab)
|
||||||
|
req.relaysEdits = append(req.relaysEdits, edit)
|
||||||
|
req.relaysHBox.AddWidget(edit.QWidget)
|
||||||
|
edit.OnTextChanged(func(text string) {
|
||||||
|
if strings.TrimSpace(text) != "" {
|
||||||
|
if edit == req.relaysEdits[len(req.relaysEdits)-1] {
|
||||||
|
req.addRelayEdit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n := len(req.relaysEdits)
|
||||||
|
if n >= 2 && strings.TrimSpace(req.relaysEdits[n-1].Text()) == "" && strings.TrimSpace(req.relaysEdits[n-2].Text()) == "" {
|
||||||
|
req.relaysHBox.RemoveWidget(req.relaysEdits[n-1].QWidget)
|
||||||
|
req.relaysEdits[n-1].DeleteLater()
|
||||||
|
req.relaysEdits = req.relaysEdits[0 : n-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
edit.OnReturnPressed(func() {
|
||||||
|
req.subscribe()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func setupReqTab() *qt.QWidget {
|
func setupReqTab() *qt.QWidget {
|
||||||
req.tab = qt.NewQWidget(window.QWidget)
|
req.tab = qt.NewQWidget(window.QWidget)
|
||||||
layout := qt.NewQVBoxLayout2()
|
layout := qt.NewQVBoxLayout2()
|
||||||
@@ -181,42 +204,18 @@ func setupReqTab() *qt.QWidget {
|
|||||||
outputHBox.AddWidget(req.outputEdit.QWidget)
|
outputHBox.AddWidget(req.outputEdit.QWidget)
|
||||||
|
|
||||||
sendButton := qt.NewQPushButton5("send request", req.tab)
|
sendButton := qt.NewQPushButton5("send request", req.tab)
|
||||||
sendButton.OnClicked(func() {
|
sendButton.OnClicked(req.subscribe)
|
||||||
req.subscribe()
|
|
||||||
})
|
|
||||||
|
|
||||||
// relays
|
// relays
|
||||||
relaysHBox := qt.NewQHBoxLayout2()
|
relaysHBox := qt.NewQHBoxLayout2()
|
||||||
|
req.relaysHBox = relaysHBox
|
||||||
layout.AddLayout(relaysHBox.QLayout)
|
layout.AddLayout(relaysHBox.QLayout)
|
||||||
relaysLabel := qt.NewQLabel2()
|
relaysLabel := qt.NewQLabel2()
|
||||||
relaysLabel.SetText("relays:")
|
relaysLabel.SetText("relays:")
|
||||||
relaysHBox.AddWidget(relaysLabel.QWidget)
|
relaysHBox.AddWidget(relaysLabel.QWidget)
|
||||||
|
|
||||||
req.relaysEdits = []*qt.QLineEdit{}
|
req.relaysEdits = []*qt.QLineEdit{}
|
||||||
var addRelayEdit func()
|
req.addRelayEdit()
|
||||||
addRelayEdit = func() {
|
|
||||||
edit := qt.NewQLineEdit(req.tab)
|
|
||||||
req.relaysEdits = append(req.relaysEdits, edit)
|
|
||||||
relaysHBox.AddWidget(edit.QWidget)
|
|
||||||
edit.OnTextChanged(func(text string) {
|
|
||||||
if strings.TrimSpace(text) != "" {
|
|
||||||
if edit == req.relaysEdits[len(req.relaysEdits)-1] {
|
|
||||||
addRelayEdit()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
n := len(req.relaysEdits)
|
|
||||||
if n >= 2 && strings.TrimSpace(req.relaysEdits[n-1].Text()) == "" && strings.TrimSpace(req.relaysEdits[n-2].Text()) == "" {
|
|
||||||
relaysHBox.RemoveWidget(req.relaysEdits[n-1].QWidget)
|
|
||||||
req.relaysEdits[n-1].DeleteLater()
|
|
||||||
req.relaysEdits = req.relaysEdits[0 : n-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
edit.OnReturnPressed(func() {
|
|
||||||
sendButton.Click()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
addRelayEdit()
|
|
||||||
|
|
||||||
// results
|
// results
|
||||||
resultsVBox := qt.NewQVBoxLayout2()
|
resultsVBox := qt.NewQVBoxLayout2()
|
||||||
@@ -233,11 +232,11 @@ func setupReqTab() *qt.QWidget {
|
|||||||
|
|
||||||
// double-click to show pretty JSON
|
// double-click to show pretty JSON
|
||||||
req.resultsList.OnItemDoubleClicked(func(item *qt.QListWidgetItem) {
|
req.resultsList.OnItemDoubleClicked(func(item *qt.QListWidgetItem) {
|
||||||
var event nostr.Event
|
var evt nostr.Event
|
||||||
if err := json.Unmarshal([]byte(item.Text()), &event); err != nil {
|
if err := json.Unmarshal([]byte(item.Text()), &evt); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pretty, _ := json.MarshalIndent(event, "", " ")
|
pretty, _ := json.MarshalIndent(evt, "", " ")
|
||||||
dialog := qt.NewQDialog(window.QWidget)
|
dialog := qt.NewQDialog(window.QWidget)
|
||||||
dialog.SetWindowTitle("event")
|
dialog.SetWindowTitle("event")
|
||||||
dialog.SetMinimumWidth(400)
|
dialog.SetMinimumWidth(400)
|
||||||
@@ -248,9 +247,31 @@ func setupReqTab() *qt.QWidget {
|
|||||||
textEdit.SetReadOnly(true)
|
textEdit.SetReadOnly(true)
|
||||||
textEdit.SetPlainText(string(pretty))
|
textEdit.SetPlainText(string(pretty))
|
||||||
dlayout.AddWidget(textEdit.QWidget)
|
dlayout.AddWidget(textEdit.QWidget)
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
buttonsHBox := qt.NewQHBoxLayout2()
|
||||||
|
dlayout.AddLayout(buttonsHBox.QLayout)
|
||||||
|
|
||||||
|
editButton := qt.NewQPushButton5("➡️ edit", dialog.QWidget)
|
||||||
|
editButton.OnClicked(func() {
|
||||||
|
paste.inputEdit.SetPlainText(string(pretty))
|
||||||
|
tabWidget.SetCurrentIndex(tabs.paste)
|
||||||
|
dialog.Close()
|
||||||
|
})
|
||||||
|
buttonsHBox.AddWidget(editButton.QWidget)
|
||||||
|
|
||||||
|
publishButton := qt.NewQPushButton5("➡️ publish", dialog.QWidget)
|
||||||
|
publishButton.OnClicked(func() {
|
||||||
|
event.populate(evt)
|
||||||
|
tabWidget.SetCurrentIndex(tabs.event)
|
||||||
|
dialog.Close()
|
||||||
|
})
|
||||||
|
buttonsHBox.AddWidget(publishButton.QWidget)
|
||||||
|
|
||||||
closeButton := qt.NewQPushButton5("close", dialog.QWidget)
|
closeButton := qt.NewQPushButton5("close", dialog.QWidget)
|
||||||
closeButton.OnClicked(func() { dialog.Close() })
|
closeButton.OnClicked(func() { dialog.Close() })
|
||||||
dlayout.AddWidget(closeButton.QWidget)
|
buttonsHBox.AddWidget(closeButton.QWidget)
|
||||||
|
|
||||||
dialog.Exec()
|
dialog.Exec()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,6 +288,7 @@ func (req *reqVars) updateReq() {
|
|||||||
authors = append(authors, pk)
|
authors = append(authors, pk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(authors) > 0 {
|
if len(authors) > 0 {
|
||||||
req.filter.Authors = authors
|
req.filter.Authors = authors
|
||||||
}
|
}
|
||||||
@@ -360,7 +382,7 @@ func (req *reqVars) subscribe() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(relays) == 0 {
|
if len(relays) == 0 {
|
||||||
statusLabel.SetText("no relays specified")
|
setStatus(tabs.req, "no relays specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +393,7 @@ func (req *reqVars) subscribe() {
|
|||||||
if len(relays) == 1 {
|
if len(relays) == 1 {
|
||||||
relay, err := sys.Pool.EnsureRelay(relays[0])
|
relay, err := sys.Pool.EnsureRelay(relays[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusLabel.SetText(fmt.Sprintf("failed to connect to %s: %s", niceRelayURL(relays[0]), err))
|
setStatus(tabs.req, "failed to connect to %s: %s", niceRelayURL(relays[0]), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,17 +402,17 @@ func (req *reqVars) subscribe() {
|
|||||||
return currentKeyer.SignEvent(ctx, evt)
|
return currentKeyer.SignEvent(ctx, evt)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusLabel.SetText(fmt.Sprintf("failed to auth to %s: %s", niceRelayURL(relay.URL), err))
|
setStatus(tabs.req, "failed to auth to %s: %s", niceRelayURL(relay.URL), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statusLabel.SetText("subscribed to " + niceRelayURL(relay.URL))
|
setStatus(tabs.req, "subscribed to "+niceRelayURL(relay.URL))
|
||||||
sub, err := relay.Subscribe(ctx, req.filter, nostr.SubscriptionOptions{
|
sub, err := relay.Subscribe(ctx, req.filter, nostr.SubscriptionOptions{
|
||||||
Label: "vnak-req-1",
|
Label: "vnak-req-1",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
statusLabel.SetText(fmt.Sprintf("failed to subscribe to %s: %s", niceRelayURL(relay.URL), err))
|
setStatus(tabs.req, "failed to subscribe to %s: %s", niceRelayURL(relay.URL), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,11 +423,11 @@ func (req *reqVars) subscribe() {
|
|||||||
reason := <-sub.ClosedReason
|
reason := <-sub.ClosedReason
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
mainthread.Wait(func() {
|
mainthread.Wait(func() {
|
||||||
statusLabel.SetText(fmt.Sprintf("subscription closed: %s", reason))
|
setStatus(tabs.req, "subscription closed: %s", reason)
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
statusLabel.SetText("subscribed to " + strings.Join(niceRelayURLs(relays), ", "))
|
setStatus(tabs.req, "subscribed to "+strings.Join(niceRelayURLs(relays), ", "))
|
||||||
eoseChan = make(chan struct{})
|
eoseChan = make(chan struct{})
|
||||||
eventsChan = make(chan nostr.Event)
|
eventsChan = make(chan nostr.Event)
|
||||||
|
|
||||||
@@ -435,7 +457,7 @@ func (req *reqVars) subscribe() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
statusLabel.SetText("subscription ended")
|
setStatus(tabs.req, "subscription ended")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -444,7 +466,7 @@ func (req *reqVars) subscribe() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *reqVars) populate(filter nostr.Filter) {
|
func (req *reqVars) populate(filter nostr.Filter, relays []string) {
|
||||||
// clear all authors except the first, set the first to ""
|
// clear all authors except the first, set the first to ""
|
||||||
for _, authorEdit := range req.authorsEdits {
|
for _, authorEdit := range req.authorsEdits {
|
||||||
req.authorsVBox.RemoveWidget(authorEdit.QWidget)
|
req.authorsVBox.RemoveWidget(authorEdit.QWidget)
|
||||||
@@ -539,6 +561,18 @@ func (req *reqVars) populate(filter nostr.Filter) {
|
|||||||
req.limitCheck.SetChecked(false)
|
req.limitCheck.SetChecked(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set relays
|
||||||
|
for _, relayEdit := range req.relaysEdits {
|
||||||
|
req.relaysHBox.RemoveWidget(relayEdit.QWidget)
|
||||||
|
relayEdit.DeleteLater()
|
||||||
|
}
|
||||||
|
req.relaysEdits = req.relaysEdits[:0]
|
||||||
|
for _, relay := range relays {
|
||||||
|
req.addRelayEdit()
|
||||||
|
req.relaysEdits[len(req.relaysEdits)-1].SetText(relay)
|
||||||
|
}
|
||||||
|
req.addRelayEdit() // extra empty
|
||||||
|
|
||||||
req.updateReq()
|
req.updateReq()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
"fiatjaf.com/nostr/eventstore/slicestore"
|
"fiatjaf.com/nostr/eventstore/slicestore"
|
||||||
@@ -108,9 +110,17 @@ func setupServeTab() *qt.QWidget {
|
|||||||
|
|
||||||
// events column
|
// events column
|
||||||
eventsVBox := qt.NewQVBoxLayout2()
|
eventsVBox := qt.NewQVBoxLayout2()
|
||||||
|
eventsHBox := qt.NewQHBoxLayout2()
|
||||||
|
eventsVBox.AddLayout(eventsHBox.QLayout)
|
||||||
|
|
||||||
eventsLabel := qt.NewQLabel2()
|
eventsLabel := qt.NewQLabel2()
|
||||||
eventsLabel.SetText("events:")
|
eventsLabel.SetText("events:")
|
||||||
eventsVBox.AddWidget(eventsLabel.QWidget)
|
eventsHBox.AddWidget(eventsLabel.QWidget)
|
||||||
|
|
||||||
|
loadEventsButton := qt.NewQPushButton5("load events", serve.tab)
|
||||||
|
loadEventsButton.OnClicked(serve.loadEventsFromFile)
|
||||||
|
eventsHBox.AddWidget(loadEventsButton.QWidget)
|
||||||
|
|
||||||
serve.eventsList = qt.NewQListWidget(serve.tab)
|
serve.eventsList = qt.NewQListWidget(serve.tab)
|
||||||
serve.eventsList.SetMinimumHeight(200)
|
serve.eventsList.SetMinimumHeight(200)
|
||||||
eventsVBox.AddWidget(serve.eventsList.QWidget)
|
eventsVBox.AddWidget(serve.eventsList.QWidget)
|
||||||
@@ -362,10 +372,10 @@ func (serve *serveVars) stopRelay() {
|
|||||||
|
|
||||||
func (serve *serveVars) log(format string, args ...interface{}) {
|
func (serve *serveVars) log(format string, args ...interface{}) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
timestamp := time.Now().Format("15:04:05")
|
||||||
mainthread.Start(func() {
|
mainthread.Start(func() {
|
||||||
item := qt.NewQListWidgetItem2(msg)
|
|
||||||
pos := serve.logsList.VerticalScrollBar().SliderPosition()
|
pos := serve.logsList.VerticalScrollBar().SliderPosition()
|
||||||
serve.logsList.InsertItem(0, item)
|
serve.logsList.InsertItem(0, qt.NewQListWidgetItem2(fmt.Sprintf("%s %s", timestamp, msg)))
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
serve.logsList.ScrollToTop()
|
serve.logsList.ScrollToTop()
|
||||||
}
|
}
|
||||||
@@ -433,6 +443,71 @@ func (serve *serveVars) updateBlossomBlobsList() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (serve *serveVars) loadEventsFromFile() {
|
||||||
|
if serve.db == nil {
|
||||||
|
serve.log("relay not started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog := qt.NewQFileDialog(serve.tab)
|
||||||
|
dialog.SetFileMode(qt.QFileDialog__ExistingFile)
|
||||||
|
dialog.SetNameFilter("JSONL Files (*.jsonl);;All Files (*)")
|
||||||
|
|
||||||
|
if dialog.Exec() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePaths := dialog.SelectedFiles()
|
||||||
|
if len(filePaths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filePaths[0]
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
serve.log("failed to open file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
lineNum := 0
|
||||||
|
loaded := 0
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
lineNum++
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var event nostr.Event
|
||||||
|
if err := json.Unmarshal([]byte(line), &event); err != nil {
|
||||||
|
serve.log("failed to parse line %d: %v", lineNum, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if serve.relay != nil {
|
||||||
|
if _, err := serve.relay.AddEvent(context.Background(), event); err != nil {
|
||||||
|
serve.log("failed to save event from line %d: %v", lineNum, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
serve.log("error reading file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serve.log("loaded %d events from %s", loaded, filePath)
|
||||||
|
serve.updateEventsList()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (serve *serveVars) updateEventsList() {
|
func (serve *serveVars) updateEventsList() {
|
||||||
mainthread.Start(func() {
|
mainthread.Start(func() {
|
||||||
serve.eventsList.Clear()
|
serve.eventsList.Clear()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
qt "github.com/mappu/miqt/qt6"
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusLabel *qt.QLabel
|
||||||
|
|
||||||
|
var statuses []string = make([]string, reflect.TypeOf(tabs).NumField())
|
||||||
|
|
||||||
|
func setStatus(fromTab int, msg string, args ...any) {
|
||||||
|
msg = fmt.Sprintf(msg, args...)
|
||||||
|
|
||||||
|
statuses[fromTab] = msg
|
||||||
|
if tabWidget.CurrentIndex() == fromTab {
|
||||||
|
statusLabel.SetText(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeSetStatusOnTab(tab int) {
|
||||||
|
if statusLabel != nil && statuses[tab] != "" {
|
||||||
|
statusLabel.SetText(statuses[tab])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user