From 4b6861a511df39176bed8398aece4f4324c3c540 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 09:52:04 +0800 Subject: [PATCH 01/15] chore: add more mobile apis --- .../mask/background/database/avatar-cache/avatar.ts | 10 ++++++++++ packages/public-api/src/mobile.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/packages/mask/background/database/avatar-cache/avatar.ts b/packages/mask/background/database/avatar-cache/avatar.ts index ea63fb5794c5..9ef336d46cf9 100644 --- a/packages/mask/background/database/avatar-cache/avatar.ts +++ b/packages/mask/background/database/avatar-cache/avatar.ts @@ -2,6 +2,7 @@ import { ProfileIdentifier } from '@masknet/shared-base' import { queryAvatarDB, isAvatarOutdatedDB, storeAvatarDB, IdentifierWithAvatar, createAvatarDBAccess } from './db' import { memoizePromise } from '../../../utils-pure' import { MaskMessages } from '../../../shared' +import { hasNativeAPI, nativeAPI } from '../../../shared/native-rpc' import { blobToDataURL } from '@dimensiondev/kit' import { createTransaction } from '../utils/openDB' @@ -11,6 +12,11 @@ import { createTransaction } from '../utils/openDB' */ export const queryAvatarDataURL = memoizePromise( async function (identifier: IdentifierWithAvatar): Promise { + if (hasNativeAPI) { + const avatar = await nativeAPI?.api.query_avatar({ identifier: identifier.toText() }) + if (!avatar) throw new Error('Avatar not found') + return avatar + } const t = createTransaction(await createAvatarDBAccess(), 'readonly')('avatars') const buffer = await queryAvatarDB(t, identifier) if (!buffer) throw new Error('Avatar not found') @@ -29,6 +35,10 @@ export const queryAvatarDataURL = memoizePromise( export async function storeAvatar(identifier: IdentifierWithAvatar, avatar: ArrayBuffer | string): Promise { if (identifier instanceof ProfileIdentifier && identifier.isUnknown) return try { + if (hasNativeAPI) { + await nativeAPI?.api.store_avatar({ identifier: identifier.toText(), avatar: avatar as string }) + return + } if (typeof avatar === 'string') { if (avatar.startsWith('https') === false) return const isOutdated = await isAvatarOutdatedDB( diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index c9751d0b2778..e9e97aed8556 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -100,6 +100,8 @@ export interface SharedNativeAPIs { }): Promise update_relation(params: { relation: Omit }): Promise delete_relation(params: { personaIdentifier: string; profileIdentifier: string }): Promise + query_avatar(params: { identifier: string }): Promise + store_avatar(params: { identifier: string; avatar: string }): Promise } /** * APIs that only implemented by iOS Mask Network From 3e04229ceb802621fa51581815c9644953380e63 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 10:14:06 +0800 Subject: [PATCH 02/15] chore: no cache for native --- packages/mask/background/database/avatar-cache/avatar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mask/background/database/avatar-cache/avatar.ts b/packages/mask/background/database/avatar-cache/avatar.ts index 9ef336d46cf9..1270f463fa03 100644 --- a/packages/mask/background/database/avatar-cache/avatar.ts +++ b/packages/mask/background/database/avatar-cache/avatar.ts @@ -7,7 +7,7 @@ import { blobToDataURL } from '@dimensiondev/kit' import { createTransaction } from '../utils/openDB' /** - * Get a (cached) blob url for an identifier. + * Get a (cached) blob url for an identifier. No cache for native api. * ? Because of cross-origin restrictions, we cannot use blob url here. sad :( */ export const queryAvatarDataURL = memoizePromise( @@ -22,7 +22,7 @@ export const queryAvatarDataURL = memoizePromise( if (!buffer) throw new Error('Avatar not found') return blobToDataURL(new Blob([buffer], { type: 'image/png' })) }, - (id) => id.toText(), + (id) => (hasNativeAPI ? Date.now().toString() : id.toText()), ) /** From ac2d1596b2294792ed1d9f119797aad9adc8604e Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 15:12:00 +0800 Subject: [PATCH 03/15] chore: reply code review --- .../database/avatar-cache/avatar.ts | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/mask/background/database/avatar-cache/avatar.ts b/packages/mask/background/database/avatar-cache/avatar.ts index 1270f463fa03..30cb4a26066c 100644 --- a/packages/mask/background/database/avatar-cache/avatar.ts +++ b/packages/mask/background/database/avatar-cache/avatar.ts @@ -10,20 +10,23 @@ import { createTransaction } from '../utils/openDB' * Get a (cached) blob url for an identifier. No cache for native api. * ? Because of cross-origin restrictions, we cannot use blob url here. sad :( */ -export const queryAvatarDataURL = memoizePromise( - async function (identifier: IdentifierWithAvatar): Promise { - if (hasNativeAPI) { - const avatar = await nativeAPI?.api.query_avatar({ identifier: identifier.toText() }) - if (!avatar) throw new Error('Avatar not found') - return avatar - } - const t = createTransaction(await createAvatarDBAccess(), 'readonly')('avatars') - const buffer = await queryAvatarDB(t, identifier) - if (!buffer) throw new Error('Avatar not found') - return blobToDataURL(new Blob([buffer], { type: 'image/png' })) - }, - (id) => (hasNativeAPI ? Date.now().toString() : id.toText()), -) +export const queryAvatarDataURL = ( + hasNativeAPI + ? async function (identifier: IdentifierWithAvatar): Promise { + return nativeAPI?.api.query_avatar({ identifier: identifier.toText() }) + } + : memoizePromise( + async function (identifier: IdentifierWithAvatar): Promise { + const t = createTransaction(await createAvatarDBAccess(), 'readonly')('avatars') + const buffer = await queryAvatarDB(t, identifier) + if (!buffer) throw new Error('Avatar not found') + return blobToDataURL(new Blob([buffer], { type: 'image/png' })) + }, + (id) => id.toText(), + ) +) as (number | ((identifier: IdentifierWithAvatar) => Promise)) & { + cache?: Map +} /** * Store an avatar with a url for an identifier. @@ -36,6 +39,8 @@ export async function storeAvatar(identifier: IdentifierWithAvatar, avatar: Arra if (identifier instanceof ProfileIdentifier && identifier.isUnknown) return try { if (hasNativeAPI) { + // ArrayBuffer is unreachable on Native side. + if (typeof avatar !== 'string') return await nativeAPI?.api.store_avatar({ identifier: identifier.toText(), avatar: avatar as string }) return } @@ -62,7 +67,7 @@ export async function storeAvatar(identifier: IdentifierWithAvatar, avatar: Arra } catch (error) { console.error('[AvatarDB] Store avatar failed', error) } finally { - queryAvatarDataURL.cache.delete(identifier.toText()) + queryAvatarDataURL.cache?.delete(identifier.toText()) if (identifier instanceof ProfileIdentifier) { MaskMessages.events.profilesChanged.sendToAll([{ of: identifier, reason: 'update' }]) } From 866991e340a8e791f0a3b4e58f8f1ea068579919 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 15:16:29 +0800 Subject: [PATCH 04/15] fix: type --- packages/mask/background/database/avatar-cache/avatar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/background/database/avatar-cache/avatar.ts b/packages/mask/background/database/avatar-cache/avatar.ts index 30cb4a26066c..dedbf3e2d16d 100644 --- a/packages/mask/background/database/avatar-cache/avatar.ts +++ b/packages/mask/background/database/avatar-cache/avatar.ts @@ -24,7 +24,7 @@ export const queryAvatarDataURL = ( }, (id) => id.toText(), ) -) as (number | ((identifier: IdentifierWithAvatar) => Promise)) & { +) as ((identifier: IdentifierWithAvatar) => Promise) & { cache?: Map } From 7d6581ecec8cea9d2f0a6a652d975815aa8d6c8b Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 17:37:18 +0800 Subject: [PATCH 05/15] chore: add post api for mobile --- .../pages/Personas/hooks/usePostHistory.ts | 1 + packages/mask/background/database/post/app.ts | 99 +++++ .../mask/background/database/post/helper.ts | 29 -- .../mask/background/database/post/index.ts | 408 +----------------- .../mask/background/database/post/type.ts | 76 ++++ packages/mask/background/database/post/web.ts | 342 +++++++++++++++ .../background-script/IdentityService.ts | 1 + packages/public-api/src/mobile.ts | 10 + packages/public-api/src/types/index.ts | 36 ++ 9 files changed, 576 insertions(+), 426 deletions(-) create mode 100644 packages/mask/background/database/post/app.ts delete mode 100644 packages/mask/background/database/post/helper.ts create mode 100644 packages/mask/background/database/post/type.ts create mode 100644 packages/mask/background/database/post/web.ts diff --git a/packages/dashboard/src/pages/Personas/hooks/usePostHistory.ts b/packages/dashboard/src/pages/Personas/hooks/usePostHistory.ts index 799ae570f17f..13435fd44c3b 100644 --- a/packages/dashboard/src/pages/Personas/hooks/usePostHistory.ts +++ b/packages/dashboard/src/pages/Personas/hooks/usePostHistory.ts @@ -20,6 +20,7 @@ export const usePostHistory = (network: string, page: number, size = 20) => { network, userIds: currentPersona?.linkedProfiles.map((profile) => profile.identifier.userId) ?? [], after: lastValue?.identifier, + pageOffset: page, }, size, ) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts new file mode 100644 index 000000000000..c357cad98597 --- /dev/null +++ b/packages/mask/background/database/post/app.ts @@ -0,0 +1,99 @@ +import type { PostRecord as NativePostRecord } from '@masknet/public-api' +import type { PostRecord, PostReadWriteTransaction, PostReadOnlyTransaction } from './type' +import { PostIVIdentifier, Identifier, AESJsonWebKey, IdentifierMap, PersonaIdentifier } from '@masknet/shared-base' +import { nativeAPI } from '../../../shared/native-rpc' + +export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { + return nativeAPI?.api.create_post({ post: postInNative(record) }) +} + +export async function updatePostDB( + updateRecord: Partial & Pick, + mode: 'append' | 'override', + t?: PostReadWriteTransaction, +): Promise { + await nativeAPI?.api.update_post({ + post: postInNative(updateRecord), + options: { + mode: mode === 'append' ? 0 : mode === 'override' ? 1 : 0, + }, + }) + return +} + +export async function queryPostDB(record: PostIVIdentifier, t?: PostReadOnlyTransaction): Promise { + const result = await nativeAPI?.api.query_post({ identifier: record.toText() }) + return result ? postOutNative(result) : null +} + +export async function queryPostPagedDB( + linked: PersonaIdentifier, + options: { + network: string + userIds: string[] + after?: PostIVIdentifier + pageOffset?: number + }, + count: number, +): Promise { + const results = await nativeAPI?.api.query_posts({ + userIds: options.userIds, + network: options.network, + encryptBy: linked.toText(), + pageOption: options.pageOffset + ? { + pageSize: count, + pageOffset: options.pageOffset, + } + : undefined, + }) + if (!results?.length) return [] + return results.map((r) => postOutNative(r)) +} + +function postInNative(record: Partial & Pick): NativePostRecord { + return { + postBy: record.postBy ? record.postBy.toText() : undefined, + identifier: record.identifier.toText(), + postCryptoKey: record.postCryptoKey, + recipients: + record.recipients === 'everyone' + ? 'everyone' + : record.recipients + ? Object.fromEntries(record.recipients.__raw_map__) + : undefined, + foundAt: record.foundAt ? record.foundAt.getTime() : undefined, + encryptBy: record.encryptBy?.toText(), + url: record.url, + summary: record.summary, + interestedMeta: record.interestedMeta, + } as NativePostRecord +} + +function postOutNative(record: NativePostRecord): PostRecord { + return { + postBy: Identifier.fromString(record.postBy).unwrap(), + identifier: Identifier.fromString(record.identifier).unwrap(), + postCryptoKey: record.postCryptoKey as unknown as AESJsonWebKey, + recipients: + record.recipients === 'everyone' + ? 'everyone' + : new IdentifierMap(new Map(Object.entries(record.recipients))), + foundAt: new Date(record.foundAt), + encryptBy: record.encryptBy ? Identifier.fromString(record.encryptBy).unwrap() : undefined, + url: record.url, + summary: record.summary, + interestedMeta: record.interestedMeta, + } as PostRecord +} + +// #region Not available on native. +export async function queryPostsDB( + query: string | ((data: PostRecord, id: PostIVIdentifier) => boolean), + t?: PostReadOnlyTransaction, +): Promise { + return [] +} + +export const PostDBAccess = () => undefined +// #endregion diff --git a/packages/mask/background/database/post/helper.ts b/packages/mask/background/database/post/helper.ts deleted file mode 100644 index c7bad98079cc..000000000000 --- a/packages/mask/background/database/post/helper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { AESCryptoKey, PostIVIdentifier } from '@masknet/shared-base' -import { CryptoKeyToJsonWebKey } from '../../../utils-pure' -import { createTransaction } from '../utils/openDB' -import { PostDBAccess, queryPostDB, createPostDB, updatePostDB, PostRecord } from './index' - -export async function savePostKeyToDB( - id: PostIVIdentifier, - key: AESCryptoKey, - extraInfo: Omit, -): Promise { - const jwk = await CryptoKeyToJsonWebKey(key) - { - const t = createTransaction(await PostDBAccess(), 'readwrite')('post') - const post = await queryPostDB(id, t) - if (!post) { - await createPostDB( - { - identifier: id, - postCryptoKey: jwk, - foundAt: new Date(), - ...extraInfo, - }, - t, - ) - } else { - await updatePostDB({ ...post, postCryptoKey: jwk }, 'override', t) - } - } -} diff --git a/packages/mask/background/database/post/index.ts b/packages/mask/background/database/post/index.ts index 988f6f1fc1fa..6a38a836b041 100644 --- a/packages/mask/background/database/post/index.ts +++ b/packages/mask/background/database/post/index.ts @@ -1,403 +1,17 @@ -import { - AESCryptoKey, - AESJsonWebKey, - ECKeyIdentifier, - GroupIdentifier, - Identifier, - IdentifierMap, - PersonaIdentifier, - PostIdentifier, - PostIVIdentifier, - ProfileIdentifier, -} from '@masknet/shared-base' -import { DBSchema, openDB } from 'idb/with-async-ittr' -import { CryptoKeyToJsonWebKey, PrototypeLess, restorePrototype, restorePrototypeArray } from '../../../utils-pure' -import { createDBAccessWithAsyncUpgrade, createTransaction, IDBPSafeTransaction } from '../utils/openDB' +export * from './type' +import { hasNativeAPI } from '../../../shared/native-rpc' -type UpgradeKnowledge = { version: 4; data: Map } | undefined -const db = createDBAccessWithAsyncUpgrade( - 4, - 7, - (currentTryOpen, knowledge) => - openDB('maskbook-post-v2', currentTryOpen, { - async upgrade(db, oldVersion, _newVersion, transaction): Promise { - type Version2PostRecord = { - postBy: PrototypeLess - identifier: string - recipientGroups?: unknown - recipients?: ProfileIdentifier[] - foundAt: Date - postCryptoKey?: CryptoKey - } - type Version3RecipientDetail = { - /** Why they're able to receive this message? */ - reason: RecipientReason[] - } - type Version3PostRecord = Omit & { - recipients: Record - } - type Version4PostRecord = Omit & { - recipients: Map - } - type Version5PostRecord = Omit & { - postCryptoKey?: AESJsonWebKey - encryptBy?: PrototypeLess - url?: string - summary?: string - interestedMeta?: ReadonlyMap - recipients: true | Map - } - type Version6PostRecord = Omit & { - encryptBy?: string - } - /** - * A type assert that make sure a and b are the same type - * @param a The latest version PostRecord - */ - function _assert(a: Version6PostRecord, b: PostDBRecord) { - a = b - b = a - } - // Prevent unused code removal - // eslint-disable-next-line no-constant-condition - if (1 + 1 === 3) _assert({} as any, {} as any) - if (oldVersion < 1) { - // inline keys - return void db.createObjectStore('post', { keyPath: 'identifier' }) - } - /** - * In the version 1 we use PostIdentifier to store post that identified by post iv - * After upgrade to version 2, we use PostIVIdentifier to store it. - * So we transform all old data into new data. - */ - if (oldVersion <= 1) { - const store = transaction.objectStore('post') - const old = await store.getAll() - await store.clear() - for (const each of old) { - const id = Identifier.fromString(each.identifier, PostIdentifier) - if (id.ok) { - const { postId, identifier } = id.val - each.identifier = new PostIVIdentifier( - (identifier as ProfileIdentifier).network, - postId, - ).toText() - await store.add(each) - } - } +export const { createPostDB, updatePostDB, queryPostDB, queryPostsDB, queryPostPagedDB, PostDBAccess } = new Proxy( + {} as any as typeof import('./web'), + { + get(_, key) { + return async function (...args: any) { + if (hasNativeAPI) { + return import('./app').then((x) => (x as any)[key](...args)) } - /** - * In the version 2 we use `recipients?: ProfileIdentifier[]` - * After upgrade to version 3, we use `recipients: Record` - */ - if (oldVersion <= 2) { - const store = transaction.objectStore('post') - for await (const cursor of store) { - const v2record: Version2PostRecord = cursor.value as any - const oldType = v2record.recipients - oldType && restorePrototypeArray(oldType, ProfileIdentifier.prototype) - const newType: Version3PostRecord['recipients'] = {} - if (oldType !== undefined) - for (const each of oldType) { - newType[each.toText()] = { reason: [{ type: 'direct', at: new Date(0) }] } - } - const next: Version3PostRecord = { - ...v2record, - recipients: newType, - postBy: ProfileIdentifier.unknown, - foundAt: new Date(0), - recipientGroups: [], - } - await cursor.update(next as Version3PostRecord as any) - } - } - - /** - * In the version 3 we use `recipients?: Record` - * After upgrade to version 4, we use `recipients: IdentifierMap` - */ - if (oldVersion <= 3) { - const store = transaction.objectStore('post') - for await (const cursor of store) { - const v3Record: Version3PostRecord = cursor.value as any - const newType: Version4PostRecord['recipients'] = new Map() - for (const [key, value] of Object.entries(v3Record.recipients)) { - newType.set(key, value) - } - const v4Record: Version4PostRecord = { - ...v3Record, - recipients: newType, - } - await cursor.update(v4Record as any) - } - } - /** - * In version 4 we use CryptoKey, in version 5 we use JsonWebKey - */ - if (oldVersion <= 4) { - const store = transaction.objectStore('post') - for await (const cursor of store) { - const v4Record: Version4PostRecord = cursor.value as any - const data = knowledge?.data! - if (!v4Record.postCryptoKey) continue - const v5Record: Version5PostRecord = { - ...v4Record, - postCryptoKey: data.get(v4Record.identifier)!, - } - if (!v5Record.postCryptoKey) delete v5Record.postCryptoKey - await cursor.update(v5Record as any) - } - } - - // version 6 ships a wrong db migration. - // therefore need to upgrade again to fix it. - if (oldVersion <= 6) { - const store = transaction.objectStore('post') - for await (const cursor of store) { - const v5Record: Version5PostRecord = cursor.value as any - const by = v5Record.encryptBy - // This is the correct data type - if (typeof by === 'string') continue - if (!by) continue - cursor.value.encryptBy = restorePrototype(by, ECKeyIdentifier.prototype).toText() - cursor.update(cursor.value) - } - store.createIndex('persona, date', ['encryptBy', 'foundAt'], { unique: false }) - } - }, - }), - async (db): Promise => { - if (db.version === 4) { - const map = new Map() - const knowledge: UpgradeKnowledge = { version: 4, data: map } - const records = await createTransaction(db, 'readonly')('post').objectStore('post').getAll() - for (const r of records) { - const x = r.postCryptoKey - if (!x) continue - try { - const key = await CryptoKeyToJsonWebKey(x as any as AESCryptoKey) - map.set(r.identifier, key) - } catch { - continue - } + return import('./web').then((x) => (x as any)[key](...args)) } - return knowledge - } - return undefined + }, }, ) -export const PostDBAccess = db - -type PostReadOnlyTransaction = IDBPSafeTransaction -type PostReadWriteTransaction = IDBPSafeTransaction - -export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { - t ||= createTransaction(await db(), 'readwrite')('post') - const toSave = postToDB(record) - await t.objectStore('post').add(toSave) -} -export async function updatePostDB( - updateRecord: Partial & Pick, - mode: 'append' | 'override', - t?: PostReadWriteTransaction, -): Promise { - t ||= createTransaction(await db(), 'readwrite')('post') - const emptyRecord: PostRecord = { - identifier: updateRecord.identifier, - recipients: new IdentifierMap(new Map()), - postBy: ProfileIdentifier.unknown, - foundAt: new Date(), - } - const currentRecord = (await queryPostDB(updateRecord.identifier, t)) || emptyRecord - const nextRecord: PostRecord = { ...currentRecord, ...updateRecord } - const nextRecipients: PostDBRecord['recipients'] = - mode === 'override' ? postToDB(nextRecord).recipients : postToDB(currentRecord).recipients - if (mode === 'append') { - if (updateRecord.recipients) { - if (typeof updateRecord.recipients === 'object' && typeof nextRecipients === 'object') { - for (const [id, patchDetail] of updateRecord.recipients) { - const idText = id.toText() - if (nextRecipients.has(idText)) { - const { reason, ...rest } = patchDetail - const nextDetail = nextRecipients.get(idText)! - Object.assign(nextDetail, rest) - nextDetail.reason = [...nextDetail.reason, ...patchDetail.reason] - } else { - nextRecipients.set(idText, patchDetail) - } - } - } else { - nextRecord.recipients = 'everyone' - } - } - } - const nextRecordInDBType = postToDB(nextRecord) - await t.objectStore('post').put(nextRecordInDBType) -} - -export async function queryPostDB(record: PostIVIdentifier, t?: PostReadOnlyTransaction): Promise { - t ||= createTransaction(await db(), 'readonly')('post') - const result = await t.objectStore('post').get(record.toText()) - if (result) return postOutDB(result) - return null -} -export async function queryPostsDB( - query: string | ((data: PostRecord, id: PostIVIdentifier) => boolean), - t?: PostReadOnlyTransaction, -): Promise { - t ||= createTransaction(await db(), 'readonly')('post') - const selected: PostRecord[] = [] - for await (const { value } of t.objectStore('post')) { - const idResult = Identifier.fromString(value.identifier, PostIVIdentifier) - if (idResult.err) { - console.warn(idResult.val.message) - continue - } - const id = idResult.val - if (typeof query === 'string') { - if (id.network === query) selected.push(postOutDB(value)) - } else { - const v = postOutDB(value) - if (query(v, id)) selected.push(v) - } - } - return selected -} - -/** - * Query posts by paged - */ -export async function queryPostPagedDB( - linked: PersonaIdentifier, - options: { - network: string - userIds: string[] - after?: PostIVIdentifier - }, - count: number, -) { - const t = createTransaction(await db(), 'readonly')('post') - - const data: PostRecord[] = [] - let firstRecord = true - - for await (const cursor of t.objectStore('post').iterate()) { - if (cursor.value.encryptBy !== linked.toText()) continue - if (!options.userIds.includes(cursor.value.postBy.userId)) continue - - const postIdentifier = Identifier.fromString(cursor.value.identifier, PostIVIdentifier).unwrap() - if (postIdentifier.network !== options.network) continue - - if (firstRecord && options.after) { - cursor.continue(options.after.toText()) - firstRecord = false - continue - } - - if (postIdentifier === options.after) continue - - if (count <= 0) break - const outData = postOutDB(cursor.value) - count -= 1 - data.push(outData) - } - return data -} - -// #region db in and out -function postOutDB(db: PostDBRecord): PostRecord { - const { identifier, foundAt, postBy, recipients, postCryptoKey, encryptBy, interestedMeta, summary, url } = db - if (typeof recipients === 'object') { - for (const detail of recipients.values()) { - detail.reason.forEach((x) => x.type === 'group' && restorePrototype(x.group, GroupIdentifier.prototype)) - } - } - return { - identifier: Identifier.fromString(identifier, PostIVIdentifier).unwrap(), - postBy: restorePrototype(postBy, ProfileIdentifier.prototype), - recipients: recipients === true ? 'everyone' : new IdentifierMap(recipients, ProfileIdentifier), - foundAt: foundAt, - postCryptoKey: postCryptoKey, - encryptBy: encryptBy ? Identifier.fromString(encryptBy, ECKeyIdentifier).unwrapOr(undefined) : undefined, - interestedMeta, - summary, - url, - } -} -function postToDB(out: PostRecord): PostDBRecord { - return { - ...out, - identifier: out.identifier.toText(), - recipients: out.recipients === 'everyone' ? true : out.recipients.__raw_map__, - encryptBy: out.encryptBy?.toText(), - } -} -// #endregion - -// #region types -/** - * When you change this, change RecipientReasonJSON as well! - */ -export type RecipientReason = ( - | { type: 'auto-share' } - | { type: 'direct' } - | { type: 'group'; group: GroupIdentifier } -) & { - /** - * When we send the key to them by this reason? - * If the unix timestamp of this Date is 0, - * should display it as "unknown" or "before Nov 2019" - */ - at: Date -} -export interface RecipientDetail { - /** Why they're able to receive this message? */ - reason: RecipientReason[] -} -export interface PostRecord { - /** - * For old data stored before version 3, this identifier may be ProfileIdentifier.unknown - */ - postBy: ProfileIdentifier - identifier: PostIVIdentifier - postCryptoKey?: AESJsonWebKey - /** - * Receivers - */ - recipients: 'everyone' | IdentifierMap - /** @deprecated */ - recipientGroups?: unknown - /** - * When does Mask find this post. - * For your own post, it is when Mask created this post. - * For others post, it is when you see it first time. - */ - foundAt: Date - encryptBy?: PersonaIdentifier - /** The URL of this post */ - url?: string - /** Summary of this post (maybe front 20 chars). */ - summary?: string - /** Interested metadata contained in this post. */ - interestedMeta?: ReadonlyMap -} - -interface PostDBRecord extends Omit { - postBy: PrototypeLess - identifier: string - recipients: true | Map - encryptBy?: string -} - -interface PostDB extends DBSchema { - /** Use inline keys */ - post: { - value: PostDBRecord - key: string - indexes: { - 'persona, date': [string, Date] - } - } -} -// #endregion diff --git a/packages/mask/background/database/post/type.ts b/packages/mask/background/database/post/type.ts new file mode 100644 index 000000000000..6ed8978dfca3 --- /dev/null +++ b/packages/mask/background/database/post/type.ts @@ -0,0 +1,76 @@ +import type { + AESJsonWebKey, + GroupIdentifier, + IdentifierMap, + PersonaIdentifier, + PostIVIdentifier, + ProfileIdentifier, +} from '@masknet/shared-base' +import type { DBSchema } from 'idb/with-async-ittr' +import type { PrototypeLess } from '../../../utils-pure' +import type { IDBPSafeTransaction } from '../utils/openDB' + +export type RecipientReason = ( + | { type: 'auto-share' } + | { type: 'direct' } + | { type: 'group'; group: GroupIdentifier } +) & { + /** + * When we send the key to them by this reason? + * If the unix timestamp of this Date is 0, + * should display it as "unknown" or "before Nov 2019" + */ + at: Date +} +export interface RecipientDetail { + /** Why they're able to receive this message? */ + reason: RecipientReason[] +} +export interface PostRecord { + /** + * For old data stored before version 3, this identifier may be ProfileIdentifier.unknown + */ + postBy: ProfileIdentifier + identifier: PostIVIdentifier + postCryptoKey?: AESJsonWebKey + /** + * Receivers + */ + recipients: 'everyone' | IdentifierMap + /** @deprecated */ + recipientGroups?: unknown + /** + * When does Mask find this post. + * For your own post, it is when Mask created this post. + * For others post, it is when you see it first time. + */ + foundAt: Date + encryptBy?: PersonaIdentifier + /** The URL of this post */ + url?: string + /** Summary of this post (maybe front 20 chars). */ + summary?: string + /** Interested metadata contained in this post. */ + interestedMeta?: ReadonlyMap +} + +export interface PostDBRecord extends Omit { + postBy: PrototypeLess + identifier: string + recipients: true | Map + encryptBy?: string +} + +export interface PostDB extends DBSchema { + /** Use inline keys */ + post: { + value: PostDBRecord + key: string + indexes: { + 'persona, date': [string, Date] + } + } +} + +export type PostReadOnlyTransaction = IDBPSafeTransaction +export type PostReadWriteTransaction = IDBPSafeTransaction diff --git a/packages/mask/background/database/post/web.ts b/packages/mask/background/database/post/web.ts new file mode 100644 index 000000000000..bab65056bee4 --- /dev/null +++ b/packages/mask/background/database/post/web.ts @@ -0,0 +1,342 @@ +import { + AESCryptoKey, + AESJsonWebKey, + ECKeyIdentifier, + GroupIdentifier, + Identifier, + IdentifierMap, + PersonaIdentifier, + PostIdentifier, + PostIVIdentifier, + ProfileIdentifier, +} from '@masknet/shared-base' +import { openDB } from 'idb/with-async-ittr' +import { CryptoKeyToJsonWebKey, PrototypeLess, restorePrototype, restorePrototypeArray } from '../../../utils-pure' +import { createDBAccessWithAsyncUpgrade, createTransaction } from '../utils/openDB' +import type { + RecipientReason, + PostRecord, + PostDB, + PostDBRecord, + PostReadOnlyTransaction, + PostReadWriteTransaction, +} from './type' + +type UpgradeKnowledge = { version: 4; data: Map } | undefined +const db = createDBAccessWithAsyncUpgrade( + 4, + 7, + (currentTryOpen, knowledge) => + openDB('maskbook-post-v2', currentTryOpen, { + async upgrade(db, oldVersion, _newVersion, transaction): Promise { + type Version2PostRecord = { + postBy: PrototypeLess + identifier: string + recipientGroups?: unknown + recipients?: ProfileIdentifier[] + foundAt: Date + postCryptoKey?: CryptoKey + } + type Version3RecipientDetail = { + /** Why they're able to receive this message? */ + reason: RecipientReason[] + } + type Version3PostRecord = Omit & { + recipients: Record + } + type Version4PostRecord = Omit & { + recipients: Map + } + type Version5PostRecord = Omit & { + postCryptoKey?: AESJsonWebKey + encryptBy?: PrototypeLess + url?: string + summary?: string + interestedMeta?: ReadonlyMap + recipients: true | Map + } + type Version6PostRecord = Omit & { + encryptBy?: string + } + /** + * A type assert that make sure a and b are the same type + * @param a The latest version PostRecord + */ + function _assert(a: Version6PostRecord, b: PostDBRecord) { + a = b + b = a + } + // Prevent unused code removal + // eslint-disable-next-line no-constant-condition + if (1 + 1 === 3) _assert({} as any, {} as any) + if (oldVersion < 1) { + // inline keys + return void db.createObjectStore('post', { keyPath: 'identifier' }) + } + /** + * In the version 1 we use PostIdentifier to store post that identified by post iv + * After upgrade to version 2, we use PostIVIdentifier to store it. + * So we transform all old data into new data. + */ + if (oldVersion <= 1) { + const store = transaction.objectStore('post') + const old = await store.getAll() + await store.clear() + for (const each of old) { + const id = Identifier.fromString(each.identifier, PostIdentifier) + if (id.ok) { + const { postId, identifier } = id.val + each.identifier = new PostIVIdentifier( + (identifier as ProfileIdentifier).network, + postId, + ).toText() + await store.add(each) + } + } + } + + /** + * In the version 2 we use `recipients?: ProfileIdentifier[]` + * After upgrade to version 3, we use `recipients: Record` + */ + if (oldVersion <= 2) { + const store = transaction.objectStore('post') + for await (const cursor of store) { + const v2record: Version2PostRecord = cursor.value as any + const oldType = v2record.recipients + oldType && restorePrototypeArray(oldType, ProfileIdentifier.prototype) + const newType: Version3PostRecord['recipients'] = {} + if (oldType !== undefined) + for (const each of oldType) { + newType[each.toText()] = { reason: [{ type: 'direct', at: new Date(0) }] } + } + const next: Version3PostRecord = { + ...v2record, + recipients: newType, + postBy: ProfileIdentifier.unknown, + foundAt: new Date(0), + recipientGroups: [], + } + await cursor.update(next as Version3PostRecord as any) + } + } + + /** + * In the version 3 we use `recipients?: Record` + * After upgrade to version 4, we use `recipients: IdentifierMap` + */ + if (oldVersion <= 3) { + const store = transaction.objectStore('post') + for await (const cursor of store) { + const v3Record: Version3PostRecord = cursor.value as any + const newType: Version4PostRecord['recipients'] = new Map() + for (const [key, value] of Object.entries(v3Record.recipients)) { + newType.set(key, value) + } + const v4Record: Version4PostRecord = { + ...v3Record, + recipients: newType, + } + await cursor.update(v4Record as any) + } + } + /** + * In version 4 we use CryptoKey, in version 5 we use JsonWebKey + */ + if (oldVersion <= 4) { + const store = transaction.objectStore('post') + for await (const cursor of store) { + const v4Record: Version4PostRecord = cursor.value as any + const data = knowledge?.data! + if (!v4Record.postCryptoKey) continue + const v5Record: Version5PostRecord = { + ...v4Record, + postCryptoKey: data.get(v4Record.identifier)!, + } + if (!v5Record.postCryptoKey) delete v5Record.postCryptoKey + await cursor.update(v5Record as any) + } + } + + // version 6 ships a wrong db migration. + // therefore need to upgrade again to fix it. + if (oldVersion <= 6) { + const store = transaction.objectStore('post') + for await (const cursor of store) { + const v5Record: Version5PostRecord = cursor.value as any + const by = v5Record.encryptBy + // This is the correct data type + if (typeof by === 'string') continue + if (!by) continue + cursor.value.encryptBy = restorePrototype(by, ECKeyIdentifier.prototype).toText() + cursor.update(cursor.value) + } + store.createIndex('persona, date', ['encryptBy', 'foundAt'], { unique: false }) + } + }, + }), + async (db): Promise => { + if (db.version === 4) { + const map = new Map() + const knowledge: UpgradeKnowledge = { version: 4, data: map } + const records = await createTransaction(db, 'readonly')('post').objectStore('post').getAll() + for (const r of records) { + const x = r.postCryptoKey + if (!x) continue + try { + const key = await CryptoKeyToJsonWebKey(x as any as AESCryptoKey) + map.set(r.identifier, key) + } catch { + continue + } + } + return knowledge + } + return undefined + }, +) +export const PostDBAccess = db + +export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { + t ||= createTransaction(await db(), 'readwrite')('post') + const toSave = postToDB(record) + await t.objectStore('post').add(toSave) +} +export async function updatePostDB( + updateRecord: Partial & Pick, + mode: 'append' | 'override', + t?: PostReadWriteTransaction, +): Promise { + t ||= createTransaction(await db(), 'readwrite')('post') + const emptyRecord: PostRecord = { + identifier: updateRecord.identifier, + recipients: new IdentifierMap(new Map()), + postBy: ProfileIdentifier.unknown, + foundAt: new Date(), + } + const currentRecord = (await queryPostDB(updateRecord.identifier, t)) || emptyRecord + const nextRecord: PostRecord = { ...currentRecord, ...updateRecord } + const nextRecipients: PostDBRecord['recipients'] = + mode === 'override' ? postToDB(nextRecord).recipients : postToDB(currentRecord).recipients + if (mode === 'append') { + if (updateRecord.recipients) { + if (typeof updateRecord.recipients === 'object' && typeof nextRecipients === 'object') { + for (const [id, patchDetail] of updateRecord.recipients) { + const idText = id.toText() + if (nextRecipients.has(idText)) { + const { reason, ...rest } = patchDetail + const nextDetail = nextRecipients.get(idText)! + Object.assign(nextDetail, rest) + nextDetail.reason = [...nextDetail.reason, ...patchDetail.reason] + } else { + nextRecipients.set(idText, patchDetail) + } + } + } else { + nextRecord.recipients = 'everyone' + } + } + } + const nextRecordInDBType = postToDB(nextRecord) + await t.objectStore('post').put(nextRecordInDBType) +} + +export async function queryPostDB(record: PostIVIdentifier, t?: PostReadOnlyTransaction): Promise { + t ||= createTransaction(await db(), 'readonly')('post') + const result = await t.objectStore('post').get(record.toText()) + if (result) return postOutDB(result) + return null +} +export async function queryPostsDB( + query: string | ((data: PostRecord, id: PostIVIdentifier) => boolean), + t?: PostReadOnlyTransaction, +): Promise { + t ||= createTransaction(await db(), 'readonly')('post') + const selected: PostRecord[] = [] + for await (const { value } of t.objectStore('post')) { + const idResult = Identifier.fromString(value.identifier, PostIVIdentifier) + if (idResult.err) { + console.warn(idResult.val.message) + continue + } + const id = idResult.val + if (typeof query === 'string') { + if (id.network === query) selected.push(postOutDB(value)) + } else { + const v = postOutDB(value) + if (query(v, id)) selected.push(v) + } + } + return selected +} + +/** + * Query posts by paged + */ +export async function queryPostPagedDB( + linked: PersonaIdentifier, + options: { + network: string + userIds: string[] + after?: PostIVIdentifier + page?: number + }, + count: number, +) { + const t = createTransaction(await db(), 'readonly')('post') + + const data: PostRecord[] = [] + let firstRecord = true + + for await (const cursor of t.objectStore('post').iterate()) { + if (cursor.value.encryptBy !== linked.toText()) continue + if (!options.userIds.includes(cursor.value.postBy.userId)) continue + + const postIdentifier = Identifier.fromString(cursor.value.identifier, PostIVIdentifier).unwrap() + if (postIdentifier.network !== options.network) continue + + if (firstRecord && options.after) { + cursor.continue(options.after.toText()) + firstRecord = false + continue + } + + if (postIdentifier === options.after) continue + + if (count <= 0) break + const outData = postOutDB(cursor.value) + count -= 1 + data.push(outData) + } + return data +} + +// #region db in and out +function postOutDB(db: PostDBRecord): PostRecord { + const { identifier, foundAt, postBy, recipients, postCryptoKey, encryptBy, interestedMeta, summary, url } = db + if (typeof recipients === 'object') { + for (const detail of recipients.values()) { + detail.reason.forEach((x) => x.type === 'group' && restorePrototype(x.group, GroupIdentifier.prototype)) + } + } + return { + identifier: Identifier.fromString(identifier, PostIVIdentifier).unwrap(), + postBy: restorePrototype(postBy, ProfileIdentifier.prototype), + recipients: recipients === true ? 'everyone' : new IdentifierMap(recipients, ProfileIdentifier), + foundAt: foundAt, + postCryptoKey: postCryptoKey, + encryptBy: encryptBy ? Identifier.fromString(encryptBy, ECKeyIdentifier).unwrapOr(undefined) : undefined, + interestedMeta, + summary, + url, + } +} +function postToDB(out: PostRecord): PostDBRecord { + return { + ...out, + identifier: out.identifier.toText(), + recipients: out.recipients === 'everyone' ? true : out.recipients.__raw_map__, + encryptBy: out.encryptBy?.toText(), + } +} +// #endregion diff --git a/packages/mask/src/extension/background-script/IdentityService.ts b/packages/mask/src/extension/background-script/IdentityService.ts index d68718dd774c..721ef764341d 100644 --- a/packages/mask/src/extension/background-script/IdentityService.ts +++ b/packages/mask/src/extension/background-script/IdentityService.ts @@ -248,6 +248,7 @@ export async function queryPagedPostHistory( network: string userIds: string[] after?: PostIVIdentifier + pageOffset?: number }, count: number, ) { diff --git a/packages/public-api/src/mobile.ts b/packages/public-api/src/mobile.ts index e9e97aed8556..4336502538c9 100644 --- a/packages/public-api/src/mobile.ts +++ b/packages/public-api/src/mobile.ts @@ -6,6 +6,7 @@ import type { ProfileRecord, LinkedProfileDetails, RelationRecord, + PostRecord, } from './types' /** @@ -102,6 +103,15 @@ export interface SharedNativeAPIs { delete_relation(params: { personaIdentifier: string; profileIdentifier: string }): Promise query_avatar(params: { identifier: string }): Promise store_avatar(params: { identifier: string; avatar: string }): Promise + create_post(params: { post: PostRecord }): Promise + query_post(params: { identifier: string }): Promise + query_posts(params: { + encryptBy?: string + userIds: string[] + network?: string + pageOption?: PageOption + }): Promise + update_post(params: { post: Partial; options: { mode: 0 | 1 } }): Promise } /** * APIs that only implemented by iOS Mask Network diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index 4210601ae269..89539ce63916 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -43,6 +43,42 @@ export interface PersonaRecord { uninitialized?: boolean } +type RecipientReason = ({ type: 'auto-share' } | { type: 'direct' } | { type: 'group'; group: any }) & { + at: number +} + +interface RecipientDetail { + reason: RecipientReason[] +} + +export interface PostRecord { + /** + * For old data stored before version 3, this identifier may be ProfileIdentifier.unknown + */ + postBy: string + identifier: string + postCryptoKey?: object + /** + * Receivers + */ + recipients: 'everyone' | Record + /** @deprecated */ + recipientGroups?: unknown + /** + * When does Mask find this post. + * For your own post, it is when Mask created this post. + * For others post, it is when you see it first time. + */ + foundAt: number + encryptBy?: string + /** The URL of this post */ + url?: string + /** Summary of this post (maybe front 20 chars). */ + summary?: string + /** Interested metadata contained in this post. */ + interestedMeta?: ReadonlyMap +} + export interface ProfileRecord { identifier: string nickname?: string From 6c61dea84239c386cb3051297b729e94d0087d7e Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Wed, 16 Feb 2022 18:32:37 +0800 Subject: [PATCH 06/15] fix: handle recipients everyone type --- packages/mask/background/database/post/app.ts | 25 ++++++++++++------- packages/public-api/src/types/index.ts | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index c357cad98597..9e6bb61e9aa9 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -1,6 +1,13 @@ import type { PostRecord as NativePostRecord } from '@masknet/public-api' -import type { PostRecord, PostReadWriteTransaction, PostReadOnlyTransaction } from './type' -import { PostIVIdentifier, Identifier, AESJsonWebKey, IdentifierMap, PersonaIdentifier } from '@masknet/shared-base' +import type { PostRecord, PostReadWriteTransaction, PostReadOnlyTransaction, RecipientDetail } from './type' +import { + PostIVIdentifier, + Identifier, + AESJsonWebKey, + IdentifierMap, + PersonaIdentifier, + ProfileIdentifier, +} from '@masknet/shared-base' import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { @@ -58,7 +65,7 @@ function postInNative(record: Partial & Pick & Pick, foundAt: new Date(record.foundAt), encryptBy: record.encryptBy ? Identifier.fromString(record.encryptBy).unwrap() : undefined, url: record.url, diff --git a/packages/public-api/src/types/index.ts b/packages/public-api/src/types/index.ts index 89539ce63916..6eeb8c317c3d 100644 --- a/packages/public-api/src/types/index.ts +++ b/packages/public-api/src/types/index.ts @@ -61,7 +61,7 @@ export interface PostRecord { /** * Receivers */ - recipients: 'everyone' | Record + recipients: Record /** @deprecated */ recipientGroups?: unknown /** From 7eb57463400d5b33c8f8fdcf51c48dea8c49194a Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Thu, 17 Feb 2022 10:35:40 +0800 Subject: [PATCH 07/15] chore: reply code review --- packages/mask/background/database/post/app.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index 9e6bb61e9aa9..ad0aa3adef7f 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -7,11 +7,12 @@ import { IdentifierMap, PersonaIdentifier, ProfileIdentifier, + ECKeyIdentifier, } from '@masknet/shared-base' import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { - return nativeAPI?.api.create_post({ post: postInNative(record) }) + return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) } export async function updatePostDB( @@ -58,23 +59,23 @@ export async function queryPostPagedDB( return results.map((r) => postOutNative(r)) } -function postInNative(record: Partial & Pick): NativePostRecord { +function postInNative(record: Partial & Pick): Partial { return { postBy: record.postBy ? record.postBy.toText() : undefined, identifier: record.identifier.toText(), postCryptoKey: record.postCryptoKey, recipients: record.recipients === 'everyone' - ? Object.fromEntries([]) + ? Object.fromEntries(new Map()) : record.recipients ? Object.fromEntries(record.recipients.__raw_map__) : undefined, - foundAt: record.foundAt ? record.foundAt.getTime() : undefined, + foundAt: record.foundAt?.getTime(), encryptBy: record.encryptBy?.toText(), url: record.url, summary: record.summary, interestedMeta: record.interestedMeta, - } as NativePostRecord + } } function postOutNative(record: NativePostRecord): PostRecord { @@ -87,11 +88,13 @@ function postOutNative(record: NativePostRecord): PostRecord { RecipientDetail >, foundAt: new Date(record.foundAt), - encryptBy: record.encryptBy ? Identifier.fromString(record.encryptBy).unwrap() : undefined, + encryptBy: record.encryptBy + ? (Identifier.fromString(record.encryptBy).unwrap() as unknown as ECKeyIdentifier) + : undefined, url: record.url, summary: record.summary, interestedMeta: record.interestedMeta, - } as PostRecord + } } // #region Not available on native. From d0868b90cd840701323327c767df4766dc037303 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Mon, 21 Feb 2022 11:15:04 +0800 Subject: [PATCH 08/15] chore: add debug code --- packages/mask/background/database/post/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index ad0aa3adef7f..e29f3f0c7a6c 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -12,6 +12,7 @@ import { import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { + console.log({ record }) return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) } From 62da62fce106cc1be0a10126aa4f8622d38ae9f4 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Mon, 21 Feb 2022 14:52:18 +0800 Subject: [PATCH 09/15] chore: add debug code --- packages/mask/background/database/post/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index e29f3f0c7a6c..ef99acdbac5e 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -12,7 +12,7 @@ import { import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { - console.log({ record }) + console.log({ record, post: postInNative(record) }) return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) } From 5d06a3ef541e845dda5a5d3b231cc9851366aca0 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Mon, 21 Feb 2022 16:49:29 +0800 Subject: [PATCH 10/15] chore: add debug code --- packages/mask/background/database/post/app.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index ef99acdbac5e..e11b48a1ca51 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -12,8 +12,12 @@ import { import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { - console.log({ record, post: postInNative(record) }) - return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) + try { + console.log({ record, post: postInNative(record) }) + return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) + } catch (error) { + console.log({ error }) + } } export async function updatePostDB( From ed39c43eb7d7bf85a790871b68690345d1f0d811 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Mon, 21 Feb 2022 17:18:11 +0800 Subject: [PATCH 11/15] chore: add debug code --- packages/mask/background/database/post/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index e11b48a1ca51..037a02d6747b 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -17,6 +17,7 @@ export async function createPostDB(record: PostRecord, t?: PostReadWriteTransact return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) } catch (error) { console.log({ error }) + return } } From 2e50e620845a1c8d5fd22f3f0af183615d709780 Mon Sep 17 00:00:00 2001 From: jk234ert Date: Tue, 22 Feb 2022 10:41:47 +0800 Subject: [PATCH 12/15] fix: createPostDB --- packages/mask/background/database/post/app.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index 037a02d6747b..89012fd66887 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -12,13 +12,7 @@ import { import { nativeAPI } from '../../../shared/native-rpc' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { - try { - console.log({ record, post: postInNative(record) }) - return nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) - } catch (error) { - console.log({ error }) - return - } + await nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) } export async function updatePostDB( From c6af408cfe22d377b41479a842468057789cc3b9 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Tue, 22 Feb 2022 12:50:28 +0800 Subject: [PATCH 13/15] fix: recipients --- packages/mask/background/database/post/app.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index 89012fd66887..64ac7363998b 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -1,5 +1,11 @@ import type { PostRecord as NativePostRecord } from '@masknet/public-api' -import type { PostRecord, PostReadWriteTransaction, PostReadOnlyTransaction, RecipientDetail } from './type' +import type { + PostRecord, + PostReadWriteTransaction, + PostReadOnlyTransaction, + RecipientDetail, + RecipientReason, +} from './type' import { PostIVIdentifier, Identifier, @@ -10,6 +16,7 @@ import { ECKeyIdentifier, } from '@masknet/shared-base' import { nativeAPI } from '../../../shared/native-rpc' +import { unreachable } from '@dimensiondev/kit' export async function createPostDB(record: PostRecord, t?: PostReadWriteTransaction) { await nativeAPI?.api.create_post({ post: postInNative(record) as NativePostRecord }) @@ -68,7 +75,14 @@ function postInNative(record: Partial & Pick [ + identifier.toText(), + { + reason: Array.from(detail.reason).map(RecipientReasonToJSON), + }, + ], + ) : undefined, foundAt: record.foundAt?.getTime(), encryptBy: record.encryptBy?.toText(), @@ -107,3 +121,18 @@ export async function queryPostsDB( export const PostDBAccess = () => undefined // #endregion + +type RecipientReasonJSON = ( + | { type: 'auto-share' } + | { type: 'direct' } + | { type: 'group'; group: string /** GroupIdentifier */ } +) & { + at: number +} + +function RecipientReasonToJSON(y: RecipientReason): RecipientReasonJSON { + if (y.type === 'direct' || y.type === 'auto-share') + return { at: y.at.getTime(), type: y.type } as RecipientReasonJSON + else if (y.type === 'group') return { at: y.at.getTime(), group: y.group.toText(), type: y.type } + return unreachable(y) +} From d317805120dc3793f18b9af22c81f933d7383152 Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Tue, 22 Feb 2022 15:51:13 +0800 Subject: [PATCH 14/15] fix: recipients --- packages/mask/background/database/post/app.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index 64ac7363998b..0952f91dfd75 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -75,14 +75,11 @@ function postInNative(record: Partial & Pick [ - identifier.toText(), - { - reason: Array.from(detail.reason).map(RecipientReasonToJSON), - }, - ], - ) + ? Array.from(record.recipients).map(([identifier, detail]) => ({ + [identifier.toText()]: { + reason: Array.from(detail.reason).map(RecipientReasonToJSON), + }, + })) : undefined, foundAt: record.foundAt?.getTime(), encryptBy: record.encryptBy?.toText(), From cca5d7291dce98b0186401626068971a1e4e23bd Mon Sep 17 00:00:00 2001 From: zhouhanseng Date: Tue, 22 Feb 2022 16:35:35 +0800 Subject: [PATCH 15/15] fix: recipients --- packages/mask/background/database/post/app.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/mask/background/database/post/app.ts b/packages/mask/background/database/post/app.ts index 0952f91dfd75..f23575109190 100644 --- a/packages/mask/background/database/post/app.ts +++ b/packages/mask/background/database/post/app.ts @@ -75,11 +75,14 @@ function postInNative(record: Partial & Pick ({ - [identifier.toText()]: { - reason: Array.from(detail.reason).map(RecipientReasonToJSON), - }, - })) + ? Object.fromEntries( + Array.from(record.recipients).map(([identifier, detail]) => [ + identifier.toText(), + { + reason: Array.from(detail.reason).map(RecipientReasonToJSON), + }, + ]), + ) : undefined, foundAt: record.foundAt?.getTime(), encryptBy: record.encryptBy?.toText(),