applesauce-common 0.0.0-next-20251203172109

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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/blueprints/app-data.d.ts +5 -0
  4. package/dist/blueprints/app-data.js +10 -0
  5. package/dist/blueprints/calendar.d.ts +5 -0
  6. package/dist/blueprints/calendar.js +7 -0
  7. package/dist/blueprints/channels.d.ts +6 -0
  8. package/dist/blueprints/channels.js +13 -0
  9. package/dist/blueprints/comment.d.ts +12 -0
  10. package/dist/blueprints/comment.js +15 -0
  11. package/dist/blueprints/delete.d.ts +9 -0
  12. package/dist/blueprints/delete.js +14 -0
  13. package/dist/blueprints/file-metadata.d.ts +8 -0
  14. package/dist/blueprints/file-metadata.js +11 -0
  15. package/dist/blueprints/follow-set.d.ts +11 -0
  16. package/dist/blueprints/follow-set.js +21 -0
  17. package/dist/blueprints/gift-wrap.d.ts +5 -0
  18. package/dist/blueprints/gift-wrap.js +6 -0
  19. package/dist/blueprints/group.d.ts +9 -0
  20. package/dist/blueprints/group.js +17 -0
  21. package/dist/blueprints/highlight.d.ts +20 -0
  22. package/dist/blueprints/highlight.js +12 -0
  23. package/dist/blueprints/index.d.ts +23 -0
  24. package/dist/blueprints/index.js +24 -0
  25. package/dist/blueprints/legacy-message.d.ts +7 -0
  26. package/dist/blueprints/legacy-message.js +29 -0
  27. package/dist/blueprints/live-stream.d.ts +6 -0
  28. package/dist/blueprints/live-stream.js +9 -0
  29. package/dist/blueprints/note.d.ts +16 -0
  30. package/dist/blueprints/note.js +30 -0
  31. package/dist/blueprints/picture-post.d.ts +12 -0
  32. package/dist/blueprints/picture-post.js +17 -0
  33. package/dist/blueprints/poll.d.ts +41 -0
  34. package/dist/blueprints/poll.js +33 -0
  35. package/dist/blueprints/profile.d.ts +3 -0
  36. package/dist/blueprints/profile.js +7 -0
  37. package/dist/blueprints/reaction.d.ts +10 -0
  38. package/dist/blueprints/reaction.js +14 -0
  39. package/dist/blueprints/share.d.ts +12 -0
  40. package/dist/blueprints/share.js +25 -0
  41. package/dist/blueprints/stream.d.ts +6 -0
  42. package/dist/blueprints/stream.js +7 -0
  43. package/dist/blueprints/torrent.d.ts +23 -0
  44. package/dist/blueprints/torrent.js +25 -0
  45. package/dist/blueprints/wrapped-message.d.ts +20 -0
  46. package/dist/blueprints/wrapped-message.js +64 -0
  47. package/dist/helpers/app-data.d.ts +39 -0
  48. package/dist/helpers/app-data.js +68 -0
  49. package/dist/helpers/app-handler.d.ts +22 -0
  50. package/dist/helpers/app-handler.js +67 -0
  51. package/dist/helpers/article.d.ts +14 -0
  52. package/dist/helpers/article.js +24 -0
  53. package/dist/helpers/blossom.d.ts +11 -0
  54. package/dist/helpers/blossom.js +40 -0
  55. package/dist/helpers/bolt11.d.ts +10 -0
  56. package/dist/helpers/bolt11.js +17 -0
  57. package/dist/helpers/bookmark.d.ts +30 -0
  58. package/dist/helpers/bookmark.js +96 -0
  59. package/dist/helpers/calendar-event.d.ts +39 -0
  60. package/dist/helpers/calendar-event.js +121 -0
  61. package/dist/helpers/calendar-rsvp.d.ts +15 -0
  62. package/dist/helpers/calendar-rsvp.js +38 -0
  63. package/dist/helpers/calendar.d.ts +6 -0
  64. package/dist/helpers/calendar.js +11 -0
  65. package/dist/helpers/channels.d.ts +13 -0
  66. package/dist/helpers/channels.js +27 -0
  67. package/dist/helpers/comment.d.ts +47 -0
  68. package/dist/helpers/comment.js +185 -0
  69. package/dist/helpers/content.d.ts +3 -0
  70. package/dist/helpers/content.js +8 -0
  71. package/dist/helpers/emoji.d.ts +21 -0
  72. package/dist/helpers/emoji.js +34 -0
  73. package/dist/helpers/encrypted-content-cache.d.ts +22 -0
  74. package/dist/helpers/encrypted-content-cache.js +138 -0
  75. package/dist/helpers/file-metadata.d.ts +55 -0
  76. package/dist/helpers/file-metadata.js +130 -0
  77. package/dist/helpers/gift-wrap.d.ts +66 -0
  78. package/dist/helpers/gift-wrap.js +204 -0
  79. package/dist/helpers/groups-helper.d.ts +6 -0
  80. package/dist/helpers/groups-helper.js +9 -0
  81. package/dist/helpers/groups.d.ts +26 -0
  82. package/dist/helpers/groups.js +49 -0
  83. package/dist/helpers/hashtag.d.ts +2 -0
  84. package/dist/helpers/hashtag.js +7 -0
  85. package/dist/helpers/highlight.d.ts +45 -0
  86. package/dist/helpers/highlight.js +76 -0
  87. package/dist/helpers/index.d.ts +37 -0
  88. package/dist/helpers/index.js +37 -0
  89. package/dist/helpers/legacy-messages.d.ts +31 -0
  90. package/dist/helpers/legacy-messages.js +49 -0
  91. package/dist/helpers/lists.d.ts +58 -0
  92. package/dist/helpers/lists.js +110 -0
  93. package/dist/helpers/lnurl.d.ts +8 -0
  94. package/dist/helpers/lnurl.js +44 -0
  95. package/dist/helpers/mailboxes.d.ts +7 -0
  96. package/dist/helpers/mailboxes.js +49 -0
  97. package/dist/helpers/messages.d.ts +31 -0
  98. package/dist/helpers/messages.js +57 -0
  99. package/dist/helpers/mute.d.ts +33 -0
  100. package/dist/helpers/mute.js +111 -0
  101. package/dist/helpers/picture-post.d.ts +5 -0
  102. package/dist/helpers/picture-post.js +6 -0
  103. package/dist/helpers/poll.d.ts +46 -0
  104. package/dist/helpers/poll.js +78 -0
  105. package/dist/helpers/reaction.d.ts +8 -0
  106. package/dist/helpers/reaction.js +56 -0
  107. package/dist/helpers/relay-discovery.d.ts +87 -0
  108. package/dist/helpers/relay-discovery.js +126 -0
  109. package/dist/helpers/reports.d.ts +28 -0
  110. package/dist/helpers/reports.js +38 -0
  111. package/dist/helpers/share.d.ts +19 -0
  112. package/dist/helpers/share.js +58 -0
  113. package/dist/helpers/stream-chat.d.ts +4 -0
  114. package/dist/helpers/stream-chat.js +9 -0
  115. package/dist/helpers/stream.d.ts +31 -0
  116. package/dist/helpers/stream.js +81 -0
  117. package/dist/helpers/threading.d.ts +55 -0
  118. package/dist/helpers/threading.js +94 -0
  119. package/dist/helpers/torrent.d.ts +55 -0
  120. package/dist/helpers/torrent.js +270 -0
  121. package/dist/helpers/user-status.d.ts +18 -0
  122. package/dist/helpers/user-status.js +22 -0
  123. package/dist/helpers/wrapped-messages.d.ts +14 -0
  124. package/dist/helpers/wrapped-messages.js +23 -0
  125. package/dist/helpers/zap.d.ts +46 -0
  126. package/dist/helpers/zap.js +125 -0
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.js +6 -0
  129. package/dist/models/blossom.d.ts +11 -0
  130. package/dist/models/blossom.js +18 -0
  131. package/dist/models/bookmarks.d.ts +8 -0
  132. package/dist/models/bookmarks.js +24 -0
  133. package/dist/models/calendar.d.ts +6 -0
  134. package/dist/models/calendar.js +15 -0
  135. package/dist/models/channels.d.ts +11 -0
  136. package/dist/models/channels.js +61 -0
  137. package/dist/models/comments.d.ts +11 -0
  138. package/dist/models/comments.js +17 -0
  139. package/dist/models/gift-wrap.d.ts +7 -0
  140. package/dist/models/gift-wrap.js +20 -0
  141. package/dist/models/index.d.ts +15 -0
  142. package/dist/models/index.js +16 -0
  143. package/dist/models/legacy-messages.d.ts +14 -0
  144. package/dist/models/legacy-messages.js +64 -0
  145. package/dist/models/mutes.d.ts +16 -0
  146. package/dist/models/mutes.js +34 -0
  147. package/dist/models/pins.d.ts +4 -0
  148. package/dist/models/pins.js +10 -0
  149. package/dist/models/reactions.d.ts +11 -0
  150. package/dist/models/reactions.js +21 -0
  151. package/dist/models/thread.d.ts +33 -0
  152. package/dist/models/thread.js +93 -0
  153. package/dist/models/user-status.d.ts +11 -0
  154. package/dist/models/user-status.js +32 -0
  155. package/dist/models/wrapped-messages.d.ts +31 -0
  156. package/dist/models/wrapped-messages.js +76 -0
  157. package/dist/models/zaps.d.ts +9 -0
  158. package/dist/models/zaps.js +26 -0
  159. package/dist/operations/app-data.d.ts +6 -0
  160. package/dist/operations/app-data.js +21 -0
  161. package/dist/operations/blossom.d.ts +5 -0
  162. package/dist/operations/blossom.js +13 -0
  163. package/dist/operations/calendar-event.d.ts +34 -0
  164. package/dist/operations/calendar-event.js +72 -0
  165. package/dist/operations/calendar-rsvp.d.ts +10 -0
  166. package/dist/operations/calendar-rsvp.js +35 -0
  167. package/dist/operations/calendar.d.ts +9 -0
  168. package/dist/operations/calendar.js +15 -0
  169. package/dist/operations/channel.d.ts +4 -0
  170. package/dist/operations/channel.js +10 -0
  171. package/dist/operations/client.d.ts +4 -0
  172. package/dist/operations/client.js +23 -0
  173. package/dist/operations/comment.d.ts +4 -0
  174. package/dist/operations/comment.js +11 -0
  175. package/dist/operations/file-metadata.d.ts +4 -0
  176. package/dist/operations/file-metadata.js +21 -0
  177. package/dist/operations/geohash.d.ts +5 -0
  178. package/dist/operations/geohash.js +17 -0
  179. package/dist/operations/gift-wrap.d.ts +13 -0
  180. package/dist/operations/gift-wrap.js +93 -0
  181. package/dist/operations/group.d.ts +11 -0
  182. package/dist/operations/group.js +34 -0
  183. package/dist/operations/hashtags.d.ts +7 -0
  184. package/dist/operations/hashtags.js +17 -0
  185. package/dist/operations/highlight.d.ts +18 -0
  186. package/dist/operations/highlight.js +47 -0
  187. package/dist/operations/index.d.ts +28 -0
  188. package/dist/operations/index.js +28 -0
  189. package/dist/operations/legacy-message.d.ts +6 -0
  190. package/dist/operations/legacy-message.js +13 -0
  191. package/dist/operations/list.d.ts +7 -0
  192. package/dist/operations/list.js +14 -0
  193. package/dist/operations/live-stream.d.ts +4 -0
  194. package/dist/operations/live-stream.js +11 -0
  195. package/dist/operations/media-attachment.d.ts +4 -0
  196. package/dist/operations/media-attachment.js +12 -0
  197. package/dist/operations/note.d.ts +9 -0
  198. package/dist/operations/note.js +42 -0
  199. package/dist/operations/picture-post.d.ts +4 -0
  200. package/dist/operations/picture-post.js +14 -0
  201. package/dist/operations/poll-response.d.ts +9 -0
  202. package/dist/operations/poll-response.js +20 -0
  203. package/dist/operations/poll.d.ts +19 -0
  204. package/dist/operations/poll.js +42 -0
  205. package/dist/operations/reaction.d.ts +7 -0
  206. package/dist/operations/reaction.js +39 -0
  207. package/dist/operations/share.d.ts +8 -0
  208. package/dist/operations/share.js +34 -0
  209. package/dist/operations/stream-chat.d.ts +7 -0
  210. package/dist/operations/stream-chat.js +27 -0
  211. package/dist/operations/stream.d.ts +41 -0
  212. package/dist/operations/stream.js +83 -0
  213. package/dist/operations/tag/bookmarks.d.ts +6 -0
  214. package/dist/operations/tag/bookmarks.js +20 -0
  215. package/dist/operations/tag/index.d.ts +1 -0
  216. package/dist/operations/tag/index.js +1 -0
  217. package/dist/operations/torrent.d.ts +33 -0
  218. package/dist/operations/torrent.js +66 -0
  219. package/dist/operations/wrapped-message.d.ts +12 -0
  220. package/dist/operations/wrapped-message.js +28 -0
  221. package/dist/operations/zap-split.d.ts +10 -0
  222. package/dist/operations/zap-split.js +20 -0
  223. package/dist/register.d.ts +11 -0
  224. package/dist/register.js +13 -0
  225. package/package.json +91 -0
