applesauce-core 0.0.0-next-20250425210455 → 0.0.0-next-20250428223834

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 (46) hide show
  1. package/README.md +28 -10
  2. package/dist/helpers/lists.d.ts +40 -12
  3. package/dist/helpers/lists.js +60 -21
  4. package/dist/queries/index.d.ts +1 -0
  5. package/dist/queries/index.js +1 -0
  6. package/dist/queries/relays.d.ts +27 -0
  7. package/dist/queries/relays.js +44 -0
  8. package/package.json +1 -1
  9. package/dist/event-store/event-store.test.d.ts +0 -1
  10. package/dist/event-store/event-store.test.js +0 -74
  11. package/dist/helpers/blossom.test.d.ts +0 -1
  12. package/dist/helpers/blossom.test.js +0 -13
  13. package/dist/helpers/bookmark.d.ts +0 -15
  14. package/dist/helpers/bookmark.js +0 -27
  15. package/dist/helpers/event.test.d.ts +0 -1
  16. package/dist/helpers/event.test.js +0 -36
  17. package/dist/helpers/file-metadata.test.d.ts +0 -1
  18. package/dist/helpers/file-metadata.test.js +0 -103
  19. package/dist/helpers/hidden-tags.test.d.ts +0 -1
  20. package/dist/helpers/hidden-tags.test.js +0 -29
  21. package/dist/helpers/mailboxes.test.d.ts +0 -1
  22. package/dist/helpers/mailboxes.test.js +0 -81
  23. package/dist/helpers/media-attachment.d.ts +0 -42
  24. package/dist/helpers/media-attachment.js +0 -72
  25. package/dist/helpers/media-attachment.test.d.ts +0 -1
  26. package/dist/helpers/media-attachment.test.js +0 -59
  27. package/dist/helpers/media-post.d.ts +0 -4
  28. package/dist/helpers/media-post.js +0 -6
  29. package/dist/helpers/mute.d.ts +0 -14
  30. package/dist/helpers/mute.js +0 -23
  31. package/dist/helpers/pipe.d.ts +0 -10
  32. package/dist/helpers/pipe.js +0 -3
  33. package/dist/helpers/tags.test.d.ts +0 -1
  34. package/dist/helpers/tags.test.js +0 -24
  35. package/dist/helpers/threading.test.d.ts +0 -1
  36. package/dist/helpers/threading.test.js +0 -41
  37. package/dist/observable/get-value.d.ts +0 -3
  38. package/dist/observable/get-value.js +0 -14
  39. package/dist/observable/share-latest-value.d.ts +0 -6
  40. package/dist/observable/share-latest-value.js +0 -24
  41. package/dist/observable/simple-timeout.test.d.ts +0 -1
  42. package/dist/observable/simple-timeout.test.js +0 -34
  43. package/dist/queries/comment.d.ts +0 -4
  44. package/dist/queries/comment.js +0 -14
  45. package/dist/query-store/query-store.test.d.ts +0 -1
  46. package/dist/query-store/query-store.test.js +0 -33
package/README.md CHANGED
@@ -1,37 +1,55 @@
1
1
  # applesauce-core
2
2
 
