applesauce-common 0.0.0-next-20251209200210 → 0.0.0-next-20251231055351

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 (116) hide show
  1. package/README.md +45 -4
  2. package/dist/blueprints/__register__.d.ts +7 -0
  3. package/dist/blueprints/__register__.js +8 -0
  4. package/dist/blueprints/comment.d.ts +3 -2
  5. package/dist/blueprints/group-mangement.d.ts +25 -0
  6. package/dist/blueprints/group-mangement.js +40 -0
  7. package/dist/blueprints/index.d.ts +1 -0
  8. package/dist/blueprints/index.js +1 -0
  9. package/dist/blueprints/torrent.d.ts +7 -0
  10. package/dist/blueprints/torrent.js +5 -1
  11. package/dist/casts/article.d.ts +19 -0
  12. package/dist/casts/article.js +47 -0
  13. package/dist/casts/bookmarks.d.ts +35 -0
  14. package/dist/casts/bookmarks.js +91 -0
  15. package/dist/casts/cast.d.ts +30 -0
  16. package/dist/casts/cast.js +67 -0
  17. package/dist/casts/comment.d.ts +18 -0
  18. package/dist/casts/comment.js +54 -0
  19. package/dist/casts/groups.d.ts +19 -0
  20. package/dist/casts/groups.js +43 -0
  21. package/dist/casts/index.d.ts +18 -0
  22. package/dist/casts/index.js +18 -0
  23. package/dist/casts/mutes.d.ts +23 -0
  24. package/dist/casts/mutes.js +54 -0
  25. package/dist/casts/note.d.ts +25 -0
  26. package/dist/casts/note.js +76 -0
  27. package/dist/casts/profile.d.ts +24 -0
  28. package/dist/casts/profile.js +52 -0
  29. package/dist/casts/reaction.d.ts +17 -0
  30. package/dist/casts/reaction.js +46 -0
  31. package/dist/casts/relay-discovery.d.ts +29 -0
  32. package/dist/casts/relay-discovery.js +54 -0
  33. package/dist/casts/relay-lists.d.ts +33 -0
  34. package/dist/casts/relay-lists.js +72 -0
  35. package/dist/casts/relay-monitor.d.ts +21 -0
  36. package/dist/casts/relay-monitor.js +41 -0
  37. package/dist/casts/report.d.ts +31 -0
  38. package/dist/casts/report.js +74 -0
  39. package/dist/casts/share.d.ts +15 -0
  40. package/dist/casts/share.js +34 -0
  41. package/dist/casts/stream.d.ts +43 -0
  42. package/dist/casts/stream.js +116 -0
  43. package/dist/casts/torrent.d.ts +31 -0
  44. package/dist/casts/torrent.js +62 -0
  45. package/dist/casts/user.d.ts +40 -0
  46. package/dist/casts/user.js +181 -0
  47. package/dist/casts/zap.d.ts +17 -0
  48. package/dist/casts/zap.js +47 -0
  49. package/dist/helpers/bookmark.d.ts +18 -17
  50. package/dist/helpers/bookmark.js +36 -49
  51. package/dist/helpers/calendar-event.d.ts +7 -1
  52. package/dist/helpers/calendar-event.js +8 -10
  53. package/dist/helpers/channels.d.ts +1 -1
  54. package/dist/helpers/channels.js +5 -8
  55. package/dist/helpers/comment.d.ts +3 -1
  56. package/dist/helpers/comment.js +12 -2
  57. package/dist/helpers/encrypted-content-cache.js +23 -25
  58. package/dist/helpers/external-id.d.ts +32 -0
  59. package/dist/helpers/external-id.js +85 -0
  60. package/dist/helpers/file-metadata.d.ts +1 -4
  61. package/dist/helpers/file-metadata.js +1 -4
  62. package/dist/helpers/gift-wrap.js +11 -5
  63. package/dist/helpers/groups.d.ts +129 -7
  64. package/dist/helpers/groups.js +317 -15
  65. package/dist/helpers/index.d.ts +1 -1
  66. package/dist/helpers/index.js +1 -1
  67. package/dist/helpers/lists.d.ts +0 -1
  68. package/dist/helpers/lists.js +4 -5
  69. package/dist/helpers/mute.d.ts +14 -11
  70. package/dist/helpers/mute.js +9 -4
  71. package/dist/helpers/relay-list.d.ts +14 -0
  72. package/dist/helpers/relay-list.js +18 -0
  73. package/dist/helpers/reports.d.ts +4 -1
  74. package/dist/helpers/reports.js +14 -10
  75. package/dist/helpers/stream-chat.d.ts +4 -1
  76. package/dist/helpers/stream-chat.js +4 -1
  77. package/dist/index.d.ts +1 -0
  78. package/dist/index.js +1 -0
  79. package/dist/models/__register__.d.ts +5 -0
  80. package/dist/models/__register__.js +6 -0
  81. package/dist/models/blossom.d.ts +2 -2
  82. package/dist/models/blossom.js +1 -1
  83. package/dist/models/bookmarks.d.ts +3 -5
  84. package/dist/models/bookmarks.js +2 -10
  85. package/dist/models/channels.js +3 -9
  86. package/dist/models/comments.d.ts +3 -2
  87. package/dist/models/comments.js +19 -1
  88. package/dist/models/index.d.ts +3 -1
  89. package/dist/models/index.js +4 -1
  90. package/dist/models/mutes.d.ts +5 -5
  91. package/dist/models/{relays.js → relay-lists.js} +2 -1
  92. package/dist/models/shares.d.ts +3 -0
  93. package/dist/models/shares.js +5 -0
  94. package/dist/models/thread.js +30 -24
  95. package/dist/observable/cast-stream.d.ts +8 -0
  96. package/dist/observable/cast-stream.js +29 -0
  97. package/dist/observable/chainable.d.ts +50 -0
  98. package/dist/observable/chainable.js +79 -0
  99. package/dist/observable/index.d.ts +2 -0
  100. package/dist/observable/index.js +2 -0
  101. package/dist/operations/comment.d.ts +3 -2
  102. package/dist/operations/comment.js +19 -5
  103. package/dist/operations/group.d.ts +14 -1
  104. package/dist/operations/group.js +42 -4
  105. package/dist/operations/index.d.ts +1 -1
  106. package/dist/operations/index.js +1 -1
  107. package/dist/operations/tag/bookmarks.d.ts +3 -2
  108. package/dist/operations/tag/bookmarks.js +34 -14
  109. package/dist/operations/torrent.d.ts +2 -0
  110. package/dist/operations/torrent.js +4 -0
  111. package/dist/register.d.ts +2 -11
  112. package/dist/register.js +2 -11
  113. package/package.json +12 -2
  114. package/dist/helpers/mailboxes.d.ts +0 -7
  115. package/dist/helpers/mailboxes.js +0 -49
  116. /package/dist/models/{relays.d.ts → relay-lists.d.ts} +0 -0