@@ -0,0 +1,34 @@
1
+ import { getTagValue } from "applesauce-core/helpers/event";
2
+ /** Gets an "emoji" tag that matches an emoji code */
3
+ export function getEmojiTag(tags, code) {
4
+ code = code.replace(/^:|:$/g, "").toLowerCase();
5
+ return (Array.isArray(tags) ? tags : tags.tags).find((t) => t[0] === "emoji" && t.length >= 3 && t[1].toLowerCase() === code);
6
+ }
7
+ /** Gets an emoji for a shortcode from an array of tags or event */
8
+ export function getEmojiFromTags(event, code) {
9
+ const tag = getEmojiTag(event, code);
10
+ if (!tag)
11
+ return undefined;
12
+ return {
13
+ shortcode: tag[1],
14
+ url: tag[2],
15
+ };
16
+ }
17
+ /** Returns the name of a NIP-30 emoji pack */
18
+ export function getPackName(pack) {
19
+ return getTagValue(pack, "title") || getTagValue(pack, "d");
20
+ }
21
+ /** Returns an array of emojis from a NIP-30 emoji pack */
22
+ export function getEmojis(pack) {
23
+ return pack.tags
24
+ .filter((t) => t[0] === "emoji" && t[1] && t[2])
25
+ .map((t) => ({ shortcode: t[1], url: t[2] }));
26
+ }
27
+ /** Returns the custom emoji for a reaction event */
28
+ export function getReactionEmoji(event) {
29
+ // Trim and strip colons
30
+ const shortcode = /^:+(.+?):+$/g.exec(event.content.trim().toLowerCase())?.[1];
31
+ if (!shortcode)
32
+ return undefined;
33
+ return getEmojiFromTags(event, shortcode);
34
+ }
@@ -0,0 +1,22 @@
1
+ import { IEventStoreStreams } from "applesauce-core/event-store";
2
+ import { NostrEvent } from "applesauce-core/helpers/event";
3
+ import { Observable } from "rxjs";
4
+ /** A symbol that is used to mark encrypted content as being from a cache */
5
+ export declare const EncryptedContentFromCacheSymbol: unique symbol;
6
+ /** An interface that is used to cache encrypted content on events */
7
+ export interface EncryptedContentCache {
8
+ getItem: (key: string) => Promise<string | null>;
9
+ setItem: (key: string, value: string) => Promise<any>;
10
+ }
11
+ /** Marks the encrypted content as being from a cache */
12
+ export declare function markEncryptedContentFromCache<T extends object>(event: T): void;
13
+ /** Checks if the encrypted content is from a cache */
14
+ export declare function isEncryptedContentFromCache<T extends object>(event: T): boolean;
15
+ /**
16
+ * Starts a process that persists and restores all encrypted content
17
+ * @param eventStore - The event store to listen to
18
+ * @param storage - The storage to use
19
+ * @param fallback - A function that will be called when the encrypted content is not found in storage
20
+ * @returns A function that can be used to stop the process
21
+ */
22
+ export declare function persistEncryptedContent(eventStore: IEventStoreStreams, storage: EncryptedContentCache | Observable<EncryptedContentCache>, fallback?: (event: NostrEvent) => any | Promise<any>): () => void;
@@ -0,0 +1,138 @@
1
+ import { logger } from "applesauce-core";
2
+ import { canHaveEncryptedContent, getEncryptedContent, isEncryptedContentUnlocked, setEncryptedContentCache, } from "applesauce-core/helpers/encrypted-content";
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";
6
+ /** A symbol that is used to mark encrypted content as being from a cache */
7
+ export const EncryptedContentFromCacheSymbol = Symbol.for("encrypted-content-from-cache");
8
+ /** Marks the encrypted content as being from a cache */
9
+ export function markEncryptedContentFromCache(event) {
10
+ Reflect.set(event, EncryptedContentFromCacheSymbol, true);
11
+ }
12
+ /** Checks if the encrypted content is from a cache */
13
+ export function isEncryptedContentFromCache(event) {
14
+ return Reflect.has(event, EncryptedContentFromCacheSymbol);
15
+ }
16
+ const log = logger.extend("EncryptedContentCache");
17
+ /**
18
+ * Starts a process that persists and restores all encrypted content
19
+ * @param eventStore - The event store to listen to
20
+ * @param storage - The storage to use
21
+ * @param fallback - A function that will be called when the encrypted content is not found in storage
22
+ * @returns A function that can be used to stop the process
23
+ */
24
+ export function persistEncryptedContent(eventStore, storage, fallback) {
25
+ const storage$ = isObservable(storage) ? storage : of(storage);
26
+ // Get the encrypted content from storage or call the fallback
27
+ const getItem = async (storage, event) => {
28
+ return (await storage.getItem(event.id)) || (fallback ? await fallback(event) : null);
29
+ };
30
+ // Restore encrypted content when it is inserted
31
+ const restore = eventStore.insert$
32
+ .pipe(
33
+ // Look for events that support encrypted content and are locked
34
+ filter((e) => canHaveEncryptedContent(e.kind) && isEncryptedContentUnlocked(e) === false),
35
+ // 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
+ }))))
42
+ .subscribe(async ([event, content]) => {
43
+ if (!content)
44
+ return;
45
+ // Restore the encrypted content and set it as from a cache
46
+ markEncryptedContentFromCache(event);
47
+ setEncryptedContentCache(event, content);
48
+ log(`Restored encrypted content for ${event.id}`);
49
+ });
50
+ // Restore seals when they are unlocked
51
+ const restoreSeals = eventStore.update$
52
+ .pipe(
53
+ // Look for gift wraps that are unlocked
54
+ filter((e) => e.kind === kinds.GiftWrap && isEncryptedContentUnlocked(e)),
55
+ // Get the seal event
56
+ map((gift) => getGiftWrapSeal(gift)),
57
+ // Look for gift wraps with locked seals
58
+ filter((seal) => seal !== undefined && isEncryptedContentUnlocked(seal) === false),
59
+ // Only attempt to unlock seals once
60
+ distinct((seal) => seal.id),
61
+ // Get encrypted content from storage
62
+ mergeMap((seal) =>
63
+ // Wait for storage to be available
64
+ storage$.pipe(switchMap((storage) => combineLatest([of(seal), getItem(storage, seal)])), catchError((error) => {
65
+ log(`Failed to restore encrypted content for ${seal.id}`, error);
66
+ return EMPTY;
67
+ }))))
68
+ .subscribe(async ([seal, content]) => {
69
+ if (!seal || !content)
70
+ return;
71
+ markEncryptedContentFromCache(seal);
72
+ setEncryptedContentCache(seal, content);
73
+ // Parse the rumor event
74
+ getSealRumor(seal);
75
+ // Trigger an update to the gift wrap event
76
+ const gift = getSealGiftWrap(seal);
77
+ if (gift)
78
+ notifyEventUpdate(gift);
79
+ log(`Restored encrypted content for ${seal.id}`);
80
+ });
81
+ // Persist encrypted content when it is updated or inserted
82
+ const persist = combineLatest([merge(eventStore.update$, eventStore.insert$), storage$])
83
+ .pipe(
84
+ // Look for events that support encrypted content and are unlocked and not from the cache
85
+ filter(([event]) => canHaveEncryptedContent(event.kind) &&
86
+ isEncryptedContentUnlocked(event) &&
87
+ !isEncryptedContentFromCache(event)),
88
+ // Only persist the encrypted content once
89
+ distinct(([event]) => event.id))
90
+ .subscribe(async ([event, storage]) => {
91
+ try {
92
+ const content = getEncryptedContent(event);
93
+ if (content) {
94
+ await storage.setItem(event.id, content);
95
+ log(`Persisted encrypted content for ${event.id}`);
96
+ }
97
+ }
98
+ catch (error) {
99
+ // Ignore errors when saving encrypted content
100
+ log(`Failed to persist encrypted content for ${event.id}`, error);
101
+ }
102
+ });
103
+ // Persist seals when the gift warp is unlocked or inserted unlocked
104
+ // 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))
117
+ .subscribe(async ([seal, storage]) => {
118
+ if (!seal)
119
+ return;
120
+ try {
121
+ const content = getEncryptedContent(seal);
122
+ if (content) {
123
+ await storage.setItem(seal.id, content);
124
+ log(`Persisted encrypted content for ${seal.id}`);
125
+ }
126
+ }
127
+ catch (error) {
128
+ // Ignore errors when saving encrypted content
129
+ log(`Failed to persist encrypted content for ${seal.id}`, error);
130
+ }
131
+ });
132
+ return () => {
133
+ restore.unsubscribe();
134
+ persist.unsubscribe();
135
+ restoreSeals.unsubscribe();
136
+ persistSeals.unsubscribe();
137
+ };
138
+ }
@@ -0,0 +1,55 @@
1
+ import { NostrEvent } from "applesauce-core/helpers/event";
2
+ import { NameValueTag } from "applesauce-core/helpers";
3
+ export type FileMetadata = {
4
+ /** URL of the file */
5
+ url?: string;
6
+ /** MIME type */
7
+ type?: string;
8
+ /** sha256 hash of the file */
9
+ sha256?: string;
10
+ /**
11
+ * The original sha256 hash before the file was transformed
12
+ * @deprecated
13
+ */
14
+ originalSha256?: string;
15
+ /** size of the file in bytes */
16
+ size?: number;
17
+ /** size of file in pixels in the form <width>x<height> */
18
+ dimensions?: string;
19
+ /** magnet */
20
+ magnet?: string;
21
+ /** torrent infohash */
22
+ infohash?: string;
23
+ /** URL to a thumbnail */
24
+ thumbnail?: string;
25
+ /** URL to a preview image with the same dimensions */
26
+ image?: string;
27
+ /** summary */
28
+ summary?: string;
29
+ /** description for accessability */
30
+ alt?: string;
31
+ /** blurhash */
32
+ blurhash?: string;
33
+ /** fallback URLs */
34
+ fallback?: string[];
35
+ };
36
+ /** Alias for {@link FileMetadata} */
37
+ export type MediaAttachment = FileMetadata;
38
+ /**
39
+ * Parses file metadata tags into {@link FileMetadata}
40
+ * @throws
41
+ */
42
+ export declare function parseFileMetadataTags(tags: string[][]): FileMetadata;
43
+ /** Parses a imeta tag into a {@link FileMetadata} */
44
+ export declare function getFileMetadataFromImetaTag(tag: string[]): FileMetadata;
45
+ export declare const MediaAttachmentsSymbol: unique symbol;
46
+ /** Gets all the media attachments on an event */
47
+ export declare function getMediaAttachments(event: NostrEvent): FileMetadata[];
48
+ /** Gets {@link FileMetadata} for a NIP-94 kind 1063 event */
49
+ export declare function getFileMetadata(file: NostrEvent): FileMetadata;
50
+ /** Returns the last 64 length hex string in a URL */
51
+ export declare function getSha256FromURL(url: string | URL): string | undefined;
52
+ /** Creates tags for {@link FileMetadata} */
53
+ export declare function createFileMetadataTags(attachment: FileMetadata): NameValueTag[];
54
+ /** Creates an imeta tag for a media attachment */
55
+ export declare function createImetaTagForAttachment(attachment: FileMetadata): string[];
@@ -0,0 +1,130 @@
1
+ import { getOrComputeCachedValue } from "applesauce-core/helpers/cache";
2
+ /**
3
+ * Parses file metadata tags into {@link FileMetadata}
4
+ * @throws
5
+ */
6
+ export function parseFileMetadataTags(tags) {
7
+ const fields = {};
8
+ let fallback = undefined;
9
+ for (const [name, value] of tags) {
10
+ switch (name) {
11
+ case "fallback":
12
+ fallback = fallback ? [...fallback, value] : [value];
13
+ break;
14
+ default:
15
+ fields[name] = value;
16
+ break;
17
+ }
18
+ }
19
+ const metadata = { url: fields.url, fallback };
20
+ // parse size
21
+ if (fields.size)
22
+ metadata.size = parseInt(fields.size);
23
+ // copy optional fields
24
+ if (fields.m)
25
+ metadata.type = fields.m;
26
+ if (fields.x)
27
+ metadata.sha256 = fields.x;
28
+ if (fields.ox)
29
+ metadata.originalSha256 = fields.ox;
30
+ if (fields.dim)
31
+ metadata.dimensions = fields.dim;
32
+ if (fields.magnet)
33
+ metadata.magnet = fields.magnet;
34
+ if (fields.i)
35
+ metadata.infohash = fields.i;
36
+ if (fields.thumb)
37
+ metadata.thumbnail = fields.thumb;
38
+ if (fields.image)
39
+ metadata.image = fields.image;
40
+ if (fields.summary)
41
+ metadata.summary = fields.summary;
42
+ if (fields.alt)
43
+ metadata.alt = fields.alt;
44
+ if (fields.blurhash)
45
+ metadata.blurhash = fields.blurhash;
46
+ return metadata;
47
+ }
48
+ /** Parses a imeta tag into a {@link FileMetadata} */
49
+ export function getFileMetadataFromImetaTag(tag) {
50
+ const parts = tag.slice(1);
51
+ const tags = [];
52
+ for (const part of parts) {
53
+ const match = part.match(/^(.+?)\s(.+)$/);
54
+ if (match) {
55
+ const [_, name, value] = match;
56
+ tags.push([name, value]);
57
+ }
58
+ }
59
+ return parseFileMetadataTags(tags);
60
+ }
61
+ export const MediaAttachmentsSymbol = Symbol.for("media-attachments");
62
+ /** Gets all the media attachments on an event */
63
+ export function getMediaAttachments(event) {
64
+ return getOrComputeCachedValue(event, MediaAttachmentsSymbol, () => {
65
+ return event.tags
66
+ .filter((t) => t[0] === "imeta")
67
+ .map((tag) => {
68
+ try {
69
+ return getFileMetadataFromImetaTag(tag);
70
+ }
71
+ catch (error) {
72
+ // ignore invalid attachments
73
+ return undefined;
74
+ }
75
+ })
76
+ .filter((a) => !!a);
77
+ });
78
+ }
79
+ /** Gets {@link FileMetadata} for a NIP-94 kind 1063 event */
80
+ export function getFileMetadata(file) {
81
+ return parseFileMetadataTags(file.tags);
82
+ }
83
+ /** Returns the last 64 length hex string in a URL */
84
+ export function getSha256FromURL(url) {
85
+ if (typeof url === "string")
86
+ url = new URL(url);
87
+ const hashes = Array.from(url.pathname.matchAll(/[0-9a-f]{64}/gi));
88
+ if (hashes.length > 0)
89
+ return hashes[hashes.length - 1][0];
90
+ return;
91
+ }
92
+ /** Creates tags for {@link FileMetadata} */
93
+ export function createFileMetadataTags(attachment) {
94
+ const tags = [];
95
+ const add = (name, value) => tags.push([name, String(value)]);
96
+ if (attachment.url)
97
+ add("url", attachment.url);
98
+ if (attachment.type)
99
+ add("m", attachment.type);
100
+ if (attachment.sha256)
101
+ add("x", attachment.sha256);
102
+ if (attachment.originalSha256)
103
+ add("ox", attachment.originalSha256);
104
+ if (attachment.size !== undefined)
105
+ add("size", attachment.size);
106
+ if (attachment.dimensions)
107
+ add("dim", attachment.dimensions);
108
+ if (attachment.magnet)
109
+ add("magnet", attachment.magnet);
110
+ if (attachment.infohash)
111
+ add("i", attachment.infohash);
112
+ if (attachment.blurhash)
113
+ add("blurhash", attachment.blurhash);
114
+ if (attachment.thumbnail)
115
+ add("thumb", attachment.thumbnail);
116
+ if (attachment.image)
117
+ add("image", attachment.image);
118
+ if (attachment.summary)
119
+ add("summary", attachment.summary);
120
+ if (attachment.alt)
121
+ add("alt", attachment.alt);
122
+ if (attachment.fallback && attachment.fallback?.length > 0)
123
+ for (const url of attachment.fallback)
124
+ add("fallback", url);
125
+ return tags;
126
+ }
127
+ /** Creates an imeta tag for a media attachment */
128
+ export function createImetaTagForAttachment(attachment) {
129
+ return ["imeta", ...createFileMetadataTags(attachment).map((t) => t.join(" "))];
130
+ }
@@ -0,0 +1,66 @@
1
+ import { EventMemory } from "applesauce-core/event-store";
2
+ import { EncryptedContentSigner } from "applesauce-core/helpers/encrypted-content";
3
+ import { kinds, KnownEvent, NostrEvent, UnsignedEvent } from "applesauce-core/helpers/event";
4
+ /**
5
+ * An internal event set to keep track of seals and rumors
6
+ * This is intentionally isolated from the main applications event store so to prevent seals and rumors from being leaked
7
+ */
8
+ export declare const internalGiftWrapEvents: EventMemory;
9
+ export type Rumor = UnsignedEvent & {
10
+ id: string;
11
+ };
12
+ /** Used to store a reference to the seal event on gift wraps (downstream) or the seal event on rumors (upstream[]) */
13
+ export declare const SealSymbol: unique symbol;
14
+ /** Used to store a reference to the rumor on seals (downstream) */
15
+ export declare const RumorSymbol: unique symbol;
16
+ /** Used to store a reference to the parent gift wrap event on seals (upstream) */
17
+ export declare const GiftWrapSymbol: unique symbol;
18
+ /** A gift wrap event that knows its seal event */
19
+ export type UnlockedGiftWrapEvent = KnownEvent<kinds.GiftWrap> & {
20
+ /** Downstream seal event */
21
+ [SealSymbol]: UnlockedSeal;
22
+ };
23
+ /** A seal that knows its parent gift wrap event */
24
+ export type UnlockedSeal = KnownEvent<kinds.Seal> & {
25
+ /** Upstream gift wrap event */
26
+ [SealSymbol]: UnlockedGiftWrapEvent;
27
+ /** Downstream rumor event */
28
+ [RumorSymbol]: Rumor;
29
+ };
30
+ /** Checks if an event is a rumor (normal event with "id" and no "sig") */
31
+ export declare function isRumor(event: any): event is Rumor;
32
+ /** Returns all the parent gift wraps for a seal event */
33
+ export declare function getSealGiftWrap(seal: UnlockedSeal): UnlockedGiftWrapEvent;
34
+ export declare function getSealGiftWrap(seal: NostrEvent): UnlockedGiftWrapEvent | undefined;
35
+ /** Returns all the parent seals for a rumor event */
36
+ export declare function getRumorSeals(rumor: Rumor): UnlockedSeal[];
37
+ /** Returns all the parent gift wraps for a rumor event */
38
+ export declare function getRumorGiftWraps(rumor: Rumor): UnlockedGiftWrapEvent[];
39
+ /** Checks if a seal event is locked and casts it to the {@link UnlockedSeal} type */
40
+ export declare function isSealUnlocked(seal: NostrEvent): seal is UnlockedSeal;
41
+ /** Returns if a gift-wrap event or gift-wrap seal is locked */
42
+ export declare function isGiftWrapUnlocked(gift: NostrEvent): gift is UnlockedGiftWrapEvent;
43
+ /**
44
+ * Gets the rumor from a seal event
45
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
46
+ */
47
+ export declare function getSealRumor(seal: UnlockedSeal): Rumor;
48
+ export declare function getSealRumor(seal: NostrEvent): Rumor | undefined;
49
+ /** Returns the seal event in a gift-wrap -> seal (downstream) */
50
+ export declare function getGiftWrapSeal(gift: UnlockedGiftWrapEvent): UnlockedSeal;
51
+ export declare function getGiftWrapSeal(gift: NostrEvent): NostrEvent | undefined;
52
+ /** Returns the unsigned rumor in the gift-wrap -> seal -> rumor (downstream) */
53
+ export declare function getGiftWrapRumor(gift: UnlockedGiftWrapEvent): Rumor;
54
+ export declare function getGiftWrapRumor(gift: NostrEvent): Rumor | undefined;
55
+ /**
56
+ * Unlocks a seal event and returns the rumor event
57
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
58
+ */
59
+ export declare function unlockSeal(seal: NostrEvent, signer: EncryptedContentSigner): Promise<Rumor>;
60
+ /**
61
+ * Unlocks and returns the unsigned seal event in a gift-wrap
62
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
63
+ */
64
+ export declare function unlockGiftWrap(gift: NostrEvent, signer: EncryptedContentSigner): Promise<Rumor>;
65
+ /** Locks a gift-wrap event and seals its seal event */
66
+ export declare function lockGiftWrap(gift: NostrEvent): void;
@@ -0,0 +1,204 @@
1
+ import { EventMemory } from "applesauce-core/event-store";
2
+ import { getEncryptedContent, isEncryptedContentUnlocked, lockEncryptedContent, unlockEncryptedContent, } from "applesauce-core/helpers/encrypted-content";
3
+ import { kinds, notifyEventUpdate, verifyWrappedEvent, } from "applesauce-core/helpers/event";
4
+ /**
5
+ * An internal event set to keep track of seals and rumors
6
+ * This is intentionally isolated from the main applications event store so to prevent seals and rumors from being leaked
7
+ */
8
+ export const internalGiftWrapEvents = new EventMemory();
9
+ /** Used to store a reference to the seal event on gift wraps (downstream) or the seal event on rumors (upstream[]) */
10
+ export const SealSymbol = Symbol.for("seal");
11
+ /** Used to store a reference to the rumor on seals (downstream) */
12
+ export const RumorSymbol = Symbol.for("rumor");
13
+ /** Used to store a reference to the parent gift wrap event on seals (upstream) */
14
+ export const GiftWrapSymbol = Symbol.for("gift-wrap");
15
+ /** Adds a parent reference to a seal or rumor */
16
+ function addParentSealReference(rumor, seal) {
17
+ const parents = Reflect.get(rumor, SealSymbol);
18
+ if (!parents)
19
+ Reflect.set(rumor, SealSymbol, new Set([seal]));
20
+ else
21
+ parents.add(seal);
22
+ }
23
+ /** Removes a parent reference from a seal or rumor */
24
+ function removeParentSealReference(rumor, seal) {
25
+ const parents = Reflect.get(rumor, SealSymbol);
26
+ if (parents)
27
+ parents.delete(seal);
28
+ }
29
+ /** Checks if an event is a rumor (normal event with "id" and no "sig") */
30
+ export function isRumor(event) {
31
+ if (event === undefined || event === null)
32
+ return false;
33
+ return (event.id?.length === 64 &&
34
+ !("sig" in event) &&
35
+ typeof event.pubkey === "string" &&
36
+ event.pubkey.length === 64 &&
37
+ typeof event.content === "string" &&
38
+ Array.isArray(event.tags) &&
39
+ typeof event.created_at === "number" &&
40
+ event.created_at > 0);
41
+ }
42
+ export function getSealGiftWrap(seal) {
43
+ return Reflect.get(seal, GiftWrapSymbol);
44
+ }
45
+ /** Returns all the parent seals for a rumor event */
46
+ export function getRumorSeals(rumor) {
47
+ let set = Reflect.get(rumor, SealSymbol);
48
+ if (!set) {
49
+ set = new Set();
50
+ Reflect.set(rumor, SealSymbol, set);
51
+ }
52
+ return Array.from(set);
53
+ }
54
+ /** Returns all the parent gift wraps for a rumor event */
55
+ export function getRumorGiftWraps(rumor) {
56
+ const giftWraps = new Set();
57
+ const seals = getRumorSeals(rumor);
58
+ for (const seal of seals) {
59
+ const upstream = getSealGiftWrap(seal);
60
+ if (upstream)
61
+ giftWraps.add(upstream);
62
+ }
63
+ return Array.from(giftWraps);
64
+ }
65
+ /** Checks if a seal event is locked and casts it to the {@link UnlockedSeal} type */
66
+ export function isSealUnlocked(seal) {
67
+ return isEncryptedContentUnlocked(seal) === true && Reflect.has(seal, RumorSymbol) === true;
68
+ }
69
+ /** Returns if a gift-wrap event or gift-wrap seal is locked */
70
+ export function isGiftWrapUnlocked(gift) {
71
+ if (isEncryptedContentUnlocked(gift) === false)
72
+ return false;
73
+ // Get the seal event
74
+ const seal = getGiftWrapSeal(gift);
75
+ if (!seal)
76
+ return false;
77
+ // If seal is locked, return false
78
+ if (!isSealUnlocked(seal))
79
+ return false;
80
+ return true;
81
+ }
82
+ export function getSealRumor(seal) {
83
+ // Non seal events cant have rumors
84
+ if (seal.kind !== kinds.Seal)
85
+ return undefined;
86
+ // If unlocked return the rumor
87
+ if (isSealUnlocked(seal))
88
+ return seal[RumorSymbol];
89
+ // Get the encrypted content plaintext
90
+ const content = getEncryptedContent(seal);
91
+ // Return undefined if the content is not found
92
+ if (!content)
93
+ return undefined;
94
+ // Parse the content as a rumor event
95
+ let rumor = JSON.parse(content);
96
+ // Check if the rumor event already exists in the internal event set
97
+ const existing = internalGiftWrapEvents.getEvent(rumor.id);
98
+ if (existing)
99
+ // Reuse the existing rumor instance
100
+ rumor = existing;
101
+ else
102
+ // Add to the internal event set
103
+ internalGiftWrapEvents.add(rumor);
104
+ // Throw an error if the seal and rumor authors do not match
105
+ if (rumor.pubkey !== seal.pubkey)
106
+ throw new Error("Seal author does not match rumor author");
107
+ // Save a reference to the parent seal event
108
+ addParentSealReference(rumor, seal);
109
+ // Cache the rumor event
110
+ Reflect.set(seal, RumorSymbol, rumor);
111
+ return rumor;
112
+ }
113
+ export function getGiftWrapSeal(gift) {
114
+ // Returned cached seal if it exists (downstream)
115
+ if (Reflect.has(gift, SealSymbol))
116
+ return Reflect.get(gift, SealSymbol);
117
+ // Get the encrypted content
118
+ const content = getEncryptedContent(gift);
119
+ // Return undefined if the content is not found
120
+ if (!content)
121
+ return undefined;
122
+ // Parse seal as nostr event
123
+ let seal = JSON.parse(content);
124
+ // Check if the seal event already exists in the internal event set
125
+ const existing = internalGiftWrapEvents.getEvent(seal.id);
126
+ if (existing) {
127
+ // Reuse the existing seal instance
128
+ seal = existing;
129
+ }
130
+ else {
131
+ // Verify the seal event
132
+ verifyWrappedEvent(seal);
133
+ // Add to the internal event set
134
+ internalGiftWrapEvents.add(seal);
135
+ // Set the reference to the parent gift wrap event (upstream)
136
+ Reflect.set(seal, GiftWrapSymbol, gift);
137
+ }
138
+ // Save a reference to the seal on the gift wrap (downstream)
139
+ Reflect.set(gift, SealSymbol, seal);
140
+ return seal;
141
+ }
142
+ export function getGiftWrapRumor(gift) {
143
+ const seal = getGiftWrapSeal(gift);
144
+ if (!seal)
145
+ return undefined;
146
+ return getSealRumor(seal);
147
+ }
148
+ /**
149
+ * Unlocks a seal event and returns the rumor event
150
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
151
+ */
152
+ export async function unlockSeal(seal, signer) {
153
+ // If already unlocked, return the rumor
154
+ if (isSealUnlocked(seal))
155
+ return seal[RumorSymbol];
156
+ // unlock encrypted content as needed
157
+ await unlockEncryptedContent(seal, seal.pubkey, signer);
158
+ const rumor = getSealRumor(seal);
159
+ if (!rumor)
160
+ throw new Error("Failed to read rumor in gift wrap");
161
+ // Notify event store
162
+ notifyEventUpdate(seal);
163
+ return rumor;
164
+ }
165
+ /**
166
+ * Unlocks and returns the unsigned seal event in a gift-wrap
167
+ * @throws {Error} If the author of the rumor event does not match the author of the seal
168
+ */
169
+ export async function unlockGiftWrap(gift, signer) {
170
+ // If already unlocked, return the rumor
171
+ if (isGiftWrapUnlocked(gift))
172
+ return getGiftWrapRumor(gift);
173
+ // Unlock the encrypted content
174
+ await unlockEncryptedContent(gift, gift.pubkey, signer);
175
+ // Parse seal as nostr event
176
+ let seal = getGiftWrapSeal(gift);
177
+ if (!seal)
178
+ throw new Error("Failed to read seal in gift wrap");
179
+ // Unlock the seal event
180
+ const rumor = await unlockSeal(seal, signer);
181
+ // if the event has been added to an event store, notify it
182
+ notifyEventUpdate(gift);
183
+ return rumor;
184
+ }
185
+ /** Locks a gift-wrap event and seals its seal event */
186
+ export function lockGiftWrap(gift) {
187
+ const seal = getGiftWrapSeal(gift);
188
+ if (seal) {
189
+ const rumor = getSealRumor(seal);
190
+ // Remove the rumors parent seal reference (upstream)
191
+ if (rumor)
192
+ removeParentSealReference(rumor, seal);
193
+ // Remove the seal's parent gift wrap reference (upstream)
194
+ Reflect.deleteProperty(seal, GiftWrapSymbol);
195
+ // Remove the seal's rumor reference (downstream)
196
+ Reflect.deleteProperty(seal, RumorSymbol);
197
+ // Lock the seal's encrypted content
198
+ lockEncryptedContent(seal);
199
+ }
200
+ // Remove the gift wrap's seal reference (downstream)
201
+ Reflect.deleteProperty(gift, SealSymbol);
202
+ // Lock the gift wrap's encrypted content
203
+ lockEncryptedContent(gift);
204
+ }
@@ -0,0 +1,6 @@
1
+ import { NameValueTag } from "applesauce-core/helpers";
2
+ import { GroupPointer } from "./groups.js";
3
+ /** Creates a "h" tag for chat messages from a {@link GroupPointer} */
4
+ export declare function createGroupHTagFromGroupPointer(group: GroupPointer): NameValueTag;
5
+ /** Creates a "group" tag from a {@link GroupPointer} */
6
+ export declare function createGroupTagFromGroupPointer(group: GroupPointer): NameValueTag;
@@ -0,0 +1,9 @@
1
+ import { ensureWebSocketURL, fillAndTrimTag } from "applesauce-core/helpers";
2
+ /** Creates a "h" tag for chat messages from a {@link GroupPointer} */
3
+ export function createGroupHTagFromGroupPointer(group) {
4
+ return fillAndTrimTag(["h", group.id, ensureWebSocketURL(group.relay)]);
5
+ }
6
+ /** Creates a "group" tag from a {@link GroupPointer} */
7
+ export function createGroupTagFromGroupPointer(group) {
8
+ return fillAndTrimTag(["group", group.id, ensureWebSocketURL(group.relay), group.name], 3);
9
+ }