applesauce-core 0.0.0-next-20250511152752 → 0.0.0-next-20250526151506

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 (65) hide show
  1. package/dist/__tests__/exports.test.js +11 -0
  2. package/dist/event-store/__tests__/event-store.test.js +6 -0
  3. package/dist/event-store/database.d.ts +3 -3
  4. package/dist/event-store/database.js +6 -3
  5. package/dist/event-store/event-store.d.ts +10 -6
  6. package/dist/event-store/event-store.js +16 -6
  7. package/dist/event-store/interface.d.ts +2 -2
  8. package/dist/helpers/__tests__/app-handlers.test.js +184 -0
  9. package/dist/helpers/__tests__/emoji.test.js +96 -1
  10. package/dist/helpers/__tests__/exports.test.js +29 -1
  11. package/dist/helpers/__tests__/groups.test.js +61 -0
  12. package/dist/helpers/__tests__/messages.test.js +91 -0
  13. package/dist/helpers/__tests__/reactions.test.d.ts +1 -0
  14. package/dist/helpers/__tests__/reactions.test.js +88 -0
  15. package/dist/helpers/app-handlers.d.ts +23 -0
  16. package/dist/helpers/app-handlers.js +68 -0
  17. package/dist/helpers/emoji.d.ts +10 -2
  18. package/dist/helpers/emoji.js +21 -3
  19. package/dist/helpers/groups.d.ts +5 -0
  20. package/dist/helpers/groups.js +11 -1
  21. package/dist/helpers/index.d.ts +4 -0
  22. package/dist/helpers/index.js +4 -0
  23. package/dist/helpers/messages.d.ts +9 -0
  24. package/dist/helpers/messages.js +19 -0
  25. package/dist/helpers/pointers.d.ts +14 -9
  26. package/dist/helpers/pointers.js +23 -43
  27. package/dist/helpers/profile.d.ts +5 -1
  28. package/dist/helpers/profile.js +14 -1
  29. package/dist/helpers/reactions.d.ts +8 -0
  30. package/dist/helpers/reactions.js +56 -0
  31. package/dist/helpers/reports.d.ts +28 -0
  32. package/dist/helpers/reports.js +38 -0
  33. package/dist/helpers/share.d.ts +10 -1
  34. package/dist/helpers/share.js +22 -8
  35. package/dist/helpers/url.d.ts +4 -0
  36. package/dist/helpers/url.js +20 -0
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/observable/__tests__/exports.test.js +3 -0
  40. package/dist/observable/__tests__/map-events-to-store.test.d.ts +1 -0
  41. package/dist/observable/__tests__/map-events-to-store.test.js +38 -0
  42. package/dist/observable/__tests__/watch-event-updates.test.d.ts +1 -0
  43. package/dist/observable/__tests__/{listen-latest-updates.test.js → watch-event-updates.test.js} +6 -6
  44. package/dist/observable/index.d.ts +3 -1
  45. package/dist/observable/index.js +3 -1
  46. package/dist/observable/map-events-timeline.d.ts +7 -0
  47. package/dist/observable/map-events-timeline.js +9 -0
  48. package/dist/observable/map-events-to-store.d.ts +5 -0
  49. package/dist/observable/map-events-to-store.js +12 -0
  50. package/dist/observable/watch-event-updates.d.ts +7 -0
  51. package/dist/observable/{listen-latest-updates.js → watch-event-updates.js} +4 -2
  52. package/dist/queries/__tests__/comments.test.d.ts +1 -0
  53. package/dist/queries/__tests__/comments.test.js +39 -0
  54. package/dist/queries/bookmarks.js +3 -3
  55. package/dist/queries/comments.js +4 -4
  56. package/dist/queries/contacts.js +3 -3
  57. package/dist/queries/mutes.js +3 -3
  58. package/dist/queries/relays.js +5 -5
  59. package/package.json +2 -2
  60. package/dist/__tests__/index.test.js +0 -17
  61. package/dist/helpers/__tests__/index.test.js +0 -220
  62. package/dist/observable/listen-latest-updates.d.ts +0 -5
  63. /package/dist/{__tests__/index.test.d.ts → helpers/__tests__/app-handlers.test.d.ts} +0 -0
  64. /package/dist/helpers/__tests__/{index.test.d.ts → groups.test.d.ts} +0 -0
  65. /package/dist/{observable/__tests__/listen-latest-updates.test.d.ts → helpers/__tests__/messages.test.d.ts} +0 -0
