aboutsummaryrefslogtreecommitdiff
path: root/extension/src/features
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-28 19:56:02 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-28 19:56:02 -0500
commit87645bfad3943e1110e4cb2e038124083e8ae793 (patch)
treec327a4437c98d973f45c313cf8259ad75515c4fe /extension/src/features
parentc438ee90e3be4e5e01ae3d045d6b841a03bd46eb (diff)
progress update
Diffstat (limited to 'extension/src/features')
-rw-r--r--extension/src/features/auth-api.ts5
-rw-r--r--extension/src/features/framework/index.ts26
-rw-r--r--extension/src/features/history.ts64
-rw-r--r--extension/src/features/identity-api.ts8
-rw-r--r--extension/src/features/index.ts6
-rw-r--r--extension/src/features/nip07allow-api.ts7
-rw-r--r--extension/src/features/nostr-api.ts28
-rw-r--r--extension/src/features/permissions.ts158
-rw-r--r--extension/src/features/server-api/index.ts25
-rw-r--r--extension/src/features/settings.ts13
-rw-r--r--extension/src/features/tagfilter-api.ts138
-rw-r--r--extension/src/features/types.ts25
-rw-r--r--extension/src/features/util.ts85
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