spasm.js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1103 @@
1
+ import {
2
+ EventBaseProtocol, EventPrivateKey, UnknownPostOrEvent,
3
+ UnknownEvent, DmpEvent, DmpEventSignedClosed, NostrEvent,
4
+ NostrSpasmEvent, NostrSpasmEventSignedOpened, Post,
5
+ NostrSpasmTag, AnyTag, StandardizedEvent, SpasmEvent
6
+ } from "./interfaces";
7
+ import { isObjectWithValues, convertHexToBech32 } from "./utils";
8
+
9
+ // Post-or-Event
10
+ // │
11
+ // │ isWeb2Post()
12
+ // ├── Web2Post
13
+ // │ ├── no signature
14
+ // │ └── no signed_message
15
+ // │
16
+ // │ isWeb3Post()
17
+ // ├── Web3Post
18
+ // │ ├── signature*
19
+ // │ └── signed_message*
20
+ // │ │
21
+ // │ │ identifyEvent()
22
+ // │ └── Event (see below)
23
+ // │
24
+ // │ identifyEvent()
25
+ // └── Event
26
+ // │
27
+ // │ isDmpEvent()
28
+ // ├── DMP event
29
+ // │ ├── version*
30
+ // │ ├── action*
31
+ // │ └── license*
32
+ // │
33
+ // │ isDmpEventSignedClosed()
34
+ // ├── DMP event closed
35
+ // │ ├── signature*
36
+ // │ └── signedString*
37
+ // │ └── DMP event*
38
+ // │ ├── version*
39
+ // │ ├── action*
40
+ // │ └── license*
41
+ // │
42
+ // │ isDmpEventSignedOpened()
43
+ // ├── DMP event opened
44
+ // │ ├── signature*
45
+ // │ ├── signedString*
46
+ // │ └── signedObject*
47
+ // │ └── DMP event*
48
+ // │ ├── version*
49
+ // │ ├── action*
50
+ // │ └── license*
51
+ // │
52
+ // │ isNostrEvent()
53
+ // ├── Nostr event
54
+ // │ ├── id*
55
+ // │ ├── content*
56
+ // │ ├── created_at*
57
+ // │ ├── kind*
58
+ // │ └── pubkey*
59
+ // │
60
+ // │ isNostrEventSignedOpened()
61
+ // ├── Nostr event signed
62
+ // │ ├── sig*
63
+ // │ ├── id*
64
+ // │ ├── content*
65
+ // │ ├── created_at*
66
+ // │ ├── kind*
67
+ // │ └── pubkey*
68
+ // │
69
+ // │ isNostrSpasmEvent()
70
+ // ├── Nostr SPASM event
71
+ // │ ├── id*
72
+ // │ ├── content*
73
+ // │ ├── created_at*
74
+ // │ ├── kind*
75
+ // │ ├── pubkey*
76
+ // │ └── tags*
77
+ // │ ├── spasm_version*
78
+ // │ ├── spasm_target?
79
+ // │ ├── spasm_action?
80
+ // │ └── spasm_title?
81
+ // │
82
+ // │ isNostrSpasmEventSignedOpened()
83
+ // └── Nostr SPASM event signed
84
+ // ├── sig*
85
+ // ├── id*
86
+ // ├── content*
87
+ // ├── created_at*
88
+ // ├── kind*
89
+ // ├── pubkey*
90
+ // └── tags*
91
+ // ├── spasm_version*
92
+ // ├── spasm_target?
93
+ // ├── spasm_action?
94
+ // └── spasm_title?
95
+ //
96
+
97
+ type EventType =
98
+ "DmpEvent" |
99
+ "DmpEventSignedClosed" |
100
+ "DmpEventSignedOpened" |
101
+ "NostrEvent" |
102
+ "NostrEventSignedOpened" |
103
+ "NostrSpasmEvent" |
104
+ "NostrSpasmEventSignedOpened" |
105
+ "SpasmEvent" |
106
+ "unknown"
107
+
108
+ type EventInfoType = EventType | "Post"
109
+
110
+ interface EventInfo {
111
+ type: EventInfoType | false
112
+ hasSignature: boolean
113
+ baseProtocol: EventBaseProtocol | false
114
+ privateKey: EventPrivateKey | false
115
+ isSpasmCompatible: boolean
116
+ hasExtraSpasmFields: boolean
117
+ license: string | false
118
+ // originalEvent: UnknownEvent
119
+ }
120
+
121
+ type WebType = "web2" | "web3"
122
+
123
+ type EventIsSealedUnderKeyName = "signed_message" | "signedObject"
124
+
125
+ // interface Web3Post extends Post {
126
+ // signed_message: string
127
+ // }
128
+
129
+ interface KnownPostOrEventInfo {
130
+ webType: WebType | false
131
+ eventIsSealed: boolean
132
+ eventIsSealedUnderKeyName: EventIsSealedUnderKeyName | false
133
+ eventInfo: EventInfo | false
134
+ }
135
+
136
+ // web2 post example
137
+ // webType: "web2",
138
+ // eventIsSealed: false,
139
+ // eventIsSealedUnderKeyName: false,
140
+ // eventInfo: false
141
+
142
+ // web3 post example
143
+ // webType: "web3",
144
+ // eventIsSealed: true,
145
+ // eventIsSealedUnderKeyName: "signed_message",
146
+ // eventInfo: {
147
+ // type: "DmpEventSignedClosed",
148
+ // hasSignature: true
149
+ // baseProtocol: "dmp",
150
+ // privateKey: "ethereum",
151
+ // isSpasmCompatible: true,
152
+ // hasExtraSpasmFields: false,
153
+ // }
154
+
155
+ // web3 event example
156
+ // webType: "web3",
157
+ // eventIsSealed: false,
158
+ // eventIsSealedUnderKeyName: false,
159
+ // eventInfo: {
160
+ // type: "NostrSpasmEventSignedOpened",
161
+ // hasSignature: true
162
+ // baseProtocol: "nostr",
163
+ // privateKey: "nostr",
164
+ // isSpasmCompatible: true,
165
+ // hasExtraSpasmFields: true,
166
+ // }
167
+
168
+ /**
169
+ There are usually 3 types of objects passed to this function:
170
+ - web3 post - an object is a post with a web3 event sealed inside
171
+ - web3 event - an object itself is a web3 event
172
+ - web2 post - an object is a post without a web3 event (e.g. RSS)
173
+ */
174
+ export const identifyPostOrEvent = (
175
+ unknownPostOrEvent: UnknownPostOrEvent
176
+ ): KnownPostOrEventInfo => {
177
+
178
+ const info: KnownPostOrEventInfo = {
179
+ webType: false,
180
+ eventIsSealed: false,
181
+ eventIsSealedUnderKeyName: false,
182
+ eventInfo: {
183
+ type: false,
184
+ hasSignature: false,
185
+ baseProtocol: false,
186
+ privateKey: false,
187
+ isSpasmCompatible: false,
188
+ hasExtraSpasmFields: false,
189
+ license: false
190
+ }
191
+ }
192
+
193
+ if (!isObjectWithValues(unknownPostOrEvent)) return info
194
+
195
+ let unknownEventOrWeb2Post: UnknownPostOrEvent
196
+
197
+ // Option 1.
198
+ // If an object is a post with a sealed event (signed string),
199
+ // we need to extract the event by parsing the signed string.
200
+ // We then check if that extracted object is a valid web3 event.
201
+ if (isWeb3Post(unknownPostOrEvent)) {
202
+ info.webType = "web3"
203
+ info.eventIsSealed = true
204
+
205
+ if (
206
+ 'signed_message' in unknownPostOrEvent &&
207
+ typeof(unknownPostOrEvent.signed_message) === "string"
208
+ ) {
209
+ const signedObject = JSON.parse(unknownPostOrEvent.signed_message)
210
+ info.eventIsSealedUnderKeyName = 'signed_message'
211
+ unknownEventOrWeb2Post = signedObject
212
+
213
+ // Edge cases.
214
+ // Signed DMP event sealed in the Post under the key
215
+ // 'signed_message' doesn't have signature inside the signed
216
+ // string, so we cannot just parse the string to extract the
217
+ // object and then pass it into an identify function.
218
+ // Instead, we have to attach a signer and signature to the
219
+ // signed string, which is a type of DmpEventSignedClosed.
220
+ if (
221
+ isDmpEvent(unknownEventOrWeb2Post) &&
222
+ 'signer' in unknownPostOrEvent &&
223
+ typeof(unknownPostOrEvent.signer) === "string" &&
224
+ 'signature' in unknownPostOrEvent &&
225
+ typeof(unknownPostOrEvent.signature) === "string"
226
+ ) {
227
+ // Recreating DmpEventSignedClosed
228
+ unknownEventOrWeb2Post = {
229
+ signer: unknownPostOrEvent.signer,
230
+ signedString: unknownPostOrEvent.signed_message,
231
+ signature: unknownPostOrEvent.signature
232
+ }
233
+ }
234
+ }
235
+
236
+ // Option 2.
237
+ // If an object doesn't have a sealed event (signed string), then
238
+ // - either the object itself is a web3 event,
239
+ // - or the object is a web2 post (e.g., from an RSS feed).
240
+ } else {
241
+ info.webType = false
242
+ info.eventIsSealed = false
243
+ info.eventIsSealedUnderKeyName = false
244
+ unknownEventOrWeb2Post = unknownPostOrEvent
245
+ }
246
+
247
+ const eventInfo: EventInfo = identifyEvent(unknownEventOrWeb2Post)
248
+
249
+ // An object has been identified as a web3 event.
250
+ if (eventInfo?.type && typeof(eventInfo.type) === "string") {
251
+ // web3 post and web3 event
252
+ info.webType = "web3"
253
+ info.eventInfo = eventInfo
254
+ return info
255
+
256
+ // An object has not been identified as a web3 event.
257
+ } else {
258
+ // web2 post
259
+ info.webType = "web2"
260
+ info.eventInfo = false
261
+ return info
262
+ }
263
+ }
264
+
265
+ export const isWeb2Post = (
266
+ unknownPostOrEvent: UnknownPostOrEvent
267
+ ): boolean => {
268
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
269
+
270
+ if (
271
+ // signatures
272
+ 'sig' in unknownPostOrEvent ||
273
+ 'signature' in unknownPostOrEvent ||
274
+ 'signed_message' in unknownPostOrEvent ||
275
+ 'signedObject' in unknownPostOrEvent ||
276
+ 'signedString' in unknownPostOrEvent
277
+ ) {
278
+ return false
279
+ }
280
+
281
+ if (isNostrEvent(unknownPostOrEvent)) return false
282
+
283
+ if (isDmpEvent(unknownPostOrEvent)) return false
284
+
285
+ if (isDmpEventSignedClosed(unknownPostOrEvent)) return false
286
+
287
+ if (isDmpEventSignedOpened(unknownPostOrEvent)) return false
288
+
289
+ return false
290
+ }
291
+
292
+ export const isWeb3Post = (
293
+ unknownPostOrEvent: UnknownPostOrEvent
294
+ ): boolean => {
295
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
296
+
297
+ if (
298
+ 'signed_message' in unknownPostOrEvent &&
299
+ typeof(unknownPostOrEvent.signed_message) === "string"
300
+ ) {
301
+ return true
302
+ }
303
+ return false
304
+ }
305
+
306
+ export const identifyEvent = (
307
+ unknownPostOrEvent: UnknownPostOrEvent
308
+ ): EventInfo => {
309
+ console.log("identifyEvent called")
310
+
311
+ const eventInfo: EventInfo = {
312
+ type: "unknown",
313
+ hasSignature: false,
314
+ baseProtocol: false,
315
+ privateKey: false,
316
+ isSpasmCompatible: false,
317
+ hasExtraSpasmFields: false,
318
+ license: false
319
+ }
320
+
321
+ if (!isObjectWithValues(unknownPostOrEvent)) return eventInfo
322
+
323
+ eventInfo.license = identifyLicense(unknownPostOrEvent)
324
+
325
+ // TODO: refactor
326
+ // verifySignature()
327
+ // add key 'isSignatureValid' to EventInfo
328
+ // check privateKey after discovering eventInfo.type
329
+
330
+ if (hasSignature(unknownPostOrEvent)) eventInfo.hasSignature = true
331
+
332
+ // Another approach is to set a private key after identifying
333
+ // the event type (eventInfo.type).
334
+ if (eventInfo.hasSignature) {
335
+ eventInfo.privateKey = identifyPrivateKey(unknownPostOrEvent)
336
+ }
337
+
338
+ if (hasExtraSpasmFields(unknownPostOrEvent)) eventInfo.hasExtraSpasmFields = true
339
+
340
+ // DMP
341
+ // Might be DMP or Nostr or web2 Post
342
+ if (!eventInfo.hasSignature && !eventInfo.hasExtraSpasmFields) {
343
+ if (isDmpEvent(unknownPostOrEvent)) {
344
+ eventInfo.type = "DmpEvent"
345
+ eventInfo.baseProtocol = "dmp"
346
+ eventInfo.isSpasmCompatible = true
347
+ return eventInfo
348
+ }
349
+
350
+ // Might be DMP or Nostr with signature or Post with signature
351
+ } else if (eventInfo.hasSignature && !eventInfo.hasExtraSpasmFields) {
352
+ if (isDmpEventSignedOpened(unknownPostOrEvent)) {
353
+ eventInfo.type = "DmpEventSignedOpened"
354
+ eventInfo.baseProtocol = "dmp"
355
+ eventInfo.isSpasmCompatible = true
356
+ return eventInfo
357
+ } else if (isDmpEventSignedClosed(unknownPostOrEvent)) {
358
+ eventInfo.type = "DmpEventSignedClosed"
359
+ eventInfo.baseProtocol = "dmp"
360
+ eventInfo.isSpasmCompatible = true
361
+ return eventInfo
362
+ }
363
+ }
364
+
365
+ // Nostr
366
+ if (eventInfo.hasSignature && eventInfo.hasExtraSpasmFields) {
367
+ // Looks like Nostr event with signature and SPASM fields
368
+ if (isNostrSpasmEventSignedOpened(unknownPostOrEvent)) {
369
+ eventInfo.type = "NostrSpasmEventSignedOpened"
370
+ eventInfo.baseProtocol = "nostr"
371
+ eventInfo.isSpasmCompatible = true
372
+ return eventInfo
373
+ }
374
+
375
+ } else if (eventInfo.hasSignature && !eventInfo.hasExtraSpasmFields ) {
376
+ // Looks like Nostr event with signature without SPASM fields
377
+ if (isNostrEventSignedOpened(unknownPostOrEvent)) {
378
+ eventInfo.type = "NostrEventSignedOpened"
379
+ eventInfo.baseProtocol = "nostr"
380
+ eventInfo.isSpasmCompatible = false
381
+ return eventInfo
382
+ }
383
+
384
+ } else if (!eventInfo.hasSignature && eventInfo.hasExtraSpasmFields) {
385
+ // Looks like Nostr event without signature, but with SPASM fields
386
+ if (isNostrSpasmEvent(unknownPostOrEvent)) {
387
+ eventInfo.type = "NostrSpasmEvent"
388
+ eventInfo.baseProtocol = "nostr"
389
+ eventInfo.isSpasmCompatible = true
390
+ return eventInfo
391
+ }
392
+
393
+ } else if (!eventInfo.hasSignature && !eventInfo.hasExtraSpasmFields) {
394
+ // Looks like Nostr event without signature and without SPASM fields
395
+ if (isNostrEvent(unknownPostOrEvent)) {
396
+ eventInfo.type = "NostrEvent"
397
+ eventInfo.baseProtocol = "nostr"
398
+ eventInfo.isSpasmCompatible = false
399
+ return eventInfo
400
+ }
401
+ }
402
+ }
403
+
404
+ // DMP event signature key is 'signature'.
405
+ // Nostr event signature key is 'sig'.
406
+ // Post signature key is 'signature'.
407
+ // Post signature can be from Nostr or DMP events.
408
+ type SignatureKey = 'signature' | 'sig'
409
+
410
+ export const hasSignature = (
411
+ unknownPostOrEvent: UnknownPostOrEvent,
412
+ signatureKey?: SignatureKey,
413
+ signatureLength: number = 40
414
+ ): boolean => {
415
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
416
+
417
+ let keys: SignatureKey[] = signatureKey
418
+
419
+ ? [signatureKey] // signature key is provided
420
+ : ['signature', 'sig']; // check all signature keys
421
+
422
+ for (let key of keys) {
423
+ if (key in unknownPostOrEvent &&
424
+ typeof(unknownPostOrEvent[key]) === 'string' &&
425
+ unknownPostOrEvent[key].length > signatureLength) {
426
+ return true;
427
+ }
428
+ }
429
+
430
+ return false
431
+ }
432
+
433
+ export const identifyLicense = (
434
+ unknownPostOrEvent: UnknownPostOrEvent
435
+ ): string | false => {
436
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
437
+
438
+ let license: string | false = false
439
+
440
+ // Option 1.
441
+ // A license can be inside a 'license' key
442
+ // or inside tags (e.g. SPASM tags in Nostr events).
443
+ if (
444
+ 'license' in unknownPostOrEvent &&
445
+ typeof(unknownPostOrEvent['license']) === 'string' &&
446
+ unknownPostOrEvent['license'].length > 0
447
+ ) {
448
+ license = unknownPostOrEvent['license'];
449
+ if (license) return license
450
+ }
451
+
452
+ license = identifyLicenseInsideTags(unknownPostOrEvent)
453
+ if (license) return license
454
+
455
+ // Option 2.
456
+ // If no license was found, then we should try to extract
457
+ // an object (event) from a signed string if such a string
458
+ // exists, and then check that object for a license.
459
+ const signedObject = extractSealedEvent(unknownPostOrEvent)
460
+
461
+ if (!signedObject) return false
462
+
463
+ if (!isObjectWithValues(signedObject)) return false
464
+
465
+ if (
466
+ 'license' in signedObject &&
467
+ typeof(signedObject['license']) === 'string' &&
468
+ signedObject['license'].length > 0
469
+ ) {
470
+ license = signedObject['license'];
471
+ if (license) return license
472
+ }
473
+
474
+ license = identifyLicenseInsideTags(signedObject)
475
+ if (license) return license
476
+
477
+ return false
478
+ }
479
+
480
+ export const identifyLicenseInsideTags = (
481
+ unknownPostOrEvent: UnknownPostOrEvent
482
+ ): string | false => {
483
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
484
+
485
+ let license: string | false = false
486
+
487
+ // A license can be placed inside SPASM tags
488
+ if ('tags' in unknownPostOrEvent &&
489
+ Array.isArray(unknownPostOrEvent.tags)) {
490
+
491
+ // Nostr events have tags of array type: NostrSpasmTag | AnyTag
492
+ // Post events have tags of string type: string
493
+ unknownPostOrEvent.tags.forEach(function (tag: NostrSpasmTag | AnyTag | string) {
494
+ if (Array.isArray(tag)) {
495
+ if (
496
+ tag[0] === "license" &&
497
+ typeof(tag[1]) === "string"
498
+ ) {
499
+ license = tag[1]
500
+ }
501
+ }
502
+ })
503
+ }
504
+
505
+ return license
506
+ }
507
+
508
+ export const extractSealedEvent = (
509
+ unknownPostOrEvent: UnknownPostOrEvent
510
+ ): UnknownEvent | false => {
511
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
512
+
513
+ let signedObject: UnknownEvent | false = false
514
+
515
+ if (
516
+ 'signed_message' in unknownPostOrEvent &&
517
+ typeof(unknownPostOrEvent['signed_message'] === "string")
518
+ ) {
519
+ signedObject = JSON.parse(unknownPostOrEvent['signed_message'])
520
+
521
+ } else if (
522
+ 'signedString' in unknownPostOrEvent &&
523
+ typeof(unknownPostOrEvent['signedString'] === "string")
524
+ ) {
525
+ signedObject = JSON.parse(unknownPostOrEvent['signedString'])
526
+ }
527
+
528
+ return signedObject
529
+ }
530
+
531
+ type PrivateKeyType = "ethereum" | "nostr"
532
+
533
+ export const identifyPrivateKey = (
534
+ unknownPostOrEvent: UnknownPostOrEvent
535
+ ): PrivateKeyType | false => {
536
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
537
+
538
+ /**
539
+ * If an object has 'sig' key, then it's most likely a Nostr event.
540
+ * Currently, all Nostr events can only be signed with a Nostr private
541
+ * key, so we can assume that a private key is "nostr".
542
+ * In the future there might be an option to sign Nostr events with
543
+ * different keys, so we will have to update the logic.
544
+ */
545
+ if ('sig' in unknownPostOrEvent &&
546
+ typeof(unknownPostOrEvent['sig']) === 'string' &&
547
+ unknownPostOrEvent['sig'].length > 40) {
548
+ return 'nostr';
549
+ }
550
+
551
+ /**
552
+ * A post with a sealed event (hidden inside the signed string)
553
+ * under the key like 'signed_message' or 'signedObject' will
554
+ * usually have a signature inside a 'signature' key.
555
+ *
556
+ * So if an object has a 'signature' key, then it can be:
557
+ * - a DMP event signed with the Ethereum private key,
558
+ * - has 'signature'
559
+ * - a Post with a sealed DMP event signed with an Ethereum private key,
560
+ * - has 'signature'
561
+ * - a Post with a sealed Nostr event signed with a Nostr private key.
562
+ * - has 'signature'
563
+ * - and also has 'sig' inside 'signed_message'
564
+ *
565
+ * In other words, a Post with a sealed Nostr event will have
566
+ * the same signature recorded twice in different places:
567
+ * 1. Inside a 'signature' key in the Post (envelope).
568
+ * 2. Inside a 'sig' key in the object extracted from a signed string.
569
+ * While, a Post with a sealed DMP event will only have the signature
570
+ * specified once inside the 'signature' key.
571
+ *
572
+ * Thus, we have to convert the signed string into an object,
573
+ * then check whether it has a 'sig' key (Nostr event),
574
+ * otherwise assume that it was signed with an Ethereum private key.
575
+ */
576
+ if ('signature' in unknownPostOrEvent &&
577
+ typeof(unknownPostOrEvent?.['signature']) === 'string' &&
578
+ unknownPostOrEvent?.['signature'].length > 40) {
579
+
580
+ if ('signed_message' in unknownPostOrEvent &&
581
+ typeof(unknownPostOrEvent?.['signed_message']) === 'string') {
582
+
583
+ const signedObject = JSON.parse(unknownPostOrEvent?.['signed_message'])
584
+
585
+ if (!isObjectWithValues(signedObject)) return false
586
+
587
+ if ('sig' in unknownPostOrEvent &&
588
+ typeof(unknownPostOrEvent['sig']) === 'string' &&
589
+ unknownPostOrEvent['sig'].length > 40) {
590
+
591
+ return 'nostr';
592
+
593
+ } else {
594
+ return 'ethereum'
595
+ }
596
+ }
597
+
598
+ if ('signedString' in unknownPostOrEvent &&
599
+ typeof(unknownPostOrEvent?.['signedString']) === 'string') {
600
+
601
+ const signedObject = JSON.parse(unknownPostOrEvent?.['signedString'])
602
+
603
+ if (!isObjectWithValues(signedObject)) return false
604
+
605
+ if (isDmpEvent(signedObject)) return 'ethereum'
606
+ }
607
+ }
608
+ }
609
+
610
+
611
+ export const hasExtraSpasmFields = (
612
+ unknownPostOrEvent: UnknownPostOrEvent
613
+ ): boolean => {
614
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
615
+
616
+ if ('tags' in unknownPostOrEvent &&
617
+ Array.isArray(unknownPostOrEvent.tags)) {
618
+
619
+ // spasm_version is optional
620
+ let hasSpasmVersion: boolean = false
621
+ // spasm_target is optional (e.g. new posts have no target)
622
+ let hasSpasmTarget: boolean = false
623
+ // spasm_action is optional for Nostr (can be taken from 'kind' key)
624
+ let hasSpasmAction: boolean = false
625
+ // spasm_title is optional (e.g. comments/reactions have no title)
626
+ let hasSpasmTitle: boolean = false
627
+
628
+ // Nostr events have tags of array type: NostrSpasmTag | AnyTag
629
+ // Post events have tags of string type: string
630
+ unknownPostOrEvent.tags.forEach(function (tag: NostrSpasmTag | AnyTag | string) {
631
+ if (Array.isArray(tag)) {
632
+ if (tag[0] === "spasm_version") {
633
+ hasSpasmVersion = true
634
+ }
635
+
636
+ if (tag[0] === "spasm_target") {
637
+ hasSpasmTarget = true
638
+ }
639
+
640
+ if (tag[0] === "spasm_action") {
641
+ hasSpasmAction = true
642
+ }
643
+
644
+ if (tag[0] === "spasm_title") {
645
+ hasSpasmTitle = true
646
+ }
647
+ }
648
+ })
649
+
650
+ if (hasSpasmVersion || hasSpasmTarget || hasSpasmAction || hasSpasmTitle) {
651
+ return true
652
+ }
653
+ }
654
+ return false
655
+ }
656
+
657
+ export const isNostrEvent = (
658
+ unknownPostOrEvent: UnknownPostOrEvent
659
+ ): boolean => {
660
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
661
+
662
+ // Unsigned Nostr event can be without 'id'
663
+ // if (!('id' in unknownPostOrEvent)) return false
664
+
665
+ // content
666
+ if (!('content' in unknownPostOrEvent)) return false
667
+ if (!unknownPostOrEvent.content) return false
668
+ if (typeof(unknownPostOrEvent.content) !== "string") return false
669
+
670
+ // created_at
671
+ if (!('created_at' in unknownPostOrEvent)) return false
672
+ // 0 is a valid created_at
673
+ if (
674
+ !unknownPostOrEvent.created_at &&
675
+ unknownPostOrEvent.created_at !== 0
676
+ ) return false
677
+ if (typeof(unknownPostOrEvent.created_at) !== "number") return false
678
+
679
+ // kind
680
+ if (!('kind' in unknownPostOrEvent)) return false
681
+ // 0 is a valid kind
682
+ if (
683
+ !unknownPostOrEvent.kind &&
684
+ unknownPostOrEvent.kind !== 0
685
+ ) return false
686
+ if (typeof(unknownPostOrEvent.kind) !== "number") return false
687
+
688
+ // pubkey
689
+ if (!('pubkey' in unknownPostOrEvent)) return false
690
+ if (!unknownPostOrEvent.pubkey) return false
691
+ if (typeof(unknownPostOrEvent.pubkey) !== "string") return false
692
+
693
+ // tags
694
+ // TODO: check if tags is a mandatory field
695
+ // if (!('tags' in unknownPostOrEvent)) return false
696
+
697
+ // sig
698
+ // Unsigned Nostr event can be without 'sig'
699
+ // if (!('sig' in unknownPostOrEvent)) return false
700
+ return true
701
+ }
702
+
703
+ export const isNostrEventSignedOpened = (
704
+ unknownPostOrEvent: UnknownPostOrEvent
705
+ ): boolean => {
706
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
707
+
708
+ if (!isNostrEvent(unknownPostOrEvent)) return false
709
+
710
+ // Signed Nostr event must have 'id'
711
+ if (!('id' in unknownPostOrEvent)) return false
712
+
713
+ if (!hasSignature(unknownPostOrEvent as NostrEvent, 'sig')) return false
714
+
715
+ return true
716
+ }
717
+
718
+ export const isNostrSpasmEvent = (
719
+ unknownPostOrEvent: UnknownPostOrEvent
720
+ ): boolean => {
721
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
722
+
723
+ if (!isNostrEvent(unknownPostOrEvent)) return false
724
+
725
+ if (!hasExtraSpasmFields(unknownPostOrEvent as NostrEvent)) return false
726
+
727
+ return true
728
+ }
729
+
730
+ export const isNostrSpasmEventSignedOpened = (
731
+ unknownPostOrEvent: UnknownPostOrEvent
732
+ ): boolean => {
733
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
734
+
735
+ if (!isNostrSpasmEvent(unknownPostOrEvent)) return false
736
+
737
+ if (!isNostrEventSignedOpened(unknownPostOrEvent as NostrSpasmEvent)) return false
738
+
739
+ return true
740
+ }
741
+
742
+ export const isDmpEvent = (
743
+ unknownPostOrEvent: UnknownPostOrEvent
744
+ ): boolean => {
745
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
746
+
747
+ // TODO: think what if unknownPostOrEvent is Post with version, action, license
748
+ if (!('version' in unknownPostOrEvent)) return false
749
+ if (!('action' in unknownPostOrEvent)) return false
750
+ if (!('license' in unknownPostOrEvent)) return false
751
+ // time is optional
752
+ // if (!('time' in unknownPostOrEvent)) return false
753
+ // target is optional (e.g. a new post has no target)
754
+ // if (!('target' in unknownPostOrEvent)) return false
755
+ // title is optional (e.g. a comment has no title)
756
+ // if (!('title' in unknownPostOrEvent)) return false
757
+ // text is optional (e.g. an event has only title)
758
+ // if (!('text' in unknownPostOrEvent)) return false
759
+
760
+ if (!unknownPostOrEvent?.version?.startsWith("dmp")) return false
761
+ return true
762
+ }
763
+
764
+ export const isDmpEventSignedClosed = (
765
+ unknownPostOrEvent: UnknownPostOrEvent
766
+ ): boolean => {
767
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
768
+
769
+ if (!('signedString' in unknownPostOrEvent)) return false
770
+ if (!('signature' in unknownPostOrEvent)) return false
771
+ // signer is optional
772
+ // if (!('signer' in unknownPostOrEvent)) return false
773
+
774
+ if (typeof(unknownPostOrEvent.signedString) !== "string") return false
775
+ const signedObject: DmpEvent = JSON.parse(unknownPostOrEvent.signedString)
776
+
777
+ if (!isDmpEvent(signedObject)) return false
778
+
779
+ return true
780
+ }
781
+
782
+ export const isDmpEventSignedOpened = (
783
+ unknownPostOrEvent: UnknownPostOrEvent
784
+ ): boolean => {
785
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
786
+
787
+ if (!isDmpEventSignedClosed) return false
788
+
789
+ if ('signedObject' in unknownPostOrEvent) {
790
+ if (isDmpEvent(unknownPostOrEvent.signedObject)) return true
791
+ }
792
+
793
+ return false
794
+ }
795
+
796
+ /**
797
+ * 1. Post can contain DPM event without signature as a string inside
798
+ * signed_message, so signature is attached with 'signature' key.
799
+ *
800
+ * isPostWithDmpEventSignedOpened()
801
+ * - contains isDmpEventSignedOpened()
802
+ * isPostWithDmpEventSignedClosed()
803
+ * - contains isDmpEventSignedClosed()
804
+ * isPostWithDmpEvent()
805
+ * - contains isDmpEvent()
806
+ *
807
+ * 2. Post can contain Nostr event with signature as a string inside
808
+ * signed_message, and signature is also attached with 'signature' key.
809
+ * In other words, signature will be passed twice:
810
+ * - inside 'sig' key of parsed 'signed_message'
811
+ * - inside 'signature' key of the post object.
812
+ *
813
+ * isPostWithNostrSpasmEventSignedOpened()
814
+ * - contains isNostrSpasmEventSignedOpened()
815
+ * isPostWithNostrEventSignedOpened()
816
+ * - contains isNostrEventSignedOpened()
817
+ * isPostWithNostrSpasmEvent()
818
+ * - contains isNostrSpasmEvent()
819
+ * isPostWithNostrEvent
820
+ * - contains isNostrEvent()
821
+ */
822
+
823
+ export const isPostWithDmpEvent = (
824
+ unknownPostOrEvent: UnknownPostOrEvent
825
+ ): boolean => {
826
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "DmpEvent")
827
+ }
828
+
829
+ export const isPostWithDmpEventSignedOpened = (
830
+ unknownPostOrEvent: UnknownPostOrEvent
831
+ ): boolean => {
832
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "DmpEventSignedOpened")
833
+ }
834
+
835
+ export const isPostWithDmpEventSignedClosed = (
836
+ unknownPostOrEvent: UnknownPostOrEvent
837
+ ): boolean => {
838
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "DmpEventSignedClosed")
839
+ }
840
+
841
+ export const isPostWithNostrSpasmEventSignedOpened = (
842
+ unknownPostOrEvent: UnknownPostOrEvent
843
+ ): boolean => {
844
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "NostrSpasmEventSignedOpened")
845
+ }
846
+
847
+ export const isPostWithNostrEventSignedOpened = (
848
+ unknownPostOrEvent: UnknownPostOrEvent
849
+ ): boolean => {
850
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "NostrEventSignedOpened")
851
+ }
852
+
853
+ export const isPostWithNostrSpasmEvent = (
854
+ unknownPostOrEvent: UnknownPostOrEvent
855
+ ): boolean => {
856
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "NostrSpasmEvent")
857
+ }
858
+
859
+ export const isPostWithNostrEvent = (
860
+ unknownPostOrEvent: UnknownEvent
861
+ ): boolean => {
862
+ return !!(identifyEventInsidePost(unknownPostOrEvent) === "NostrEvent")
863
+ }
864
+
865
+ export const identifyEventInsidePost = (
866
+ unknownPostOrEvent: UnknownPostOrEvent
867
+ ): EventType | false => {
868
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
869
+
870
+ if (!('signed_message' in unknownPostOrEvent)) return false
871
+ if (typeof(unknownPostOrEvent.signed_message) !== "string") return false
872
+ const signedObject: NostrSpasmEvent | DmpEvent = JSON.parse(unknownPostOrEvent.signed_message)
873
+
874
+ // isNostrEvent() will return true for NostrSpasmEvent,
875
+ // so the order of calling the functions is important.
876
+ // Sorted by popularity/frequency.
877
+ // TODO: Ideally, refactor and call identifyEvent(), which has
878
+ // if statements and calls isSomething... functions in the right
879
+ // order, depending on whether event has signature, etc.
880
+ // DMP
881
+ if (isDmpEventSignedOpened(signedObject)) return "DmpEventSignedOpened"
882
+ if (isDmpEventSignedClosed(signedObject)) return "DmpEventSignedClosed"
883
+ if (isDmpEvent(signedObject)) return "DmpEvent"
884
+ // Nostr
885
+ if (isNostrSpasmEventSignedOpened(signedObject)) return "NostrSpasmEventSignedOpened"
886
+ if (isNostrSpasmEvent(signedObject)) return "NostrSpasmEvent"
887
+ if (isNostrEventSignedOpened(signedObject)) return "NostrEventSignedOpened"
888
+ if (isNostrEvent(signedObject)) return "NostrEvent"
889
+ }
890
+
891
+ export const standartizePostOrEvent = (
892
+ unknownPostOrEvent: UnknownPostOrEvent,
893
+ info?: KnownPostOrEventInfo
894
+ ): StandardizedEvent | false => {
895
+
896
+ if (!isObjectWithValues(unknownPostOrEvent)) return false
897
+
898
+ // Info about post/event might be provided.
899
+ // If not, then we should identify an event.
900
+ if (!info) {
901
+ info = identifyPostOrEvent(unknownPostOrEvent)
902
+ }
903
+
904
+ if (!info || !info.webType) return false
905
+
906
+ let standartizedEvent: StandardizedEvent = {} as StandardizedEvent;
907
+
908
+ // DMP event submitted via UI
909
+ if (
910
+ info.eventInfo &&
911
+ info.eventInfo.type === "DmpEventSignedClosed" &&
912
+ info.eventIsSealed === false
913
+ ) {
914
+ standartizedEvent = standartizeDmpEventSignedClosed(
915
+ unknownPostOrEvent as DmpEventSignedClosed
916
+ )
917
+ }
918
+
919
+ // Nostr SPASM event submitted via UI
920
+ if (
921
+ info.eventInfo &&
922
+ info.eventInfo.type === "NostrSpasmEventSignedOpened" &&
923
+ info.eventIsSealed === false
924
+ ) {
925
+ standartizedEvent = standartizeNostrSpasmEventSignedOpened(
926
+ unknownPostOrEvent as NostrSpasmEventSignedOpened
927
+ )
928
+ }
929
+
930
+ // Post with sealed DMP event (received e.g. via SPASM module)
931
+ if (
932
+ info.eventInfo &&
933
+ info.eventInfo.type === "DmpEventSignedClosed" &&
934
+ info.eventIsSealed === true
935
+ ) {
936
+ standartizedEvent = standartizePostWithDmpEventSignedClosed(
937
+ unknownPostOrEvent as Post
938
+ )
939
+ }
940
+
941
+ // Post with sealed Nostr event (received e.g. via SPASM module)
942
+ if (
943
+ info.eventInfo &&
944
+ info.eventInfo.type === "NostrSpasmEventSignedOpened" &&
945
+ info.eventIsSealed === true
946
+ ) {
947
+ standartizedEvent = standartizePostWithNostrSpasmEventSignedOpened(
948
+ unknownPostOrEvent as Post
949
+ )
950
+ }
951
+
952
+ return standartizedEvent
953
+ }
954
+
955
+ // standardizeDmpEventSignedClosed
956
+ export const standartizeDmpEventSignedClosed = (
957
+ event: DmpEventSignedClosed,
958
+ ): StandardizedEvent | null => {
959
+
960
+ if (!isObjectWithValues(event)) return null
961
+
962
+ if (!isDmpEventSignedClosed(event)) return null
963
+
964
+ const signedString = event.signedString
965
+ const signedObject: DmpEvent = JSON.parse(signedString)
966
+ const signature = event.signature
967
+ const signer = event.signer
968
+ const target = signedObject.target
969
+ const action = signedObject.action
970
+ const title = signedObject.title
971
+ const text = signedObject.text
972
+ const signedDate = signedObject.time
973
+
974
+ return {
975
+ signedString,
976
+ signature,
977
+ signer,
978
+ target,
979
+ action,
980
+ title,
981
+ text,
982
+ signedDate
983
+ }
984
+ }
985
+
986
+ // standardizeNostrSpasmEventSignedOpened
987
+ export const standartizeNostrSpasmEventSignedOpened = (
988
+ event: NostrSpasmEventSignedOpened,
989
+ ): StandardizedEvent | null => {
990
+
991
+ if (!isObjectWithValues(event)) return null
992
+
993
+ if (!isNostrSpasmEventSignedOpened(event)) return null
994
+
995
+ const signedString = JSON.stringify(event)
996
+ // const signedObject: DmpEvent = JSON.parse(signedString)
997
+ const signature = event.sig
998
+ const signer = convertHexToBech32(event.pubkey)
999
+ const text = event.content
1000
+
1001
+ // Convert the Unix timestamp to a JavaScript Date object
1002
+ const date = new Date(event.created_at * 1000);
1003
+
1004
+ // Format the date in ISO format
1005
+ const timestamptz = date.toISOString();
1006
+
1007
+ const signedDate = timestamptz
1008
+
1009
+ let target: string
1010
+ let action: string
1011
+ let title: string
1012
+
1013
+ if (event.tags &&
1014
+ Array.isArray(event.tags)
1015
+ ) {
1016
+ event.tags.forEach(function (tag) {
1017
+ if (Array.isArray(tag) && tag[0] === "spasm_target") {
1018
+ target = tag[1]
1019
+ }
1020
+
1021
+ if (Array.isArray(tag) && tag[0] === "spasm_action") {
1022
+ action = tag[1]
1023
+ }
1024
+
1025
+ if (Array.isArray(tag) && tag[0] === "spasm_title") {
1026
+ title = tag[1]
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ return {
1032
+ signedString,
1033
+ signature,
1034
+ signer,
1035
+ target,
1036
+ action,
1037
+ title,
1038
+ text,
1039
+ signedDate
1040
+ }
1041
+ }
1042
+
1043
+ // standartizePostWithDmpEventSignedClosed
1044
+ export const standartizePostWithDmpEventSignedClosed = (
1045
+ post: Post,
1046
+ ): StandardizedEvent | null => {
1047
+
1048
+ if (!isObjectWithValues(post)) return null
1049
+
1050
+ if (
1051
+ !('signed_message' in post) ||
1052
+ typeof(post.signed_message) !== "string"
1053
+ ) {
1054
+ return null
1055
+ }
1056
+
1057
+ const signedString = post.signed_message
1058
+ const signedObject: DmpEvent = JSON.parse(signedString)
1059
+ const signature = post.signature
1060
+ const signer = post.signer
1061
+ const target = signedObject.target
1062
+ const action = signedObject.action
1063
+ const title = signedObject.title
1064
+ const text = signedObject.text
1065
+ const signedDate = signedObject.time
1066
+
1067
+ return {
1068
+ signedString,
1069
+ signature,
1070
+ signer,
1071
+ target,
1072
+ action,
1073
+ title,
1074
+ text,
1075
+ signedDate
1076
+ }
1077
+ }
1078
+
1079
+ // standardizePostWithNostrSpasmEventSignedOpened
1080
+ export const standartizePostWithNostrSpasmEventSignedOpened = (
1081
+ post: Post,
1082
+ ): StandardizedEvent | null => {
1083
+
1084
+ if (!isObjectWithValues(post)) return null
1085
+
1086
+ if (
1087
+ !('signed_message' in post) ||
1088
+ typeof(post.signed_message) !== "string"
1089
+ ) {
1090
+ return null
1091
+ }
1092
+
1093
+ // Extract the event
1094
+ const event = extractSealedEvent(post)
1095
+
1096
+ return standartizeNostrSpasmEventSignedOpened(event as NostrSpasmEventSignedOpened)
1097
+ }
1098
+
1099
+ export const convertToSpasm = (
1100
+ unknownPostOrEvent: UnknownPostOrEvent
1101
+ ): SpasmEvent | false => {
1102
+ return standartizePostOrEvent(unknownPostOrEvent)
1103
+ }