parse.mx raw
1 package nostr
2
3 // Minimal JSON parsing for Nostr relay messages.
4 // No encoding/json. Hand-rolled for speed.
5
6 // ParseEvent parses a JSON event object into an Event.
7 func ParseEvent(s string) *Event {
8 ev := &Event{}
9 i := skipWS(s, 0)
10 if i >= len(s) || s[i] != '{' {
11 return nil
12 }
13 i++
14 for i < len(s) {
15 i = skipWS(s, i)
16 if i >= len(s) {
17 return nil
18 }
19 if s[i] == '}' {
20 return ev
21 }
22 if s[i] == ',' {
23 i++
24 continue
25 }
26 // Key.
27 key, ni := parseString(s, i)
28 if ni < 0 {
29 return nil
30 }
31 i = skipWS(s, ni)
32 if i >= len(s) || s[i] != ':' {
33 return nil
34 }
35 i = skipWS(s, i+1)
36
37 switch key {
38 case "id":
39 ev.ID, i = parseString(s, i)
40 if i < 0 {
41 return nil
42 }
43 case "pubkey":
44 ev.PubKey, i = parseString(s, i)
45 if i < 0 {
46 return nil
47 }
48 case "created_at":
49 ev.CreatedAt, i = parseInt(s, i)
50 if i < 0 {
51 return nil
52 }
53 case "kind":
54 var k int64
55 k, i = parseInt(s, i)
56 if i < 0 {
57 return nil
58 }
59 ev.Kind = int(k)
60 case "content":
61 ev.Content, i = parseString(s, i)
62 if i < 0 {
63 return nil
64 }
65 case "sig":
66 ev.Sig, i = parseString(s, i)
67 if i < 0 {
68 return nil
69 }
70 case "tags":
71 ev.Tags, i = parseTags(s, i)
72 if i < 0 {
73 return nil
74 }
75 default:
76 // Skip unknown field value.
77 i = skipValue(s, i)
78 if i < 0 {
79 return nil
80 }
81 }
82 }
83 return ev
84 }
85
86 // ParseRelayMessage parses a relay message array.
87 // Returns (label, subscriptionID, payload) where:
88 // - EVENT: label="EVENT", subID set, payload = event JSON string
89 // - EOSE: label="EOSE", subID set
90 // - OK: label="OK", subID = eventID, payload = "true:<msg>" or "false:<msg>"
91 // - NOTICE: label="NOTICE", payload = message
92 // - AUTH: label="AUTH", payload = challenge
93 func ParseRelayMessage(s string) (label, subID, payload string) {
94 i := skipWS(s, 0)
95 if i >= len(s) || s[i] != '[' {
96 return
97 }
98 i = skipWS(s, i+1)
99
100 // First element: label string.
101 label, i = parseString(s, i)
102 if i < 0 {
103 label = ""
104 return
105 }
106
107 switch label {
108 case "EVENT":
109 i = skipWS(s, i)
110 if i >= len(s) || s[i] != ',' {
111 return
112 }
113 i = skipWS(s, i+1)
114 subID, i = parseString(s, i)
115 if i < 0 {
116 return
117 }
118 i = skipWS(s, i)
119 if i >= len(s) || s[i] != ',' {
120 return
121 }
122 i = skipWS(s, i+1)
123 // Rest until closing ] is the event JSON.
124 start := i
125 i = skipValue(s, i)
126 if i < 0 {
127 return
128 }
129 payload = s[start:i]
130
131 case "EOSE":
132 i = skipWS(s, i)
133 if i >= len(s) || s[i] != ',' {
134 return
135 }
136 i = skipWS(s, i+1)
137 subID, i = parseString(s, i)
138
139 case "OK":
140 i = skipWS(s, i)
141 if i >= len(s) || s[i] != ',' {
142 return
143 }
144 i = skipWS(s, i+1)
145 subID, i = parseString(s, i) // actually eventID
146 if i < 0 {
147 return
148 }
149 i = skipWS(s, i)
150 if i >= len(s) || s[i] != ',' {
151 return
152 }
153 i = skipWS(s, i+1)
154 // Boolean.
155 ok := false
156 if i+4 <= len(s) && s[i:i+4] == "true" {
157 ok = true
158 i += 4
159 } else if i+5 <= len(s) && s[i:i+5] == "false" {
160 i += 5
161 }
162 // Optional message.
163 i = skipWS(s, i)
164 msg := ""
165 if i < len(s) && s[i] == ',' {
166 i = skipWS(s, i+1)
167 msg, i = parseString(s, i)
168 }
169 if ok {
170 payload = "true:" + msg
171 } else {
172 payload = "false:" + msg
173 }
174
175 case "NOTICE":
176 i = skipWS(s, i)
177 if i >= len(s) || s[i] != ',' {
178 return
179 }
180 i = skipWS(s, i+1)
181 payload, i = parseString(s, i)
182
183 case "AUTH":
184 i = skipWS(s, i)
185 if i >= len(s) || s[i] != ',' {
186 return
187 }
188 i = skipWS(s, i+1)
189 payload, i = parseString(s, i)
190 }
191
192 return
193 }
194
195 // ParseFilter parses a JSON filter object into a Filter.
196 func ParseFilter(s string) *Filter {
197 f := &Filter{}
198 i := skipWS(s, 0)
199 if i >= len(s) || s[i] != '{' {
200 return nil
201 }
202 i++
203 for i < len(s) {
204 i = skipWS(s, i)
205 if i >= len(s) {
206 return nil
207 }
208 if s[i] == '}' {
209 return f
210 }
211 if s[i] == ',' {
212 i++
213 continue
214 }
215 key, ni := parseString(s, i)
216 if ni < 0 {
217 return nil
218 }
219 i = skipWS(s, ni)
220 if i >= len(s) || s[i] != ':' {
221 return nil
222 }
223 i = skipWS(s, i+1)
224
225 switch key {
226 case "ids":
227 f.IDs, i = parseStrArray(s, i)
228 case "authors":
229 f.Authors, i = parseStrArray(s, i)
230 case "kinds":
231 f.Kinds, i = parseIntArray(s, i)
232 case "since":
233 f.Since, i = parseInt(s, i)
234 case "until":
235 f.Until, i = parseInt(s, i)
236 case "limit":
237 var l int64
238 l, i = parseInt(s, i)
239 f.Limit = int(l)
240 case "_proxy":
241 f.Proxy, i = parseStrArray(s, i)
242 default:
243 if len(key) == 2 && key[0] == '#' {
244 if f.Tags == nil {
245 f.Tags = map[string][]string{}
246 }
247 f.Tags[key], i = parseStrArray(s, i)
248 } else {
249 i = skipValue(s, i)
250 }
251 }
252 if i < 0 {
253 return nil
254 }
255 }
256 return f
257 }
258
259 // ParseEventsJSON parses a JSON array of event objects.
260 func ParseEventsJSON(s string) []*Event {
261 i := skipWS(s, 0)
262 if i >= len(s) || s[i] != '[' {
263 return nil
264 }
265 i++
266 var events []*Event
267 for {
268 i = skipWS(s, i)
269 if i >= len(s) {
270 return events
271 }
272 if s[i] == ']' {
273 return events
274 }
275 if s[i] == ',' {
276 i++
277 continue
278 }
279 start := i
280 i = skipValue(s, i)
281 if i < 0 {
282 return events
283 }
284 ev := ParseEvent(s[start:i])
285 if ev != nil {
286 events = append(events, ev)
287 }
288 }
289 }
290
291 func parseStrArray(s string, i int) ([]string, int) {
292 i = skipWS(s, i)
293 if i >= len(s) || s[i] != '[' {
294 return nil, -1
295 }
296 i++
297 var out []string
298 for {
299 i = skipWS(s, i)
300 if i >= len(s) {
301 return nil, -1
302 }
303 if s[i] == ']' {
304 return out, i + 1
305 }
306 if s[i] == ',' {
307 i++
308 continue
309 }
310 v, ni := parseString(s, i)
311 if ni < 0 {
312 return nil, -1
313 }
314 out = append(out, v)
315 i = ni
316 }
317 }
318
319 func parseIntArray(s string, i int) ([]int, int) {
320 i = skipWS(s, i)
321 if i >= len(s) || s[i] != '[' {
322 return nil, -1
323 }
324 i++
325 var out []int
326 for {
327 i = skipWS(s, i)
328 if i >= len(s) {
329 return nil, -1
330 }
331 if s[i] == ']' {
332 return out, i + 1
333 }
334 if s[i] == ',' {
335 i++
336 continue
337 }
338 n, ni := parseInt(s, i)
339 if ni < 0 {
340 return nil, -1
341 }
342 out = append(out, int(n))
343 i = ni
344 }
345 }
346
347 // --- Low-level JSON parsing ---
348
349 func skipWS(s string, i int) int {
350 for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r') {
351 i++
352 }
353 return i
354 }
355
356 func parseString(s string, i int) (string, int) {
357 if i >= len(s) || s[i] != '"' {
358 return "", -1
359 }
360 i++
361 start := i
362 // Use string concat, not []byte — tinyjs strings are UTF-16, byte ops corrupt emoji.
363 result := ""
364 for i < len(s) {
365 if s[i] == '\\' {
366 result += s[start:i]
367 i++
368 if i >= len(s) {
369 return "", -1
370 }
371 switch s[i] {
372 case '"', '\\', '/':
373 result += s[i : i+1]
374 case 'n':
375 result += "\n"
376 case 'r':
377 result += "\r"
378 case 't':
379 result += "\t"
380 case 'b':
381 result += "\b"
382 case 'f':
383 result += "\f"
384 case 'u':
385 if i+4 >= len(s) {
386 return "", -1
387 }
388 cp := hexVal(s[i+1])<<12 | hexVal(s[i+2])<<8 | hexVal(s[i+3])<<4 | hexVal(s[i+4])
389 // Surrogate pair: \uD800-\uDBFF followed by \uDC00-\uDFFF.
390 if cp >= 0xD800 && cp <= 0xDBFF && i+10 <= len(s) && s[i+5] == '\\' && s[i+6] == 'u' {
391 lo := hexVal(s[i+7])<<12 | hexVal(s[i+8])<<8 | hexVal(s[i+9])<<4 | hexVal(s[i+10])
392 if lo >= 0xDC00 && lo <= 0xDFFF {
393 cp = 0x10000 + (cp-0xD800)*0x400 + (lo - 0xDC00)
394 i += 6
395 }
396 }
397 result += string(rune(cp))
398 i += 4
399 default:
400 result += s[i : i+1]
401 }
402 i++
403 start = i
404 continue
405 }
406 if s[i] == '"' {
407 result += s[start:i]
408 return result, i + 1
409 }
410 i++
411 }
412 return "", -1
413 }
414
415 func hexVal(c byte) int {
416 if c >= '0' && c <= '9' {
417 return int(c - '0')
418 }
419 if c >= 'a' && c <= 'f' {
420 return int(c-'a') + 10
421 }
422 if c >= 'A' && c <= 'F' {
423 return int(c-'A') + 10
424 }
425 return 0
426 }
427
428 func parseInt(s string, i int) (int64, int) {
429 if i >= len(s) {
430 return 0, -1
431 }
432 neg := false
433 if s[i] == '-' {
434 neg = true
435 i++
436 }
437 if i >= len(s) || s[i] < '0' || s[i] > '9' {
438 return 0, -1
439 }
440 var n int64
441 for i < len(s) && s[i] >= '0' && s[i] <= '9' {
442 n = n*10 + int64(s[i]-'0')
443 i++
444 }
445 if neg {
446 n = -n
447 }
448 return n, i
449 }
450
451 func parseTags(s string, i int) (Tags, int) {
452 if i >= len(s) || s[i] != '[' {
453 return nil, -1
454 }
455 i++
456 var tags Tags
457 for {
458 i = skipWS(s, i)
459 if i >= len(s) {
460 return nil, -1
461 }
462 if s[i] == ']' {
463 return tags, i + 1
464 }
465 if s[i] == ',' {
466 i++
467 continue
468 }
469 // Parse inner array.
470 if s[i] != '[' {
471 return nil, -1
472 }
473 i++
474 var tag Tag
475 for {
476 i = skipWS(s, i)
477 if i >= len(s) {
478 return nil, -1
479 }
480 if s[i] == ']' {
481 i++
482 break
483 }
484 if s[i] == ',' {
485 i++
486 continue
487 }
488 var val string
489 val, i = parseString(s, i)
490 if i < 0 {
491 return nil, -1
492 }
493 tag = append(tag, val)
494 }
495 tags = append(tags, tag)
496 }
497 }
498
499 // skipValue skips a JSON value (string, number, object, array, bool, null).
500 func skipValue(s string, i int) int {
501 if i >= len(s) {
502 return -1
503 }
504 switch s[i] {
505 case '"':
506 _, ni := parseString(s, i)
507 return ni
508 case '{':
509 return skipBracketed(s, i, '{', '}')
510 case '[':
511 return skipBracketed(s, i, '[', ']')
512 case 't': // true
513 if i+4 <= len(s) {
514 return i + 4
515 }
516 return -1
517 case 'f': // false
518 if i+5 <= len(s) {
519 return i + 5
520 }
521 return -1
522 case 'n': // null
523 if i+4 <= len(s) {
524 return i + 4
525 }
526 return -1
527 default:
528 // Number.
529 for i < len(s) && s[i] != ',' && s[i] != '}' && s[i] != ']' && s[i] != ' ' && s[i] != '\n' {
530 i++
531 }
532 return i
533 }
534 }
535
536 func skipBracketed(s string, i int, open, close byte) int {
537 if i >= len(s) || s[i] != open {
538 return -1
539 }
540 depth := 1
541 i++
542 inStr := false
543 for i < len(s) && depth > 0 {
544 if inStr {
545 if s[i] == '\\' {
546 i++
547 } else if s[i] == '"' {
548 inStr = false
549 }
550 } else {
551 if s[i] == '"' {
552 inStr = true
553 } else if s[i] == open {
554 depth++
555 } else if s[i] == close {
556 depth--
557 }
558 }
559 i++
560 }
561 if depth != 0 {
562 return -1
563 }
564 return i
565 }
566