@@ -10,7 +10,18 @@ describe("exports", () => {
10
10
  "Helpers",
11
11
  "Queries",
12
12
  "QueryStore",
13
+ "TimeoutError",
14
+ "defined",
15
+ "firstValueFrom",
16
+ "getObservableValue",
17
+ "lastValueFrom",
18
+ "listenLatestUpdates",
13
19
  "logger",
20
+ "mapEventsToStore",
21
+ "mapEventsToTimeline",
22
+ "simpleTimeout",
23
+ "watchEventUpdates",
24
+ "withImmediateValueOrDefault",
14
25
  ]
15
26
  `);
16
27
  });
@@ -98,6 +98,12 @@ describe("add", () => {
98
98
  expect(eventStore.getEvent(newEvent.id)).toBeDefined();
99
99
  expect(eventStore.getReplaceable(event.kind, event.pubkey, "test")).toBe(newEvent);
100
100
  });
101
+ it("should return null when event is invalid and there isn't an existing event", () => {
102
+ const verifyEvent = vi.fn().mockReturnValue(false);
103
+ eventStore.verifyEvent = verifyEvent;
104
+ expect(eventStore.add(profile)).toBeNull();
105
+ expect(verifyEvent).toHaveBeenCalledWith(profile);
106
+ });
101
107
  });
102
108
  describe("inserts", () => {
103
109
  it("should emit newer replaceable events", () => {
@@ -23,7 +23,7 @@ export declare class Database {
23
23
  /** A stream of events removed from the database */
24
24
  removed: Subject<import("nostr-tools").Event>;
25
25
  /** A method thats called before a new event is inserted */
26
- onBeforeInsert?: (event: NostrEvent) => void;
26
+ onBeforeInsert?: (event: NostrEvent) => boolean;
27
27
  get size(): number;
28
28
  protected claims: WeakMap<import("nostr-tools").Event, any>;
29
29
  /** Index helper methods */
@@ -41,9 +41,9 @@ export declare class Database {
41
41
  /** Gets an array of replaceable events */
42
42
  getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
43
43
  /** Inserts an event into the database and notifies all subscriptions */
44
- addEvent(event: NostrEvent): NostrEvent;
44
+ addEvent(event: NostrEvent): NostrEvent | null;
45
45
  /** Inserts and event into the database and notifies all subscriptions that the event has updated */
46
- updateEvent(event: NostrEvent): NostrEvent;
46
+ updateEvent(event: NostrEvent): boolean;
47
47
  /** Removes an event from the database and notifies all subscriptions */
48
48
  removeEvent(eventOrId: string | NostrEvent): boolean;
49
49
  /** Sets the claim on the event and touches it */
@@ -86,7 +86,9 @@ export class Database {
86
86
  const current = this.events.get(id);
87
87
  if (current)
88
88
  return current;
89
- this.onBeforeInsert?.(event);
89
+ // Ignore events if before insert returns false
90
+ if (this.onBeforeInsert?.(event) === false)
91
+ return null;
90
92
  this.events.set(id, event);
91
93
  this.getKindIndex(event.kind).add(event);
92
94
  this.getAuthorsIndex(event.pubkey).add(event);
@@ -115,8 +117,9 @@ export class Database {
115
117
  /** Inserts and event into the database and notifies all subscriptions that the event has updated */
116
118
  updateEvent(event) {
117
119
  const inserted = this.addEvent(event);
118
- this.updated.next(inserted);
119
- return inserted;
120
+ if (inserted)
121
+ this.updated.next(inserted);
122
+ return inserted !== null;
120
123
  }
121
124
  /** Removes an event from the database and notifies all subscriptions */
122
125
  removeEvent(eventOrId) {
@@ -7,7 +7,10 @@ export declare class EventStore implements IEventStore {
7
7
  database: Database;
8
8
  /** Enable this to keep old versions of replaceable events */
9
9
  keepOldVersions: boolean;
10
- /** A method used to verify new events before added them */
10
+ /**
11
+ * A method used to verify new events before added them
12
+ * @returns true if the event is valid, false if it should be ignored
13
+ */
11
14
  verifyEvent?: (event: NostrEvent) => boolean;
12
15
  /** A stream of new events added to the store */
13
16
  inserts: Observable<NostrEvent>;
@@ -23,20 +26,21 @@ export declare class EventStore implements IEventStore {
23
26
  /** Copies important metadata from and identical event to another */
24
27
  static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
25
28
  /**
26
- * Adds an event to the database and update subscriptions
27
- * @throws
29
+ * Adds an event to the store and update subscriptions
30
+ * @returns The existing event or the event that was added, if it was ignored returns null
28
31
  */
29
- add(event: NostrEvent, fromRelay?: string): NostrEvent;
32
+ add(event: NostrEvent, fromRelay?: string): NostrEvent | null;
30
33
  /** Removes an event from the database and updates subscriptions */
31
34
  remove(event: string | NostrEvent): boolean;
32
35
  /** Removes any event that is not being used by a subscription */
33
36
  prune(max?: number): number;
34
37
  /** Add an event to the store and notifies all subscribes it has updated */
35
- update(event: NostrEvent): NostrEvent;
38
+ update(event: NostrEvent): boolean;
36
39
  /** Get all events matching a filter */
37
40
  getAll(filters: Filter | Filter[]): Set<NostrEvent>;
38
- /** Check if the store has an event */
41
+ /** Check if the store has an event by id */
39
42
  hasEvent(id: string): boolean;
43
+ /** Get an event by id from the store */
40
44
  getEvent(id: string): NostrEvent | undefined;
41
45
  /** Check if the store has a replaceable event */
42
46
  hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
@@ -18,7 +18,10 @@ export class EventStore {
18
18
  database;
19
19
  /** Enable this to keep old versions of replaceable events */
20
20
  keepOldVersions = false;
21
- /** A method used to verify new events before added them */
21
+ /**
22
+ * A method used to verify new events before added them
23
+ * @returns true if the event is valid, false if it should be ignored
24
+ */
22
25
  verifyEvent;
23
26
  /** A stream of new events added to the store */
24
27
  inserts;
@@ -28,10 +31,13 @@ export class EventStore {
28
31
  removes;
29
32
  constructor() {
30
33
  this.database = new Database();
34
+ // verify events before they are added to the database
31
35
  this.database.onBeforeInsert = (event) => {
32
- // reject events that are invalid
36
+ // Ignore events that are invalid
33
37
  if (this.verifyEvent && this.verifyEvent(event) === false)
34
- throw new Error("Invalid event");
38
+ return false;
39
+ else
40
+ return true;
35
41
  };
36
42
  // when events are added to the database, add the symbol
37
43
  this.database.inserted.subscribe((event) => {
@@ -100,8 +106,8 @@ export class EventStore {
100
106
  Reflect.set(dest, FromCacheSymbol, fromCache);
101
107
  }
102
108
  /**
103
- * Adds an event to the database and update subscriptions
104
- * @throws
109
+ * Adds an event to the store and update subscriptions
110
+ * @returns The existing event or the event that was added, if it was ignored returns null
105
111
  */
106
112
  add(event, fromRelay) {
107
113
  if (event.kind === kinds.EventDeletion)
@@ -130,6 +136,9 @@ export class EventStore {
130
136
  }
131
137
  // Insert event into database
132
138
  const inserted = this.database.addEvent(event);
139
+ // If the event was ignored, return null
140
+ if (inserted === null)
141
+ return null;
133
142
  // Copy cached data if its a duplicate event
134
143
  if (event !== inserted)
135
144
  EventStore.mergeDuplicateEvent(event, inserted);
@@ -167,10 +176,11 @@ export class EventStore {
167
176
  getAll(filters) {
168
177
  return this.database.getEventsForFilters(Array.isArray(filters) ? filters : [filters]);
169
178
  }
170
- /** Check if the store has an event */
179
+ /** Check if the store has an event by id */
171
180
  hasEvent(id) {
172
181
  return this.database.hasEvent(id);
173
182
  }
183
+ /** Get an event by id from the store */
174
184
  getEvent(id) {
175
185
  return this.database.getEvent(id);
176
186
  }
@@ -27,7 +27,7 @@ export interface IStreamEventStore {
27
27
  timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
28
28
  }
29
29
  export interface IEventStore extends ISyncEventStore, IStreamEventStore {
30
- add(event: NostrEvent, fromRelay?: string): NostrEvent;
30
+ add(event: NostrEvent, fromRelay?: string): NostrEvent | null;
31
31
  remove(event: string | NostrEvent): boolean;
32
- update(event: NostrEvent): NostrEvent;
32
+ update(event: NostrEvent): void;
33
33
  }
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { kinds } from "nostr-tools";
3
+ import { getHandlerSupportedKinds, getHandlerName, getHandlerPicture, getHandlerDescription, getHandlerLinkTemplate, createHandlerProfileLink, createHandlerEventLink, createHandlerAddressLink, createHandlerLink, } from "../app-handlers.js";
4
+ import { FakeUser } from "../../__tests__/fixtures.js";
5
+ import { naddrEncode, neventEncode, noteEncode, nprofileEncode, npubEncode } from "nostr-tools/nip19";
6
+ // Create a fake user for testing
7
+ const user = new FakeUser();
8
+ // Mock handler event based on NIP-89 specification
9
+ const mockHandler = user.event({
10
+ kind: kinds.Handlerinformation,
11
+ tags: [
12
+ ["d", "app-handler"],
13
+ ["k", "0"], // supports profiles
14
+ ["k", "1"], // supports notes
15
+ ["k", "30023"], // supports long-form content
16
+ ["web", "https://example.com/npub/<bech32>", "npub"],
17
+ ["web", "https://example.com/profile?nprofile=<bech32>", "nprofile"],
18
+ ["web", "https://example.com/note?id=<bech32>", "note"],
19
+ ["web", "https://example.com/event?nevent=<bech32>", "nevent"],
20
+ ["web", "https://example.com/article?naddr=<bech32>", "naddr"],
21
+ ["ios", "testhandler://<bech32>"],
22
+ ["android", "testhandler://<bech32>"],
23
+ ],
24
+ content: JSON.stringify({
25
+ name: "Test Handler",
26
+ display_name: "Test Handler App",
27
+ picture: "https://example.com/logo.png",
28
+ about: "A test handler for NIP-89",
29
+ }),
30
+ });
31
+ describe("getHandlerSupportedKinds", () => {
32
+ it("should return an array of supported kinds", () => {
33
+ expect(getHandlerSupportedKinds(mockHandler)).toEqual([0, 1, 30023]);
34
+ });
35
+ it("should return an empty array when no kinds are specified", () => {
36
+ const handlerWithoutKinds = user.event({
37
+ kind: kinds.Handlerinformation,
38
+ tags: mockHandler.tags.filter((tag) => tag[0] !== "k"),
39
+ content: mockHandler.content,
40
+ });
41
+ expect(getHandlerSupportedKinds(handlerWithoutKinds)).toEqual([]);
42
+ });
43
+ });
44
+ describe("getHandlerName", () => {
45
+ it("should return the handler name", () => {
46
+ expect(getHandlerName(mockHandler)).toBe("Test Handler App");
47
+ });
48
+ });
49
+ describe("getHandlerPicture", () => {
50
+ it("should return the handler picture", () => {
51
+ const picture = getHandlerPicture(mockHandler);
52
+ expect(picture).toBe("https://example.com/logo.png");
53
+ });
54
+ it("should return the fallback when no picture is available", () => {
55
+ const handlerWithoutPicture = user.event({
56
+ kind: kinds.Handlerinformation,
57
+ tags: mockHandler.tags,
58
+ content: JSON.stringify({
59
+ name: "Test Handler",
60
+ display_name: "Test Handler App",
61
+ about: "A test handler for NIP-89",
62
+ }),
63
+ });
64
+ const fallback = "https://fallback.com/default.png";
65
+ const picture = getHandlerPicture(handlerWithoutPicture, fallback);
66
+ expect(picture).toBe(fallback);
67
+ });
68
+ });
69
+ describe("getHandlerDescription", () => {
70
+ it("should return the handler description", () => {
71
+ const description = getHandlerDescription(mockHandler);
72
+ expect(description).toBe("A test handler for NIP-89");
73
+ });
74
+ });
75
+ describe("getHandlerLinkTemplate", () => {
76
+ it("should return the web link template for a specific type", () => {
77
+ const template = getHandlerLinkTemplate(mockHandler, "web", "npub");
78
+ expect(template).toBe("https://example.com/npub/<bech32>");
79
+ });
80
+ it("should return the ios link template", () => {
81
+ const template = getHandlerLinkTemplate(mockHandler, "ios");
82
+ expect(template).toBe("testhandler://<bech32>");
83
+ });
84
+ it("should return undefined when no template exists", () => {
85
+ // @ts-expect-error - unknown-type is not a valid type
86
+ const template = getHandlerLinkTemplate(mockHandler, "web", "unknown-type");
87
+ expect(template).toBeUndefined();
88
+ });
89
+ });
90
+ describe("createHandlerProfileLink", () => {
91
+ it("should create a profile link using nprofile format", () => {
92
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
93
+ expect(createHandlerProfileLink(mockHandler, pointer)).toEqual(`https://example.com/profile?nprofile=${nprofileEncode(pointer)}`);
94
+ });
95
+ it("should fallback to npub when nprofile template is not available", () => {
96
+ const handlerWithoutNprofile = user.event({
97
+ kind: kinds.Handlerinformation,
98
+ tags: mockHandler.tags.filter((tag) => !(tag[0] === "web" && tag[2] === "nprofile")),
99
+ content: mockHandler.content,
100
+ });
101
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
102
+ expect(createHandlerProfileLink(handlerWithoutNprofile, pointer)).toEqual(`https://example.com/npub/${npubEncode(pointer.pubkey)}`);
103
+ });
104
+ it("should use default when link type is not available", () => {
105
+ const handlerWithoutNprofile = user.event({
106
+ kind: kinds.Handlerinformation,
107
+ tags: [...mockHandler.tags.filter((tag) => !tag[2]), ["web", "https://example.com/<bech32>"]],
108
+ content: mockHandler.content,
109
+ });
110
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
111
+ expect(createHandlerProfileLink(handlerWithoutNprofile, pointer)).toEqual(`https://example.com/${nprofileEncode(pointer)}`);
112
+ });
113
+ });
114
+ describe("createHandlerEventLink", () => {
115
+ it("should create an event link using nevent format", () => {
116
+ const pointer = { id: user.event({ kind: 1111, content: "hello" }).id, relays: ["wss://relay.example.com"] };
117
+ expect(createHandlerEventLink(mockHandler, pointer)).toEqual(`https://example.com/event?nevent=${neventEncode(pointer)}`);
118
+ });
119
+ it("should fallback to note when nevent template is not available", () => {
120
+ const handlerWithoutNevent = user.event({
121
+ kind: kinds.Handlerinformation,
122
+ tags: mockHandler.tags.filter((tag) => tag[2] !== "nevent"),
123
+ content: mockHandler.content,
124
+ });
125
+ const pointer = { id: user.note("hello").id, relays: ["wss://relay.example.com"] };
126
+ expect(createHandlerEventLink(handlerWithoutNevent, pointer)).toEqual(`https://example.com/note?id=${noteEncode(pointer.id)}`);
127
+ });
128
+ it("should use default when link type is not available", () => {
129
+ const handlerWithoutNevent = user.event({
130
+ kind: kinds.Handlerinformation,
131
+ tags: [...mockHandler.tags.filter((tag) => !tag[2]), ["web", "https://example.com/<bech32>"]],
132
+ content: mockHandler.content,
133
+ });
134
+ const pointer = { id: user.note("hello").id, relays: ["wss://relay.example.com"] };
135
+ expect(createHandlerEventLink(handlerWithoutNevent, pointer)).toEqual(`https://example.com/${neventEncode(pointer)}`);
136
+ });
137
+ });
138
+ describe("createHandlerAddressLink", () => {
139
+ it("should create an address link using naddr format", () => {
140
+ const pointer = { identifier: "article1", pubkey: user.pubkey, kind: 30023, relays: ["wss://relay.example.com"] };
141
+ expect(createHandlerAddressLink(mockHandler, pointer)).toEqual(`https://example.com/article?naddr=${naddrEncode(pointer)}`);
142
+ });
143
+ it("should use default when link type is not available", () => {
144
+ const handlerWithoutNaddr = user.event({
145
+ kind: kinds.Handlerinformation,
146
+ tags: [...mockHandler.tags.filter((tag) => !tag[2]), ["web", "https://example.com/<bech32>"]],
147
+ content: mockHandler.content,
148
+ });
149
+ const pointer = { identifier: "article1", pubkey: user.pubkey, kind: 30023, relays: ["wss://relay.example.com"] };
150
+ expect(createHandlerAddressLink(handlerWithoutNaddr, pointer)).toEqual(`https://example.com/${naddrEncode(pointer)}`);
151
+ });
152
+ });
153
+ describe("createHandlerLink", () => {
154
+ it("should create a profile link for profile pointers", () => {
155
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
156
+ expect(createHandlerLink(mockHandler, pointer)).toEqual(`https://example.com/profile?nprofile=${nprofileEncode(pointer)}`);
157
+ });
158
+ it("should create an event link for event pointers", () => {
159
+ const pointer = { id: user.event({ kind: 1111, content: "hello" }).id, relays: ["wss://relay.example.com"] };
160
+ expect(createHandlerLink(mockHandler, pointer)).toEqual(`https://example.com/event?nevent=${neventEncode(pointer)}`);
161
+ });
162
+ it("should create an address link for address pointers", () => {
163
+ const pointer = { identifier: "article1", pubkey: user.pubkey, kind: 30023, relays: ["wss://relay.example.com"] };
164
+ expect(createHandlerLink(mockHandler, pointer)).toEqual(`https://example.com/article?naddr=${naddrEncode(pointer)}`);
165
+ });
166
+ it("should fallback to web platform when specified platform has no template", () => {
167
+ const handlerWithoutAndroid = user.event({
168
+ kind: kinds.Handlerinformation,
169
+ tags: mockHandler.tags.filter((tag) => tag[0] !== "android"),
170
+ content: mockHandler.content,
171
+ });
172
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
173
+ expect(createHandlerLink(handlerWithoutAndroid, pointer, "android")).toContain("https://example.com/profile?nprofile=");
174
+ });
175
+ it("should not fallback to web platform when webFallback is false", () => {
176
+ const handlerWithoutAndroid = user.event({
177
+ kind: kinds.Handlerinformation,
178
+ tags: mockHandler.tags.filter((tag) => tag[0] !== "android"),
179
+ content: mockHandler.content,
180
+ });
181
+ const pointer = { pubkey: user.pubkey, relays: ["wss://relay.example.com"] };
182
+ expect(createHandlerLink(handlerWithoutAndroid, pointer, "android", false)).toBeUndefined();
183
+ });
184
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { getEmojiTag } from "../emoji.js";
2
+ import { getEmojiTag, getReactionEmoji } from "../emoji.js";
3
3
  import { FakeUser } from "../../__tests__/fixtures.js";
4
4
  const user = new FakeUser();
5
5
  describe("getEmojiTag", () => {
@@ -13,3 +13,98 @@ describe("getEmojiTag", () => {
13
13
  expect(getEmojiTag(user.note("hello :custom:", { tags: [["emoji", "custom", "https://cdn.example.com/reaction1.png"]] }), "CustoM")).toEqual(["emoji", "custom", "https://cdn.example.com/reaction1.png"]);
14
14
  });
15
15
  });
16
+ describe("getReactionEmoji", () => {
17
+ it("returns emoji object when content matches emoji tag", () => {
18
+ const event = user.event({
19
+ kind: 7,
20
+ tags: [["emoji", "heart", "https://cdn.example.com/heart.png"]],
21
+ content: ":heart:",
22
+ });
23
+ const result = getReactionEmoji(event);
24
+ expect(result).toEqual({
25
+ shortcode: "heart",
26
+ url: "https://cdn.example.com/heart.png",
27
+ });
28
+ });
29
+ it("should return undefined when content is invalid shortcode", () => {
30
+ const event = user.event({
31
+ kind: 7,
32
+ tags: [["emoji", "smile", "https://cdn.example.com/smile.png"]],
33
+ content: ":smile",
34
+ });
35
+ const result = getReactionEmoji(event);
36
+ expect(result).toBeUndefined();
37
+ });
38
+ it("handles double colon issue", () => {
39
+ const event = user.event({
40
+ kind: 7,
41
+ tags: [["emoji", "smile", "https://cdn.example.com/smile.png"]],
42
+ content: "::smile::",
43
+ });
44
+ const result = getReactionEmoji(event);
45
+ expect(result).toEqual({
46
+ shortcode: "smile",
47
+ url: "https://cdn.example.com/smile.png",
48
+ });
49
+ });
50
+ it("trims whitespace from content", () => {
51
+ const event = user.event({
52
+ kind: 7,
53
+ tags: [["emoji", "thumbsup", "https://cdn.example.com/thumbsup.png"]],
54
+ content: " :thumbsup: ",
55
+ });
56
+ const result = getReactionEmoji(event);
57
+ expect(result).toEqual({
58
+ shortcode: "thumbsup",
59
+ url: "https://cdn.example.com/thumbsup.png",
60
+ });
61
+ });
62
+ it("returns undefined when emoji tag is missing", () => {
63
+ const event = user.event({
64
+ kind: 7,
65
+ tags: [["p", "pub1"]],
66
+ content: ":missing:",
67
+ });
68
+ const result = getReactionEmoji(event);
69
+ expect(result).toBeUndefined();
70
+ });
71
+ it("returns undefined when content is empty", () => {
72
+ const event = user.event({
73
+ kind: 7,
74
+ tags: [["emoji", "star", "https://cdn.example.com/star.png"]],
75
+ content: "",
76
+ });
77
+ const result = getReactionEmoji(event);
78
+ expect(result).toBeUndefined();
79
+ });
80
+ it("returns undefined when content is just colons", () => {
81
+ const event = user.event({
82
+ kind: 7,
83
+ tags: [["emoji", "fire", "https://cdn.example.com/fire.png"]],
84
+ content: "::",
85
+ });
86
+ const result = getReactionEmoji(event);
87
+ expect(result).toBeUndefined();
88
+ });
89
+ it("returns undefined when emoji tag is invalid (missing url)", () => {
90
+ const event = user.event({
91
+ kind: 7,
92
+ tags: [["emoji", "invalid"]],
93
+ content: ":invalid:",
94
+ });
95
+ const result = getReactionEmoji(event);
96
+ expect(result).toBeUndefined();
97
+ });
98
+ it("handles capital letters", () => {
99
+ const event = user.event({
100
+ kind: 7,
101
+ tags: [["emoji", "heart", "https://cdn.example.com/heart.png"]],
102
+ content: ":HEART:",
103
+ });
104
+ const result = getReactionEmoji(event);
105
+ expect(result).toEqual({
106
+ shortcode: "heart",
107
+ url: "https://cdn.example.com/heart.png",
108
+ });
109
+ });
110
+ });
@@ -37,11 +37,17 @@ describe("exports", () => {
37
37
  "NIP05_REGEX",
38
38
  "Nip10ThreadRefsSymbol",
39
39
  "PICTURE_POST_KIND",
40
+ "ParsedReportSymbol",
40
41
  "ProfileContentSymbol",
41
42
  "PublicContactsSymbol",
43
+ "ReactionAddressPointerSymbol",
44
+ "ReactionEventPointerSymbol",
42
45
  "ReplaceableIdentifierSymbol",
46
+ "ReportReason",
43
47
  "STREAM_EXT",
44
48
  "SeenRelaysSymbol",
49
+ "SharedAddressPointerSymbol",
50
+ "SharedEventPointerSymbol",
45
51
  "SharedEventSymbol",
46
52
  "UserStatusPointerSymbol",
47
53
  "VIDEO_EXT",
@@ -50,11 +56,16 @@ describe("exports", () => {
50
56
  "ZapFromSymbol",
51
57
  "ZapInvoiceSymbol",
52
58
  "ZapRequestSymbol",
59
+ "addRelayHintsToPointer",
53
60
  "addSeenRelay",
54
61
  "areBlossomServersEqual",
55
62
  "canHaveHiddenContent",
56
63
  "canHaveHiddenTags",
57
64
  "convertToUrl",
65
+ "createHandlerAddressLink",
66
+ "createHandlerEventLink",
67
+ "createHandlerLink",
68
+ "createHandlerProfileLink",
58
69
  "createMutedWordsRegExp",
59
70
  "createReplaceableAddress",
60
71
  "decodeGroupPointer",
@@ -62,6 +73,8 @@ describe("exports", () => {
62
73
  "decryptDirectMessage",
63
74
  "encodeDecodeResult",
64
75
  "encodeGroupPointer",
76
+ "ensureProtocol",
77
+ "ensureWebSocketURL",
65
78
  "fakeVerifyEvent",
66
79
  "getAddressPointerForEvent",
67
80
  "getAddressPointerFromATag",
@@ -82,6 +95,8 @@ describe("exports", () => {
82
95
  "getDeleteCoordinates",
83
96
  "getDeleteIds",
84
97
  "getDisplayName",
98
+ "getEmbededSharedEvent",
99
+ "getEmojiFromTags",
85
100
  "getEmojiTag",
86
101
  "getEmojis",
87
102
  "getEventPointerForEvent",
@@ -96,6 +111,12 @@ describe("exports", () => {
96
111
  "getGiftWrapEvent",
97
112
  "getGiftWrapSeal",
98
113
  "getGroupPointerFromGroupTag",
114
+ "getGroupPointerFromHTag",
115
+ "getHandlerDescription",
116
+ "getHandlerLinkTemplate",
117
+ "getHandlerName",
118
+ "getHandlerPicture",
119
+ "getHandlerSupportedKinds",
99
120
  "getHashtagTag",
100
121
  "getHiddenBookmarks",
101
122
  "getHiddenContacts",
@@ -118,8 +139,8 @@ describe("exports", () => {
118
139
  "getParentEventStore",
119
140
  "getPicturePostAttachments",
120
141
  "getPointerForEvent",
121
- "getPointerFromTag",
122
142
  "getProfileContent",
143
+ "getProfilePicture",
123
144
  "getProfilePointerFromPTag",
124
145
  "getProfilePointersFromList",
125
146
  "getPubkeyFromDecodeResult",
@@ -127,13 +148,19 @@ describe("exports", () => {
127
148
  "getPublicContacts",
128
149
  "getPublicGroups",
129
150
  "getPublicMutedThings",
151
+ "getReactionAddressPointer",
152
+ "getReactionEmoji",
153
+ "getReactionEventPointer",
130
154
  "getRelaysFromContactsEvent",
131
155
  "getRelaysFromList",
132
156
  "getReplaceableAddress",
133
157
  "getReplaceableIdentifier",
134
158
  "getReplaceableUID",
159
+ "getReported",
135
160
  "getSeenRelays",
136
161
  "getSha256FromURL",
162
+ "getSharedAddressPointer",
163
+ "getSharedEventPointer",
137
164
  "getTagValue",
138
165
  "getURLFilename",
139
166
  "getUserStatusPointer",
@@ -145,6 +172,7 @@ describe("exports", () => {
145
172
  "getZapRequest",
146
173
  "getZapSender",
147
174
  "getZapSplits",
175
+ "groupMessageEvents",
148
176
  "hasHiddenContent",
149
177
  "hasHiddenTags",
150
178
  "interpretThreadTags",
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { decodeGroupPointer, encodeGroupPointer } from "../groups.js";
3
+ describe("Group pointer utilities", () => {
4
+ describe("decodeGroupPointer", () => {
5
+ it("should decode a valid group pointer", () => {
6
+ const pointer = decodeGroupPointer("relay.example.com'group123");
7
+ expect(pointer).toEqual({
8
+ relay: "wss://relay.example.com",
9
+ id: "group123",
10
+ });
11
+ });
12
+ it("should add wss:// protocol if missing", () => {
13
+ const pointer = decodeGroupPointer("relay.example.com'group123");
14
+ expect(pointer.relay).toBe("wss://relay.example.com");
15
+ });
16
+ it("should preserve existing protocol if present", () => {
17
+ const pointer = decodeGroupPointer("wss://relay.example.com'group123");
18
+ expect(pointer.relay).toBe("wss://relay.example.com");
19
+ const wsPointer = decodeGroupPointer("ws://relay.example.com'group123");
20
+ expect(wsPointer.relay).toBe("ws://relay.example.com");
21
+ });
22
+ it("should handle default group id", () => {
23
+ const pointer = decodeGroupPointer("relay.example.com'");
24
+ expect(pointer).toEqual({
25
+ relay: "wss://relay.example.com",
26
+ id: "_",
27
+ });
28
+ });
29
+ it("should throw error if relay is missing", () => {
30
+ expect(() => decodeGroupPointer("'group123")).toThrow("Group pointer missing relay");
31
+ });
32
+ });
33
+ describe("encodeGroupPointer", () => {
34
+ it("should encode a valid group pointer", () => {
35
+ const pointer = {
36
+ relay: "wss://relay.example.com",
37
+ id: "group123",
38
+ };
39
+ expect(encodeGroupPointer(pointer)).toBe("relay.example.com'group123");
40
+ });
41
+ it("should strip protocol from relay", () => {
42
+ const pointer = {
43
+ relay: "wss://relay.example.com",
44
+ id: "group123",
45
+ };
46
+ expect(encodeGroupPointer(pointer)).toBe("relay.example.com'group123");
47
+ const wsPointer = {
48
+ relay: "ws://relay.example.com",
49
+ id: "group123",
50
+ };
51
+ expect(encodeGroupPointer(wsPointer)).toBe("relay.example.com'group123");
52
+ });
53
+ it("should handle invalid URLs by using the raw value", () => {
54
+ const pointer = {
55
+ relay: "invalid-url",
56
+ id: "group123",
57
+ };
58
+ expect(encodeGroupPointer(pointer)).toBe("invalid-url'group123");
59
+ });
60
+ });
61
+ });