spasm.js 1.0.2 → 2.0.0-alpha

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.
Files changed (123) hide show
  1. package/README.md +454 -113
  2. package/lib.commonjs/convert/convertToEventForSpasmid.d.ts +4 -0
  3. package/lib.commonjs/convert/convertToEventForSpasmid.d.ts.map +1 -0
  4. package/lib.commonjs/convert/convertToEventForSpasmid.js +171 -0
  5. package/lib.commonjs/convert/convertToEventForSpasmid.js.map +1 -0
  6. package/lib.commonjs/convert/convertToSpasm.d.ts +17 -15
  7. package/lib.commonjs/convert/convertToSpasm.d.ts.map +1 -1
  8. package/lib.commonjs/convert/convertToSpasm.js +772 -265
  9. package/lib.commonjs/convert/convertToSpasm.js.map +1 -1
  10. package/lib.commonjs/convert/convertToSpasmEventDatabase.d.ts +4 -0
  11. package/lib.commonjs/convert/convertToSpasmEventDatabase.d.ts.map +1 -0
  12. package/lib.commonjs/convert/convertToSpasmEventDatabase.js +130 -0
  13. package/lib.commonjs/convert/convertToSpasmEventDatabase.js.map +1 -0
  14. package/lib.commonjs/convert/index.d.ts +2 -0
  15. package/lib.commonjs/convert/index.d.ts.map +1 -1
  16. package/lib.commonjs/convert/index.js +5 -1
  17. package/lib.commonjs/convert/index.js.map +1 -1
  18. package/lib.commonjs/id/getSpasmId.d.ts +4 -0
  19. package/lib.commonjs/id/getSpasmId.d.ts.map +1 -0
  20. package/lib.commonjs/id/getSpasmId.js +31 -0
  21. package/lib.commonjs/id/getSpasmId.js.map +1 -0
  22. package/lib.commonjs/id/index.d.ts +2 -0
  23. package/lib.commonjs/id/index.d.ts.map +1 -0
  24. package/lib.commonjs/id/index.js +6 -0
  25. package/lib.commonjs/id/index.js.map +1 -0
  26. package/lib.commonjs/identify/identifyEvent.d.ts.map +1 -1
  27. package/lib.commonjs/identify/identifyEvent.js +13 -5
  28. package/lib.commonjs/identify/identifyEvent.js.map +1 -1
  29. package/lib.commonjs/sort/index.d.ts +2 -0
  30. package/lib.commonjs/sort/index.d.ts.map +1 -0
  31. package/lib.commonjs/sort/index.js +6 -0
  32. package/lib.commonjs/sort/index.js.map +1 -0
  33. package/lib.commonjs/sort/sortEventForSpasmid.d.ts +3 -0
  34. package/lib.commonjs/sort/sortEventForSpasmid.d.ts.map +1 -0
  35. package/lib.commonjs/sort/sortEventForSpasmid.js +15 -0
  36. package/lib.commonjs/sort/sortEventForSpasmid.js.map +1 -0
  37. package/lib.commonjs/spasm.d.ts +5 -3
  38. package/lib.commonjs/spasm.d.ts.map +1 -1
  39. package/lib.commonjs/spasm.js +4 -5
  40. package/lib.commonjs/spasm.js.map +1 -1
  41. package/lib.commonjs/types/index.d.ts +1 -1
  42. package/lib.commonjs/types/index.d.ts.map +1 -1
  43. package/lib.commonjs/types/interfaces.d.ts +468 -90
  44. package/lib.commonjs/types/interfaces.d.ts.map +1 -1
  45. package/lib.commonjs/types/interfaces.js +2 -0
  46. package/lib.commonjs/types/interfaces.js.map +1 -1
  47. package/lib.commonjs/utils/nostrUtils.d.ts +10 -2
  48. package/lib.commonjs/utils/nostrUtils.d.ts.map +1 -1
  49. package/lib.commonjs/utils/nostrUtils.js +122 -22
  50. package/lib.commonjs/utils/nostrUtils.js.map +1 -1
  51. package/lib.commonjs/utils/utils.d.ts +26 -1
  52. package/lib.commonjs/utils/utils.d.ts.map +1 -1
  53. package/lib.commonjs/utils/utils.js +723 -3
  54. package/lib.commonjs/utils/utils.js.map +1 -1
  55. package/lib.esm/convert/convertToEventForSpasmid.d.ts +4 -0
  56. package/lib.esm/convert/convertToEventForSpasmid.d.ts.map +1 -0
  57. package/lib.esm/convert/convertToEventForSpasmid.js +168 -0
  58. package/lib.esm/convert/convertToEventForSpasmid.js.map +1 -0
  59. package/lib.esm/convert/convertToSpasm.d.ts +17 -15
  60. package/lib.esm/convert/convertToSpasm.d.ts.map +1 -1
  61. package/lib.esm/convert/convertToSpasm.js +759 -251
  62. package/lib.esm/convert/convertToSpasm.js.map +1 -1
  63. package/lib.esm/convert/convertToSpasmEventDatabase.d.ts +4 -0
  64. package/lib.esm/convert/convertToSpasmEventDatabase.d.ts.map +1 -0
  65. package/lib.esm/convert/convertToSpasmEventDatabase.js +137 -0
  66. package/lib.esm/convert/convertToSpasmEventDatabase.js.map +1 -0
  67. package/lib.esm/convert/index.d.ts +2 -0
  68. package/lib.esm/convert/index.d.ts.map +1 -1
  69. package/lib.esm/convert/index.js +2 -0
  70. package/lib.esm/convert/index.js.map +1 -1
  71. package/lib.esm/id/getSpasmId.d.ts +4 -0
  72. package/lib.esm/id/getSpasmId.d.ts.map +1 -0
  73. package/lib.esm/id/getSpasmId.js +31 -0
  74. package/lib.esm/id/getSpasmId.js.map +1 -0
  75. package/lib.esm/id/index.d.ts +2 -0
  76. package/lib.esm/id/index.d.ts.map +1 -0
  77. package/lib.esm/id/index.js +2 -0
  78. package/lib.esm/id/index.js.map +1 -0
  79. package/lib.esm/identify/identifyEvent.d.ts.map +1 -1
  80. package/lib.esm/identify/identifyEvent.js +13 -5
  81. package/lib.esm/identify/identifyEvent.js.map +1 -1
  82. package/lib.esm/sort/index.d.ts +2 -0
  83. package/lib.esm/sort/index.d.ts.map +1 -0
  84. package/lib.esm/sort/index.js +2 -0
  85. package/lib.esm/sort/index.js.map +1 -0
  86. package/lib.esm/sort/sortEventForSpasmid.d.ts +3 -0
  87. package/lib.esm/sort/sortEventForSpasmid.d.ts.map +1 -0
  88. package/lib.esm/sort/sortEventForSpasmid.js +17 -0
  89. package/lib.esm/sort/sortEventForSpasmid.js.map +1 -0
  90. package/lib.esm/spasm.d.ts +5 -3
  91. package/lib.esm/spasm.d.ts.map +1 -1
  92. package/lib.esm/spasm.js +4 -2
  93. package/lib.esm/spasm.js.map +1 -1
  94. package/lib.esm/types/index.d.ts +1 -1
  95. package/lib.esm/types/index.d.ts.map +1 -1
  96. package/lib.esm/types/interfaces.d.ts +468 -90
  97. package/lib.esm/types/interfaces.d.ts.map +1 -1
  98. package/lib.esm/types/interfaces.js +2 -0
  99. package/lib.esm/types/interfaces.js.map +1 -1
  100. package/lib.esm/utils/nostrUtils.d.ts +10 -2
  101. package/lib.esm/utils/nostrUtils.d.ts.map +1 -1
  102. package/lib.esm/utils/nostrUtils.js +119 -21
  103. package/lib.esm/utils/nostrUtils.js.map +1 -1
  104. package/lib.esm/utils/utils.d.ts +26 -1
  105. package/lib.esm/utils/utils.d.ts.map +1 -1
  106. package/lib.esm/utils/utils.js +701 -2
  107. package/lib.esm/utils/utils.js.map +1 -1
  108. package/package.json +9 -2
  109. package/src.ts/convert/convertToEventForSpasmid.ts +289 -0
  110. package/src.ts/convert/convertToSpasm.ts +962 -292
  111. package/src.ts/convert/convertToSpasmEventDatabase.ts +204 -0
  112. package/src.ts/convert/index.ts +2 -0
  113. package/src.ts/docs/architecture.md +413 -0
  114. package/src.ts/id/getSpasmId.ts +56 -0
  115. package/src.ts/id/index.ts +1 -0
  116. package/src.ts/identify/identifyEvent.ts +12 -5
  117. package/src.ts/sort/index.ts +1 -0
  118. package/src.ts/sort/sortEventForSpasmid.ts +30 -0
  119. package/src.ts/spasm.ts +5 -3
  120. package/src.ts/types/index.ts +1 -1
  121. package/src.ts/types/interfaces.ts +729 -125
  122. package/src.ts/utils/nostrUtils.ts +150 -22
  123. package/src.ts/utils/utils.ts +1005 -4
