Use welshman relays

This commit is contained in:
Jon Staab 2024-08-30 16:10:15 -07:00
parent cf06ab1788
commit e554acc79d
15 changed files with 68 additions and 176 deletions

View File

@ -8,11 +8,11 @@
import * as lib from "@welshman/lib"
import * as util from "@welshman/util"
import * as network from "@welshman/net"
import {session, pubkey} from "@welshman/app"
import {session, pubkey, relays} from "@welshman/app"
import logger from "src/util/logger"
import * as misc from "src/util/misc"
import * as nostr from "src/util/nostr"
import {storage, relays, getSetting} from "src/engine"
import {storage, getSetting} from "src/engine"
import * as engine from "src/engine"
import * as domain from "src/domain"
import {loadAppData, slowConnections, loadUserData} from "src/app/state"
@ -459,39 +459,14 @@
const {lastOpen, lastPublish, lastRequest, lastFault} = connection.meta
const lastActivity = lib.max([lastOpen, lastPublish, lastRequest, lastFault])
if (lastFault) {
relays.key(url).update($r => ({
...$r,
faults: lib.uniq(($r.faults || []).concat(lastFault)).slice(-10),
}))
}
if (lastActivity < Date.now() - 60_000) {
connection.disconnect()
}
}
}, 5_000)
const interval2 = setInterval(async () => {
if (!getSetting("dufflepud_url")) {
return
}
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not asking for too much data at once
const staleRelays = relays
.get()
.filter(r => (r.last_checked || 0) < lib.now() - seconds(7, "day"))
.slice(0, 20)
for (const relay of staleRelays) {
engine.loadRelay(relay.url)
}
}, 30_000)
return () => {
clearInterval(interval1)
clearInterval(interval2)
}
})
</script>

View File

@ -1,7 +1,8 @@
<script lang="ts">
import {FeedType} from "@welshman/feeds"
import {relaySearch} from "@welshman/app"
import SearchSelect from "src/partials/SearchSelect.svelte"
import {relaySearch} from "src/engine"
import {displayRelayUrl} from 'src/domain'
export let feed
export let onChange
@ -13,5 +14,5 @@
value={feed.slice(1)}
search={$relaySearch.searchValues}
onChange={urls => onChange([FeedType.Relay, ...urls])}>
<span slot="item" let:item>{$relaySearch.displayValue(item)}</span>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>

View File

@ -2,6 +2,7 @@
import {join, uniqBy} from "ramda"
import {ucFirst} from "hurdak"
import {Address, GROUP, COMMUNITY} from "@welshman/util"
import {relaySearch} from "@welshman/app"
import {toSpliced} from "src/util/misc"
import {fly} from "src/util/transition"
import {formCtrl} from "src/partials/utils"
@ -20,7 +21,7 @@
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import type {GroupMeta} from "src/domain"
import {normalizeRelayUrl, displayRelayUrl} from "src/domain"
import {env, hints, relaySearch, feedSearch} from "src/engine"
import {env, hints, feedSearch} from "src/engine"
export let onSubmit
export let values: GroupMeta & {members: string[]}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import {identity} from "@welshman/lib"
import {Tags, NAMED_PEOPLE, NAMED_RELAYS, NAMED_TOPICS} from "@welshman/util"
import {topicSearch} from "@welshman/app"
import {topicSearch, relaySearch} from "@welshman/app"
import {showInfo} from "src/partials/Toast.svelte"
import Field from "src/partials/Field.svelte"
import Modal from "src/partials/Modal.svelte"
@ -11,8 +11,15 @@
import Input from "src/partials/Input.svelte"
import SearchSelect from "src/partials/SearchSelect.svelte"
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import {hints, mention, relaySearch, createAndPublish, deleteEvent} from "src/engine"
import {KindSearch, normalizeRelayUrl, createList, displayList, editList} from "src/domain"
import {hints, mention, createAndPublish, deleteEvent} from "src/engine"
import {
KindSearch,
normalizeRelayUrl,
createList,
displayList,
editList,
displayRelayUrl,
} from "src/domain"
export let list
export let exit
@ -98,7 +105,7 @@
search={$relaySearch.searchValues}
termToItem={normalizeRelayUrl}
onChange={onRelaysChange}>
<span slot="item" let:item>{$relaySearch.displayValue(item)}</span>
<span slot="item" let:item>{displayRelayUrl(item)}</span>
</SearchSelect>
{:else if list.kind === NAMED_TOPICS}
<SearchSelect

View File

