applesauce-core 2.3.0 → 3.0.0
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/event-set.js +5 -3
- package/dist/event-store/event-store.d.ts +54 -24
- package/dist/event-store/event-store.js +139 -47
- package/dist/event-store/interface.d.ts +36 -18
- package/dist/helpers/calendar-event.d.ts +36 -0
- package/dist/helpers/calendar-event.js +110 -0
- package/dist/helpers/calendar-rsvp.d.ts +15 -0
- package/dist/helpers/calendar-rsvp.js +38 -0
- package/dist/helpers/calendar.d.ts +6 -0
- package/dist/helpers/calendar.js +11 -0
- package/dist/helpers/encrypted-content.d.ts +18 -10
- package/dist/helpers/encrypted-content.js +11 -3
- package/dist/helpers/event-cache.d.ts +15 -0
- package/dist/helpers/event-cache.js +32 -0
- package/dist/helpers/event.d.ts +1 -1
- package/dist/helpers/event.js +1 -1
- package/dist/helpers/expiration.js +1 -2
- 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/highlight.d.ts +45 -0
- package/dist/helpers/highlight.js +76 -0
- package/dist/helpers/index.d.ts +8 -0
- package/dist/helpers/index.js +8 -0
- package/dist/helpers/pointers.js +1 -1
- package/dist/helpers/poll.d.ts +46 -0
- package/dist/helpers/poll.js +78 -0
- package/dist/helpers/stream-chat.d.ts +4 -0
- package/dist/helpers/stream-chat.js +9 -0
- package/dist/helpers/stream.d.ts +31 -0
- package/dist/helpers/stream.js +81 -0
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +1 -0
- package/dist/models/blossom.d.ts +2 -1
- package/dist/models/blossom.js +4 -2
- package/dist/models/calendar.d.ts +6 -0
- package/dist/models/calendar.js +15 -0
- package/dist/models/common.d.ts +14 -10
- package/dist/models/common.js +47 -76
- package/dist/models/contacts.d.ts +1 -1
- package/dist/models/contacts.js +4 -2
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/mailboxes.d.ts +2 -1
- package/dist/models/mailboxes.js +4 -2
- package/dist/models/mutes.d.ts +3 -2
- package/dist/models/mutes.js +4 -2
- package/dist/models/profile.d.ts +2 -1
- package/dist/models/profile.js +4 -2
- package/package.json +1 -1
- package/dist/event-store/common.d.ts +0 -1
- package/dist/event-store/common.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
|
|
2
2
|
import { Subject } from "rxjs";
|
|
3
3
|
import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
|
|
4
|
-
import { createReplaceableAddress,
|
|
4
|
+
import { createReplaceableAddress, isReplaceable } from "../helpers/event.js";
|
|
5
5
|
import { LRU } from "../helpers/lru.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
/**
|
|
@@ -92,7 +92,8 @@ export class EventSet {
|
|
|
92
92
|
insertEventIntoDescendingList(this.created_at, event);
|
|
93
93
|
// Insert into replaceable index
|
|
94
94
|
if (isReplaceable(event.kind)) {
|
|
95
|
-
const
|
|
95
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
96
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
96
97
|
let array = this.replaceable.get(address);
|
|
97
98
|
if (!this.replaceable.has(address)) {
|
|
98
99
|
// add an empty array if there is no array
|
|
@@ -135,7 +136,8 @@ export class EventSet {
|
|
|
135
136
|
this.events.delete(id);
|
|
136
137
|
// remove from replaceable index
|
|
137
138
|
if (isReplaceable(event.kind)) {
|
|
138
|
-
const
|
|
139
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
140
|
+
const address = createReplaceableAddress(event.kind, event.pubkey, identifier);
|
|
139
141
|
const array = this.replaceable.get(address);
|
|
140
142
|
if (array && array.includes(event)) {
|
|
141
143
|
const idx = array.indexOf(event);
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
|
+
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
|
|
4
|
+
import { AddressPointerWithoutD } from "../helpers/pointers.js";
|
|
3
5
|
import { EventSet } from "./event-set.js";
|
|
4
6
|
import { IEventStore, ModelConstructor } from "./interface.js";
|
|
5
|
-
|
|
7
|
+
/** An extended {@link EventSet} that handles replaceable events, delets, and models */
|
|
6
8
|
export declare class EventStore implements IEventStore {
|
|
7
9
|
database: EventSet;
|
|
8
10
|
/** Enable this to keep old versions of replaceable events */
|
|
9
11
|
keepOldVersions: boolean;
|
|
12
|
+
/** Enable this to keep expired events */
|
|
13
|
+
keepExpired: boolean;
|
|
10
14
|
/**
|
|
11
15
|
* A method used to verify new events before added them
|
|
12
16
|
* @returns true if the event is valid, false if it should be ignored
|
|
@@ -18,10 +22,33 @@ export declare class EventStore implements IEventStore {
|
|
|
18
22
|
update$: Observable<NostrEvent>;
|
|
19
23
|
/** A stream of events that have been removed */
|
|
20
24
|
remove$: Observable<NostrEvent>;
|
|
25
|
+
/**
|
|
26
|
+
* A method that will be called when an event isn't found in the store
|
|
27
|
+
* @experimental
|
|
28
|
+
*/
|
|
29
|
+
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* A method that will be called when a replaceable event isn't found in the store
|
|
32
|
+
* @experimental
|
|
33
|
+
*/
|
|
34
|
+
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
35
|
+
/**
|
|
36
|
+
* A method that will be called when an addressable event isn't found in the store
|
|
37
|
+
* @experimental
|
|
38
|
+
*/
|
|
39
|
+
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
21
40
|
constructor();
|
|
22
41
|
protected deletedIds: Set<string>;
|
|
23
42
|
protected deletedCoords: Map<string, number>;
|
|
24
43
|
protected checkDeleted(event: string | NostrEvent): boolean;
|
|
44
|
+
protected expirations: Map<string, number>;
|
|
45
|
+
/** Adds an event to the expiration map */
|
|
46
|
+
protected addExpiration(event: NostrEvent): void;
|
|
47
|
+
protected expirationTimeout: number | null;
|
|
48
|
+
protected nextExpirationCheck: number | null;
|
|
49
|
+
protected handleExpiringEvent(event: NostrEvent): void;
|
|
50
|
+
/** Remove expired events from the store */
|
|
51
|
+
protected pruneExpired(): void;
|
|
25
52
|
protected handleDeleteEvent(deleteEvent: NostrEvent): void;
|
|
26
53
|
/** Copies important metadata from and identical event to another */
|
|
27
54
|
static mergeDuplicateEvent(source: NostrEvent, dest: NostrEvent): void;
|
|
@@ -75,36 +102,39 @@ export declare class EventStore implements IEventStore {
|
|
|
75
102
|
/** Creates an observable that emits when event is updated */
|
|
76
103
|
updated(event: string | NostrEvent): Observable<NostrEvent>;
|
|
77
104
|
/** Creates a {@link EventModel} */
|
|
78
|
-
event(
|
|
105
|
+
event(pointer: string | EventPointer): Observable<NostrEvent | undefined>;
|
|
79
106
|
/** Creates a {@link ReplaceableModel} */
|
|
107
|
+
replaceable(pointer: AddressPointer | AddressPointerWithoutD): Observable<NostrEvent | undefined>;
|
|
80
108
|
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
|
|
109
|
+
/** Subscribe to an addressable event by pointer */
|
|
110
|
+
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
|
|
81
111
|
/** Creates a {@link TimelineModel} */
|
|
82
112
|
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/** Creates a {@link ProfileModel} */
|
|
92
|
-
profile(pubkey: string): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
|
|
93
|
-
/** Creates a {@link ContactsModel} */
|
|
94
|
-
contacts(pubkey: string): Observable<import("nostr-tools/nip19").ProfilePointer[]>;
|
|
95
|
-
/** Creates a {@link MuteModel} */
|
|
96
|
-
mutes(pubkey: string): Observable<import("../helpers/mutes.js").Mutes | undefined>;
|
|
97
|
-
/** Creates a {@link ReactionsModel} */
|
|
98
|
-
reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
|
|
99
|
-
/** Creates a {@link MailboxesModel} */
|
|
100
|
-
mailboxes(pubkey: string): Observable<{
|
|
113
|
+
/** Subscribe to a users profile */
|
|
114
|
+
profile(user: string | ProfilePointer): Observable<import("../helpers/profile.js").ProfileContent | undefined>;
|
|
115
|
+
/** Subscribe to a users contacts */
|
|
116
|
+
contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
|
|
117
|
+
/** Subscribe to a users mutes */
|
|
118
|
+
mutes(user: string | ProfilePointer): Observable<import("../helpers/mutes.js").Mutes | undefined>;
|
|
119
|
+
/** Subscribe to a users NIP-65 mailboxes */
|
|
120
|
+
mailboxes(user: string | ProfilePointer): Observable<{
|
|
101
121
|
inboxes: string[];
|
|
102
122
|
outboxes: string[];
|
|
103
123
|
} | undefined>;
|
|
104
|
-
/**
|
|
105
|
-
blossomServers(
|
|
106
|
-
/**
|
|
124
|
+
/** Subscribe to a users blossom servers */
|
|
125
|
+
blossomServers(user: string | ProfilePointer): Observable<URL[]>;
|
|
126
|
+
/** Subscribe to an event's reactions */
|
|
127
|
+
reactions(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
|
|
128
|
+
/** Subscribe to a thread */
|
|
107
129
|
thread(root: string | EventPointer | AddressPointer): Observable<import("../models/thread.js").Thread>;
|
|
108
|
-
/**
|
|
130
|
+
/** Subscribe to a event's comments */
|
|
109
131
|
comments(event: NostrEvent): Observable<import("nostr-tools").Event[]>;
|
|
132
|
+
/** @deprecated use multiple {@link EventModel} instead */
|
|
133
|
+
events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
|
|
134
|
+
/** @deprecated use multiple {@link ReplaceableModel} instead */
|
|
135
|
+
replaceableSet(pointers: {
|
|
136
|
+
kind: number;
|
|
137
|
+
pubkey: string;
|
|
138
|
+
identifier?: string;
|
|
139
|
+
}[]): Observable<Record<string, NostrEvent | undefined>>;
|
|
110
140
|
}
|
|
@@ -3,23 +3,28 @@ import { isAddressableKind } from "nostr-tools/kinds";
|
|
|
3
3
|
import { EMPTY, filter, finalize, from, merge, mergeMap, ReplaySubject, share, take, timer } from "rxjs";
|
|
4
4
|
import hash_sum from "hash-sum";
|
|
5
5
|
import { getDeleteCoordinates, getDeleteIds } from "../helpers/delete.js";
|
|
6
|
-
import { EventStoreSymbol, FromCacheSymbol,
|
|
6
|
+
import { createReplaceableAddress, EventStoreSymbol, FromCacheSymbol, isReplaceable } from "../helpers/event.js";
|
|
7
|
+
import { getExpirationTimestamp } from "../helpers/expiration.js";
|
|
7
8
|
import { matchFilters } from "../helpers/filter.js";
|
|
8
9
|
import { parseCoordinate } from "../helpers/pointers.js";
|
|
9
10
|
import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
|
|
11
|
+
import { unixNow } from "../helpers/time.js";
|
|
12
|
+
import { UserBlossomServersModel } from "../models/blossom.js";
|
|
10
13
|
import { EventModel, EventsModel, ReplaceableModel, ReplaceableSetModel, TimelineModel } from "../models/common.js";
|
|
11
|
-
import { EventSet } from "./event-set.js";
|
|
12
|
-
import { ProfileModel } from "../models/profile.js";
|
|
13
14
|
import { ContactsModel } from "../models/contacts.js";
|
|
15
|
+
import { CommentsModel, ThreadModel } from "../models/index.js";
|
|
16
|
+
import { MailboxesModel } from "../models/mailboxes.js";
|
|
14
17
|
import { MuteModel } from "../models/mutes.js";
|
|
18
|
+
import { ProfileModel } from "../models/profile.js";
|
|
15
19
|
import { ReactionsModel } from "../models/reactions.js";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
import { CommentsModel, ThreadModel } from "../models/index.js";
|
|
20
|
+
import { EventSet } from "./event-set.js";
|
|
21
|
+
/** An extended {@link EventSet} that handles replaceable events, delets, and models */
|
|
19
22
|
export class EventStore {
|
|
20
23
|
database;
|
|
21
24
|
/** Enable this to keep old versions of replaceable events */
|
|
22
25
|
keepOldVersions = false;
|
|
26
|
+
/** Enable this to keep expired events */
|
|
27
|
+
keepExpired = false;
|
|
23
28
|
/**
|
|
24
29
|
* A method used to verify new events before added them
|
|
25
30
|
* @returns true if the event is valid, false if it should be ignored
|
|
@@ -31,6 +36,21 @@ export class EventStore {
|
|
|
31
36
|
update$;
|
|
32
37
|
/** A stream of events that have been removed */
|
|
33
38
|
remove$;
|
|
39
|
+
/**
|
|
40
|
+
* A method that will be called when an event isn't found in the store
|
|
41
|
+
* @experimental
|
|
42
|
+
*/
|
|
43
|
+
eventLoader;
|
|
44
|
+
/**
|
|
45
|
+
* A method that will be called when a replaceable event isn't found in the store
|
|
46
|
+
* @experimental
|
|
47
|
+
*/
|
|
48
|
+
replaceableLoader;
|
|
49
|
+
/**
|
|
50
|
+
* A method that will be called when an addressable event isn't found in the store
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
53
|
+
addressableLoader;
|
|
34
54
|
constructor() {
|
|
35
55
|
this.database = new EventSet();
|
|
36
56
|
// verify events before they are added to the database
|
|
@@ -63,12 +83,53 @@ export class EventStore {
|
|
|
63
83
|
if (this.deletedIds.has(event.id))
|
|
64
84
|
return true;
|
|
65
85
|
if (isAddressableKind(event.kind)) {
|
|
66
|
-
const
|
|
86
|
+
const identifier = event.tags.find((t) => t[0] === "d")?.[1];
|
|
87
|
+
const deleted = this.deletedCoords.get(createReplaceableAddress(event.kind, event.pubkey, identifier));
|
|
67
88
|
if (deleted)
|
|
68
89
|
return deleted > event.created_at;
|
|
69
90
|
}
|
|
70
|
-
return false;
|
|
71
91
|
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
expirations = new Map();
|
|
95
|
+
/** Adds an event to the expiration map */
|
|
96
|
+
addExpiration(event) {
|
|
97
|
+
const expiration = getExpirationTimestamp(event);
|
|
98
|
+
if (expiration && Number.isFinite(expiration))
|
|
99
|
+
this.expirations.set(event.id, expiration);
|
|
100
|
+
}
|
|
101
|
+
expirationTimeout = null;
|
|
102
|
+
nextExpirationCheck = null;
|
|
103
|
+
handleExpiringEvent(event) {
|
|
104
|
+
const expiration = getExpirationTimestamp(event);
|
|
105
|
+
if (!expiration)
|
|
106
|
+
return;
|
|
107
|
+
// Add event to expiration map
|
|
108
|
+
this.expirations.set(event.id, expiration);
|
|
109
|
+
// Exit if the next check is already less than the next expiration
|
|
110
|
+
if (this.expirationTimeout && this.nextExpirationCheck && this.nextExpirationCheck < expiration)
|
|
111
|
+
return;
|
|
112
|
+
// Set timeout to prune expired events
|
|
113
|
+
if (this.expirationTimeout)
|
|
114
|
+
clearTimeout(this.expirationTimeout);
|
|
115
|
+
const timeout = expiration - unixNow();
|
|
116
|
+
this.expirationTimeout = setTimeout(this.pruneExpired.bind(this), timeout * 1000 + 10);
|
|
117
|
+
this.nextExpirationCheck = expiration;
|
|
118
|
+
}
|
|
119
|
+
/** Remove expired events from the store */
|
|
120
|
+
pruneExpired() {
|
|
121
|
+
const now = unixNow();
|
|
122
|
+
for (const [id, expiration] of this.expirations) {
|
|
123
|
+
if (expiration <= now) {
|
|
124
|
+
this.expirations.delete(id);
|
|
125
|
+
this.remove(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Cleanup timers
|
|
129
|
+
if (this.expirationTimeout)
|
|
130
|
+
clearTimeout(this.expirationTimeout);
|
|
131
|
+
this.nextExpirationCheck = null;
|
|
132
|
+
this.expirationTimeout = null;
|
|
72
133
|
}
|
|
73
134
|
// handling delete events
|
|
74
135
|
handleDeleteEvent(deleteEvent) {
|
|
@@ -117,11 +178,15 @@ export class EventStore {
|
|
|
117
178
|
// Ignore if the event was deleted
|
|
118
179
|
if (this.checkDeleted(event))
|
|
119
180
|
return event;
|
|
181
|
+
// Reject expired events if keepExpired is false
|
|
182
|
+
const expiration = getExpirationTimestamp(event);
|
|
183
|
+
if (this.keepExpired === false && expiration && expiration <= unixNow())
|
|
184
|
+
return null;
|
|
120
185
|
// Get the replaceable identifier
|
|
121
|
-
const
|
|
186
|
+
const identifier = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
|
|
122
187
|
// Don't insert the event if there is already a newer version
|
|
123
188
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
124
|
-
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey,
|
|
189
|
+
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
|
|
125
190
|
// If there is already a newer version, copy cached symbols and return existing event
|
|
126
191
|
if (existing && existing.length > 0 && existing[0].created_at >= event.created_at) {
|
|
127
192
|
EventStore.mergeDuplicateEvent(event, existing[0]);
|
|
@@ -149,7 +214,7 @@ export class EventStore {
|
|
|
149
214
|
addSeenRelay(inserted, fromRelay);
|
|
150
215
|
// remove all old version of the replaceable event
|
|
151
216
|
if (!this.keepOldVersions && isReplaceable(event.kind)) {
|
|
152
|
-
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey,
|
|
217
|
+
const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, identifier);
|
|
153
218
|
if (existing) {
|
|
154
219
|
const older = Array.from(existing).filter((e) => e.created_at < event.created_at);
|
|
155
220
|
for (const old of older)
|
|
@@ -160,6 +225,9 @@ export class EventStore {
|
|
|
160
225
|
return existing[0];
|
|
161
226
|
}
|
|
162
227
|
}
|
|
228
|
+
// Add event to expiration map
|
|
229
|
+
if (this.keepExpired === false && expiration)
|
|
230
|
+
this.handleExpiringEvent(inserted);
|
|
163
231
|
return inserted;
|
|
164
232
|
}
|
|
165
233
|
/** Removes an event from the database and updates subscriptions */
|
|
@@ -284,55 +352,79 @@ export class EventStore {
|
|
|
284
352
|
}
|
|
285
353
|
// Helper methods for creating models
|
|
286
354
|
/** Creates a {@link EventModel} */
|
|
287
|
-
event(
|
|
288
|
-
|
|
355
|
+
event(pointer) {
|
|
356
|
+
if (typeof pointer === "string")
|
|
357
|
+
pointer = { id: pointer };
|
|
358
|
+
return this.model(EventModel, pointer);
|
|
359
|
+
}
|
|
360
|
+
replaceable(...args) {
|
|
361
|
+
let pointer;
|
|
362
|
+
// Parse arguments
|
|
363
|
+
if (args.length === 1) {
|
|
364
|
+
pointer = args[0];
|
|
365
|
+
}
|
|
366
|
+
else if (args.length === 3 || args.length === 2) {
|
|
367
|
+
let [kind, pubkey, identifier] = args;
|
|
368
|
+
pointer = { kind, pubkey, identifier };
|
|
369
|
+
}
|
|
370
|
+
if (!pointer)
|
|
371
|
+
throw new Error("Invalid arguments, expected address pointer or kind, pubkey, identifier");
|
|
372
|
+
return this.model(ReplaceableModel, pointer);
|
|
289
373
|
}
|
|
290
|
-
/**
|
|
291
|
-
|
|
292
|
-
return this.model(ReplaceableModel,
|
|
374
|
+
/** Subscribe to an addressable event by pointer */
|
|
375
|
+
addressable(pointer) {
|
|
376
|
+
return this.model(ReplaceableModel, pointer);
|
|
293
377
|
}
|
|
294
378
|
/** Creates a {@link TimelineModel} */
|
|
295
379
|
timeline(filters, includeOldVersion = false) {
|
|
296
380
|
return this.model(TimelineModel, filters, includeOldVersion);
|
|
297
381
|
}
|
|
298
|
-
/**
|
|
299
|
-
|
|
300
|
-
return this.model(
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return this.model(
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
382
|
+
/** Subscribe to a users profile */
|
|
383
|
+
profile(user) {
|
|
384
|
+
return this.model(ProfileModel, user);
|
|
385
|
+
}
|
|
386
|
+
/** Subscribe to a users contacts */
|
|
387
|
+
contacts(user) {
|
|
388
|
+
if (typeof user === "string")
|
|
389
|
+
user = { pubkey: user };
|
|
390
|
+
return this.model(ContactsModel, user);
|
|
391
|
+
}
|
|
392
|
+
/** Subscribe to a users mutes */
|
|
393
|
+
mutes(user) {
|
|
394
|
+
if (typeof user === "string")
|
|
395
|
+
user = { pubkey: user };
|
|
396
|
+
return this.model(MuteModel, user);
|
|
397
|
+
}
|
|
398
|
+
/** Subscribe to a users NIP-65 mailboxes */
|
|
399
|
+
mailboxes(user) {
|
|
400
|
+
if (typeof user === "string")
|
|
401
|
+
user = { pubkey: user };
|
|
402
|
+
return this.model(MailboxesModel, user);
|
|
403
|
+
}
|
|
404
|
+
/** Subscribe to a users blossom servers */
|
|
405
|
+
blossomServers(user) {
|
|
406
|
+
if (typeof user === "string")
|
|
407
|
+
user = { pubkey: user };
|
|
408
|
+
return this.model(UserBlossomServersModel, user);
|
|
409
|
+
}
|
|
410
|
+
/** Subscribe to an event's reactions */
|
|
319
411
|
reactions(event) {
|
|
320
412
|
return this.model(ReactionsModel, event);
|
|
321
413
|
}
|
|
322
|
-
/**
|
|
323
|
-
mailboxes(pubkey) {
|
|
324
|
-
return this.model(MailboxesModel, pubkey);
|
|
325
|
-
}
|
|
326
|
-
/** Creates a {@link UserBlossomServersModel} */
|
|
327
|
-
blossomServers(pubkey) {
|
|
328
|
-
return this.model(UserBlossomServersModel, pubkey);
|
|
329
|
-
}
|
|
330
|
-
/** Creates a {@link ThreadModel} */
|
|
414
|
+
/** Subscribe to a thread */
|
|
331
415
|
thread(root) {
|
|
332
416
|
return this.model(ThreadModel, root);
|
|
333
417
|
}
|
|
334
|
-
/**
|
|
418
|
+
/** Subscribe to a event's comments */
|
|
335
419
|
comments(event) {
|
|
336
420
|
return this.model(CommentsModel, event);
|
|
337
421
|
}
|
|
422
|
+
/** @deprecated use multiple {@link EventModel} instead */
|
|
423
|
+
events(ids) {
|
|
424
|
+
return this.model(EventsModel, ids);
|
|
425
|
+
}
|
|
426
|
+
/** @deprecated use multiple {@link ReplaceableModel} instead */
|
|
427
|
+
replaceableSet(pointers) {
|
|
428
|
+
return this.model(ReplaceableSetModel, pointers);
|
|
429
|
+
}
|
|
338
430
|
}
|
|
@@ -5,6 +5,7 @@ import { LRU } from "../helpers/lru.js";
|
|
|
5
5
|
import { Mutes } from "../helpers/mutes.js";
|
|
6
6
|
import { ProfileContent } from "../helpers/profile.js";
|
|
7
7
|
import { Thread } from "../models/thread.js";
|
|
8
|
+
import { AddressPointerWithoutD } from "../helpers/pointers.js";
|
|
8
9
|
/** The read interface for an event store */
|
|
9
10
|
export interface IEventStoreRead {
|
|
10
11
|
/** Check if the event store has an event with id */
|
|
@@ -51,27 +52,26 @@ export interface IEventClaims {
|
|
|
51
52
|
/** Removes all claims on an event */
|
|
52
53
|
clearClaim(event: NostrEvent): void;
|
|
53
54
|
}
|
|
55
|
+
/** An event store that can be subscribed to */
|
|
56
|
+
export interface IEventStoreSubscriptions {
|
|
57
|
+
/** Susbscribe to an event by id */
|
|
58
|
+
event(id: string | EventPointer): Observable<NostrEvent | undefined>;
|
|
59
|
+
/** Subscribe to a replaceable event by pointer */
|
|
60
|
+
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
|
|
61
|
+
/** Subscribe to an addressable event by pointer */
|
|
62
|
+
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
|
|
63
|
+
/** Subscribe to a batch of events that match the filters */
|
|
64
|
+
filter(filters: Filter | Filter[]): Observable<NostrEvent[]>;
|
|
65
|
+
}
|
|
54
66
|
/** Methods for creating common models */
|
|
55
67
|
export interface IEventStoreModels {
|
|
68
|
+
model<T extends unknown, Args extends Array<any>>(constructor: ModelConstructor<T, Args>, ...args: Args): Observable<T>;
|
|
56
69
|
event(id: string): Observable<NostrEvent | undefined>;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
replaceableSet(pointers: {
|
|
60
|
-
kind: number;
|
|
61
|
-
pubkey: string;
|
|
62
|
-
identifier?: string;
|
|
63
|
-
}[]): Observable<Record<string, NostrEvent>>;
|
|
70
|
+
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
|
|
71
|
+
addressable(pointer: AddressPointer): Observable<NostrEvent | undefined>;
|
|
64
72
|
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
mutes(pubkey: string): Observable<Mutes | undefined>;
|
|
68
|
-
reactions(event: NostrEvent): Observable<NostrEvent[]>;
|
|
69
|
-
mailboxes(pubkey: string): Observable<{
|
|
70
|
-
inboxes: string[];
|
|
71
|
-
outboxes: string[];
|
|
72
|
-
} | undefined>;
|
|
73
|
-
blossomServers(pubkey: string): Observable<URL[]>;
|
|
74
|
-
thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
|
|
73
|
+
events(ids: string[]): Observable<Record<string, NostrEvent | undefined>>;
|
|
74
|
+
replaceableSet(pointers: (AddressPointer | AddressPointerWithoutD)[]): Observable<Record<string, NostrEvent | undefined>>;
|
|
75
75
|
}
|
|
76
76
|
/** A computed view of an event set or event store */
|
|
77
77
|
export type Model<T extends unknown> = (events: IEventStore) => Observable<T>;
|
|
@@ -84,8 +84,26 @@ export interface IEventSet extends IEventStoreRead, IEventStoreStreams, IEventSt
|
|
|
84
84
|
events: LRU<NostrEvent>;
|
|
85
85
|
}
|
|
86
86
|
export interface IEventStore extends IEventStoreRead, IEventStoreStreams, IEventStoreActions, IEventStoreModels, IEventClaims {
|
|
87
|
+
/** Enable this to keep old versions of replaceable events */
|
|
88
|
+
keepOldVersions: boolean;
|
|
89
|
+
/** Enable this to keep expired events */
|
|
90
|
+
keepExpired: boolean;
|
|
87
91
|
filters(filters: Filter | Filter[]): Observable<NostrEvent>;
|
|
88
92
|
updated(id: string | NostrEvent): Observable<NostrEvent>;
|
|
89
93
|
removed(id: string): Observable<never>;
|
|
90
|
-
|
|
94
|
+
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
|
|
95
|
+
replaceable(pointer: AddressPointerWithoutD): Observable<NostrEvent | undefined>;
|
|
96
|
+
eventLoader?: (pointer: EventPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
97
|
+
replaceableLoader?: (pointer: AddressPointerWithoutD) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
98
|
+
addressableLoader?: (pointer: AddressPointer) => Observable<NostrEvent> | Promise<NostrEvent | undefined>;
|
|
99
|
+
profile(user: string | ProfilePointer): Observable<ProfileContent | undefined>;
|
|
100
|
+
contacts(user: string | ProfilePointer): Observable<ProfilePointer[]>;
|
|
101
|
+
mutes(user: string | ProfilePointer): Observable<Mutes | undefined>;
|
|
102
|
+
mailboxes(user: string | ProfilePointer): Observable<{
|
|
103
|
+
inboxes: string[];
|
|
104
|
+
outboxes: string[];
|
|
105
|
+
} | undefined>;
|
|
106
|
+
blossomServers(user: string | ProfilePointer): Observable<URL[]>;
|
|
107
|
+
reactions(event: NostrEvent): Observable<NostrEvent[]>;
|
|
108
|
+
thread(root: string | EventPointer | AddressPointer): Observable<Thread>;
|
|
91
109
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { ProfilePointer } from "nostr-tools/nip19";
|
|
3
|
+
export declare const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
|
|
4
|
+
export declare const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
|
|
5
|
+
export type CalendarEventParticipant = ProfilePointer & {
|
|
6
|
+
role?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const CalendarEventLocationsSymbol: unique symbol;
|
|
9
|
+
export declare const CalendarEventParticipantsSymbol: unique symbol;
|
|
10
|
+
export declare const CalendarEventHashtagsSymbol: unique symbol;
|
|
11
|
+
export declare const CalendarEventReferencesSymbol: unique symbol;
|
|
12
|
+
export declare const CalendarEventGeohashSymbol: unique symbol;
|
|
13
|
+
/** Gets the title of a calendar event or calendar */
|
|
14
|
+
export declare function getCalendarEventTitle(event: NostrEvent): string | undefined;
|
|
15
|
+
/** Gets the summary of a calendar event */
|
|
16
|
+
export declare function getCalendarEventSummary(event: NostrEvent): string | undefined;
|
|
17
|
+
/** Gets the image URL of a calendar event */
|
|
18
|
+
export declare function getCalendarEventImage(event: NostrEvent): string | undefined;
|
|
19
|
+
/** Gets the start Unix timestamp of a calendar event */
|
|
20
|
+
export declare function getCalendarEventStart(event: NostrEvent): number | undefined;
|
|
21
|
+
/** Gets the timezone of the start timestamp of a calendar event */
|
|
22
|
+
export declare function getCalendarEventStartTimezone(event: NostrEvent): string | undefined;
|
|
23
|
+
/** Gets the timezone of the end timestamp of a calendar event */
|
|
24
|
+
export declare function getCalendarEventEndTimezone(event: NostrEvent): string | undefined;
|
|
25
|
+
/** Gets the end Unix timestamp of a calendar event */
|
|
26
|
+
export declare function getCalendarEventEnd(event: NostrEvent): number | undefined;
|
|
27
|
+
/** Gets all locations from a calendar event */
|
|
28
|
+
export declare function getCalendarEventLocations(event: NostrEvent): string[];
|
|
29
|
+
/** Gets the geohash of a calendar event */
|
|
30
|
+
export declare function getCalendarEventGeohash(event: NostrEvent): string | undefined;
|
|
31
|
+
/** Gets all participants from a calendar event */
|
|
32
|
+
export declare function getCalendarEventParticipants(event: NostrEvent): CalendarEventParticipant[];
|
|
33
|
+
/** Gets all hashtags from a calendar event */
|
|
34
|
+
export declare function getCalendarEventHashtags(event: NostrEvent): string[];
|
|
35
|
+
/** Gets all references from a calendar event */
|
|
36
|
+
export declare function getCalendarEventReferences(event: NostrEvent): string[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getOrComputeCachedValue } from "./cache.js";
|
|
2
|
+
import { getTagValue } from "./event-tags.js";
|
|
3
|
+
import { getProfilePointerFromPTag } from "./pointers.js";
|
|
4
|
+
import { isPTag, isRTag, isTTag } from "./tags.js";
|
|
5
|
+
// NIP-52 Calendar Event Kinds
|
|
6
|
+
export const DATE_BASED_CALENDAR_EVENT_KIND = 31922;
|
|
7
|
+
export const TIME_BASED_CALENDAR_EVENT_KIND = 31923;
|
|
8
|
+
// Cache symbols for complex operations only
|
|
9
|
+
export const CalendarEventLocationsSymbol = Symbol.for("calendar-event-locations");
|
|
10
|
+
export const CalendarEventParticipantsSymbol = Symbol.for("calendar-event-participants");
|
|
11
|
+
export const CalendarEventHashtagsSymbol = Symbol.for("calendar-event-hashtags");
|
|
12
|
+
export const CalendarEventReferencesSymbol = Symbol.for("calendar-event-references");
|
|
13
|
+
export const CalendarEventGeohashSymbol = Symbol.for("calendar-event-geohash");
|
|
14
|
+
/** Gets the title of a calendar event or calendar */
|
|
15
|
+
export function getCalendarEventTitle(event) {
|
|
16
|
+
return getTagValue(event, "title") || getTagValue(event, "name"); // fallback to deprecated "name" tag
|
|
17
|
+
}
|
|
18
|
+
/** Gets the summary of a calendar event */
|
|
19
|
+
export function getCalendarEventSummary(event) {
|
|
20
|
+
return getTagValue(event, "summary");
|
|
21
|
+
}
|
|
22
|
+
/** Gets the image URL of a calendar event */
|
|
23
|
+
export function getCalendarEventImage(event) {
|
|
24
|
+
return getTagValue(event, "image");
|
|
25
|
+
}
|
|
26
|
+
/** Gets the start Unix timestamp of a calendar event */
|
|
27
|
+
export function getCalendarEventStart(event) {
|
|
28
|
+
const value = getTagValue(event, "start");
|
|
29
|
+
if (!value)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
|
|
32
|
+
return new Date(value).valueOf() / 1000;
|
|
33
|
+
else if (event.kind === TIME_BASED_CALENDAR_EVENT_KIND)
|
|
34
|
+
return parseInt(value);
|
|
35
|
+
else
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Gets the timezone of the start timestamp of a calendar event */
|
|
39
|
+
export function getCalendarEventStartTimezone(event) {
|
|
40
|
+
if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
|
|
41
|
+
return undefined;
|
|
42
|
+
return getTagValue(event, "start_tzid");
|
|
43
|
+
}
|
|
44
|
+
/** Gets the timezone of the end timestamp of a calendar event */
|
|
45
|
+
export function getCalendarEventEndTimezone(event) {
|
|
46
|
+
if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
|
|
47
|
+
return undefined;
|
|
48
|
+
return getTagValue(event, "end_tzid");
|
|
49
|
+
}
|
|
50
|
+
/** Gets the end Unix timestamp of a calendar event */
|
|
51
|
+
export function getCalendarEventEnd(event) {
|
|
52
|
+
const value = getTagValue(event, "end");
|
|
53
|
+
if (!value)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (event.kind === DATE_BASED_CALENDAR_EVENT_KIND)
|
|
56
|
+
return new Date(value).valueOf() / 1000;
|
|
57
|
+
else if (event.kind === TIME_BASED_CALENDAR_EVENT_KIND)
|
|
58
|
+
return parseInt(value);
|
|
59
|
+
else
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Gets all locations from a calendar event */
|
|
63
|
+
export function getCalendarEventLocations(event) {
|
|
64
|
+
if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
|
|
65
|
+
throw new Error("Event is not a date-based or time-based calendar event");
|
|
66
|
+
return getOrComputeCachedValue(event, CalendarEventLocationsSymbol, () => {
|
|
67
|
+
return event.tags.filter((t) => t[0] === "location" && t[1]).map((t) => t[1]);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** Gets the geohash of a calendar event */
|
|
71
|
+
export function getCalendarEventGeohash(event) {
|
|
72
|
+
if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
|
|
73
|
+
throw new Error("Event is not a date-based or time-based calendar event");
|
|
74
|
+
return getOrComputeCachedValue(event, CalendarEventGeohashSymbol, () => {
|
|
75
|
+
let hash = undefined;
|
|
76
|
+
for (const tag of event.tags) {
|
|
77
|
+
if (tag[0] === "g" && tag[1] && (!hash || tag[1].length > hash.length))
|
|
78
|
+
hash = tag[1];
|
|
79
|
+
}
|
|
80
|
+
return hash;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Gets all participants from a calendar event */
|
|
84
|
+
export function getCalendarEventParticipants(event) {
|
|
85
|
+
if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
|
|
86
|
+
throw new Error("Event is not a date-based or time-based calendar event");
|
|
87
|
+
return getOrComputeCachedValue(event, CalendarEventParticipantsSymbol, () => {
|
|
88
|
+
return event.tags.filter(isPTag).map((tag) => ({
|
|
89
|
+
...getProfilePointerFromPTag(tag),
|
|
90
|
+
// Third index of tag is optional "role"
|
|
91
|
+
role: tag[3] || undefined,
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Gets all hashtags from a calendar event */
|
|
96
|
+
export function getCalendarEventHashtags(event) {
|
|
97
|
+
if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
|
|
98
|
+
throw new Error("Event is not a date-based or time-based calendar event");
|
|
99
|
+
return getOrComputeCachedValue(event, CalendarEventHashtagsSymbol, () => {
|
|
100
|
+
return event.tags.filter(isTTag).map((t) => t[1]);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Gets all references from a calendar event */
|
|
104
|
+
export function getCalendarEventReferences(event) {
|
|
105
|
+
if (event.kind !== DATE_BASED_CALENDAR_EVENT_KIND && event.kind !== TIME_BASED_CALENDAR_EVENT_KIND)
|
|
106
|
+
throw new Error("Event is not a date-based or time-based calendar event");
|
|
107
|
+
return getOrComputeCachedValue(event, CalendarEventReferencesSymbol, () => {
|
|
108
|
+
return event.tags.filter(isRTag).map((t) => t[1]);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NostrEvent } from "nostr-tools";
|
|
2
|
+
import { AddressPointer, EventPointer, ProfilePointer } from "nostr-tools/nip19";
|
|
3
|
+
export declare const CALENDAR_EVENT_RSVP_KIND = 31925;
|
|
4
|
+
export type RSVPStatus = "accepted" | "declined" | "tentative";
|
|
5
|
+
export type RSVPFreeBusy = "free" | "busy";
|
|
6
|
+
/** Gets the RSVP status from a calendar event RSVP */
|
|
7
|
+
export declare function getRSVPStatus(event: NostrEvent): RSVPStatus | undefined;
|
|
8
|
+
/** Gets the free/busy status from a calendar event RSVP (will be undefined if the RSVP is declined) */
|
|
9
|
+
export declare function getRSVPFreeBusy(event: NostrEvent): RSVPFreeBusy | undefined;
|
|
10
|
+
/** Gets the referenced calendar event coordinate that the RSVP is responding to */
|
|
11
|
+
export declare function getRSVPAddressPointer(event: NostrEvent): AddressPointer | undefined;
|
|
12
|
+
/** Gets the referenced calendar event pointer that the RSVP is responding to */
|
|
13
|
+
export declare function getRSVPEventPointer(event: NostrEvent): EventPointer | undefined;
|
|
14
|
+
/** Gets the profile pointer that the RSVP is responding to */
|
|
15
|
+
export declare function getRSVPProfilePointer(event: NostrEvent): ProfilePointer | undefined;
|