applesauce-core 2.1.1 → 2.2.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.
@@ -1 +1 @@
1
- export declare const INDEXABLE_TAGS: Set<string>;
1
+ export {};
@@ -1,2 +1 @@
1
- const LETTERS = "abcdefghijklmnopqrstuvwxyz";
2
- export const INDEXABLE_TAGS = new Set((LETTERS + LETTERS.toUpperCase()).split(""));
1
+ export {};
@@ -1,9 +1,9 @@
1
1
  import { binarySearch, insertEventIntoDescendingList } from "nostr-tools/utils";
2
2
  import { Subject } from "rxjs";
3
- import { createReplaceableAddress, getIndexableTags, getReplaceableAddress, isReplaceable } from "../helpers/event.js";
3
+ import { getIndexableTags, INDEXABLE_TAGS } from "../helpers/event-tags.js";
4
+ import { createReplaceableAddress, getReplaceableAddress, isReplaceable } from "../helpers/event.js";
4
5
  import { LRU } from "../helpers/lru.js";
5
6
  import { logger } from "../logger.js";
6
- import { INDEXABLE_TAGS } from "./common.js";
7
7
  /**
8
8
  * A set of nostr events that can be queried and subscribed to
9
9
  * NOTE: does not handle replaceable events or any deletion logic
@@ -3,8 +3,6 @@ import { Observable } from "rxjs";
3
3
  import { EventSet } from "./event-set.js";
4
4
  import { IEventStore, ModelConstructor } from "./interface.js";
5
5
  import { AddressPointer, EventPointer } from "nostr-tools/nip19";
6
- /** A symbol on an event that marks which event store its part of */
7
- export declare const EventStoreSymbol: unique symbol;
8
6
  export declare class EventStore implements IEventStore {
9
7
  database: EventSet;
10
8
  /** Enable this to keep old versions of replaceable events */
@@ -3,7 +3,7 @@ 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 { FromCacheSymbol, getReplaceableAddress, getTagValue, isReplaceable } from "../helpers/event.js";
6
+ import { EventStoreSymbol, FromCacheSymbol, getReplaceableAddress, isReplaceable } from "../helpers/event.js";
7
7
  import { matchFilters } from "../helpers/filter.js";
8
8
  import { parseCoordinate } from "../helpers/pointers.js";
9
9
  import { addSeenRelay, getSeenRelays } from "../helpers/relays.js";
@@ -16,8 +16,6 @@ import { ReactionsModel } from "../models/reactions.js";
16
16
  import { MailboxesModel } from "../models/mailboxes.js";
17
17
  import { UserBlossomServersModel } from "../models/blossom.js";
18
18
  import { CommentsModel, ThreadModel } from "../models/index.js";
19
- /** A symbol on an event that marks which event store its part of */
20
- export const EventStoreSymbol = Symbol.for("event-store");
21
19
  export class EventStore {
22
20
  database;
23
21
  /** Enable this to keep old versions of replaceable events */
@@ -120,7 +118,7 @@ export class EventStore {
120
118
  if (this.checkDeleted(event))
121
119
  return event;
122
120
  // Get the replaceable identifier
123
- const d = isReplaceable(event.kind) ? getTagValue(event, "d") : undefined;
121
+ const d = isReplaceable(event.kind) ? event.tags.find((t) => t[0] === "d")?.[1] : undefined;
124
122
  // Don't insert the event if there is already a newer version
125
123
  if (!this.keepOldVersions && isReplaceable(event.kind)) {
126
124
  const existing = this.database.getReplaceableHistory(event.kind, event.pubkey, d);
@@ -1,4 +1,4 @@
1
- import { getTagValue } from "./event.js";
1
+ import { getTagValue } from "./event-tags.js";
2
2
  /** Returns an articles title, if it exists */
3
3
  export function getArticleTitle(article) {
4
4
  return getTagValue(article, "title");
@@ -1,4 +1,4 @@
1
- import { getTagValue } from "./event.js";
1
+ import { getTagValue } from "./event-tags.js";
2
2
  /** Gets an "emoji" tag that matches an emoji code */
3
3
  export function getEmojiTag(tags, code) {
4
4
  code = code.replace(/^:|:$/g, "").toLowerCase();
@@ -1,3 +1,4 @@
1
+ import { NostrEvent } from "nostr-tools";
1
2
  import { Observable } from "rxjs";
2
3
  import { IEventStoreStreams } from "../event-store/interface.js";
3
4
  /** A symbol that is used to mark encrypted content as being from a cache */
@@ -11,5 +12,11 @@ export interface EncryptedContentCache {
11
12
  export declare function markEncryptedContentFromCache<T extends object>(event: T): void;
12
13
  /** Checks if the encrypted content is from a cache */
13
14
  export declare function isEncryptedContentFromCache<T extends object>(event: T): boolean;
14
- /** Starts a process that persists and restores all encrypted content */
15
- export declare function persistEncryptedContent(eventStore: IEventStoreStreams, storage: EncryptedContentCache | Observable<EncryptedContentCache>): () => void;
15
+ /**
16
+ * Starts a process that persists and restores all encrypted content
17
+ * @param eventStore - The event store to listen to
18
+ * @param storage - The storage to use
19
+ * @param fallback - A function that will be called when the encrypted content is not found in storage
20
+ * @returns A function that can be used to stop the process
21
+ */
22
+ export declare function persistEncryptedContent(eventStore: IEventStoreStreams, storage: EncryptedContentCache | Observable<EncryptedContentCache>, fallback?: (event: NostrEvent) => any | Promise<any>): () => void;
@@ -15,9 +15,19 @@ export function isEncryptedContentFromCache(event) {
15
15
  return Reflect.has(event, EncryptedContentFromCacheSymbol);
16
16
  }
17
17
  const log = logger.extend("EncryptedContentCache");
18
- /** Starts a process that persists and restores all encrypted content */
19
- export function persistEncryptedContent(eventStore, storage) {
18
+ /**
19
+ * Starts a process that persists and restores all encrypted content
20
+ * @param eventStore - The event store to listen to
21
+ * @param storage - The storage to use
22
+ * @param fallback - A function that will be called when the encrypted content is not found in storage
23
+ * @returns A function that can be used to stop the process
24
+ */
25
+ export function persistEncryptedContent(eventStore, storage, fallback) {
20
26
  const storage$ = isObservable(storage) ? storage : of(storage);
27
+ // Get the encrypted content from storage or call the fallback
28
+ const getItem = async (storage, event) => {
29
+ return (await storage.getItem(event.id)) || (fallback ? await fallback(event) : null);
30
+ };
21
31
  // Restore encrypted content when it is inserted
22
32
  const restore = eventStore.insert$
23
33
  .pipe(
@@ -26,7 +36,7 @@ export function persistEncryptedContent(eventStore, storage) {
26
36
  // Get the encrypted content from storage
27
37
  mergeMap((event) =>
28
38
  // Wait for storage to be available
29
- storage$.pipe(switchMap((storage) => combineLatest([of(event), storage.getItem(event.id)])), catchError((error) => {
39
+ storage$.pipe(switchMap((storage) => combineLatest([of(event), getItem(storage, event)])), catchError((error) => {
30
40
  log(`Failed to restore encrypted content for ${event.id}`, error);
31
41
  return EMPTY;
32
42
  }))))
@@ -52,7 +62,7 @@ export function persistEncryptedContent(eventStore, storage) {
52
62
  // Get encrypted content from storage
53
63
  mergeMap(([gift, seal]) =>
54
64
  // Wait for storage to be available
55
- storage$.pipe(switchMap((storage) => combineLatest([of(gift), of(seal), storage.getItem(seal.id)])), catchError((error) => {
65
+ storage$.pipe(switchMap((storage) => combineLatest([of(gift), of(seal), getItem(storage, seal)])), catchError((error) => {
56
66
  log(`Failed to restore encrypted content for ${seal.id}`, error);
57
67
  return EMPTY;
58
68
  }))))
@@ -16,6 +16,8 @@ export function setEncryptedContentEncryptionMethod(kind, method) {
16
16
  /** Returns either nip04 or nip44 encryption methods depending on event kind */
17
17
  export function getEncryptedContentEncryptionMethods(kind, signer) {
18
18
  const method = EventContentEncryptionMethod[kind];
19
+ if (!method)
20
+ throw new Error(`Event kind ${kind} does not support encrypted content`);
19
21
  const encryption = signer[method];
20
22
  if (!encryption)
21
23
  throw new Error(`Signer does not support ${method} encryption`);
@@ -0,0 +1,14 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ export declare const INDEXABLE_TAGS: Set<string>;
3
+ export declare const EventIndexableTagsSymbol: unique symbol;
4
+ /**
5
+ * Returns the second index ( tag[1] ) of the first tag that matches the name
6
+ * If the event has any hidden tags they will be searched first
7
+ */
8
+ export declare function getTagValue<T extends {
9
+ kind: number;
10
+ tags: string[][];
11
+ content: string;
12
+ }>(event: T, name: string): string | undefined;
13
+ /** Returns a Set of tag names and values that are indexable */
14
+ export declare function getIndexableTags(event: NostrEvent): Set<string>;
@@ -0,0 +1,30 @@
1
+ import { getHiddenTags } from "./hidden-tags.js";
2
+ const LETTERS = "abcdefghijklmnopqrstuvwxyz";
3
+ export const INDEXABLE_TAGS = new Set((LETTERS + LETTERS.toUpperCase()).split(""));
4
+ export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
5
+ /**
6
+ * Returns the second index ( tag[1] ) of the first tag that matches the name
7
+ * If the event has any hidden tags they will be searched first
8
+ */
9
+ export function getTagValue(event, name) {
10
+ const hidden = getHiddenTags(event);
11
+ const hiddenValue = hidden?.find((t) => t[0] === name)?.[1];
12
+ if (hiddenValue)
13
+ return hiddenValue;
14
+ return event.tags.find((t) => t[0] === name)?.[1];
15
+ }
16
+ /** Returns a Set of tag names and values that are indexable */
17
+ export function getIndexableTags(event) {
18
+ let indexable = Reflect.get(event, EventIndexableTagsSymbol);
19
+ if (!indexable) {
20
+ const tags = new Set();
21
+ for (const tag of event.tags) {
22
+ if (tag.length >= 2 && tag[0].length === 1 && INDEXABLE_TAGS.has(tag[0])) {
23
+ tags.add(tag[0] + ":" + tag[1]);
24
+ }
25
+ }
26
+ indexable = tags;
27
+ Reflect.set(event, EventIndexableTagsSymbol, tags);
28
+ }
29
+ return indexable;
30
+ }
@@ -1,13 +1,14 @@
1
1
  import { NostrEvent, VerifiedEvent } from "nostr-tools";
2
2
  import { IEventStore } from "../event-store/interface.js";
3
+ /** A symbol on an event that marks which event store its part of */
4
+ export declare const EventStoreSymbol: unique symbol;
3
5
  export declare const EventUIDSymbol: unique symbol;
4
6
  export declare const ReplaceableAddressSymbol: unique symbol;
5
- export declare const EventIndexableTagsSymbol: unique symbol;
6
7
  export declare const FromCacheSymbol: unique symbol;
7
8
  export declare const ReplaceableIdentifierSymbol: unique symbol;
8
9
  /**
9
10
  * Checks if an object is a nostr event
10
- * NOTE: does not validation the signature on the event
11
+ * NOTE: does not validate the signature on the event
11
12
  */
12
13
  export declare function isEvent(event: any): event is NostrEvent;
13
14
  /**
@@ -28,17 +29,6 @@ export declare function getReplaceableAddress(event: NostrEvent): string;
28
29
  export declare function createReplaceableAddress(kind: number, pubkey: string, identifier?: string): string;
29
30
  /** @deprecated use createReplaceableAddress instead */
30
31
  export declare const getReplaceableUID: typeof createReplaceableAddress;
31
- /** Returns a Set of tag names and values that are indexable */
32
- export declare function getIndexableTags(event: NostrEvent): Set<string>;
33
- /**
34
- * Returns the second index ( tag[1] ) of the first tag that matches the name
35
- * If the event has any hidden tags they will be searched first
36
- */
37
- export declare function getTagValue<T extends {
38
- kind: number;
39
- tags: string[][];
40
- content: string;
41
- }>(event: T, name: string): string | undefined;
42
32
  /** Sets events verified flag without checking anything */
43
33
  export declare function fakeVerifyEvent(event: NostrEvent): event is VerifiedEvent;
44
34
  /** Marks an event as being from a cache */
@@ -1,17 +1,15 @@
1
1
  import { verifiedSymbol } from "nostr-tools";
2
2
  import { isAddressableKind, isReplaceableKind } from "nostr-tools/kinds";
3
- import { INDEXABLE_TAGS } from "../event-store/common.js";
4
- import { EventStoreSymbol } from "../event-store/event-store.js";
5
3
  import { getOrComputeCachedValue } from "./cache.js";
6
- import { getHiddenTags } from "./hidden-tags.js";
4
+ /** A symbol on an event that marks which event store its part of */
5
+ export const EventStoreSymbol = Symbol.for("event-store");
7
6
  export const EventUIDSymbol = Symbol.for("event-uid");
8
7
  export const ReplaceableAddressSymbol = Symbol.for("replaceable-address");
9
- export const EventIndexableTagsSymbol = Symbol.for("indexable-tags");
10
8
  export const FromCacheSymbol = Symbol.for("from-cache");
11
9
  export const ReplaceableIdentifierSymbol = Symbol.for("replaceable-identifier");
12
10
  /**
13
11
  * Checks if an object is a nostr event
14
- * NOTE: does not validation the signature on the event
12
+ * NOTE: does not validate the signature on the event
15
13
  */
16
14
  export function isEvent(event) {
17
15
  if (event === undefined || event === null)
@@ -64,32 +62,6 @@ export function createReplaceableAddress(kind, pubkey, identifier) {
64
62
  }
65
63
  /** @deprecated use createReplaceableAddress instead */
66
64
  export const getReplaceableUID = createReplaceableAddress;
67
- /** Returns a Set of tag names and values that are indexable */
68
- export function getIndexableTags(event) {
69
- let indexable = Reflect.get(event, EventIndexableTagsSymbol);
70
- if (!indexable) {
71
- const tags = new Set();
72
- for (const tag of event.tags) {
73
- if (tag.length >= 2 && tag[0].length === 1 && INDEXABLE_TAGS.has(tag[0])) {
74
- tags.add(tag[0] + ":" + tag[1]);
75
- }
76
- }
77
- indexable = tags;
78
- Reflect.set(event, EventIndexableTagsSymbol, tags);
79
- }
80
- return indexable;
81
- }
82
- /**
83
- * Returns the second index ( tag[1] ) of the first tag that matches the name
84
- * If the event has any hidden tags they will be searched first
85
- */
86
- export function getTagValue(event, name) {
87
- const hidden = getHiddenTags(event);
88
- const hiddenValue = hidden?.find((t) => t[0] === name)?.[1];
89
- if (hiddenValue)
90
- return hiddenValue;
91
- return event.tags.find((t) => t[0] === name)?.[1];
92
- }
93
65
  /** Sets events verified flag without checking anything */
94
66
  export function fakeVerifyEvent(event) {
95
67
  event[verifiedSymbol] = true;
@@ -121,7 +93,7 @@ export function getReplaceableIdentifier(event) {
121
93
  if (!isAddressableKind(event.kind))
122
94
  throw new Error("Event is not addressable");
123
95
  return getOrComputeCachedValue(event, ReplaceableIdentifierSymbol, () => {
124
- const d = getTagValue(event, "d");
96
+ const d = event.tags.find((t) => t[0] === "d")?.[1];
125
97
  if (d === undefined)
126
98
  throw new Error("Event missing identifier");
127
99
  return d;
@@ -1,6 +1,6 @@
1
- import { getTagValue } from "./event.js";
2
1
  import { getOrComputeCachedValue } from "./cache.js";
3
2
  import { unixNow } from "./time.js";
3
+ import { getTagValue } from "./event-tags.js";
4
4
  export const ExpirationTimestampSymbol = Symbol("expiration-timestamp");
5
5
  /** Returns the NIP-40 expiration timestamp for an event */
6
6
  export function getExpirationTimestamp(event) {
@@ -1,5 +1,5 @@
1
- import { getIndexableTags } from "./event.js";
2
1
  import equal from "fast-deep-equal";
2
+ import { getIndexableTags } from "./event-tags.js";
3
3
  /**
4
4
  * Copied from nostr-tools and modified to use getIndexableTags
5
5
  * @see https://github.com/nbd-wtf/nostr-tools/blob/a61cde77eacc9518001f11d7f67f1a50ae05fd80/filter.ts
@@ -16,6 +16,8 @@ export declare function getSealRumor(seal: NostrEvent): Rumor | undefined;
16
16
  * @throws {Error} If the author of the rumor event does not match the author of the seal
17
17
  */
18
18
  export declare function getGiftWrapRumor(gift: NostrEvent): Rumor | undefined;
19
+ /** Checks if an event is a rumor (normal event with "id" and no "sig") */
20
+ export declare function isRumor(event: any): event is Rumor;
19
21
  /** Returns if a gift-wrap event or gift-wrap seal is locked */
20
22
  export declare function isGiftWrapLocked(gift: NostrEvent): boolean;
21
23
  /** Unlocks and returns the unsigned seal event in a gift-wrap */
@@ -47,6 +47,19 @@ export function getGiftWrapRumor(gift) {
47
47
  return undefined;
48
48
  return getOrComputeCachedValue(gift, GiftWrapRumorSymbol, () => getSealRumor(seal));
49
49
  }
50
+ /** Checks if an event is a rumor (normal event with "id" and no "sig") */
51
+ export function isRumor(event) {
52
+ if (event === undefined || event === null)
53
+ return false;
54
+ return (event.id?.length === 64 &&
55
+ !("sig" in event) &&
56
+ typeof event.pubkey === "string" &&
57
+ event.pubkey.length === 64 &&
58
+ typeof event.content === "string" &&
59
+ Array.isArray(event.tags) &&
60
+ typeof event.created_at === "number" &&
61
+ event.created_at > 0);
62
+ }
50
63
  /** Returns if a gift-wrap event or gift-wrap seal is locked */
51
64
  export function isGiftWrapLocked(gift) {
52
65
  if (isEncryptedContentLocked(gift))
@@ -14,6 +14,7 @@ export * from "./emoji.js";
14
14
  export * from "./encrypted-content-cache.js";
15
15
  export * from "./encrypted-content.js";
16
16
  export * from "./encryption.js";
17
+ export * from "./event-tags.js";
17
18
  export * from "./event.js";
18
19
  export * from "./expiration.js";
19
20
  export * from "./external-id.js";
@@ -14,6 +14,7 @@ export * from "./emoji.js";
14
14
  export * from "./encrypted-content-cache.js";
15
15
  export * from "./encrypted-content.js";
16
16
  export * from "./encryption.js";
17
+ export * from "./event-tags.js";
17
18
  export * from "./event.js";
18
19
  export * from "./expiration.js";
19
20
  export * from "./external-id.js";
@@ -1,3 +1,5 @@
1
+ import { NostrEvent } from "nostr-tools";
2
+ import { Rumor } from "./gift-wraps.js";
1
3
  /**
2
4
  * Groups messages into bubble sets based on the pubkey and time
3
5
  *
@@ -9,3 +11,21 @@ export declare function groupMessageEvents<T extends {
9
11
  created_at: number;
10
12
  pubkey: string;
11
13
  }>(messages: T[], buffer?: number): T[][];
14
+ /**
15
+ * Returns all pubkeys of participants of a conversation
16
+ * @param participants - A conversation identifier (pubkey1:pubkey2:pubkey3), or a users pubkey, or a list of participant pubkeys, or a rumor message
17
+ * @returns The participants of the conversation
18
+ */
19
+ export declare function getConversationParticipants(participants: string | string[] | Rumor | NostrEvent): string[];
20
+ /**
21
+ * Creates a conversation identifier from a users pubkey and alist of correspondants
22
+ * @param participants - The participants of the conversation
23
+ * @returns The conversation identifier
24
+ */
25
+ export declare function createConversationIdentifier(...participants: (string | string[])[]): string;
26
+ /**
27
+ * Returns the conversation identifier for a wrapped direct message
28
+ * @param message - The NIP-17 Rumor or NIP-04 message event
29
+ * @returns The conversation identifier
30
+ */
31
+ export declare function getConversationIdentifierFromMessage(message: Rumor | NostrEvent): string;
@@ -1,3 +1,5 @@
1
+ import { kinds } from "nostr-tools";
2
+ import { isPTag } from "./tags.js";
1
3
  /**
2
4
  * Groups messages into bubble sets based on the pubkey and time
3
5
  *
@@ -17,3 +19,39 @@ export function groupMessageEvents(messages, buffer = 5 * 60) {
17
19
  }
18
20
  return groups;
19
21
  }
22
+ /**
23
+ * Returns all pubkeys of participants of a conversation
24
+ * @param participants - A conversation identifier (pubkey1:pubkey2:pubkey3), or a users pubkey, or a list of participant pubkeys, or a rumor message
25
+ * @returns The participants of the conversation
26
+ */
27
+ export function getConversationParticipants(participants) {
28
+ let participantList;
29
+ if (typeof participants === "string") {
30
+ participantList = participants.split(":");
31
+ }
32
+ else if (Array.isArray(participants)) {
33
+ participantList = participants;
34
+ }
35
+ else {
36
+ if (participants.kind !== kinds.EncryptedDirectMessage && participants.kind !== kinds.PrivateDirectMessage)
37
+ throw new Error("Can only get participants from direct message event (4, 14)");
38
+ participantList = [participants.pubkey, ...participants.tags.filter(isPTag).map((t) => t[1])];
39
+ }
40
+ return Array.from(new Set(participantList));
41
+ }
42
+ /**
43
+ * Creates a conversation identifier from a users pubkey and alist of correspondants
44
+ * @param participants - The participants of the conversation
45
+ * @returns The conversation identifier
46
+ */
47
+ export function createConversationIdentifier(...participants) {
48
+ return Array.from(new Set(participants.flat())).sort().join(":");
49
+ }
50
+ /**
51
+ * Returns the conversation identifier for a wrapped direct message
52
+ * @param message - The NIP-17 Rumor or NIP-04 message event
53
+ * @returns The conversation identifier
54
+ */
55
+ export function getConversationIdentifierFromMessage(message) {
56
+ return createConversationIdentifier(getConversationParticipants(message));
57
+ }
@@ -1,5 +1,5 @@
1
1
  import { getOrComputeCachedValue } from "./cache.js";
2
- import { getTagValue } from "./event.js";
2
+ import { getTagValue } from "./event-tags.js";
3
3
  import { getAddressPointerFromATag, getEventPointerFromETag, getProfilePointerFromPTag } from "./pointers.js";
4
4
  import { mergeRelaySets } from "./relays.js";
5
5
  import { isATag, isETag, isPTag } from "./tags.js";
@@ -1,22 +1,4 @@
1
1
  import { Rumor } from "./gift-wraps.js";
2
- /**
3
- * Returns all pubkeys of participants of a conversation
4
- * @param participants - The conversation identifier (pubkey1:pubkey2:pubkey3), or a users pubkey, or a list of participant pubkeys, or a rumor message
5
- * @returns The participants of the conversation
6
- */
7
- export declare function getConversationParticipants(participants: string | string[] | Rumor): string[];
8
- /**
9
- * Creates a conversation identifier from a users pubkey and alist of correspondants
10
- * @param participants - The participants of the conversation
11
- * @returns The conversation identifier
12
- */
13
- export declare function createConversationIdentifier(participants: string[]): string;
14
- /**
15
- * Returns the conversation identifier for a wrapped direct message
16
- * @param message - The message to get the conversation identifier for
17
- * @returns The conversation identifier
18
- */
19
- export declare function getConversationIdentifierFromMessage(message: Rumor): string;
20
2
  /** Returns the subject of a warpped direct message */
21
3
  export declare function getWrappedMessageSubject(message: Rumor): string | undefined;
22
4
  /** Returns the parent id of a wrapped direct message */
@@ -1,33 +1,4 @@
1
1
  import { getTagValue } from "./index.js";
2
- import { isPTag } from "./tags.js";
3
- /**
4
- * Returns all pubkeys of participants of a conversation
5
- * @param participants - The conversation identifier (pubkey1:pubkey2:pubkey3), or a users pubkey, or a list of participant pubkeys, or a rumor message
6
- * @returns The participants of the conversation
7
- */
8
- export function getConversationParticipants(participants) {
9
- return Array.from(new Set(typeof participants === "string"
10
- ? participants.split(":")
11
- : Array.isArray(participants)
12
- ? participants
13
- : [participants.pubkey, ...participants.tags.filter(isPTag).map((t) => t[1])]));
14
- }
15
- /**
16
- * Creates a conversation identifier from a users pubkey and alist of correspondants
17
- * @param participants - The participants of the conversation
18
- * @returns The conversation identifier
19
- */
20
- export function createConversationIdentifier(participants) {
21
- return Array.from(new Set(participants)).sort().join(":");
22
- }
23
- /**
24
- * Returns the conversation identifier for a wrapped direct message
25
- * @param message - The message to get the conversation identifier for
26
- * @returns The conversation identifier
27
- */
28
- export function getConversationIdentifierFromMessage(message) {
29
- return createConversationIdentifier(getConversationParticipants(message));
30
- }
31
2
  /** Returns the subject of a warpped direct message */
32
3
  export function getWrappedMessageSubject(message) {
33
4
  return getTagValue(message, "subject");
@@ -1,6 +1,6 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
- import { ParsedInvoice } from "./bolt11.js";
3
2
  import { AddressPointer, EventPointer } from "nostr-tools/nip19";
3
+ import { ParsedInvoice } from "./bolt11.js";
4
4
  export declare const ZapRequestSymbol: unique symbol;
5
5
  export declare const ZapSenderSymbol: unique symbol;
6
6
  export declare const ZapReceiverSymbol: unique symbol;
@@ -1,9 +1,9 @@
1
1
  import { kinds, nip57 } from "nostr-tools";
2
+ import { parseBolt11 } from "./bolt11.js";
2
3
  import { getOrComputeCachedValue } from "./cache.js";
3
- import { getTagValue } from "./event.js";
4
- import { isATag, isETag } from "./tags.js";
4
+ import { getTagValue } from "./event-tags.js";
5
5
  import { getAddressPointerFromATag, getEventPointerFromETag } from "./pointers.js";
6
- import { parseBolt11 } from "./bolt11.js";
6
+ import { isATag, isETag } from "./tags.js";
7
7
  export const ZapRequestSymbol = Symbol.for("zap-request");
8
8
  export const ZapSenderSymbol = Symbol.for("zap-sender");
9
9
  export const ZapReceiverSymbol = Symbol.for("zap-receiver");
@@ -1,7 +1,13 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
2
  import { Model } from "../event-store/interface.js";
3
- /** Returns all legacy direct messages in a conversation */
4
- export declare function LegacyMessagesConversation(self: string, corraspondant: string): Model<NostrEvent[]>;
3
+ /** A model that returns all legacy message groups (1-1) that a pubkey is participating in */
4
+ export declare function LegacyMessagesGroups(self: string): Model<{
5
+ id: string;
6
+ participants: string[];
7
+ lastMessage: NostrEvent;
8
+ }[]>;
9
+ /** Returns all legacy direct messages in a group */
10
+ export declare function LegacyMessagesGroup(self: string, corraspondant: string): Model<NostrEvent[]>;
5
11
  /** Returns an array of legacy messages that have replies */
6
12
  export declare function LegacyMessageThreads(self: string, corraspondant: string): Model<NostrEvent[]>;
7
13
  /** Returns all the legacy direct messages that are replies to a given message */
@@ -1,17 +1,41 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { getLegacyMessageCorraspondant, getLegacyMessageParent } from "../helpers/legacy-messages.js";
3
3
  import { map } from "rxjs";
4
- /** Returns all legacy direct messages in a conversation */
5
- export function LegacyMessagesConversation(self, corraspondant) {
6
- return (store) => store.timeline({
7
- kinds: [kinds.EncryptedDirectMessage],
8
- "#p": [self, corraspondant],
9
- authors: [self, corraspondant],
10
- });
4
+ import { getConversationIdentifierFromMessage, getConversationParticipants } from "../helpers/messages.js";
5
+ /** A model that returns all legacy message groups (1-1) that a pubkey is participating in */
6
+ export function LegacyMessagesGroups(self) {
7
+ return (store) => store.timeline({ kinds: [kinds.EncryptedDirectMessage], "#p": [self] }).pipe(map((messages) => {
8
+ const groups = {};
9
+ for (const message of messages) {
10
+ const id = getConversationIdentifierFromMessage(message);
11
+ if (!groups[id] || groups[id].created_at < message.created_at)
12
+ groups[id] = message;
13
+ }
14
+ return Object.values(groups).map((message) => ({
15
+ id: getConversationIdentifierFromMessage(message),
16
+ participants: getConversationParticipants(message),
17
+ lastMessage: message,
18
+ }));
19
+ }));
20
+ }
21
+ /** Returns all legacy direct messages in a group */
22
+ export function LegacyMessagesGroup(self, corraspondant) {
23
+ return (store) => store.timeline([
24
+ {
25
+ kinds: [kinds.EncryptedDirectMessage],
26
+ "#p": [self],
27
+ authors: [corraspondant],
28
+ },
29
+ {
30
+ kinds: [kinds.EncryptedDirectMessage],
31
+ "#p": [corraspondant],
32
+ authors: [self],
33
+ },
34
+ ]);
11
35
  }
12
36
  /** Returns an array of legacy messages that have replies */
13
37
  export function LegacyMessageThreads(self, corraspondant) {
14
- return (store) => store.model(LegacyMessagesConversation, self, corraspondant).pipe(map((messages) => messages.filter((message) =>
38
+ return (store) => store.model(LegacyMessagesGroup, self, corraspondant).pipe(map((messages) => messages.filter((message) =>
15
39
  // Only select messages that are not replies
16
40
  !getLegacyMessageParent(message) &&
17
41
  // Check if message has any replies
@@ -20,10 +44,18 @@ export function LegacyMessageThreads(self, corraspondant) {
20
44
  /** Returns all the legacy direct messages that are replies to a given message */
21
45
  export function LegacyMessageReplies(self, message) {
22
46
  const corraspondant = getLegacyMessageCorraspondant(message, self);
23
- return (store) => store.timeline({
24
- kinds: [kinds.EncryptedDirectMessage],
25
- "#p": [self, corraspondant],
26
- authors: [self, corraspondant],
27
- "#e": [message.id],
28
- });
47
+ return (store) => store.timeline([
48
+ {
49
+ kinds: [kinds.EncryptedDirectMessage],
50
+ "#p": [self],
51
+ authors: [corraspondant],
52
+ "#e": [message.id],
53
+ },
54
+ {
55
+ kinds: [kinds.EncryptedDirectMessage],
56
+ "#p": [corraspondant],
57
+ authors: [self],
58
+ "#e": [message.id],
59
+ },
60
+ ]);
29
61
  }
@@ -1,7 +1,7 @@
1
1
  import { NostrEvent } from "nostr-tools";
2
2
  import { AddressPointer, EventPointer } from "nostr-tools/nip19";
3
- import { ThreadReferences } from "../helpers/threading.js";
4
3
  import { Model } from "../event-store/interface.js";
4
+ import { ThreadReferences } from "../helpers/threading.js";
5
5
  export type Thread = {
6
6
  root?: ThreadItem;
7
7
  all: Map<string, ThreadItem>;
@@ -1,10 +1,11 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { isAddressableKind } from "nostr-tools/kinds";
3
3
  import { map } from "rxjs/operators";
4
- import { getNip10References, interpretThreadTags } from "../helpers/threading.js";
5
- import { getCoordinateFromAddressPointer, isAddressPointer, isEventPointer } from "../helpers/pointers.js";
6
- import { getEventUID, createReplaceableAddress, getTagValue, isEvent } from "../helpers/event.js";
7
4
  import { COMMENT_KIND } from "../helpers/comment.js";
5
+ import { getTagValue } from "../helpers/event-tags.js";
6
+ import { createReplaceableAddress, getEventUID, isEvent } from "../helpers/event.js";
7
+ import { getCoordinateFromAddressPointer, isAddressPointer, isEventPointer } from "../helpers/pointers.js";
8
+ import { getNip10References, interpretThreadTags } from "../helpers/threading.js";
8
9
  const defaultOptions = {
9
10
  kinds: [kinds.ShortTextNote],
10
11
  };
@@ -5,18 +5,24 @@ import { Rumor } from "../helpers/gift-wraps.js";
5
5
  * @param self - The pubkey of the user
6
6
  */
7
7
  export declare function WrappedMessagesModel(self: string): Model<Rumor[]>;
8
+ /** A model that returns all conversations that a pubkey is participating in */
9
+ export declare function WrappedMessagesGroups(self: string): Model<{
10
+ id: string;
11
+ participants: string[];
12
+ lastMessage: Rumor;
13
+ }[]>;
8
14
  /**
9
15
  * A model that returns all wrapped direct messages in a conversation
10
16
  * @param self - The pubkey of the user
11
17
  * @param participants - A conversation identifier or a list of participant pubkeys
12
18
  */
13
- export declare function WrappedMessagesConversation(self: string, participants: string | string[]): Model<Rumor[]>;
19
+ export declare function WrappedMessagesGroup(self: string, participants: string | string[]): Model<Rumor[]>;
14
20
  /**
15
21
  * Returns an array of root wrapped messages that have replies
16
22
  * @param self - The pubkey of the user
17
- * @param conversation - The conversation identifier
23
+ * @param participants - A conversation identifier or a list of participant pubkeys
18
24
  */
19
- export declare function WrappedMessageThreads(self: string, conversation: string): Model<Rumor[]>;
25
+ export declare function WrappedMessageThreads(self: string, participants: string | string[]): Model<Rumor[]>;
20
26
  /**
21
27
  * A model that returns all the gift wrapped direct messages that are replies to a given message
22
28
  * @param self - The pubkey of the user
@@ -1,7 +1,8 @@
1
1
  import { kinds } from "nostr-tools";
2
2
  import { map } from "rxjs";
3
3
  import { getGiftWrapRumor } from "../helpers/gift-wraps.js";
4
- import { createConversationIdentifier, getConversationIdentifierFromMessage, getConversationParticipants, getWrappedMessageParent, } from "../helpers/wrapped-messages.js";
4
+ import { createConversationIdentifier, getConversationIdentifierFromMessage, getConversationParticipants, } from "../helpers/messages.js";
5
+ import { getWrappedMessageParent } from "../helpers/wrapped-messages.js";
5
6
  import { watchEventsUpdates } from "../observable/watch-event-updates.js";
6
7
  /**
7
8
  * A model that returns all wrapped messages for a pubkey
@@ -15,19 +16,33 @@ export function WrappedMessagesModel(self) {
15
16
  map((rumors) => rumors
16
17
  .map((gift) => getGiftWrapRumor(gift))
17
18
  .filter((e) => !!e)
19
+ .filter((e) => e.kind === kinds.PrivateDirectMessage)
18
20
  .sort((a, b) => b.created_at - a.created_at)));
19
21
  }
22
+ /** A model that returns all conversations that a pubkey is participating in */
23
+ export function WrappedMessagesGroups(self) {
24
+ return (store) => store.model(WrappedMessagesModel, self).pipe(map((messages) => {
25
+ const groups = {};
26
+ for (const message of messages) {
27
+ const id = getConversationIdentifierFromMessage(message);
28
+ if (!groups[id] || groups[id].created_at < message.created_at)
29
+ groups[id] = message;
30
+ }
31
+ return Object.values(groups).map((message) => ({
32
+ id: getConversationIdentifierFromMessage(message),
33
+ participants: getConversationParticipants(message),
34
+ lastMessage: message,
35
+ }));
36
+ }));
37
+ }
20
38
  /**
21
39
  * A model that returns all wrapped direct messages in a conversation
22
40
  * @param self - The pubkey of the user
23
41
  * @param participants - A conversation identifier or a list of participant pubkeys
24
42
  */
25
- export function WrappedMessagesConversation(self, participants) {
43
+ export function WrappedMessagesGroup(self, participants) {
26
44
  // Get the conversation identifier include the users pubkey
27
- const identifier = createConversationIdentifier([
28
- self,
29
- ...(typeof participants === "string" ? getConversationParticipants(participants) : participants),
30
- ]);
45
+ const identifier = createConversationIdentifier(self, participants);
31
46
  return (store) => store.model(WrappedMessagesModel, self).pipe(
32
47
  // Only select direct messages for this conversation
33
48
  map((rumors) => rumors.filter((rumor) => rumor.kind === kinds.PrivateDirectMessage &&
@@ -37,10 +52,10 @@ export function WrappedMessagesConversation(self, participants) {
37
52
  /**
38
53
  * Returns an array of root wrapped messages that have replies
39
54
  * @param self - The pubkey of the user
40
- * @param conversation - The conversation identifier
55
+ * @param participants - A conversation identifier or a list of participant pubkeys
41
56
  */
42
- export function WrappedMessageThreads(self, conversation) {
43
- return (store) => store.model(WrappedMessagesConversation, self, conversation).pipe(
57
+ export function WrappedMessageThreads(self, participants) {
58
+ return (store) => store.model(WrappedMessagesGroup, self, participants).pipe(
44
59
  // Filter down messages to only include root messages that have replies
45
60
  map((rumors) => rumors.filter((rumor) =>
46
61
  // Only select root messages
@@ -55,7 +70,7 @@ export function WrappedMessageThreads(self, conversation) {
55
70
  */
56
71
  export function WrappedMessageReplies(self, message) {
57
72
  const conversation = getConversationIdentifierFromMessage(message);
58
- return (store) => store.model(WrappedMessagesConversation, self, conversation).pipe(
73
+ return (store) => store.model(WrappedMessagesGroup, self, conversation).pipe(
59
74
  // Only select replies to this message
60
75
  map((rumors) => rumors.filter((rumor) => getWrappedMessageParent(rumor) === message.id)));
61
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-core",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",