Fix nip 65, remove relays from people, pulling stats from routes table only

This commit is contained in:
Jonathan Staab 2023-02-16 13:25:01 -06:00
parent 12506c015b
commit 130773a90c
31 changed files with 156 additions and 195 deletions

View File

@ -7,10 +7,12 @@
- [ ] Separate user info and relays so we can still select/figure out relays for anons
- [ ] Separate petnames out as well so anons can follow people
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
- nip07.getRelays, nip05, relay.nostr.band
- [ ] Fix bugs on bugsnag
# Snacks
- [ ] DM/chat read status in encrypted note
- [ ] Relay recommendations based on follows/followers
- [ ] Pinned posts ala snort
- [ ] Likes list on note detail. Maybe a sidebar or header for note detail page?

View File

@ -13,7 +13,7 @@
import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, shuffle, now, sleep} from 'src/util/misc'
import cmd from 'src/agent/cmd'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getUserRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import keys from 'src/agent/keys'

View File

@ -13,7 +13,17 @@ const updateUser = (relays, updates) =>
publishEvent(relays, 0, {content: JSON.stringify(updates)})
const setRelays = (relays, newRelays) =>
publishEvent(relays, 10001, {tags: newRelays.map(r => [r.url, r.read || "", r.write || ""])})
publishEvent(relays, 10002, {
tags: newRelays.map(r => {
const t = ["r", r.url]
if (!r.write) {
t.push('read')
}
return t
}),
})
const setPetnames = (relays, petnames) =>
publishEvent(relays, 3, {tags: petnames})

View File

@ -1,47 +0,0 @@
import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {uniq, last} from 'ramda'
import {derived, get} from 'svelte/store'
import {Tags} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
import database from 'src/agent/database'
import keys from 'src/agent/keys'
export const user = derived(
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
if (!pubkey) {
return null
}
return ($people[pubkey] || {pubkey})
}
) as Readable<Person>
export const getMuffle = () => {
const $user = get(user) as Person
if (!$user?.muffle) {
return []
}
const shouldMuffle = t => Math.random() > parseFloat(last(t))
return Tags.wrap($user.muffle.filter(shouldMuffle)).values().all()
}
export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
export const getNetwork = pubkey =>
uniq(getFollows(pubkey).flatMap(getFollows))
export const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about
return uniq(pubkeys).filter(pubkey => {
const p = database.people.get(pubkey)
return !p || p.updated_at < now() - timedelta(1, 'days')
})
}

View File

@ -3,12 +3,12 @@
*
* cmd
* -> network
* -> helpers, pool
* -> user, pool
* -> keys
* -> sync
* -> database
*
* In other words, command/network depend on utility functions and the network to
* do their job. The database sits at the bottom since it's shared between helpers
* do their job. The database sits at the bottom since it's shared between user
* which query the database, and network which both queries and updates it.
*/

View File

