feat: NIP-24

This commit is contained in:
2023-08-17 19:54:14 +01:00
parent 8500dee24f
commit f6a46e3523
51 changed files with 792 additions and 319 deletions

View File

@ -2,27 +2,46 @@ import { useSyncExternalStore } from "react";
import { Nip4ChatSystem } from "./nip4";
import { EventKind, EventPublisher, NostrEvent, RequestBuilder, SystemInterface, UserMetadata } from "@snort/system";
import { unwrap } from "@snort/shared";
import { Chats } from "Cache";
import { Chats, GiftsCache } from "Cache";
import { findTag, unixNow } from "SnortUtils";
import { Nip29ChatSystem } from "./nip29";
import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin";
import { Nip24ChatSystem } from "./nip24";
export enum ChatType {
DirectMessage = 1,
PublicGroupChat = 2,
PrivateGroupChat = 3,
PrivateDirectMessage = 4,
}
export interface ChatMessage {
id: string;
from: string;
created_at: number;
tags: Array<Array<string>>;
needsDecryption: boolean;
content: string;
decrypt: (pub: EventPublisher) => Promise<string>;
}
export interface ChatParticipant {
type: "pubkey" | "generic";
id: string;
profile?: UserMetadata;
}
export interface Chat {
type: ChatType;
id: string;
title?: string;
unread: number;
lastMessage: number;
messages: Array<NostrEvent>;
profile?: UserMetadata;
createMessage(msg: string, pub: EventPublisher): Promise<NostrEvent>;
sendMessage(ev: NostrEvent, system: SystemInterface): void | Promise<void>;
participants: Array<ChatParticipant>;
messages: Array<ChatMessage>;
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
}
export interface ChatSystem {
@ -37,6 +56,7 @@ export interface ChatSystem {
export const Nip4Chats = new Nip4ChatSystem(Chats);
export const Nip29Chats = new Nip29ChatSystem(Chats);
export const Nip24Chats = new Nip24ChatSystem(GiftsCache);
/**
* Extract the P tag of the event
@ -89,10 +109,18 @@ export function useNip29Chat() {
);
}
export function useNip24Chat() {
const { publicKey } = useLogin();
return useSyncExternalStore(
c => Nip24Chats.hook(c),
() => Nip24Chats.snapshot(publicKey)
);
}
export function useChatSystem() {
const nip4 = useNip4Chat();
const nip29 = useNip29Chat();
const nip24 = useNip24Chat();
const { muted, blocked } = useModeration();
return [...nip4, ...nip29].filter(a => !(muted.includes(a.id) || blocked.includes(a.id)));
return [...nip4, ...nip24].filter(a => !(muted.includes(a.id) || blocked.includes(a.id)));
}

View File

@ -0,0 +1,129 @@
import { ExternalStore, dedupe } from "@snort/shared";
import {
EventKind,
SystemInterface,
NostrPrefix,
encodeTLVEntries,
TLVEntryType,
TLVEntry,
decodeTLV,
} from "@snort/system";
import { GiftWrapCache } from "Cache/GiftWrapCache";
import { UnwrappedGift } from "Db";
import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat";
export class Nip24ChatSystem extends ExternalStore<Array<Chat>> 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<UnwrappedGift>) {
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: "",
}
);
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 = [];
for (const pt of participants) {
const recvSealedN = await pub.giftWrap(await pub.sealRumor(gossip, pt.id), pt.id);
messages.push(recvSealedN);
}
const sendSealed = await pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey);
return [...messages, sendSealed];
},
sendMessage: (ev, system: SystemInterface) => {
console.debug(ev);
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);
}
}

View File

View File

@ -57,19 +57,41 @@ export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
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,
createMessage: (msg, pub) => {
return pub.generic(eb => {
return eb
.kind(EventKind.SimpleChatMessage)
.tag(["g", `/${channel}`, relay])
.content(msg);
});
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: NostrEvent, system: SystemInterface) => {
await system.WriteOnceToRelay(`wss://${relay}`, ev);
sendMessage: async (ev, system: SystemInterface) => {
ev.forEach(async a => await system.WriteOnceToRelay(`wss://${relay}`, a));
},
} as Chat;
});

View File

@ -1,5 +1,14 @@
import { ExternalStore, FeedCache, dedupe } from "@snort/shared";
import { EventKind, NostrEvent, RequestBuilder, SystemInterface } from "@snort/system";
import {
EventKind,
NostrEvent,
NostrPrefix,
RequestBuilder,
SystemInterface,
TLVEntryType,
decodeTLV,
encodeTLVEntries,
} from "@snort/system";
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat, selfChat } from "chat";
import { debug } from "debug";
@ -40,27 +49,50 @@ export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSy
listChats(pk: string): Chat[] {
const myDms = this.#nip4Events();
return dedupe(myDms.map(a => inChatWith(a, pk))).map(a => {
const messages = myDms.filter(
b => (a === pk && selfChat(b, pk)) || (!selfChat(b, pk) && inChatWith(b, pk) === a)
);
const chatId = (a: NostrEvent) => {
return encodeTLVEntries("chat4" as NostrPrefix, {
type: TLVEntryType.Author,
value: inChatWith(a, pk),
length: 0,
});
};
return dedupe(myDms.map(chatId)).map(a => {
const messages = myDms.filter(b => chatId(b) === a);
return Nip4ChatSystem.createChatObj(a, messages);
});
}
static createChatObj(id: string, messages: Array<NostrEvent>) {
const last = lastReadInChat(id);
const pk = decodeTLV(id).find(a => a.type === TLVEntryType.Author)?.value as string;
return {
type: ChatType.DirectMessage,
id,
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),
messages,
createMessage: (msg, pub) => {
return pub.sendDm(msg, id);
participants: [
{
type: "pubkey",
id: pk,
},
],
messages: messages.map(m => ({
id: m.id,
created_at: m.created_at,
from: m.pubkey,
tags: m.tags,
content: "",
needsDecryption: true,
decrypt: async pub => {
return await pub.decryptDm(m);
},
})),
createMessage: async (msg, pub) => {
return [await pub.sendDm(msg, pk)];
},
sendMessage: (ev: NostrEvent, system: SystemInterface) => {
system.BroadcastEvent(ev);
sendMessage: (ev, system: SystemInterface) => {
ev.forEach(a => system.BroadcastEvent(a));
},
} as Chat;
}