2023-10-07 23:23:53 +00:00
|
|
|
import { z } from "https://esm.sh/zod@3.22.4";
|
2023-09-24 21:56:29 +00:00
|
|
|
import { ConversationLists } from "./UI/conversation-list.ts";
|
2023-10-07 10:59:06 +00:00
|
|
|
import { parseJSON } from "./features/profile.ts";
|
|
|
|
import { prepareEncryptedNostrEvent } from "./lib/nostr-ts/event.ts";
|
2023-09-24 21:56:29 +00:00
|
|
|
import { PrivateKey, PublicKey } from "./lib/nostr-ts/key.ts";
|
|
|
|
import { InMemoryAccountContext, NostrAccountContext, NostrEvent, NostrKind } from "./lib/nostr-ts/nostr.ts";
|
2023-10-07 23:23:53 +00:00
|
|
|
import { GroupMessageGetter } from "./UI/app_update.tsx";
|
|
|
|
import { getTags } from "./nostr.ts";
|
|
|
|
import { ChatMessage } from "./UI/message.ts";
|
|
|
|
|
|
|
|
export type GroupMessage = {
|
|
|
|
event: NostrEvent<NostrKind.Group_Message>;
|
|
|
|
};
|
2023-09-24 21:56:29 +00:00
|
|
|
|
|
|
|
export type GroupChatCreation = {
|
2023-10-07 10:59:06 +00:00
|
|
|
cipherKey: InMemoryAccountContext;
|
|
|
|
groupKey: InMemoryAccountContext;
|
|
|
|
};
|
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
export class GroupChatController implements GroupMessageGetter {
|
2023-09-24 21:56:29 +00:00
|
|
|
created_groups = new Map<string, GroupChatCreation>();
|
2023-10-07 23:23:53 +00:00
|
|
|
messages = new Map<string, ChatMessage[]>();
|
2023-09-24 21:56:29 +00:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
private readonly ctx: NostrAccountContext,
|
|
|
|
private readonly conversationLists: ConversationLists,
|
|
|
|
) {}
|
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
getGroupMessages(publicKey: string): ChatMessage[] {
|
|
|
|
const msgs = this.messages.get(publicKey);
|
|
|
|
return msgs ? msgs : [];
|
|
|
|
}
|
|
|
|
|
2023-10-07 10:59:06 +00:00
|
|
|
async encodeCreationToNostrEvent(groupCreation: GroupChatCreation) {
|
2023-10-03 15:15:45 +00:00
|
|
|
const event = prepareEncryptedNostrEvent(this.ctx, {
|
|
|
|
encryptKey: this.ctx.publicKey,
|
2023-10-07 10:59:06 +00:00
|
|
|
kind: NostrKind.Group_Message,
|
2023-10-03 15:15:45 +00:00
|
|
|
tags: [],
|
2023-10-07 10:59:06 +00:00
|
|
|
content: JSON.stringify({
|
2023-10-07 23:23:53 +00:00
|
|
|
type: "gm_creation",
|
2023-10-07 10:59:06 +00:00
|
|
|
cipherKey: groupCreation.cipherKey.privateKey.bech32,
|
|
|
|
groupKey: groupCreation.groupKey.privateKey.bech32,
|
|
|
|
}),
|
2023-09-24 21:56:29 +00:00
|
|
|
});
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
|
2023-10-03 15:15:45 +00:00
|
|
|
createGroupChat() {
|
|
|
|
const groupChatCreation: GroupChatCreation = {
|
2023-10-07 10:59:06 +00:00
|
|
|
cipherKey: InMemoryAccountContext.New(PrivateKey.Generate()),
|
|
|
|
groupKey: InMemoryAccountContext.New(PrivateKey.Generate()),
|
2023-10-03 15:15:45 +00:00
|
|
|
};
|
2023-10-07 10:59:06 +00:00
|
|
|
this.created_groups.set(groupChatCreation.groupKey.publicKey.bech32(), groupChatCreation);
|
|
|
|
return groupChatCreation;
|
2023-09-24 21:56:29 +00:00
|
|
|
}
|
|
|
|
|
2023-10-07 10:59:06 +00:00
|
|
|
async addEvent(event: NostrEvent<NostrKind.Group_Message>) {
|
2023-10-07 23:23:53 +00:00
|
|
|
if (isCreation(event)) {
|
|
|
|
return await this.handleCreation(event);
|
|
|
|
} else if (isMessage(event)) {
|
|
|
|
return await this.handleMessage(event);
|
|
|
|
} else {
|
|
|
|
console.log(GroupChatController.name, "ignore", event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleMessage(event: NostrEvent<NostrKind.Group_Message>) {
|
|
|
|
const groupAddr = getTags(event).p[0];
|
|
|
|
const decryptedContent = await this.ctx.decrypt(groupAddr, event.content);
|
2023-10-07 10:59:06 +00:00
|
|
|
if (decryptedContent instanceof Error) {
|
2023-10-07 23:23:53 +00:00
|
|
|
return decryptedContent;
|
|
|
|
}
|
|
|
|
|
|
|
|
const json = parseJSON<unknown>(decryptedContent);
|
|
|
|
if (json instanceof Error) {
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
|
|
|
const author = PublicKey.FromHex(event.pubkey);
|
|
|
|
if (author instanceof Error) {
|
|
|
|
return author;
|
2023-10-07 10:59:06 +00:00
|
|
|
}
|
2023-10-07 23:23:53 +00:00
|
|
|
const message = z.object({
|
|
|
|
type: z.string(),
|
|
|
|
text: z.string(),
|
|
|
|
}).parse(json);
|
|
|
|
const chatMessage: ChatMessage = {
|
|
|
|
event: event,
|
|
|
|
author: author,
|
|
|
|
content: message.text,
|
|
|
|
created_at: new Date(event.created_at * 1000),
|
|
|
|
lamport: getTags(event).lamport_timestamp,
|
|
|
|
type: "text",
|
|
|
|
};
|
|
|
|
|
|
|
|
const messages = this.messages.get(groupAddr);
|
|
|
|
if (messages) {
|
|
|
|
messages.push(chatMessage);
|
|
|
|
} else {
|
|
|
|
this.messages.set(groupAddr, [chatMessage]);
|
2023-10-07 10:59:06 +00:00
|
|
|
}
|
2023-10-07 23:23:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async handleCreation(event: NostrEvent<NostrKind.Group_Message>) {
|
|
|
|
const decryptedContent = await this.ctx.decrypt(event.pubkey, event.content);
|
|
|
|
if (decryptedContent instanceof Error) {
|
|
|
|
return decryptedContent;
|
2023-10-07 10:59:06 +00:00
|
|
|
}
|
2023-10-07 23:23:53 +00:00
|
|
|
|
|
|
|
const json = parseJSON<unknown>(decryptedContent);
|
|
|
|
if (json instanceof Error) {
|
|
|
|
return json;
|
2023-10-07 10:59:06 +00:00
|
|
|
}
|
2023-10-04 15:07:40 +00:00
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
try {
|
|
|
|
const schema = z.object({
|
|
|
|
type: z.string(),
|
|
|
|
});
|
|
|
|
const content = schema.parse(json);
|
|
|
|
if (content.type == "gm_creation") {
|
|
|
|
const schema = z.object({
|
|
|
|
type: z.string(),
|
|
|
|
cipherKey: z.string(),
|
|
|
|
groupKey: z.string(),
|
|
|
|
});
|
|
|
|
const content = schema.parse(json);
|
|
|
|
const groupKey = PrivateKey.FromString(content.groupKey);
|
|
|
|
if (groupKey instanceof Error) {
|
|
|
|
return groupKey;
|
|
|
|
}
|
|
|
|
const cipherKey = PrivateKey.FromString(content.cipherKey);
|
|
|
|
if (cipherKey instanceof Error) {
|
|
|
|
return cipherKey;
|
|
|
|
}
|
|
|
|
|
|
|
|
const groupChatCreation = {
|
|
|
|
groupKey: InMemoryAccountContext.New(groupKey),
|
|
|
|
cipherKey: InMemoryAccountContext.New(cipherKey),
|
|
|
|
};
|
|
|
|
this.created_groups.set(groupKey.toPublicKey().bech32(), groupChatCreation);
|
2023-10-04 15:07:40 +00:00
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
this.conversationLists.addGroupCreation(groupChatCreation);
|
|
|
|
} else if (content.type == "gm_message") {
|
|
|
|
console.log(content);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
return e as Error;
|
|
|
|
}
|
2023-09-24 21:56:29 +00:00
|
|
|
}
|
|
|
|
|
2023-10-07 23:23:53 +00:00
|
|
|
getGroupChatCtx(group_addr: PublicKey): InMemoryAccountContext | undefined {
|
|
|
|
const invitation = this.created_groups.get(group_addr.bech32());
|
|
|
|
if (invitation == undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return invitation.groupKey;
|
|
|
|
}
|
2023-09-24 21:56:29 +00:00
|
|
|
|
2023-10-04 15:07:40 +00:00
|
|
|
getGroupAdminCtx(group_addr: PublicKey): InMemoryAccountContext | undefined {
|
|
|
|
const creation = this.created_groups.get(group_addr.bech32());
|
|
|
|
if (!creation) {
|
|
|
|
return;
|
|
|
|
}
|
2023-10-07 10:59:06 +00:00
|
|
|
return creation.groupKey;
|
2023-10-04 15:07:40 +00:00
|
|
|
}
|
2023-09-24 21:56:29 +00:00
|
|
|
}
|
2023-10-07 23:23:53 +00:00
|
|
|
|
|
|
|
function isCreation(event: NostrEvent<NostrKind.Group_Message>) {
|
|
|
|
return event.tags.length == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function isMessage(event: NostrEvent<NostrKind.Group_Message>) {
|
|
|
|
return event.tags.length != 0;
|
|
|
|
}
|