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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/blueprints/app-data.d.ts +5 -0
- package/dist/blueprints/app-data.js +10 -0
- package/dist/blueprints/calendar.d.ts +5 -0
- package/dist/blueprints/calendar.js +7 -0
- package/dist/blueprints/channels.d.ts +6 -0
- package/dist/blueprints/channels.js +13 -0
- package/dist/blueprints/comment.d.ts +12 -0
- package/dist/blueprints/comment.js +15 -0
- package/dist/blueprints/delete.d.ts +9 -0
- package/dist/blueprints/delete.js +14 -0
- package/dist/blueprints/file-metadata.d.ts +8 -0
- package/dist/blueprints/file-metadata.js +11 -0
- package/dist/blueprints/follow-set.d.ts +11 -0
- package/dist/blueprints/follow-set.js +21 -0
- package/dist/blueprints/gift-wrap.d.ts +5 -0
- package/dist/blueprints/gift-wrap.js +6 -0
- package/dist/blueprints/group.d.ts +9 -0
- package/dist/blueprints/group.js +17 -0
- package/dist/blueprints/highlight.d.ts +20 -0
- package/dist/blueprints/highlight.js +12 -0
- package/dist/blueprints/index.d.ts +23 -0
- package/dist/blueprints/index.js +24 -0
- package/dist/blueprints/legacy-message.d.ts +7 -0
- package/dist/blueprints/legacy-message.js +29 -0
- package/dist/blueprints/live-stream.d.ts +6 -0
- package/dist/blueprints/live-stream.js +9 -0
- package/dist/blueprints/note.d.ts +16 -0
- package/dist/blueprints/note.js +30 -0
- package/dist/blueprints/picture-post.d.ts +12 -0
- package/dist/blueprints/picture-post.js +17 -0
- package/dist/blueprints/poll.d.ts +41 -0
- package/dist/blueprints/poll.js +33 -0
- package/dist/blueprints/profile.d.ts +3 -0
- package/dist/blueprints/profile.js +7 -0
- package/dist/blueprints/reaction.d.ts +10 -0
- package/dist/blueprints/reaction.js +14 -0
- package/dist/blueprints/share.d.ts +12 -0
- package/dist/blueprints/share.js +25 -0
- package/dist/blueprints/stream.d.ts +6 -0
- package/dist/blueprints/stream.js +7 -0
- package/dist/blueprints/torrent.d.ts +23 -0
- package/dist/blueprints/torrent.js +25 -0
- package/dist/blueprints/wrapped-message.d.ts +20 -0
- package/dist/blueprints/wrapped-message.js +64 -0
- package/dist/helpers/app-data.d.ts +39 -0
- package/dist/helpers/app-data.js +68 -0
- package/dist/helpers/app-handler.d.ts +22 -0
- package/dist/helpers/app-handler.js +67 -0
- package/dist/helpers/article.d.ts +14 -0
- package/dist/helpers/article.js +24 -0
- package/dist/helpers/blossom.d.ts +11 -0
- package/dist/helpers/blossom.js +40 -0
- package/dist/helpers/bolt11.d.ts +10 -0
- package/dist/helpers/bolt11.js +17 -0
- package/dist/helpers/bookmark.d.ts +30 -0
- package/dist/helpers/bookmark.js +96 -0
- package/dist/helpers/calendar-event.d.ts +39 -0
- package/dist/helpers/calendar-event.js +121 -0
- package/dist/helpers/calendar-rsvp.d.ts +15 -0
- package/dist/helpers/calendar-rsvp.js +38 -0
- package/dist/helpers/calendar.d.ts +6 -0
- package/dist/helpers/calendar.js +11 -0
- package/dist/helpers/channels.d.ts +13 -0
- package/dist/helpers/channels.js +27 -0
- package/dist/helpers/comment.d.ts +47 -0
- package/dist/helpers/comment.js +185 -0
- package/dist/helpers/content.d.ts +3 -0
- package/dist/helpers/content.js +8 -0
- package/dist/helpers/emoji.d.ts +21 -0
- package/dist/helpers/emoji.js +34 -0
- package/dist/helpers/encrypted-content-cache.d.ts +22 -0
- package/dist/helpers/encrypted-content-cache.js +138 -0
- package/dist/helpers/file-metadata.d.ts +55 -0
- package/dist/helpers/file-metadata.js +130 -0
- package/dist/helpers/gift-wrap.d.ts +66 -0
- package/dist/helpers/gift-wrap.js +204 -0
- package/dist/helpers/groups-helper.d.ts +6 -0
- package/dist/helpers/groups-helper.js +9 -0
- package/dist/helpers/groups.d.ts +26 -0
- package/dist/helpers/groups.js +49 -0
- package/dist/helpers/hashtag.d.ts +2 -0
- package/dist/helpers/hashtag.js +7 -0
- package/dist/helpers/highlight.d.ts +45 -0
- package/dist/helpers/highlight.js +76 -0
- package/dist/helpers/index.d.ts +37 -0
- package/dist/helpers/index.js +37 -0
- package/dist/helpers/legacy-messages.d.ts +31 -0
- package/dist/helpers/legacy-messages.js +49 -0
- package/dist/helpers/lists.d.ts +58 -0
- package/dist/helpers/lists.js +110 -0
- package/dist/helpers/lnurl.d.ts +8 -0
- package/dist/helpers/lnurl.js +44 -0
- package/dist/helpers/mailboxes.d.ts +7 -0
- package/dist/helpers/mailboxes.js +49 -0
- package/dist/helpers/messages.d.ts +31 -0
- package/dist/helpers/messages.js +57 -0
- package/dist/helpers/mute.d.ts +33 -0
- package/dist/helpers/mute.js +111 -0
- package/dist/helpers/picture-post.d.ts +5 -0
- package/dist/helpers/picture-post.js +6 -0
- package/dist/helpers/poll.d.ts +46 -0
- package/dist/helpers/poll.js +78 -0
- package/dist/helpers/reaction.d.ts +8 -0
- package/dist/helpers/reaction.js +56 -0
- package/dist/helpers/relay-discovery.d.ts +87 -0
- package/dist/helpers/relay-discovery.js +126 -0
- package/dist/helpers/reports.d.ts +28 -0
- package/dist/helpers/reports.js +38 -0
- package/dist/helpers/share.d.ts +19 -0
- package/dist/helpers/share.js +58 -0
- package/dist/helpers/stream-chat.d.ts +4 -0
- package/dist/helpers/stream-chat.js +9 -0
- package/dist/helpers/stream.d.ts +31 -0
- package/dist/helpers/stream.js +81 -0
- package/dist/helpers/threading.d.ts +55 -0
- package/dist/helpers/threading.js +94 -0
- package/dist/helpers/torrent.d.ts +55 -0
- package/dist/helpers/torrent.js +270 -0
- package/dist/helpers/user-status.d.ts +18 -0
- package/dist/helpers/user-status.js +22 -0
- package/dist/helpers/wrapped-messages.d.ts +14 -0
- package/dist/helpers/wrapped-messages.js +23 -0
- package/dist/helpers/zap.d.ts +46 -0
- package/dist/helpers/zap.js +125 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/models/blossom.d.ts +11 -0
- package/dist/models/blossom.js +18 -0
- package/dist/models/bookmarks.d.ts +8 -0
- package/dist/models/bookmarks.js +24 -0
- package/dist/models/calendar.d.ts +6 -0
- package/dist/models/calendar.js +15 -0
- package/dist/models/channels.d.ts +11 -0
- package/dist/models/channels.js +61 -0
- package/dist/models/comments.d.ts +11 -0
- package/dist/models/comments.js +17 -0
- package/dist/models/gift-wrap.d.ts +7 -0
- package/dist/models/gift-wrap.js +20 -0
- package/dist/models/index.d.ts +15 -0
- package/dist/models/index.js +16 -0
- package/dist/models/legacy-messages.d.ts +14 -0
- package/dist/models/legacy-messages.js +64 -0
- package/dist/models/mutes.d.ts +16 -0
- package/dist/models/mutes.js +34 -0
- package/dist/models/pins.d.ts +4 -0
- package/dist/models/pins.js +10 -0
- package/dist/models/reactions.d.ts +11 -0
- package/dist/models/reactions.js +21 -0
- package/dist/models/thread.d.ts +33 -0
- package/dist/models/thread.js +93 -0
- package/dist/models/user-status.d.ts +11 -0
- package/dist/models/user-status.js +32 -0
- package/dist/models/wrapped-messages.d.ts +31 -0
- package/dist/models/wrapped-messages.js +76 -0
- package/dist/models/zaps.d.ts +9 -0
- package/dist/models/zaps.js +26 -0
- package/dist/operations/app-data.d.ts +6 -0
- package/dist/operations/app-data.js +21 -0
- package/dist/operations/blossom.d.ts +5 -0
- package/dist/operations/blossom.js +13 -0
- package/dist/operations/calendar-event.d.ts +34 -0
- package/dist/operations/calendar-event.js +72 -0
- package/dist/operations/calendar-rsvp.d.ts +10 -0
- package/dist/operations/calendar-rsvp.js +35 -0
- package/dist/operations/calendar.d.ts +9 -0
- package/dist/operations/calendar.js +15 -0
- package/dist/operations/channel.d.ts +4 -0
- package/dist/operations/channel.js +10 -0
- package/dist/operations/client.d.ts +4 -0
- package/dist/operations/client.js +23 -0
- package/dist/operations/comment.d.ts +4 -0
- package/dist/operations/comment.js +11 -0
- package/dist/operations/file-metadata.d.ts +4 -0
- package/dist/operations/file-metadata.js +21 -0
- package/dist/operations/geohash.d.ts +5 -0
- package/dist/operations/geohash.js +17 -0
- package/dist/operations/gift-wrap.d.ts +13 -0
- package/dist/operations/gift-wrap.js +93 -0
- package/dist/operations/group.d.ts +11 -0
- package/dist/operations/group.js +34 -0
- package/dist/operations/hashtags.d.ts +7 -0
- package/dist/operations/hashtags.js +17 -0
- package/dist/operations/highlight.d.ts +18 -0
- package/dist/operations/highlight.js +47 -0
- package/dist/operations/index.d.ts +28 -0
- package/dist/operations/index.js +28 -0
- package/dist/operations/legacy-message.d.ts +6 -0
- package/dist/operations/legacy-message.js +13 -0
- package/dist/operations/list.d.ts +7 -0
- package/dist/operations/list.js +14 -0
- package/dist/operations/live-stream.d.ts +4 -0
- package/dist/operations/live-stream.js +11 -0
- package/dist/operations/media-attachment.d.ts +4 -0
- package/dist/operations/media-attachment.js +12 -0
- package/dist/operations/note.d.ts +9 -0
- package/dist/operations/note.js +42 -0
- package/dist/operations/picture-post.d.ts +4 -0
- package/dist/operations/picture-post.js +14 -0
- package/dist/operations/poll-response.d.ts +9 -0
- package/dist/operations/poll-response.js +20 -0
- package/dist/operations/poll.d.ts +19 -0
- package/dist/operations/poll.js +42 -0
- package/dist/operations/reaction.d.ts +7 -0
- package/dist/operations/reaction.js +39 -0
- package/dist/operations/share.d.ts +8 -0
- package/dist/operations/share.js +34 -0
- package/dist/operations/stream-chat.d.ts +7 -0
- package/dist/operations/stream-chat.js +27 -0
- package/dist/operations/stream.d.ts +41 -0
- package/dist/operations/stream.js +83 -0
- package/dist/operations/tag/bookmarks.d.ts +6 -0
- package/dist/operations/tag/bookmarks.js +20 -0
- package/dist/operations/tag/index.d.ts +1 -0
- package/dist/operations/tag/index.js +1 -0
- package/dist/operations/torrent.d.ts +33 -0
- package/dist/operations/torrent.js +66 -0
- package/dist/operations/wrapped-message.d.ts +12 -0
- package/dist/operations/wrapped-message.js +28 -0
- package/dist/operations/zap-split.d.ts +10 -0
- package/dist/operations/zap-split.js +20 -0
- package/dist/register.d.ts +11 -0
- package/dist/register.js +13 -0
- 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
|
+
}
|