summaryrefslogtreecommitdiffstats
path: root/testclient
diff options
context:
space:
mode:
Diffstat (limited to 'testclient')
-rw-r--r--testclient/main.go195
1 files changed, 195 insertions, 0 deletions
diff --git a/testclient/main.go b/testclient/main.go
new file mode 100644
index 0000000..92726bb
--- /dev/null
+++ b/testclient/main.go
@@ -0,0 +1,195 @@
1package main
2
3import (
4 "context"
5 "encoding/hex"
6 "flag"
7 "fmt"
8 "log"
9 "os"
10 "time"
11
12 "fiatjaf.com/nostr"
13 "fiatjaf.com/nostr/nip19"
14)
15
16const keyFile = "testclient.key"
17
18func main() {
19 relayURL := flag.String("relay", "ws://localhost:8080", "Relay WebSocket URL")
20 flag.Parse()
21
22 // Load or generate keypair
23 sk, err := loadOrGenerateKey()
24 if err != nil {
25 log.Fatalf("Failed to load/generate key: %v", err)
26 }
27
28 pubkey := nostr.GetPublicKey(sk)
29 npub := nip19.EncodeNpub(pubkey)
30 log.Printf("Pubkey (hex): %s", hex.EncodeToString(pubkey[:]))
31 log.Printf("Pubkey (npub): %s", npub)
32
33 // Connect to relay with NIP-42 auth handler
34 log.Printf("Connecting to %s...", *relayURL)
35
36 // Channel to signal when AUTH completes
37 authCompleted := make(chan error, 1)
38
39 opts := nostr.RelayOptions{
40 // NIP-42 AUTH handler - signs auth challenges automatically
41 AuthHandler: func(ctx context.Context, r *nostr.Relay, authEvent *nostr.Event) error {
42 log.Printf("AUTH challenge received! Signing auth event...")
43 if err := authEvent.Sign(sk); err != nil {
44 authErr := fmt.Errorf("failed to sign auth event: %w", err)
45 authCompleted <- authErr
46 return authErr
47 }
48 log.Printf("AUTH event signed and sent")
49 authCompleted <- nil
50 return nil
51 },
52 // Notice handler - logs relay notices
53 NoticeHandler: func(r *nostr.Relay, notice string) {
54 log.Printf("NOTICE from relay: %s", notice)
55 },
56 }
57
58 relay, err := nostr.RelayConnect(context.Background(), *relayURL, opts)
59 if err != nil {
60 log.Fatalf("Failed to connect: %v", err)
61 }
62 defer relay.Close()
63
64 log.Printf("Connected! (NIP-42 auth enabled)")
65
66 // Try to publish an event (will trigger AUTH if required)
67 log.Printf("Publishing test event...")
68 event := nostr.Event{
69 PubKey: pubkey,
70 CreatedAt: nostr.Now(),
71 Kind: nostr.KindTextNote,
72 Tags: nostr.Tags{},
73 Content: fmt.Sprintf("NIP-42 test from testclient at %s", time.Now().Format(time.RFC3339)),
74 }
75
76 event.Sign(sk)
77
78 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
79 defer cancel()
80
81 // Try to publish - this may trigger AUTH if relay requires it
82 err = relay.Publish(ctx, event)
83 if err != nil {
84 // Publish failed - likely due to AUTH requirement
85 log.Printf("First publish attempt failed: %v", err)
86
87 // Wait for AUTH to complete
88 log.Printf("Waiting for AUTH to complete...")
89 select {
90 case authErr := <-authCompleted:
91 if authErr != nil {
92 log.Fatalf("AUTH failed: %v", authErr)
93 }
94 log.Printf("AUTH completed successfully")
95 case <-ctx.Done():
96 log.Fatalf("Timeout waiting for AUTH")
97 }
98
99 // Retry publish now that we're authenticated
100 log.Printf("Retrying publish after successful auth...")
101 err = relay.Publish(ctx, event)
102 if err != nil {
103 log.Printf("Publish error after retry: %v", err)
104 } else {
105 log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:]))
106 }
107 } else {
108 // Publish succeeded immediately - no AUTH required
109 log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:]))
110 }
111
112 // Query events
113 log.Printf("Querying recent events (kind 1, limit 10)...")
114 filters := nostr.Filter{
115 Kinds: []nostr.Kind{nostr.KindTextNote},
116 Limit: 10,
117 }
118
119 queryCtx, queryCancel := context.WithTimeout(context.Background(), 5*time.Second)
120 defer queryCancel()
121
122 sub, err := relay.Subscribe(queryCtx, filters, nostr.SubscriptionOptions{})
123 if err != nil {
124 log.Printf("Subscribe error: %v", err)
125 return
126 }
127
128 count := 0
129 timeout := time.After(3 * time.Second)
130
131 log.Printf("Listening for events...")
132 for {
133 select {
134 case evt, ok := <-sub.Events:
135 if !ok {
136 goto done
137 }
138 count++
139 log.Printf(" Event %d: kind=%d author=%s content=%q",
140 count, evt.Kind, hex.EncodeToString(evt.PubKey[:])[:16], truncate(evt.Content, 50))
141 if count >= 10 {
142 goto done
143 }
144 case <-sub.EndOfStoredEvents:
145 log.Printf("End of stored events (EOSE)")
146 goto done
147 case <-timeout:
148 log.Printf("Query timeout")
149 goto done
150 case <-queryCtx.Done():
151 goto done
152 }
153 }
154
155done:
156 log.Printf("Retrieved %d events total", count)
157 sub.Close()
158
159 log.Printf("Test complete!")
160}
161
162// loadOrGenerateKey loads an existing key or generates a new one
163func loadOrGenerateKey() (nostr.SecretKey, error) {
164 // Try to load existing key
165 data, err := os.ReadFile(keyFile)
166 if err == nil {
167 skHex := string(data)
168 skBytes, err := hex.DecodeString(skHex)
169 if err == nil && len(skBytes) == 32 {
170 var sk nostr.SecretKey
171 copy(sk[:], skBytes)
172 log.Printf("Loaded existing key from %s", keyFile)
173 return sk, nil
174 }
175 }
176
177 // Generate new key
178 sk := nostr.Generate()
179 skHex := hex.EncodeToString(sk[:])
180
181 if err := os.WriteFile(keyFile, []byte(skHex), 0o600); err != nil {
182 return sk, fmt.Errorf("failed to save key: %w", err)
183 }
184
185 log.Printf("Generated new key and saved to %s", keyFile)
186 return sk, nil
187}
188
189// truncate truncates a string to maxLen characters
190func truncate(s string, maxLen int) string {
191 if len(s) <= maxLen {
192 return s
193 }
194 return s[:maxLen] + "..."
195}