blowater/database.ts

429 lines
16 KiB
TypeScript
Raw Normal View History

2023-07-12 08:45:55 +00:00
import { PublicKey } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts";
2023-06-30 14:05:57 +00:00
import {
DecryptionFailure,
decryptNostrEvent,
NostrAccountContext,
NostrEvent,
NostrKind,
RelayResponse_REQ_Message,
} from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts";
2023-07-12 08:45:55 +00:00
import {
CustomAppData,
CustomAppData_Event,
2023-07-12 08:45:55 +00:00
getTags,
PlainText_Nostr_Event,
2023-07-14 14:13:15 +00:00
Profile_Nostr_Event,
2023-07-12 08:45:55 +00:00
Tag,
} from "./nostr.ts";
2023-06-30 14:05:57 +00:00
import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
2023-07-11 09:49:58 +00:00
import { DexieDatabase } from "./UI/dexie-db.ts";
import { parseProfileData } from "./features/profile.ts";
import { parseContent } from "./UI/message.ts";
import { NoteID } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nip19.ts";
2023-06-30 14:05:57 +00:00
export const NotFound = Symbol("Not Found");
const buffer_size = 1000;
export interface Indices {
readonly id?: string;
readonly create_at?: number;
readonly kind?: NostrKind;
readonly tags?: Tag[];
readonly pubkey?: string;
}
2023-07-11 09:49:58 +00:00
export class Database_Contextual_View {
2023-07-14 14:13:15 +00:00
private readonly sourceOfChange = csp.chan<
PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event
2023-07-14 14:13:15 +00:00
>(buffer_size);
private readonly caster = csp.multi<PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event>(
2023-07-14 14:13:15 +00:00
this.sourceOfChange,
);
2023-06-30 14:05:57 +00:00
static async New(database: DexieDatabase, ctx: NostrAccountContext) {
2023-07-12 08:28:45 +00:00
const t = Date.now();
2023-07-14 14:13:15 +00:00
2023-07-13 10:29:32 +00:00
const onload: (NostrEvent)[] = await database.events.filter(
2023-07-12 08:45:55 +00:00
(e: NostrEvent) => {
return e.kind != NostrKind.CustomAppData;
},
).toArray();
const cache: (PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event)[] = [];
2023-07-14 14:13:15 +00:00
for (const event of onload) {
2023-07-15 05:58:03 +00:00
const pubkey = PublicKey.FromHex(event.pubkey);
if (pubkey instanceof Error) {
console.error(pubkey);
continue;
}
2023-07-14 14:13:15 +00:00
switch (event.kind) {
case NostrKind.META_DATA:
{
const profileData = parseProfileData(event.content);
if (profileData instanceof Error) {
console.error(profileData);
console.log("Database:delete", event.id);
database.events.delete(event.id);
continue;
}
const e: Profile_Nostr_Event = {
...event,
kind: event.kind,
parsedTags: getTags(event),
profile: profileData,
2023-07-15 05:58:03 +00:00
publicKey: pubkey,
2023-07-14 14:13:15 +00:00
};
cache.push(e);
}
break;
case NostrKind.TEXT_NOTE:
case NostrKind.RECOMMED_SERVER:
case NostrKind.CONTACTS:
case NostrKind.DIRECT_MESSAGE:
case NostrKind.DELETE:
{
const e: PlainText_Nostr_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),
2023-07-15 05:58:03 +00:00
publicKey: pubkey,
parsedContentItems: Array.from(parseContent(event.content)),
2023-07-14 14:13:15 +00:00
};
cache.push(e);
}
break;
case NostrKind.CustomAppData:
// ignore
break;
}
}
2023-07-13 09:06:35 +00:00
const db = new Database_Contextual_View(
database,
2023-07-13 10:29:32 +00:00
cache,
2023-07-13 09:06:35 +00:00
ctx,
);
2023-07-12 08:45:55 +00:00
(async () => {
let tt = 0;
const events: NostrEvent<NostrKind.CustomAppData>[] = await database.events.filter(
(e: NostrEvent) => {
return e.kind == NostrKind.CustomAppData;
},
).toArray();
2023-07-12 08:45:55 +00:00
for (const event of events) {
2023-07-15 05:58:03 +00:00
const pubkey = PublicKey.FromHex(event.pubkey);
if (pubkey instanceof Error) {
console.error(pubkey);
continue;
}
2023-07-12 08:45:55 +00:00
if (event.kind == NostrKind.CustomAppData) {
const e = await transformEvent(event, ctx);
2023-07-15 05:58:03 +00:00
2023-07-12 08:45:55 +00:00
if (e == undefined) {
continue;
}
if (e instanceof Error) {
console.log("Database:delete", event.id);
database.events.delete(event.id);
continue;
}
cache.push(e);
await db.sourceOfChange.put(e);
} else {
2023-07-13 09:06:35 +00:00
const e: PlainText_Nostr_Event = {
2023-07-12 08:45:55 +00:00
content: event.content,
created_at: event.created_at,
id: event.id,
kind: event.kind,
pubkey: event.pubkey,
sig: event.sig,
tags: event.tags,
2023-07-13 09:06:35 +00:00
parsedTags: getTags(event),
2023-07-15 05:58:03 +00:00
publicKey: pubkey,
parsedContentItems: Array.from(parseContent(event.content)),
2023-07-12 08:45:55 +00:00
};
cache.push(e);
await db.sourceOfChange.put(e);
}
}
2023-07-12 08:45:55 +00:00
console.log("Database_Contextual_View:transformEvent", tt);
})();
2023-07-12 08:28:45 +00:00
console.log("Database_Contextual_View:New", Date.now() - t);
2023-07-12 08:45:55 +00:00
return db;
2023-07-11 09:49:58 +00:00
}
2023-06-30 14:05:57 +00:00
constructor(
2023-07-11 09:49:58 +00:00
private readonly database: DexieDatabase,
public readonly events: (PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event)[],
private readonly ctx: NostrAccountContext,
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> => {
return this.database.events.get(keys);
};
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
};
2023-07-14 14:13:15 +00:00
async addEvent(event: NostrEvent): Promise<boolean> {
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
// todo: verify event
const pubkey = PublicKey.FromHex(event.pubkey);
if (pubkey instanceof Error) {
console.error(pubkey);
return false;
}
2023-06-30 14:05:57 +00:00
console.log("Database.addEvent", event.id);
let e: PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event;
2023-07-12 08:45:55 +00:00
if (event.kind == NostrKind.CustomAppData) {
2023-07-14 14:13:15 +00:00
const _e = await transformEvent({
2023-07-12 08:45:55 +00:00
content: event.content,
created_at: event.created_at,
id: event.id,
kind: event.kind,
pubkey: event.pubkey,
sig: event.sig,
tags: event.tags,
}, this.ctx);
2023-07-14 14:13:15 +00:00
if (_e == undefined) {
return false;
2023-07-12 08:45:55 +00:00
}
2023-07-14 14:13:15 +00:00
if (_e instanceof Error) {
console.log("Database:delete", event.id);
2023-07-14 14:13:15 +00:00
this.database.events.delete(event.id); // todo: remove
return false;
}
2023-07-14 14:13:15 +00:00
e = _e;
2023-07-12 08:45:55 +00:00
} else {
2023-07-14 14:13:15 +00:00
if (event.kind == NostrKind.META_DATA) {
const profileData = parseProfileData(event.content);
if (profileData instanceof Error) {
return false;
}
e = {
...event,
kind: event.kind,
profile: profileData,
parsedTags: getTags(event),
2023-07-15 05:58:03 +00:00
publicKey: pubkey,
2023-07-14 14:13:15 +00:00
};
} else {
e = {
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),
2023-07-15 05:58:03 +00:00
publicKey: pubkey,
parsedContentItems: Array.from(parseContent(event.content)),
2023-07-14 14:13:15 +00:00
};
}
}
2023-07-14 14:13:15 +00:00
await this.database.events.put(event);
this.events.push(e);
/* not await */ this.sourceOfChange.put(e);
return true;
2023-06-30 14:05:57 +00:00
}
syncEvents(
filter: (e: NostrEvent) => boolean,
events: csp.Channel<[NostrEvent, string /*relay url*/]>,
): csp.Channel<NostrEvent> {
const resChan = csp.chan<NostrEvent>(buffer_size);
(async () => {
for await (const [e, url] of events) {
if (resChan.closed()) {
await events.close(
"db syncEvents, resChan is closed, closing the source events",
);
return;
}
if (filter(e)) {
await this.addEvent(e);
2023-06-30 14:05:57 +00:00
} else {
console.log(
"event",
e,
"does not satisfy filterer",
filter,
);
}
}
await resChan.close(
"db syncEvents, source events is closed, closing the resChan",
);
})();
return resChan;
}
async syncNewDirectMessageEventsOf(
accountContext: NostrAccountContext,
msgs: csp.Channel<{ res: RelayResponse_REQ_Message; url: string }>,
): Promise<csp.Channel<NostrEvent | DecryptionFailure>> {
const resChan = csp.chan<NostrEvent | DecryptionFailure>(buffer_size);
const publicKey = accountContext.publicKey;
(async () => {
for await (const { res: msg, url } of msgs) {
if (msg.type !== "EVENT") {
continue;
}
const encryptedEvent = msg.event;
const theirPubKey = whoIamTalkingTo(encryptedEvent, publicKey);
2023-06-30 14:05:57 +00:00
if (theirPubKey instanceof Error) {
// this could happen if the user send an event without p tag
// because the application is subscribing all events send by the user
console.warn(theirPubKey);
continue;
}
const decryptedEvent = await decryptNostrEvent(
encryptedEvent,
accountContext,
theirPubKey,
);
if (decryptedEvent instanceof DecryptionFailure) {
resChan.put(decryptedEvent).then(async (res) => {
if (res instanceof csp.PutToClosedChannelError) {
await msgs.close(
"resChan has been closed, closing the source chan",
);
}
});
continue;
}
const storedEvent = await this.getEvent({
id: encryptedEvent.id,
});
if (storedEvent === undefined) {
try {
await this.addEvent(decryptedEvent);
} catch (e) {
console.log(e.message);
}
resChan.put(decryptedEvent).then(async (res) => {
if (res instanceof csp.PutToClosedChannelError) {
await msgs.close(
"resChan has been closed, closing the source chan",
);
}
});
}
// else do nothing
}
await resChan.close("source chan is clsoed, closing the resChan");
})();
return resChan;
}
//////////////////
// On DB Change //
//////////////////
onChange(filter: (e: PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event) => boolean) {
2023-06-30 14:05:57 +00:00
const c = this.caster.copy();
const res = csp.chan<PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event>(
buffer_size,
);
2023-06-30 14:05:57 +00:00
(async () => {
for await (const newE of c) {
if (filter(newE)) {
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;
}
export async function transformEvent(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;
}
}
}