2023-07-12 08:45:55 +00:00
|
|
|
import {
|
2023-07-21 08:50:38 +00:00
|
|
|
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";
|
2023-07-16 15:04:23 +00:00
|
|
|
import { parseProfileData } from "./features/profile.ts";
|
|
|
|
import { parseContent } from "./UI/message.ts";
|
2023-08-28 17:58:05 +00:00
|
|
|
import {
|
|
|
|
DecryptionFailure,
|
|
|
|
decryptNostrEvent,
|
|
|
|
NostrAccountContext,
|
|
|
|
NostrEvent,
|
|
|
|
NostrKind,
|
|
|
|
RelayResponse_REQ_Message,
|
|
|
|
} from "./lib/nostr-ts/nostr.ts";
|
|
|
|
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 = 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<
|
2023-07-21 08:50:38 +00:00
|
|
|
PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event
|
2023-07-14 14:13:15 +00:00
|
|
|
>(buffer_size);
|
2023-07-21 08:50:38 +00:00
|
|
|
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
|
|
|
|
2023-07-11 10:45: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();
|
2023-07-30 06:39:53 +00:00
|
|
|
console.log("Database_Contextual_View:onload", Date.now() - t);
|
2023-07-21 08:50:38 +00:00
|
|
|
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,
|
2023-07-16 15:04:23 +00:00
|
|
|
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;
|
2023-07-21 08:50:38 +00:00
|
|
|
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) {
|
2023-07-21 08:50:38 +00:00
|
|
|
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,
|
2023-07-16 15:04:23 +00:00
|
|
|
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 07:14:45 +00:00
|
|
|
}
|
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,
|
2023-07-21 08:50:38 +00:00
|
|
|
public readonly events: (PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event)[],
|
2023-07-11 10:45:57 +00:00
|
|
|
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> {
|
2023-07-05 09:32:02 +00:00
|
|
|
const storedEvent = await this.getEvent({ id: event.id });
|
|
|
|
if (storedEvent) { // event exist
|
2023-07-14 14:13:15 +00:00
|
|
|
return false;
|
2023-07-05 09:32:02 +00:00
|
|
|
}
|
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);
|
2023-07-21 08:50:38 +00:00
|
|
|
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) {
|
2023-07-12 07:14:45 +00:00
|
|
|
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-12 07:14:45 +00:00
|
|
|
}
|
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,
|
2023-07-16 15:04:23 +00:00
|
|
|
parsedContentItems: Array.from(parseContent(event.content)),
|
2023-07-14 14:13:15 +00:00
|
|
|
};
|
|
|
|
}
|
2023-07-12 07:14:45 +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)) {
|
2023-07-05 09:32:02 +00:00
|
|
|
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;
|
2023-07-11 10:45:57 +00:00
|
|
|
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 //
|
|
|
|
//////////////////
|
2023-07-21 08:50:38 +00:00
|
|
|
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();
|
2023-07-21 08:50:38 +00:00
|
|
|
const res = csp.chan<PlainText_Nostr_Event | CustomAppData_Event | Profile_Nostr_Event>(
|
2023-07-15 16:40:21 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-11 10:45:57 +00:00
|
|
|
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
|
2023-07-11 10:45:57 +00:00
|
|
|
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];
|
2023-07-11 10:45:57 +00:00
|
|
|
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-07-12 07:14:45 +00:00
|
|
|
|
2023-07-21 08:50:38 +00:00
|
|
|
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;
|
2023-07-12 07:14:45 +00:00
|
|
|
}
|
2023-07-21 08:50:38 +00:00
|
|
|
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-07-12 07:14:45 +00:00
|
|
|
}
|
|
|
|
}
|