@@ -1,8 +1,31 @@
1
1
  import {
2
2
  UnknownPostOrEvent, UnknownEvent, NostrSpasmEvent,
3
- NostrSpasmEventSignedOpened, NostrSpasmVersion
3
+ NostrSpasmEventSignedOpened, NostrSpasmVersion,
4
+ LinkObject,
5
+ SpasmEventIdFormatV2,
6
+ SpasmEventAddressFormatV2,
7
+ SpasmEventSignatureFormatV2,
8
+ SpasmEventV2,
9
+ SpasmEventAuthorV2,
10
+ SpasmEventBodyHostV2,
11
+ SpasmEventMediaV2,
12
+ SpasmEventIdV2,
13
+ SpasmEventHashV2,
14
+ SpasmEventLinkV2,
15
+ SpasmEventBodyReferenceV2,
16
+ SpasmEventAddressV2,
17
+ // SpasmEventUsernameV2,
18
+ SpasmEventBodyParentV2
4
19
  } from "./../types/interfaces.js"
5
20
 
21
+ /*
22
+ * Using sha256 from 'js-sha256' npm package, because
23
+ * built-in 'crypto' module works only in a server-side
24
+ * Node.js environment, not on the client-side (browser).
25
+ */
26
+ import { sha256 } from "js-sha256"
27
+ import { ethers} from "ethers";
28
+
6
29
  // Filter out undefined, null, 0, '', false, NaN, {}, []
7
30
  // Keep {a: null}, {b: undefined}
8
31
  // Examples:
@@ -93,6 +116,8 @@ export const extractSealedEvent = (
93
116
  let signedObject: UnknownEvent | false = false
94
117
 
95
118
  if (
119
+ unknownPostOrEvent &&
120
+ typeof(unknownPostOrEvent) === "object" &&
96
121
  'signed_message' in unknownPostOrEvent &&
97
122
  unknownPostOrEvent['signed_message'] &&
98
123
  typeof(unknownPostOrEvent['signed_message'] === "string")
@@ -100,7 +125,10 @@ export const extractSealedEvent = (
100
125
  signedObject = JSON.parse(unknownPostOrEvent['signed_message'])
101
126
 
102
127
  } else if (
128
+ unknownPostOrEvent &&
129
+ typeof(unknownPostOrEvent) === "object" &&
103
130
  'signedString' in unknownPostOrEvent &&
131
+ unknownPostOrEvent['signedString'] &&
104
132
  typeof(unknownPostOrEvent['signedString'] === "string")
105
133
  ) {
106
134
  signedObject = JSON.parse(unknownPostOrEvent['signedString'])
@@ -113,10 +141,17 @@ export const toBeTimestamp = (time: any): number | undefined => {
113
141
  const date = new Date(time);
114
142
  const timestamp = date.getTime();
115
143
 
116
- // Check if the timestamp is NaN, indicating an invalid date
117
- if (Number.isNaN(timestamp)) {
144
+ // Check if the timestamp is NaN, indicating an invalid date
145
+ if (Number.isNaN(timestamp)) {
118
146
  return undefined;
119
- }
147
+ }
148
+
149
+ // Optional
150
+ // Standardize the timestamp to 10 characters (seconds)
151
+ // by rounding down the timestamp to the nearest second.
152
+ // if (timestamp.toString().length > 10) {
153
+ // timestamp = Math.floor(timestamp / 1000) * 1000;
154
+ // }
120
155
 
121
156
  return timestamp;
122
157
  };
@@ -143,3 +178,969 @@ export const getNostrSpasmVersion = (
143
178
  }
144
179
  return nostrSpasmVersion
145
180
  }
181
+
182
+ // Example usage
183
+ // getSchemeFromUrl('https://example.com/news') // return 'https'
184
+ // getSchemeFromUrl('http://example.com') // return 'http'
185
+ // getSchemeFromUrl('ftp://example.com') // return 'ftp'
186
+ // getSchemeFromUrl('mailto://...') // return 'mailto'
187
+ // getSchemeFromUrl('ipfs://123abc') // return 'ipfs'
188
+ // export const getSchemeFromUrl = (url: any) => {
189
+ // if (!url || typeof(url) !== "string") return ""
190
+ // try {
191
+ // const urlObject = new URL(url);
192
+ // return urlObject.protocol.slice(0, -1); // Remove the trailing colon
193
+ // } catch (error) {
194
+ // console.log('Invalid URL:', url);
195
+ // return "";
196
+ // }
197
+ // }
198
+
199
+ export const isValidUrl = (value?: any): boolean => {
200
+ if (!value) return false
201
+ try {
202
+ // new URL() constructor is less vulnerable to ReDoS attacks
203
+ // because it's a built-it JS function that doesn't use regex
204
+ new URL(value);
205
+ return true;
206
+ }
207
+ catch(e) {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ export const createLinkObjectFromUrl = (
213
+ url: any,
214
+ key?: any
215
+ ): LinkObject | null => {
216
+ if (!url || typeof(url) !== "string") return null
217
+
218
+ try {
219
+ const urlObject = new URL(url);
220
+
221
+ const linkObject: LinkObject = {
222
+ value: url,
223
+ // protocol: urlObject.protocol.slice(0, -1),
224
+ // host: urlObject.host,
225
+ // path: urlObject.pathname,
226
+ // search: urlObject.search,
227
+ }
228
+
229
+ if (urlObject.protocol) {
230
+ linkObject.protocol = urlObject.protocol.slice(0, -1)
231
+ }
232
+
233
+ if (urlObject.origin) {
234
+ linkObject.origin = urlObject.origin
235
+ }
236
+
237
+ if (urlObject.host) {
238
+ linkObject.host = urlObject.host
239
+ }
240
+
241
+ if (
242
+ urlObject.pathname &&
243
+ typeof(urlObject.pathname) === "string" // &&
244
+ // urlObject.pathname.length > 1
245
+ ) {
246
+ linkObject.pathname = urlObject.pathname
247
+ }
248
+
249
+ if (
250
+ urlObject.search &&
251
+ typeof(urlObject.search) === "string" // &&
252
+ // urlObject.search.length > 1
253
+ ) {
254
+ linkObject.search = urlObject.search
255
+ }
256
+
257
+ if (urlObject.port) {
258
+ linkObject.port = urlObject.port
259
+ }
260
+
261
+ if (urlObject.hash) {
262
+ linkObject.hash = urlObject.hash
263
+ }
264
+
265
+ if (
266
+ key &&
267
+ (typeof(key) === "string" || typeof(key) === "number")
268
+ ) {
269
+ linkObject.originalProtocolKey = key
270
+ }
271
+
272
+ return linkObject
273
+
274
+ } catch (error) {
275
+ console.log('Invalid URL:', url);
276
+ return null;
277
+ }
278
+ }
279
+
280
+ export const getFormatFromValue = (
281
+ value?: string | number
282
+ ): SpasmEventIdFormatV2 | SpasmEventSignatureFormatV2 | undefined => {
283
+ let format: SpasmEventIdFormatV2 | undefined =
284
+ undefined
285
+
286
+ if (!value) return format
287
+ if (typeof(value) !== "string" && typeof(value) !== "number") {
288
+ return format
289
+ }
290
+
291
+ if (typeof(value) === "number") {
292
+ return format = { name: "number" }
293
+ }
294
+
295
+ if (value && typeof(value) === "string") {
296
+
297
+ // Spasm ID
298
+ if (value.length === 64 + 9 && value.startsWith("spasmid")) {
299
+ const version = value.slice(7,9)
300
+ format = { name: "spasmid", version: version }
301
+ return format
302
+ }
303
+
304
+ // Dmp ID (signature)
305
+ if (value.length === 132 && value.startsWith("0x")) {
306
+ format = { name: "ethereum-sig" }
307
+ return format
308
+ }
309
+
310
+ // Nostr ID
311
+ if (value.length === 63 && value.startsWith("note")) {
312
+ format = { name: "nostr-note" }
313
+ return format
314
+ }
315
+
316
+ if (value.length === 68 && value.startsWith("nevent")) {
317
+ format = { name: "nostr-nevent" }
318
+ return format
319
+ }
320
+
321
+ // Spasm signer
322
+ // if (address.length === 64 + 9 && address.startsWith("spasmer")) {
323
+ // const version = address.slice(7,9)
324
+ // format = { name: "spasmer", version: version }
325
+ // return format
326
+ // }
327
+
328
+ // Ethereum signer
329
+ if (value.length === 42 && value.startsWith("0x")) {
330
+ format = { name: "ethereum-pubkey" }
331
+ return format
332
+ }
333
+
334
+ // Nostr signer
335
+ if (value.length === 63 && value.startsWith("npub")) {
336
+ format = { name: "nostr-npub" }
337
+ return format
338
+ }
339
+
340
+ // url
341
+ if (isValidUrl(value)) {
342
+ format = { name: "url" }
343
+ return format
344
+ }
345
+
346
+ if (
347
+ value.length === 64 &&
348
+ !value.startsWith("note") &&
349
+ !value.startsWith("nevent") &&
350
+ !value.startsWith("npub")
351
+ ) {
352
+ format = { name: "nostr-hex" }
353
+ return format
354
+ }
355
+
356
+ }
357
+
358
+ if (typeof(value) === "string") {
359
+ return format = { name: "string" }
360
+ }
361
+
362
+ return format
363
+ }
364
+
365
+ export const getFormatFromId = (
366
+ id: string | number
367
+ ): SpasmEventIdFormatV2 => {
368
+ return getFormatFromValue(id) as SpasmEventIdFormatV2
369
+ }
370
+
371
+ export const getFormatFromAddress = (
372
+ address: string | number
373
+ ): SpasmEventAddressFormatV2 => {
374
+ return getFormatFromValue(address) as SpasmEventAddressFormatV2
375
+ }
376
+
377
+ export const getFormatFromSignature = (
378
+ address: string | number
379
+ ): SpasmEventSignatureFormatV2 => {
380
+ return getFormatFromValue(address) as SpasmEventSignatureFormatV2
381
+ }
382
+
383
+ export const getHashOfString = (
384
+ string: string,
385
+ algorithm = "sha256"
386
+ ): string => {
387
+ if (typeof(string) !== "string") return ""
388
+
389
+ if (algorithm === "sha256") {
390
+ return sha256(string)
391
+ }
392
+
393
+ return ""
394
+ }
395
+
396
+ // Keep only specified keys in an object.
397
+ export const keepTheseKeysInObject = (
398
+ obj: Record<string, any>, keys: string[]
399
+ ): Partial<Record<string, any>> => {
400
+ return keys.reduce((acc, key) => {
401
+ if (obj.hasOwnProperty(key)) {
402
+ acc[key] = obj[key];
403
+ }
404
+ return acc;
405
+ }, {} as Record<string, any>);
406
+ }
407
+
408
+ // Keep only specified keys in each object of an array.
409
+ export const keepTheseKeysInObjectsInArray = (
410
+ array: Record<string, any>[], keys: string[]
411
+ ): Partial<Record<string, any>[]> => {
412
+ return array.map(obj => keepTheseKeysInObject(obj, keys));
413
+ }
414
+
415
+ // This function only sorts string and number values.
416
+ export const sortArrayOfStringsAndNumbers = (
417
+ array: any[]
418
+ ): any[] => {
419
+ // Separate values into valid and invalid categories.
420
+ const {
421
+ validValues, invalidValues
422
+ } = array.reduce<{
423
+ validValues: any[]; invalidValues: any[];
424
+ }>(
425
+ (acc, value) => {
426
+ if (
427
+ typeof value === 'string' ||
428
+ typeof value === 'number'
429
+ ) {
430
+ acc.validValues.push(value)
431
+ } else {
432
+ acc.invalidValues.push(value)
433
+ }
434
+ return acc
435
+ },
436
+ { validValues: [], invalidValues: [] }
437
+ )
438
+
439
+ // Sort the valid values
440
+ const sortedValidValues = validValues.sort(
441
+ (a, b) => String(a).localeCompare(String(b))
442
+ )
443
+
444
+ // Combine sorted valid values with invalid values
445
+ const result = [...sortedValidValues,...invalidValues]
446
+
447
+ return result
448
+ }
449
+
450
+ export const sortArrayOfObjects = (
451
+ objects: any[],
452
+ sortBy: string | string[] = ["id"]
453
+ ): any[] => {
454
+ if (
455
+ !objects ||
456
+ !Array.isArray(objects) ||
457
+ !objects[0]
458
+ ) {
459
+ return []
460
+ }
461
+ // Ensure sortBy is always treated as an array
462
+ const sortedBy = Array.isArray(sortBy)? sortBy : [sortBy]
463
+
464
+ // Separate objects into valid and invalid categories based
465
+ // on the existence of the specified property(ies)
466
+ const {
467
+ validObjects, invalidValues
468
+ } = objects.reduce<{
469
+ validObjects: SpasmEventV2[]; invalidValues: any[];
470
+ }>(
471
+ (acc, item) => {
472
+ let isValid: boolean = false
473
+
474
+ // Only one prop should exist in item in order
475
+ // to make it a valid item.
476
+ sortedBy.forEach((key) => {
477
+ if (
478
+ typeof(item) === 'object' && item &&
479
+ key in item && item[key] &&
480
+ (
481
+ typeof(item[key]) === "string" ||
482
+ typeof(item[key]) === "number"
483
+ )
484
+ ) {
485
+ isValid = true
486
+ }
487
+ })
488
+
489
+ if (isValid) {
490
+ acc.validObjects.push(item as SpasmEventV2);
491
+ } else {
492
+ acc.invalidValues.push(item);
493
+ }
494
+ return acc;
495
+ },
496
+ { validObjects: [], invalidValues: [] }
497
+ );
498
+
499
+ // Sort the valid objects by the specified property(ies)
500
+ const sortedValidObjects = validObjects.sort((a, b) => {
501
+ for (const key of sortedBy) {
502
+ const aValue =
503
+ typeof a[key] === 'string'? a[key] : String(a[key]);
504
+ const bValue =
505
+ typeof b[key] === 'string'? b[key] : String(b[key]);
506
+ const compareResult = aValue.localeCompare(bValue);
507
+ if (compareResult!== 0) {
508
+ return compareResult;
509
+ }
510
+ }
511
+ return 0; // Equal
512
+ });
513
+
514
+ const sortedInvalidValues = sortArrayOfStringsAndNumbers(
515
+ invalidValues
516
+ )
517
+
518
+ // Combine sorted valid objects with invalid objects
519
+ const result = [...sortedValidObjects, ...sortedInvalidValues]
520
+
521
+ return result;
522
+ }
523
+
524
+ export const sortAuthorsForSpasmEventV2 = (
525
+ authors: SpasmEventAuthorV2[],
526
+ ): SpasmEventAuthorV2[] => {
527
+
528
+ // Clean and sort addresses
529
+ authors.forEach(author => {
530
+ if (
531
+ author && typeof(author) === "object" &&
532
+ 'addresses' in author && author.addresses &&
533
+ Array.isArray(author.addresses) &&
534
+ author.addresses[0]
535
+ ) {
536
+ // Clean addresses to keep only 'value' and 'format' keys
537
+ // and remove 'verified' and 'hosts' keys.
538
+ author.addresses = keepTheseKeysInObjectsInArray(
539
+ author.addresses, ["value", "format"]
540
+ ) as SpasmEventAddressV2[]
541
+
542
+ // Sort addresses
543
+ author.addresses = sortArrayOfObjects(
544
+ author.addresses, "value"
545
+ )
546
+ }
547
+ })
548
+
549
+ // Clean and sort usernames
550
+ authors.forEach(author => {
551
+ if (
552
+ author && typeof(author) === "object" &&
553
+ 'usernames' in author && author.usernames &&
554
+ Array.isArray(author.usernames) &&
555
+ author.usernames[0]
556
+ ) {
557
+ // There is no need to clean usernames because all fields
558
+ // should be calculated for the Spasm ID 01.
559
+
560
+ // Sort usernames
561
+ author.usernames = sortArrayOfObjects(
562
+ author.usernames, "value"
563
+ )
564
+ }
565
+ })
566
+
567
+
568
+ let authorsWithAddress: SpasmEventAuthorV2[] = []
569
+ // Authors without address are used temporary until we split
570
+ // them further depending on whether they have usernames.
571
+ let authorsWithoutAddress: any[] = []
572
+ let authorsWithoutAddressWithUsername: SpasmEventAuthorV2[] = []
573
+ let authorsWithoutAddressWithoutUsername: any[] = []
574
+
575
+ authors.forEach(author => {
576
+ if (
577
+ author && typeof(author) === "object" &&
578
+ 'addresses' in author && author.addresses &&
579
+ Array.isArray(author.addresses) && author.addresses[0] &&
580
+ author.addresses[0].value &&
581
+ (
582
+ typeof(author.addresses[0].value) === "string" ||
583
+ typeof(author.addresses[0].value) === "number"
584
+ )
585
+ ) {
586
+ authorsWithAddress.push(author)
587
+ } else {
588
+ authorsWithoutAddress.push(author)
589
+ }
590
+ })
591
+
592
+ authorsWithoutAddress.forEach(author => {
593
+ if (
594
+ author && typeof(author) === "object" &&
595
+ 'usernames' in author && author.usernames &&
596
+ Array.isArray(author.usernames) && author.usernames[0] &&
597
+ author.usernames[0].value &&
598
+ (
599
+ typeof(author.usernames[0].value) === "string" ||
600
+ typeof(author.usernames[0].value) === "number"
601
+ )
602
+ ) {
603
+ authorsWithoutAddressWithUsername.push(author)
604
+ } else {
605
+ authorsWithoutAddressWithoutUsername.push(author)
606
+ }
607
+ })
608
+
609
+ // Sort all 3 arrays
610
+ const sortedAuthorsWithAddress: SpasmEventAuthorV2[] =
611
+ sortArrayOfObjectsByKeyValue(
612
+ authorsWithAddress, "addresses"
613
+ ) as SpasmEventAuthorV2[]
614
+
615
+ const sortedAuthorsWithoutAddressWithUsername: SpasmEventAuthorV2[] =
616
+ sortArrayOfObjectsByKeyValue(
617
+ authorsWithoutAddressWithUsername, "usernames"
618
+ ) as SpasmEventAuthorV2[]
619
+
620
+ const sortedAuthorsWithoutAddressWithoutUsername: any[] =
621
+ sortArrayOfObjects(
622
+ authorsWithoutAddressWithoutUsername, ["id"]
623
+ )
624
+
625
+ const result = [
626
+ ...sortedAuthorsWithAddress,
627
+ ...sortedAuthorsWithoutAddressWithUsername,
628
+ ...sortedAuthorsWithoutAddressWithoutUsername
629
+ ]
630
+
631
+ return result
632
+ }
633
+
634
+ export const sortAuthorsForSpasmid01 = sortAuthorsForSpasmEventV2
635
+
636
+ export const sortArrayOfObjectsByKeyValue = (
637
+ objects: SpasmEventAuthorV2[] | SpasmEventMediaV2[],
638
+ key: string
639
+ ): any[] => {
640
+ const sortedObjects = objects.sort((a, b) => {
641
+ let aValue = ""
642
+ let bValue = ""
643
+
644
+ if (
645
+ a[key] && a[key][0] &&
646
+ a[key][0].value
647
+ ) {
648
+ if (typeof(a[key][0].value) === 'string') {
649
+ aValue = a[key][0].value
650
+ } else if ( typeof(a[key][0].value) === 'number') {
651
+ aValue = String(a[key][0].value);
652
+ }
653
+ }
654
+
655
+ if (
656
+ b[key] && b[key][0] &&
657
+ b[key][0].value
658
+ ) {
659
+ if (typeof(b[key][0].value) === 'string') {
660
+ bValue = b[key][0].value
661
+ } else if ( typeof(b[key][0].value) === 'number') {
662
+ bValue = String(b[key][0].value);
663
+ }
664
+ }
665
+
666
+ const compareResult = aValue.localeCompare(bValue);
667
+ if (compareResult!== 0) {
668
+ return compareResult;
669
+ }
670
+
671
+ return 0; // Equal
672
+ });
673
+
674
+ return sortedObjects
675
+ }
676
+
677
+ export const sortHostsForSpasmEventV2 = (
678
+ hosts: SpasmEventBodyHostV2[],
679
+ ): SpasmEventBodyHostV2[] => {
680
+ if (
681
+ !hosts ||
682
+ !Array.isArray(hosts) ||
683
+ !hosts[0]
684
+ ) {
685
+ return hosts
686
+ }
687
+
688
+ const sortedHosts: SpasmEventBodyHostV2[] =
689
+ sortArrayOfObjects(
690
+ hosts, "value"
691
+ )
692
+ return sortedHosts
693
+ }
694
+
695
+ export const sortHostsForSpasmid01 = sortHostsForSpasmEventV2
696
+
697
+ export const sortLinksForSpasmEventV2 = sortHostsForSpasmEventV2
698
+
699
+ export const sortLinksForSpasmid01 = sortLinksForSpasmEventV2
700
+
701
+ export const sortMediasForSpasmid01 = (
702
+ medias: any,
703
+ ): SpasmEventMediaV2[] => {
704
+
705
+ if (!medias || !Array.isArray(medias)) return []
706
+
707
+ // Clean and sort IDs
708
+ medias.forEach(media => {
709
+ if (
710
+ media && typeof(media) === "object" &&
711
+ 'ids' in media && media.ids &&
712
+ Array.isArray(media.ids) &&
713
+ media.ids[0]
714
+ ) {
715
+ // Clean ids to keep only 'value' key
716
+ media.ids = keepTheseKeysInObjectsInArray(
717
+ media.ids, ["value"]
718
+ ) as SpasmEventIdV2[]
719
+
720
+ // Sort ids
721
+ media.ids = sortArrayOfObjects(
722
+ media.ids, "value"
723
+ )
724
+ }
725
+ })
726
+
727
+ // Clean and sort hashes
728
+ medias.forEach(media => {
729
+ if (
730
+ media && typeof(media) === "object" &&
731
+ 'hashes' in media && media.hashes &&
732
+ Array.isArray(media.hashes) &&
733
+ media.hashes[0]
734
+ ) {
735
+ // Clean hashes to keep only 'value' key
736
+ media.hashes = keepTheseKeysInObjectsInArray(
737
+ media.hashes, ["value"]
738
+ ) as SpasmEventHashV2[]
739
+
740
+ // Sort hashes
741
+ media.hashes = sortArrayOfObjects(
742
+ media.hashes, "value"
743
+ )
744
+ }
745
+ })
746
+
747
+ // Clean and sort links
748
+ medias.forEach(media => {
749
+ if (
750
+ media && typeof(media) === "object" &&
751
+ 'links' in media && media.links &&
752
+ Array.isArray(media.links) &&
753
+ media.links[0]
754
+ ) {
755
+ // Clean links to keep only 'value' key
756
+ media.links = keepTheseKeysInObjectsInArray(
757
+ media.links, ["value"]
758
+ ) as SpasmEventLinkV2[]
759
+
760
+ // Sort links
761
+ media.links = sortArrayOfObjects(
762
+ media.links, "value"
763
+ )
764
+ }
765
+ })
766
+
767
+ // mediasWithIds might also have hashes and links
768
+ let mediasWithIds: SpasmEventMediaV2[] = []
769
+ let mediasWithoutIds: any[] = []
770
+ // mediasWithHashes might also have links, but no ids
771
+ let mediasWithHashes: SpasmEventMediaV2[] = []
772
+ let mediasWithoutIdsHashes: any[] = []
773
+ // mediasWithLinks only has links, but no ids and hashes
774
+ let mediasWithLinks: SpasmEventMediaV2[] = []
775
+ let mediasWithoutIdsHashesLinks: any[] = []
776
+
777
+ // Sort medias by ids
778
+ medias.forEach(media => {
779
+ if (
780
+ media && typeof(media) === "object" &&
781
+ 'ids' in media && media.ids &&
782
+ Array.isArray(media.ids) && media.ids[0] &&
783
+ media.ids[0].value &&
784
+ (
785
+ typeof(media.ids[0].value) === "string" ||
786
+ typeof(media.ids[0].value) === "number"
787
+ )
788
+ ) {
789
+ mediasWithIds.push(media)
790
+ } else {
791
+ mediasWithoutIds.push(media)
792
+ }
793
+ })
794
+
795
+ // Sort medias by hashes
796
+ mediasWithoutIds.forEach(media => {
797
+ if (
798
+ media && typeof(media) === "object" &&
799
+ 'hashes' in media && media.hashes &&
800
+ Array.isArray(media.hashes) && media.hashes[0] &&
801
+ media.hashes[0].value &&
802
+ (
803
+ typeof(media.hashes[0].value) === "string" ||
804
+ typeof(media.hashes[0].value) === "number"
805
+ )
806
+ ) {
807
+ mediasWithHashes.push(media)
808
+ } else {
809
+ mediasWithoutIdsHashes.push(media)
810
+ }
811
+ })
812
+
813
+ // Sort medias by links
814
+ mediasWithoutIdsHashes.forEach(media => {
815
+ if (
816
+ media && typeof(media) === "object" &&
817
+ 'links' in media && media.links &&
818
+ Array.isArray(media.links) && media.links[0] &&
819
+ media.links[0].value &&
820
+ (
821
+ typeof(media.links[0].value) === "string" ||
822
+ typeof(media.links[0].value) === "number"
823
+ )
824
+ ) {
825
+ mediasWithLinks.push(media)
826
+ } else {
827
+ mediasWithoutIdsHashesLinks.push(media)
828
+ }
829
+ })
830
+
831
+ const mediasOther: any[] = mediasWithoutIdsHashesLinks
832
+
833
+ // Sort all 3 arrays
834
+ const sortedMediasWithIds: SpasmEventMediaV2[] =
835
+ sortArrayOfObjectsByKeyValue(
836
+ mediasWithIds, "ids"
837
+ ) as SpasmEventMediaV2[]
838
+
839
+ const sortedMediasWithHashes: SpasmEventMediaV2[] =
840
+ sortArrayOfObjectsByKeyValue(
841
+ mediasWithHashes, "hashes"
842
+ ) as SpasmEventMediaV2[]
843
+
844
+ const sortedMediasWithLinks: SpasmEventMediaV2[] =
845
+ sortArrayOfObjectsByKeyValue(
846
+ mediasWithLinks, "links"
847
+ ) as SpasmEventMediaV2[]
848
+
849
+ const sortedMediasOther: any[] =
850
+ sortArrayOfObjects(mediasOther, ["id"])
851
+
852
+ const result = [
853
+ ...sortedMediasWithIds,
854
+ ...sortedMediasWithHashes,
855
+ ...sortedMediasWithLinks,
856
+ ...sortedMediasOther
857
+ ]
858
+
859
+ return result
860
+ }
861
+
862
+ // Deprecated sortMediasForSpasmEventV2 because we only keep
863
+ // a 'value' key to calculate Spasm ID 01.
864
+ // export const sortMediasForSpasmid01 = sortMediasforSpasmEventV2
865
+
866
+ export const sortReferencesForSpasmid01 = (
867
+ references: SpasmEventBodyReferenceV2[]
868
+ ): SpasmEventBodyReferenceV2[] => {
869
+
870
+ if (!references || !Array.isArray(references)) return []
871
+
872
+ // Clean and sort IDs
873
+ references.forEach(reference => {
874
+ if (
875
+ reference && typeof(reference) === "object" &&
876
+ 'ids' in reference && reference.ids &&
877
+ Array.isArray(reference.ids) &&
878
+ reference.ids[0]
879
+ ) {
880
+ // Clean ids to keep only 'value' key
881
+ reference.ids = keepTheseKeysInObjectsInArray(
882
+ reference.ids, ["value"]
883
+ ) as SpasmEventIdV2[]
884
+
885
+ // Sort ids
886
+ reference.ids = sortArrayOfObjects(
887
+ reference.ids, "value"
888
+ )
889
+ }
890
+ })
891
+
892
+ // Sort references based on IDs
893
+ const sortedReferences: SpasmEventBodyReferenceV2[] =
894
+ sortArrayOfObjectsByKeyValue(
895
+ references, "ids"
896
+ ) as SpasmEventBodyReferenceV2[]
897
+
898
+ return sortedReferences
899
+ }
900
+
901
+ export const sortParentForSpasmid01 = (
902
+ parent: SpasmEventBodyParentV2
903
+ ): SpasmEventBodyParentV2 => {
904
+
905
+ if (!parent || typeof(parent) !== "object") return parent
906
+
907
+ // Clean and sort IDs
908
+ if (
909
+ parent && typeof(parent) === "object" &&
910
+ 'ids' in parent && parent.ids &&
911
+ Array.isArray(parent.ids) &&
912
+ parent.ids[0]
913
+ ) {
914
+ // Clean ids to keep only 'value' key
915
+ parent.ids = keepTheseKeysInObjectsInArray(
916
+ parent.ids, ["value"]
917
+ ) as SpasmEventIdV2[]
918
+
919
+ // Sort ids
920
+ parent.ids = sortArrayOfObjects(
921
+ parent.ids, "value"
922
+ )
923
+ }
924
+
925
+ return parent
926
+ }
927
+
928
+ export const sortTagsForSpasmid01 = (
929
+ tags: any[][]
930
+ ): any[][] => {
931
+
932
+ if (!tags || !Array.isArray(tags)) return [[]]
933
+
934
+ /**
935
+ * Tags are an array of arrays (e.g., Nostr tags).
936
+ * Each tag is an array with any number of elements.
937
+ * Some tags will have the same one-letter first element,
938
+ * so sorting by the first element is not a good approach.
939
+ * Instead, the current sorting logic for spasmid01 is
940
+ * to find the length of the longest tag array (e.g., 10),
941
+ * and start sorting tags by the 10th element, then
942
+ * by the 9th element, and continue until sorting is
943
+ * done by the first element.
944
+ *
945
+ * Each tag is an array of values. However, values inside
946
+ * each tag should not be sorted as it can affect the
947
+ * intention of the event. For example, the order of an
948
+ * element in a Nostr tag array has a meaning.
949
+ */
950
+
951
+ const sortTagsByElementNumber = (
952
+ elementNumber: number = 0
953
+ ): void => {
954
+ tags = tags.sort((a, b) => {
955
+ const key = elementNumber
956
+ let aValue = ""
957
+ let bValue = ""
958
+
959
+ if (a[key]) {
960
+ if (typeof(a[key]) === 'string') {
961
+ aValue = a[key]
962
+ } else if (typeof(a[key]) === 'number') {
963
+ aValue = String(a[key])
964
+ }
965
+ }
966
+
967
+ if (b[key]) {
968
+ if (typeof(b[key]) === 'string') {
969
+ bValue = b[key]
970
+ } else if (typeof(b[key]) === 'number') {
971
+ bValue = String(b[key])
972
+ }
973
+ }
974
+
975
+ const compareResult = aValue.localeCompare(bValue);
976
+ if (compareResult!== 0) {
977
+ return compareResult;
978
+ }
979
+
980
+ return 0; // Equal
981
+ });
982
+ }
983
+
984
+ let longestTagArrayLength = 1
985
+
986
+ // Find the longest array (tag) to be used for sorting.
987
+ tags.forEach(tag => {
988
+ if (
989
+ tag && Array.isArray(tag) &&
990
+ tag.length > longestTagArrayLength
991
+ ) {
992
+ longestTagArrayLength = tag.length
993
+ }
994
+ })
995
+
996
+ for (let i = longestTagArrayLength; i >= 0; i--) {
997
+ sortTagsByElementNumber(i)
998
+ }
999
+
1000
+ return tags
1001
+ }
1002
+
1003
+ export const markSpasmEventAddressAsVerified = (
1004
+ spasmEvent: SpasmEventV2,
1005
+ verifiedAddress: string | number,
1006
+ version: string = "2.0.0"
1007
+ ): void => {
1008
+ if (version === "2.0.0") {
1009
+ if (spasmEvent.authors) {
1010
+ spasmEvent.authors.forEach(author => {
1011
+ if (author.addresses) {
1012
+ author.addresses.forEach(address => {
1013
+ if (address.value === verifiedAddress) {
1014
+ address.verified = true;
1015
+ }
1016
+ });
1017
+ }
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+
1024
+ export const verifyEthereumSignature = (
1025
+ messageString: string,
1026
+ signature: string,
1027
+ signerAddress: string
1028
+ ): boolean => {
1029
+ try {
1030
+ if (signature && typeof (signature) === 'string') {
1031
+ const recoveredAddress = ethers.verifyMessage(
1032
+ messageString, signature
1033
+ )
1034
+
1035
+ return recoveredAddress.toLowerCase() ===
1036
+ signerAddress.toLowerCase()
1037
+ }
1038
+
1039
+ return false
1040
+
1041
+ } catch (error) {
1042
+ return false
1043
+ }
1044
+ }
1045
+
1046
+ export const utilsStatus = (): void => {
1047
+ console.log("spasm.js utils status: success")
1048
+ }
1049
+
1050
+ // var sha256pure = function sha256(ascii) {
1051
+ // function rightRotate(value, amount) {
1052
+ // return (value>>>amount) | (value<<(32 - amount));
1053
+ // };
1054
+ //
1055
+ // var mathPow = Math.pow;
1056
+ // var maxWord = mathPow(2, 32);
1057
+ // var lengthProperty = 'length'
1058
+ // var i, j; // Used as a counter across the whole file
1059
+ // var result = ''
1060
+ //
1061
+ // var words = [];
1062
+ // var asciiBitLength = ascii[lengthProperty]*8;
1063
+ //
1064
+ // /[> caching results is optional - remove/add slash from front of this line to toggle
1065
+ // // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes
1066
+ // // (we actually calculate the first 64, but extra values are just ignored)
1067
+ // var hash = sha256.h = sha256.h || [];
1068
+ // // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes
1069
+ // var k = sha256.k = sha256.k || [];
1070
+ // var primeCounter = k[lengthProperty];
1071
+ // [>/
1072
+ // var hash = [], k = [];
1073
+ // var primeCounter = 0;
1074
+ // /[>/
1075
+ //
1076
+ // var isComposite = {};
1077
+ // for (var candidate = 2; primeCounter < 64; candidate++) {
1078
+ // if (!isComposite[candidate]) {
1079
+ // for (i = 0; i < 313; i += candidate) {
1080
+ // isComposite[i] = candidate;
1081
+ // }
1082
+ // hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0;
1083
+ // k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0;
1084
+ // }
1085
+ // }
1086
+ //
1087
+ // ascii += '\x80' // Append Ƈ' bit (plus zero padding)
1088
+ // while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding
1089
+ // for (i = 0; i < ascii[lengthProperty]; i++) {
1090
+ // j = ascii.charCodeAt(i);
1091
+ // if (j>>8) return; // ASCII check: only accept characters in range 0-255
1092
+ // words[i>>2] |= j << ((3 - i)%4)*8;
1093
+ // }
1094
+ // words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0);
1095
+ // words[words[lengthProperty]] = (asciiBitLength)
1096
+ //
1097
+ // // process each chunk
1098
+ // for (j = 0; j < words[lengthProperty];) {
1099
+ // var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration
1100
+ // var oldHash = hash;
1101
+ // // This is now the undefinedworking hash", often labelled as variables a...g
1102
+ // // (we have to truncate as well, otherwise extra entries at the end accumulate
1103
+ // hash = hash.slice(0, 8);
1104
+ //
1105
+ // for (i = 0; i < 64; i++) {
1106
+ // // Typescript error: i2 value is never read
1107
+ // var i2 = i + j;
1108
+ // // Expand the message into 64 words
1109
+ // // Used below if
1110
+ // var w15 = w[i - 15], w2 = w[i - 2];
1111
+ //
1112
+ // // Iterate
1113
+ // var a = hash[0], e = hash[4];
1114
+ // var temp1 = hash[7]
1115
+ // + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1
1116
+ // + ((e&hash[5])^((~e)&hash[6])) // ch
1117
+ // + k[i]
1118
+ // // Expand the message schedule if needed
1119
+ // + (w[i] = (i < 16) ? w[i] : (
1120
+ // w[i - 16]
1121
+ // + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0
1122
+ // + w[i - 7]
1123
+ // + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1
1124
+ // )|0
1125
+ // );
1126
+ // // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble
1127
+ // var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0
1128
+ // + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj
1129
+ //
1130
+ // hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice()
1131
+ // hash[4] = (hash[4] + temp1)|0;
1132
+ // }
1133
+ //
1134
+ // for (i = 0; i < 8; i++) {
1135
+ // hash[i] = (hash[i] + oldHash[i])|0;
1136
+ // }
1137
+ // }
1138
+ //
1139
+ // for (i = 0; i < 8; i++) {
1140
+ // for (j = 3; j + 1; j--) {
1141
+ // var b = (hash[i]>>(j*8))&255;
1142
+ // result += ((b < 16) ? 0 : '') + b.toString(16);
1143
+ // }
1144
+ // }
1145
+ // return result;
1146
+ // };