@ -1,16 +1,25 @@
import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
import {personKinds, findReplyId} from 'src/util/nostr'
import {chunk} from 'hurdak/lib/hurdak'
import {batch} from 'src/util/misc'
import {getStalePubkeys} from 'src/agent/helpers'
import {batch, timedelta, now} from 'src/util/misc'
import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserNetworkWriteRelays,
} from 'src/agent/relays'
import database from 'src/agent/database'
import pool from 'src/agent/pool'
import keys from 'src/agent/keys'
import sync from 'src/agent/sync'
const getStalePubkeys = pubkeys => {
// If we're not reloading, only get pubkeys we don't already know about
return uniq(pubkeys).filter(pubkey => {
const p = database.people.get(pubkey)
return !p || p.updated_at < now() - timedelta(1, 'days')
})
}
const publish = async (relays, event) => {
const signedEvent = await keys.sign(event)

View File

@ -1,8 +1,9 @@
import {get} from 'svelte/store'
import {sortBy, uniq, uniqBy, prop, pluck} from 'ramda'
import {createMapOf, first} from 'hurdak/lib/hurdak'
import type {Relay} from 'src/util/types'
import {writable, get} from 'svelte/store'
import {pick, map, assoc, sortBy, uniq, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {Tags} from 'src/util/nostr'
import {getFollows} from 'src/agent/helpers'
import {getFollows} from 'src/agent/social'
import database from 'src/agent/database'
import keys from 'src/agent/keys'
@ -18,25 +19,24 @@ import keys from 'src/agent/keys'
// doesn't need to see.
// 5) Advertise relays — write and read back your own relay list
export const relays = writable([])
// Pubkey relays
export const getPubkeyRelays = pubkey => {
const person = database.getPersonWithFallback(pubkey)
export const getPubkeyRelays = (pubkey, mode = null) => {
const filter = mode ? {pubkey, mode} : {pubkey}
return scoreRelays(pubkey, person.relays || [])
return sortByScore(map(pick(['url', 'score']), database.routes.all(filter)))
}
export const getPubkeyReadRelays = pubkey =>
getPubkeyRelays(pubkey).filter(r => r.read !== '!')
export const getPubkeyReadRelays = pubkey => getPubkeyRelays(pubkey, 'read')
export const getPubkeyWriteRelays = pubkey =>
getPubkeyRelays(pubkey).filter(r => r.write !== '!')
export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, 'write')
// Multiple pubkeys
export const getAllPubkeyRelays = pubkeys =>
aggregateScores(pubkeys.map(getPubkeyRelays))
export const getAllPubkeyRelays = (pubkeys, mode = null) =>
aggregateScores(pubkeys.map(pubkey => getPubkeyRelays(pubkey, mode)))
export const getAllPubkeyReadRelays = pubkeys =>
aggregateScores(pubkeys.map(getPubkeyReadRelays))
@ -46,9 +46,9 @@ export const getAllPubkeyWriteRelays = pubkeys =>
// Current user
export const getUserRelays = () => getPubkeyRelays(get(keys.pubkey))
export const getUserReadRelays = () => getPubkeyReadRelays(get(keys.pubkey))
export const getUserWriteRelays = () => getPubkeyWriteRelays(get(keys.pubkey))
export const getUserRelays = (): Array<Relay> => get(relays).map(assoc('score', 1))
export const getUserReadRelays = () => getUserRelays().filter(prop('read'))
export const getUserWriteRelays = () => getUserRelays().filter(prop('write'))
// Network relays
@ -111,13 +111,6 @@ export const getEventPublishRelays = event => {
const uniqByUrl = uniqBy(prop('url'))
const sortByScore = sortBy(r => -r.score)
const scoreRelays = (pubkey, relays) => {
const routes = database.routes.all({pubkey, url: pluck('url', relays)})
const scores = createMapOf('url', 'score', routes)
return uniqByUrl(sortByScore(relays.map(r => ({...r, score: scores[r.url] || 0}))))
}
export const aggregateScores = relayGroups => {
const scores = {} as Record<string, {
score: number,

9
src/agent/social.ts Normal file
View File

@ -0,0 +1,9 @@
import {uniq} from 'ramda'
import {Tags} from 'src/util/nostr'
import database from 'src/agent/database'
export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
export const getNetwork = pubkey =>
uniq(getFollows(pubkey).flatMap(getFollows))

View File

@ -41,49 +41,8 @@ const processProfileEvents = async events => {
return content
})
},
2: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
const {relays = []} = database.getPersonWithFallback(e.pubkey)
return {
relays: relays.concat({url: e.content}),
relays_updated_at: e.created_at,
}
}
},
3: () => {
const data = {petnames: e.tags}
if (e.created_at > (person.relays_updated_at || 0)) {
tryJson(() => {
Object.assign(data, {
relays_updated_at: e.created_at,
relays: Object.entries(JSON.parse(e.content))
.map(([url, conditions]) => {
const {write, read} = conditions as Record<string, boolean|string>
return {
url,
write: [false, '!'].includes(write) ? '!' : '',
read: [false, '!'].includes(read) ? '!' : '',
}
})
.filter(r => isRelay(r.url)),
})
})
}
return data
},
3: () => ({petnames: e.tags}),
12165: () => ({muffle: e.tags}),
10001: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
return {
relays: e.tags.map(([url, read, write]) => ({url, read, write})),
relays_updated_at: e.created_at,
}
}
},
default: () => {
log(`Received unsupported event type ${e.kind}`)
},
@ -153,7 +112,7 @@ const processMessages = async events => {
const getWeight = type => {
if (type === 'nip05') return 1
if (type === 'kind:10001') return 1
if (type === 'kind:10002') return 1
if (type === 'kind:3') return 0.8
if (type === 'kind:2') return 0.5
if (type === 'seen') return 0.2
@ -216,19 +175,14 @@ const processRoutes = async events => {
})
})
},
10001: () => {
10002: () => {
e.tags
.forEach(([url, read, write]) => {
if (![false, '!'].includes(write)) {
updates.push(
calculateRoute(e.pubkey, url, 'kind:100001', 'write', e.created_at)
)
}
if (![false, '!'].includes(read)) {
updates.push(
calculateRoute(e.pubkey, url, 'kind:100001', 'read', e.created_at)
)
.forEach(([url, read, mode]) => {
if (mode) {
calculateRoute(e.pubkey, url, 'kind:10002', mode, e.created_at)
} else {
calculateRoute(e.pubkey, url, 'kind:10002', 'read', e.created_at)
calculateRoute(e.pubkey, url, 'kind:10002', 'write', e.created_at)
}
})
},

16
src/agent/user.ts Normal file
View File

@ -0,0 +1,16 @@
import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {derived} from 'svelte/store'
import database from 'src/agent/database'
import keys from 'src/agent/keys'
export const user = derived(
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
if (!pubkey) {
return null
}
return ($people[pubkey] || {pubkey})
}
) as Readable<Person>

View File

@ -5,9 +5,9 @@ import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store'
import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user, getNetwork} from 'src/agent/helpers'
import {getUserWriteRelays} from 'src/agent/relays'
import defaults from 'src/agent/defaults'
import {user} from 'src/agent/user'
import {getNetwork} from 'src/agent/social'
import {relays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import keys from 'src/agent/keys'
@ -49,44 +49,50 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
export const addRelay = async url => {
const person = get(user) as Person
const modify = relays => relays.concat({url, write: '!'})
// Set to defaults to support anonymous usage
defaults.relays = modify(defaults.relays)
relays.update($relays => {
$relays.push({url, write: false, read: true})
if (person) {
const relays = modify(person.relays || [])
if (person) {
(async () => {
// Publish to the new set of relays
await cmd.setRelays($relays, $relays)
// Publish to the new set of relays
await cmd.setRelays(relays, relays)
// Reload alerts, messages, etc
await loadAppData(person.pubkey)
})()
}
// Reload alerts, messages, etc
await loadAppData(person.pubkey)
}
return $relays
})
}
export const removeRelay = async url => {
const person = get(user) as Person
const modify = relays => reject(whereEq({url}), relays)
// Set to defaults to support anonymous usage
defaults.relays = modify(defaults.relays)
relays.update($relays => {
$relays = reject(whereEq({url}), $relays)
if (person) {
await cmd.setRelays(getUserWriteRelays(), modify(person.relays || []))
}
if (person && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}
return $relays
})
}
export const setRelayWriteCondition = async (url, write) => {
const person = get(user) as Person
const modify = relays => relays.map(when(whereEq({url}), assoc('write', write)))
// Set to defaults to support anonymous usage
defaults.relays = modify(defaults.relays)
relays.update($relays => {
$relays = $relays.map(when(whereEq({url}), assoc('write', write)))
if (person) {
await cmd.setRelays(getUserWriteRelays(), modify(person.relays || []))
}
if (person && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}
return $relays
})
}
export const renderNote = (note, {showEntire = false}) => {

View File

@ -1,7 +1,7 @@
import {pluck, reject} from 'ramda'
import {get} from 'svelte/store'
import {synced, now, timedelta} from 'src/util/misc'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'

View File

@ -8,7 +8,7 @@
import Badge from 'src/partials/Badge.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import database from 'src/agent/database'
import {renderNote} from 'src/app'

View File

@ -16,7 +16,7 @@
import {formatTimestamp, stringToColor} from 'src/util/misc'
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getEventPublishRelays, getRelaysForEventParent} from 'src/agent/relays'
import database from 'src/agent/database'
import cmd from 'src/agent/cmd'
@ -217,7 +217,7 @@
{/if}
</Anchor>
<Anchor
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on.url]})}
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on]})}
class="text-sm text-light"
type="unstyled">
{formatTimestamp(note.created_at)}

