Load user data before updating, prefer newer events in repository over old ones with the same created_at, add list update utils
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
|||||||
sendManagementRequest,
|
sendManagementRequest,
|
||||||
ManagementRequest,
|
ManagementRequest,
|
||||||
addToListPublicly,
|
addToListPublicly,
|
||||||
|
addToListPrivately,
|
||||||
|
updateList,
|
||||||
EventTemplate,
|
EventTemplate,
|
||||||
removeFromList,
|
removeFromList,
|
||||||
makeHttpAuth,
|
makeHttpAuth,
|
||||||
@@ -30,10 +32,15 @@ import {Nip59, stamp} from "@welshman/signer"
|
|||||||
import {Router, addMaximalFallbacks} from "@welshman/router"
|
import {Router, addMaximalFallbacks} from "@welshman/router"
|
||||||
import {
|
import {
|
||||||
userRelaySelections,
|
userRelaySelections,
|
||||||
|
loadUserRelaySelections,
|
||||||
userInboxRelaySelections,
|
userInboxRelaySelections,
|
||||||
|
loadUserInboxRelaySelections,
|
||||||
userFollows,
|
userFollows,
|
||||||
|
loadUserFollows,
|
||||||
userMutes,
|
userMutes,
|
||||||
|
loadUserMutes,
|
||||||
userPins,
|
userPins,
|
||||||
|
loadUserPins,
|
||||||
} from "./user.js"
|
} from "./user.js"
|
||||||
import {nip44EncryptToSelf, signer} from "./session.js"
|
import {nip44EncryptToSelf, signer} from "./session.js"
|
||||||
import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
|
import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
|
||||||
@@ -41,6 +48,8 @@ import {ThunkOptions, MergedThunk, publishThunk} from "./thunk.js"
|
|||||||
// NIP 65
|
// NIP 65
|
||||||
|
|
||||||
export const removeRelay = async (url: string, mode: RelayMode) => {
|
export const removeRelay = async (url: string, mode: RelayMode) => {
|
||||||
|
await loadUserRelaySelections([], true)
|
||||||
|
|
||||||
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
||||||
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
|
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
|
||||||
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
|
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
|
||||||
@@ -61,6 +70,8 @@ export const removeRelay = async (url: string, mode: RelayMode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const addRelay = async (url: string, mode: RelayMode) => {
|
export const addRelay = async (url: string, mode: RelayMode) => {
|
||||||
|
await loadUserRelaySelections([], true)
|
||||||
|
|
||||||
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
const list = get(userRelaySelections) || makeList({kind: RELAYS})
|
||||||
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
|
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
|
||||||
const tag = removeNil(["r", url, dup && dup[2] !== mode ? undefined : mode])
|
const tag = removeNil(["r", url, dup && dup[2] !== mode ? undefined : mode])
|
||||||
@@ -74,6 +85,8 @@ export const addRelay = async (url: string, mode: RelayMode) => {
|
|||||||
// NIP 17
|
// NIP 17
|
||||||
|
|
||||||
export const removeInboxRelay = async (url: string) => {
|
export const removeInboxRelay = async (url: string) => {
|
||||||
|
await loadUserInboxRelaySelections([], true)
|
||||||
|
|
||||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
||||||
const event = await removeFromList(list, url).reconcile(nip44EncryptToSelf)
|
const event = await removeFromList(list, url).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -82,6 +95,8 @@ export const removeInboxRelay = async (url: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const addInboxRelay = async (url: string) => {
|
export const addInboxRelay = async (url: string) => {
|
||||||
|
await loadUserInboxRelaySelections([], true)
|
||||||
|
|
||||||
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
|
||||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, ["relay", url]).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -102,6 +117,8 @@ export const setProfile = (profile: Profile) => {
|
|||||||
// NIP 02
|
// NIP 02
|
||||||
|
|
||||||
export const unfollow = async (value: string) => {
|
export const unfollow = async (value: string) => {
|
||||||
|
await loadUserFollows([], true)
|
||||||
|
|
||||||
const list = get(userFollows) || makeList({kind: FOLLOWS})
|
const list = get(userFollows) || makeList({kind: FOLLOWS})
|
||||||
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -110,6 +127,8 @@ export const unfollow = async (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const follow = async (tag: string[]) => {
|
export const follow = async (tag: string[]) => {
|
||||||
|
await loadUserFollows([], true)
|
||||||
|
|
||||||
const list = get(userFollows) || makeList({kind: FOLLOWS})
|
const list = get(userFollows) || makeList({kind: FOLLOWS})
|
||||||
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -118,6 +137,8 @@ export const follow = async (tag: string[]) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const unmute = async (value: string) => {
|
export const unmute = async (value: string) => {
|
||||||
|
await loadUserMutes([], true)
|
||||||
|
|
||||||
const list = get(userMutes) || makeList({kind: MUTES})
|
const list = get(userMutes) || makeList({kind: MUTES})
|
||||||
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -125,7 +146,9 @@ export const unmute = async (value: string) => {
|
|||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mute = async (tag: string[]) => {
|
export const mutePublicly = async (tag: string[]) => {
|
||||||
|
await loadUserMutes([], true)
|
||||||
|
|
||||||
const list = get(userMutes) || makeList({kind: MUTES})
|
const list = get(userMutes) || makeList({kind: MUTES})
|
||||||
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -133,7 +156,35 @@ export const mute = async (tag: string[]) => {
|
|||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mutePrivately = async (tag: string[]) => {
|
||||||
|
await loadUserMutes([], true)
|
||||||
|
|
||||||
|
const list = get(userMutes) || makeList({kind: MUTES})
|
||||||
|
const event = await addToListPrivately(list, tag).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
|
|
||||||
|
return publishThunk({event, relays})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setMutes = async ({
|
||||||
|
publicTags,
|
||||||
|
privateTags,
|
||||||
|
}: {
|
||||||
|
publicTags?: string[][]
|
||||||
|
privateTags?: string[][]
|
||||||
|
}) => {
|
||||||
|
await loadUserMutes([], true)
|
||||||
|
|
||||||
|
const list = get(userMutes) || makeList({kind: MUTES})
|
||||||
|
const event = await updateList(list, {publicTags, privateTags}).reconcile(nip44EncryptToSelf)
|
||||||
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
|
|
||||||
|
return publishThunk({event, relays})
|
||||||
|
}
|
||||||
|
|
||||||
export const unpin = async (value: string) => {
|
export const unpin = async (value: string) => {
|
||||||
|
await loadUserPins([], true)
|
||||||
|
|
||||||
const list = get(userPins) || makeList({kind: PINS})
|
const list = get(userPins) || makeList({kind: PINS})
|
||||||
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
const event = await removeFromList(list, value).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
@@ -142,6 +193,8 @@ export const unpin = async (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const pin = async (tag: string[]) => {
|
export const pin = async (tag: string[]) => {
|
||||||
|
await loadUserPins([], true)
|
||||||
|
|
||||||
const list = get(userPins) || makeList({kind: PINS})
|
const list = get(userPins) || makeList({kind: PINS})
|
||||||
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
const event = await addToListPublicly(list, tag).reconcile(nip44EncryptToSelf)
|
||||||
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
const relays = Router.get().FromUser().policy(addMaximalFallbacks).getUrls()
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
} from "./relaySelections.js"
|
} from "./relaySelections.js"
|
||||||
import {wotGraph} from "./wot.js"
|
import {wotGraph} from "./wot.js"
|
||||||
|
|
||||||
|
export type UserDataLoader = (pubkey: string, relays?: string[], force?: boolean) => unknown
|
||||||
|
|
||||||
export type MakeUserDataOptions<T> = {
|
export type MakeUserDataOptions<T> = {
|
||||||
mapStore: Readable<Map<string, T>>
|
mapStore: Readable<Map<string, T>>
|
||||||
loadItem: (pubkey: string) => unknown
|
loadItem: UserDataLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) =>
|
const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) =>
|
||||||
@@ -30,41 +32,65 @@ const makeUserData = <T>({mapStore, loadItem}: MakeUserDataOptions<T>) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const makeUserLoader =
|
||||||
|
(loadItem: UserDataLoader) =>
|
||||||
|
async (relays: string[] = [], force = false) => {
|
||||||
|
const $pubkey = pubkey.get()
|
||||||
|
|
||||||
|
if ($pubkey) {
|
||||||
|
await loadItem($pubkey, relays, force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const userProfile = makeUserData({
|
export const userProfile = makeUserData({
|
||||||
mapStore: profilesByPubkey,
|
mapStore: profilesByPubkey,
|
||||||
loadItem: loadProfile,
|
loadItem: loadProfile,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserProfile = makeUserLoader(loadProfile)
|
||||||
|
|
||||||
export const userFollows = makeUserData({
|
export const userFollows = makeUserData({
|
||||||
mapStore: followsByPubkey,
|
mapStore: followsByPubkey,
|
||||||
loadItem: loadFollows,
|
loadItem: loadFollows,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserFollows = makeUserLoader(loadFollows)
|
||||||
|
|
||||||
export const userMutes = makeUserData({
|
export const userMutes = makeUserData({
|
||||||
mapStore: mutesByPubkey,
|
mapStore: mutesByPubkey,
|
||||||
loadItem: loadMutes,
|
loadItem: loadMutes,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserMutes = makeUserLoader(loadMutes)
|
||||||
|
|
||||||
export const userPins = makeUserData({
|
export const userPins = makeUserData({
|
||||||
mapStore: pinsByPubkey,
|
mapStore: pinsByPubkey,
|
||||||
loadItem: loadPins,
|
loadItem: loadPins,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserPins = makeUserLoader(loadPins)
|
||||||
|
|
||||||
export const userRelaySelections = makeUserData({
|
export const userRelaySelections = makeUserData({
|
||||||
mapStore: relaySelectionsByPubkey,
|
mapStore: relaySelectionsByPubkey,
|
||||||
loadItem: loadRelaySelections,
|
loadItem: loadRelaySelections,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserRelaySelections = makeUserLoader(loadRelaySelections)
|
||||||
|
|
||||||
export const userInboxRelaySelections = makeUserData({
|
export const userInboxRelaySelections = makeUserData({
|
||||||
mapStore: inboxRelaySelectionsByPubkey,
|
mapStore: inboxRelaySelectionsByPubkey,
|
||||||
loadItem: loadInboxRelaySelections,
|
loadItem: loadInboxRelaySelections,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserInboxRelaySelections = makeUserLoader(loadInboxRelaySelections)
|
||||||
|
|
||||||
export const userBlossomServers = makeUserData({
|
export const userBlossomServers = makeUserData({
|
||||||
mapStore: blossomServersByPubkey,
|
mapStore: blossomServersByPubkey,
|
||||||
loadItem: loadBlossomServers,
|
loadItem: loadBlossomServers,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const loadUserBlossomServers = makeUserLoader(loadBlossomServers)
|
||||||
|
|
||||||
export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0
|
export const getUserWotScore = (tpk: string) => wotGraph.get().get(tpk) || 0
|
||||||
|
|
||||||
export const deriveUserWotScore = (tpk: string) => derived(wotGraph, $g => $g.get(tpk) || 0)
|
export const deriveUserWotScore = (tpk: string) => derived(wotGraph, $g => $g.get(tpk) || 0)
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
} from "./message.js"
|
} from "./message.js"
|
||||||
import {Socket, SocketStatus, SocketEvent} from "./socket.js"
|
import {Socket, SocketStatus, SocketEvent} from "./socket.js"
|
||||||
import {AuthStatus, AuthStateEvent} from "./auth.js"
|
import {AuthStatus, AuthStateEvent} from "./auth.js"
|
||||||
|
import {Unsubscriber} from "./util.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The contract for socket policies
|
||||||
|
* @param socket - a Socket object
|
||||||
|
* @return a cleanup function
|
||||||
|
*/
|
||||||
|
export type SocketPolicy = (socket: Socket) => Unsubscriber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles auth-related message management:
|
* Handles auth-related message management:
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export class Repository<E extends HashedEvent = TrustedEvent> extends Emitter {
|
|||||||
|
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
// If our event is younger than the duplicate, we're done
|
// If our event is younger than the duplicate, we're done
|
||||||
if (event.created_at <= duplicate.created_at) {
|
if (event.created_at < duplicate.created_at) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ export type SignerMethodWrapper = <T>(method: string, thunk: () => Promise<T>) =
|
|||||||
|
|
||||||
export class WrappedSigner extends Emitter implements ISigner {
|
export class WrappedSigner extends Emitter implements ISigner {
|
||||||
constructor(
|
constructor(
|
||||||
private signer: ISigner,
|
readonly signer: ISigner,
|
||||||
private wrapMethod: SignerMethodWrapper,
|
readonly wrapMethod: SignerMethodWrapper,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const makeCachedLoader = <T>({
|
|||||||
const pending = new Map<string, Promise<T | void>>()
|
const pending = new Map<string, Promise<T | void>>()
|
||||||
const loadAttempts = new Map<string, number>()
|
const loadAttempts = new Map<string, number>()
|
||||||
|
|
||||||
return async (key: string, relays: string[] = []) => {
|
return async (key: string, relays: string[] = [], force = false) => {
|
||||||
const stale = indexStore.get().get(key)
|
const stale = indexStore.get().get(key)
|
||||||
|
|
||||||
// If we have no loader function, nothing we can do
|
// If we have no loader function, nothing we can do
|
||||||
@@ -57,7 +57,7 @@ export const makeCachedLoader = <T>({
|
|||||||
const freshness = getFreshness(name, key)
|
const freshness = getFreshness(name, key)
|
||||||
|
|
||||||
// If we have an item, reload if it's stale
|
// If we have an item, reload if it's stale
|
||||||
if (stale && freshness > now() - 3600) {
|
if (stale && freshness > now() - 3600 && !force) {
|
||||||
return stale
|
return stale
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export const makeCachedLoader = <T>({
|
|||||||
const attempt = loadAttempts.get(key) || 0
|
const attempt = loadAttempts.get(key) || 0
|
||||||
|
|
||||||
// Use exponential backoff to throttle attempts
|
// Use exponential backoff to throttle attempts
|
||||||
if (freshness > now() - Math.pow(2, attempt)) {
|
if (freshness > now() - Math.pow(2, attempt) && !force) {
|
||||||
return stale
|
return stale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ export class Encryptable<T extends EventTemplate> {
|
|||||||
*/
|
*/
|
||||||
async reconcile(encrypt: Encrypt) {
|
async reconcile(encrypt: Encrypt) {
|
||||||
const encryptContent = () => {
|
const encryptContent = () => {
|
||||||
if (!this.updates.content) return null
|
if (!this.updates.content) return undefined
|
||||||
|
|
||||||
return encrypt(this.updates.content)
|
return encrypt(this.updates.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptTags = () => {
|
const encryptTags = () => {
|
||||||
if (!this.updates.tags) return null
|
if (!this.updates.tags) return undefined
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
this.updates.tags.map(async tag => {
|
this.updates.tags.map(async tag => {
|
||||||
|
|||||||
@@ -89,6 +89,25 @@ export const addToListPrivately = (list: List, ...tags: string[][]) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateList = (
|
||||||
|
list: List,
|
||||||
|
{publicTags, privateTags}: {publicTags?: string[][]; privateTags?: string[][]},
|
||||||
|
) => {
|
||||||
|
const template = {
|
||||||
|
kind: list.kind,
|
||||||
|
content: list.event?.content || "",
|
||||||
|
tags: publicTags || list.publicTags,
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: EncryptableUpdates = {}
|
||||||
|
|
||||||
|
if (privateTags) {
|
||||||
|
updates.content = JSON.stringify(privateTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Encryptable(template, updates)
|
||||||
|
}
|
||||||
|
|
||||||
export const getRelaysFromList = (list?: List, mode?: RelayMode): string[] => {
|
export const getRelaysFromList = (list?: List, mode?: RelayMode): string[] => {
|
||||||
let tags = getRelayTags(getListTags(list))
|
let tags = getRelayTags(getListTags(list))
|
||||||
|
|
||||||
|
|||||||
@@ -119,4 +119,6 @@ export const uniqTags = (tags: string[][]) => uniqBy(t => t.slice(0, 2).join(":"
|
|||||||
|
|
||||||
export const tagsFromIMeta = (imeta: string[]) => imeta.map((m: string) => m.split(" "))
|
export const tagsFromIMeta = (imeta: string[]) => imeta.map((m: string) => m.split(" "))
|
||||||
|
|
||||||
export const tagger = (name: string) => (value: string) => [name, value]
|
export const tagger =
|
||||||
|
(name: string) =>
|
||||||
|
(value: string, ...args: unknown[]) => [name, value]
|
||||||
|
|||||||
Reference in New Issue
Block a user