@ -1,14 +1,14 @@
<script lang="ts">
import {last} from "ramda"
import {derived} from "svelte/store"
import {signer} from "@welshman/app"
import {signer, deriveRelay} from "@welshman/app"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {relays, userRelayPolicies, joinRelay, leaveRelay} from "src/engine"
import {userRelayPolicies, joinRelay, leaveRelay} from "src/engine"
import {router} from "src/app/util/router"
export let url
const relay = relays.key(url)
const relay = deriveRelay(url)
const joined = derived(userRelayPolicies, $policies =>
Boolean($policies.find(p => p.url === url)),
)
@ -46,9 +46,9 @@
})
}
if ($relay?.contact) {
if ($relay?.profile?.contact) {
actions.push({
onClick: () => window.open("mailto:" + last($relay.contact.split(":"))),
onClick: () => window.open("mailto:" + last($relay.profile?.contact.split(":"))),
label: "Contact",
icon: "envelope",
})

View File

@ -1,7 +1,7 @@
<script lang="ts">
import cx from "classnames"
import {isNil} from "@welshman/lib"
import {signer} from "@welshman/app"
import {signer, deriveRelay} from "@welshman/app"
import {onMount} from "svelte"
import {quantify} from "hurdak"
import {stringToHue, displayUrl, hsl} from "src/util/misc"
@ -14,14 +14,7 @@
import RelayCardActions from "src/app/shared/RelayCardActions.svelte"
import {router} from "src/app/util/router"
import {displayRelayUrl, RelayMode} from "src/domain"
import {
deriveRelay,
getSetting,
setInboxPolicy,
setOutboxPolicy,
deriveUserRelayPolicy,
loadRelay,
} from "src/engine"
import {getSetting, setInboxPolicy, setOutboxPolicy, deriveUserRelayPolicy} from "src/engine"
export let url
export let claim = null
@ -45,10 +38,6 @@
setOutboxPolicy(newPolicy)
}
}
onMount(() => {
loadRelay(url)
})
</script>
<div
@ -88,28 +77,29 @@
</div>
{#if !hideDescription}
<slot name="description">
{#if $relay.description}
<p>{$relay.description}</p>
{#if $relay.profile?.description}
<p>{$relay.profile.description}</p>
{/if}
</slot>
{#if !isNil($relay.count)}
{#if !$relay.stats}
<span class="flex items-center gap-1 text-sm text-neutral-400">
{#if $relay.contact}
<Anchor external underline href={$relay.contact}>{displayUrl($relay.contact)}</Anchor>
{#if $relay.profile?.contact}
<Anchor external underline href={$relay.profile.contact}
>{displayUrl($relay.profile.contact)}</Anchor>
&bull;
{/if}
{#if $relay.supported_nips}
{#if $relay.profile?.supported_nips}
<Popover>
<span slot="trigger" class="cursor-pointer underline">
{$relay.supported_nips.length} NIPs
{$relay.profile.supported_nips.length} NIPs
</span>
<span slot="tooltip">
NIPs supported: {$relay.supported_nips.join(", ")}
NIPs supported: {$relay.profile.supported_nips.join(", ")}
</span>
</Popover>
&bull;
{/if}
Seen {quantify($relay.count || 0, "time")}
Connected {quantify($relay.stats.connect_count, "time")}
</span>
{/if}
{/if}

View File

@ -1,6 +1,7 @@
<script lang="ts">
import {without, identity} from "@welshman/lib"
import {getAddress} from "@welshman/util"
import {relaySearch} from "@welshman/app"
import {onMount} from "svelte"
import {pickVals, toSpliced} from "src/util/misc"
import Card from "src/partials/Card.svelte"
@ -16,7 +17,7 @@
import PersonSelect from "src/app/shared/PersonSelect.svelte"
import {router} from "src/app/util/router"
import {displayRelayUrl} from "src/domain"
import {hints, relaySearch, groupMetaSearch, displayGroupByAddress} from "src/engine"
import {hints, groupMetaSearch, displayGroupByAddress} from "src/engine"
export let initialPubkey = null
export let initialGroupAddress = null

View File

@ -3,6 +3,7 @@
import {quantify} from "hurdak"
import {fromPairs, uniq, without, remove, append, nth, nthEq} from "@welshman/lib"
import {getPubkeyTagValues, getAddress} from "@welshman/util"
import {relaySearch} from "@welshman/app"
import Card from "src/partials/Card.svelte"
import Input from "src/partials/Input.svelte"
import Modal from "src/partials/Modal.svelte"
@ -11,7 +12,7 @@
import Subheading from "src/partials/Subheading.svelte"
import PersonSummary from "src/app/shared/PersonSummary.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {createPeopleLoader, profileSearch, relaySearch} from "src/engine"
import {createPeopleLoader, profileSearch} from "src/engine"
export let relays
export let follows

View File

@ -1,13 +1,13 @@
<script lang="ts">
import {batch} from "hurdak"
import {makeRelayFeed, feedFromFilter} from "@welshman/feeds"
import {deriveRelay} from "@welshman/app"
import {getAvgRating} from "src/util/nostr"
import Feed from "src/app/shared/Feed.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Rating from "src/partials/Rating.svelte"
import RelayTitle from "src/app/shared/RelayTitle.svelte"
import RelayActions from "src/app/shared/RelayActions.svelte"
import {deriveRelay} from "src/engine"
import {makeFeed, normalizeRelayUrl, displayRelayUrl} from "src/domain"
export let url
@ -54,8 +54,8 @@
<Rating inert value={rating} />
</div>
{/if}
{#if $relay.description}
<p>{$relay.description}</p>
{#if $relay.profile?.description}
<p>{$relay.profile.description}</p>
{/if}
<Tabs {tabs} {activeTab} {setActiveTab} />
{#if activeTab === "reviews"}

View File

@ -4,7 +4,7 @@
import {groupBy, sortBy, uniqBy, prop} from "ramda"
import {displayList} from "hurdak"
import {pushToMapKey} from "@welshman/lib"
import {pubkey} from "@welshman/app"
import {pubkey, relays, relaySearch, type Relay} from "@welshman/app"
import {Tags, isShareableRelayUrl, normalizeRelayUrl} from "@welshman/util"
import {createScroller} from "src/util/misc"
import {showWarning} from "src/partials/Toast.svelte"
@ -17,16 +17,13 @@
import RelayCard from "src/app/shared/RelayCard.svelte"
import Note from "src/app/shared/Note.svelte"
import {profileHasName, RelayMode} from "src/domain"
import type {RelayInfo} from "src/engine"
import {
load,
hints,
relays,
userFollows,
getProfile,
displayProfileByPubkey,
userRelayPolicies,
relaySearch,
getPubkeyRelayPolicies,
sortEventsDesc,
joinRelay,
@ -58,14 +55,14 @@
(term
? $relaySearch.searchOptions(term)
: sortBy(p => -(pubkeysByUrl.get(p.url)?.length || 0), $relaySearch.options)
).map((profile: RelayInfo) => {
const pubkeys = pubkeysByUrl.get(profile.url) || []
).map((relay: Relay) => {
const pubkeys = pubkeysByUrl.get(relay.url) || []
const description =
pubkeys.length > 0
? "Used by " + displayList(pubkeys.map(displayProfileByPubkey))
: profile.description
: relay.profile?.description
return {...profile, description}
return {...relay, description}
}),
)
@ -189,9 +186,9 @@
placeholder="Search relays or add a custom url">
<i slot="before" class="fa-solid fa-search" />
</Input>
{#each $searchRelays(q).slice(0, limit) as { url, description } (url)}
{#each $searchRelays(q).slice(0, limit) as { url, profile } (url)}
<RelayCard {url} ratings={ratings[url]}>
<p slot="description">{description || ""}</p>
<p slot="description">{profile?.description || ""}</p>
</RelayCard>
{/each}
{/if}

View File

@ -5,7 +5,7 @@
import {getAddress, WRAP, GROUP} from "@welshman/util"
import type {SignedEvent} from "@welshman/util"
import {Nip59, Nip01Signer, getPubkey} from "@welshman/signer"
import {session} from "@welshman/app"
import {session, relaySearch} from "@welshman/app"
import {toHex, nsecEncode, isKeyValid} from "src/util/nostr"
import {showInfo, showWarning} from "src/partials/Toast.svelte"
import CopyValue from "src/partials/CopyValue.svelte"
@ -21,7 +21,6 @@
import {
hints,
groupSharedKeys,
relaySearch,
userIsGroupMember,
groupAdminKeys,
subscribe,
@ -204,9 +203,8 @@
<Field label="Relays to search">
<SearchSelect
multiple
getKey={$relaySearch.displayValue}
search={$relaySearch.searchValues}
bind:value={relays}
search={$relaySearch.searchValues}
placeholder="wss://..." />
</Field>
<Anchor button accent loading={importing} on:click={finishImport}>Import key</Anchor>

View File

@ -3,14 +3,6 @@ import type {Nip46Handler} from "@welshman/signer"
import type {TrustedEvent, Zapper as WelshmanZapper} from "@welshman/util"
import type {Session} from "@welshman/app"
import {isTrustedEvent} from "@welshman/util"
import type {RelayProfile} from "src/domain"
export type RelayInfo = RelayProfile & {
count?: number
faults?: number[]
first_seen?: number
last_checked?: number
}
export enum GroupAccess {
None = null,

View File

@ -22,7 +22,6 @@ import {parseJson} from "src/util/misc"
import {normalizeRelayUrl} from "src/domain"
import {GroupAccess, type SessionWithMeta} from "src/engine/model"
import {
relays,
deriveAdminKeyForGroup,
getGroupStatus,
groupAdminKeys,
@ -181,21 +180,6 @@ projections.addHandler(0, e => {
updateZapper(e, content)
})
// Relays
projections.addHandler(RELAYS, (e: TrustedEvent) => {
for (const [key, value] of e.tags) {
if (["r", "relay"].includes(key) && isShareableRelayUrl(value)) {
relays.key(normalizeRelayUrl(value)).update($relay => ({
url: value,
last_checked: 0,
count: inc($relay?.count || 0),
first_seen: $relay?.first_seen || e.created_at,
}))
}
}
})
// Decrypt encrypted events eagerly
projections.addHandler(SEEN_GENERAL, ensurePlaintext)

View File

@ -48,12 +48,12 @@ import {
DEPRECATED_DIRECT_MESSAGE,
} from "@welshman/util"
import {makeDvmRequest} from "@welshman/dvm"
import {pubkey, repository, signer, updateSession} from "@welshman/app"
import {pubkey, relays, repository, signer, updateSession} from "@welshman/app"
import {updateIn} from "src/util/misc"
import {noteKinds, reactionKinds, repostKinds} from "src/util/nostr"
import {always, partition, pluck, uniq, without} from "ramda"
import {LIST_KINDS} from "src/domain"
import type {Zapper, RelayInfo, SessionWithMeta} from "src/engine/model"
import {LIST_KINDS, filterRelaysByNip} from "src/domain"
import type {Zapper, SessionWithMeta} from "src/engine/model"
import {
getUserCircles,
getGroupReqInfo,
@ -72,11 +72,9 @@ import {
getNetwork,
primeWotCaches,
publish,
getNip50Relays,
subscribe,
subscribePersistent,
dufflepud,
relays,
getFreshness,
setFreshness,
sessionWithMeta,
@ -247,6 +245,10 @@ export const createPeopleLoader = ({
onEvent = noop,
}: PeopleLoaderOpts = {}) => {
const loading = writable(false)
const nip50Relays = uniq([
...env.SEARCH_RELAYS,
...filterRelaysByNip(50, relays.get()).map(r => r.url),
])
return {
loading,
@ -260,7 +262,7 @@ export const createPeopleLoader = ({
onEvent,
skipCache: true,
forcePlatform: false,
relays: getNip50Relays().slice(0, 8),
relays: nip50Relays.slice(0, 8),
filters: [{kinds: [0], search: term, limit: 100}],
onComplete: async () => {
await sleep(Math.min(1000, Date.now() - now))
@ -557,34 +559,3 @@ export const loadHandlers = () =>
addSinceToFilter({kinds: [HANDLER_INFORMATION]}),
],
})
export const loadRelay = batcher(800, async (urls: string[]) => {
const urlSet = new Set(
urls
.map(url => normalizeRelayUrl(url))
.filter(url => getFreshness("relay", url) < now() - 3600),
)
for (const url of urlSet) {
setFreshness("relay", url, now())
}
const res = urlSet.size && (await postJson(dufflepud("relay/info"), {urls: Array.from(urlSet)}))
const index = indexBy((item: any) => item.url, res?.data || [])
const items: RelayInfo[] = urls.map(url => {
const normalizedUrl = normalizeRelayUrl(url)
const {info = {}} = index.get(normalizedUrl) || {}
return {...info, url: normalizedUrl, last_checked: now()}
})
relays.mapStore.update($relays => {
for (const relay of items) {
$relays.set(relay.url, {...($relays.get(relay.url) || {}), ...relay})
}
return $relays
})
return items
})

View File

@ -112,6 +112,7 @@ import {
tracker,
pubkey,
sessions,
relaysByUrl,
} from "@welshman/app"
import {fuzzy, synced, parseJson, fromCsv, SearchHelper} from "src/util/misc"
import {Collection as CollectionStore} from "src/util/store"
@ -150,7 +151,6 @@ import {
asDecryptedEvent,
normalizeRelayUrl,
makeRelayPolicy,
filterRelaysByNip,
displayRelayUrl,
readGroupMeta,
displayGroupMeta,
@ -167,7 +167,6 @@ import type {
Topic,
Zapper,
AnonymousUserState,
RelayInfo,
} from "src/engine/model"
import {sortEventsAsc} from "src/engine/utils"
import {GroupAccess, OnboardingTask} from "src/engine/model"
@ -219,7 +218,6 @@ export const groupHints = withGetter(writable<Record<string, string[]>>({}))
export const publishes = withGetter(writable<Record<string, PublishInfo>>({}))
export const groups = new CollectionStore<Group>("address")
export const relays = new CollectionStore<RelayInfo>("url")
export const groupAdminKeys = new CollectionStore<GroupKey>("pubkey")
export const groupSharedKeys = new CollectionStore<GroupKey>("pubkey")
export const groupRequests = new CollectionStore<GroupRequest>("id")
@ -1145,31 +1143,6 @@ export const channelHasNewMessages = (channel: Channel) =>
export const hasNewMessages = derived(channels, $channels => $channels.some(channelHasNewMessages))
// Relays
export const getRelay = url => defaultTo({url}, relays.key(url).get())
export const deriveRelay = url => derived(relays.key(url), defaultTo({url}))
export const getNip50Relays = () =>
uniq([...env.SEARCH_RELAYS, ...filterRelaysByNip(50, relays.get()).map(r => r.url)])
export class RelaySearch extends SearchHelper<RelayInfo, string> {
config = {keys: ["url", "name", "description"]}
getSearch = () => {
const search = fuzzy(this.options, this.config)
return term => (term ? search(term) : sortBy(r => -r.count || 0, this.options))
}
getValue = (option: RelayInfo) => option.url
displayValue = displayRelayUrl
}
export const relaySearch = derived(relays, $relays => new RelaySearch($relays))
// Relay policies
export const relayListEvents = deriveEvents(repository, {filters: [{kinds: [RELAYS]}]})
@ -1349,24 +1322,26 @@ export const hints = new Router({
const oneHour = 60 * oneMinute
const oneDay = 24 * oneHour
const oneWeek = 7 * oneDay
const {count = 0, faults = []} = relays.key(url).get() || {}
const relay = relaysByUrl.get().get(url)
const connect_count = relay?.stats?.connect_count || 0
const recent_errors = relay?.stats?.recent_errors || []
const connection = NetworkContext.pool.get(url, {autoConnect: false})
// If we haven't connected, consult our relay record and see if there has
// been a recent fault. If there has been, penalize the relay. If there have been several,
// don't use the relay.
if (!connection) {
const lastFault = last(faults) || 0
const lastFault = last(recent_errors) || 0
if (faults.filter(n => n > Date.now() - oneHour).length > 10) {
if (recent_errors.filter(n => n > Date.now() - oneHour).length > 10) {
return 0
}
if (faults.filter(n => n > Date.now() - oneDay).length > 50) {
if (recent_errors.filter(n => n > Date.now() - oneDay).length > 50) {
return 0
}
if (faults.filter(n => n > Date.now() - oneWeek).length > 100) {
if (recent_errors.filter(n => n > Date.now() - oneWeek).length > 100) {
return 0
}
@ -1380,7 +1355,7 @@ export const hints = new Router({
[ConnectionStatus.Closed]: 0.6,
[ConnectionStatus.Slow]: 0.5,
[ConnectionStatus.Ok]: 1,
default: clamp([0.5, 1], count / 1000),
default: clamp([0.5, 1], connect_count / 1000),
})
},
})
@ -2323,7 +2298,6 @@ export const storage = new Storage(16, [
objectAdapter("zappers", "key", zappers, {limit: 10000}),
objectAdapter("plaintext", "key", plaintext, {limit: 100000}),
objectAdapter("publishes2", "id", publishes, {sort: sortBy(prop("created_at"))}),
collectionAdapter("relays", "url", relays, {limit: 1000, sort: sortBy(prop("count"))}),
collectionAdapter("groups", "address", groups, {limit: 1000, sort: sortBy(prop("count"))}),
collectionAdapter("groupAlerts", "id", groupAlerts, {sort: sortBy(prop("created_at"))}),
collectionAdapter("groupRequests", "id", groupRequests, {sort: sortBy(prop("created_at"))}),