import { ExternalStore, dedupe } from "@snort/shared"; import { EventKind, NostrPrefix, encodeTLVEntries, TLVEntryType, TLVEntry, decodeTLV, NostrEvent } from "@snort/system"; import { GiftWrapCache } from "Cache/GiftWrapCache"; import { UnwrappedGift } from "Db"; import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat"; import { DefaultPowWorker } from "index"; export class Nip24ChatSystem extends ExternalStore> implements ChatSystem { #cache: GiftWrapCache; constructor(cache: GiftWrapCache) { super(); this.#cache = cache; this.#cache.hook(() => this.notifyChange(), "*"); } subscription() { // ignored return undefined; } onEvent() { // ignored } listChats(pk: string): Chat[] { const evs = this.#nip24Events(); const messages = evs.filter(a => a.to === pk); const chatId = (u: UnwrappedGift) => { const pTags = dedupe([...(u.tags ?? []).filter(a => a[0] === "p").map(a => a[1]), u.inner.pubkey]) .sort() .filter(a => a !== pk); return encodeTLVEntries( "chat24" as NostrPrefix, ...pTags.map( v => ({ value: v, type: TLVEntryType.Author, length: v.length, }) as TLVEntry, ), ); }; return dedupe(messages.map(a => chatId(a))).map(a => { const chatMessages = messages.filter(b => chatId(b) === a); return Nip24ChatSystem.createChatObj(a, chatMessages); }); } static createChatObj(id: string, messages: Array) { const last = lastReadInChat(id); const participants = decodeTLV(id) .filter(v => v.type === TLVEntryType.Author) .map(v => ({ type: "pubkey", id: v.value as string, })); const title = messages.reduce( (acc, v) => { const sbj = v.tags?.find(a => a[0] === "subject")?.[1]; if (v.created_at > acc.t && sbj) { acc.title = sbj; acc.t = v.created_at; } return acc; }, { t: 0, title: undefined, } as { t: number; title: string | undefined; }, ); return { type: ChatType.PrivateDirectMessage, id, title: title.title, unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0), lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0), participants, messages: messages.map(m => ({ id: m.id, created_at: m.created_at, from: m.inner.pubkey, tags: m.tags, content: "", needsDecryption: true, decrypt: async pub => { return await pub.decryptDm(m.inner); }, })), createMessage: async (msg, pub) => { const gossip = pub.createUnsigned(EventKind.ChatRumor, msg, eb => { for (const pt of participants) { eb.tag(["p", pt.id]); } return eb; }); const messages: Array> = []; const powTarget = 4 * 4; // 4-char zero for (const pt of participants) { const recvSealedN = pub.giftWrap(await pub.sealRumor(gossip, pt.id), pt.id, powTarget); messages.push(recvSealedN); } messages.push(pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey, powTarget, DefaultPowWorker)); return await Promise.all(messages); }, sendMessage: (ev, system) => { ev.forEach(a => system.BroadcastEvent(a)); }, } as Chat; } takeSnapshot(p: string): Chat[] { return this.listChats(p); } #nip24Events() { const sn = this.#cache.takeSnapshot(); return sn.filter(a => a.inner.kind === EventKind.SealedRumor); } }