applesauce-core 0.0.0-next-20241204152949 → 0.0.0-next-20241207161834

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.
@@ -3,15 +3,19 @@ import { Observable } from "rxjs";
3
3
  import { Database } from "./database.js";
4
4
  export declare class EventStore {
5
5
  database: Database;
6
- /** Whether to keep old versions of replaceable events */
6
+ /** Enable this to keep old versions of replaceable events */
7
7
  keepOldVersions: boolean;
8
8
  constructor();
9
- /** Adds an event to the database */
9
+ /** Adds an event to the database and update subscriptions */
10
10
  add(event: NostrEvent, fromRelay?: string): NostrEvent;
11
+ /** Removes an event from the database and updates subscriptions */
12
+ remove(event: string | NostrEvent): boolean;
11
13
  protected deletedIds: Set<string>;
12
14
  protected deletedCoords: Map<string, number>;
13
15
  protected handleDeleteEvent(deleteEvent: NostrEvent): void;
14
16
  protected checkDeleted(event: NostrEvent): boolean;
17
+ /** Removes any event that is not being used by a subscription */
18
+ prune(max?: number): number;
15
19
  /** Add an event to the store and notifies all subscribes it has updated */
16
20
  update(event: NostrEvent): NostrEvent;
17
21
  getAll(filters: Filter[]): Set<NostrEvent>;
@@ -34,8 +38,12 @@ export declare class EventStore {
34
38
  pubkey: string;
35
39
  identifier?: string;
36
40
  }[]): Observable<Map<string, NostrEvent>>;
37
- /** Creates an observable that streams all events that match the filter */
38
- stream(filters: Filter[]): Observable<NostrEvent>;
41
+ /**
42
+ * Creates an observable that streams all events that match the filter
43
+ * @param filters
44
+ * @param [onlyNew=false] Only subscribe to new events
45
+ */
46
+ stream(filters: Filter | Filter[], onlyNew?: boolean): Observable<NostrEvent>;
39
47
  /** Creates an observable that updates with an array of sorted events */
40
- timeline(filters: Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
48
+ timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<NostrEvent[]>;
41
49
  }
@@ -9,12 +9,12 @@ import { addSeenRelay } from "../helpers/relays.js";
9
9
  import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
10
10
  export class EventStore {
11
11
  database;
12
- /** Whether to keep old versions of replaceable events */
12
+ /** Enable this to keep old versions of replaceable events */
13
13
  keepOldVersions = false;
14
14
  constructor() {
15
15
  this.database = new Database();
16
16
  }
17
- /** Adds an event to the database */
17
+ /** Adds an event to the database and update subscriptions */
18
18
  add(event, fromRelay) {
19
19
  if (event.kind === kinds.EventDeletion)
20
20
  this.handleDeleteEvent(event);
@@ -40,6 +40,16 @@ export class EventStore {
40
40
  addSeenRelay(inserted, fromRelay);
41
41
  return inserted;
42
42
  }
43
+ /** Removes an event from the database and updates subscriptions */
44
+ remove(event) {
45
+ if (typeof event === "string")
46
+ return this.database.deleteEvent(event);
47
+ else if (this.database.hasEvent(event.id)) {
48
+ return this.database.deleteEvent(event.id);
49
+ }
50
+ else
51
+ return false;
52
+ }
43
53
  deletedIds = new Set();
44
54
  deletedCoords = new Map();
45
55
  handleDeleteEvent(deleteEvent) {
@@ -70,6 +80,10 @@ export class EventStore {
70
80
  }
71
81
  return false;
72
82
  }
83
+ /** Removes any event that is not being used by a subscription */
84
+ prune(max) {
85
+ return this.database.prune(max);
86
+ }
73
87
  /** Add an event to the store and notifies all subscribes it has updated */
74
88
  update(event) {
75
89
  return this.database.updateEvent(event);
@@ -282,35 +296,30 @@ export class EventStore {
282
296
  };
283
297
  });
284
298
  }
