diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-28 19:56:02 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-28 19:56:02 -0500 |
commit | 87645bfad3943e1110e4cb2e038124083e8ae793 (patch) | |
tree | c327a4437c98d973f45c313cf8259ad75515c4fe /extension/src/features | |
parent | c438ee90e3be4e5e01ae3d045d6b841a03bd46eb (diff) |
progress update
Diffstat (limited to 'extension/src/features')
-rw-r--r-- | extension/src/features/auth-api.ts | 5 | ||||
-rw-r--r-- | extension/src/features/framework/index.ts | 26 | ||||
-rw-r--r-- | extension/src/features/history.ts | 64 | ||||
-rw-r--r-- | extension/src/features/identity-api.ts | 8 | ||||
-rw-r--r-- | extension/src/features/index.ts | 6 | ||||
-rw-r--r-- | extension/src/features/nip07allow-api.ts | 7 | ||||
-rw-r--r-- | extension/src/features/nostr-api.ts | 28 | ||||
-rw-r--r-- | extension/src/features/permissions.ts | 158 | ||||
-rw-r--r-- | extension/src/features/server-api/index.ts | 25 | ||||
-rw-r--r-- | extension/src/features/settings.ts | 13 | ||||
-rw-r--r-- | extension/src/features/tagfilter-api.ts | 138 | ||||
-rw-r--r-- | extension/src/features/types.ts | 25 | ||||
-rw-r--r-- | extension/src/features/util.ts | 85 |
13 files changed, 399 insertions, 189 deletions
diff --git a/extension/src/features/auth-api.ts b/extension/src/features/auth-api.ts index fbc9420..4e4d0ed 100644 --- a/extension/src/features/auth-api.ts +++ b/extension/src/features/auth-api.ts @@ -22,7 +22,7 @@ import { IMfaFlowContinuiation, totpMfaProcessor, useMfaLogin, usePkiAuth, useSe } from "@vnuge/vnlib.browser"; import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, popupAndOptionsOnly, popupOnly } from "./framework"; import { waitForChangeFn } from "./util"; -import type { ClientStatus } from "./types"; +import type { ClientStatus, Watchable } from "./types"; import type { AppSettings } from "./settings"; import type { JsonObject } from "type-fest"; @@ -39,12 +39,11 @@ export interface ApiMessageHandler<T extends JsonObject> { (message: T, apiHandle: { axios: AxiosInstance }): Promise<any> } -export interface UserApi extends FeatureApi { +export interface UserApi extends FeatureApi, Watchable { login(username: string, password?: string): Promise<boolean> logout: () => Promise<void> getProfile: () => Promise<any> getStatus: () => Promise<ClientStatus> - waitForChange: () => Promise<void> submitMfa: (submission: IMfaSubmission) => Promise<boolean> } diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts index b545335..755d27e 100644 --- a/extension/src/features/framework/index.ts +++ b/extension/src/features/framework/index.ts @@ -25,6 +25,7 @@ export interface BgRuntime<T> { readonly state: T; onInstalled(callback: () => Promise<void>): void; onConnected(callback: () => Promise<void>): void; + openBackChannel<T extends FeatureApi>(name: string, callback: (feature: T | undefined) => void): void; } export type FeatureApi = { @@ -84,6 +85,8 @@ export const protectMethod = <T extends Function>(func: T, ...protection: Channe return func; } +type BgCallback = (feature: FeatureApi | undefined) => void + /** * Creates a background runtime context for registering background * script feature api handlers @@ -92,12 +95,26 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper const { openOnMessageChannel } = createMessageChannel('background'); const { onMessage } = openOnMessageChannel() + + const backChannels = new Map<string, BgCallback>() + + const openBackChannel = async (name: string, callback: BgCallback) => { + backChannels.set(name, callback) + } + const notifyBackChannels = (pool: Map<string, FeatureApi>) => { + //Loop through all features + for (const [name, waiter] of backChannels.entries()){ + //Notify the waiter of the feature + waiter(pool.get(name)) + } + } const rt = { state, onConnected: runtime.onConnect.addListener, onInstalled: runtime.onInstalled.addListener, + openBackChannel } as BgRuntime<TState> /** @@ -109,12 +126,18 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper return{ register: <TFeature extends FeatureApi>(features: FeatureConstructor<TState, TFeature>[]) => { + + const featurePool = new Map<string, FeatureApi>() + //Loop through features for (const feature of features) { //Init feature const f = feature().background(rt) + //Add to pool + featurePool.set(feature.name, f) + //Get all exported function for (const externFuncName in f) { @@ -155,6 +178,9 @@ export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper }); } } + + //Notify all back channels that the load is complete + notifyBackChannels(featurePool) } } } diff --git a/extension/src/features/history.ts b/extension/src/features/history.ts index 82f31c4..e00a9af 100644 --- a/extension/src/features/history.ts +++ b/extension/src/features/history.ts @@ -13,29 +13,67 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -import { ref } from "vue"; -import { } from "./types"; -import { FeatureApi, BgRuntime, IFeatureExport } from "./framework"; +import { shallowRef } from "vue"; +import { watchDebounced, set, get, useToggle } from '@vueuse/core' +import { EventEntry, NostrEvent, Watchable } from "./types"; +import { FeatureApi, BgRuntime, IFeatureExport, exportForegroundApi, optionsOnly } from "./framework"; import { AppSettings } from "./settings"; +import { waitForChangeFn } from "./util"; +import { Endpoints } from "./server-api"; +import { useSession } from "@vnuge/vnlib.browser"; +import { } from "lodash"; -export interface HistoryEvent extends Object{ - +export interface SignedNEvent extends NostrEvent { + readonly signature: string } -export interface HistoryApi extends FeatureApi{ - +export interface HistoryApi extends FeatureApi, Watchable{ + getEvents: () => Promise<EventEntry[]>; + deleteEvent: (entry: EventEntry) => Promise<void>; + refresh: () => Promise<void>; } export const useHistoryApi = () : IFeatureExport<AppSettings, HistoryApi> => { return{ - background: ({ }: BgRuntime<AppSettings>): HistoryApi =>{ - const evHistory = ref([]); + background: ({ state }: BgRuntime<AppSettings>): HistoryApi =>{ + const { loggedIn } = useSession(); + const { execRequest } = state.useServerApi(); + const [ onRefresh, refresh ] = useToggle() + + const history = shallowRef<EventEntry[]>([]); + + //Watch for login changes and manual refreshes + watchDebounced([loggedIn, onRefresh], async ([li]) => { + + if(!li){ + set(history, []) + return + } + + //load history from server + history.value = await execRequest(Endpoints.GetHistory); + + }, { debounce: 1000 }) + + return{ + waitForChange:waitForChangeFn([history]), - return{ } + getEvents: () => Promise.resolve(history.value), + deleteEvent: optionsOnly(async (entry: EventEntry) => { + await execRequest(Endpoints.DeleteSingleEvent, entry) + refresh() + }), + refresh () { + refresh() + return Promise.resolve() + } + } }, - foreground: (): HistoryApi =>{ - return { } - } + foreground: exportForegroundApi<HistoryApi>([ + 'waitForChange', + 'getEvents', + 'deleteEvent', + ]) } } diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts index 0b8973d..73f7ab2 100644 --- a/extension/src/features/identity-api.ts +++ b/extension/src/features/identity-api.ts @@ -28,10 +28,10 @@ import { shallowRef } from "vue"; import { useSession } from "@vnuge/vnlib.browser"; import { set, useToggle, watchDebounced } from "@vueuse/core"; import { isArray } from "lodash"; -import { waitForChange, waitForChangeFn } from "./util"; +import { waitForChangeFn } from "./util"; export interface IdentityApi extends FeatureApi, Watchable { - createIdentity: (identity: NostrPubKey) => Promise<NostrPubKey> + createIdentity: (identity: Partial<NostrPubKey>) => Promise<NostrPubKey> updateIdentity: (identity: NostrPubKey) => Promise<NostrPubKey> deleteIdentity: (key: NostrPubKey) => Promise<void> getAllKeys: () => Promise<NostrPubKey[]>; @@ -65,9 +65,7 @@ export const useIdentityApi = (): IFeatureExport<AppSettings, IdentityApi> => { selectedKey.value = undefined; } - //Wait for changes to trigger a new key-load - await waitForChange([ loggedIn, onKeyUpdateTriggered ]) - }, { debounce: 100 }) + }, { debounce: 100, immediate: true }) return { //Identity is only available in options context diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts index 0a8e182..d7e1b05 100644 --- a/extension/src/features/index.ts +++ b/extension/src/features/index.ts @@ -14,7 +14,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. //Export all shared types -export type { NostrPubKey, LoginMessage } from './types' +export type { NostrPubKey, LoginMessage, NostrEvent, NostrRelay, EventEntry } from './types' export type * from './framework' export type { PluginConfig } from './settings' export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './pki-api' @@ -33,6 +33,6 @@ export { useSettingsApi, useAppSettings } from './settings' export { useHistoryApi } from './history' export { useEventTagFilterApi } from './tagfilter-api' export { useInjectAllowList } from './nip07allow-api' -export { onWatchableChange } from './util' +export { onWatchableChange, waitOne, useQuery } from './util' export { useMfaConfigApi } from './mfa-api' -export { usePermissionApi, PrStatus } from './permissions'
\ No newline at end of file +export { usePermissionApi, PrStatus, CreateRuleType } from './permissions'
\ No newline at end of file diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts index 4787437..89639d1 100644 --- a/extension/src/features/nip07allow-api.ts +++ b/extension/src/features/nip07allow-api.ts @@ -20,7 +20,7 @@ import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi, popupAndOpt import { AppSettings } from "./settings"; import { set, get, toRefs } from "@vueuse/core"; import { computed, shallowRef } from "vue"; -import { waitForChangeFn } from "./util"; +import { waitForChangeFn, push, remove } from "./util"; interface AllowedSites{ origins: string[]; @@ -99,7 +99,7 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis //See if origin is already in the list if (!includes(origins.value, originOnly)) { //Add to the list - origins.value.push(originOnly); + push(origins, originOnly); //If current tab was added, reload the tab if (!origin) { @@ -117,10 +117,9 @@ export const useInjectAllowList = (): IFeatureExport<AppSettings, InjectAllowlis //Get origin part of url const delOriginOnly = new URL(delOrigin).origin - const allowList = get(origins) //Remove the origin - origins.value = filter(allowList, (o) => !isEqual(o, delOriginOnly)); + remove(origins, delOriginOnly) //If current tab was removed, reload the tab if (!origin) { diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts index 4aee660..7f1bedf 100644 --- a/extension/src/features/nostr-api.ts +++ b/extension/src/features/nostr-api.ts @@ -17,8 +17,9 @@ import { cloneDeep } from "lodash"; import { Endpoints } from "./server-api"; import { type FeatureApi, type BgRuntime, type IFeatureExport, optionsOnly, exportForegroundApi } from "./framework"; import { type AppSettings } from "./settings"; -import { useTagFilter } from "./tagfilter-api"; +import { EventTagFilterApi } from "./tagfilter-api"; import type { NostrRelay, EncryptionRequest, NostrEvent } from './types'; +import { HistoryApi } from "./history"; /** @@ -36,10 +37,16 @@ export interface NostrApi extends FeatureApi { export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => { return{ - background: ({ state }: BgRuntime<AppSettings>) =>{ + background: ({ state, openBackChannel }: BgRuntime<AppSettings>) =>{ const { execRequest } = state.useServerApi(); - const { filterTags } = useTagFilter(state) + + let tagFilter: EventTagFilterApi | undefined; + let evHistory: HistoryApi | undefined; + + //Register for tag filter, and history back channel + openBackChannel<EventTagFilterApi>('useEventTagFilterApi', (feature) => tagFilter = feature) + openBackChannel<HistoryApi>('useHistoryApi', (feature) => evHistory = feature) return { getRelays: async (): Promise<NostrRelay[]> => { @@ -50,16 +57,21 @@ export const useNostrApi = (): IFeatureExport<AppSettings, NostrApi> => { signEvent: async (req: NostrEvent): Promise<NostrEvent | undefined> => { //Store copy to prevent mutation - req = cloneDeep(req) + const event = cloneDeep(req) //If tag filter is enabled, filter before continuing - if(state.currentConfig.value.tagFilter){ - await filterTags(req) + if (tagFilter){ + //Filter tags + await tagFilter.filterTags(event); } //Sign the event - const event = await execRequest(Endpoints.SignEvent, req); - return event; + const result = await execRequest(Endpoints.SignEvent, event); + + //Refresh history + evHistory?.refresh(); + + return result; }, nip04Encrypt: async (data: EncryptionRequest): Promise<string> => { const message: EncryptionRequest = { diff --git a/extension/src/features/permissions.ts b/extension/src/features/permissions.ts index 5473962..82b023f 100644 --- a/extension/src/features/permissions.ts +++ b/extension/src/features/permissions.ts @@ -13,13 +13,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -import { Mutable, get, set, toRefs } from "@vueuse/core"; +import { Mutable, get, set, toRefs, useTimestamp } from "@vueuse/core"; import { Ref } from "vue"; import { defaultTo, defaults, defer, filter, find, forEach, isEqual, isNil } from "lodash"; import { nanoid } from "nanoid"; -import { useSession } from "@vnuge/vnlib.browser"; +import { debugLog, useSession } from "@vnuge/vnlib.browser"; import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, optionsOnly } from "./framework"; -import { waitForChangeFn, waitOne } from "./util"; +import { remove, waitForChangeFn, waitOne } from "./util"; import { windows, runtime, Windows, tabs } from "webextension-polyfill"; import type { TotpUpdateMessage, Watchable } from "./types"; import type { AppSettings } from "./settings"; @@ -27,6 +27,7 @@ import type { AppSettings } from "./settings"; export interface AutoAllowRule{ origin: string type: string + readonly expires?: number readonly timestamp: number } @@ -37,6 +38,16 @@ export enum PrStatus{ Denied } +export enum CreateRuleType{ + AllowOnce, + AllowForever, + FiveMinutes, + OneHour, + OneDay, + OneWeek, + OneMonth, +} + export interface PermissionRequest{ readonly uuid: string readonly origin: string @@ -49,7 +60,7 @@ export type MfaUpdateResult = TotpUpdateMessage export interface PermissionApi extends FeatureApi, Watchable { getRequests(): Promise<PermissionRequest[]> - allow(requestId: string, addRule: boolean): Promise<void> + allow(requestId: string, addRule: CreateRuleType): Promise<void> deny(requestId: string): Promise<void> clearRequests(): Promise<void> requestAndWaitResult(request: Partial<PermissionRequest>): Promise<PrStatus> @@ -67,6 +78,24 @@ interface RuleSlot{ rules: AutoAllowRule[] } +const setExpirationRule = (expType: CreateRuleType): { expires?: number } => { + switch (expType) { + case CreateRuleType.AllowOnce: + case CreateRuleType.AllowForever: + return { } + case CreateRuleType.FiveMinutes: + return { expires: Date.now() + (5 * 60 * 1000) }; + case CreateRuleType.OneHour: + return { expires: Date.now() + (60 * 60 * 1000) }; + case CreateRuleType.OneDay: + return { expires: Date.now() + (24 * 60 * 60 * 1000) }; + case CreateRuleType.OneWeek: + return { expires: Date.now() + (7 * 24 * 60 * 60 * 1000) }; + case CreateRuleType.OneMonth: + return { expires: Date.now() + (30 * 24 * 60 * 60 * 1000) }; + } +} + const useRuleSet = (slot: Ref<RuleSlot>) => { defaults(slot.value, { rules: [] }) @@ -76,6 +105,14 @@ const useRuleSet = (slot: Ref<RuleSlot>) => { isAllowed: (request: PermissionRequest): boolean => { //find existing rule const rule = find(get(rules), r => isEqual(r.origin, request.origin) && isEqual(r.type, request.requestType)) + + //See if rule exists and is expired + if (rule && rule.expires && rule.expires < Date.now()) { + //remove expired rule + remove(rules, rule) + return false + } + return !isNil(rule) }, addRule: (rule: Partial<AutoAllowRule>) => { @@ -99,7 +136,12 @@ const useRuleSet = (slot: Ref<RuleSlot>) => { const wo = filter(get(rules), r => !(isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) set(rules, wo) }, - getRules:(): AutoAllowRule[] =>get(rules) + getRules:(): AutoAllowRule[] => { + //Filter all expired rules + const wo = filter(get(rules), r => !r.expires || r.expires > Date.now()) + set(rules, wo) + return wo + } } } @@ -110,41 +152,13 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR defaults(slot.value, { rules: [] }) const { requests } = toRefs(slot) - const drawWindow = async ({ uuid }: Partial<PermissionRequest>): Promise<Windows.CreateCreateDataType> => { - const current = await windows.getCurrent() - - const minWidth = 350 - const minHeight = 180 - - const maxWidth = 500 - const maxHeight = 250 - - const width = Math.min(Math.max(current.width! - 100, minWidth), maxWidth) - const height = Math.min(Math.max(current.height! - 100, minHeight), maxHeight) - - //draw half way across screen minus half its width - const left = current.left! + (current.width! / 2) - (width / 2) - - return { - url: `${permPopupUrl}?uuid=${uuid}&closeable`, - type: "popup", - height: height, - width: width, - focused: true, - allowScriptsToClose: true, - top: 100, - //try to center popup - left: left, - } - } - const activePopups = new Map<number, PermissionRequest>() const getRequest = (requestId: string): PermissionRequest | undefined => { return find(get(requests), r => r.uuid === requestId) } - const updateRequest = (request: PermissionRequest, addRule: boolean) => { + const updateRequest = (request: PermissionRequest, addRule: CreateRuleType) => { const current = get(requests) const index = current.findIndex(r => r.uuid === request.uuid) @@ -159,8 +173,14 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR set(requests, current) //Add rule if needed - if (addRule) { - rules.addRule({ origin: request.origin, type: request.requestType }) + switch (addRule) { + case CreateRuleType.AllowOnce: + //Do nothing + break; + //Compute expiration + default: + const { expires } = setExpirationRule(addRule); + rules.addRule({ origin: request.origin, type: request.requestType, expires }) } } @@ -173,6 +193,41 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR } as PermissionRequest } + const showPermsWindow = async (request: PermissionRequest): Promise<void> => { + + const drawWindow = async ({ uuid }: Partial<PermissionRequest>): Promise<Windows.CreateCreateDataType> => { + const current = await windows.getCurrent() + + const minWidth = 350 + const minHeight = 180 + + const maxWidth = 500 + const maxHeight = 250 + + const width = Math.min(Math.max(current.width! - 100, minWidth), maxWidth) + const height = Math.min(Math.max(current.height! - 100, minHeight), maxHeight) + + //draw half way across screen minus half its width + const left = current.left! + (current.width! / 2) - (width / 2) + + return { + url: `${permPopupUrl}?uuid=${uuid}&closeable`, + type: "popup", + height: height, + width: width, + focused: true, + allowScriptsToClose: true, + top: 100, + //try to center popup + left: left, + } + } + + const windowsArgs = await drawWindow(request) + const { id } = await windows.create(windowsArgs) + activePopups.set(id!, request) + } + //Listen for popup close to cleanup request windows.onRemoved.addListener(async (id) => { const req = activePopups.get(id) @@ -199,12 +254,6 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR return{ getRequest, - async showPermsWindow (request: PermissionRequest): Promise<void> { - const windowsArgs = await drawWindow(request) - const { id } = await windows.create(windowsArgs) - activePopups.set(id!, request) - }, - pushRequest (request: Partial<PermissionRequest>, showPopup: boolean): PermissionRequest { //Create new request const req = initNewRequest(request) @@ -223,13 +272,13 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR //Show popup if needed if (showPopup) { - this.showPermsWindow(req) + showPermsWindow(req) } return req }, - allow (requestId: string, addRule: boolean): void { + allow(requestId: string, addRule: CreateRuleType): void { const request = getRequest(requestId) if(!request){ throw new Error("Request not found") @@ -248,7 +297,7 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR //set denied (request as Mutable<PermissionRequest>).status = PrStatus.Denied //update request - updateRequest(request, false) + updateRequest(request, CreateRuleType.AllowOnce) }, clearAll: () => { @@ -257,12 +306,13 @@ const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useR //set denied (r as Mutable<PermissionRequest>).status = PrStatus.Denied //update request - updateRequest(r, false) + updateRequest(r, CreateRuleType.AllowOnce) }) //Then defer clear defer(() => set(requests, [])) }, + getAll: () => get(requests) } } @@ -282,8 +332,11 @@ export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> = const ruleSet = useRuleSet(ruleStore) const permissions = usePermissions(reqStore, ruleSet) + //Computed current time to trigger an update every second + const currentTime = useTimestamp({ interval: 1000 }) + return { - waitForChange: waitForChangeFn([currentConfig, loggedIn, reqStore, ruleStore]), + waitForChange: waitForChangeFn([currentConfig, loggedIn, reqStore, ruleStore, currentTime]), getRequests: () => Promise.resolve(permissions.getAll()), @@ -292,7 +345,7 @@ export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> = return Promise.resolve() }, - allow(requestId: string, addRule: boolean) { + allow(requestId: string, addRule: CreateRuleType) { permissions.allow(requestId, addRule) return Promise.resolve() }, @@ -304,8 +357,11 @@ export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> = }), async requestAndWaitResult(request: Partial<PermissionRequest>) { - //push request - const req = permissions.pushRequest(request, true) + + debugLog("Requesting permission", request) + + //push request and show popup only if enabled + const req = permissions.pushRequest(request, currentConfig.value.authPopup) //See if pending if(req.status !== PrStatus.Pending){ @@ -316,7 +372,7 @@ export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> = do { //wait for a change - await waitOne([reqStore]) + await waitOne([ reqStore ]) //check if request was approved const status = permissions.getRequest(req.uuid); diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts index b9524ed..35bed6f 100644 --- a/extension/src/features/server-api/index.ts +++ b/extension/src/features/server-api/index.ts @@ -19,7 +19,7 @@ import { get } from '@vueuse/core' import { type WebMessage, type UserProfile } from "@vnuge/vnlib.browser" import { initEndponts } from "./endpoints" import { cloneDeep } from "lodash" -import type { EncryptionRequest, NostrEvent, NostrPubKey, NostrRelay } from "../types" +import type { EncryptionRequest, EventEntry, NostrEvent, NostrPubKey, NostrRelay } from "../types" export enum Endpoints { GetKeys = 'getKeys', @@ -32,6 +32,8 @@ export enum Endpoints { CreateId = 'createIdentity', UpdateId = 'updateIdentity', UpdateProfile = 'updateProfile', + GetHistory = 'getEvents', + DeleteSingleEvent = 'deleteSingleEvent', } export interface ExecRequestHandler{ @@ -45,6 +47,8 @@ export interface ExecRequestHandler{ (id: Endpoints.CreateId, identity: NostrPubKey):Promise<NostrPubKey> (id: Endpoints.UpdateId, identity: NostrPubKey):Promise<NostrPubKey> (id: Endpoints.UpdateProfile, profile: UserProfile):Promise<string> + (id: Endpoints.GetHistory):Promise<EventEntry[]> + (id: Endpoints.DeleteSingleEvent, evntId: EventEntry):Promise<void> } export interface ServerApi{ @@ -65,7 +69,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server registerEndpoint({ id: Endpoints.DeleteKey, method: 'DELETE', - path: (key: NostrPubKey) => `${get(nostrUrl)}?type=identity&key_id=${key.Id}`, + path: (key: NostrPubKey) => `${get(nostrUrl)}?type=identity&id=${key.Id}`, onRequest: () => Promise.resolve(), onResponse: async (response: WebMessage) => response.getResultOrThrow() }) @@ -147,5 +151,22 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server onResponse: async (response: WebMessage<string>) => response.getResultOrThrow() }) + //History api + registerEndpoint({ + id: Endpoints.GetHistory, + method: 'GET', + path: () => `${get(nostrUrl)}?type=getEvents`, + onRequest: () => Promise.resolve(), + onResponse: (response : EventEntry[]) => Promise.resolve(response) //Pass through response, should be an array of events or an error + }) + + registerEndpoint({ + id: Endpoints.DeleteSingleEvent, + method: 'DELETE', + path: (evnt: EventEntry) => `${get(nostrUrl)}?type=event&id=${evnt.Id}`, + onRequest: () => Promise.resolve(), + onResponse: (response) => Promise.resolve(response) + }) + return { execRequest } }
\ No newline at end of file diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts index ca714a5..0bd7101 100644 --- a/extension/src/features/settings.ts +++ b/extension/src/features/settings.ts @@ -14,7 +14,7 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. import { storage } from "webextension-polyfill" -import { } from 'lodash' +import { defaultsDeep } from 'lodash' import { configureApi, debugLog } from '@vnuge/vnlib.browser' import { MaybeRefOrGetter, readonly, Ref, shallowRef, watch } from "vue"; import { JsonObject } from "type-fest"; @@ -30,18 +30,20 @@ export interface PluginConfig extends JsonObject { readonly nostrEndpoint: string; readonly heartbeat: boolean; readonly maxHistory: number; - readonly tagFilter: boolean, + readonly tagFilter: boolean; + readonly authPopup: boolean; } //Default storage config -const defaultConfig : PluginConfig = { +const defaultConfig : PluginConfig = Object.freeze({ apiUrl: import.meta.env.VITE_API_URL, accountBasePath: import.meta.env.VITE_ACCOUNTS_BASE_PATH, nostrEndpoint: import.meta.env.VITE_NOSTR_ENDPOINT, heartbeat: import.meta.env.VITE_HEARTBEAT_ENABLED === 'true', maxHistory: 50, tagFilter: true, -}; + authPopup: true, +}); export interface AppSettings{ saveConfig(config: PluginConfig): void; @@ -62,6 +64,9 @@ export const useAppSettings = (): AppSettings => { const _storageBackend = storage.local; const store = useStorage<PluginConfig>(_storageBackend, 'siteConfig', defaultConfig); + //Merge the default config for nullables with the current config on startyup + defaultsDeep(store.value, defaultConfig); + watch(store, (config, _) => { //Configure the vnlib api configureApi({ diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts index 22369d0..b8778a1 100644 --- a/extension/src/features/tagfilter-api.ts +++ b/extension/src/features/tagfilter-api.ts @@ -17,8 +17,8 @@ import { TaggedNostrEvent, Watchable } from "./types"; import { filter, isEmpty, isEqual, isRegExp } from "lodash"; import { BgRuntime, FeatureApi, IFeatureExport, exportForegroundApi } from "./framework"; import { AppSettings } from "./settings"; -import { get, toRefs } from "@vueuse/core"; -import { waitForChangeFn } from "./util"; +import { get, toRefs, set } from "@vueuse/core"; +import { push, remove, waitForChangeFn } from "./util"; interface EventTagFilteStorage { filters: string[]; @@ -28,98 +28,90 @@ interface EventTagFilteStorage { export interface EventTagFilterApi extends FeatureApi, Watchable { filterTags(event: TaggedNostrEvent): Promise<void>; addFilter(tag: string): Promise<void>; + addFilter(tags: string[]): Promise<void>; removeFilter(tag: string): Promise<void>; - addFilters(tags: string[]): Promise<void>; isEnabled(): Promise<boolean>; enable(value:boolean): Promise<void>; } -export const useTagFilter = (settings: AppSettings): EventTagFilterApi => { - //use storage - const store = settings.useStorageSlot<EventTagFilteStorage>('tag-filter-struct', { filters: [], enabled: false }); - const { filters, enabled } = toRefs(store) - - return { - waitForChange: waitForChangeFn([filters, enabled]), - filterTags: async (event: TaggedNostrEvent): Promise<void> => { - - if(!event.tags){ - return; - } +export const useEventTagFilterApi = (): IFeatureExport<AppSettings, EventTagFilterApi> => { + return{ + background: ({ state }: BgRuntime<AppSettings>) => { - if(isEmpty(event.tags)){ - return; - } + //use storage + const store = state.useStorageSlot<EventTagFilteStorage>('tag-filter-struct', { filters: [], enabled: false }); - /* - * Nostr events contain a nested array of tags, they may be any - * json type. The first element of the array should be the tag name - * and the rest of the array is the tag data. - */ - const allowedTags = filter(event.tags, ([tagName]) => { - if(!tagName){ - return false; - } + const { filters, enabled } = toRefs(store) - if(!filters.value.length){ - return true; - } + return{ + waitForChange: waitForChangeFn([filters, enabled]), + filterTags (event: TaggedNostrEvent): Promise<void> { - const asString = tagName.toString(); + if (!event.tags || isEmpty(event.tags)) { + return Promise.resolve(); + } - for (const filter of get(filters)) { - //if the filter is a regex, test it, if it fails, its allowed - if (isRegExp(filter)) { - if (filter.test(asString)) { + /* + * Nostr events contain a nested array of tags, they may be any + * json type. The first element of the array should be the tag name + * and the rest of the array is the tag data. + */ + const allowedTags = filter(event.tags, ([tagName]) => { + //May be an undefined tag, so ignore it + if (!tagName) { return false; } - } - //If the filter is a string, compare it, if it matches, it's not allowed - if (isEqual(filter, asString)) { - return false; - } - } - //Its allowed - return true; - }) + if (!filters.value.length) { + return true; + } + + const asString = tagName.toString(); - //overwrite tags array - event.tags = allowedTags; - }, - addFilter: async (tag: string) => { - //add new filter to list - filters.value.push(tag); - }, - removeFilter: async (tag: string) => { - //remove filter from list - filters.value = filter(filters.value, t => !isEqual(t, tag)); - }, - addFilters: async (tags: string[]) => { - //add new filters to list - filters.value.push(...tags); - }, - isEnabled: async () => { - return enabled.value; - }, - enable: async (value:boolean) => { - enabled.value = value; - } - } -} + for (const filter of get(filters)) { + //if the filter is a regex, test it, if it fails, its allowed + if (isRegExp(filter)) { + if (filter.test(asString)) { + return false; + } + } + //If the filter is a string, compare it, if it matches, it's not allowed + if (isEqual(filter, asString)) { + return false; + } + } -export const useEventTagFilterApi = (): IFeatureExport<AppSettings, EventTagFilterApi> => { - return{ - background: ({ state }: BgRuntime<AppSettings>) => { - return{ - ...useTagFilter(state) + //Its allowed + return true; + }) + + //overwrite tags array + event.tags = allowedTags; + return Promise.resolve(); + }, + addFilter(tags: string | string[]) { + //add new filter to list + push(filters, tags) + return Promise.resolve(); + }, + removeFilter(tag: string) { + //remove filter from list + remove(filters, tag); + return Promise.resolve(); + }, + isEnabled: () => Promise.resolve(enabled.value), + enable (value: boolean) { + set(enabled, value); + return Promise.resolve(); + } } }, foreground: exportForegroundApi([ 'filterTags', 'addFilter', 'removeFilter', - 'addFilters', + 'isEnabled', + 'enable' ]) } }
\ No newline at end of file diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts index fe59011..4689ccc 100644 --- a/extension/src/features/types.ts +++ b/extension/src/features/types.ts @@ -15,14 +15,21 @@ import { JsonObject } from "type-fest"; -export interface NostrPubKey extends JsonObject { +export interface DbEntry extends JsonObject { readonly Id: string, - readonly UserName: string, - readonly PublicKey: string, readonly Created: string, readonly LastModified: string } +export interface NostrPubKey extends DbEntry { + readonly UserName: string, + readonly PublicKey: string, +} + +export interface EventEntry extends DbEntry { + readonly EventData: string +} + export interface NostrEvent extends JsonObject { KeyId: string, readonly id: string, @@ -48,12 +55,9 @@ export interface EncryptionRequest extends JsonObject { readonly pubkey: string } -export interface NostrRelay extends JsonObject { - readonly Id: string, - readonly url: string, - readonly flags: number, - readonly Created: string, - readonly LastModified: string +export interface NostrRelay extends DbEntry { + readonly url: string; + readonly flags: number; } export interface LoginMessage extends JsonObject { @@ -105,4 +109,5 @@ export interface TotpUpdateMessage extends JsonObject { readonly period: number readonly algorithm: string readonly secret: string -}
\ No newline at end of file +} + diff --git a/extension/src/features/util.ts b/extension/src/features/util.ts index 6ec8f15..53f6ffd 100644 --- a/extension/src/features/util.ts +++ b/extension/src/features/util.ts @@ -14,24 +14,25 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. -import { defer } from "lodash"; -import { RemovableRef, SerializerAsync, StorageLikeAsync, useStorageAsync, watchOnce } from "@vueuse/core"; -import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw } from "vue"; +import { defer, filter, isEqual } from "lodash"; +import { RemovableRef, SerializerAsync, StorageLikeAsync, useStorageAsync, watchOnce, get, set } from "@vueuse/core"; +import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw, MaybeRef, shallowRef } from "vue"; import type { Watchable } from "./types"; export const waitForChange = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]):Promise<void> => { - return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) + return new Promise((resolve) => watchOnce<any>(source, () => defer(() => resolve()), { deep: true })) } export const waitForChangeFn = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]) => { - return (): Promise<void> => { - return new Promise((resolve) => watchOnce<any>(source, () => resolve(), {deep: true})) - } + return (): Promise<void> => waitForChange(source) } -export const waitOne = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]): Promise<void> => { - return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) -} +/** + * Waits for a change to occur on the given watch source + * once. + * @returns A promise that resolves when the change occurs. + */ +export const waitOne = waitForChange; export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, initialValue: MaybeRefOrGetter<T>): RemovableRef<T> => { @@ -69,10 +70,10 @@ export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: st } } - return useStorageAsync<T>(key, initialValue, wrapper, { serializer, deep: true, shallow: true }); + return useStorageAsync<T>(key, initialValue, wrapper, { serializer, shallow: true }); } -export const onWatchableChange = (watchable: Watchable, onChangeCallback: () => Promise<any>, controls?: { immediate: boolean }) => { +export const onWatchableChange = ({ waitForChange }: Watchable, onChangeCallback: () => Promise<any>, controls?: { immediate: boolean }) => { defer(async () => { if (controls?.immediate) { @@ -80,8 +81,66 @@ export const onWatchableChange = (watchable: Watchable, onChangeCallback: () => } while (true) { - await watchable.waitForChange(); + await waitForChange(); await onChangeCallback(); } }) +} + +export const push = <T>(arr: MaybeRef<T[]>, item: T | T[]) => { + //get the reactuve value first + const current = get(arr) + if (Array.isArray(item)) { + //push the items + current.push(...item) + } else { + //push the item + current.push(item) + } + //set the value + set(arr, current) +} + +export const remove = <T>(arr: MaybeRef<T[]>, item: T) => { + //get the reactuve value first + const current = get(arr) + //Get all items that are not the item + const wo = filter(current, (i) => !isEqual(i, item)) + //set the value + set(arr, wo) +} + +export const useQuery = (query: string) => { + + const get = () => { + const args = new URLSearchParams(window.location.search) + return args.get(query) + } + + const set = (value: string) => { + const args = new URLSearchParams(window.location.search); + args.set(query, value); + (window as any).customHistory.replaceState({}, '', `${window.location.pathname}?${args.toString()}`) + } + + const mutable = shallowRef<string | null>(get()) + + //Setup custom historu + if (!('customHistory' in window)) { + (window as any).customHistory = { + replaceState: (...args: any[]) => { + window.history.replaceState(...args) + window.dispatchEvent(new Event('replaceState')) + } + } + } + + //Listen for custom history events and update the mutable state + window.addEventListener('replaceState', () => mutable.value = get()) + + return{ + get, + set, + asRef: mutable + } }
\ No newline at end of file |