View File

@ -1,13 +1,14 @@
<script lang="ts">
import {onMount} from 'svelte'
import {partition, propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
import {partition, last, propEq, always, mergeRight, uniqBy, sortBy, prop} from 'ramda'
import {slide} from 'svelte/transition'
import {quantify} from 'hurdak/lib/hurdak'
import {createScroller, now, Cursor} from 'src/util/misc'
import {Tags} from 'src/util/nostr'
import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte'
import Note from "src/partials/Note.svelte"
import {getMuffle} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import network from 'src/agent/network'
import {modal, mergeParents} from "src/app"
@ -20,8 +21,10 @@
const since = now()
const maxNotes = 300
const muffle = getMuffle()
const cursor = new Cursor()
const muffle = Tags
.wrap(($user?.muffle || []).filter(t => Math.random() > parseFloat(last(t))))
.values().all()
const processNewNotes = async newNotes => {
// Remove people we're not interested in hearing about, sort by created date

View File

@ -6,7 +6,7 @@
import {between} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import Toggle from "src/partials/Toggle.svelte"
import {user} from "src/agent/helpers"
import {relays} from "src/agent/relays"
import pool from 'src/agent/pool'
import {addRelay, removeRelay, setRelayWriteCondition} from "src/app"
@ -19,7 +19,7 @@
let showStatus = false
let joined = false
$: joined = find(propEq('url', relay.url), $user?.relays || [])
$: joined = find(propEq('url', relay.url), $relays)
onMount(() => {
return poll(10_000, async () => {

View File

@ -4,7 +4,7 @@
import {nip19} from 'nostr-tools'
import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc"
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import network from 'src/agent/network'
import database from 'src/agent/database'
import {getUserReadRelays} from 'src/agent/relays'

View File

@ -3,7 +3,7 @@
import {nip19} from 'nostr-tools'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getRelaysForEventChildren} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'

View File

@ -4,7 +4,7 @@
import {personKinds} from 'src/util/nostr'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getAllPubkeyRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'

View File

@ -6,7 +6,7 @@
import Tabs from "src/partials/Tabs.svelte"
import Network from "src/views/notes/Network.svelte"
import Popular from "src/views/notes/Popular.svelte"
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
export let activeTab

View File

@ -16,7 +16,7 @@
import Notes from "src/views/person/Notes.svelte"
import Likes from "src/views/person/Likes.svelte"
import Relays from "src/views/person/Relays.svelte"
import {user} from "src/agent/helpers"
import {user} from "src/agent/user"
import {getPubkeyWriteRelays, getUserWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import keys from "src/agent/keys"

View File

@ -11,7 +11,7 @@
import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {user} from "src/agent/helpers"
import {user} from "src/agent/user"
import {getUserWriteRelays} from 'src/agent/relays'
import cmd from "src/agent/cmd"
import {toast} from "src/app"

View File

@ -8,14 +8,14 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {user} from "src/agent/helpers"
import {relays} from "src/agent/relays"
import database from 'src/agent/database'
import {modal, settings} from "src/app"
import defaults from "src/agent/defaults"
let q = ""
let search
let relays = []
let knownRelays = database.watch('relays', t => t.all())
fetch(get(settings).dufflepudUrl + '/relay')
.then(async res => {
@ -26,15 +26,13 @@
database.relays.bulkPatch(createMap('url', defaults.relays))
const knownRelays = database.watch('relays', relays => relays.all())
$: {
relays = $user?.relays || []
const joined = new Set(pluck('url', $relays))
const joined = new Set(pluck('url', relays))
const data = ($knownRelays || []).filter(r => !joined.has(r.url))
search = fuzzy(data, {keys: ["name", "description", "url"]})
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}
</script>
@ -52,11 +50,11 @@
Relays are hubs for your content and connections. At least one is required to
interact with the network, but you can join as many as you like.
</p>
{#if relays.length === 0}
{#if $relays.length === 0}
<div class="text-center">No relays connected</div>
{/if}
<div class="grid grid-cols-1 gap-4">
{#each relays as relay (relay.url)}
{#each $relays as relay (relay.url)}
<RelayCard showControls {relay} />
{/each}
</div>
@ -79,8 +77,8 @@
<RelayCard {relay} />
{/each}
<small class="text-center">
Showing {Math.min(($knownRelays || []).length - relays.length, 50)}
of {($knownRelays || []).length - relays.length} known relays
Showing {Math.min(($knownRelays || []).length - $relays.length, 50)}
of {($knownRelays || []).length - $relays.length} known relays
</small>
</div>
</Content>

View File

@ -7,7 +7,7 @@
import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {toast, settings} from "src/app"
let values = {...$settings}

View File

@ -2,7 +2,7 @@ import {last, identity, objOf, prop, flatten, uniq} from 'ramda'
import {nip19} from 'nostr-tools'
import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak'
export const personKinds = [0, 2, 3, 10001, 12165]
export const personKinds = [0, 2, 3, 10002, 12165]
export class Tags {
constructor(tags) {

View File

@ -2,6 +2,9 @@ import type {Event} from 'nostr-tools'
export type Relay = {
url: string
score?: number
write?: boolean
read?: boolean
}
export type Person = {

View File

@ -2,7 +2,7 @@
import {onMount} from "svelte"
import {nip19} from 'nostr-tools'
import {quantify} from 'hurdak/lib/hurdak'
import {last, whereEq, find, reject, pluck, propEq} from 'ramda'
import {last, reject, pluck, propEq} from 'ramda'
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc"
@ -13,7 +13,7 @@
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import Heading from 'src/partials/Heading.svelte'
import {user} from "src/agent/helpers"
import {user} from "src/agent/user"
import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import cmd from "src/agent/cmd"
@ -27,12 +27,15 @@
let q = ''
let search
const knownRelays = database.watch('relays', relays => relays.all())
const knownRelays = database.watch('relays', t => t.all())
$: {
const data = reject(({url}) => find(whereEq({url}), relays), $knownRelays || [])
const joined = new Set(pluck('url', relays))
search = fuzzy(data, {keys: ["name", "description", "url"]})
search = fuzzy(
$knownRelays.filter(r => !joined.has(r.url)),
{keys: ["name", "description", "url"]}
)
}
const onSubmit = async () => {

View File

@ -5,7 +5,7 @@
import Button from "src/partials/Button.svelte"
import Content from 'src/partials/Content.svelte'
import SelectButton from "src/partials/SelectButton.svelte"
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getUserWriteRelays} from 'src/agent/relays'
import cmd from 'src/agent/cmd'
import {modal} from 'src/app'

View File

@ -3,7 +3,7 @@
import {personKinds} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import PersonInfo from 'src/partials/PersonInfo.svelte'
import {user} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'

View File

@ -1,7 +1,8 @@
<script>
import {uniq} from 'ramda'
import Notes from "src/partials/Notes.svelte"
import {user, getFollows, getNetwork} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getFollows, getNetwork} from 'src/agent/social'
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
// Get first- and second-order follows. shuffle and slice network so we're not

View File

@ -2,7 +2,8 @@
import {uniq} from 'ramda'
import Notes from "src/partials/Notes.svelte"
import {isLike} from 'src/util/nostr'
import {user, getFollows, getNetwork} from 'src/agent/helpers'
import {user} from 'src/agent/user'
import {getFollows, getNetwork} from 'src/agent/social'
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
// Get first- and second-order follows. shuffle and slice network so we're not