2023-06-30 14:05:57 +00:00
|
|
|
import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
|
2023-11-17 07:50:53 +00:00
|
|
|
import { NostrAccountContext, NostrEvent, NostrKind, Tags } from "../lib/nostr-ts/nostr.ts";
|
2023-11-03 13:09:13 +00:00
|
|
|
import { ConnectionPool } from "../lib/nostr-ts/relay-pool.ts";
|
2023-11-17 07:50:53 +00:00
|
|
|
import { compare, Encrypted_Event, getTags, Parsed_Event, prepareNostrImageEvent, Tag } from "../nostr.ts";
|
2023-08-28 17:58:05 +00:00
|
|
|
import { PublicKey } from "../lib/nostr-ts/key.ts";
|
2023-10-09 18:39:52 +00:00
|
|
|
import { prepareEncryptedNostrEvent } from "../lib/nostr-ts/event.ts";
|
|
|
|
import { DirectMessageGetter } from "../UI/app_update.tsx";
|
2023-11-17 07:50:53 +00:00
|
|
|
import { ChatMessage, parseContent } from "../UI/message.ts";
|
2023-10-09 21:02:13 +00:00
|
|
|
import { decodeInvitation, gmEventType } from "./gm.ts";
|
2023-11-20 09:14:17 +00:00
|
|
|
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
|
|
|
|
import { NewMessageListener } from "../UI/message-panel.tsx";
|
2023-06-30 14:05:57 +00:00
|
|
|
|
|
|
|
export async function sendDMandImages(args: {
|
|
|
|
sender: NostrAccountContext;
|
2023-07-11 09:19:57 +00:00
|
|
|
receiverPublicKey: PublicKey;
|
2023-06-30 14:05:57 +00:00
|
|
|
message: string;
|
|
|
|
files: Blob[];
|
|
|
|
lamport_timestamp: number;
|
|
|
|
pool: ConnectionPool;
|
|
|
|
tags: Tag[];
|
|
|
|
}) {
|
2023-10-07 23:23:53 +00:00
|
|
|
const { tags, sender, receiverPublicKey, message, files, lamport_timestamp, pool } = args;
|
2023-06-30 14:05:57 +00:00
|
|
|
console.log("sendDMandImages", message, files);
|
|
|
|
const eventsToSend: NostrEvent[] = [];
|
|
|
|
if (message.trim().length !== 0) {
|
|
|
|
// build the nostr event
|
|
|
|
const nostrEvent = await prepareEncryptedNostrEvent(
|
|
|
|
sender,
|
2023-09-19 19:38:38 +00:00
|
|
|
{
|
|
|
|
encryptKey: receiverPublicKey,
|
2023-10-07 23:23:53 +00:00
|
|
|
kind: NostrKind.DIRECT_MESSAGE,
|
2023-09-19 19:38:38 +00:00
|
|
|
tags: [
|
|
|
|
["p", receiverPublicKey.hex],
|
|
|
|
["lamport", String(lamport_timestamp)],
|
|
|
|
...tags,
|
|
|
|
],
|
|
|
|
content: message,
|
|
|
|
},
|
2023-06-30 14:05:57 +00:00
|
|
|
);
|
|
|
|
if (nostrEvent instanceof Error) {
|
|
|
|
return nostrEvent;
|
|
|
|
}
|
|
|
|
eventsToSend.push(nostrEvent);
|
|
|
|
}
|
|
|
|
for (let blob of files) {
|
2023-09-11 20:40:56 +00:00
|
|
|
const imgEvent = await prepareNostrImageEvent(
|
2023-06-30 14:05:57 +00:00
|
|
|
sender,
|
|
|
|
receiverPublicKey,
|
|
|
|
blob,
|
2023-10-07 23:23:53 +00:00
|
|
|
NostrKind.DIRECT_MESSAGE,
|
2023-06-30 14:05:57 +00:00
|
|
|
);
|
|
|
|
if (imgEvent instanceof Error) {
|
|
|
|
return imgEvent;
|
|
|
|
}
|
2023-10-21 11:29:47 +00:00
|
|
|
eventsToSend.push(imgEvent);
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
// send the event
|
|
|
|
for (const event of eventsToSend) {
|
2023-09-11 18:09:38 +00:00
|
|
|
const err = await pool.sendEvent(event);
|
|
|
|
if (err instanceof Error) {
|
|
|
|
return err;
|
|
|
|
}
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
2023-09-11 18:09:38 +00:00
|
|
|
return eventsToSend;
|
2023-06-30 14:05:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getAllEncryptedMessagesOf(
|
2023-07-11 09:19:57 +00:00
|
|
|
publicKey: PublicKey,
|
2023-06-30 14:05:57 +00:00
|
|
|
relay: ConnectionPool,
|
|
|
|
) {
|
|
|
|
const stream1 = getAllEncryptedMessagesSendBy(
|
|
|
|
publicKey,
|
|
|
|
relay,
|
|
|
|
);
|
|
|
|
const stream2 = getAllEncryptedMessagesReceivedBy(
|
|
|
|
publicKey,
|
|
|
|
relay,
|
|
|
|
);
|
|
|
|
return merge(stream1, stream2);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function* getAllEncryptedMessagesSendBy(
|
2023-07-11 09:19:57 +00:00
|
|
|
publicKey: PublicKey,
|
2023-06-30 14:05:57 +00:00
|
|
|
relay: ConnectionPool,
|
|
|
|
) {
|
|
|
|
let resp = await relay.newSub(
|
2023-08-03 09:13:16 +00:00
|
|
|
`getAllEncryptedMessagesSendBy`,
|
2023-06-30 14:05:57 +00:00
|
|
|
{
|
2023-07-11 09:19:57 +00:00
|
|
|
authors: [publicKey.hex],
|
2023-06-30 14:05:57 +00:00
|
|
|
kinds: [4],
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (resp instanceof Error) {
|
|
|
|
throw resp;
|
|
|
|
}
|
2023-08-28 17:58:05 +00:00
|
|
|
for await (const nostrMessage of resp.chan) {
|
2023-06-30 14:05:57 +00:00
|
|
|
yield nostrMessage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function* getAllEncryptedMessagesReceivedBy(
|
2023-07-11 09:19:57 +00:00
|
|
|
publicKey: PublicKey,
|
2023-06-30 14:05:57 +00:00
|
|
|
relay: ConnectionPool,
|
|
|
|
) {
|
|
|
|
let resp = await relay.newSub(
|
2023-08-03 09:13:16 +00:00
|
|
|
`getAllEncryptedMessagesReceivedBy`,
|
2023-06-30 14:05:57 +00:00
|
|
|
{
|
|
|
|
kinds: [4],
|
2023-07-11 09:19:57 +00:00
|
|
|
"#p": [publicKey.hex],
|
2023-06-30 14:05:57 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
if (resp instanceof Error) {
|
|
|
|
throw resp;
|
|
|
|
}
|
2023-08-28 17:58:05 +00:00
|
|
|
for await (const nostrMessage of resp.chan) {
|
2023-06-30 14:05:57 +00:00
|
|
|
yield nostrMessage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function merge<T>(...iters: AsyncIterable<T>[]) {
|
|
|
|
let merged = csp.chan<T>();
|
|
|
|
async function coroutine<T>(
|
|
|
|
source: AsyncIterable<T>,
|
|
|
|
destination: csp.Channel<T>,
|
|
|
|
) {
|
|
|
|
for await (let ele of source) {
|
|
|
|
if (destination.closed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let err = await destination.put(ele);
|
|
|
|
if (err instanceof csp.PutToClosedChannelError) {
|
|
|
|
// this means the merged channel was not closed when
|
|
|
|
// line 319 is called,
|
|
|
|
// but during waiting time of line 319, no consumer pops it and it was closed.
|
|
|
|
// This is normal semantics of channels
|
|
|
|
// so that it's fine to not throw it up to the call stack
|
|
|
|
// but then this ele has already been popped from the iter,
|
|
|
|
// it will be lost.
|
|
|
|
throw new Error("destination channel should not be closed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let iter of iters) {
|
|
|
|
coroutine(iter, merged);
|
|
|
|
}
|
|
|
|
return merged;
|
|
|
|
}
|
2023-10-09 18:39:52 +00:00
|
|
|
|
2023-11-20 09:14:17 +00:00
|
|
|
export class DirectedMessageController implements DirectMessageGetter, NewMessageListener {
|
2023-10-09 18:39:52 +00:00
|
|
|
constructor(
|
|
|
|
public readonly ctx: NostrAccountContext,
|
|
|
|
) {}
|
|
|
|
|
2023-11-20 09:14:17 +00:00
|
|
|
private readonly directed_messages = new Map<string, ChatMessage>();
|
|
|
|
private readonly new_message_chan = new Channel<ChatMessage>();
|
|
|
|
private readonly caster = new csp.Multicaster(this.new_message_chan);
|
2023-10-09 18:39:52 +00:00
|
|
|
|
|
|
|
// get the direct messages between me and this pubkey
|
2023-11-20 09:14:17 +00:00
|
|
|
public getChatMessages(pubkey: string): ChatMessage[] {
|
2023-10-09 21:02:13 +00:00
|
|
|
const messages = [];
|
|
|
|
for (const message of this.directed_messages.values()) {
|
|
|
|
if (is_DM_between(message.event, this.ctx.publicKey.hex, pubkey)) {
|
|
|
|
messages.push(message);
|
2023-10-09 18:39:52 +00:00
|
|
|
}
|
|
|
|
}
|
2023-10-09 21:02:13 +00:00
|
|
|
messages.sort((a, b) => compare(a.event, b.event));
|
2023-10-09 18:52:30 +00:00
|
|
|
return messages;
|
2023-10-09 18:39:52 +00:00
|
|
|
}
|
|
|
|
|
2023-11-20 09:14:17 +00:00
|
|
|
public getDirectMessageStream(pubkey: string): Channel<ChatMessage> {
|
|
|
|
const messages = new Channel<ChatMessage>();
|
|
|
|
(async () => {
|
|
|
|
for await (const message of this.caster.copy()) {
|
|
|
|
if (is_DM_between(message.event, this.ctx.publicKey.hex, pubkey)) {
|
|
|
|
const err = await messages.put(message);
|
|
|
|
if (err instanceof csp.PutToClosedChannelError) {
|
|
|
|
// the channel is closed by external code, most likely the caller
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// should never reach here, but doesn't matter
|
|
|
|
// because messages does not need to be closed
|
|
|
|
await messages.close();
|
|
|
|
})();
|
|
|
|
return messages;
|
|
|
|
}
|
|
|
|
|
|
|
|
async addEvent(
|
|
|
|
event:
|
|
|
|
| Parsed_Event<NostrKind.DIRECT_MESSAGE | NostrKind.Group_Message>
|
|
|
|
| NostrEvent<NostrKind.DIRECT_MESSAGE>,
|
|
|
|
) {
|
2023-10-09 21:02:13 +00:00
|
|
|
const kind = event.kind;
|
|
|
|
if (kind == NostrKind.Group_Message) {
|
|
|
|
const gmEvent = { ...event, kind };
|
2023-10-19 03:44:43 +00:00
|
|
|
const type = await gmEventType(this.ctx, gmEvent);
|
2023-10-09 21:02:13 +00:00
|
|
|
if (type == "gm_invitation") {
|
|
|
|
const invitation = await decodeInvitation(this.ctx, gmEvent);
|
|
|
|
if (invitation instanceof Error) {
|
|
|
|
return invitation;
|
|
|
|
}
|
2023-11-20 09:14:17 +00:00
|
|
|
const message: ChatMessage = {
|
2023-10-19 06:09:38 +00:00
|
|
|
type: "gm_invitation",
|
2023-10-09 21:02:13 +00:00
|
|
|
event: gmEvent,
|
2023-10-19 06:09:38 +00:00
|
|
|
invitation: invitation,
|
2023-10-09 21:02:13 +00:00
|
|
|
author: gmEvent.publicKey,
|
|
|
|
created_at: new Date(gmEvent.created_at * 1000),
|
|
|
|
lamport: gmEvent.parsedTags.lamport_timestamp,
|
2023-10-19 06:09:38 +00:00
|
|
|
content: gmEvent.content,
|
2023-11-20 09:14:17 +00:00
|
|
|
};
|
|
|
|
this.directed_messages.set(gmEvent.id, message);
|
|
|
|
/* do not await */ this.new_message_chan.put(message);
|
2023-10-09 21:02:13 +00:00
|
|
|
}
|
|
|
|
// else ignore
|
|
|
|
} else {
|
2023-11-20 09:14:17 +00:00
|
|
|
let parsedTags;
|
|
|
|
if ("parsedTags" in event) {
|
|
|
|
parsedTags = event.parsedTags;
|
|
|
|
} else {
|
|
|
|
parsedTags = getTags(event);
|
|
|
|
}
|
|
|
|
let publicKey;
|
|
|
|
if ("publicKey" in event) {
|
|
|
|
publicKey = event.publicKey;
|
|
|
|
} else {
|
|
|
|
publicKey = PublicKey.FromHex(event.pubkey);
|
|
|
|
if (publicKey instanceof Error) {
|
|
|
|
return publicKey;
|
|
|
|
}
|
|
|
|
}
|
2023-10-09 21:02:13 +00:00
|
|
|
const dmEvent = await parseDM(
|
|
|
|
{
|
|
|
|
...event,
|
|
|
|
kind,
|
|
|
|
},
|
|
|
|
this.ctx,
|
2023-11-20 09:14:17 +00:00
|
|
|
parsedTags,
|
|
|
|
publicKey,
|
2023-10-09 21:02:13 +00:00
|
|
|
);
|
|
|
|
if (dmEvent instanceof Error) {
|
|
|
|
return dmEvent;
|
|
|
|
}
|
|
|
|
const isImage = dmEvent.parsedTags.image;
|
2023-11-20 09:14:17 +00:00
|
|
|
let chatMessage: ChatMessage;
|
2023-10-09 21:02:13 +00:00
|
|
|
if (isImage) {
|
2023-10-21 11:29:47 +00:00
|
|
|
const imageBase64 = dmEvent.decryptedContent;
|
2023-11-20 09:14:17 +00:00
|
|
|
chatMessage = {
|
2023-10-09 21:02:13 +00:00
|
|
|
event: dmEvent,
|
|
|
|
author: dmEvent.publicKey,
|
|
|
|
content: imageBase64,
|
|
|
|
type: "image",
|
|
|
|
created_at: new Date(dmEvent.created_at * 1000),
|
|
|
|
lamport: dmEvent.parsedTags.lamport_timestamp,
|
2023-11-20 09:14:17 +00:00
|
|
|
};
|
2023-10-09 21:02:13 +00:00
|
|
|
} else {
|
2023-11-20 09:14:17 +00:00
|
|
|
chatMessage = {
|
2023-10-09 21:02:13 +00:00
|
|
|
event: dmEvent,
|
|
|
|
author: dmEvent.publicKey,
|
|
|
|
content: dmEvent.decryptedContent,
|
|
|
|
type: "text",
|
|
|
|
created_at: new Date(dmEvent.created_at * 1000),
|
|
|
|
lamport: dmEvent.parsedTags.lamport_timestamp,
|
2023-11-20 09:14:17 +00:00
|
|
|
};
|
2023-10-09 21:02:13 +00:00
|
|
|
}
|
2023-11-20 09:14:17 +00:00
|
|
|
this.directed_messages.set(event.id, chatMessage);
|
|
|
|
/* do not await */ this.new_message_chan.put(chatMessage);
|
2023-10-09 18:39:52 +00:00
|
|
|
}
|
|
|
|
}
|
2023-11-20 09:14:17 +00:00
|
|
|
|
|
|
|
onChange() {
|
|
|
|
return this.caster.copy();
|
|
|
|
}
|
2023-10-09 18:39:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function is_DM_between(event: NostrEvent, myPubkey: string, theirPubKey: string) {
|
|
|
|
if (event.pubkey == myPubkey) {
|
|
|
|
return getTags(event).p[0] == theirPubKey;
|
|
|
|
} else if (event.pubkey == theirPubKey) {
|
|
|
|
return getTags(event).p[0] == myPubkey;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2023-11-17 07:50:53 +00:00
|
|
|
|
|
|
|
async function parseDM(
|
|
|
|
event: NostrEvent<NostrKind.DIRECT_MESSAGE>,
|
|
|
|
ctx: NostrAccountContext,
|
|
|
|
parsedTags: Tags,
|
|
|
|
publicKey: PublicKey,
|
|
|
|
): Promise<Encrypted_Event | Error> {
|
|
|
|
const theOther = whoIamTalkingTo(event, ctx.publicKey);
|
|
|
|
if (theOther instanceof Error) {
|
|
|
|
return theOther;
|
|
|
|
}
|
|
|
|
const decrypted = await ctx.decrypt(theOther, event.content);
|
|
|
|
if (decrypted instanceof Error) {
|
|
|
|
return decrypted;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
...event,
|
|
|
|
kind: event.kind,
|
|
|
|
parsedTags,
|
|
|
|
publicKey,
|
|
|
|
decryptedContent: decrypted,
|
|
|
|
parsedContentItems: Array.from(parseContent(decrypted)),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-11-20 09:14:17 +00:00
|
|
|
export class InvalidEvent extends Error {
|
|
|
|
constructor(kind: NostrKind, message: string) {
|
|
|
|
super(`invliad event, expecting kind:${kind}, ${message}`);
|
|
|
|
this.name = "InvalidEvent";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-17 07:50:53 +00:00
|
|
|
export function whoIamTalkingTo(event: NostrEvent, myPublicKey: PublicKey) {
|
|
|
|
if (event.kind !== NostrKind.DIRECT_MESSAGE) {
|
|
|
|
console.log(event);
|
|
|
|
return new Error(`event ${event.id} is not a DM`);
|
|
|
|
}
|
|
|
|
// first asuming the other user is the sender
|
|
|
|
let whoIAmTalkingTo = event.pubkey;
|
|
|
|
const tags = getTags(event).p;
|
|
|
|
// if I am the sender
|
|
|
|
if (event.pubkey === myPublicKey.hex) {
|
|
|
|
if (tags.length === 1) {
|
|
|
|
const theirPubKey = tags[0];
|
|
|
|
whoIAmTalkingTo = theirPubKey;
|
|
|
|
return whoIAmTalkingTo;
|
|
|
|
} else if (tags.length === 0) {
|
|
|
|
console.log(event);
|
2023-11-20 09:14:17 +00:00
|
|
|
return new InvalidEvent(
|
|
|
|
NostrKind.DIRECT_MESSAGE,
|
2023-11-17 07:50:53 +00:00
|
|
|
`No p tag is found - Not a valid DM - id ${event.id}, kind ${event.kind}`,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return Error(`Multiple tag p: ${event}`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (tags.length === 1) {
|
|
|
|
const receiverPubkey = tags[0];
|
|
|
|
if (receiverPubkey !== myPublicKey.hex) {
|
|
|
|
return Error(
|
|
|
|
`Not my message, receiver is ${receiverPubkey}, sender is ${event.pubkey}, my key is ${myPublicKey}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else if (tags.length === 0) {
|
|
|
|
return Error(
|
|
|
|
`This is not a valid DM, id ${event.id}, kind ${event.kind}`,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return Error(`Multiple tag p: ${event}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// I am the receiver
|
|
|
|
return whoIAmTalkingTo;
|
|
|
|
}
|