flotilla/src/app/state.ts

353 lines
9.8 KiB
TypeScript
Raw Normal View History

2024-08-13 19:41:25 +00:00
import type {Readable} from "svelte/store"
import {writable, readable, derived} from "svelte/store"
import {uniq, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} from "@welshman/lib"
import {getIdentifier, normalizeRelayUrl, getPubkeyTagValues, GROUP_META, PROFILE, FOLLOWS, MUTES, GROUPS, getGroupTagValues} from "@welshman/util"
import type {Filter, SignedEvent, CustomEvent} from '@welshman/util'
import {subscribe} from '@welshman/net'
import {readProfile, readList, asDecryptedEvent} from '@welshman/domain'
import type {PublishedProfile, PublishedList} from '@welshman/domain'
import {decrypt} from '@welshman/signer'
import {deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
2024-08-09 23:22:09 +00:00
import {synced, parseJson} from '@lib/util'
2024-08-13 19:41:25 +00:00
import type {Session, Handle, Relay} from '@app/types'
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSessions, makeSigner} from "@app/base"
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
// Utils
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const createCollection = <T>({
store,
getKey,
isStale,
loadItem,
}: {
store: Readable<T[]>,
getKey: (item: T) => string,
isStale: (item: T) => boolean,
loadItem: (key: string, ...args: any) => Promise<any>
}) => {
const indexStore = derived(store, $items => indexBy(getKey, $items))
const getIndex = getter(indexStore)
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
const getItem = async (key: string, ...args: any[]) => {
const item = getIndex().get(key)
if (item && isStale(item)) {
return item
}
await loadItem(key, ...args)
return getIndex().get(key)
}
const deriveItem = (key: string | undefined, ...args: any[]) => {
if (!key) {
return readable(undefined)
}
// If we don't yet have the item, or it's stale, trigger a request for it. The derived
// store will update when it arrives
loadItem(key, ...args)
return derived(indexStore, $index => $index.get(key))
}
return {indexStore, getIndex, deriveItem, loadItem, getItem}
}
export const load = ({relays, filters}: {relays: string[], filters: Filter[]}) =>
new Promise<CustomEvent | undefined>(resolve => {
const sub = subscribe({relays, filters, closeOnEose: true})
sub.emitter.on('event', (url: string, event: SignedEvent) => {
const e: CustomEvent = {...event, fetched_at: now()}
repository.publish(e)
resolve(e)
})
sub.emitter.on('close', () => resolve(undefined))
})
// Plaintext
export const plaintext = withGetter(writable<Record<string, string>>({}))
export const getPlaintext = (e: CustomEvent) => plaintext.get()[e.id]
export const setPlaintext = (e: CustomEvent, content: string) =>
plaintext.update(assoc(e.id, content))
export const ensurePlaintext = async (e: CustomEvent) => {
if (!getPlaintext(e)) {
const sessions = getSessions()
const session = sessions[e.pubkey]
const signer = makeSigner(session)
if (signer) {
setPlaintext(e, await decrypt(signer, e.pubkey, e.content))
}
}
return getPlaintext(e)
}
// Relay info
export const relays = writable<Relay[]>([])
export const {
indexStore: relaysByUrl,
getIndex: getRelaysByUrl,
deriveItem: deriveRelay,
loadItem: loadRelay,
getItem: getRelay,
} = createCollection({
store: relays,
getKey: (relay: Relay) => relay.url,
isStale: (relay: Relay) => relay.fetched_at < now() - 3600,
loadItem: batcher(800, async (urls: string[]) => {
const urlSet = new Set(urls)
const res = await postJson(`${DUFFLEPUD_URL}/relay/info`, {urls: Array.from(urlSet)})
const items: Relay[] = (res?.data || []).map(({url, info}: any) => ({...info, url, fetched_at: now()}))
relays.update($relays => [
...$relays.filter($relay => !urlSet.has($relay.url)),
...items,
])
return items
}),
2024-08-09 23:22:09 +00:00
})
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
// Handles
export const handles = writable<Handle[]>([])
export const {
indexStore: handlesByPubkey,
getIndex: getHandlesByPubkey,
deriveItem: deriveHandle,
loadItem: loadHandle,
getItem: getHandle,
} = createCollection({
store: handles,
getKey: (handle: Handle) => handle.pubkey,
isStale: (handle: Handle) => handle.fetched_at < now() - 3600,
loadItem: batcher(800, async (nip05s: string[]) => {
const nip05Set = new Set(nip05s)
const res = await postJson(`${DUFFLEPUD_URL}/handle/info`, {handles: Array.from(nip05Set)})
const items: Handle[] = (res?.data || []).map(({handle: nip05, info}: any) => ({...info, nip05, fetched_at: now()}))
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
handles.update($handles => [
...$handles.filter($handle => !nip05Set.has($handle.nip05)),
...items,
])
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
return items
}),
})
// Profiles
export const profiles = deriveEventsMapped<PublishedProfile>({
repository,
filters: [{kinds: [PROFILE]}],
eventToItem: readProfile,
itemToEvent: item => item.event,
})
export const {
indexStore: profilesByPubkey,
getIndex: getProfilesByPubkey,
deriveItem: deriveProfile,
loadItem: loadProfile,
getItem: getProfile,
} = createCollection({
store: profiles,
getKey: profile => profile.event.pubkey,
isStale: (profile: PublishedProfile) => profile.event.fetched_at < now() - 3600,
loadItem: (pubkey: string, relays = []) =>
load({
relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [PROFILE], authors: [pubkey]}],
}),
})
// Follows
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
export const follows = deriveEventsMapped<PublishedList>({
repository,
2024-08-09 23:22:09 +00:00
filters: [{kinds: [FOLLOWS]}],
2024-08-13 19:41:25 +00:00
itemToEvent: item => item.event,
eventToItem: async (event: CustomEvent) =>
readList(
asDecryptedEvent(event, {
content: await ensurePlaintext(event),
}),
),
2024-08-09 23:22:09 +00:00
})
2024-08-13 19:41:25 +00:00
export const {
indexStore: followsByPubkey,
getIndex: getFollowsByPubkey,
deriveItem: deriveFollows,
loadItem: loadFollows,
getItem: getFollows,
} = createCollection({
store: follows,
getKey: follows => follows.event.pubkey,
isStale: (follows: PublishedList) => follows.event.fetched_at < now() - 3600,
loadItem: (pubkey: string, relays = []) =>
load({
relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [FOLLOWS], authors: [pubkey]}],
})
})
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
// Mutes
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
export const mutes = deriveEventsMapped<PublishedList>({
repository,
2024-08-09 23:22:09 +00:00
filters: [{kinds: [MUTES]}],
2024-08-13 19:41:25 +00:00
itemToEvent: item => item.event,
eventToItem: async (event: CustomEvent) =>
readList(
asDecryptedEvent(event, {
content: await ensurePlaintext(event),
}),
),
2024-08-09 23:22:09 +00:00
})
2024-08-13 19:41:25 +00:00
export const {
indexStore: mutesByPubkey,
getIndex: getMutesByPubkey,
deriveItem: deriveMutes,
loadItem: loadMutes,
getItem: getMutes,
} = createCollection({
store: mutes,
getKey: mute => mute.event.pubkey,
isStale: (mutes: PublishedList) => mutes.event.fetched_at < now() - 3600,
loadItem: (pubkey: string, relays = []) =>
load({
relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [MUTES], authors: [pubkey]}],
})
})
// Groups
export const GROUP_DELIMITER = `'`
export const makeGroupId = (url: string, nom: string) =>
[stripProtocol(url), nom].join(GROUP_DELIMITER)
2024-08-09 23:22:09 +00:00
2024-08-13 19:41:25 +00:00
export const splitGroupId = (groupId: string) => {
const [url, nom] = groupId.split(GROUP_DELIMITER)
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
return [normalizeRelayUrl(url), nom]
}
export const getGroupUrl = (groupId: string) => splitGroupId(groupId)[0]
export const getGroupNom = (groupId: string) => splitGroupId(groupId)[1]
export const getGroupName = (e?: CustomEvent) => e?.tags.find(nthEq(0, "name"))?.[1]
export const getGroupPicture = (e?: CustomEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
export type Group = {
id: string,
nom: string,
url: string,
name?: string,
picture?: string,
event?: CustomEvent
}
export type PublishedGroup = Omit<Group, "event"> & {
event: CustomEvent
}
export const readGroup = (event: CustomEvent) => {
const id = getIdentifier(event)!
const [url, nom] = id.split(GROUP_DELIMITER)
const name = event?.tags.find(nthEq(0, "name"))?.[1]
const picture = event?.tags.find(nthEq(0, "picture"))?.[1]
return {id, nom, url, name, picture, event}
}
export const groups = deriveEventsMapped<PublishedGroup>({
repository,
2024-08-06 22:46:37 +00:00
filters: [{kinds: [GROUP_META]}],
2024-08-13 19:41:25 +00:00
eventToItem: readGroup,
itemToEvent: item => item.event,
2024-08-06 22:46:37 +00:00
})
2024-08-13 19:41:25 +00:00
export const validGroups = derived([relaysByUrl, groups], ([$relaysByUrl, $groups]) =>
$groups.filter(group => $relaysByUrl.get(group.url)?.pubkey === group.event.pubkey)
2024-08-06 22:46:37 +00:00
)
2024-08-13 19:41:25 +00:00
export const {
indexStore: groupsById,
getIndex: getGroupsById,
deriveItem: deriveGroup,
loadItem: loadGroup,
getItem: getGroup,
} = createCollection({
store: validGroups,
getKey: (group: PublishedGroup) => group.id,
isStale: (group: PublishedGroup) => group.event.fetched_at < now() - 3600,
loadItem: (id: string) => {
const url = getGroupUrl(id)
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
return Promise.all([
loadRelay(url),
load({
relays: [url],
filters: [{kinds: [GROUP_META], '#d': [id]}],
}),
])
}
2024-08-06 22:46:37 +00:00
})
2024-08-13 19:41:25 +00:00
// Group membership
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export type GroupMembership = {
ids: Set<string>
event?: CustomEvent
}
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export type PublishedGroupMembership = Omit<GroupMembership, "event"> & {
event: CustomEvent
}
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const readGroupMembership = (event: CustomEvent) =>
({event, ids: new Set(getGroupTagValues(event.tags))})
export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>({
repository,
filters: [{kinds: [GROUPS]}],
eventToItem: readGroupMembership,
itemToEvent: item => item.event,
})
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const {
indexStore: groupMembershipsByPubkey,
getIndex: getGroupMembersipsByPubkey,
deriveItem: deriveGroupMembership,
loadItem: loadGroupMembership,
getItem: getGroupMembership,
} = createCollection({
store: groupMemberships,
getKey: groupMembership => groupMembership.event.pubkey,
isStale: (groupMembership: PublishedGroupMembership) => groupMembership.event.fetched_at < now() - 3600,
loadItem: (pubkey: string, relays = []) =>
load({
relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [GROUPS], authors: [pubkey]}],
})
2024-08-06 22:46:37 +00:00
})