@@ -0,0 +1,47 @@
1
+ import { of } from "rxjs";
2
+ import { getZapAddressPointer, getZapAmount, getZapEventPointer, getZapPayment, getZapPreimage, getZapRecipient, getZapRequest, getZapSender, isValidZap, } from "../helpers/zap.js";
3
+ import { EventCast } from "./cast.js";
4
+ import { castUser } from "./user.js";
5
+ // NOTE: extending BaseCast since there is no need for author$ or comments$
6
+ /** Cast a kind 9735 event to a Zap */
7
+ export class Zap extends EventCast {
8
+ constructor(event, store) {
9
+ if (!isValidZap(event))
10
+ throw new Error("Invalid zap");
11
+ super(event, store);
12
+ }
13
+ get sender() {
14
+ return castUser(getZapSender(this.event), this.store);
15
+ }
16
+ get recipient() {
17
+ return castUser(getZapRecipient(this.event), this.store);
18
+ }
19
+ get payment() {
20
+ return getZapPayment(this.event);
21
+ }
22
+ get amount() {
23
+ return getZapAmount(this.event);
24
+ }
25
+ get preimage() {
26
+ return getZapPreimage(this.event);
27
+ }
28
+ get request() {
29
+ return getZapRequest(this.event);
30
+ }
31
+ get addressPointer() {
32
+ return getZapAddressPointer(this.event);
33
+ }
34
+ get eventPointer() {
35
+ return getZapEventPointer(this.event);
36
+ }
37
+ /** An observable of the zapped event */
38
+ get event$() {
39
+ return this.$$ref("event$", (store) => {
40
+ if (this.addressPointer)
41
+ return store.replaceable(this.addressPointer);
42
+ if (this.eventPointer)
43
+ return store.event(this.eventPointer.id);
44
+ return of(undefined);
45
+ });
46
+ }
47
+ }
@@ -1,30 +1,31 @@
1
- import { NostrEvent } from "applesauce-core/helpers/event";
1
+ import { kinds, KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
2
2
  import { HiddenContentSigner } from "applesauce-core/helpers/hidden-content";
3
3
  import { AddressPointer, EventPointer } from "applesauce-core/helpers/pointers";
4
+ /** Type for a validated bookmark list event */
5
+ export type BookmarkListEvent = KnownEvent<kinds.BookmarkList>;
6
+ /** Type for a validated bookmark set event */
7
+ export type BookmarkSetEvent = KnownEvent<kinds.Bookmarksets>;
8
+ /** Validates that an event is a valid bookmark list (kind 10000) */
9
+ export declare function isValidBookmarkList(event: NostrEvent): event is BookmarkListEvent;
10
+ /** Validates that an event is a valid bookmark set (kind 30003) */
11
+ export declare function isValidBookmarkSet(event: NostrEvent): event is BookmarkSetEvent;
4
12
  export declare const BookmarkPublicSymbol: unique symbol;
5
13
  export declare const BookmarkHiddenSymbol: unique symbol;
14
+ export type BookmarkPointer = EventPointer | AddressPointer;
6
15
  /** Type for unlocked bookmarks events */
7
16
  export type UnlockedBookmarks = {
8
- [BookmarkHiddenSymbol]: Bookmarks;
9
- };
10
- export type Bookmarks = {
11
- notes: EventPointer[];
12
- articles: AddressPointer[];
13
- hashtags: string[];
14
- urls: string[];
17
+ [BookmarkHiddenSymbol]: BookmarkPointer[];
15
18
  };
16
19
  /** Parses an array of tags into a {@link Bookmarks} object */
17
- export declare function parseBookmarkTags(tags: string[][]): Bookmarks;
20
+ export declare function parseBookmarkTags(tags: string[][]): BookmarkPointer[];
18
21
  /** Merges any number of {@link Bookmarks} objects */
19
- export declare function mergeBookmarks(...bookmarks: (Bookmarks | undefined)[]): Bookmarks;
20
- /** Returns all the bookmarks of the event */
21
- export declare function getBookmarks(bookmark: NostrEvent): Bookmarks;
22
- /** Returns the public bookmarks of the event */
23
- export declare function getPublicBookmarks(bookmark: NostrEvent): Bookmarks;
22
+ export declare function mergeBookmarks(...bookmarks: (BookmarkPointer[] | undefined)[]): BookmarkPointer[];
23
+ /** Returns the bookmarks of the event */
24
+ export declare function getBookmarks(bookmark: NostrEvent): BookmarkPointer[];
24
25
  /** Checks if the hidden bookmarks are unlocked */
25
26
  export declare function isHiddenBookmarksUnlocked<T extends NostrEvent>(bookmark: T): bookmark is T & UnlockedBookmarks;
26
27
  /** Returns the bookmarks of the event if its unlocked */
27
- export declare function getHiddenBookmarks<T extends NostrEvent & UnlockedBookmarks>(bookmark: T): Bookmarks;
28
- export declare function getHiddenBookmarks<T extends NostrEvent>(bookmark: T): Bookmarks | undefined;
28
+ export declare function getHiddenBookmarks<T extends NostrEvent & UnlockedBookmarks>(bookmark: T): BookmarkPointer[];
29
+ export declare function getHiddenBookmarks<T extends NostrEvent>(bookmark: T): BookmarkPointer[] | undefined;
29
30
  /** Unlocks the hidden bookmarks on a bookmarks event */
30
- export declare function unlockHiddenBookmarks(bookmark: NostrEvent, signer: HiddenContentSigner): Promise<Bookmarks>;
31
+ export declare function unlockHiddenBookmarks(bookmark: NostrEvent, signer: HiddenContentSigner): Promise<BookmarkPointer[]>;
@@ -1,78 +1,65 @@
1
- import { getOrComputeCachedValue, notifyEventUpdate } from "applesauce-core/helpers";
1
+ import { getOrComputeCachedValue, isATag, isETag, notifyEventUpdate, processTags } from "applesauce-core/helpers";
2
2
  import { kinds } from "applesauce-core/helpers/event";
3
3
  import { getHiddenTags, isHiddenTagsUnlocked, unlockHiddenTags } from "applesauce-core/helpers/hidden-tags";
4
- import { getAddressPointerFromATag, getReplaceableAddressFromPointer, getEventPointerFromETag, mergeAddressPointers, mergeEventPointers, } from "applesauce-core/helpers/pointers";
4
+ import { getAddressPointerFromATag, getEventPointerFromETag, getReplaceableAddressFromPointer, isAddressPointer, isEventPointer, mergeAddressPointers, mergeEventPointers, } from "applesauce-core/helpers/pointers";
5
+ /** Validates that an event is a valid bookmark list (kind 10000) */
6
+ export function isValidBookmarkList(event) {
7
+ return event.kind === kinds.BookmarkList;
8
+ }
9
+ /** Validates that an event is a valid bookmark set (kind 30003) */
10
+ export function isValidBookmarkSet(event) {
11
+ return event.kind === kinds.Bookmarksets;
12
+ }
5
13
  export const BookmarkPublicSymbol = Symbol.for("bookmark-public");
6
14
  export const BookmarkHiddenSymbol = Symbol.for("bookmark-hidden");
7
15
  /** Parses an array of tags into a {@link Bookmarks} object */
8
16
  export function parseBookmarkTags(tags) {
9
- const notes = tags
10
- .filter((t) => t[0] === "e" && t[1])
11
- .map(getEventPointerFromETag)
12
- .filter((pointer) => pointer !== null);
13
- const articles = tags
14
- .filter((t) => t[0] === "a" && t[1])
15
- .map(getAddressPointerFromATag)
16
- .filter((pointer) => pointer !== null)
17
- .filter((pointer) => pointer.kind === kinds.LongFormArticle);
18
- const hashtags = tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1]);
19
- const urls = tags.filter((t) => t[0] === "r" && t[1]).map((t) => t[1]);
20
- return { notes, articles, hashtags, urls };
17
+ return processTags(tags, (t) => {
18
+ if (isETag(t))
19
+ return getEventPointerFromETag(t) ?? undefined;
20
+ if (isATag(t)) {
21
+ const pointer = getAddressPointerFromATag(t) ?? undefined;
22
+ // Ensure the address pointer is a long form article
23
+ if (pointer?.kind !== kinds.LongFormArticle)
24
+ return undefined;
25
+ return pointer;
26
+ }
27
+ return undefined;
28
+ });
21
29
  }
