applesauce-core 0.0.0-next-20250522030625 → 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.
@@ -73,6 +73,8 @@ describe("exports", () => {
73
73
  "decryptDirectMessage",
74
74
  "encodeDecodeResult",
75
75
  "encodeGroupPointer",
76
+ "ensureProtocol",
77
+ "ensureWebSocketURL",
76
78
  "fakeVerifyEvent",
77
79
  "getAddressPointerForEvent",
78
80
  "getAddressPointerFromATag",
@@ -109,6 +111,7 @@ describe("exports", () => {
109
111
  "getGiftWrapEvent",
110
112
  "getGiftWrapSeal",
111
113
  "getGroupPointerFromGroupTag",
114
+ "getGroupPointerFromHTag",
112
115
  "getHandlerDescription",
113
116
  "getHandlerLinkTemplate",
114
117
  "getHandlerName",
@@ -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
+ });
@@ -3,8 +3,11 @@ export declare const GROUPS_LIST_KIND = 10009;
3
3
  export declare const GROUP_MESSAGE_KIND = 9;
4
4
  /** NIP-29 group pointer */
5
5
  export type GroupPointer = {
6
+ /** the id of the group */
6
7
  id: string;
8
+ /** The url to the relay with wss:// or ws:// protocol */
7
9
  relay: string;
10
+ /** The name of the group */
8
11
  name?: string;
9
12
  };
10
13
  /**
@@ -16,6 +19,8 @@ export declare function decodeGroupPointer(str: string): GroupPointer;
16
19
  export declare function encodeGroupPointer(pointer: GroupPointer): string;
17
20
  export declare const GroupsPublicSymbol: unique symbol;
18
21
  export declare const GroupsHiddenSymbol: unique symbol;
22
+ /** gets a {@link GroupPointer} from a "h" tag if it has a relay hint */
23
+ export declare function getGroupPointerFromHTag(tag: string[]): GroupPointer | undefined;
19
24
  /** gets a {@link GroupPointer} from a "group" tag */
20
25
  export declare function getGroupPointerFromGroupTag(tag: string[]): GroupPointer;
21
26
  /** Returns all the public groups from a k:10009 list */
@@ -8,9 +8,12 @@ export const GROUP_MESSAGE_KIND = 9;
8
8
  * @throws
9
9
  */
10
10
  export function decodeGroupPointer(str) {
11
- const [relay, id] = str.split("'");
11
+ let [relay, id] = str.split("'");
12
12
  if (!relay)
13
13
  throw new Error("Group pointer missing relay");
14
+ // Prepend wss:// if missing
15
+ if (!relay.match(/^wss?:/))
16
+ relay = `wss://${relay}`;
14
17
  return { relay, id: id || "_" };
15
18
  }
16
19
  /** Converts a group pointer into a group identifier */
@@ -20,6 +23,13 @@ export function encodeGroupPointer(pointer) {
20
23
  }
21
24
  export const GroupsPublicSymbol = Symbol.for("groups-public");
22
25
  export const GroupsHiddenSymbol = Symbol.for("groups-hidden");
26
+ /** gets a {@link GroupPointer} from a "h" tag if it has a relay hint */
27
+ export function getGroupPointerFromHTag(tag) {
28
+ const [_, id, relay] = tag;
29
+ if (!id || !relay)
30
+ return undefined;
31
+ return { id, relay };
32
+ }
23
33
  /** gets a {@link GroupPointer} from a "group" tag */
24
34
  export function getGroupPointerFromGroupTag(tag) {
25
35
  const [_, id, relay, name] = tag;
@@ -14,6 +14,10 @@ export declare function isStreamURL(url: string | URL): boolean;
14
14
  export declare function isAudioURL(url: string | URL): boolean;
15
15
  /** Tests if two URLs are the same */
16
16
  export declare function isSameURL(a: string | URL, b: string | URL): boolean;
17
+ /** Adds a protocol to a URL string if its missing one */
18
+ export declare function ensureProtocol(url: string, protocol?: string): string;
19
+ /** Converts a URL to a WebSocket URL */
20
+ export declare function ensureWebSocketURL<T extends string | URL>(url: T): T;
17
21
  /**
18
22
  * Normalizes a string into a relay URL
19
23
  * Does not remove the trailing slash
@@ -39,6 +39,26 @@ export function isSameURL(a, b) {
39
39
  return false;
40
40
  }
41
41
  }
42
+ /** Adds a protocol to a URL string if its missing one */
43
+ export function ensureProtocol(url, protocol = "https:") {
44
+ // Check if the URL already has a protocol
45
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]+:/.test(url))
46
+ return url;
47
+ return protocol + "//" + url;
48
+ }
49
+ /** Converts a URL to a WebSocket URL */
50
+ export function ensureWebSocketURL(url) {
51
+ const p = typeof url === "string" ? new URL(ensureProtocol(url, "wss:")) : new URL(url);
52
+ if (p.protocol === "http:")
53
+ p.protocol = "ws:";
54
+ else if (p.protocol === "https:")
55
+ p.protocol = "wss:";
56
+ else
57
+ p.protocol = "wss:";
58
+ // return a string if a string was passed in
59
+ // @ts-expect-error
60
+ return typeof url === "string" ? p.toString() : p;
61
+ }
42
62
  /**
43
63
  * Normalizes a string into a relay URL
44
64
  * Does not remove the trailing slash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "0.0.0-next-20250522030625",
3
+ "version": "0.0.0-next-20250526151506",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import * as core from "../index.js";
3
- describe("core", () => {
4
- it("should export the correct functions", () => {
5
- expect(Object.keys(core).sort()).toMatchInlineSnapshot(`
6
- [
7
- "Database",
8
- "EventStore",
9
- "EventStoreSymbol",
10
- "Helpers",
11
- "Queries",
12
- "QueryStore",
13
- "logger",
14
- ]
15
- `);
16
- });
17
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,220 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import * as helpers from "../index.js";
3
- describe("helpers", () => {
4
- it("should export the correct functions", () => {
5
- expect(Object.keys(helpers).sort()).toMatchInlineSnapshot(`
6
- [
7
- "AUDIO_EXT",
8
- "BLOSSOM_SERVER_LIST_KIND",
9
- "BookmarkHiddenSymbol",
10
- "BookmarkPublicSymbol",
11
- "COMMENT_KIND",
12
- "ChannelMetadataSymbol",
13
- "CommentReplyPointerSymbol",
14
- "CommentRootPointerSymbol",
15
- "ContactsRelaysSymbol",
16
- "EventContentEncryptionMethod",
17
- "EventIndexableTagsSymbol",
18
- "EventUIDSymbol",
19
- "FAVORITE_RELAYS_KIND",
20
- "FromCacheSymbol",
21
- "GROUPS_LIST_KIND",
22
- "GROUP_MESSAGE_KIND",
23
- "GiftWrapEventSymbol",
24
- "GiftWrapSealSymbol",
25
- "GroupsHiddenSymbol",
26
- "GroupsPublicSymbol",
27
- "HiddenContactsSymbol",
28
- "HiddenContentSymbol",
29
- "HiddenTagsSymbol",
30
- "IMAGE_EXT",
31
- "LRU",
32
- "MailboxesInboxesSymbol",
33
- "MailboxesOutboxesSymbol",
34
- "MediaAttachmentsSymbol",
35
- "MuteHiddenSymbol",
36
- "MutePublicSymbol",
37
- "NIP05_REGEX",
38
- "Nip10ThreadRefsSymbol",
39
- "PICTURE_POST_KIND",
40
- "ProfileContentSymbol",
41
- "PublicContactsSymbol",
42
- "ReplaceableIdentifierSymbol",
43
- "STREAM_EXT",
44
- "SeenRelaysSymbol",
45
- "SharedEventSymbol",
46
- "UserStatusPointerSymbol",
47
- "VIDEO_EXT",
48
- "ZapAddressPointerSymbol",
49
- "ZapEventPointerSymbol",
50
- "ZapFromSymbol",
51
- "ZapInvoiceSymbol",
52
- "ZapRequestSymbol",
53
- "addSeenRelay",
54
- "areBlossomServersEqual",
55
- "canHaveHiddenContent",
56
- "canHaveHiddenTags",
57
- "convertToUrl",
58
- "createMutedWordsRegExp",
59
- "createReplaceableAddress",
60
- "decodeGroupPointer",
61
- "decodeLNURL",
62
- "decryptDirectMessage",
63
- "encodeDecodeResult",
64
- "encodeGroupPointer",
65
- "fakeVerifyEvent",
66
- "getAddressPointerForEvent",
67
- "getAddressPointerFromATag",
68
- "getAddressPointersFromList",
69
- "getBlossomServersFromList",
70
- "getBookmarks",
71
- "getCachedValue",
72
- "getChannelMetadataContent",
73
- "getChannelPointer",
74
- "getCommentAddressPointer",
75
- "getCommentEventPointer",
76
- "getCommentExternalPointer",
77
- "getCommentReplyPointer",
78
- "getCommentRootPointer",
79
- "getContacts",
80
- "getContentWarning",
81
- "getCoordinateFromAddressPointer",
82
- "getDeleteCoordinates",
83
- "getDeleteIds",
84
- "getDisplayName",
85
- "getEmojiTag",
86
- "getEmojis",
87
- "getEventPointerForEvent",
88
- "getEventPointerFromETag",
89
- "getEventPointerFromQTag",
90
- "getEventPointerFromThreadTag",
91
- "getEventPointersFromList",
92
- "getEventUID",
93
- "getExternalPointerFromTag",
94
- "getFileMetadata",
95
- "getFileMetadataFromImetaTag",
96
- "getGiftWrapEvent",
97
- "getGiftWrapSeal",
98
- "getGroupPointerFromGroupTag",
99
- "getHashtagTag",
100
- "getHiddenBookmarks",
101
- "getHiddenContacts",
102
- "getHiddenContent",
103
- "getHiddenContentEncryptionMethods",
104
- "getHiddenGroups",
105
- "getHiddenMutedThings",
106
- "getHiddenTags",
107
- "getHiddenTagsEncryptionMethods",
108
- "getInboxes",
109
- "getIndexableTags",
110
- "getInvoice",
111
- "getListTags",
112
- "getMediaAttachments",
113
- "getMutedThings",
114
- "getNip10References",
115
- "getOrComputeCachedValue",
116
- "getOutboxes",
117
- "getPackName",
118
- "getParentEventStore",
119
- "getPicturePostAttachments",
120
- "getPointerForEvent",
121
- "getPointerFromTag",
122
- "getProfileContent",
123
- "getProfilePointerFromPTag",
124
- "getProfilePointersFromList",
125
- "getPubkeyFromDecodeResult",
126
- "getPublicBookmarks",
127
- "getPublicContacts",
128
- "getPublicGroups",
129
- "getPublicMutedThings",
130
- "getRelaysFromContactsEvent",
131
- "getRelaysFromList",
132
- "getReplaceableAddress",
133
- "getReplaceableIdentifier",
134
- "getReplaceableUID",
135
- "getSeenRelays",
136
- "getSha256FromURL",
137
- "getTagValue",
138
- "getURLFilename",
139
- "getUserStatusPointer",
140
- "getZapAddressPointer",
141
- "getZapEventPointer",
142
- "getZapPayment",
143
- "getZapPreimage",
144
- "getZapRecipient",
145
- "getZapRequest",
146
- "getZapSender",
147
- "getZapSplits",
148
- "hasHiddenContent",
149
- "hasHiddenTags",
150
- "interpretThreadTags",
151
- "isATag",
152
- "isAddressPointer",
153
- "isAddressPointerInList",
154
- "isAudioURL",
155
- "isCommentAddressPointer",
156
- "isCommentEventPointer",
157
- "isDTag",
158
- "isETag",
159
- "isEvent",
160
- "isEventPointer",
161
- "isEventPointerInList",
162
- "isFilterEqual",
163
- "isFromCache",
164
- "isGiftWrapLocked",
165
- "isHex",
166
- "isHexKey",
167
- "isHiddenContentLocked",
168
- "isHiddenTagsLocked",
169
- "isImageURL",
170
- "isNameValueTag",
171
- "isNip05",
172
- "isPTag",
173
- "isProfilePointerInList",
174
- "isRTag",
175
- "isReplaceable",
176
- "isSafeRelayURL",
177
- "isSameURL",
178
- "isSha256",
179
- "isStreamURL",
180
- "isTTag",
181
- "isValidList",
182
- "isValidProfile",
183
- "isValidZap",
184
- "isVideoURL",
185
- "lockHiddenContent",
186
- "lockHiddenTags",
187
- "markFromCache",
188
- "matchFilter",
189
- "matchFilters",
190
- "matchMutes",
191
- "mergeBlossomServers",
192
- "mergeBookmarks",
193
- "mergeContacts",
194
- "mergeFilters",
195
- "mergeMutes",
196
- "mergeRelaySets",
197
- "normalizeURL",
198
- "parseBolt11",
199
- "parseBookmarkTags",
200
- "parseCoordinate",
201
- "parseExternalPointer",
202
- "parseFileMetadataTags",
203
- "parseLNURLOrAddress",
204
- "parseLightningAddress",
205
- "parseMutedTags",
206
- "parseNIP05Address",
207
- "parseSharedEvent",
208
- "processTags",
209
- "safeParse",
210
- "setCachedValue",
211
- "setEventContentEncryptionMethod",
212
- "stripInvisibleChar",
213
- "unixNow",
214
- "unlockGiftWrap",
215
- "unlockHiddenContent",
216
- "unlockHiddenTags",
217
- ]
218
- `);
219
- });
220
- });
@@ -1,55 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
2
- import { subscribeSpyTo } from "@hirez_io/observer-spy";
3
- import { of } from "rxjs";
4
- import { EventStore } from "../../event-store/event-store.js";
5
- import { listenLatestUpdates } from "../listen-latest-updates.js";
6
- import { FakeUser } from "../../__tests__/fixtures.js";
7
- let eventStore;
8
- let user;
9
- let event;
10
- beforeEach(() => {
11
- eventStore = new EventStore();
12
- user = new FakeUser();
13
- event = user.note("original content");
14
- });
15
- describe("listenLatestUpdates", () => {
16
- it("should emit the initial event", () => {
17
- const source = of(event);
18
- const spy = subscribeSpyTo(source.pipe(listenLatestUpdates(eventStore)));
19
- expect(spy.getValues()).toEqual([event]);
20
- });
21
- it("should emit the event again when it's updated in the event store", () => {
22
- // Add the event to the store first
23
- eventStore.add(event);
24
- // Create a source that emits the event
25
- const source = of(event);
26
- const spy = subscribeSpyTo(source.pipe(listenLatestUpdates(eventStore)));
27
- // Create an updated version of the event
28
- Reflect.set(event, Symbol.for("new-prop"), "testing");
29
- // Update the event in the store
30
- eventStore.update(event);
31
- // Should have received both the original and updated event
32
- expect(spy.getValues()).toEqual([event, event]);
33
- });
34
- it("should not emit updates for other events", () => {
35
- // Add the event to the store
36
- eventStore.add(event);
37
- // Create a source that emits the event
38
- const source = of(event);
39
- const spy = subscribeSpyTo(source.pipe(listenLatestUpdates(eventStore)));
40
- // Create a different event
41
- const otherEvent = user.note("other content");
42
- // Add the other event to the store
43
- eventStore.add(otherEvent);
44
- // Should only have received the original event
45
- expect(spy.getValues()).toEqual([event]);
46
- });
47
- it("should handle undefined initial event", () => {
48
- const source = of(undefined);
49
- const spy = subscribeSpyTo(source.pipe(listenLatestUpdates(eventStore)));
50
- expect(spy.getValues()).toEqual([undefined]);
51
- // Adding events to the store should not trigger emissions
52
- eventStore.add(event);
53
- expect(spy.getValues()).toEqual([undefined]);
54
- });
55
- });
@@ -1,5 +0,0 @@
1
- import { NostrEvent } from "nostr-tools";
2
- import { MonoTypeOperatorFunction } from "rxjs";
3
- import { IStreamEventStore } from "../event-store/interface.js";
4
- /** Lists for any updates to the latest event and remits it */
5
- export declare function listenLatestUpdates(eventStore: IStreamEventStore): MonoTypeOperatorFunction<NostrEvent | undefined>;
@@ -1,12 +0,0 @@
1
- import { filter, merge, tap } from "rxjs";
2
- /** Lists for any updates to the latest event and remits it */
3
- export function listenLatestUpdates(eventStore) {
4
- return (source) => {
5
- let latest;
6
- return merge(
7
- // Get the latest event
8
- source.pipe(tap((value) => (latest = value))),
9
- // listen for updates
10
- eventStore.updates.pipe(filter((e) => e.id === latest?.id)));
11
- };
12
- }