Encrypted Custom App Data in IndexedDB (#66)

This commit is contained in:
BlowaterNostr 2023-07-12 15:14:45 +08:00 committed by GitHub
parent 3b74649f3e
commit e849cf98ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 111 additions and 55 deletions

View File

@ -123,7 +123,6 @@ async function initProfileSyncer(
// Sync Custom App Data // Sync Custom App Data
(async () => { (async () => {
const chan = new Channel<[NostrEvent, string]>();
let subId = newSubID(); let subId = newSubID();
let resp = await pool.newSub( let resp = await pool.newSub(
subId, subId,
@ -135,31 +134,12 @@ async function initProfileSyncer(
if (resp instanceof Error) { if (resp instanceof Error) {
throw resp; throw resp;
} }
(async () => { for await (const { res, url } of resp) {
for await (let { res: nostrMessage, url: relayUrl } of resp) { if (res.type == "EVENT") {
if (nostrMessage.type === "EVENT" && nostrMessage.event.content) { database.addEvent(res.event);
const event = nostrMessage.event;
const decryptedEvent = await decryptNostrEvent(
event,
accountContext,
accountContext.publicKey.hex,
);
if (decryptedEvent instanceof DecryptionFailure) {
console.error(decryptedEvent);
continue;
}
await chan.put([
decryptedEvent,
relayUrl,
]);
}
} }
console.log("closed"); }
})(); })();
return chan;
})().then((customAppDataChan) => {
database.syncEvents((e) => e.kind == NostrKind.CustomAppData, customAppDataChan);
});
/////////////////////////////////// ///////////////////////////////////
// Add relays to Connection Pool // // Add relays to Connection Pool //
@ -274,13 +254,13 @@ export function AppComponent(props: {
let socialPostsPanel: VNode | undefined; let socialPostsPanel: VNode | undefined;
if (model.navigationModel.activeNav == "Social") { if (model.navigationModel.activeNav == "Social") {
const allUserInfo = getAllUsersInformation(app.database, myAccountCtx); const allUserInfo = getAllUsersInformation(app.database, myAccountCtx);
console.log("AppComponent:getSocialPosts before", Date.now() - t); // console.log("AppComponent:getSocialPosts before", Date.now() - t);
const socialPosts = getSocialPosts(app.database, allUserInfo); const socialPosts = getSocialPosts(app.database, allUserInfo);
console.log("AppComponent:getSocialPosts after", Date.now() - t, Date.now()); // console.log("AppComponent:getSocialPosts after", Date.now() - t, Date.now());
let focusedContentGetter = () => { let focusedContentGetter = () => {
console.log("AppComponent:getFocusedContent before", Date.now() - t); // console.log("AppComponent:getFocusedContent before", Date.now() - t);
let _ = getFocusedContent(model.social.focusedContent, allUserInfo, socialPosts); let _ = getFocusedContent(model.social.focusedContent, allUserInfo, socialPosts);
console.log("AppComponent:getFocusedContent", Date.now() - t); // console.log("AppComponent:getFocusedContent", Date.now() - t);
if (_?.type === "MessageThread") { if (_?.type === "MessageThread") {
let editor = model.social.replyEditors.get(_.data.root.event.id); let editor = model.social.replyEditors.get(_.data.root.event.id);
if (editor == undefined) { if (editor == undefined) {

View File

@ -99,7 +99,7 @@ function socialPostsStream(pubkeys: Iterable<string>, pool: ConnectionPool) {
export function getAllUsersInformation( export function getAllUsersInformation(
database: Database_Contextual_View, database: Database_Contextual_View,
myAccountContext: NostrAccountContext, ctx: NostrAccountContext,
): Map<string, UserInfo> { ): Map<string, UserInfo> {
const t = Date.now(); const t = Date.now();
const res = new Map<string, UserInfo>(); const res = new Map<string, UserInfo>();
@ -140,10 +140,10 @@ export function getAllUsersInformation(
case NostrKind.DIRECT_MESSAGE: case NostrKind.DIRECT_MESSAGE:
{ {
let whoAm_I_TalkingTo = ""; let whoAm_I_TalkingTo = "";
if (event.pubkey == myAccountContext.publicKey.hex) { if (event.pubkey == ctx.publicKey.hex) {
// I am the sender // I am the sender
whoAm_I_TalkingTo = getTags(event).p[0]; whoAm_I_TalkingTo = getTags(event).p[0];
} else if (getTags(event).p[0] == myAccountContext.publicKey.hex) { } else if (getTags(event).p[0] == ctx.publicKey.hex) {
// I am the receiver // I am the receiver
whoAm_I_TalkingTo = event.pubkey; whoAm_I_TalkingTo = event.pubkey;
} else { } else {
@ -152,7 +152,7 @@ export function getAllUsersInformation(
} }
const userInfo = res.get(whoAm_I_TalkingTo); const userInfo = res.get(whoAm_I_TalkingTo);
if (userInfo) { if (userInfo) {
if (whoAm_I_TalkingTo == myAccountContext.publicKey.hex) { if (whoAm_I_TalkingTo == ctx.publicKey.hex) {
// talking to myself // talking to myself
if (userInfo.newestEventSendByMe) { if (userInfo.newestEventSendByMe) {
if (event.created_at > userInfo.newestEventSendByMe?.created_at) { if (event.created_at > userInfo.newestEventSendByMe?.created_at) {
@ -164,7 +164,7 @@ export function getAllUsersInformation(
userInfo.newestEventReceivedByMe = event; userInfo.newestEventReceivedByMe = event;
} }
} else { } else {
if (myAccountContext.publicKey.hex == event.pubkey) { if (ctx.publicKey.hex == event.pubkey) {
// I am the sender // I am the sender
if (userInfo.newestEventSendByMe) { if (userInfo.newestEventSendByMe) {
if (event.created_at > userInfo.newestEventSendByMe.created_at) { if (event.created_at > userInfo.newestEventSendByMe.created_at) {
@ -192,12 +192,12 @@ export function getAllUsersInformation(
newestEventSendByMe: undefined, newestEventSendByMe: undefined,
profile: undefined, profile: undefined,
}; };
if (whoAm_I_TalkingTo == myAccountContext.publicKey.hex) { if (whoAm_I_TalkingTo == ctx.publicKey.hex) {
// talking to myself // talking to myself
newUserInfo.newestEventSendByMe = event; newUserInfo.newestEventSendByMe = event;
newUserInfo.newestEventReceivedByMe = event; newUserInfo.newestEventReceivedByMe = event;
} else { } else {
if (myAccountContext.publicKey.hex == event.pubkey) { if (ctx.publicKey.hex == event.pubkey) {
// I am the sender // I am the sender
newUserInfo.newestEventSendByMe = event; newUserInfo.newestEventSendByMe = event;
} else { } else {
@ -212,7 +212,10 @@ export function getAllUsersInformation(
case NostrKind.DELETE: case NostrKind.DELETE:
break; break;
case NostrKind.CustomAppData: { case NostrKind.CustomAppData: {
const obj: CustomAppData = JSON.parse(event.content); if (event.kind == NostrKind.CustomAppData) {
event;
}
const obj: CustomAppData = JSON.parse(event.decryptedContent);
if (obj.type == "PinContact" || obj.type == "UnpinContact") { if (obj.type == "PinContact" || obj.type == "UnpinContact") {
const userInfo = res.get(obj.pubkey); const userInfo = res.get(obj.pubkey);
if (userInfo) { if (userInfo) {

View File

@ -1,5 +1,4 @@
import * as dexie from "https://unpkg.com/dexie@3.2.3/dist/modern/dexie.mjs"; import * as dexie from "https://unpkg.com/dexie@3.2.3/dist/modern/dexie.mjs";
import { Database_Contextual_View, Indices } from "../database.ts";
import { NostrEvent } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; import { NostrEvent } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts";
export class DexieDatabase extends dexie.Dexie { export class DexieDatabase extends dexie.Dexie {

View File

@ -10,7 +10,7 @@ import {
NostrKind, NostrKind,
RelayResponse_REQ_Message, RelayResponse_REQ_Message,
} from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts";
import { getTags, Tag } from "./nostr.ts"; import { Decrypted_Nostr_Event, getTags, PlainText_Nostr_Event, Tag } from "./nostr.ts";
import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { DexieDatabase } from "./UI/dexie-db.ts"; import { DexieDatabase } from "./UI/dexie-db.ts";
@ -25,17 +25,30 @@ export interface Indices {
} }
export class Database_Contextual_View { export class Database_Contextual_View {
private readonly sourceOfChange = csp.chan<NostrEvent>(buffer_size); private readonly sourceOfChange = csp.chan<Decrypted_Nostr_Event | PlainText_Nostr_Event>(buffer_size);
private readonly caster = csp.multi<NostrEvent>(this.sourceOfChange); private readonly caster = csp.multi<Decrypted_Nostr_Event | PlainText_Nostr_Event>(this.sourceOfChange);
static async New(database: DexieDatabase, ctx: NostrAccountContext) { static async New(database: DexieDatabase, ctx: NostrAccountContext) {
const cache: NostrEvent[] = await database.events.filter((_: any) => true).toArray(); const events: NostrEvent[] = await database.events.filter((_: any) => true).toArray();
const cache = new Array<PlainText_Nostr_Event | Decrypted_Nostr_Event>();
for (const event of events) {
const e = await transformEvent(event, ctx);
if (e == undefined) {
continue;
}
if (e instanceof Error) {
console.log("Database:delete", event.id);
database.events.delete(event.id);
continue;
}
cache.push(e);
}
return new Database_Contextual_View(database, cache, ctx); return new Database_Contextual_View(database, cache, ctx);
} }
constructor( constructor(
private readonly database: DexieDatabase, private readonly database: DexieDatabase,
private readonly cache: NostrEvent[], private readonly cache: (PlainText_Nostr_Event | Decrypted_Nostr_Event)[],
private readonly ctx: NostrAccountContext, private readonly ctx: NostrAccountContext,
) {} ) {}
@ -47,19 +60,23 @@ export class Database_Contextual_View {
return this.cache.filter(filter); return this.cache.filter(filter);
}; };
private readonly addToIndexedDB = async (event: NostrEvent) => {
await this.database.events.put(event);
this.cache.push(event);
};
async addEvent(event: NostrEvent) { async addEvent(event: NostrEvent) {
const storedEvent = await this.getEvent({ id: event.id }); const storedEvent = await this.getEvent({ id: event.id });
if (storedEvent) { // event exist if (storedEvent) { // event exist
return; return;
} }
console.log("Database.addEvent", event.id); console.log("Database.addEvent", event.id);
await this.addToIndexedDB(event); await this.database.events.put(event);
await this.sourceOfChange.put(event); const e = await transformEvent(event, this.ctx);
if (e) {
if (e instanceof Error) {
console.log("Database:delete", event.id);
this.database.events.delete(event.id);
return;
}
this.cache.push(e);
await this.sourceOfChange.put(e);
}
} }
syncEvents( syncEvents(
@ -219,3 +236,37 @@ export function whoIamTalkingTo(event: NostrEvent, myPublicKey: PublicKey) {
// I am the receiver // I am the receiver
return whoIAmTalkingTo; return whoIAmTalkingTo;
} }
async function transformEvent(event: NostrEvent, ctx: NostrAccountContext) {
if (event.kind == NostrKind.CustomAppData) {
if (event.pubkey == ctx.publicKey.hex) {
// if I am the author
const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content);
if (decrypted instanceof Error) {
return decrypted;
}
const e: Decrypted_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,
decryptedContent: decrypted,
};
return e;
}
} else {
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,
};
return e;
}
}

View File

@ -1,11 +1,7 @@
/* /*
Extension to common Nostr types Extension to common Nostr types
*/ */
import { import { PrivateKey, PublicKey } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts";
PrivateKey,
PublicKey,
publicKeyHexFromNpub,
} from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts";
import * as nostr from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; import * as nostr from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts";
import { import {
groupBy, groupBy,
@ -325,6 +321,33 @@ export interface Unsigned_CustomAppData_Typed_Event {
readonly content: CustomAppData; readonly content: CustomAppData;
} }
export type Decrypted_Nostr_Event = {
readonly id: nostr.EventID;
readonly sig: string;
readonly pubkey: string;
readonly kind: nostr.NostrKind.CustomAppData;
readonly created_at: number;
readonly tags: Tag[];
readonly content: string;
readonly decryptedContent: string;
};
export type PlainText_Nostr_Event = {
readonly id: nostr.EventID;
readonly sig: string;
readonly pubkey: string;
readonly kind:
| NostrKind.DIRECT_MESSAGE
| NostrKind.CONTACTS
| NostrKind.DELETE
| NostrKind.META_DATA
| NostrKind.TEXT_NOTE
| NostrKind.RECOMMED_SERVER;
readonly created_at: number;
readonly tags: Tag[];
readonly content: string;
};
export type CustomAppData = PinContact | UnpinContact | UserLogin; export type CustomAppData = PinContact | UnpinContact | UserLogin;
export type PinContact = { export type PinContact = {