blowater/group-chat.test.ts

274 lines
11 KiB
TypeScript
Raw Normal View History

import { assertEquals, fail } from "https://deno.land/std@0.176.0/testing/asserts.ts";
import { prepareCustomAppDataEvent, prepareEncryptedNostrEvent } from "./lib/nostr-ts/event.ts";
import { PrivateKey } from "./lib/nostr-ts/key.ts";
import { InMemoryAccountContext, NostrEvent, RelayResponse_REQ_Message } from "./lib/nostr-ts/nostr.ts";
import { Channel, closed } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { ConnectionPool } from "./lib/nostr-ts/relay.ts";
import { relays } from "./lib/nostr-ts/relay-list.test.ts";
Deno.test("group chat", async () => {
const pool = new ConnectionPool();
const err = await pool.addRelayURL(relays[1]);
if (err instanceof Error) fail(err.message);
// user A creates a group X,
// group X invites user B, with decryption key D
// group X invites user C, with decryption key D'
// user B sends to group X
const key_A = PrivateKey.Generate();
const key_B = PrivateKey.Generate();
const key_C = PrivateKey.Generate();
const group_decrypt_key = PrivateKey.Generate();
const group_key = PrivateKey.Generate().toPublicKey();
// User A
const a = (async () => {
const ctx_A = InMemoryAccountContext.New(key_A);
// Create the group
// const ctx_group = InMemoryAccountContext.New(group_key);
const group_decrypt_ctx_created_by_A = InMemoryAccountContext.New(group_decrypt_key);
// const createGroupChatEvent = await prepareCustomAppDataEvent(ctx_A, {
// type: "CreateGroupChat",
// groupAdminKey: ctx_group.privateKey.hex,
// groupMemberKey: ctx_member_created_by_A.privateKey.hex,
// });
// if (createGroupChatEvent instanceof Error) fail(createGroupChatEvent.message);
// Invite B
{
const groupInviationEvent = await prepareEncryptedNostrEvent(
ctx_A,
key_B.toPublicKey(),
4,
[
["p", key_B.toPublicKey().hex],
],
JSON.stringify({
decrypt_key: group_decrypt_ctx_created_by_A.privateKey.bech32,
public_key: group_key.bech32(),
}),
);
if (groupInviationEvent instanceof Error) fail(groupInviationEvent.message);
const err = await pool.sendEvent(groupInviationEvent);
if (err instanceof Error) fail(err.message);
}
// Send Message to Group
{
const groupMsg = await prepareEncryptedNostrEvent(
ctx_A,
group_decrypt_ctx_created_by_A.publicKey,
4,
[
["p", group_key.hex],
],
"hi all, this is A",
);
if (groupMsg instanceof Error) fail(groupMsg.message);
const err = await pool.sendEvent(groupMsg);
if (err instanceof Error) fail(err.message);
}
// receive from Group
const stream_group = await pool.newSub("a receives from group", {
"#p": [group_key.hex],
});
if (stream_group instanceof Error) fail(stream_group.message);
{
{
const groupMsg_1 = await next(stream_group.chan);
assertEquals(groupMsg_1.pubkey, ctx_A.publicKey.hex); // from self
}
{
const groupMsg_2 = await next(stream_group.chan);
assertEquals(groupMsg_2.pubkey, key_B.toPublicKey().hex); // from B
const content_2 = await group_decrypt_ctx_created_by_A.decrypt(
groupMsg_2.pubkey,
groupMsg_2.content,
);
if (content_2 instanceof Error) fail(content_2.message);
assertEquals(content_2, "hi all, this is B");
}
// invite C
{
const groupInviationEvent = await prepareEncryptedNostrEvent(
ctx_A,
key_C.toPublicKey(),
4,
[
["p", key_C.toPublicKey().hex],
],
JSON.stringify({
decrypt_key: group_decrypt_ctx_created_by_A.privateKey.bech32,
public_key: group_key.bech32(),
}),
);
if (groupInviationEvent instanceof Error) fail(groupInviationEvent.message);
const err = await pool.sendEvent(groupInviationEvent);
if (err instanceof Error) fail(err.message);
}
}
})();
// User B
const b = (async () => {
const ctx_B = InMemoryAccountContext.New(key_B);
// receive the invitation
const stream = await pool.newSub("b", { "#p": [ctx_B.publicKey.hex] });
if (stream instanceof Error) fail(stream.message);
const invitationEvent = await next(stream.chan);
const invitation = await ctx_B.decrypt(invitationEvent.pubkey, invitationEvent.content);
if (invitation instanceof Error) fail(invitation.message);
const decrypt_key = JSON.parse(invitation).decrypt_key;
console.log("group member private key:", invitation);
const ctx_decrypt_received_by_B = InMemoryAccountContext.New(
PrivateKey.FromString(decrypt_key) as PrivateKey,
);
assertEquals(ctx_decrypt_received_by_B.privateKey.hex, group_decrypt_key.hex);
const stream_group = await pool.newSub("b receives from group", {
"#p": [group_key.hex],
});
if (stream_group instanceof Error) fail(stream_group.message);
// receives from A
{
const groupMsg = await next(stream_group.chan);
assertEquals(groupMsg.pubkey, key_A.toPublicKey().hex); // make sure the event is from A
const content = await ctx_decrypt_received_by_B.decrypt(
groupMsg.pubkey,
groupMsg.content,
);
if (content instanceof Error) fail(content.message);
assertEquals(content, "hi all, this is A");
}
// send to group
{
const groupMsg = await prepareEncryptedNostrEvent(ctx_B, ctx_decrypt_received_by_B.publicKey, 4, [
["p", group_key.hex],
], "hi all, this is B");
if (groupMsg instanceof Error) fail(groupMsg.message);
const err = await pool.sendEvent(groupMsg);
if (err instanceof Error) fail(err.message);
}
// receive from self
{
const groupMsg_2 = await next(stream_group.chan);
assertEquals(groupMsg_2.pubkey, key_B.toPublicKey().hex); // make sure the event is from B
const content_2 = await ctx_decrypt_received_by_B.decrypt(
groupMsg_2.pubkey,
groupMsg_2.content,
);
if (content_2 instanceof Error) fail(content_2.message);
assertEquals(content_2, "hi all, this is B");
}
})();
// User C
const c = (async () => {
const ctx_C = InMemoryAccountContext.New(key_C);
// receive the invitation
{
const stream = await pool.newSub("c", { "#p": [ctx_C.publicKey.hex] });
if (stream instanceof Error) fail(stream.message);
const invitationEvent = await next(stream.chan);
const invitation = await ctx_C.decrypt(invitationEvent.pubkey, invitationEvent.content);
if (invitation instanceof Error) fail(invitation.message);
const decrypt_key = JSON.parse(invitation).decrypt_key;
const group_decrypt_ctx_received_by_C = InMemoryAccountContext.New(
PrivateKey.FromString(decrypt_key) as PrivateKey,
);
assertEquals(group_decrypt_ctx_received_by_C.privateKey.hex, group_decrypt_key.hex);
// receives from group
const stream_group = await pool.newSub("c receives from group", {
"#p": [group_key.hex],
});
if (stream_group instanceof Error) fail(stream_group.message);
{
// from A
{
const groupMsg = await next(stream_group.chan);
assertEquals(groupMsg.pubkey, key_A.toPublicKey().hex); // make sure the event is from A
const content = await group_decrypt_ctx_received_by_C.decrypt(
groupMsg.pubkey,
groupMsg.content,
);
if (content instanceof Error) fail(content.message);
assertEquals(content, "hi all, this is A");
}
// from B
{
const groupMsg_2 = await next(stream_group.chan);
assertEquals(groupMsg_2.pubkey, key_B.toPublicKey().hex); // make sure the event is from B
const content_2 = await group_decrypt_ctx_received_by_C.decrypt(
groupMsg_2.pubkey,
groupMsg_2.content,
);
if (content_2 instanceof Error) fail(content_2.message);
assertEquals(content_2, "hi all, this is B");
}
}
// send to group
{
const groupMsg = await prepareEncryptedNostrEvent(
ctx_C,
group_decrypt_ctx_received_by_C.publicKey,
4,
[
["p", group_key.hex],
],
"hi all, this is C",
);
if (groupMsg instanceof Error) fail(groupMsg.message);
const err = await pool.sendEvent(groupMsg);
if (err instanceof Error) fail(err.message);
}
// from C
{
const groupMsg = await next(stream_group.chan);
assertEquals(groupMsg.pubkey, key_C.toPublicKey().hex); // make sure the event is from A
const content = await group_decrypt_ctx_received_by_C.decrypt(
groupMsg.pubkey,
groupMsg.content,
);
if (content instanceof Error) fail(content.message);
assertEquals(content, "hi all, this is C");
}
}
})();
await Promise.all([a, b, c]);
await pool.close();
});
async function next(
chan: Channel<{
res: RelayResponse_REQ_Message;
url: string;
}>,
) {
while (true) {
const msg = await chan.pop();
if (msg == closed) {
fail();
}
console.log(msg);
if (msg.res.type == "EOSE") {
continue;
}
return msg.res.event;
}
fail();
}