3
- AppleSauce Core is an interpretation layer for nostr clients, Push events into the in-memory [database](https://hzrd149.github.io/applesauce/typedoc/classes/Database.html) and get nicely formatted data out with [queries](https://hzrd149.github.io/applesauce/typedoc/modules/Queries)
3
+ AppleSauce is a collection of utilities for building reactive nostr applications. The core package provides an in-memory event database and reactive queries to help you build nostr UIs with less code.
4
4
 
5
- # Example
5
+ ## Key Components
6
+
7
+ - **Helpers**: Core utility methods for parsing and extracting data from nostr events
8
+ - **EventStore**: In-memory database for storing and subscribing to nostr events
9
+ - **QueryStore**: Manages queries and ensures efficient subscription handling
10
+ - **Queries**: Complex subscriptions for common nostr data patterns
11
+
12
+ ## Documentation
13
+
14
+ For detailed documentation and guides, visit:
15
+
16
+ - [Getting Started](https://hzrd149.github.io/applesauce/introduction/getting-started)
17
+ - [API Reference](https://hzrd149.github.io/applesauce/typedoc/)
18
+
19
+ ## Example
6
20
 
7
21
  ```js
8
22
  import { EventStore, QueryStore } from "applesauce-core";
9
23
  import { Relay } from "nostr-tools/relay";
10
24
 
11
- // The EventStore handles all the events
25
+ // Create a single EventStore instance for your app
12
26
  const eventStore = new EventStore();
13
27
 
14
- // The QueryStore handles queries and makes sure not to run multiple of the same query
28
+ // Create a QueryStore to manage subscriptions efficiently
15
29
  const queryStore = new QueryStore(eventStore);
16
30
 
17
- // Use nostr-tools or anything else to talk to relays
31
+ // Use any nostr library for relay connections (nostr-tools, ndk, nostrify, etc...)
18
32
  const relay = await Relay.connect("wss://relay.example.com");
19
33
 
20
- const sub = relay.subscribe([{ authors: ["266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5"] }], {
34
+ // Subscribe to events and add them to the store
35
+ const sub = relay.subscribe([{ authors: ["3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"] }], {
21
36
  onevent(event) {
22
37
  eventStore.add(event);
23
38
  },
24
39
  });
25
40
 
26
- // This will return an Observable<ProfileContent | undefined> of the parsed metadata
27
- const profile = queryStore.profile("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
41
+ // Subscribe to profile changes using ProfileQuery
42
+ const profile = queryStore.createQuery(
43
+ ProfileQuery,
44
+ "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
45
+ );
28
46
 
29
47
  profile.subscribe((parsed) => {
30
48
  if (parsed) console.log(parsed);
31
49
  });
32
50
 
33
- // This will return an Observable<NostrEvent[]> of all kind 1 events sorted by created_at
34
- const timeline = queryStore.timeline({ kinds: [1] });
51
+ // Subscribe to a timeline of events
52
+ const timeline = queryStore.createQuery(TimelineQuery, { kinds: [1] });
35
53
 
36
54
  timeline.subscribe((events) => {
37
55
  console.log(events);
@@ -1,28 +1,56 @@
1
1
  import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
2
2
  import { NostrEvent } from "nostr-tools";
3
+ export declare const FAVORITE_RELAYS_KIND = 10012;
4
+ export type ReadListTags = "public" | "hidden" | "all";
5
+ /** Returns all the tags of a list or set */
6
+ export declare function getListTags(list: NostrEvent, type?: ReadListTags): string[][];
3
7
  /**
4
8
  * Checks if an event pointer is anywhere in a list or set
5
9
  * NOTE: Ignores the `relay` field in EventPointer
6
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
10
+ * @param list - The list or set to check
11
+ * @param pointer - The event pointer to check
12
+ * @param type - Which types of tags to check
7
13
  */
8
- export declare function isEventPointerInList(list: NostrEvent, pointer: string | EventPointer): boolean;
14
+ export declare function isEventPointerInList(list: NostrEvent, pointer: string | EventPointer, type?: ReadListTags): boolean;
9
15
  /**
10
16
  * Checks if an address pointer is anywhere in a list or set
11
17
  * NOTE: Ignores the `relay` field in AddressPointer
12
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
18
+ * @param list - The list or set to check
19
+ * @param pointer - The address pointer to check
20
+ * @param type - Which types of tags to check
13
21
  */
14
- export declare function isAddressPointerInList(list: NostrEvent, pointer: string | AddressPointer): boolean;
22
+ export declare function isAddressPointerInList(list: NostrEvent, pointer: string | AddressPointer, type?: ReadListTags): boolean;
15
23
  /**
16
24
  * Checks if an profile pointer is anywhere in a list or set
17
25
  * NOTE: Ignores the `relay` field in ProfilePointer
18
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
26
+ * @param list - The list or set to check
27
+ * @param pointer - The profile pointer to check
28
+ * @param type - Which types of tags to check
19
29
  */
20
- export declare function isProfilePointerInList(list: NostrEvent, pointer: string | ProfilePointer): boolean;
21
- /** Returns all the EventPointer in a list or set */
22
- export declare function getEventPointersFromList(list: NostrEvent): EventPointer[];
23
- /** Returns all the AddressPointer in a list or set */
24
- export declare function getAddressPointersFromList(list: NostrEvent): AddressPointer[];
25
- /** Returns all the ProfilePointer in a list or set */
26
- export declare function getProfilePointersFromList(list: NostrEvent): ProfilePointer[];
30
+ export declare function isProfilePointerInList(list: NostrEvent, pointer: string | ProfilePointer, type?: ReadListTags): boolean;
31
+ /**
32
+ * Returns all the EventPointer in a list or set
33
+ * @param list - The list or set to get the event pointers from
34
+ * @param type - Which types of tags to read
35
+ */
36
+ export declare function getEventPointersFromList(list: NostrEvent, type?: ReadListTags): EventPointer[];
37
+ /**
38
+ * Returns all the AddressPointer in a list or set
39
+ * @param list - The list or set to get the address pointers from
40
+ * @param type - Which types of tags to read
41
+ */
42
+ export declare function getAddressPointersFromList(list: NostrEvent, type?: ReadListTags): AddressPointer[];
43
+ /**
44
+ * Returns all the ProfilePointer in a list or set
45
+ * @param list - The list or set to get the profile pointers from
46
+ * @param type - Which types of tags to read
47
+ */
48
+ export declare function getProfilePointersFromList(list: NostrEvent, type?: ReadListTags): ProfilePointer[];
49
+ /**
50
+ * Returns a deduplicated array of all 'relay' tags in a list or set
51
+ * @param list - The list or set to get the relays from
52
+ * @param type - Which types of tags to read
53
+ */
54
+ export declare function getRelaysFromList(list: NostrEvent, type?: ReadListTags): string[];
27
55
  /** Returns if an event is a valid list or set */
28
56
  export declare function isValidList(event: NostrEvent): boolean;
@@ -3,48 +3,87 @@ import { getHiddenTags } from "./hidden-tags.js";
3
3
  import { getAddressPointerFromATag, getCoordinateFromAddressPointer, getEventPointerFromETag, getProfilePointerFromPTag, } from "./pointers.js";
4
4
  import { isATag, isETag, isPTag, processTags } from "./tags.js";
5
5
  import { getReplaceableIdentifier } from "./event.js";
6
- function listGetAllTags(list) {
7
- const hidden = getHiddenTags(list);
8
- return hidden ? [...hidden, ...list.tags] : list.tags;
6
+ import { mergeRelaySets } from "./relays.js";
7
+ export const FAVORITE_RELAYS_KIND = 10012;
8
+ /** Returns all the tags of a list or set */
9
+ export function getListTags(list, type) {
10
+ switch (type) {
11
+ case "public":
12
+ return list.tags;
13
+ case "hidden":
14
+ return getHiddenTags(list) ?? [];
15
+ default:
16
+ case "all":
17
+ return [...(getHiddenTags(list) ?? []), ...list.tags];
18
+ }
9
19
  }
10
20
  /**
11
21
  * Checks if an event pointer is anywhere in a list or set
12
22
  * NOTE: Ignores the `relay` field in EventPointer
13
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
23
+ * @param list - The list or set to check
24
+ * @param pointer - The event pointer to check
25
+ * @param type - Which types of tags to check
14
26
  */
15
- export function isEventPointerInList(list, pointer) {
27
+ export function isEventPointerInList(list, pointer, type) {
16
28
  const id = typeof pointer === "string" ? pointer : pointer.id;
17
- return listGetAllTags(list).some((t) => t[0] === "e" && t[1] === id);
29
+ const tags = getListTags(list, type);
30
+ return tags.some((t) => t[0] === "e" && t[1] === id);
18
31
  }
19
32
  /**
20
33
  * Checks if an address pointer is anywhere in a list or set
21
34
  * NOTE: Ignores the `relay` field in AddressPointer
22
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
35
+ * @param list - The list or set to check
36
+ * @param pointer - The address pointer to check
37
+ * @param type - Which types of tags to check
23
38
  */
24
- export function isAddressPointerInList(list, pointer) {
39
+ export function isAddressPointerInList(list, pointer, type) {
25
40
  const cord = typeof pointer === "string" ? pointer : getCoordinateFromAddressPointer(pointer);
26
- return listGetAllTags(list).some((t) => t[0] === "a" && t[1] === cord);
41
+ const tags = getListTags(list, type);
42
+ return tags.some((t) => t[0] === "a" && t[1] === cord);
27
43
  }
28
44
  /**
29
45
  * Checks if an profile pointer is anywhere in a list or set
30
46
  * NOTE: Ignores the `relay` field in ProfilePointer
31
- * NOTE: This will check the hidden tags if the list has hidden tags and they are unlocked
47
+ * @param list - The list or set to check
48
+ * @param pointer - The profile pointer to check
49
+ * @param type - Which types of tags to check
32
50
  */
33
- export function isProfilePointerInList(list, pointer) {
51
+ export function isProfilePointerInList(list, pointer, type) {
34
52
  const pubkey = typeof pointer === "string" ? pointer : pointer.pubkey;
35
- return listGetAllTags(list).some((t) => t[0] === "p" && t[1] === pubkey);
53
+ const tags = getListTags(list, type);
54
+ return tags.some((t) => t[0] === "p" && t[1] === pubkey);
36
55
  }
37
- /** Returns all the EventPointer in a list or set */
38
- export function getEventPointersFromList(list) {
39
- return processTags(listGetAllTags(list), (tag) => (isETag(tag) ? tag : undefined), getEventPointerFromETag);
56
+ /**
57
+ * Returns all the EventPointer in a list or set
58
+ * @param list - The list or set to get the event pointers from
59
+ * @param type - Which types of tags to read
60
+ */
61
+ export function getEventPointersFromList(list, type) {
62
+ return processTags(getListTags(list, type), (tag) => (isETag(tag) ? tag : undefined), getEventPointerFromETag);
63
+ }
64
+ /**
65
+ * Returns all the AddressPointer in a list or set
66
+ * @param list - The list or set to get the address pointers from
67
+ * @param type - Which types of tags to read
68
+ */
69
+ export function getAddressPointersFromList(list, type) {
70
+ return processTags(getListTags(list, type), (t) => (isATag(t) ? t : undefined), getAddressPointerFromATag);
40
71
  }
41
- /** Returns all the AddressPointer in a list or set */
42
- export function getAddressPointersFromList(list) {
43
- return processTags(listGetAllTags(list), (t) => (isATag(t) ? t : undefined), getAddressPointerFromATag);
72
+ /**
73
+ * Returns all the ProfilePointer in a list or set
74
+ * @param list - The list or set to get the profile pointers from
75
+ * @param type - Which types of tags to read
76
+ */
77
+ export function getProfilePointersFromList(list, type) {
78
+ return processTags(getListTags(list, type), (t) => (isPTag(t) ? t : undefined), getProfilePointerFromPTag);
44
79
  }
45
- /** Returns all the ProfilePointer in a list or set */
46
- export function getProfilePointersFromList(list) {
47
- return processTags(listGetAllTags(list), (t) => (isPTag(t) ? t : undefined), getProfilePointerFromPTag);
80
+ /**
81
+ * Returns a deduplicated array of all 'relay' tags in a list or set
82
+ * @param list - The list or set to get the relays from
83
+ * @param type - Which types of tags to read
84
+ */
85
+ export function getRelaysFromList(list, type) {
86
+ return mergeRelaySets(processTags(getListTags(list, type), (t) => (t[0] === "relay" ? t[1] : undefined)));
48
87
  }
49
88
  /** Returns if an event is a valid list or set */
50
89
  export function isValidList(event) {
@@ -3,6 +3,7 @@ export * from "./bookmarks.js";
3
3
  export * from "./channels.js";
4
4
  export * from "./comments.js";
5
5
  export * from "./contacts.js";
6
+ export * from "./relays.js";
6
7
  export * from "./mailboxes.js";
7
8
  export * from "./mutes.js";
8
9
  export * from "./pins.js";
@@ -3,6 +3,7 @@ export * from "./bookmarks.js";
3
3
  export * from "./channels.js";
4
4
  export * from "./comments.js";
5
5
  export * from "./contacts.js";
6
+ export * from "./relays.js";
6
7
  export * from "./mailboxes.js";
7
8
  export * from "./mutes.js";
8
9
  export * from "./pins.js";
@@ -0,0 +1,27 @@
1
+ import { AddressPointer } from "nostr-tools/nip19";
2
+ import { ReadListTags } from "../helpers/lists.js";
3
+ import { Query } from "../query-store/query-store.js";
4
+ /**
5
+ * A query that returns all favorite relays for a pubkey
6
+ * @param pubkey - The pubkey to get the favorite relays for
7
+ * @param type - Which types of tags to read
8
+ */
9
+ export declare function FavoriteRelays(pubkey: string, type?: ReadListTags): Query<string[] | undefined>;
10
+ /**
11
+ * A query that returns all favorite relay sets for a pubkey
12
+ * @param pubkey - The pubkey to get the favorite relay sets for
13
+ * @param type - Which types of tags to read
14
+ */
15
+ export declare function FavoriteRelaySets(pubkey: string, type?: ReadListTags): Query<AddressPointer[] | undefined>;
16
+ /**
17
+ * A query that returns all search relays for a pubkey
18
+ * @param pubkey - The pubkey to get the search relays for
19
+ * @param type - Which types of tags to read
20
+ */
21
+ export declare function SearchRelays(pubkey: string, type?: ReadListTags): Query<string[] | undefined>;
22
+ /**
23
+ * A query that returns all blocked relays for a pubkey
24
+ * @param pubkey - The pubkey to get the blocked relays for
25
+ * @param type - Which types of tags to read
26
+ */
27
+ export declare function BlockedRelays(pubkey: string, type?: ReadListTags): Query<string[] | undefined>;
@@ -0,0 +1,44 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { identity, map } from "rxjs";
3
+ import { FAVORITE_RELAYS_KIND, getAddressPointersFromList, getRelaysFromList } from "../helpers/lists.js";
4
+ import { listenLatestUpdates } from "../observable/listen-latest-updates.js";
5
+ /**
6
+ * A query that returns all favorite relays for a pubkey
7
+ * @param pubkey - The pubkey to get the favorite relays for
8
+ * @param type - Which types of tags to read
9
+ */
10
+ export function FavoriteRelays(pubkey, type) {
11
+ return (events) => {
12
+ return events.replaceable(FAVORITE_RELAYS_KIND, pubkey).pipe(type !== "public" ? listenLatestUpdates(events) : map(identity), map((e) => e && getRelaysFromList(e, type)));
13
+ };
14
+ }
15
+ /**
16
+ * A query that returns all favorite relay sets for a pubkey
17
+ * @param pubkey - The pubkey to get the favorite relay sets for
18
+ * @param type - Which types of tags to read
19
+ */
20
+ export function FavoriteRelaySets(pubkey, type) {
21
+ return (events) => {
22
+ return events.replaceable(FAVORITE_RELAYS_KIND, pubkey).pipe(type !== "public" ? listenLatestUpdates(events) : map(identity), map((e) => e && getAddressPointersFromList(e, type)));
23
+ };
24
+ }
25
+ /**
26
+ * A query that returns all search relays for a pubkey
27
+ * @param pubkey - The pubkey to get the search relays for
28
+ * @param type - Which types of tags to read
29
+ */
30
+ export function SearchRelays(pubkey, type) {
31
+ return (events) => {
32
+ return events.replaceable(kinds.SearchRelaysList, pubkey).pipe(type !== "public" ? listenLatestUpdates(events) : map(identity), map((e) => e && getRelaysFromList(e, type)));
33
+ };
34
+ }
35
+ /**
36
+ * A query that returns all blocked relays for a pubkey
37
+ * @param pubkey - The pubkey to get the blocked relays for
38
+ * @param type - Which types of tags to read
39
+ */
40
+ export function BlockedRelays(pubkey, type) {
41
+ return (events) => {
42
+ return events.replaceable(kinds.BlockedRelaysList, pubkey).pipe(type !== "public" ? listenLatestUpdates(events) : map(identity), map((e) => e && getRelaysFromList(e, type)));
43
+ };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250425210455",
3
+ "version": "0.0.0-next-20250428223834",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1 +0,0 @@
1
- export {};
@@ -1,74 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { kinds } from "nostr-tools";
3
- import { EventStore } from "./event-store.js";
4
- import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
5
- let eventStore;
6
- beforeEach(() => {
7
- eventStore = new EventStore();
8
- });
9
- const event = {
10
- content: '{"name":"hzrd149","picture":"https://cdn.hzrd149.com/5ed3fe5df09a74e8c126831eac999364f9eb7624e2b86d521521b8021de20bdc.png","about":"JavaScript developer working on some nostr stuff\\n- noStrudel https://nostrudel.ninja/ \\n- Blossom https://github.com/hzrd149/blossom \\n- Applesauce https://hzrd149.github.io/applesauce/","website":"https://hzrd149.com","nip05":"_@hzrd149.com","lud16":"hzrd1499@minibits.cash","pubkey":"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5","display_name":"hzrd149","displayName":"hzrd149","banner":""}',
11
- created_at: 1738362529,
12
- id: "e9df8d5898c4ccfbd21fcd59f3f48abb3ff0ab7259b19570e2f1756de1e9306b",
13
- kind: 0,
14
- pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
15
- sig: "465a47b93626a587bf81dadc2b306b8f713a62db31d6ce1533198e9ae1e665a6eaf376a03250bf9ffbb02eb9059c8eafbd37ae1092d05d215757575bd8357586",
16
- tags: [],
17
- };
18
- describe("add", () => {
19
- it("should return original event in case of duplicates", () => {
20
- const a = { ...event };
21
- expect(eventStore.add(a)).toBe(a);
22
- const b = { ...event };
23
- expect(eventStore.add(b)).toBe(a);
24
- const c = { ...event };
25
- expect(eventStore.add(c)).toBe(a);
26
- });
27
- it("should merge seen relays on duplicate events", () => {
28
- const a = { ...event };
29
- addSeenRelay(a, "wss://relay.a.com");
30
- eventStore.add(a);
31
- const b = { ...event };
32
- addSeenRelay(b, "wss://relay.b.com");
33
- eventStore.add(b);
34
- expect(eventStore.getEvent(event.id)).toBeDefined();
35
- expect([...getSeenRelays(eventStore.getEvent(event.id))]).toEqual(expect.arrayContaining(["wss://relay.a.com", "wss://relay.b.com"]));
36
- });
37
- it("should ignore deleted events", () => {
38
- const deleteEvent = {
39
- id: "delete event id",
40
- kind: kinds.EventDeletion,
41
- created_at: event.created_at + 100,
42
- pubkey: event.pubkey,
43
- tags: [["e", event.id]],
44
- sig: "this should be ignored for the test",
45
- content: "test",
46
- };
47
- // add delete event first
48
- eventStore.add(deleteEvent);
49
- // now event should be ignored
50
- eventStore.add(event);
51
- expect(eventStore.getEvent(event.id)).toBeUndefined();
52
- });
53
- });
54
- describe("verifyEvent", () => {
55
- it("should be called for all events added", () => {
56
- const verifyEvent = vi.fn().mockReturnValue(true);
57
- eventStore.verifyEvent = verifyEvent;
58
- eventStore.add(event);
59
- expect(verifyEvent).toHaveBeenCalledWith(event);
60
- });
61
- it("should not be called for duplicate events", () => {
62
- const verifyEvent = vi.fn().mockReturnValue(true);
63
- eventStore.verifyEvent = verifyEvent;
64
- const a = { ...event };
65
- eventStore.add(a);
66
- expect(verifyEvent).toHaveBeenCalledWith(a);
67
- const b = { ...event };
68
- eventStore.add(b);
69
- expect(verifyEvent).toHaveBeenCalledTimes(1);
70
- const c = { ...event };
71
- eventStore.add(c);
72
- expect(verifyEvent).toHaveBeenCalledTimes(1);
73
- });
74
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,13 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { areBlossomServersEqual } from "./blossom.js";
3
- describe("areBlossomServersEqual", () => {
4
- it("should ignore path", () => {
5
- expect(areBlossomServersEqual("https://cdn.server.com/pathname", "https://cdn.server.com")).toBe(true);
6
- });
7
- it("should not ignore protocol", () => {
8
- expect(areBlossomServersEqual("http://cdn.server.com", "https://cdn.server.com")).toBe(false);
9
- });
10
- it("should not ignore port", () => {
11
- expect(areBlossomServersEqual("http://cdn.server.com:4658", "https://cdn.server.com")).toBe(false);
12
- });
13
- });
@@ -1,15 +0,0 @@
1
- import { NostrEvent } from "nostr-tools";
2
- import { AddressPointer, EventPointer } from "nostr-tools/nip19";
3
- export declare const BookmarkPublicSymbol: unique symbol;
4
- export declare const BookmarkHiddenSymbol: unique symbol;
5
- export type Bookmarks = {
6
- notes: EventPointer[];
7
- articles: AddressPointer[];
8
- hashtags: string[];
9
- urls: string[];
10
- };
11
- export declare function parseBookmarkTags(tags: string[][]): Bookmarks;
12
- /** Returns the public bookmarks of the event */
13
- export declare function getBookmarks(bookmark: NostrEvent): Bookmarks;
14
- /** Returns the bookmarks of the event if its unlocked */
15
- export declare function getHiddenBookmarks(bookmark: NostrEvent): Bookmarks | undefined;
@@ -1,27 +0,0 @@
1
- import { kinds } from "nostr-tools";
2
- import { getAddressPointerFromATag, getEventPointerFromETag } from "./pointers.js";
3
- import { getOrComputeCachedValue } from "./cache.js";
4
- import { getHiddenTags } from "./index.js";
5
- export const BookmarkPublicSymbol = Symbol.for("bookmark-public");
6
- export const BookmarkHiddenSymbol = Symbol.for("bookmark-hidden");
7
- export function parseBookmarkTags(tags) {
8
- const notes = tags.filter((t) => t[0] === "e" && t[1]).map(getEventPointerFromETag);
9
- const articles = tags
10
- .filter((t) => t[0] === "a" && t[1])
11
- .map(getAddressPointerFromATag)
12
- .filter((addr) => addr.kind === kinds.LongFormArticle);
13
- const hashtags = tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1]);
14
- const urls = tags.filter((t) => t[0] === "r" && t[1]).map((t) => t[1]);
15
- return { notes, articles, hashtags, urls };
16
- }
17
- /** Returns the public bookmarks of the event */
18
- export function getBookmarks(bookmark) {
19
- return getOrComputeCachedValue(bookmark, BookmarkPublicSymbol, () => parseBookmarkTags(bookmark.tags));
20
- }
21
- /** Returns the bookmarks of the event if its unlocked */
22
- export function getHiddenBookmarks(bookmark) {
23
- return getOrComputeCachedValue(bookmark, BookmarkHiddenSymbol, () => {
24
- const tags = getHiddenTags(bookmark);
25
- return tags && parseBookmarkTags(tags);
26
- });
27
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,36 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { EventIndexableTagsSymbol, getIndexableTags, getTagValue } from "./event.js";
3
- const event = {
4
- content: "",
5
- created_at: 1732889913,
6
- id: "2d53511f321cc82dd13eedfb597c9fe834d12d271c10d8068e9d8cfb8f58d1b4",
7
- kind: 30000,
8
- pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
9
- sig: "e6a442487ef44a8a00ec1e0a852e547991fcd5cbf19aa1a4219fa65d6f41022675e0745207649f4b16fe9a6c5c7c3693dc3e13966ffa5b2891634867c874cf22",
10
- tags: [
11
- ["d", "qRxLhBbTfRlxsvKSu0iUl"],
12
- ["title", "Musicians"],
13
- ["client", "noStrudel", "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546"],
14
- ["p", "2842e34860c59dfacd5df48ba7a65065e6760d08c35f779553d83c2c2310b493"],
15
- ["p", "28ca019b78b494c25a9da2d645975a8501c7e99b11302e5cbe748ee593fcb2cc"],
16
- ["p", "f46192b8b9be1b43fc30ea27c7cb16210aede17252b3aa9692fbb3f2ba153199"],
17
- ],
18
- };
19
- describe("getIndexableTags", () => {
20
- it("should return a set of indexable tags for event", () => {
21
- expect(Array.from(getIndexableTags(event))).toEqual(expect.arrayContaining([
22
- "p:2842e34860c59dfacd5df48ba7a65065e6760d08c35f779553d83c2c2310b493",
23
- "p:28ca019b78b494c25a9da2d645975a8501c7e99b11302e5cbe748ee593fcb2cc",
24
- "p:f46192b8b9be1b43fc30ea27c7cb16210aede17252b3aa9692fbb3f2ba153199",
25
- ]));
26
- });
27
- it("should cache value on EventIndexableTagsSymbol", () => {
28
- getIndexableTags(event);
29
- expect(Reflect.has(event, EventIndexableTagsSymbol)).toBe(true);
30
- });
31
- });
32
- describe("getTagValue", () => {
33
- it("should return value of tag if present", () => {
34
- expect(getTagValue(event, "title")).toBe("Musicians");
35
- });
36
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,103 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getFileMetadataFromImetaTag, parseFileMetadataTags } from "./file-metadata.js";
3
- describe("file metadata helpers", () => {
4
- describe("parseFileMetadataTags", () => {
5
- it("should parse a simple 1060 event", () => {
6
- const tags = [
7
- ["url", "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif"],
8
- ["ox", "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae"],
9
- ["fallback", "https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
10
- ["x", "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5"],
11
- ["m", "image/gif"],
12
- ["dim", "360x306"],
13
- ["bh", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
14
- ["blurhash", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
15
- [
16
- "thumb",
17
- "https://image.nostr.build/thumb/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
18
- ],
19
- ["t", "gifbuddy"],
20
- ["summary", "Khaleesi call dragons Daenerys Targaryen"],
21
- ["alt", "a woman with blonde hair and a brooch on her shoulder"],
22
- [
23
- "thumb",
24
- "https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
25
- "5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
26
- ],
27
- [
28
- "image",
29
- "https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
30
- "5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
31
- ],
32
- ];
33
- expect(parseFileMetadataTags(tags)).toEqual({
34
- url: "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
35
- type: "image/gif",
36
- dimensions: "360x306",
37
- blurhash: "L38zleNL00~W^kRj0L-p0KM_^kx]",
38
- sha256: "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5",
39
- originalSha256: "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae",
40
- thumbnail: "https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
41
- image: "https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
42
- summary: "Khaleesi call dragons Daenerys Targaryen",
43
- fallback: ["https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
44
- alt: "a woman with blonde hair and a brooch on her shoulder",
45
- });
46
- });
47
- });
48
- describe("getFileMetadataFromImetaTag", () => {
49
- it("should parse simple imeta tag", () => {
50
- expect(getFileMetadataFromImetaTag([
51
- "imeta",
52
- "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
53
- "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
54
- "dim 1024x1024",
55
- "m image/jpeg",
56
- "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
57
- ])).toEqual({
58
- url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
59
- sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
60
- dimensions: "1024x1024",
61
- type: "image/jpeg",
62
- blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
63
- });
64
- });
65
- it("should parse thumbnail url", () => {
66
- expect(getFileMetadataFromImetaTag([
67
- "imeta",
68
- "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
69
- "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
70
- "dim 1024x1024",
71
- "m image/jpeg",
72
- "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
73
- "thumb https://exmaple.com/thumb.jpg",
74
- ])).toEqual({
75
- url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
76
- sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
77
- dimensions: "1024x1024",
78
- type: "image/jpeg",
79
- blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
80
- thumbnail: "https://exmaple.com/thumb.jpg",
81
- });
82
- });
83
- it("should parse multiple fallback urls", () => {
84
- expect(getFileMetadataFromImetaTag([
85
- "imeta",
86
- "url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
87
- "x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
88
- "dim 1024x1024",
89
- "m image/jpeg",
90
- "blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
91
- "fallback https://exmaple.com/image2.jpg",
92
- "fallback https://exmaple.com/image3.jpg",
93
- ])).toEqual({
94
- url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
95
- sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
96
- dimensions: "1024x1024",
97
- type: "image/jpeg",
98
- blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
99
- fallback: ["https://exmaple.com/image2.jpg", "https://exmaple.com/image3.jpg"],
100
- });
101
- });
102
- });
103
- });
@@ -1 +0,0 @@
1
- export {};