22
30
  /** Merges any number of {@link Bookmarks} objects */
23
31
  export function mergeBookmarks(...bookmarks) {
24
32
  const notes = new Map();
25
33
  const articles = new Map();
26
- const hashtags = new Set();
27
- const urls = new Set();
28
- for (const bookmark of bookmarks) {
29
- if (!bookmark)
30
- continue;
31
- for (const note of bookmark.notes) {
32
- const existing = notes.get(note.id);
34
+ for (const pointer of bookmarks.flat()) {
35
+ if (isEventPointer(pointer)) {
36
+ const existing = notes.get(pointer.id);
33
37
  if (existing)
34
- notes.set(note.id, mergeEventPointers(existing, note));
38
+ notes.set(pointer.id, mergeEventPointers(existing, pointer));
35
39
  else
36
- notes.set(note.id, note);
40
+ notes.set(pointer.id, pointer);
37
41
  }
38
- for (const article of bookmark.articles) {
39
- const coord = getReplaceableAddressFromPointer(article);
40
- const existing = articles.get(coord);
42
+ else if (isAddressPointer(pointer)) {
43
+ const address = getReplaceableAddressFromPointer(pointer);
44
+ const existing = articles.get(address);
41
45
  if (existing)
42
- articles.set(coord, mergeAddressPointers(existing, article));
46
+ articles.set(address, mergeAddressPointers(existing, pointer));
43
47
  else
44
- articles.set(coord, article);
48
+ articles.set(address, pointer);
45
49
  }
46
- for (const hashtag of bookmark.hashtags)
47
- hashtags.add(hashtag);
48
- for (const url of bookmark.urls)
49
- urls.add(url);
50
50
  }
51
- return {
52
- notes: Array.from(notes.values()),
53
- articles: Array.from(articles.values()),
54
- hashtags: Array.from(hashtags),
55
- urls: Array.from(urls),
56
- };
51
+ return [...notes.values(), ...articles.values()];
57
52
  }
58
- /** Returns all the bookmarks of the event */
53
+ /** Returns the bookmarks of the event */
59
54
  export function getBookmarks(bookmark) {
60
- const hidden = getHiddenBookmarks(bookmark);
61
- if (hidden)
62
- return mergeBookmarks(hidden, getPublicBookmarks(bookmark));
63
- else
64
- return getPublicBookmarks(bookmark);
65
- }
66
- /** Returns the public bookmarks of the event */
67
- export function getPublicBookmarks(bookmark) {
68
55
  return getOrComputeCachedValue(bookmark, BookmarkPublicSymbol, () => parseBookmarkTags(bookmark.tags));
69
56
  }
70
57
  /** Checks if the hidden bookmarks are unlocked */
71
58
  export function isHiddenBookmarksUnlocked(bookmark) {
72
- return isHiddenTagsUnlocked(bookmark) && Reflect.has(bookmark, BookmarkHiddenSymbol);
59
+ return (isHiddenTagsUnlocked(bookmark) && (BookmarkHiddenSymbol in bookmark || getHiddenBookmarks(bookmark) !== undefined));
73
60
  }
74
61
  export function getHiddenBookmarks(bookmark) {
75
- if (isHiddenBookmarksUnlocked(bookmark))
62
+ if (BookmarkHiddenSymbol in bookmark)
76
63
  return bookmark[BookmarkHiddenSymbol];
77
64
  //get hidden tags
78
65
  const tags = getHiddenTags(bookmark);
@@ -1,8 +1,14 @@
1
- import { NostrEvent } from "applesauce-core/helpers/event";
1
+ import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
2
2
  import { ProfilePointer } from "applesauce-core/helpers/pointers";
3
3
  import { NameValueTag } from "applesauce-core/helpers/tags";
4
4
  export declare const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
5
5
  export declare const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
6
+ export type DateBasedCalendarEvent = KnownEvent<typeof DATE_BASED_CALENDAR_EVENT_KIND>;
7
+ export type TimeBasedCalendarEvent = KnownEvent<typeof TIME_BASED_CALENDAR_EVENT_KIND>;
8
+ /** Checks if an event is a date-based calendar event */
9
+ export declare function isValidDateBasedCalendarEvent(event: NostrEvent): event is DateBasedCalendarEvent;
10
+ /** Checks if an event is a time-based calendar event */
11
+ export declare function isValidTimeBasedCalendarEvent(event: NostrEvent): event is TimeBasedCalendarEvent;
6
12
  export type CalendarEventParticipant = ProfilePointer & {
7
13
  role?: string;
8
14
  };
@@ -6,6 +6,14 @@ import { fillAndTrimTag, isPTag, isRTag, isTTag } from "applesauce-core/helpers/
6
6
  // NIP-52 Calendar Event Kinds
7
7
  export const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
8
8
  export const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
9
+ /** Checks if an event is a date-based calendar event */
10
+ export function isValidDateBasedCalendarEvent(event) {
11
+ return event.kind === DATE_BASED_CALENDAR_EVENT_KIND;
12
+ }
13
+ /** Checks if an event is a time-based calendar event */
14
+ export function isValidTimeBasedCalendarEvent(event) {
15
+ return event.kind === TIME_BASED_CALENDAR_EVENT_KIND;
16
+ }
9
17
  // Cache symbols for complex operations only
10
18
  export const CalendarEventLocationsSymbol = Symbol.for("calendar-event-locations");
11
19
  export const CalendarEventParticipantsSymbol = Symbol.for("calendar-event-participants");
@@ -62,16 +70,12 @@ export function getCalendarEventEnd(event) {
62
70
  }
63
71
  /** Gets all locations from a calendar event */
64
72
  export function getCalendarEventLocations(event) {
65
- if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
66
- throw new Error("Event is not a date-based or time-based calendar event");
67
73
  return getOrComputeCachedValue(event, CalendarEventLocationsSymbol, () => {
68
74
  return event.tags.filter((t) => t[0] === "location" && t[1]).map((t) => t[1]);
69
75
  });
70
76
  }
71
77
  /** Gets the geohash of a calendar event */
72
78
  export function getCalendarEventGeohash(event) {
73
- if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
74
- throw new Error("Event is not a date-based or time-based calendar event");
75
79
  return getOrComputeCachedValue(event, CalendarEventGeohashSymbol, () => {
76
80
  let hash = undefined;
77
81
  for (const tag of event.tags) {
@@ -83,8 +87,6 @@ export function getCalendarEventGeohash(event) {
83
87
  }
84
88
  /** Gets all participants from a calendar event */
85
89
  export function getCalendarEventParticipants(event) {
86
- if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
87
- throw new Error("Event is not a date-based or time-based calendar event");
88
90
  return getOrComputeCachedValue(event, CalendarEventParticipantsSymbol, () => {
89
91
  return event.tags
90
92
  .filter(isPTag)
@@ -102,16 +104,12 @@ export function getCalendarEventParticipants(event) {
102
104
  }
103
105
  /** Gets all hashtags from a calendar event */
104
106
  export function getCalendarEventHashtags(event) {
105
- if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
106
- throw new Error("Event is not a date-based or time-based calendar event");
107
107
  return getOrComputeCachedValue(event, CalendarEventHashtagsSymbol, () => {
108
108
  return event.tags.filter(isTTag).map((t) => t[1]);
109
109
  });
110
110
  }
111
111
  /** Gets all references from a calendar event */
112
112
  export function getCalendarEventReferences(event) {
113
- if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
114
- throw new Error("Event is not a date-based or time-based calendar event");
115
113
  return getOrComputeCachedValue(event, CalendarEventReferencesSymbol, () => {
116
114
  return event.tags.filter(isRTag).map((t) => t[1]);
117
115
  });
@@ -8,6 +8,6 @@ export type ChannelMetadataContent = {
8
8
  relays?: string[];
9
9
  };
10
10
  /** Gets the parsed metadata on a channel creation or channel metadata event */
11
- export declare function getChannelMetadataContent(channel: NostrEvent): ChannelMetadataContent;
11
+ export declare function getChannelMetadataContent(channel: NostrEvent): ChannelMetadataContent | null;
12
12
  /** gets the EventPointer for a channel message or metadata event */
13
13
  export declare function getChannelPointer(event: NostrEvent): EventPointer | undefined;
@@ -2,14 +2,11 @@ import { getOrComputeCachedValue } from "applesauce-core/helpers/cache";
2
2
  export const ChannelMetadataSymbol = Symbol.for("channel-metadata");
3
3
  function parseChannelMetadataContent(channel) {
4
4
  const metadata = JSON.parse(channel.content);
5
- if (metadata.name === undefined)
6
- throw new Error("Missing name");
7
- if (metadata.about === undefined)
8
- throw new Error("Missing about");
9
- if (metadata.picture === undefined)
10
- throw new Error("Missing picture");
11
- if (metadata.relays && !Array.isArray(metadata.relays))
12
- throw new Error("Invalid relays");
5
+ if (metadata.name === undefined ||
6
+ metadata.about === undefined ||
7
+ metadata.picture === undefined ||
8
+ (metadata.relays && !Array.isArray(metadata.relays)))
9
+ return null;
13
10
  return metadata;
14
11
  }
15
12
  /** Gets the parsed metadata on a channel creation or channel metadata event */
@@ -1,5 +1,5 @@
1
1
  import { KnownEvent, NostrEvent } from "applesauce-core/helpers/event";
2
- import { ExternalIdentifiers, ExternalPointer } from "applesauce-core/helpers/external-id";
2
+ import { ExternalIdentifiers, ExternalPointer } from "./external-id.js";
3
3
  export declare const COMMENT_KIND = 1111;
4
4
  /** Type for validated comment events */
5
5
  export type CommentEvent = KnownEvent<typeof COMMENT_KIND>;
@@ -39,6 +39,8 @@ export declare function getCommentReplyPointer(comment: NostrEvent): CommentPoin
39
39
  export declare function isCommentEventPointer(pointer: any): pointer is CommentEventPointer;
40
40
  /** Checks if a pointer is a {@link CommentAddressPointer} */
41
41
  export declare function isCommentAddressPointer(pointer: any): pointer is CommentAddressPointer;
42
+ /** Checks if a pointer is a {@link CommentExternalPointer} */
43
+ export declare function isCommentExternalPointer(pointer: any): pointer is CommentExternalPointer<keyof ExternalIdentifiers>;
42
44
  /** Checks if a comment event is valid */
43
45
  export declare function isValidComment(comment: NostrEvent): comment is CommentEvent;
44
46
  /** Create a set fo tags for a single CommentPointer */
@@ -1,9 +1,9 @@
1
1
  import { getOrComputeCachedValue } from "applesauce-core/helpers/cache";
2
2
  import { createReplaceableAddress, getTagValue, isAddressableKind, } from "applesauce-core/helpers/event";
3
- import { getExternalPointerFromTag } from "applesauce-core/helpers/external-id";
4
3
  import { getAddressPointerFromATag } from "applesauce-core/helpers/pointers";
5
4
  import { isSafeRelayURL } from "applesauce-core/helpers/relays";
6
5
  import { fillAndTrimTag } from "applesauce-core/helpers/tags";
6
+ import { getExternalPointerFromTag } from "./external-id.js";
7
7
  export const COMMENT_KIND = 1111;
8
8
  export const CommentRootPointerSymbol = Symbol.for("comment-root-pointer");
9
9
  export const CommentReplyPointerSymbol = Symbol.for("comment-reply-pointer");
@@ -56,9 +56,12 @@ export function getCommentAddressPointer(tags, root = false) {
56
56
  export function getCommentExternalPointer(tags, root = false) {
57
57
  const iTag = tags.find((t) => t[0] === (root ? "I" : "i"));
58
58
  if (iTag) {
59
+ const pointer = getExternalPointerFromTag(iTag);
60
+ if (!pointer)
61
+ return null;
59
62
  return {
60
63
  type: "external",
61
- ...getExternalPointerFromTag(iTag),
64
+ ...pointer,
62
65
  };
63
66
  }
64
67
  return null;
@@ -112,6 +115,13 @@ export function isCommentAddressPointer(pointer) {
112
115
  Reflect.has(pointer, "kind") &&
113
116
  typeof pointer.kind === "number");
114
117
  }
118
+ /** Checks if a pointer is a {@link CommentExternalPointer} */
119
+ export function isCommentExternalPointer(pointer) {
120
+ return (pointer?.type === "external" &&
121
+ Reflect.has(pointer, "kind") &&
122
+ Reflect.has(pointer, "identifier") &&
123
+ typeof pointer.kind === "string");
124
+ }
115
125
  /** Checks if a comment event is valid */
116
126
  export function isValidComment(comment) {
117
127
  return (comment.kind === COMMENT_KIND && getCommentRootPointer(comment) !== null && getCommentReplyPointer(comment) !== null);
@@ -1,8 +1,8 @@
1
1
  import { logger } from "applesauce-core";
2
2
  import { canHaveEncryptedContent, getEncryptedContent, isEncryptedContentUnlocked, setEncryptedContentCache, } from "applesauce-core/helpers/encrypted-content";
3
3
  import { kinds, notifyEventUpdate } from "applesauce-core/helpers/event";
4
- import { catchError, combineLatest, distinct, EMPTY, filter, isObservable, map, merge, mergeMap, of, switchMap, } from "rxjs";
5
- import { getGiftWrapSeal, getSealGiftWrap, getSealRumor } from "./gift-wrap.js";
4
+ import { catchError, combineLatest, combineLatestWith, distinct, EMPTY, filter, isObservable, map, merge, mergeMap, of, switchMap, } from "rxjs";
5
+ import { getGiftWrapSeal, getSealGiftWrap, getSealRumor, isGiftWrapUnlocked } from "./gift-wrap.js";
6
6
  /** A symbol that is used to mark encrypted content as being from a cache */
7
7
  export const EncryptedContentFromCacheSymbol = Symbol.for("encrypted-content-from-cache");
8
8
  /** Marks the encrypted content as being from a cache */
@@ -32,15 +32,20 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
32
32
  .pipe(
33
33
  // Look for events that support encrypted content and are locked
34
34
  filter((e) => canHaveEncryptedContent(e.kind) && isEncryptedContentUnlocked(e) === false),
35
+ // Get the storage
36
+ combineLatestWith(storage$),
35
37
  // Get the encrypted content from storage
36
- mergeMap((event) =>
37
- // Wait for storage to be available
38
- storage$.pipe(switchMap((storage) => combineLatest([of(event), getItem(storage, event)])), catchError((error) => {
39
- log(`Failed to restore encrypted content for ${event.id}`, error);
40
- return EMPTY;
41
- }))))
38
+ mergeMap(([event, storage]) =>
39
+ // Get content from storage
40
+ combineLatest([
41
+ of(event),
42
+ getItem(storage, event).catch((error) => {
43
+ log(`Failed to restore encrypted content for ${event.id}`, error);
44
+ return of(null);
45
+ }),
46
+ ])))
42
47
  .subscribe(async ([event, content]) => {
43
- if (!content)
48
+ if (typeof content !== "string")
44
49
  return;
45
50
  // Restore the encrypted content and set it as from a cache
46
51
  markEncryptedContentFromCache(event);
@@ -79,14 +84,16 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
79
84
  log(`Restored encrypted content for ${seal.id}`);
80
85
  });
81
86
  // Persist encrypted content when it is updated or inserted
82
- const persist = combineLatest([merge(eventStore.update$, eventStore.insert$), storage$])
87
+ const persist = merge(eventStore.update$, eventStore.insert$)
83
88
  .pipe(
84
89
  // Look for events that support encrypted content and are unlocked and not from the cache
85
- filter(([event]) => canHaveEncryptedContent(event.kind) &&
90
+ filter((event) => canHaveEncryptedContent(event.kind) &&
86
91
  isEncryptedContentUnlocked(event) &&
87
- !isEncryptedContentFromCache(event)),
92
+ isEncryptedContentFromCache(event) === false),
88
93
  // Only persist the encrypted content once
89
- distinct(([event]) => event.id))
94
+ distinct((event) => event.id),
95
+ // get the storage
96
+ combineLatestWith(storage$))
90
97
  .subscribe(async ([event, storage]) => {
91
98
  try {
92
99
  const content = getEncryptedContent(event);
@@ -102,18 +109,9 @@ export function persistEncryptedContent(eventStore, storage, fallback) {
102
109
  });
103
110
  // Persist seals when the gift warp is unlocked or inserted unlocked
104
111
  // This relies on the gift wrap event being updated when a seal is unlocked
105
- const persistSeals = combineLatest([merge(eventStore.update$, eventStore.insert$), storage$])
106
- .pipe(
107
- // Look for gift wraps that are unlocked
108
- filter(([event]) => event.kind === kinds.GiftWrap && isEncryptedContentUnlocked(event)),
109
- // Get the seal event
110
- map(([gift, storage]) => [getGiftWrapSeal(gift), storage]),
111
- // Make sure the seal is defined
112
- filter(([seal]) => seal !== undefined),
113
- // Make sure seal is unlocked and not from cache
114
- filter(([seal]) => isEncryptedContentUnlocked(seal) && !isEncryptedContentFromCache(seal)),
115
- // Only persist the seal once
116
- distinct(([seal]) => seal.id))
112
+ const unlockedSeals$ = merge(eventStore.update$, eventStore.insert$).pipe(filter((event) => event.kind === kinds.GiftWrap), filter(isGiftWrapUnlocked), map((gift) => getGiftWrapSeal(gift)), distinct((seal) => seal.id));
113
+ const persistSeals = unlockedSeals$
114
+ .pipe(filter((seal) => isEncryptedContentFromCache(seal) === false), combineLatestWith(storage$))
117
115
  .subscribe(async ([seal, storage]) => {
118
116
  if (!seal)
119
117
  return;
@@ -0,0 +1,32 @@
1
+ export type ExternalIdentifiers = {
2
+ web: string;
3
+ "#": `#${string}`;
4
+ geo: `geo:${string}`;
5
+ isbn: `isbn:${string}`;
6
+ "podcast:guid": `podcast:guid:${string}`;
7
+ "podcast:item:guid": `podcast:item:guid:${string}`;
8
+ "podcast:publisher:guid": `podcast:publisher:guid:${string}`;
9
+ isan: `isan:${string}`;
10
+ doi: `doi:${string}`;
11
+ "bitcoin:tx": `bitcoin:tx:${string}`;
12
+ "bitcoin:address": `bitcoin:address:${string}`;
13
+ "ethereum:tx": `ethereum:${string}:tx:${string}`;
14
+ "ethereum:address": `ethereum:${string}:address:${string}`;
15
+ [key: `${string}:tx`]: `${string}:tx:${string}`;
16
+ [key: `${string}:address`]: `${string}:address:${string}`;
17
+ };
18
+ export type ExternalPointer<Prefix extends keyof ExternalIdentifiers> = {
19
+ kind: Prefix;
20
+ identifier: ExternalIdentifiers[Prefix];
21
+ };
22
+ export type ParseResult = {
23
+ [P in keyof ExternalIdentifiers]: ExternalPointer<P>;
24
+ }[keyof ExternalIdentifiers];
25
+ /** Casts a string to a valid external pointer */
26
+ export declare function isValidExternalPointer(identifier: string): identifier is `${keyof ExternalIdentifiers}1${string}`;
27
+ /** Parses a NIP-73 external identifier */
28
+ export declare function parseExternalPointer<Prefix extends keyof ExternalIdentifiers>(identifier: `${Prefix}1${string}`): ExternalPointer<Prefix>;
29
+ export declare function parseExternalPointer(identifier: string): ParseResult | null;
30
+ /** Gets an ExternalPointer for a "i" tag */
31
+ export declare function getExternalPointerFromTag<Prefix extends keyof ExternalIdentifiers>(tag: string[]): ExternalPointer<Prefix> | null;
32
+ export declare function getExternalPointerFromTag(tag: string[]): ParseResult | null;
@@ -0,0 +1,85 @@
1
+ /** Casts a string to a valid external pointer */
2
+ export function isValidExternalPointer(identifier) {
3
+ return parseExternalPointer(identifier) !== null;
4
+ }
5
+ /**
6
+ * Normalizes a URL according to NIP-73:
7
+ * - Removes fragment
8
+ * - Returns the normalized URL string
9
+ */
10
+ function normalizeUrl(url) {
11
+ try {
12
+ const urlObj = new URL(url);
13
+ urlObj.hash = ""; // Remove fragment
14
+ return urlObj.toString();
15
+ }
16
+ catch {
17
+ // If URL parsing fails, return original (will be caught by validation)
18
+ return url;
19
+ }
20
+ }
21
+ export function parseExternalPointer(identifier) {
22
+ // Check explicit prefixes first (these take precedence over URL parsing)
23
+ if (identifier.startsWith("#"))
24
+ return { kind: "#", identifier: identifier };
25
+ if (identifier.startsWith("geo:"))
26
+ return { kind: "geo", identifier: identifier };
27
+ if (identifier.startsWith("isbn:"))
28
+ return { kind: "isbn", identifier: identifier };
29
+ if (identifier.startsWith("podcast:guid:"))
30
+ return { kind: "podcast:guid", identifier: identifier };
31
+ if (identifier.startsWith("podcast:item:guid:"))
32
+ return { kind: "podcast:item:guid", identifier: identifier };
33
+ if (identifier.startsWith("podcast:publisher:guid:"))
34
+ return { kind: "podcast:publisher:guid", identifier: identifier };
35
+ if (identifier.startsWith("isan:"))
36
+ return { kind: "isan", identifier: identifier };
37
+ if (identifier.startsWith("doi:"))
38
+ return { kind: "doi", identifier: identifier };
39
+ // Check for blockchain identifiers
40
+ // Bitcoin: bitcoin:tx:<txid> or bitcoin:address:<address>
41
+ if (identifier.startsWith("bitcoin:tx:")) {
42
+ return { kind: "bitcoin:tx", identifier: identifier };
43
+ }
44
+ if (identifier.startsWith("bitcoin:address:")) {
45
+ return { kind: "bitcoin:address", identifier: identifier };
46
+ }
47
+ // Ethereum: ethereum:<chainId>:tx:<txHash> or ethereum:<chainId>:address:<address>
48
+ const ethereumTxMatch = identifier.match(/^ethereum:(\d+):tx:(.+)$/);
49
+ if (ethereumTxMatch) {
50
+ return { kind: "ethereum:tx", identifier: identifier };
51
+ }
52
+ const ethereumAddressMatch = identifier.match(/^ethereum:(\d+):address:(.+)$/);
53
+ if (ethereumAddressMatch) {
54
+ return { kind: "ethereum:address", identifier: identifier };
55
+ }
56
+ // Other blockchains: <blockchain>:tx:<txid> or <blockchain>:address:<address>
57
+ // Exclude known prefixes to avoid false matches
58
+ const blockchainTxMatch = identifier.match(/^([a-z0-9]+):tx:(.+)$/);
59
+ if (blockchainTxMatch && !identifier.startsWith("bitcoin:") && !identifier.startsWith("ethereum:")) {
60
+ const blockchain = blockchainTxMatch[1];
61
+ return { kind: `${blockchain}:tx`, identifier: identifier };
62
+ }
63
+ const blockchainAddressMatch = identifier.match(/^([a-z0-9]+):address:(.+)$/);
64
+ if (blockchainAddressMatch && !identifier.startsWith("bitcoin:") && !identifier.startsWith("ethereum:")) {
65
+ const blockchain = blockchainAddressMatch[1];
66
+ return { kind: `${blockchain}:address`, identifier: identifier };
67
+ }
68
+ // Check for URL (must be a valid URL, normalized, no fragment)
69
+ // URLs don't have a prefix, so we check if it's a valid URL after checking all prefixes
70
+ try {
71
+ new URL(identifier); // Validate URL
72
+ // Valid URL - normalize it (remove fragment) and return
73
+ const normalized = normalizeUrl(identifier);
74
+ return { kind: "web", identifier: normalized };
75
+ }
76
+ catch {
77
+ // Not a valid URL
78
+ }
79
+ return null;
80
+ }
81
+ export function getExternalPointerFromTag(tag) {
82
+ if (!tag[1])
83
+ return null;
84
+ return parseExternalPointer(tag[1]);
85
+ }
@@ -35,10 +35,7 @@ export type FileMetadata = {
35
35
  };
36
36
  /** Alias for {@link FileMetadata} */
37
37
  export type MediaAttachment = FileMetadata;
38
- /**
39
- * Parses file metadata tags into {@link FileMetadata}
40
- * @throws
41
- */
38
+ /** Parses file metadata tags into {@link FileMetadata} */
42
39
  export declare function parseFileMetadataTags(tags: string[][]): FileMetadata;
43
40
  /** Parses a imeta tag into a {@link FileMetadata} */
44
41
  export declare function getFileMetadataFromImetaTag(tag: string[]): FileMetadata;
@@ -1,8 +1,5 @@
1
1
  import { getOrComputeCachedValue } from "applesauce-core/helpers/cache";
2
- /**
3
- * Parses file metadata tags into {@link FileMetadata}
4
- * @throws
5
- */
2
+ /** Parses file metadata tags into {@link FileMetadata} */
6
3
  export function parseFileMetadataTags(tags) {
7
4
  const fields = {};
8
5
  let fallback = undefined;
@@ -1,4 +1,5 @@
1
1
  import { EventMemory } from "applesauce-core/event-store";
2
+ import { safeParse } from "applesauce-core/helpers";
2
3
  import { getEncryptedContent, isEncryptedContentUnlocked, lockEncryptedContent, unlockEncryptedContent, } from "applesauce-core/helpers/encrypted-content";
3
4
  import { kinds, notifyEventUpdate, verifyWrappedEvent, } from "applesauce-core/helpers/event";
4
5
  /**
@@ -64,7 +65,7 @@ export function getRumorGiftWraps(rumor) {
64
65
  }
65
66
  /** Checks if a seal event is locked and casts it to the {@link UnlockedSeal} type */
66
67
  export function isSealUnlocked(seal) {
67
- return isEncryptedContentUnlocked(seal) === true && Reflect.has(seal, RumorSymbol) === true;
68
+ return RumorSymbol in seal || (isEncryptedContentUnlocked(seal) === true && getSealRumor(seal) !== undefined);
68
69
  }
69
70
  /** Returns if a gift-wrap event or gift-wrap seal is locked */
70
71
  export function isGiftWrapUnlocked(gift) {
@@ -84,7 +85,7 @@ export function getSealRumor(seal) {
84
85
  if (seal.kind !== kinds.Seal)
85
86
  return undefined;
86
87
  // If unlocked return the rumor
87
- if (isSealUnlocked(seal))
88
+ if (RumorSymbol in seal)
88
89
  return seal[RumorSymbol];
89
90
  // Get the encrypted content plaintext
90
91
  const content = getEncryptedContent(seal);
@@ -92,7 +93,12 @@ export function getSealRumor(seal) {
92
93
  if (!content)
93
94
  return undefined;
94
95
  // Parse the content as a rumor event
95
- let rumor = JSON.parse(content);
96
+ let rumor = safeParse(content);
97
+ // Failed to parse rumor, save undefined and return undefined
98
+ if (!rumor) {
99
+ Reflect.set(seal, RumorSymbol, undefined);
100
+ return undefined;
101
+ }
96
102
  // Check if the rumor event already exists in the internal event set
97
103
  const existing = internalGiftWrapEvents.getEvent(rumor.id);
98
104
  if (existing)
@@ -112,8 +118,8 @@ export function getSealRumor(seal) {
112
118
  }
113
119
  export function getGiftWrapSeal(gift) {
114
120
  // Returned cached seal if it exists (downstream)
115
- if (Reflect.has(gift, SealSymbol))
116
- return Reflect.get(gift, SealSymbol);
121
+ if (SealSymbol in gift)
122
+ return gift[SealSymbol];
117
123
  // Get the encrypted content
118
124
  const content = getEncryptedContent(gift);
119
125
  // Return undefined if the content is not found