Files
bakery/src/modules/direct-message-manager.ts
hzrd149 9efd5019e3 format
2025-02-06 16:11:00 -06:00

170 lines
5.4 KiB
TypeScript

import { NostrEvent, kinds } from "nostr-tools";
import { SubCloser } from "nostr-tools/abstract-pool";
import { Subscription } from "nostr-tools/abstract-relay";
import { getInboxes } from "applesauce-core/helpers";
import { EventEmitter } from "events";
import { getRelaysFromContactList } from "@satellite-earth/core/helpers/nostr/contacts.js";
import { logger } from "../logger.js";
import type App from "../app/index.js";
import { arrayFallback } from "../helpers/array.js";
type EventMap = {
open: [string, string];
close: [string, string];
message: [NostrEvent];
};
/** handles sending and receiving direct messages */
export default class DirectMessageManager extends EventEmitter<EventMap> {
log = logger.extend("DirectMessageManager");
app: App;
private explicitRelays: string[] = [];
constructor(app: App) {
super();
this.app = app;
// Load profiles for participants when
// a conversation thread is opened
this.on("open", (a, b) => {
this.app.profileBook.loadProfile(a, this.app.addressBook.getOutboxes(a));
this.app.profileBook.loadProfile(b, this.app.addressBook.getOutboxes(b));
});
// emit a "message" event when a new kind4 message is detected
this.app.eventStore.on("event:inserted", (event) => {
if (event.kind === kinds.EncryptedDirectMessage) this.emit("message", event);
});
}
/** sends a DM event to the receivers inbox relays */
async forwardMessage(event: NostrEvent) {
if (event.kind !== kinds.EncryptedDirectMessage) return;
const addressedTo = event.tags.find((t) => t[0] === "p")?.[1];
if (!addressedTo) return;
// get users inboxes
let relays = await this.app.addressBook.loadInboxes(addressedTo);
if (!relays || relays.length === 0) {
// try to send the DM to the users legacy app relays
const contacts = await this.app.contactBook.loadContacts(addressedTo);
if (contacts) {
const appRelays = getRelaysFromContactList(contacts);
if (appRelays) relays = appRelays.filter((r) => r.write).map((r) => r.url);
}
}
if (!relays || relays.length === 0) {
// use fallback relays
relays = this.explicitRelays;
}
this.log(`Forwarding message to ${relays.length} relays`);
const results = await Promise.allSettled(this.app.pool.publish(relays, event));
return results;
}
private getConversationKey(a: string, b: string) {
if (a < b) return a + ":" + b;
else return b + ":" + a;
}
watching = new Map<string, Map<string, Subscription>>();
async watchInbox(pubkey: string) {
if (this.watching.has(pubkey)) return;
this.log(`Watching ${pubkey} inboxes for mail`);
const mailboxes = await this.app.addressBook.loadMailboxes(pubkey);
if (!mailboxes) {
this.log(`Failed to get ${pubkey} mailboxes`);
return;
}
const relays = arrayFallback(getInboxes(mailboxes), this.explicitRelays);
const subscriptions = new Map<string, Subscription>();
for (const url of relays) {
const subscribe = async () => {
const relay = await this.app.pool.ensureRelay(url);
const sub = relay.subscribe([{ kinds: [kinds.EncryptedDirectMessage], "#p": [pubkey] }], {
onevent: (event) => {
this.app.eventStore.addEvent(event);
},
onclose: () => {
// reconnect if we are still watching this pubkey
if (this.watching.has(pubkey)) {
this.log(`Reconnecting to ${relay.url} for ${pubkey} inbox DMs`);
setTimeout(() => subscribe(), 30_000);
}
},
});
subscriptions.set(relay.url, sub);
};
subscribe();
}
this.watching.set(pubkey, subscriptions);
}
stopWatchInbox(pubkey: string) {
const subs = this.watching.get(pubkey);
if (subs) {
this.watching.delete(pubkey);
for (const [_, sub] of subs) {
sub.close();
}
}
}
subscriptions = new Map<string, SubCloser>();
async openConversation(a: string, b: string) {
const key = this.getConversationKey(a, b);
if (this.subscriptions.has(key)) return;
const aMailboxes = await this.app.addressBook.loadMailboxes(a);
const bMailboxes = await this.app.addressBook.loadMailboxes(b);
// If inboxes for either user cannot be determined, either because nip65
// was not found, or nip65 had no listed read relays, fall back to explicit
const aInboxes = aMailboxes ? arrayFallback(getInboxes(aMailboxes), this.explicitRelays) : this.explicitRelays;
const bInboxes = bMailboxes ? arrayFallback(getInboxes(bMailboxes), this.explicitRelays) : this.explicitRelays;
const relays = new Set([...aInboxes, ...bInboxes]);
let events = 0;
const sub = this.app.pool.subscribeMany(
Array.from(relays),
[{ kinds: [kinds.EncryptedDirectMessage], authors: [a, b], "#p": [a, b] }],
{
onevent: (event) => {
events += +this.app.eventStore.addEvent(event);
},
oneose: () => {
if (events) this.log(`Found ${events} new messages`);
},
},
);
this.log(`Opened conversation ${key} on ${relays.size} relays`);
this.subscriptions.set(key, sub);
this.emit("open", a, b);
}
closeConversation(a: string, b: string) {
const key = this.getConversationKey(a, b);
const sub = this.subscriptions.get(key);
if (sub) {
sub.close();
this.subscriptions.delete(key);
this.emit("close", a, b);
}
}
}