285
- /** Creates an observable that streams all events that match the filter */
286
- stream(filters) {
299
+ /**
300
+ * Creates an observable that streams all events that match the filter
301
+ * @param filters
302
+ * @param [onlyNew=false] Only subscribe to new events
303
+ */
304
+ stream(filters, onlyNew = false) {
305
+ filters = Array.isArray(filters) ? filters : [filters];
287
306
  return new Observable((observer) => {
288
- let claimed = new Set();
289
- let events = this.database.getForFilters(filters);
290
- for (const event of events) {
291
- observer.next(event);
292
- this.database.claimEvent(event, observer);
293
- claimed.add(event);
307
+ if (!onlyNew) {
308
+ let events = this.database.getForFilters(filters);
309
+ for (const event of events)
310
+ observer.next(event);
294
311
  }
295
312
  // subscribe to future events
296
313
  const sub = this.database.inserted.subscribe((event) => {
297
- if (matchFilters(filters, event)) {
314
+ if (matchFilters(filters, event))
298
315
  observer.next(event);
299
- this.database.claimEvent(event, observer);
300
- claimed.add(event);
301
- }
302
316
  });
303
- return () => {
304
- sub.unsubscribe();
305
- // remove all claims
306
- for (const event of claimed)
307
- this.database.removeClaim(event, observer);
308
- claimed.clear();
309
- };
317
+ return () => sub.unsubscribe();
310
318
  });
311
319
  }
312
320
  /** Creates an observable that updates with an array of sorted events */
313
- timeline(filters, keepOldVersions = this.keepOldVersions) {
321
+ timeline(filters, keepOldVersions = false) {
322
+ filters = Array.isArray(filters) ? filters : [filters];
314
323
  return new Observable((observer) => {
315
324
  const seen = new Map();
316
325
  const timeline = [];
@@ -1,5 +1,6 @@
1
1
  import { EventTemplate, NostrEvent } from "nostr-tools";
2
- export declare function getEmojiTag(event: NostrEvent | EventTemplate, code: string): ["emoji", string, string];
2
+ /** Gets an "emoji" tag that matches an emoji code */
3
+ export declare function getEmojiTag(event: NostrEvent | EventTemplate, code: string): ["emoji", string, string] | undefined;
3
4
  /** Returns the name of a NIP-30 emoji pack */
4
5
  export declare function getPackName(pack: NostrEvent): string | undefined;
5
6
  /** Returns an array of emojis from a NIP-30 emoji pack */
@@ -1,4 +1,5 @@
1
1
  import { getTagValue } from "./event.js";
2
+ /** Gets an "emoji" tag that matches an emoji code */
2
3
  export function getEmojiTag(event, code) {
3
4
  code = code.replace(/^:|:$/g, "").toLocaleLowerCase();
4
5
  return event.tags.filter((t) => t[0] === "emoji" && t[1] && t[2]).find((t) => t[1].toLowerCase() === code);
@@ -9,6 +9,11 @@ declare module "nostr-tools" {
9
9
  [FromCacheSymbol]?: boolean;
10
10
  }
11
11
  }
12
+ /**
13
+ * Checks if an object is a nostr event
14
+ * NOTE: does not validation the signature on the event
15
+ */
16
+ export declare function isEvent(event: any): event is NostrEvent;
12
17
  /**
13
18
  * Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
14
19
  * or parameterized replaceable ( 30000 <= n < 40000 )
@@ -4,6 +4,22 @@ import { getHiddenTags } from "./hidden-tags.js";
4
4
  export const EventUIDSymbol = Symbol.for("event-uid");
5
5
  export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
6
6
  export const FromCacheSymbol = Symbol.for("from-cache");
7
+ /**
8
+ * Checks if an object is a nostr event
9
+ * NOTE: does not validation the signature on the event
10
+ */
11
+ export function isEvent(event) {
12
+ if (event === undefined || event === null)
13
+ return false;
14
+ return (event.id?.length === 64 &&
15
+ typeof event.sig === "string" &&
16
+ typeof event.pubkey === "string" &&
17
+ event.pubkey.length === 64 &&
18
+ typeof event.content === "string" &&
19
+ Array.isArray(event.tags) &&
20
+ typeof event.created_at === "number" &&
21
+ event.created_at > 0);
22
+ }
7
23
  /**
8
24
  * Returns if a kind is replaceable ( 10000 <= n < 20000 || n == 0 || n == 3 )
9
25
  * or parameterized replaceable ( 30000 <= n < 40000 )
@@ -1,26 +1,33 @@
1
1
  import { EventTemplate, NostrEvent, UnsignedEvent } from "nostr-tools";
2
2
  import { EventStore } from "applesauce-core";
3
3
  export type HiddenTagsSigner = {
4
- nip04: {
4
+ nip04?: {
5
+ encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
6
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
7
+ };
8
+ nip44?: {
5
9
  encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
6
10
  decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
7
11
  };
8
12
  };
9
13
  export type TagOperation = (tags: string[][]) => string[][];
10
14
  export declare const HiddenTagsSymbol: unique symbol;
11
- export declare const EventsWithHiddenTags: number[];
15
+ /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
16
+ export declare const EventEncryptionMethod: Record<number, "nip04" | "nip44">;
12
17
  /** Checks if an event can have hidden tags */
13
- export declare function canHaveHiddenTags(event: NostrEvent | EventTemplate): boolean;
18
+ export declare function canHaveHiddenTags(kind: number): boolean;
14
19
  /** Checks if an event has hidden tags */
15
20
  export declare function hasHiddenTags(event: NostrEvent | EventTemplate): boolean;
16
- /** Returns the hidden tags from an event if they are unlocked */
21
+ /** Returns the hidden tags for an event if they are unlocked */
17
22
  export declare function getHiddenTags(event: NostrEvent | EventTemplate): string[][] | undefined;
23
+ /** Checks if the hidden tags are locked */
18
24
  export declare function isHiddenTagsLocked(event: NostrEvent): boolean;
19
25
  /**
20
26
  * Decrypts the private list
21
27
  * @param event The list event to decrypt
22
28
  * @param signer A signer to use to decrypt the tags
23
29
  * @param store An optional EventStore to notify about the update
30
+ * @throws
24
31
  */
25
32
  export declare function unlockHiddenTags(event: NostrEvent, signer: HiddenTagsSigner, store?: EventStore): Promise<NostrEvent>;
26
33
  /**
@@ -28,6 +35,7 @@ export declare function unlockHiddenTags(event: NostrEvent, signer: HiddenTagsSi
28
35
  * @param event Event to modify
29
36
  * @param operations Operations for hidden and public tags
30
37
  * @param signer A signer to use to decrypt the tags
38
+ * @throws
31
39
  */
32
40
  export declare function modifyEventTags(event: NostrEvent | UnsignedEvent, operations: {
33
41
  public?: TagOperation;
@@ -35,5 +43,6 @@ export declare function modifyEventTags(event: NostrEvent | UnsignedEvent, opera
35
43
  }, signer?: HiddenTagsSigner): Promise<EventTemplate>;
36
44
  /**
37
45
  * Override the hidden tags in an event
46
+ * @throws
38
47
  */
39
48
  export declare function overrideHiddenTags(event: NostrEvent, hidden: string[][], signer: HiddenTagsSigner): Promise<EventTemplate>;
@@ -1,47 +1,59 @@
1
1
  import { unixNow } from "applesauce-core/helpers";
2
2
  import { kinds } from "nostr-tools";
3
3
  export const HiddenTagsSymbol = Symbol.for("hidden-tags");
4
- export const EventsWithHiddenTags = [
5
- 37375, // NIP-60 wallet
4
+ /** Various event kinds that can have encrypted tags in their content and which encryption method they use */
5
+ export const EventEncryptionMethod = {
6
+ // NIP-60 wallet
7
+ 37375: "nip44",
6
8
  // NIP-51 lists
7
- kinds.BookmarkList,
8
- kinds.InterestsList,
9
- kinds.Mutelist,
10
- kinds.CommunitiesList,
11
- kinds.PublicChatsList,
12
- kinds.SearchRelaysList,
13
- kinds.SearchRelaysList,
14
- 10009, // NIP-29 groups
9
+ [kinds.BookmarkList]: "nip04",
10
+ [kinds.InterestsList]: "nip04",
11
+ [kinds.Mutelist]: "nip04",
12
+ [kinds.CommunitiesList]: "nip04",
13
+ [kinds.PublicChatsList]: "nip04",
14
+ [kinds.SearchRelaysList]: "nip04",
15
15
  // NIP-51 sets
16
- kinds.Bookmarksets,
17
- kinds.Relaysets,
18
- kinds.Followsets,
19
- kinds.Curationsets,
20
- kinds.Interestsets,
21
- ];
16
+ [kinds.Bookmarksets]: "nip04",
17
+ [kinds.Relaysets]: "nip04",
18
+ [kinds.Followsets]: "nip04",
19
+ [kinds.Curationsets]: "nip04",
20
+ [kinds.Interestsets]: "nip04",
21
+ };
22
22
  /** Checks if an event can have hidden tags */
23
- export function canHaveHiddenTags(event) {
24
- return EventsWithHiddenTags.includes(event.kind);
23
+ export function canHaveHiddenTags(kind) {
24
+ return EventEncryptionMethod[kind] !== undefined;
25
25
  }
26
26
  /** Checks if an event has hidden tags */
27
27
  export function hasHiddenTags(event) {
28
- return canHaveHiddenTags(event) && event.content.length > 0;
28
+ return canHaveHiddenTags(event.kind) && event.content.length > 0;
29
29
  }
30
- /** Returns the hidden tags from an event if they are unlocked */
30
+ /** Returns the hidden tags for an event if they are unlocked */
31
31
  export function getHiddenTags(event) {
32
32
  return Reflect.get(event, HiddenTagsSymbol);
33
33
  }
34
+ /** Checks if the hidden tags are locked */
34
35
  export function isHiddenTagsLocked(event) {
35
36
  return hasHiddenTags(event) && getHiddenTags(event) === undefined;
36
37
  }
38
+ function getEventEncryption(kind, signer) {
39
+ const method = EventEncryptionMethod[kind];
40
+ const encryption = signer[method];
41
+ if (!encryption)
42
+ throw new Error(`Signer does not support ${method} encryption`);
43
+ return encryption;
44
+ }
37
45
  /**
38
46
  * Decrypts the private list
39
47
  * @param event The list event to decrypt
40
48
  * @param signer A signer to use to decrypt the tags
41
49
  * @param store An optional EventStore to notify about the update
50
+ * @throws
42
51
  */
43
52
  export async function unlockHiddenTags(event, signer, store) {
44
- const plaintext = await signer.nip04.decrypt(event.pubkey, event.content);
53
+ if (!canHaveHiddenTags(event.kind))
54
+ throw new Error("Event kind does not support hidden tags");
55
+ const encryption = getEventEncryption(event.kind, signer);
56
+ const plaintext = await encryption.decrypt(event.pubkey, event.content);
45
57
  const parsed = JSON.parse(plaintext);
46
58
  if (!Array.isArray(parsed))
47
59
  throw new Error("Content is not an array of tags");
@@ -57,6 +69,7 @@ export async function unlockHiddenTags(event, signer, store) {
57
69
  * @param event Event to modify
58
70
  * @param operations Operations for hidden and public tags
59
71
  * @param signer A signer to use to decrypt the tags
72
+ * @throws
60
73
  */
61
74
  export async function modifyEventTags(event, operations, signer) {
62
75
  const draft = { content: event.content, tags: event.tags, kind: event.kind, created_at: unixNow() };
@@ -66,21 +79,26 @@ export async function modifyEventTags(event, operations, signer) {
66
79
  if (operations.hidden) {
67
80
  if (!signer)
68
81
  throw new Error("Missing signer for hidden tags");
69
- if (!canHaveHiddenTags(event))
70
- throw new Error("Event can not have hidden tags");
82
+ if (!canHaveHiddenTags(event.kind))
83
+ throw new Error("Event kind does not support hidden tags");
71
84
  const hidden = hasHiddenTags(event) ? getHiddenTags(event) : [];
72
85
  if (!hidden)
73
86
  throw new Error("Hidden tags are locked");
74
87
  const newHidden = operations.hidden(hidden);
75
- draft.content = await signer.nip04.encrypt(event.pubkey, JSON.stringify(newHidden));
88
+ const encryption = getEventEncryption(event.kind, signer);
89
+ draft.content = await encryption.encrypt(event.pubkey, JSON.stringify(newHidden));
76
90
  }
77
91
  return draft;
78
92
  }
79
93
  /**
80
94
  * Override the hidden tags in an event
95
+ * @throws
81
96
  */
82
97
  export async function overrideHiddenTags(event, hidden, signer) {
83
- const ciphertext = await signer.nip04.encrypt(event.pubkey, JSON.stringify(hidden));
98
+ if (!canHaveHiddenTags(event.kind))
99
+ throw new Error("Event kind does not support hidden tags");
100
+ const encryption = getEventEncryption(event.kind, signer);
101
+ const ciphertext = await encryption.encrypt(event.pubkey, JSON.stringify(hidden));
84
102
  return {
85
103
  kind: event.kind,
86
104
  content: ciphertext,
@@ -15,13 +15,17 @@ export declare function parseCoordinate(a: string, requireD: false, silent: true
15
15
  export declare function getPubkeyFromDecodeResult(result?: DecodeResult): string | undefined;
16
16
  /** Encodes the result of nip19.decode */
17
17
  export declare function encodeDecodeResult(result: DecodeResult): "" | `nprofile1${string}` | `nevent1${string}` | `naddr1${string}` | `nsec1${string}` | `npub1${string}` | `note1${string}`;
18
+ /** @throws */
18
19
  export declare function getEventPointerFromTag(tag: string[]): EventPointer;
20
+ /** @throws */
19
21
  export declare function getAddressPointerFromTag(tag: string[]): AddressPointer;
22
+ /** @throws */
20
23
  export declare function getProfilePointerFromTag(tag: string[]): ProfilePointer;
21
- /** Parses a tag into a pointer */
24
+ /** Parses "e", "a", "p", and "q" tags into a pointer */
22
25
  export declare function getPointerFromTag(tag: string[]): DecodeResult | null;
23
26
  export declare function isAddressPointer(pointer: DecodeResult["data"]): pointer is AddressPointer;
24
27
  export declare function isEventPointer(pointer: DecodeResult["data"]): pointer is EventPointer;
28
+ /** Returns the coordinate string for an AddressPointer */
25
29
  export declare function getCoordinateFromAddressPointer(pointer: AddressPointer): string;
26
30
  /** Returns a tag for an address pointer */
27
31
  export declare function getATagFromAddressPointer(pointer: AddressPointer): ["a", ...string[]];
@@ -65,6 +65,7 @@ export function encodeDecodeResult(result) {
65
65
  }
66
66
  return "";
67
67
  }
68
+ /** @throws */
68
69
  export function getEventPointerFromTag(tag) {
69
70
  if (!tag[1])
70
71
  throw new Error("Missing event id in tag");
@@ -76,6 +77,7 @@ export function getEventPointerFromTag(tag) {
76
77
  pointer.author = tag[3];
77
78
  return pointer;
78
79
  }
80
+ /** @throws */
79
81
  export function getAddressPointerFromTag(tag) {
80
82
  if (!tag[1])
81
83
  throw new Error("Missing coordinate in tag");
@@ -84,6 +86,7 @@ export function getAddressPointerFromTag(tag) {
84
86
  pointer.relays = safeRelayUrls([tag[2]]);
85
87
  return pointer;
86
88
  }
89
+ /** @throws */
87
90
  export function getProfilePointerFromTag(tag) {
88
91
  if (!tag[1])
89
92
  throw new Error("Missing pubkey in tag");
@@ -92,7 +95,7 @@ export function getProfilePointerFromTag(tag) {
92
95
  pointer.relays = safeRelayUrls([tag[2]]);
93
96
  return pointer;
94
97
  }
95
- /** Parses a tag into a pointer */
98
+ /** Parses "e", "a", "p", and "q" tags into a pointer */
96
99
  export function getPointerFromTag(tag) {
97
100
  try {
98
101
  switch (tag[0]) {
@@ -115,13 +118,14 @@ export function getPointerFromTag(tag) {
115
118
  }
116
119
  export function isAddressPointer(pointer) {
117
120
  return (typeof pointer !== "string" &&
118
- Object.hasOwn(pointer, "identifier") &&
119
- Object.hasOwn(pointer, "pubkey") &&
120
- Object.hasOwn(pointer, "kind"));
121
+ Reflect.has(pointer, "identifier") &&
122
+ Reflect.has(pointer, "pubkey") &&
123
+ Reflect.has(pointer, "kind"));
121
124
  }
122
125
  export function isEventPointer(pointer) {
123
- return typeof pointer !== "string" && Object.hasOwn(pointer, "id");
126
+ return typeof pointer !== "string" && Reflect.has(pointer, "id");
124
127
  }
128
+ /** Returns the coordinate string for an AddressPointer */
125
129
  export function getCoordinateFromAddressPointer(pointer) {
126
130
  return `${pointer.kind}:${pointer.pubkey}:${pointer.identifier}`;
127
131
  }
@@ -1,6 +1,12 @@
1
+ /** Checks if tag is an "e" tag and has at least one value */
1
2
  export declare function isETag(tag: string[]): tag is ["e", string, ...string[]];
3
+ /** Checks if tag is an "p" tag and has at least one value */
2
4
  export declare function isPTag(tag: string[]): tag is ["p", string, ...string[]];
5
+ /** Checks if tag is an "r" tag and has at least one value */
3
6
  export declare function isRTag(tag: string[]): tag is ["r", string, ...string[]];
7
+ /** Checks if tag is an "d" tag and has at least one value */
4
8
  export declare function isDTag(tag: string[]): tag is ["d", string, ...string[]];
9
+ /** Checks if tag is an "a" tag and has at least one value */
5
10
  export declare function isATag(tag: string[]): tag is ["a", string, ...string[]];
11
+ /** Checks if tag is an "a" tag and has at least one value */
6
12
  export declare function isTTag(tag: string[]): tag is ["t", string, ...string[]];
@@ -1,18 +1,24 @@
1
+ /** Checks if tag is an "e" tag and has at least one value */
1
2
  export function isETag(tag) {
2
3
  return tag[0] === "e" && tag[1] !== undefined;
3
4
  }
5
+ /** Checks if tag is an "p" tag and has at least one value */
4
6
  export function isPTag(tag) {
5
7
  return tag[0] === "p" && tag[1] !== undefined;
6
8
  }
9
+ /** Checks if tag is an "r" tag and has at least one value */
7
10
  export function isRTag(tag) {
8
11
  return tag[0] === "r" && tag[1] !== undefined;
9
12
  }
13
+ /** Checks if tag is an "d" tag and has at least one value */
10
14
  export function isDTag(tag) {
11
15
  return tag[0] === "d" && tag[1] !== undefined;
12
16
  }
17
+ /** Checks if tag is an "a" tag and has at least one value */
13
18
  export function isATag(tag) {
14
19
  return tag[0] === "a" && tag[1] !== undefined;
15
20
  }
21
+ /** Checks if tag is an "a" tag and has at least one value */
16
22
  export function isTTag(tag) {
17
23
  return tag[0] === "a" && tag[1] !== undefined;
18
24
  }
@@ -4,8 +4,11 @@ export declare const IMAGE_EXT: string[];
4
4
  export declare const VIDEO_EXT: string[];
5
5
  export declare const STREAM_EXT: string[];
6
6
  export declare const AUDIO_EXT: string[];
7
- export declare function isVisualMediaURL(url: string | URL): boolean;
7
+ /** Checks if a url is a image URL */
8
8
  export declare function isImageURL(url: string | URL): boolean;
9
+ /** Checks if a url is a video URL */
9
10
  export declare function isVideoURL(url: string | URL): boolean;
11
+ /** Checks if a url is a stream URL */
10
12
  export declare function isStreamURL(url: string | URL): boolean;
13
+ /** Checks if a url is a audio URL */
11
14
  export declare function isAudioURL(url: string | URL): boolean;
@@ -4,24 +4,25 @@ export const IMAGE_EXT = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".av
4
4
  export const VIDEO_EXT = [".mp4", ".mkv", ".webm", ".mov"];
5
5
  export const STREAM_EXT = [".m3u8"];
6
6
  export const AUDIO_EXT = [".mp3", ".wav", ".ogg", ".aac"];
7
- export function isVisualMediaURL(url) {
8
- return isImageURL(url) || isVideoURL(url) || isStreamURL(url);
9
- }
7
+ /** Checks if a url is a image URL */
10
8
  export function isImageURL(url) {
11
9
  url = convertToUrl(url);
12
10
  const filename = getURLFilename(url);
13
11
  return !!filename && IMAGE_EXT.some((ext) => filename.endsWith(ext));
14
12
  }
13
+ /** Checks if a url is a video URL */
15
14
  export function isVideoURL(url) {
16
15
  url = convertToUrl(url);
17
16
  const filename = getURLFilename(url);
18
17
  return !!filename && VIDEO_EXT.some((ext) => filename.endsWith(ext));
19
18
  }
19
+ /** Checks if a url is a stream URL */
20
20
  export function isStreamURL(url) {
21
21
  url = convertToUrl(url);
22
22
  const filename = getURLFilename(url);
23
23
  return !!filename && STREAM_EXT.some((ext) => filename.endsWith(ext));
24
24
  }
25
+ /** Checks if a url is a audio URL */
25
26
  export function isAudioURL(url) {
26
27
  url = convertToUrl(url);
27
28
  const filename = getURLFilename(url);
@@ -4,11 +4,28 @@ export declare const ZapFromSymbol: unique symbol;
4
4
  export declare const ZapInvoiceSymbol: unique symbol;
5
5
  export declare const ZapEventPointerSymbol: unique symbol;
6
6
  export declare const ZapAddressPointerSymbol: unique symbol;
7
+ /** Returns the senders pubkey */
7
8
  export declare function getZapSender(zap: NostrEvent): string;
9
+ /**
10
+ * Gets the receivers pubkey
11
+ * @throws
12
+ */
8
13
  export declare function getZapRecipient(zap: NostrEvent): string;
14
+ /** Returns the parsed bolt11 invoice */
9
15
  export declare function getZapPayment(zap: NostrEvent): import("./bolt11.js").ParsedInvoice | undefined;
16
+ /** Gets the AddressPointer that was zapped */
10
17
  export declare function getZapAddressPointer(zap: NostrEvent): import("nostr-tools/nip19").AddressPointer | null;
18
+ /** Gets the EventPointer that was zapped */
11
19
  export declare function getZapEventPointer(zap: NostrEvent): import("nostr-tools/nip19").EventPointer | null;
20
+ /** Gets the preimage for the bolt11 invoice */
12
21
  export declare function getZapPreimage(zap: NostrEvent): string | undefined;
22
+ /**
23
+ * Returns the zap request event inside the zap receipt
24
+ * @throws
25
+ */
13
26
  export declare function getZapRequest(zap: NostrEvent): import("nostr-tools").Event;
27
+ /**
28
+ * Checks if the zap is valid
29
+ * DOES NOT validate LNURL address
30
+ */
14
31
  export declare function isValidZap(zap?: NostrEvent): boolean;
@@ -9,36 +9,49 @@ export const ZapFromSymbol = Symbol.for("zap-from");
9
9
  export const ZapInvoiceSymbol = Symbol.for("zap-bolt11");
10
10
  export const ZapEventPointerSymbol = Symbol.for("zap-event-pointer");
11
11
  export const ZapAddressPointerSymbol = Symbol.for("zap-address-pointer");
12
+ /** Returns the senders pubkey */
12
13
  export function getZapSender(zap) {
13
14
  return getTagValue(zap, "P") || getZapRequest(zap).pubkey;
14
15
  }
16
+ /**
17
+ * Gets the receivers pubkey
18
+ * @throws
19
+ */
15
20
  export function getZapRecipient(zap) {
16
21
  const recipient = getTagValue(zap, "p");
17
22
  if (!recipient)
18
23
  throw new Error("Missing recipient");
19
24
  return recipient;
20
25
  }
26
+ /** Returns the parsed bolt11 invoice */
21
27
  export function getZapPayment(zap) {
22
28
  return getOrComputeCachedValue(zap, ZapInvoiceSymbol, () => {
23
29
  const bolt11 = getTagValue(zap, "bolt11");
24
30
  return bolt11 ? parseBolt11(bolt11) : undefined;
25
31
  });
26
32
  }
33
+ /** Gets the AddressPointer that was zapped */
27
34
  export function getZapAddressPointer(zap) {
28
35
  return getOrComputeCachedValue(zap, ZapAddressPointerSymbol, () => {
29
36
  const a = zap.tags.find(isATag);
30
37
  return a ? getAddressPointerFromTag(a) : null;
31
38
  });
32
39
  }
40
+ /** Gets the EventPointer that was zapped */
33
41
  export function getZapEventPointer(zap) {
34
42
  return getOrComputeCachedValue(zap, ZapEventPointerSymbol, () => {
35
43
  const e = zap.tags.find(isETag);
36
44
  return e ? getEventPointerFromTag(e) : null;
37
45
  });
38
46
  }
47
+ /** Gets the preimage for the bolt11 invoice */
39
48
  export function getZapPreimage(zap) {
40
49
  return getTagValue(zap, "preimage");
41
50
  }
51
+ /**
52
+ * Returns the zap request event inside the zap receipt
53
+ * @throws
54
+ */
42
55
  export function getZapRequest(zap) {
43
56
  return getOrComputeCachedValue(zap, ZapRequestSymbol, () => {
44
57
  const description = getTagValue(zap, "description");
@@ -50,6 +63,10 @@ export function getZapRequest(zap) {
50
63
  return JSON.parse(description);
51
64
  });
52
65
  }
66
+ /**
67
+ * Checks if the zap is valid
68
+ * DOES NOT validate LNURL address
69
+ */
53
70
  export function isValidZap(zap) {
54
71
  if (!zap)
55
72
  return false;
@@ -1,2 +1,3 @@
1
1
  import { Observable } from "rxjs";
2
+ /** Subscribes and returns the observables current or next value */
2
3
  export declare function getValue<T>(observable: Observable<T>): T | Promise<T>;
@@ -1,4 +1,5 @@
1
1
  import { BehaviorSubject } from "rxjs";
2
+ /** Subscribes and returns the observables current or next value */
2
3
  export function getValue(observable) {
3
4
  if (observable instanceof BehaviorSubject)
4
5
  return observable.value;
@@ -2,4 +2,5 @@ export type Deferred<T> = Promise<T> & {
2
2
  resolve: (value?: T | PromiseLike<T>) => void;
3
3
  reject: (reason?: any) => void;
4
4
  };
5
+ /** Creates a controlled promise */
5
6
  export declare function createDefer<T>(): Deferred<T>;
@@ -1,3 +1,4 @@
1
+ /** Creates a controlled promise */
1
2
  export function createDefer() {
2
3
  let _resolve;
3
4
  let _reject;
@@ -1,4 +1,5 @@
1
1
  import { Query } from "../query-store/index.js";
2
+ /** A query that gets and parses the inbox and outbox relays for a pubkey */
2
3
  export declare function MailboxesQuery(pubkey: string): Query<{
3
4
  inboxes: string[];
4
5
  outboxes: string[];
@@ -1,6 +1,7 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { map } from "rxjs/operators";
3
3
  import { getInboxes, getOutboxes } from "../helpers/mailboxes.js";
4
+ /** A query that gets and parses the inbox and outbox relays for a pubkey */
4
5
  export function MailboxesQuery(pubkey) {
5
6
  return {
6
7
  key: pubkey,
@@ -1,3 +1,4 @@
1
1
  import { ProfileContent } from "../helpers/profile.js";
2
2
  import { Query } from "../query-store/index.js";
3
+ /** A query that gets and parses the kind 0 metadata for a pubkey */
3
4
  export declare function ProfileQuery(pubkey: string): Query<ProfileContent | undefined>;
@@ -1,6 +1,7 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { filter, map } from "rxjs/operators";
3
3
  import { getProfileContent, isValidProfile } from "../helpers/profile.js";
4
+ /** A query that gets and parses the kind 0 metadata for a pubkey */
4
5
  export function ProfileQuery(pubkey) {
5
6
  return {
6
7
  key: pubkey,
@@ -1,4 +1,4 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
2
  import { Query } from "../query-store/index.js";
3
- /** Creates a query that returns all reactions to an event (supports replaceable events) */
3
+ /** A query that returns all reactions to an event (supports replaceable events) */
4
4
  export declare function ReactionsQuery(event: NostrEvent): Query<NostrEvent[]>;
@@ -1,6 +1,6 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { getEventUID, isReplaceable } from "../helpers/event.js";
3
- /** Creates a query that returns all reactions to an event (supports replaceable events) */
3
+ /** A query that returns all reactions to an event (supports replaceable events) */
4
4
  export function ReactionsQuery(event) {
5
5
  return {
6
6
  key: getEventUID(event),
@@ -36,5 +36,3 @@ export function ReplaceableSetQuery(pointers) {
36
36
  run: (events) => events.replaceableSet(pointers),
37
37
  };
38
38
  }
39
- // @ts-expect-error
40
- window.hash_sum = hash_sum;
@@ -21,3 +21,5 @@ export type ThreadQueryOptions = {
21
21
  kinds?: number[];
22
22
  };
23
23
  export declare function ThreadQuery(root: string | AddressPointer | EventPointer, opts?: ThreadQueryOptions): Query<Thread>;
24
+ /** A query that gets all legacy and NIP-10 replies for an event */
25
+ export declare function RepliesQuery(event: NostrEvent, overrideKinds?: number[]): Query<NostrEvent[]>;
@@ -1,8 +1,9 @@
1
1
  import { kinds } from "nostr-tools";
2
+ import { isParameterizedReplaceableKind } from "nostr-tools/kinds";
2
3
  import { map } from "rxjs/operators";
3
- import { getNip10References } from "../helpers/threading.js";
4
- import { getCoordinateFromAddressPointer, isAddressPointer } from "../helpers/pointers.js";
5
- import { getEventUID } from "../helpers/event.js";
4
+ import { getNip10References, interpretThreadTags } from "../helpers/threading.js";
5
+ import { getCoordinateFromAddressPointer, isAddressPointer, isEventPointer } from "../helpers/pointers.js";
6
+ import { getEventUID, getReplaceableUID, getTagValue, isEvent } from "../helpers/event.js";
6
7
  const defaultOptions = {
7
8
  kinds: [kinds.ShortTextNote],
8
9
  };
@@ -64,3 +65,27 @@ export function ThreadQuery(root, opts) {
64
65
  })),
65
66
  };
66
67
  }
68
+ /** A query that gets all legacy and NIP-10 replies for an event */
69
+ export function RepliesQuery(event, overrideKinds) {
70
+ return {
71
+ key: getEventUID(event),
72
+ run: (events) => {
73
+ const kinds = overrideKinds || event.kind === 1 ? [1, 1111] : [1111];
74
+ const filter = { kinds };
75
+ if (isEvent(parent) || isEventPointer(event))
76
+ filter["#e"] = [event.id];
77
+ const address = isParameterizedReplaceableKind(event.kind)
78
+ ? getReplaceableUID(event.kind, event.pubkey, getTagValue(event, "d"))
79
+ : undefined;
80
+ if (address) {
81
+ filter["#a"] = [address];
82
+ }
83
+ return events.timeline(filter).pipe(map((events) => {
84
+ return events.filter((e) => {
85
+ const refs = interpretThreadTags(e);
86
+ return refs.reply?.e?.[1] === event.id || refs.reply?.a?.[1] === address;
87
+ });
88
+ }));
89
+ },
90
+ };
91
+ }
@@ -1,4 +1,5 @@
1
1
  import { AddressPointer, EventPointer } from "nostr-tools/nip19";
2
2
  import { NostrEvent } from "nostr-tools";
3
3
  import { Query } from "../query-store/index.js";
4
+ /** A query that gets all zap events for an event */
4
5
  export declare function EventZapsQuery(id: string | EventPointer | AddressPointer): Query<NostrEvent[]>;
@@ -2,6 +2,7 @@ import { map } from "rxjs";
2
2
  import { kinds } from "nostr-tools";
3
3
  import { getCoordinateFromAddressPointer, isAddressPointer } from "../helpers/pointers.js";
4
4
  import { isValidZap } from "../helpers/zap.js";
5
+ /** A query that gets all zap events for an event */
5
6
  export function EventZapsQuery(id) {
6
7
  return {
7
8
  key: JSON.stringify(id),
@@ -5,8 +5,15 @@ import { LRU } from "../helpers/lru.js";
5
5
  import * as Queries from "../queries/index.js";
6
6
  import { AddressPointer, EventPointer } from "nostr-tools/nip19";
7
7
  export type Query<T extends unknown> = {
8
+ /**
9
+ * A unique key for this query. this is used to detect duplicate queries
10
+ */
8
11
  key: string;
12
+ /** The args array this query was created with. This is mostly for debugging */
9
13
  args?: Array<any>;
14
+ /**
15
+ * The meat of the query, this should return an Observables that subscribes to the eventStore in some way
16
+ */
10
17
  run: (events: EventStore, store: QueryStore) => Observable<T>;
11
18
  };
12
19
  export type QueryConstructor<T extends unknown, Args extends Array<any>> = (...args: Args) => Query<T>;
@@ -17,33 +24,34 @@ export declare class QueryStore {
17
24
  queries: LRU<Query<any>>;
18
25
  observables: WeakMap<Query<any>, Observable<any> | BehaviorSubject<any>>;
19
26
  /** Creates a cached query */
20
- runQuery<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => {
27
+ createQuery<T extends unknown, Args extends Array<any>>(queryConstructor: (...args: Args) => {
21
28
  key: string;
22
29
  run: (events: EventStore, store: QueryStore) => Observable<T>;
23
- }): (...args: Args) => Observable<T>;
24
- /** Returns a single event */
30
+ }, ...args: Args): Observable<T>;
31
+ /** Creates a SingleEventQuery */
25
32
  event(id: string): Observable<import("nostr-tools").Event | undefined>;
26
- /** Returns a single event */
33
+ /** Creates a MultipleEventsQuery */
27
34
  events(ids: string[]): Observable<Map<string, import("nostr-tools").Event>>;
28
- /** Returns the latest version of a replaceable event */
35
+ /** Creates a ReplaceableQuery */
29
36
  replaceable(kind: number, pubkey: string, d?: string): Observable<import("nostr-tools").Event | undefined>;
30
- /** Returns a directory of events by their UID */
37
+ /** Creates a ReplaceableSetQuery */
31
38
  replaceableSet(pointers: {
32
39
  kind: number;
33
40
  pubkey: string;
34
41
  identifier?: string;
35
42
  }[]): Observable<Map<string, import("nostr-tools").Event>>;
36
- /** Returns an array of events that match the filter */
43
+ /** Creates a TimelineQuery */
37
44
  timeline(filters: Filter | Filter[], keepOldVersions?: boolean): Observable<import("nostr-tools").Event[]>;
38
- /** Returns the parsed profile (0) for a pubkey */
45
+ /** Creates a ProfileQuery */
39
46
  profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
40
- /** Returns all reactions for an event (supports replaceable events) */
47
+ /** Creates a ReactionsQuery */
41
48
  reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
42
- /** Returns the parsed relay list (10002) for the pubkey */
49
+ /** Creates a MailboxesQuery */
43
50
  mailboxes(pubkey: string): Observable<{
44
51
  inboxes: string[];
45
52
  outboxes: string[];
46
53
  } | undefined>;
54
+ /** Creates a ThreadQuery */
47
55
  thread(root: string | EventPointer | AddressPointer): Observable<Queries.Thread>;
48
56
  }
49
57
  export { Queries };
@@ -10,58 +10,57 @@ export class QueryStore {
10
10
  queries = new LRU();
11
11
  observables = new WeakMap();
12
12
  /** Creates a cached query */
13
- runQuery(queryConstructor) {
14
- return (...args) => {
15
- const tempQuery = queryConstructor(...args);
16
- const key = `${queryConstructor.name}|${tempQuery.key}`;
17
- let query = this.queries.get(key);
18
- if (!query) {
19
- query = tempQuery;
20
- this.queries.set(key, tempQuery);
21
- }
22
- if (!this.observables.has(query)) {
23
- query.args = args;
24
- const observable = query.run(this.store, this).pipe(shareLatestValue());
25
- this.observables.set(query, observable);
26
- return observable;
27
- }
28
- return this.observables.get(query);
29
- };
13
+ createQuery(queryConstructor, ...args) {
14
+ const tempQuery = queryConstructor(...args);
15
+ const key = `${queryConstructor.name}|${tempQuery.key}`;
16
+ let query = this.queries.get(key);
17
+ if (!query) {
18
+ query = tempQuery;
19
+ this.queries.set(key, tempQuery);
20
+ }
21
+ if (!this.observables.has(query)) {
22
+ query.args = args;
23
+ const observable = query.run(this.store, this).pipe(shareLatestValue());
24
+ this.observables.set(query, observable);
25
+ return observable;
26
+ }
27
+ return this.observables.get(query);
30
28
  }
31
- /** Returns a single event */
29
+ /** Creates a SingleEventQuery */
32
30
  event(id) {
33
- return this.runQuery(Queries.SingleEventQuery)(id);
31
+ return this.createQuery(Queries.SingleEventQuery, id);
34
32
  }
35
- /** Returns a single event */
33
+ /** Creates a MultipleEventsQuery */
36
34
  events(ids) {
37
- return this.runQuery(Queries.MultipleEventsQuery)(ids);
35
+ return this.createQuery(Queries.MultipleEventsQuery, ids);
38
36
  }
39
- /** Returns the latest version of a replaceable event */
37
+ /** Creates a ReplaceableQuery */
40
38
  replaceable(kind, pubkey, d) {
41
- return this.runQuery(Queries.ReplaceableQuery)(kind, pubkey, d);
39
+ return this.createQuery(Queries.ReplaceableQuery, kind, pubkey, d);
42
40
  }
43
- /** Returns a directory of events by their UID */
41
+ /** Creates a ReplaceableSetQuery */
44
42
  replaceableSet(pointers) {
45
- return this.runQuery(Queries.ReplaceableSetQuery)(pointers);
43
+ return this.createQuery(Queries.ReplaceableSetQuery, pointers);
46
44
  }
47
- /** Returns an array of events that match the filter */
45
+ /** Creates a TimelineQuery */
48
46
  timeline(filters, keepOldVersions) {
49
- return this.runQuery(Queries.TimelineQuery)(filters, keepOldVersions);
47
+ return this.createQuery(Queries.TimelineQuery, filters, keepOldVersions);
50
48
  }
51
- /** Returns the parsed profile (0) for a pubkey */
49
+ /** Creates a ProfileQuery */
52
50
  profile(pubkey) {
53
- return this.runQuery(Queries.ProfileQuery)(pubkey);
51
+ return this.createQuery(Queries.ProfileQuery, pubkey);
54
52
  }
55
- /** Returns all reactions for an event (supports replaceable events) */
53
+ /** Creates a ReactionsQuery */
56
54
  reactions(event) {
57
- return this.runQuery(Queries.ReactionsQuery)(event);
55
+ return this.createQuery(Queries.ReactionsQuery, event);
58
56
  }
59
- /** Returns the parsed relay list (10002) for the pubkey */
57
+ /** Creates a MailboxesQuery */
60
58
  mailboxes(pubkey) {
61
- return this.runQuery(Queries.MailboxesQuery)(pubkey);
59
+ return this.createQuery(Queries.MailboxesQuery, pubkey);
62
60
  }
61
+ /** Creates a ThreadQuery */
63
62
  thread(root) {
64
- return this.runQuery(Queries.ThreadQuery)(root);
63
+ return this.createQuery(Queries.ThreadQuery, root);
65
64
  }
66
65
  }
67
66
  export { Queries };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20241204152949",
3
+ "version": "0.0.0-next-20241207161834",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",