Files
snort/packages/app/src/chat/nip29.ts
2023-09-05 15:17:51 +01:00

104 lines
3.1 KiB
TypeScript

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<Array<Chat>> implements ChatSystem {
readonly #cache: FeedCache<NostrEvent>;
constructor(cache: FeedCache<NostrEvent>) {
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);
}
}