applesauce-core 0.0.0-next-20250312111321 → 0.0.0-next-20250312152821
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-store.d.ts +3 -2
- package/dist/event-store/event-store.js +3 -3
- package/dist/event-store/event-store.test.d.ts +1 -0
- package/dist/event-store/event-store.test.js +74 -0
- package/dist/event-store/index.d.ts +1 -0
- package/dist/event-store/index.js +1 -0
- package/dist/event-store/interface.d.ts +27 -0
- package/dist/event-store/interface.js +1 -0
- package/dist/helpers/cache.d.ts +3 -4
- package/dist/helpers/cache.js +1 -1
- package/dist/helpers/event.d.ts +9 -4
- 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 +10 -10
- package/dist/helpers/hidden-content.js +7 -5
- package/dist/helpers/hidden-tags.d.ts +9 -18
- package/dist/helpers/hidden-tags.js +19 -34
- package/dist/helpers/hidden-tags.test.d.ts +1 -0
- package/dist/helpers/hidden-tags.test.js +29 -0
- package/dist/helpers/mailboxes.test.d.ts +1 -0
- package/dist/helpers/mailboxes.test.js +81 -0
- package/dist/helpers/mutes.d.ts +1 -0
- package/dist/helpers/mutes.js +2 -1
- 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/observable/claim-latest.d.ts +3 -2
- package/dist/observable/claim-latest.js +2 -1
- 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/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/observable/with-immediate-value.d.ts +3 -0
- package/dist/observable/with-immediate-value.js +19 -0
- package/dist/queries/mutes.js +1 -1
- package/dist/queries/simple.d.ts +1 -1
- package/dist/queries/simple.js +3 -3
- 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/query-store.d.ts +4 -4
- package/dist/query-store/query-store.js +8 -3
- package/dist/utils/lru.d.ts +32 -0
- package/dist/utils/lru.js +148 -0
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
2
|
import { Observable } from "rxjs";
|
|
3
3
|
import { Database } from "./database.js";
|
|
4
|
+
import { IEventStore } from "./interface.js";
|
|
4
5
|
export declare const EventStoreSymbol: unique symbol;
|
|
5
|
-
export declare class EventStore {
|
|
6
|
+
export declare class EventStore implements IEventStore {
|
|
6
7
|
database: Database;
|
|
7
8
|
/** Enable this to keep old versions of replaceable events */
|
|
8
9
|
keepOldVersions: boolean;
|
|
@@ -50,7 +51,7 @@ export declare class EventStore {
|
|
|
50
51
|
/** Returns an observable that completes when an event is removed */
|
|
51
52
|
removed(id: string): Observable<never>;
|
|
52
53
|
/** Creates an observable that emits when event is updated */
|
|
53
|
-
updated(
|
|
54
|
+
updated(event: string | NostrEvent): Observable<NostrEvent>;
|
|
54
55
|
/** Creates an observable that subscribes to a single event */
|
|
55
56
|
event(id: string): Observable<NostrEvent | undefined>;
|
|
56
57
|
/** Creates an observable that subscribes to multiple events */
|
|
@@ -181,8 +181,8 @@ export class EventStore {
|
|
|
181
181
|
mergeMap(() => EMPTY));
|
|
182
182
|
}
|
|
183
183
|
/** Creates an observable that emits when event is updated */
|
|
184
|
-
updated(
|
|
185
|
-
return this.database.updated.pipe(filter((e) => e.id ===
|
|
184
|
+
updated(event) {
|
|
185
|
+
return this.database.updated.pipe(filter((e) => e.id === event || e === event));
|
|
186
186
|
}
|
|
187
187
|
/** Creates an observable that subscribes to a single event */
|
|
188
188
|
event(id) {
|
|
@@ -199,7 +199,7 @@ export class EventStore {
|
|
|
199
199
|
// emit undefined when deleted
|
|
200
200
|
this.removed(id).pipe(endWith(undefined))).pipe(
|
|
201
201
|
// claim all events
|
|
202
|
-
|
|
202
|
+
claimLatest(this.database));
|
|
203
203
|
}
|
|
204
204
|
/** Creates an observable that subscribes to multiple events */
|
|
205
205
|
events(ids) {
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "nostr-tools";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
export interface IEventStore {
|
|
4
|
+
updates: Observable<NostrEvent>;
|
|
5
|
+
add(event: NostrEvent, fromRelay?: string): NostrEvent;
|
|
6
|
+
remove(event: string | NostrEvent): boolean;
|
|
7
|
+
update(event: NostrEvent): NostrEvent;
|
|
8
|
+
hasEvent(id: string): boolean;
|
|
9
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
10
|
+
getEvent(id: string): NostrEvent | undefined;
|
|
11
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
12
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
|
|
13
|
+
getAll(filters: Filter | Filter[]): Set<NostrEvent>;
|
|
14
|
+
getTimeline(filters: Filter | Filter[]): NostrEvent[];
|
|
15
|
+
filters(filters: Filter | Filter[]): Observable<NostrEvent>;
|
|
16
|
+
updated(id: string | NostrEvent): Observable<NostrEvent>;
|
|
17
|
+
removed(id: string): Observable<never>;
|
|
18
|
+
event(id: string): Observable<NostrEvent | undefined>;
|
|
19
|
+
events(ids: string[]): Observable<Record<string, NostrEvent>>;
|
|
20
|
+
replaceable(kind: number, pubkey: string, identifier?: string): Observable<NostrEvent | undefined>;
|
|
21
|
+
replaceableSet(pointers: {
|
|
22
|
+
kind: number;
|
|
23
|
+
pubkey: string;
|
|
24
|
+
identifier?: string;
|
|
25
|
+
}[]): Observable<Record<string, NostrEvent>>;
|
|
26
|
+
timeline(filters: Filter | Filter[], includeOldVersion?: boolean): Observable<NostrEvent[]>;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/helpers/cache.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function setCachedValue<T extends unknown>(event: NostrEvent | EventTemplate, symbol: symbol, value: T): void;
|
|
1
|
+
export declare function getCachedValue<T extends unknown>(event: any, symbol: symbol): T | undefined;
|
|
2
|
+
export declare function setCachedValue<T extends unknown>(event: any, symbol: symbol, value: T): void;
|
|
4
3
|
/** Internal method used to cache computed values on events */
|
|
5
|
-
export declare function getOrComputeCachedValue<T extends unknown>(event:
|
|
4
|
+
export declare function getOrComputeCachedValue<T extends unknown>(event: any, symbol: symbol, compute: () => T): T;
|
package/dist/helpers/cache.js
CHANGED
package/dist/helpers/event.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { NostrEvent, VerifiedEvent } from "nostr-tools";
|
|
2
|
+
import { IEventStore } from "../event-store/interface.js";
|
|
3
3
|
export declare const EventUIDSymbol: unique symbol;
|
|
4
4
|
export declare const EventIndexableTagsSymbol: unique symbol;
|
|
5
5
|
export declare const FromCacheSymbol: unique symbol;
|
|
@@ -35,7 +35,12 @@ export declare function getIndexableTags(event: NostrEvent): Set<string>;
|
|
|
35
35
|
* Returns the second index ( tag[1] ) of the first tag that matches the name
|
|
36
36
|
* If the event has any hidden tags they will be searched first
|
|
37
37
|
*/
|
|
38
|
-
export declare function getTagValue
|
|
38
|
+
export declare function getTagValue<T extends {
|
|
39
|
+
kind: number;
|
|
40
|
+
tags: string[][];
|
|
41
|
+
content: string;
|
|
42
|
+
pubkey: string;
|
|
43
|
+
}>(event: T, name: string): string | undefined;
|
|
39
44
|
/** Sets events verified flag without checking anything */
|
|
40
45
|
export declare function fakeVerifyEvent(event: NostrEvent): event is VerifiedEvent;
|
|
41
46
|
/** Marks an event as being from a cache */
|
|
@@ -43,7 +48,7 @@ export declare function markFromCache(event: NostrEvent): void;
|
|
|
43
48
|
/** Returns if an event was from a cache */
|
|
44
49
|
export declare function isFromCache(event: NostrEvent): boolean;
|
|
45
50
|
/** Returns the EventStore of an event if its been added to one */
|
|
46
|
-
export declare function getParentEventStore(event:
|
|
51
|
+
export declare function getParentEventStore<T extends object>(event: T): IEventStore | undefined;
|
|
47
52
|
/**
|
|
48
53
|
* Returns the replaceable identifier for a replaceable event
|
|
49
54
|
* @throws
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getFileMetadataFromImetaTag, parseFileMetadataTags } from "./file-metadata.js";
|
|
3
|
+
describe("file metadata helpers", () => {
|
|
4
|
+
describe("parseFileMetadataTags", () => {
|
|
5
|
+
it("should parse a simple 1060 event", () => {
|
|
6
|
+
const tags = [
|
|
7
|
+
["url", "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif"],
|
|
8
|
+
["ox", "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae"],
|
|
9
|
+
["fallback", "https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
|
|
10
|
+
["x", "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5"],
|
|
11
|
+
["m", "image/gif"],
|
|
12
|
+
["dim", "360x306"],
|
|
13
|
+
["bh", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
|
|
14
|
+
["blurhash", "L38zleNL00~W^kRj0L-p0KM_^kx]"],
|
|
15
|
+
[
|
|
16
|
+
"thumb",
|
|
17
|
+
"https://image.nostr.build/thumb/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
|
|
18
|
+
],
|
|
19
|
+
["t", "gifbuddy"],
|
|
20
|
+
["summary", "Khaleesi call dragons Daenerys Targaryen"],
|
|
21
|
+
["alt", "a woman with blonde hair and a brooch on her shoulder"],
|
|
22
|
+
[
|
|
23
|
+
"thumb",
|
|
24
|
+
"https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
|
|
25
|
+
"5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"image",
|
|
29
|
+
"https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
|
|
30
|
+
"5d92423664fc15874b1d26c70a05a541ec09b5c438bf157977a87c8e64b31463",
|
|
31
|
+
],
|
|
32
|
+
];
|
|
33
|
+
expect(parseFileMetadataTags(tags)).toEqual({
|
|
34
|
+
url: "https://image.nostr.build/30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae.gif",
|
|
35
|
+
type: "image/gif",
|
|
36
|
+
dimensions: "360x306",
|
|
37
|
+
blurhash: "L38zleNL00~W^kRj0L-p0KM_^kx]",
|
|
38
|
+
sha256: "77fcf42b2b720babcdbe686eff67273d8a68862d74a2672db672bc48439a3ea5",
|
|
39
|
+
originalSha256: "30696696e57a2732d4e9f1b15ff4d4d4eaa64b759df6876863f436ff5d736eae",
|
|
40
|
+
thumbnail: "https://media.tenor.com/wpvrkjn192gAAAAx/daenerys-targaryen.webp",
|
|
41
|
+
image: "https://media.tenor.com/wpvrkjn192gAAAAe/daenerys-targaryen.png",
|
|
42
|
+
summary: "Khaleesi call dragons Daenerys Targaryen",
|
|
43
|
+
fallback: ["https://media.tenor.com/wpvrkjn192gAAAAC/daenerys-targaryen.gif"],
|
|
44
|
+
alt: "a woman with blonde hair and a brooch on her shoulder",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("getFileMetadataFromImetaTag", () => {
|
|
49
|
+
it("should parse simple imeta tag", () => {
|
|
50
|
+
expect(getFileMetadataFromImetaTag([
|
|
51
|
+
"imeta",
|
|
52
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
53
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
54
|
+
"dim 1024x1024",
|
|
55
|
+
"m image/jpeg",
|
|
56
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
57
|
+
])).toEqual({
|
|
58
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
59
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
60
|
+
dimensions: "1024x1024",
|
|
61
|
+
type: "image/jpeg",
|
|
62
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("should parse thumbnail url", () => {
|
|
66
|
+
expect(getFileMetadataFromImetaTag([
|
|
67
|
+
"imeta",
|
|
68
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
69
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
70
|
+
"dim 1024x1024",
|
|
71
|
+
"m image/jpeg",
|
|
72
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
73
|
+
"thumb https://exmaple.com/thumb.jpg",
|
|
74
|
+
])).toEqual({
|
|
75
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
76
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
77
|
+
dimensions: "1024x1024",
|
|
78
|
+
type: "image/jpeg",
|
|
79
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
80
|
+
thumbnail: "https://exmaple.com/thumb.jpg",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
it("should parse multiple fallback urls", () => {
|
|
84
|
+
expect(getFileMetadataFromImetaTag([
|
|
85
|
+
"imeta",
|
|
86
|
+
"url https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
87
|
+
"x 3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
88
|
+
"dim 1024x1024",
|
|
89
|
+
"m image/jpeg",
|
|
90
|
+
"blurhash ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
91
|
+
"fallback https://exmaple.com/image2.jpg",
|
|
92
|
+
"fallback https://exmaple.com/image3.jpg",
|
|
93
|
+
])).toEqual({
|
|
94
|
+
url: "https://blossom.primal.net/3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c.jpg",
|
|
95
|
+
sha256: "3f4dbf2797ac4e90b00bcfe2728e5c8367ed909c48230ac454cc325f1993646c",
|
|
96
|
+
dimensions: "1024x1024",
|
|
97
|
+
type: "image/jpeg",
|
|
98
|
+
blurhash: "ggH{Aws:RPWBRjaeay?^ozV@aeRjaej[$gt7kCofWVofkCrrofxuofa|ozbHx]s:tRofaet7ay",
|
|
99
|
+
fallback: ["https://exmaple.com/image2.jpg", "https://exmaple.com/image3.jpg"],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { UnsignedEvent, type EventTemplate, type NostrEvent } from "nostr-tools";
|
|
2
1
|
export declare const HiddenContentSymbol: unique symbol;
|
|
3
2
|
export type HiddenContentSigner = {
|
|
4
3
|
nip04?: {
|
|
@@ -17,14 +16,14 @@ export declare function setEventContentEncryptionMethod(kind: number, method: "n
|
|
|
17
16
|
/** Checks if an event can have hidden content */
|
|
18
17
|
export declare function canHaveHiddenContent(kind: number): boolean;
|
|
19
18
|
/** Checks if an event has hidden content */
|
|
20
|
-
export declare function hasHiddenContent
|
|
19
|
+
export declare function hasHiddenContent<T extends {
|
|
21
20
|
kind: number;
|
|
22
21
|
content: string;
|
|
23
|
-
}): boolean;
|
|
22
|
+
}>(event: T): boolean;
|
|
24
23
|
/** Returns the hidden tags for an event if they are unlocked */
|
|
25
|
-
export declare function getHiddenContent(event:
|
|
24
|
+
export declare function getHiddenContent<T extends object>(event: T): string | undefined;
|
|
26
25
|
/** Checks if the hidden tags are locked */
|
|
27
|
-
export declare function isHiddenContentLocked(event:
|
|
26
|
+
export declare function isHiddenContentLocked<T extends object>(event: T): boolean;
|
|
28
27
|
/** Returns either nip04 or nip44 encryption methods depending on event kind */
|
|
29
28
|
export declare function getHiddenContentEncryptionMethods(kind: number, signer: HiddenContentSigner): {
|
|
30
29
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
@@ -33,14 +32,15 @@ export declare function getHiddenContentEncryptionMethods(kind: number, signer:
|
|
|
33
32
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
34
33
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
35
34
|
};
|
|
35
|
+
export type HiddenContentEvent = {
|
|
36
|
+
kind: number;
|
|
37
|
+
pubkey: string;
|
|
38
|
+
content: string;
|
|
39
|
+
};
|
|
36
40
|
/**
|
|
37
41
|
* Unlocks the encrypted content in an event
|
|
38
42
|
* @param event The event with content to decrypt
|
|
39
43
|
* @param signer A signer to use to decrypt the tags
|
|
40
44
|
* @throws
|
|
41
45
|
*/
|
|
42
|
-
export declare function unlockHiddenContent(event:
|
|
43
|
-
kind: number;
|
|
44
|
-
pubkey: string;
|
|
45
|
-
content: string;
|
|
46
|
-
}, signer: HiddenContentSigner): Promise<string>;
|
|
46
|
+
export declare function unlockHiddenContent<T extends HiddenContentEvent>(event: T, signer: HiddenContentSigner): Promise<string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as kinds from "nostr-tools/kinds";
|
|
2
2
|
import { GROUPS_LIST_KIND } from "./groups.js";
|
|
3
|
-
import { getParentEventStore } from "./event.js";
|
|
3
|
+
import { getParentEventStore, isEvent } from "./event.js";
|
|
4
4
|
export const HiddenContentSymbol = Symbol.for("hidden-content");
|
|
5
5
|
/** Various event kinds that can have encrypted tags in their content and which encryption method they use */
|
|
6
6
|
export const EventContentEncryptionMethod = {
|
|
@@ -46,7 +46,7 @@ export function getHiddenContent(event) {
|
|
|
46
46
|
}
|
|
47
47
|
/** Checks if the hidden tags are locked */
|
|
48
48
|
export function isHiddenContentLocked(event) {
|
|
49
|
-
return
|
|
49
|
+
return Reflect.has(event, HiddenContentSymbol) === false;
|
|
50
50
|
}
|
|
51
51
|
/** Returns either nip04 or nip44 encryption methods depending on event kind */
|
|
52
52
|
export function getHiddenContentEncryptionMethods(kind, signer) {
|
|
@@ -69,8 +69,10 @@ export async function unlockHiddenContent(event, signer) {
|
|
|
69
69
|
const plaintext = await encryption.decrypt(event.pubkey, event.content);
|
|
70
70
|
Reflect.set(event, HiddenContentSymbol, plaintext);
|
|
71
71
|
// if the event has been added to an event store, notify it
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
eventStore
|
|
72
|
+
if (isEvent(event)) {
|
|
73
|
+
const eventStore = getParentEventStore(event);
|
|
74
|
+
if (eventStore)
|
|
75
|
+
eventStore.update(event);
|
|
76
|
+
}
|
|
75
77
|
return plaintext;
|
|
76
78
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { EventStore } from "../event-store/event-store.js";
|
|
3
|
-
import { HiddenContentSigner } from "./hidden-content.js";
|
|
1
|
+
import { HiddenContentEvent, HiddenContentSigner } from "./hidden-content.js";
|
|
4
2
|
export declare const HiddenTagsSymbol: unique symbol;
|
|
5
3
|
/** Checks if an event can have hidden tags */
|
|
6
4
|
export declare function canHaveHiddenTags(kind: number): boolean;
|
|
7
5
|
/** Checks if an event has hidden tags */
|
|
8
|
-
export declare function hasHiddenTags
|
|
6
|
+
export declare function hasHiddenTags<T extends {
|
|
7
|
+
content: string;
|
|
8
|
+
kind: number;
|
|
9
|
+
}>(event: T): boolean;
|
|
9
10
|
/** Returns the hidden tags for an event if they are unlocked */
|
|
10
|
-
export declare function getHiddenTags(event:
|
|
11
|
+
export declare function getHiddenTags<T extends object>(event: T): string[][] | undefined;
|
|
11
12
|
/** Checks if the hidden tags are locked */
|
|
12
|
-
export declare function isHiddenTagsLocked(event:
|
|
13
|
+
export declare function isHiddenTagsLocked<T extends object>(event: T): boolean;
|
|
13
14
|
/** Returns either nip04 or nip44 encryption method depending on list kind */
|
|
14
|
-
export declare function
|
|
15
|
+
export declare function getHiddenTagsEncryptionMethods(kind: number, signer: HiddenContentSigner): {
|
|
15
16
|
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
|
16
17
|
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
|
17
18
|
} | {
|
|
@@ -25,14 +26,4 @@ export declare function getListEncryptionMethods(kind: number, signer: HiddenCon
|
|
|
25
26
|
* @param store An optional EventStore to notify about the update
|
|
26
27
|
* @throws
|
|
27
28
|
*/
|
|
28
|
-
export declare function unlockHiddenTags<T extends
|
|
29
|
-
kind: number;
|
|
30
|
-
pubkey: string;
|
|
31
|
-
content: string;
|
|
32
|
-
}>(event: T, signer: HiddenContentSigner, store?: EventStore): Promise<string[][]>;
|
|
33
|
-
/**
|
|
34
|
-
* Override the hidden tags in an event
|
|
35
|
-
* @deprecated use EventFactory to create draft events
|
|
36
|
-
* @throws
|
|
37
|
-
*/
|
|
38
|
-
export declare function overrideHiddenTags(event: NostrEvent, hidden: string[][], signer: HiddenContentSigner): Promise<EventTemplate>;
|
|
29
|
+
export declare function unlockHiddenTags<T extends HiddenContentEvent>(event: T, signer: HiddenContentSigner): Promise<string[][]>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { canHaveHiddenContent, getHiddenContentEncryptionMethods, unlockHiddenContent, } from "./hidden-content.js";
|
|
1
|
+
import { canHaveHiddenContent, getHiddenContent, getHiddenContentEncryptionMethods, isHiddenContentLocked, unlockHiddenContent, } from "./hidden-content.js";
|
|
2
|
+
import { getOrComputeCachedValue } from "./cache.js";
|
|
4
3
|
export const HiddenTagsSymbol = Symbol.for("hidden-tags");
|
|
5
4
|
/** Checks if an event can have hidden tags */
|
|
6
5
|
export function canHaveHiddenTags(kind) {
|
|
@@ -12,14 +11,23 @@ export function hasHiddenTags(event) {
|
|
|
12
11
|
}
|
|
13
12
|
/** Returns the hidden tags for an event if they are unlocked */
|
|
14
13
|
export function getHiddenTags(event) {
|
|
15
|
-
|
|
14
|
+
if (isHiddenTagsLocked(event))
|
|
15
|
+
return undefined;
|
|
16
|
+
return getOrComputeCachedValue(event, HiddenTagsSymbol, () => {
|
|
17
|
+
const plaintext = getHiddenContent(event);
|
|
18
|
+
const parsed = JSON.parse(plaintext);
|
|
19
|
+
if (!Array.isArray(parsed))
|
|
20
|
+
throw new Error("Content is not an array of tags");
|
|
21
|
+
// Convert array to tags array string[][]
|
|
22
|
+
return parsed.filter((t) => Array.isArray(t)).map((t) => t.map((v) => String(v)));
|
|
23
|
+
});
|
|
16
24
|
}
|
|
17
25
|
/** Checks if the hidden tags are locked */
|
|
18
26
|
export function isHiddenTagsLocked(event) {
|
|
19
|
-
return
|
|
27
|
+
return isHiddenContentLocked(event);
|
|
20
28
|
}
|
|
21
29
|
/** Returns either nip04 or nip44 encryption method depending on list kind */
|
|
22
|
-
export function
|
|
30
|
+
export function getHiddenTagsEncryptionMethods(kind, signer) {
|
|
23
31
|
return getHiddenContentEncryptionMethods(kind, signer);
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
@@ -29,34 +37,11 @@ export function getListEncryptionMethods(kind, signer) {
|
|
|
29
37
|
* @param store An optional EventStore to notify about the update
|
|
30
38
|
* @throws
|
|
31
39
|
*/
|
|
32
|
-
export async function unlockHiddenTags(event, signer
|
|
40
|
+
export async function unlockHiddenTags(event, signer) {
|
|
33
41
|
if (!canHaveHiddenTags(event.kind))
|
|
34
42
|
throw new Error("Event kind does not support hidden tags");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Convert array to tags array string[][]
|
|
40
|
-
const tags = parsed.filter((t) => Array.isArray(t)).map((t) => t.map((v) => String(v)));
|
|
41
|
-
Reflect.set(event, HiddenTagsSymbol, tags);
|
|
42
|
-
if (store && isEvent(event))
|
|
43
|
-
store.update(event);
|
|
44
|
-
return tags;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Override the hidden tags in an event
|
|
48
|
-
* @deprecated use EventFactory to create draft events
|
|
49
|
-
* @throws
|
|
50
|
-
*/
|
|
51
|
-
export async function overrideHiddenTags(event, hidden, signer) {
|
|
52
|
-
if (!canHaveHiddenTags(event.kind))
|
|
53
|
-
throw new Error("Event kind does not support hidden tags");
|
|
54
|
-
const encryption = getListEncryptionMethods(event.kind, signer);
|
|
55
|
-
const ciphertext = await encryption.encrypt(event.pubkey, JSON.stringify(hidden));
|
|
56
|
-
return {
|
|
57
|
-
kind: event.kind,
|
|
58
|
-
content: ciphertext,
|
|
59
|
-
created_at: unixNow(),
|
|
60
|
-
tags: event.tags,
|
|
61
|
-
};
|
|
43
|
+
// unlock hidden content is needed
|
|
44
|
+
if (isHiddenContentLocked(event))
|
|
45
|
+
await unlockHiddenContent(event, signer);
|
|
46
|
+
return getHiddenTags(event);
|
|
62
47
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, beforeEach, it, expect } from "vitest";
|
|
2
|
+
import { finalizeEvent, generateSecretKey, getPublicKey, kinds, nip04 } from "nostr-tools";
|
|
3
|
+
import { getHiddenTags, unlockHiddenTags } from "./hidden-tags.js";
|
|
4
|
+
import { unixNow } from "./time.js";
|
|
5
|
+
const key = generateSecretKey();
|
|
6
|
+
const pubkey = getPublicKey(key);
|
|
7
|
+
const signer = {
|
|
8
|
+
nip04: {
|
|
9
|
+
encrypt: (pubkey, plaintext) => nip04.encrypt(key, pubkey, plaintext),
|
|
10
|
+
decrypt: (pubkey, ciphertext) => nip04.decrypt(key, pubkey, ciphertext),
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
describe("Private Lists", () => {
|
|
14
|
+
describe("unlockHiddenTags", () => {
|
|
15
|
+
let list;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
list = finalizeEvent({
|
|
18
|
+
kind: kinds.Mutelist,
|
|
19
|
+
created_at: unixNow(),
|
|
20
|
+
content: await nip04.encrypt(key, pubkey, JSON.stringify([["p", "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"]])),
|
|
21
|
+
tags: [],
|
|
22
|
+
}, key);
|
|
23
|
+
});
|
|
24
|
+
it("should unlock hidden tags", async () => {
|
|
25
|
+
await unlockHiddenTags(list, signer);
|
|
26
|
+
expect(getHiddenTags(list)).toEqual(expect.arrayContaining([["p", "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"]]));
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { getInboxes, getOutboxes } from "./mailboxes.js";
|
|
3
|
+
const emptyEvent = {
|
|
4
|
+
kind: 10002,
|
|
5
|
+
content: "",
|
|
6
|
+
tags: [],
|
|
7
|
+
created_at: 0,
|
|
8
|
+
sig: "",
|
|
9
|
+
id: "",
|
|
10
|
+
pubkey: "",
|
|
11
|
+
};
|
|
12
|
+
describe("Mailboxes", () => {
|
|
13
|
+
describe("getInboxes", () => {
|
|
14
|
+
test("should transform urls", () => {
|
|
15
|
+
expect(Array.from(getInboxes({
|
|
16
|
+
...emptyEvent,
|
|
17
|
+
tags: [["r", "wss://inbox.com"]],
|
|
18
|
+
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
|
|
19
|
+
});
|
|
20
|
+
test("should remove bad urls", () => {
|
|
21
|
+
expect(Array.from(getInboxes({
|
|
22
|
+
...emptyEvent,
|
|
23
|
+
tags: [["r", "bad://inbox.com"]],
|
|
24
|
+
}))).toHaveLength(0);
|
|
25
|
+
expect(Array.from(getInboxes({
|
|
26
|
+
...emptyEvent,
|
|
27
|
+
tags: [["r", "something that is not a url"]],
|
|
28
|
+
}))).toHaveLength(0);
|
|
29
|
+
expect(Array.from(getInboxes({
|
|
30
|
+
...emptyEvent,
|
|
31
|
+
tags: [["r", "wss://inbox.com,wss://inbox.org"]],
|
|
32
|
+
}))).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
test("without marker", () => {
|
|
35
|
+
expect(Array.from(getInboxes({
|
|
36
|
+
...emptyEvent,
|
|
37
|
+
tags: [["r", "wss://inbox.com/"]],
|
|
38
|
+
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
|
|
39
|
+
});
|
|
40
|
+
test("with marker", () => {
|
|
41
|
+
expect(Array.from(getInboxes({
|
|
42
|
+
...emptyEvent,
|
|
43
|
+
tags: [["r", "wss://inbox.com/", "read"]],
|
|
44
|
+
}))).toEqual(expect.arrayContaining(["wss://inbox.com/"]));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe("getOutboxes", () => {
|
|
48
|
+
test("should transform urls", () => {
|
|
49
|
+
expect(Array.from(getOutboxes({
|
|
50
|
+
...emptyEvent,
|
|
51
|
+
tags: [["r", "wss://outbox.com"]],
|
|
52
|
+
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
|
|
53
|
+
});
|
|
54
|
+
test("should remove bad urls", () => {
|
|
55
|
+
expect(Array.from(getOutboxes({
|
|
56
|
+
...emptyEvent,
|
|
57
|
+
tags: [["r", "bad://inbox.com"]],
|
|
58
|
+
}))).toHaveLength(0);
|
|
59
|
+
expect(Array.from(getOutboxes({
|
|
60
|
+
...emptyEvent,
|
|
61
|
+
tags: [["r", "something that is not a url"]],
|
|
62
|
+
}))).toHaveLength(0);
|
|
63
|
+
expect(Array.from(getOutboxes({
|
|
64
|
+
...emptyEvent,
|
|
65
|
+
tags: [["r", "wss://outbox.com,wss://inbox.org"]],
|
|
66
|
+
}))).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
test("without marker", () => {
|
|
69
|
+
expect(Array.from(getOutboxes({
|
|
70
|
+
...emptyEvent,
|
|
71
|
+
tags: [["r", "wss://outbox.com/"]],
|
|
72
|
+
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
|
|
73
|
+
});
|
|
74
|
+
test("with marker", () => {
|
|
75
|
+
expect(Array.from(getOutboxes({
|
|
76
|
+
...emptyEvent,
|
|
77
|
+
tags: [["r", "wss://outbox.com/", "write"]],
|
|
78
|
+
}))).toEqual(expect.arrayContaining(["wss://outbox.com/"]));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
package/dist/helpers/mutes.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type Mutes = {
|
|
|
7
7
|
hashtags: Set<string>;
|
|
8
8
|
words: Set<string>;
|
|
9
9
|
};
|
|
10
|
+
/** Parses mute tags */
|
|
10
11
|
export declare function parseMutedTags(tags: string[][]): Mutes;
|
|
11
12
|
/** Returns muted things */
|
|
12
13
|
export declare function getMutedThings(mute: NostrEvent): Mutes;
|