applesauce-core 0.0.0-next-20250806165639 → 0.0.0-next-20250815164532
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/event-store/database.d.ts +67 -0
- package/dist/event-store/database.js +316 -0
- package/dist/event-store/event-store.test.d.ts +1 -0
- package/dist/event-store/event-store.test.js +74 -0
- package/dist/helpers/__tests__/exports.test.js +2 -4
- package/dist/helpers/__tests__/nip-19.test.d.ts +1 -0
- package/dist/helpers/__tests__/nip-19.test.js +42 -0
- package/dist/helpers/direct-messages.d.ts +4 -0
- package/dist/helpers/direct-messages.js +5 -0
- package/dist/helpers/encrypted-content.d.ts +18 -10
- package/dist/helpers/encrypted-content.js +11 -3
- package/dist/helpers/file-metadata.test.d.ts +1 -0
- package/dist/helpers/file-metadata.test.js +103 -0
- package/dist/helpers/hidden-content.d.ts +3 -3
- package/dist/helpers/hidden-content.js +2 -2
- package/dist/helpers/hidden-tags.d.ts +5 -10
- package/dist/helpers/hidden-tags.js +3 -3
- package/dist/helpers/hidden-tags.test.d.ts +1 -0
- package/dist/helpers/hidden-tags.test.js +29 -0
- package/dist/helpers/highlight.d.ts +45 -0
- package/dist/helpers/highlight.js +76 -0
- package/dist/helpers/index.d.ts +2 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/legacy-direct-messages.d.ts +8 -0
- package/dist/helpers/legacy-direct-messages.js +17 -0
- package/dist/helpers/mailboxes.test.d.ts +1 -0
- package/dist/helpers/mailboxes.test.js +81 -0
- package/dist/helpers/nip-19.d.ts +4 -0
- package/dist/helpers/nip-19.js +27 -0
- package/dist/helpers/poll.d.ts +46 -0
- package/dist/helpers/poll.js +78 -0
- package/dist/helpers/tags.test.d.ts +1 -0
- package/dist/helpers/tags.test.js +16 -0
- package/dist/helpers/threading.test.d.ts +1 -0
- package/dist/helpers/threading.test.js +41 -0
- package/dist/helpers/wrapped-direct-messages.d.ts +6 -0
- package/dist/helpers/wrapped-direct-messages.js +11 -0
- package/dist/observable/getValue.d.ts +2 -0
- package/dist/observable/getValue.js +13 -0
- package/dist/observable/share-behavior.d.ts +2 -0
- package/dist/observable/share-behavior.js +7 -0
- package/dist/observable/share-latest-value.d.ts +6 -0
- package/dist/observable/share-latest-value.js +24 -0
- package/dist/observable/stateful.d.ts +10 -0
- package/dist/observable/stateful.js +60 -0
- package/dist/observable/throttle.d.ts +3 -0
- package/dist/observable/throttle.js +23 -0
- package/dist/queries/blossom.d.ts +2 -0
- package/dist/queries/blossom.js +10 -0
- package/dist/queries/bookmarks.d.ts +8 -0
- package/dist/queries/bookmarks.js +23 -0
- package/dist/queries/channels.d.ts +11 -0
- package/dist/queries/channels.js +73 -0
- package/dist/queries/comments.d.ts +4 -0
- package/dist/queries/comments.js +14 -0
- package/dist/queries/contacts.d.ts +3 -0
- package/dist/queries/contacts.js +12 -0
- package/dist/queries/index.d.ts +13 -0
- package/dist/queries/index.js +13 -0
- package/dist/queries/mailboxes.d.ts +6 -0
- package/dist/queries/mailboxes.js +13 -0
- package/dist/queries/mutes.d.ts +8 -0
- package/dist/queries/mutes.js +23 -0
- package/dist/queries/pins.d.ts +3 -0
- package/dist/queries/pins.js +12 -0
- package/dist/queries/profile.d.ts +4 -0
- package/dist/queries/profile.js +12 -0
- package/dist/queries/reactions.d.ts +4 -0
- package/dist/queries/reactions.js +19 -0
- package/dist/queries/simple.d.ts +16 -0
- package/dist/queries/simple.js +38 -0
- package/dist/queries/thread.d.ts +25 -0
- package/dist/queries/thread.js +92 -0
- package/dist/queries/user-status.d.ts +11 -0
- package/dist/queries/user-status.js +39 -0
- package/dist/queries/zaps.d.ts +5 -0
- package/dist/queries/zaps.js +21 -0
- package/dist/query-store/__tests__/query-store.test.d.ts +1 -0
- package/dist/query-store/__tests__/query-store.test.js +63 -0
- package/dist/query-store/index.d.ts +1 -0
- package/dist/query-store/index.js +1 -0
- package/dist/query-store/query-store.d.ts +53 -0
- package/dist/query-store/query-store.js +97 -0
- package/dist/query-store/query-store.test.d.ts +1 -0
- package/dist/query-store/query-store.test.js +33 -0
- package/dist/utils/lru.d.ts +32 -0
- package/dist/utils/lru.js +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { LRU } from "../helpers/lru.js";
|
|
4
|
+
/**
|
|
5
|
+
* An in-memory database for nostr events
|
|
6
|
+
* NOTE: does not handle replaceable events
|
|
7
|
+
*/
|
|
8
|
+
export declare class Database {
|
|
9
|
+
protected log: import("debug").Debugger;
|
|
10
|
+
/** Indexes */
|
|
11
|
+
protected kinds: Map<number, Set<import("nostr-tools").Event>>;
|
|
12
|
+
protected authors: Map<string, Set<import("nostr-tools").Event>>;
|
|
13
|
+
protected tags: LRU<Set<import("nostr-tools").Event>>;
|
|
14
|
+
protected created_at: NostrEvent[];
|
|
15
|
+
/** LRU cache of last events touched */
|
|
16
|
+
events: LRU<import("nostr-tools").Event>;
|
|
17
|
+
/** A sorted array of replaceable events by uid */
|
|
18
|
+
protected replaceable: Map<string, import("nostr-tools").Event[]>;
|
|
19
|
+
/** A stream of events inserted into the database */
|
|
20
|
+
inserted: Subject<import("nostr-tools").Event>;
|
|
21
|
+
/** A stream of events that have been updated */
|
|
22
|
+
updated: Subject<import("nostr-tools").Event>;
|
|
23
|
+
/** A stream of events removed from the database */
|
|
24
|
+
removed: Subject<import("nostr-tools").Event>;
|
|
25
|
+
/** A method thats called before a new event is inserted */
|
|
26
|
+
onBeforeInsert?: (event: NostrEvent) => void;
|
|
27
|
+
get size(): number;
|
|
28
|
+
protected claims: WeakMap<import("nostr-tools").Event, any>;
|
|
29
|
+
/** Index helper methods */
|
|
30
|
+
protected getKindIndex(kind: number): Set<import("nostr-tools").Event>;
|
|
31
|
+
protected getAuthorsIndex(author: string): Set<import("nostr-tools").Event>;
|
|
32
|
+
protected getTagIndex(tagAndValue: string): Set<import("nostr-tools").Event>;
|
|
33
|
+
/** Moves an event to the top of the LRU cache */
|
|
34
|
+
touch(event: NostrEvent): void;
|
|
35
|
+
/** Checks if the database contains an event without touching it */
|
|
36
|
+
hasEvent(id: string): boolean;
|
|
37
|
+
/** Gets a single event based on id */
|
|
38
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
39
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
40
|
+
hasReplaceable(kind: number, pubkey: string, d?: string): boolean;
|
|
41
|
+
/** Gets an array of replaceable events */
|
|
42
|
+
getReplaceable(kind: number, pubkey: string, d?: string): NostrEvent[] | undefined;
|
|
43
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
44
|
+
addEvent(event: NostrEvent): NostrEvent;
|
|
45
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
46
|
+
updateEvent(event: NostrEvent): NostrEvent;
|
|
47
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
48
|
+
removeEvent(eventOrId: string | NostrEvent): boolean;
|
|
49
|
+
/** Sets the claim on the event and touches it */
|
|
50
|
+
claimEvent(event: NostrEvent, claim: any): void;
|
|
51
|
+
/** Checks if an event is claimed by anything */
|
|
52
|
+
isClaimed(event: NostrEvent): boolean;
|
|
53
|
+
/** Removes a claim from an event */
|
|
54
|
+
removeClaim(event: NostrEvent, claim: any): void;
|
|
55
|
+
/** Removes all claims on an event */
|
|
56
|
+
clearClaim(event: NostrEvent): void;
|
|
57
|
+
iterateAuthors(authors: Iterable<string>): Generator<NostrEvent>;
|
|
58
|
+
iterateTag(tag: string, values: Iterable<string>): Generator<NostrEvent>;
|
|
59
|
+
iterateKinds(kinds: Iterable<number>): Generator<NostrEvent>;
|
|
60
|
+
iterateTime(since: number | undefined, until: number | undefined): Generator<NostrEvent>;
|
|
61
|
+
iterateIds(ids: Iterable<string>): Generator<NostrEvent>;
|
|
62
|
+
/** Returns all events that match the filter */
|
|
63
|
+
getEventsForFilter(filter: Filter): Set<NostrEvent>;
|
|
64
|
+
getEventsForFilters(filters: Filter[]): Set<NostrEvent>;
|
|
65
|
+
/** Remove the oldest events that are not claimed */
|
|
66
|
+
prune(limit?: number): number;
|
|
67
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
|
+
import { Subject } from "rxjs";
|
|
3
|
+
import { getEventUID, getIndexableTags, getReplaceableUID, isReplaceable } from "../helpers/event.js";
|
|
4
|
+
import { INDEXABLE_TAGS } from "./common.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { LRU } from "../helpers/lru.js";
|
|
7
|
+
/**
|
|
8
|
+
* An in-memory database for nostr events
|
|
9
|
+
* NOTE: does not handle replaceable events
|
|
10
|
+
*/
|
|
11
|
+
export class Database {
|
|
12
|
+
log = logger.extend("Database");
|
|
13
|
+
/** Indexes */
|
|
14
|
+
kinds = new Map();
|
|
15
|
+
authors = new Map();
|
|
16
|
+
tags = new LRU();
|
|
17
|
+
created_at = [];
|
|
18
|
+
/** LRU cache of last events touched */
|
|
19
|
+
events = new LRU();
|
|
20
|
+
/** A sorted array of replaceable events by uid */
|
|
21
|
+
replaceable = new Map();
|
|
22
|
+
/** A stream of events inserted into the database */
|
|
23
|
+
inserted = new Subject();
|
|
24
|
+
/** A stream of events that have been updated */
|
|
25
|
+
updated = new Subject();
|
|
26
|
+
/** A stream of events removed from the database */
|
|
27
|
+
removed = new Subject();
|
|
28
|
+
/** A method thats called before a new event is inserted */
|
|
29
|
+
onBeforeInsert;
|
|
30
|
+
get size() {
|
|
31
|
+
return this.events.size;
|
|
32
|
+
}
|
|
33
|
+
claims = new WeakMap();
|
|
34
|
+
/** Index helper methods */
|
|
35
|
+
getKindIndex(kind) {
|
|
36
|
+
if (!this.kinds.has(kind))
|
|
37
|
+
this.kinds.set(kind, new Set());
|
|
38
|
+
return this.kinds.get(kind);
|
|
39
|
+
}
|
|
40
|
+
getAuthorsIndex(author) {
|
|
41
|
+
if (!this.authors.has(author))
|
|
42
|
+
this.authors.set(author, new Set());
|
|
43
|
+
return this.authors.get(author);
|
|
44
|
+
}
|
|
45
|
+
getTagIndex(tagAndValue) {
|
|
46
|
+
if (!this.tags.has(tagAndValue)) {
|
|
47
|
+
// build new tag index from existing events
|
|
48
|
+
const events = new Set();
|
|
49
|
+
const ts = Date.now();
|
|
50
|
+
for (const event of this.events.values()) {
|
|
51
|
+
if (getIndexableTags(event).has(tagAndValue)) {
|
|
52
|
+
events.add(event);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const took = Date.now() - ts;
|
|
56
|
+
if (took > 100)
|
|
57
|
+
this.log(`Built index ${tagAndValue} took ${took}ms`);
|
|
58
|
+
this.tags.set(tagAndValue, events);
|
|
59
|
+
}
|
|
60
|
+
return this.tags.get(tagAndValue);
|
|
61
|
+
}
|
|
62
|
+
/** Moves an event to the top of the LRU cache */
|
|
63
|
+
touch(event) {
|
|
64
|
+
this.events.set(event.id, event);
|
|
65
|
+
}
|
|
66
|
+
/** Checks if the database contains an event without touching it */
|
|
67
|
+
hasEvent(id) {
|
|
68
|
+
return this.events.has(id);
|
|
69
|
+
}
|
|
70
|
+
/** Gets a single event based on id */
|
|
71
|
+
getEvent(id) {
|
|
72
|
+
return this.events.get(id);
|
|
73
|
+
}
|
|
74
|
+
/** Checks if the database contains a replaceable event without touching it */
|
|
75
|
+
hasReplaceable(kind, pubkey, d) {
|
|
76
|
+
const events = this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
77
|
+
return !!events && events.length > 0;
|
|
78
|
+
}
|
|
79
|
+
/** Gets an array of replaceable events */
|
|
80
|
+
getReplaceable(kind, pubkey, d) {
|
|
81
|
+
return this.replaceable.get(getReplaceableUID(kind, pubkey, d));
|
|
82
|
+
}
|
|
83
|
+
/** Inserts an event into the database and notifies all subscriptions */
|
|
84
|
+
addEvent(event) {
|
|
85
|
+
const id = event.id;
|
|
86
|
+
const current = this.events.get(id);
|
|
87
|
+
if (current)
|
|
88
|
+
return current;
|
|
89
|
+
this.onBeforeInsert?.(event);
|
|
90
|
+
this.events.set(id, event);
|
|
91
|
+
this.getKindIndex(event.kind).add(event);
|
|
92
|
+
this.getAuthorsIndex(event.pubkey).add(event);
|
|
93
|
+
for (const tag of getIndexableTags(event)) {
|
|
94
|
+
if (this.tags.has(tag)) {
|
|
95
|
+
this.getTagIndex(tag).add(event);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// insert into time index
|
|
99
|
+
insertEventIntoDescendingList(this.created_at, event);
|
|
100
|
+
// insert into replaceable index
|
|
101
|
+
if (isReplaceable(event.kind)) {
|
|
102
|
+
const uid = getEventUID(event);
|
|
103
|
+
let array = this.replaceable.get(uid);
|
|
104
|
+
if (!this.replaceable.has(uid)) {
|
|
105
|
+
// add an empty array if there is no array
|
|
106
|
+
array = [];
|
|
107
|
+
this.replaceable.set(uid, array);
|
|
108
|
+
}
|
|
109
|
+
// insert the event into the sorted array
|
|
110
|
+
insertEventIntoDescendingList(array, event);
|
|
111
|
+
}
|
|
112
|
+
this.inserted.next(event);
|
|
113
|
+
return event;
|
|
114
|
+
}
|
|
115
|
+
/** Inserts and event into the database and notifies all subscriptions that the event has updated */
|
|
116
|
+
updateEvent(event) {
|
|
117
|
+
const inserted = this.addEvent(event);
|
|
118
|
+
this.updated.next(inserted);
|
|
119
|
+
return inserted;
|
|
120
|
+
}
|
|
121
|
+
/** Removes an event from the database and notifies all subscriptions */
|
|
122
|
+
removeEvent(eventOrId) {
|
|
123
|
+
let event = typeof eventOrId === "string" ? this.events.get(eventOrId) : eventOrId;
|
|
124
|
+
if (!event)
|
|
125
|
+
throw new Error("Missing event");
|
|
126
|
+
const id = event.id;
|
|
127
|
+
// only remove events that are known
|
|
128
|
+
if (!this.events.has(id))
|
|
129
|
+
return false;
|
|
130
|
+
this.getAuthorsIndex(event.pubkey).delete(event);
|
|
131
|
+
this.getKindIndex(event.kind).delete(event);
|
|
132
|
+
for (const tag of getIndexableTags(event)) {
|
|
133
|
+
if (this.tags.has(tag)) {
|
|
134
|
+
this.getTagIndex(tag).delete(event);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// remove from created_at index
|
|
138
|
+
const i = this.created_at.indexOf(event);
|
|
139
|
+
this.created_at.splice(i, 1);
|
|
140
|
+
this.events.delete(id);
|
|
141
|
+
// remove from replaceable index
|
|
142
|
+
if (isReplaceable(event.kind)) {
|
|
143
|
+
const uid = getEventUID(event);
|
|
144
|
+
const array = this.replaceable.get(uid);
|
|
145
|
+
if (array && array.includes(event)) {
|
|
146
|
+
const idx = array.indexOf(event);
|
|
147
|
+
array.splice(idx, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// remove any claims this event has
|
|
151
|
+
this.claims.delete(event);
|
|
152
|
+
// notify subscribers this event was removed
|
|
153
|
+
this.removed.next(event);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** Sets the claim on the event and touches it */
|
|
157
|
+
claimEvent(event, claim) {
|
|
158
|
+
if (!this.claims.has(event)) {
|
|
159
|
+
this.claims.set(event, claim);
|
|
160
|
+
}
|
|
161
|
+
// always touch event
|
|
162
|
+
this.touch(event);
|
|
163
|
+
}
|
|
164
|
+
/** Checks if an event is claimed by anything */
|
|
165
|
+
isClaimed(event) {
|
|
166
|
+
return this.claims.has(event);
|
|
167
|
+
}
|
|
168
|
+
/** Removes a claim from an event */
|
|
169
|
+
removeClaim(event, claim) {
|
|
170
|
+
const current = this.claims.get(event);
|
|
171
|
+
if (current === claim)
|
|
172
|
+
this.claims.delete(event);
|
|
173
|
+
}
|
|
174
|
+
/** Removes all claims on an event */
|
|
175
|
+
clearClaim(event) {
|
|
176
|
+
this.claims.delete(event);
|
|
177
|
+
}
|
|
178
|
+
*iterateAuthors(authors) {
|
|
179
|
+
for (const author of authors) {
|
|
180
|
+
const events = this.authors.get(author);
|
|
181
|
+
if (events) {
|
|
182
|
+
for (const event of events)
|
|
183
|
+
yield event;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
*iterateTag(tag, values) {
|
|
188
|
+
for (const value of values) {
|
|
189
|
+
const events = this.getTagIndex(tag + ":" + value);
|
|
190
|
+
if (events) {
|
|
191
|
+
for (const event of events)
|
|
192
|
+
yield event;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
*iterateKinds(kinds) {
|
|
197
|
+
for (const kind of kinds) {
|
|
198
|
+
const events = this.kinds.get(kind);
|
|
199
|
+
if (events) {
|
|
200
|
+
for (const event of events)
|
|
201
|
+
yield event;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
*iterateTime(since, until) {
|
|
206
|
+
let untilIndex = 0;
|
|
207
|
+
let sinceIndex = this.created_at.length - 1;
|
|
208
|
+
let start = until
|
|
209
|
+
? binarySearch(this.created_at, (mid) => {
|
|
210
|
+
return mid.created_at - until;
|
|
211
|
+
})
|
|
212
|
+
: undefined;
|
|
213
|
+
if (start)
|
|
214
|
+
untilIndex = start[0];
|
|
215
|
+
const end = since
|
|
216
|
+
? binarySearch(this.created_at, (mid) => {
|
|
217
|
+
return mid.created_at - since;
|
|
218
|
+
})
|
|
219
|
+
: undefined;
|
|
220
|
+
if (end)
|
|
221
|
+
sinceIndex = end[0];
|
|
222
|
+
for (let i = untilIndex; i < sinceIndex; i++) {
|
|
223
|
+
yield this.created_at[i];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
*iterateIds(ids) {
|
|
227
|
+
for (const id of ids) {
|
|
228
|
+
if (this.events.has(id))
|
|
229
|
+
yield this.events.get(id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Returns all events that match the filter */
|
|
233
|
+
getEventsForFilter(filter) {
|
|
234
|
+
// search is not supported, return an empty set
|
|
235
|
+
if (filter.search)
|
|
236
|
+
return new Set();
|
|
237
|
+
let first = true;
|
|
238
|
+
let events = new Set();
|
|
239
|
+
const and = (iterable) => {
|
|
240
|
+
const set = iterable instanceof Set ? iterable : new Set(iterable);
|
|
241
|
+
if (first) {
|
|
242
|
+
events = set;
|
|
243
|
+
first = false;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const event of events) {
|
|
247
|
+
if (!set.has(event))
|
|
248
|
+
events.delete(event);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return events;
|
|
252
|
+
};
|
|
253
|
+
if (filter.ids)
|
|
254
|
+
and(this.iterateIds(filter.ids));
|
|
255
|
+
let time = null;
|
|
256
|
+
// query for time first if since is set
|
|
257
|
+
if (filter.since !== undefined) {
|
|
258
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
259
|
+
and(time);
|
|
260
|
+
}
|
|
261
|
+
for (const t of INDEXABLE_TAGS) {
|
|
262
|
+
const key = `#${t}`;
|
|
263
|
+
const values = filter[key];
|
|
264
|
+
if (values?.length)
|
|
265
|
+
and(this.iterateTag(t, values));
|
|
266
|
+
}
|
|
267
|
+
if (filter.authors)
|
|
268
|
+
and(this.iterateAuthors(filter.authors));
|
|
269
|
+
if (filter.kinds)
|
|
270
|
+
and(this.iterateKinds(filter.kinds));
|
|
271
|
+
// query for time last if only until is set
|
|
272
|
+
if (filter.since === undefined && filter.until !== undefined) {
|
|
273
|
+
time = Array.from(this.iterateTime(filter.since, filter.until));
|
|
274
|
+
and(time);
|
|
275
|
+
}
|
|
276
|
+
// if the filter queried on time and has a limit. truncate the events now
|
|
277
|
+
if (filter.limit && time) {
|
|
278
|
+
const limited = new Set();
|
|
279
|
+
for (const event of time) {
|
|
280
|
+
if (limited.size >= filter.limit)
|
|
281
|
+
break;
|
|
282
|
+
if (events.has(event))
|
|
283
|
+
limited.add(event);
|
|
284
|
+
}
|
|
285
|
+
return limited;
|
|
286
|
+
}
|
|
287
|
+
return events;
|
|
288
|
+
}
|
|
289
|
+
getEventsForFilters(filters) {
|
|
290
|
+
if (filters.length === 0)
|
|
291
|
+
throw new Error("No Filters");
|
|
292
|
+
let events = new Set();
|
|
293
|
+
for (const filter of filters) {
|
|
294
|
+
const filtered = this.getEventsForFilter(filter);
|
|
295
|
+
for (const event of filtered)
|
|
296
|
+
events.add(event);
|
|
297
|
+
}
|
|
298
|
+
return events;
|
|
299
|
+
}
|
|
300
|
+
/** Remove the oldest events that are not claimed */
|
|
301
|
+
prune(limit = 1000) {
|
|
302
|
+
let removed = 0;
|
|
303
|
+
let cursor = this.events.first;
|
|
304
|
+
while (cursor) {
|
|
305
|
+
const event = cursor.value;
|
|
306
|
+
if (!this.isClaimed(event)) {
|
|
307
|
+
this.removeEvent(event);
|
|
308
|
+
removed++;
|
|
309
|
+
if (removed >= limit)
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
cursor = cursor.next;
|
|
313
|
+
}
|
|
314
|
+
return removed;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
});
|
|
@@ -23,7 +23,7 @@ describe("exports", () => {
|
|
|
23
23
|
"FromCacheSymbol",
|
|
24
24
|
"GROUPS_LIST_KIND",
|
|
25
25
|
"GROUP_MESSAGE_KIND",
|
|
26
|
-
"
|
|
26
|
+
"GiftWrapEventSymbol",
|
|
27
27
|
"GiftWrapSealSymbol",
|
|
28
28
|
"GroupsHiddenSymbol",
|
|
29
29
|
"GroupsPublicSymbol",
|
|
@@ -124,7 +124,7 @@ describe("exports", () => {
|
|
|
124
124
|
"getExternalPointerFromTag",
|
|
125
125
|
"getFileMetadata",
|
|
126
126
|
"getFileMetadataFromImetaTag",
|
|
127
|
-
"
|
|
127
|
+
"getGiftWrapEvent",
|
|
128
128
|
"getGiftWrapSeal",
|
|
129
129
|
"getGroupPointerFromGroupTag",
|
|
130
130
|
"getGroupPointerFromHTag",
|
|
@@ -175,7 +175,6 @@ describe("exports", () => {
|
|
|
175
175
|
"getReplaceableIdentifier",
|
|
176
176
|
"getReplaceableUID",
|
|
177
177
|
"getReported",
|
|
178
|
-
"getSealRumor",
|
|
179
178
|
"getSeenRelays",
|
|
180
179
|
"getSha256FromURL",
|
|
181
180
|
"getSharedAddressPointer",
|
|
@@ -231,7 +230,6 @@ describe("exports", () => {
|
|
|
231
230
|
"isReplaceable",
|
|
232
231
|
"isSafeRelayURL",
|
|
233
232
|
"isSameURL",
|
|
234
|
-
"isSealLocked",
|
|
235
233
|
"isSha256",
|
|
236
234
|
"isStreamURL",
|
|
237
235
|
"isTTag",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
|
+
import { normalizeToPubkey, normalizeToSecretKey } from "../nip-19.js";
|
|
4
|
+
describe("normalizeToPubkey", () => {
|
|
5
|
+
it("should get pubkey from npub", () => {
|
|
6
|
+
expect(normalizeToPubkey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
7
|
+
});
|
|
8
|
+
it("should get pubkey from nprofile", () => {
|
|
9
|
+
expect(normalizeToPubkey("nprofile1qyw8wumn8ghj7umpw3jkcmrfw3jju6r6wfjrzdpe9e3k7mf0qyf8wumn8ghj7mn0wd68yat99e3k7mf0qqszv6q4uryjzr06xfxxew34wwc5hmjfmfpqn229d72gfegsdn2q3fg5g7lja")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
10
|
+
});
|
|
11
|
+
it("should return hex pubkey", () => {
|
|
12
|
+
expect(normalizeToPubkey("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5")).toEqual("266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5");
|
|
13
|
+
});
|
|
14
|
+
it("should throw on invalid hex pubkey", () => {
|
|
15
|
+
expect(() => {
|
|
16
|
+
normalizeToPubkey("5028372");
|
|
17
|
+
}).toThrow();
|
|
18
|
+
});
|
|
19
|
+
it("should throw on invalid string", () => {
|
|
20
|
+
expect(() => {
|
|
21
|
+
normalizeToPubkey("testing");
|
|
22
|
+
}).toThrow();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("normalizeToSecretKey", () => {
|
|
26
|
+
it("should get secret key from nsec", () => {
|
|
27
|
+
expect(bytesToHex(normalizeToSecretKey("nsec1xe7znq745x5n68566l32ru72aajz3pk2cys9lnf3tuexvkw0dldsj8v2lm"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
|
|
28
|
+
});
|
|
29
|
+
it("should get secret key from raw hex", () => {
|
|
30
|
+
expect(bytesToHex(normalizeToSecretKey("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb"))).toEqual("367c2983d5a1a93d1e9ad7e2a1f3caef642886cac1205fcd315f326659cf6fdb");
|
|
31
|
+
});
|
|
32
|
+
it("should throw on invalid hex key", () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
normalizeToSecretKey("209573290");
|
|
35
|
+
}).toThrow();
|
|
36
|
+
});
|
|
37
|
+
it("should throw on npub", () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
normalizeToSecretKey("npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr");
|
|
40
|
+
}).toThrow();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { HiddenContentSigner } from "./hidden-content.js";
|
|
3
|
+
/** Returns the decrypted content of a direct message */
|
|
4
|
+
export declare function decryptDirectMessage(message: NostrEvent, signer: HiddenContentSigner): Promise<string>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { getHiddenContent, unlockHiddenContent } from "./hidden-content.js";
|
|
2
|
+
/** Returns the decrypted content of a direct message */
|
|
3
|
+
export async function decryptDirectMessage(message, signer) {
|
|
4
|
+
return getHiddenContent(message) || (await unlockHiddenContent(message, signer));
|
|
5
|
+
}
|
|
@@ -10,18 +10,26 @@ export interface EncryptedContentSigner {
|
|
|
10
10
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export declare function setEncryptedContentEncryptionMethod(kind: number, method: "nip04" | "nip44"): number;
|
|
17
|
-
/** Returns either nip04 or nip44 encryption methods depending on event kind */
|
|
18
|
-
export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner): {
|
|
13
|
+
export type EncryptionMethod = "nip04" | "nip44";
|
|
14
|
+
/** A pair of encryption methods for encrypting and decrypting event content */
|
|
15
|
+
export interface EncryptionMethods {
|
|
19
16
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
20
17
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
}
|
|
19
|
+
/** Various event kinds that can have encrypted content and which encryption method they use */
|
|
20
|
+
export declare const EventContentEncryptionMethod: Record<number, EncryptionMethod>;
|
|
21
|
+
/** Sets the encryption method that is used for the contents of a specific event kind */
|
|
22
|
+
export declare function setEncryptedContentEncryptionMethod(kind: number, method: EncryptionMethod): number;
|
|
23
|
+
/**
|
|
24
|
+
* Returns either nip04 or nip44 encryption methods depending on event kind
|
|
25
|
+
* @param kind The event kind to get the encryption method for
|
|
26
|
+
* @param signer The signer to use to get the encryption methods
|
|
27
|
+
* @param override The encryption method to use instead of the default
|
|
28
|
+
* @throws If the event kind does not support encrypted content
|
|
29
|
+
* @throws If the signer does not support the encryption method
|
|
30
|
+
* @returns The encryption methods for the event kind
|
|
31
|
+
*/
|
|
32
|
+
export declare function getEncryptedContentEncryptionMethods(kind: number, signer: EncryptedContentSigner, override?: EncryptionMethod): EncryptionMethods;
|
|
25
33
|
/** Checks if an event can have encrypted content */
|
|
26
34
|
export declare function canHaveEncryptedContent(kind: number): boolean;
|
|
27
35
|
/** Checks if an event has encrypted content */
|
|
@@ -13,9 +13,17 @@ export function setEncryptedContentEncryptionMethod(kind, method) {
|
|
|
13
13
|
EventContentEncryptionMethod[kind] = method;
|
|
14
14
|
return kind;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Returns either nip04 or nip44 encryption methods depending on event kind
|
|
18
|
+
* @param kind The event kind to get the encryption method for
|
|
19
|
+
* @param signer The signer to use to get the encryption methods
|
|
20
|
+
* @param override The encryption method to use instead of the default
|
|
21
|
+
* @throws If the event kind does not support encrypted content
|
|
22
|
+
* @throws If the signer does not support the encryption method
|
|
23
|
+
* @returns The encryption methods for the event kind
|
|
24
|
+
*/
|
|
25
|
+
export function getEncryptedContentEncryptionMethods(kind, signer, override) {
|
|
26
|
+
const method = override ?? EventContentEncryptionMethod[kind];
|
|
19
27
|
if (!method)
|
|
20
28
|
throw new Error(`Event kind ${kind} does not support encrypted content`);
|
|
21
29
|
const encryption = signer[method];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|