blowater/database.ts

371 lines
11 KiB
TypeScript
Raw Normal View History

2023-07-12 08:45:55 +00:00
import {
CustomAppData,
CustomAppData_Event,
2023-09-12 14:51:27 +00:00
Encrypted_Event,
2023-07-12 08:45:55 +00:00
getTags,
2023-07-14 14:13:15 +00:00
Profile_Nostr_Event,
2023-07-12 08:45:55 +00:00
Tag,
2023-09-12 14:51:27 +00:00
Text_Note_Event,
2023-07-12 08:45:55 +00:00
} from "./nostr.ts";
2023-06-30 14:05:57 +00:00
import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { parseProfileData } from "./features/profile.ts";
import { parseContent } from "./UI/message.ts";
2023-09-12 14:51:27 +00:00
import { NostrAccountContext, NostrEvent, NostrKind, Tags, verifyEvent } from "./lib/nostr-ts/nostr.ts";
2023-08-28 17:58:05 +00:00
import { PublicKey } from "./lib/nostr-ts/key.ts";
2023-06-30 14:05:57 +00:00
export const NotFound = Symbol("Not Found");
const buffer_size = 2000;
2023-06-30 14:05:57 +00:00
export interface Indices {
readonly id?: string;
readonly create_at?: number;
readonly kind?: NostrKind;
readonly tags?: Tag[];
readonly pubkey?: string;
}
export interface EventsFilter {
filter(f: (e: NostrEvent) => boolean): Promise<NostrEvent[]>;
}
2023-09-12 14:51:27 +00:00
export interface EventRemover {
remove(id: string): Promise<void>;
}
export interface EventGetter {
get(keys: Indices): Promise<NostrEvent | undefined>;
}
export interface EventPutter {
put(e: NostrEvent): Promise<void>;
}
2023-09-12 14:51:27 +00:00
export type EventsAdapter = EventsFilter & EventRemover & EventGetter & EventPutter;
2023-09-12 14:51:27 +00:00
type Accepted_Event = Text_Note_Event | Encrypted_Event | Profile_Nostr_Event;
2023-07-11 09:49:58 +00:00
export class Database_Contextual_View {
2023-09-12 14:51:27 +00:00
private readonly sourceOfChange = csp.chan<Accepted_Event>(buffer_size);
private readonly caster = csp.multi<Accepted_Event>(this.sourceOfChange);
2023-06-30 14:05:57 +00:00
static async New(eventsAdapter: EventsAdapter, ctx: NostrAccountContext) {
2023-07-12 08:28:45 +00:00
const t = Date.now();
2023-09-12 14:51:27 +00:00
let kind4 = 0;
const allEvents = await eventsAdapter.filter((_) => {
if (_.kind == NostrKind.DIRECT_MESSAGE) {
kind4++;
2023-07-14 14:13:15 +00:00
}
2023-09-12 14:51:27 +00:00
return true;
});
console.log("Database_Contextual_View:onload", Date.now() - t, allEvents.length, kind4);
const initialEvents = await loadInitialData(
allEvents,
ctx,
eventsAdapter,
);
if (initialEvents instanceof Error) {
return initialEvents;
2023-07-14 14:13:15 +00:00
}
2023-09-12 14:51:27 +00:00
console.log("Database_Contextual_View:parsed", Date.now() - t);
2023-07-13 09:06:35 +00:00
const db = new Database_Contextual_View(
eventsAdapter,
2023-09-12 14:51:27 +00:00
initialEvents,
2023-07-13 09:06:35 +00:00
ctx,
);
2023-09-08 13:09:14 +00:00
console.log("Database_Contextual_View:New time spent", Date.now() - t);
2023-07-12 08:45:55 +00:00
return db;
2023-07-11 09:49:58 +00:00
}
private constructor(
private readonly eventsAdapter: EventsAdapter,
2023-09-12 14:51:27 +00:00
public readonly events: (Text_Note_Event | Encrypted_Event | Profile_Nostr_Event)[],
private readonly ctx: NostrAccountContext,
2023-09-12 14:51:27 +00:00
) {
for (const event of events) {
this.sourceOfChange.put(event);
}
}
2023-06-30 14:05:57 +00:00
2023-07-11 09:49:58 +00:00
public readonly getEvent = async (keys: Indices): Promise<NostrEvent | undefined> => {
2023-09-08 13:09:14 +00:00
const e = await this.eventsAdapter.get(keys);
return e;
2023-07-11 09:49:58 +00:00
};
public readonly filterEvents = (filter: (e: NostrEvent) => boolean) => {
2023-07-13 09:06:35 +00:00
return this.events.filter(filter);
2023-07-11 09:49:58 +00:00
};
async addEvent(event: NostrEvent) {
2023-09-12 14:51:27 +00:00
// check if the event exists
const storedEvent = await this.getEvent({ id: event.id });
if (storedEvent) { // event exist
2023-07-14 14:13:15 +00:00
return false;
}
2023-07-15 05:58:03 +00:00
2023-09-12 14:51:27 +00:00
const ok = await verifyEvent(event);
if (!ok) {
return ok;
}
2023-06-30 14:05:57 +00:00
2023-09-12 14:51:27 +00:00
// parse the event to desired format
const parsedEvent = await originalEventToParsedEvent(event, this.ctx, this.eventsAdapter);
if (parsedEvent instanceof Error) {
return parsedEvent;
}
if (parsedEvent == false) {
return parsedEvent;
}
2023-06-30 14:05:57 +00:00
2023-09-12 14:51:27 +00:00
// add event to database and notify subscribers
console.log("Database.addEvent", event.id);
this.events.push(parsedEvent);
await this.eventsAdapter.put(event);
/* not await */ this.sourceOfChange.put(parsedEvent);
return parsedEvent;
2023-06-30 14:05:57 +00:00
}
//////////////////
// On DB Change //
//////////////////
2023-09-12 14:51:27 +00:00
subscribe(filter?: (e: Accepted_Event) => boolean) {
2023-06-30 14:05:57 +00:00
const c = this.caster.copy();
2023-09-12 14:51:27 +00:00
const res = csp.chan<Accepted_Event>(buffer_size);
2023-06-30 14:05:57 +00:00
(async () => {
for await (const newE of c) {
2023-09-08 13:09:14 +00:00
if (filter == undefined || filter(newE)) {
2023-06-30 14:05:57 +00:00
const err = await res.put(newE);
if (err instanceof csp.PutToClosedChannelError) {
await c.close(
"onChange listern has been closed, closing the source",
);
}
}
}
await res.close(
"onChange source has been closed, closing the listener",
);
})();
return res;
}
}
export function whoIamTalkingTo(event: NostrEvent, myPublicKey: PublicKey) {
2023-06-30 14:05:57 +00:00
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) {
2023-06-30 14:05:57 +00:00
if (tags.length === 1) {
const theirPubKey = tags[0];
whoIAmTalkingTo = theirPubKey;
return whoIAmTalkingTo;
} else if (tags.length === 0) {
console.log(event);
return Error(
`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) {
2023-06-30 14:05:57 +00:00
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 {
console.log(event);
return Error(`Multiple tag p: ${event}`);
}
}
// I am the receiver
return whoIAmTalkingTo;
}
2023-09-12 14:51:27 +00:00
export async function parseCustomAppDataEvent(
event: NostrEvent<NostrKind.CustomAppData>,
ctx: NostrAccountContext,
) {
if (event.pubkey == ctx.publicKey.hex) { // if I am the author
2023-07-12 08:45:55 +00:00
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content);
if (decrypted instanceof Error) {
return decrypted;
}
let customAppData: CustomAppData;
try {
customAppData = JSON.parse(decrypted);
} catch (e) {
return e as Error;
}
if (customAppData) {
const e: CustomAppData_Event = {
content: event.content,
created_at: event.created_at,
id: event.id,
kind: event.kind,
pubkey: event.pubkey,
sig: event.sig,
tags: event.tags,
parsedTags: getTags(event),
customAppData: customAppData,
publicKey: ctx.publicKey,
};
return e;
}
}
}
2023-09-12 14:51:27 +00:00
async function loadInitialData(events: NostrEvent[], ctx: NostrAccountContext, eventsRemover: EventRemover) {
const initialEvents: Accepted_Event[] = [];
for await (const event of events) {
const pubkey = PublicKey.FromHex(event.pubkey);
if (pubkey instanceof Error) {
return pubkey;
}
const parsedEvent = await originalEventToParsedEvent(
{
...event,
kind: event.kind,
},
ctx,
eventsRemover,
);
if (parsedEvent instanceof Error) {
console.error(parsedEvent.message);
await eventsRemover.remove(event.id);
continue;
}
if (parsedEvent == false) {
continue;
}
initialEvents.push(parsedEvent);
}
return initialEvents;
}
async function originalEventToParsedEvent(
event: NostrEvent,
ctx: NostrAccountContext,
eventsRemover: EventRemover,
) {
const publicKey = PublicKey.FromHex(event.pubkey);
if (publicKey instanceof Error) {
return publicKey;
}
const parsedTags = getTags(event);
let e: Text_Note_Event | Encrypted_Event | Profile_Nostr_Event;
if (event.kind == NostrKind.CustomAppData || event.kind == NostrKind.DIRECT_MESSAGE) {
const _e = await originalEventToEncryptedEvent(
{
...event,
kind: event.kind,
},
ctx,
parsedTags,
publicKey,
eventsRemover,
);
if (_e instanceof Error || _e == false) {
return _e;
}
e = _e;
} else if (event.kind == NostrKind.META_DATA || event.kind == NostrKind.TEXT_NOTE) {
const _e = originalEventToUnencryptedEvent(
{
...event,
kind: event.kind,
},
parsedTags,
publicKey,
);
if (_e instanceof Error) {
return _e;
}
e = _e;
} else {
return new Error(`currently not accepting kind ${event.kind}`);
}
return e;
}
function originalEventToUnencryptedEvent(
event: NostrEvent<NostrKind.META_DATA | NostrKind.TEXT_NOTE>,
parsedTags: Tags,
publicKey: PublicKey,
) {
if (event.kind == NostrKind.META_DATA) {
const profileData = parseProfileData(event.content);
if (profileData instanceof Error) {
return profileData;
}
return {
...event,
kind: event.kind,
profile: profileData,
parsedTags,
publicKey,
};
}
{
return {
...event,
kind: event.kind,
parsedTags,
publicKey,
parsedContentItems: Array.from(parseContent(event.content)),
};
}
}
async function originalEventToEncryptedEvent(
event: NostrEvent,
ctx: NostrAccountContext,
parsedTags: Tags,
publicKey: PublicKey,
eventsAdapter: EventRemover,
): Promise<Encrypted_Event | Error | false> {
if (event.kind == NostrKind.CustomAppData) {
const _e = await parseCustomAppDataEvent({
...event,
kind: event.kind,
}, ctx);
if (_e == undefined) {
return false;
}
if (_e instanceof Error) {
console.log("Database:delete", event.id);
eventsAdapter.remove(event.id); // todo: remove
return _e;
}
return _e;
} else if (event.kind == NostrKind.DIRECT_MESSAGE) {
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(event.content)),
};
}
return false;
}