import { ExternalStore, FeedCache, dedupe } from "@snort/shared"; import { RequestBuilder, NostrEvent, EventKind, SystemInterface, TaggedNostrEvent } from "@snort/system"; import { unwrap } from "SnortUtils"; import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat"; export class Nip29ChatSystem extends ExternalStore> implements ChatSystem { readonly #cache: FeedCache; constructor(cache: FeedCache) { super(); this.#cache = cache; } takeSnapshot(): Chat[] { return this.listChats(); } subscription(id: string) { const gs = id.split("/", 2); const rb = new RequestBuilder(`nip29:${id}`); const last = this.listChats().find(a => a.id === id)?.lastMessage; rb.withFilter() .relay(`wss://${gs[0]}`) .kinds([EventKind.SimpleChatMessage]) .tag("g", [`/${gs[1]}`]) .since(last); rb.withFilter() .relay(`wss://${gs[0]}`) .kinds([EventKind.SimpleChatMetadata]) .tag("d", [`/${gs[1]}`]); return rb; } async onEvent(evs: readonly TaggedNostrEvent[]) { const msg = evs.filter(a => a.kind === EventKind.SimpleChatMessage && a.tags.some(b => b[0] === "g")); if (msg.length > 0) { await this.#cache.bulkSet(msg); this.notifyChange(); } } listChats(): Chat[] { const allMessages = this.#nip29Chats(); const groups = dedupe( allMessages .map(a => a.tags.find(b => b[0] === "g")) .filter(a => a !== undefined) .map(a => unwrap(a)) .map(a => `${a[2]}${a[1]}`), ); return groups.map(g => { const [relay, channel] = g.split("/", 2); const messages = allMessages.filter( a => `${a.tags.find(b => b[0] === "g")?.[2]}${a.tags.find(b => b[0] === "g")?.[1]}` === g, ); const lastRead = lastReadInChat(g); return { type: ChatType.PublicGroupChat, id: g, title: `${relay}/${channel}`, unread: messages.reduce((acc, v) => (v.created_at > lastRead ? acc++ : acc), 0), lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0), messages: messages.map(m => ({ id: m.id, created_at: m.created_at, from: m.pubkey, tags: m.tags, needsDecryption: false, content: m.content, decrypt: async () => { return m.content; }, })), participants: [ { type: "generic", id: "", profile: { name: `${relay}/${channel}`, }, }, ], createMessage: async (msg, pub) => { return [ await pub.generic(eb => { return eb .kind(EventKind.SimpleChatMessage) .tag(["g", `/${channel}`, relay]) .content(msg); }), ]; }, sendMessage: async (ev, system: SystemInterface) => { ev.forEach(async a => await system.WriteOnceToRelay(`wss://${relay}`, a)); }, } as Chat; }); } #nip29Chats() { return this.#cache.snapshot().filter(a => a.kind === EventKind.SimpleChatMessage); } }