Move cmd over

This commit is contained in:
Jonathan Staab 2023-06-30 14:14:20 -07:00
parent 9b9fe3e86d
commit b34a0c51ea
34 changed files with 376 additions and 353 deletions

View File

@ -1,197 +0,0 @@
import {identity, last, pick, uniqBy} from "ramda"
import {get} from "svelte/store"
import {doPipe} from "hurdak/lib/hurdak"
import {Tags, roomAttrs, findRoot, findReply} from "src/util/nostr"
import {parseContent} from "src/util/notes"
import {getRelayForPersonHint, getRelayForEventHint} from "src/agent/relays"
import pool from "src/agent/pool"
import sync from "src/agent/sync"
import keys from "src/system/keys"
const ext = {displayPubkey: identity}
const authenticate = (url, challenge) =>
new PublishableEvent(22242, {
tags: [
["challenge", challenge],
["relay", url],
],
})
const updateUser = updates => new PublishableEvent(0, {content: JSON.stringify(updates)})
const setRelays = newRelays =>
new PublishableEvent(10002, {
tags: newRelays.map(r => {
const t = ["r", r.url]
if (!r.write) {
t.push("read")
}
return t
}),
})
const setAppData = (d, content) => new PublishableEvent(30078, {content, tags: [["d", d]]})
const setPetnames = petnames => new PublishableEvent(3, {tags: petnames})
const setMutes = mutes => new PublishableEvent(10000, {tags: mutes})
const createList = list => new PublishableEvent(30001, {tags: list})
const createRoom = room =>
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
const updateRoom = ({id, ...room}) =>
new PublishableEvent(41, {content: JSON.stringify(pick(roomAttrs, room)), tags: [["e", id]]})
const createChatMessage = (roomId, content, url) =>
new PublishableEvent(42, {content, tags: [["e", roomId, url, "root"]]})
const createDirectMessage = (pubkey, content) =>
new PublishableEvent(4, {content, tags: [["p", pubkey]]})
const createNote = (content, tags = []) =>
new PublishableEvent(1, {content, tags: uniqTags(tagsFromContent(content, tags))})
const createReaction = (note, content) =>
new PublishableEvent(7, {content, tags: getReplyTags(note)})
const createReply = (note, content, tags = []) =>
new PublishableEvent(1, {
content,
tags: doPipe(tags, [
tags => tags.concat(getReplyTags(note, true)),
tags => tagsFromContent(content, tags),
uniqTags,
]),
})
const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => {
const tags = [
["relays", ...relays],
["amount", amount.toString()],
["lnurl", lnurl],
["p", pubkey],
]
if (eventId) {
tags.push(["e", eventId])
}
return new PublishableEvent(9734, {content, tags, tagClient: false})
}
const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id])})
const createLabel = payload => new PublishableEvent(1985, payload)
// Utils
const tagsFromContent = (content, tags) => {
const seen = new Set(Tags.wrap(tags).values().all())
for (const {type, value} of parseContent({content})) {
if (type === "topic") {
tags = tags.concat([["t", value]])
seen.add(value)
}
if (type.match(/nostr:(note|nevent)/) && !seen.has(value.id)) {
tags = tags.concat([["e", value.id, value.relays?.[0] || "", "mention"]])
seen.add(value.id)
}
if (type.match(/nostr:(nprofile|npub)/) && !seen.has(value.pubkey)) {
tags = tags.concat([mention(value.pubkey)])
seen.add(value.pubkey)
}
}
return tags
}
const mention = pubkey => {
const name = ext.displayPubkey(pubkey)
const hint = getRelayForPersonHint(pubkey)
return ["p", pubkey, hint?.url || "", name]
}
const getReplyTags = (n, inherit = false) => {
const extra = inherit
? Tags.from(n)
.type("e")
.reject(t => last(t) === "mention")
.all()
.map(t => t.slice(0, 3))
: []
const pHint = getRelayForPersonHint(n.pubkey)
const eHint = getRelayForEventHint(n) || pHint
const reply = ["e", n.id, eHint?.url || "", "reply"]
const root = doPipe(findRoot(n) || findReply(n) || reply, [
t => (t.length < 3 ? t.concat(eHint?.url || "") : t),
t => t.slice(0, 3).concat("root"),
])
return [mention(n.pubkey), root, ...extra, reply]
}
const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
class PublishableEvent {
event: Record<string, any>
constructor(kind, {content = "", tags = [], tagClient = true}) {
const pubkey = get(keys.pubkey)
const createdAt = Math.round(new Date().valueOf() / 1000)
if (tagClient) {
tags = tags.filter(t => t[0] !== "client").concat([["client", "coracle"]])
}
this.event = {kind, content, tags, pubkey, created_at: createdAt}
}
getSignedEvent() {
try {
return keys.sign(this.event)
} catch (e) {
console.log(this.event)
throw e
}
}
async publish(relays, onProgress = null, verb = "EVENT") {
const event = await this.getSignedEvent()
// return console.log(event)
const promise = pool.publish({relays, event, onProgress, verb})
// Copy the event since loki mutates it to add metadata
sync.processEvents({...event, seen_on: []})
return [event, promise]
}
}
export default {
ext,
mention,
authenticate,
updateUser,
setRelays,
setAppData,
setPetnames,
setMutes,
createList,
createRoom,
updateRoom,
createChatMessage,
createDirectMessage,
createNote,
createReaction,
createReply,
requestZap,
deleteEvent,
createLabel,
PublishableEvent,
}

View File

@ -5,7 +5,6 @@ import {partition, uniqBy, sortBy, prop, always, pluck, without, is} from "ramda
import {throttle} from "throttle-debounce"
import {writable} from "svelte/store"
import {ensurePlural, noop, createMap} from "hurdak/lib/hurdak"
import {fuzzy} from "src/util/misc"
const Adapter = window.indexedDB ? IncrementalIndexedDBAdapter : Loki.LokiMemoryAdapter
@ -210,7 +209,6 @@ export const userEvents = new Table("userEvents", "id", {max: 2000, sort: sortBy
export const notifications = new Table("notifications", "id", {sort: sortByCreatedAt})
export const contacts = new Table("contacts", "pubkey")
export const rooms = new Table("rooms", "id")
export const relays = new Table("relays", "url")
export const routes = new Table("routes", "id", {max: 10000, sort: sortByScore})
const ready = writable(false)

View File

@ -7,7 +7,10 @@ import {ensurePlural, switcher} from "hurdak/lib/hurdak"
import {warn, log, error} from "src/util/logger"
import {union, sleep, difference} from "src/util/misc"
import {normalizeRelayUrl} from "src/util/nostr"
import {relays} from "src/agent/db"
const ext = {
routing: null,
}
const Config = {
multiplextrUrl: null,
@ -218,8 +221,8 @@ async function getExecutor(urls, {bypassBoot = false} = {}) {
// Eagerly connect and handle AUTH
await Promise.all(
ensurePlural(executor.target.sockets).map(async socket => {
const relay = relays.get(socket.url)
const waitForBoot = relay?.limitation?.payment_required || relay?.limitation?.auth_required
const {limitation} = ext.routing.getRelayMeta(socket.url)
const waitForBoot = limitation?.payment_required || limitation?.auth_required
if (socket.status === Socket.STATUS.NEW) {
socket.booted = sleep(2000)
@ -398,6 +401,7 @@ async function count(filter) {
}
export default {
ext,
Config,
Meta,
forceUrls,

View File

@ -1,11 +1,10 @@
import type {Relay} from "src/util/types"
import LRUCache from "lru-cache"
import {warn} from "src/util/logger"
import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from "ramda"
import {first} from "hurdak/lib/hurdak"
import {Tags, isRelay, findReplyId} from "src/util/nostr"
import {shuffle, fetchJson} from "src/util/misc"
import {relays, routes} from "src/agent/db"
import {shuffle} from "src/util/misc"
import {routes} from "src/agent/db"
import pool from "src/agent/pool"
import user from "src/agent/user"
@ -21,25 +20,6 @@ import user from "src/agent/user"
// doesn't need to see.
// 5) Advertise relays — write and read back your own relay list
// Initialize our database
export const initializeRelayList = async () => {
// Throw some hardcoded defaults in there
await relays.patch(pool.defaultRelays)
// Load relays from nostr.watch via dufflepud
if (pool.forceUrls.length === 0) {
try {
const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay"
const json = await fetchJson(url)
await relays.patch(json.relays.filter(isRelay).map(objOf("url")))
} catch (e) {
warn("Failed to fetch relays list", e)
}
}
}
// Pubkey relays
const _getPubkeyRelaysCache = new LRUCache({max: 1000})

View File

@ -1,8 +1,8 @@
import {partition, is, uniq, reject, pick, identity} from "ramda"
import {partition, is, reject, pick, identity} from "ramda"
import {ensurePlural, chunk} from "hurdak/lib/hurdak"
import {now, sleep, tryJson, timedelta, hash} from "src/util/misc"
import {Tags, roomAttrs, isRelay, isShareableRelay, normalizeRelayUrl} from "src/util/nostr"
import {userEvents, rooms, routes} from "src/agent/db"
import {sleep, tryJson} from "src/util/misc"
import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from "src/util/nostr"
import {userEvents, rooms} from "src/agent/db"
import {uniqByUrl} from "src/agent/relays"
import keys from "src/system/keys"
import user from "src/agent/user"

View File

@ -6,7 +6,8 @@ import {synced} from "src/util/misc"
import {derived, get} from "svelte/store"
import keys from "src/system/keys"
import pool from "src/agent/pool"
import cmd from "src/agent/cmd"
const ext = {cmd: null}
const profile = synced("agent/user/profile", {
pubkey: null,
@ -44,6 +45,8 @@ keys.pubkey.subscribe($pubkey => {
})
export default {
ext,
// Profile
profile,
@ -59,7 +62,7 @@ export default {
const d = `coracle/${key}`
const v = await keys.encryptJson(content)
return cmd.setAppData(d, v).publish(profileCopy.relays)
return ext.cmd.setAppData(d, v).publish(profileCopy.relays)
}
},
setLastChecked(k, v) {
@ -82,7 +85,7 @@ export default {
profile.update(assoc("relays", $relays))
if (get(keys.canSign)) {
return cmd.setRelays($relays).publish($relays)
return ext.cmd.setRelays($relays).publish($relays)
}
},
addRelay(url) {
@ -112,7 +115,7 @@ export default {
profile.update(assoc("mutes", $mutes))
if (get(keys.canSign)) {
return cmd.setMutes($mutes.map(slice(0, 2))).publish(profileCopy.relays)
return ext.cmd.setMutes($mutes.map(slice(0, 2))).publish(profileCopy.relays)
}
},
addMute(type, value) {
@ -135,12 +138,12 @@ export default {
const tags = [["d", name]].concat(params).concat(relays)
if (id) {
await cmd.deleteEvent([id]).publish(profileCopy.relays)
await ext.cmd.deleteEvent([id]).publish(profileCopy.relays)
}
await cmd.createList(tags).publish(profileCopy.relays)
await ext.cmd.createList(tags).publish(profileCopy.relays)
},
removeList(id) {
return cmd.deleteEvent([id]).publish(profileCopy.relays)
return ext.cmd.deleteEvent([id]).publish(profileCopy.relays)
},
}

View File

@ -7,15 +7,21 @@
import {get} from "svelte/store"
import {Router, links} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history"
import {identity, isNil, last} from "ramda"
import {isNil, last} from "ramda"
import {first} from "hurdak/lib/hurdak"
import {warn} from "src/util/logger"
import {timedelta, hexToBech32, bech32ToHex, shuffle, now, tryFunc} from "src/util/misc"
import cmd from "src/agent/cmd"
import {onReady, relays} from "src/agent/db"
import {
timedelta,
hexToBech32,
bech32ToHex,
shuffle,
now,
tryFunc,
fetchJson,
tryFetch,
} from "src/util/misc"
import {onReady} from "src/agent/db"
import * as system from "src/system"
import pool from "src/agent/pool"
import {initializeRelayList} from "src/agent/relays"
import user from "src/agent/user"
import {loadAppData} from "src/app/state"
import {theme, getThemeVariables, appName, modal} from "src/partials/state"
@ -53,7 +59,7 @@
if (get(system.keys.canSign) && !seenChallenges.has(challenge)) {
seenChallenges.add(challenge)
const publishable = await cmd.authenticate(url, challenge)
const publishable = await system.cmd.authenticate(url, challenge)
return first(publishable.publish([{url}], null, "AUTH"))
}
@ -116,7 +122,7 @@
})
onReady(() => {
initializeRelayList()
system.initialize()
const pubkey = user.getPubkey()
@ -133,32 +139,28 @@
// Find relays with old/missing metadata and refresh them. Only pick a
// few so we're not sending too many concurrent http requests
const query = {refreshed_at: {$lt: now() - timedelta(7, "days")}}
const staleRelays = shuffle(relays.all(query)).slice(0, 10)
const query = {"meta.last_checked": {$lt: now() - timedelta(7, "days")}}
const staleRelays = shuffle(system.routing.relays.all(query)).slice(0, 10)
const freshRelays = await Promise.all(
staleRelays.map(async ({url}) => {
try {
const res = await fetch(dufflepudUrl + "/relay/info", {
method: "POST",
body: JSON.stringify({url}),
headers: {
"Content-Type": "application/json",
},
system.routing.relays.patch(
await Promise.all(
staleRelays.map(relay =>
tryFetch(async () => {
const meta = await fetchJson(dufflepudUrl + "/relay/info", {
method: "POST",
body: JSON.stringify({url: relay.url}),
headers: {
"Content-Type": "application/json",
},
})
meta.last_checked = now()
return {...relay, meta}
})
return {...(await res.json()), url, refreshed_at: now()}
} catch (e) {
if (!e.toString().includes("Failed to fetch")) {
warn(e)
}
return {url, refreshed_at: now()}
}
})
)
)
)
relays.patch(freshRelays.filter(identity))
}, 30_000)
return () => {

View File

@ -4,7 +4,7 @@
import {tweened} from "svelte/motion"
import {find, reject, identity, propEq, pathEq, sum, pluck, sortBy} from "ramda"
import {stringToHue, formatSats, hsl} from "src/util/misc"
import {displayRelay, isLike} from "src/util/nostr"
import {isLike} from "src/util/nostr"
import {quantify, first} from "hurdak/lib/hurdak"
import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte"
@ -14,11 +14,10 @@
import CopyValue from "src/partials/CopyValue.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {ENABLE_ZAPS, keys, nip57} from "src/system"
import {ENABLE_ZAPS, keys, nip57, cmd, routing} from "src/system"
import {getEventPublishRelays} from "src/agent/relays"
import pool from "src/agent/pool"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
export let note
export let reply
@ -164,7 +163,7 @@
style={`background: ${hsl(stringToHue(url))}`}
on:click={() => setFeedRelay?.({url})} />
</div>
<div slot="tooltip">{displayRelay({url})}</div>
<div slot="tooltip">{routing.displayRelay({url})}</div>
</Popover>
{/each}
</div>

View File

@ -1,10 +1,10 @@
<script lang="ts">
import {switcher, switcherFn} from "hurdak/lib/hurdak"
import {displayRelay, Tags} from "src/util/nostr"
import {Tags} from "src/util/nostr"
import {modal} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Rating from "src/partials/Rating.svelte"
import {directory} from "src/system"
import {directory, routing} from "src/system"
export let note, rating
@ -19,7 +19,7 @@
})
const display = switcherFn(type, {
r: () => displayRelay({url: value}),
r: () => routing.displayRelay({url: value}),
p: () => directory.displayProfile(value),
e: () => "a note",
default: "something",

View File

@ -9,10 +9,9 @@
import Chip from "src/partials/Chip.svelte"
import Media from "src/partials/Media.svelte"
import Compose from "src/partials/Compose.svelte"
import {directory} from "src/system"
import {directory, cmd} from "src/system"
import {getEventPublishRelays} from "src/agent/relays"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
import {publishWithToast} from "src/app/state"
export let note

View File

@ -47,9 +47,9 @@
})
}
if (relay.contact) {
if (relay.meta.contact) {
actions.push({
onClick: () => window.open("mailto:" + last(relay.contact.split(":"))),
onClick: () => window.open("mailto:" + last(relay.meta.contact.split(":"))),
label: "Contact",
icon: "envelope",
})

View File

@ -5,12 +5,11 @@
import {onMount} from "svelte"
import {fly} from "src/util/transition"
import {poll, stringToHue, hsl} from "src/util/misc"
import {displayRelay} from "src/util/nostr"
import {modal} from "src/partials/state"
import Toggle from "src/partials/Toggle.svelte"
import Rating from "src/partials/Rating.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {keys} from "src/system"
import {keys, routing} from "src/system"
import pool from "src/agent/pool"
import user from "src/agent/user"
import {loadAppData} from "src/app/state"
@ -65,7 +64,7 @@
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 text-xl">
<i class={relay.url.startsWith("ws://") ? "fa fa-unlock" : "fa fa-lock"} />
<Anchor on:click={openModal}>{displayRelay(relay)}</Anchor>
<Anchor on:click={openModal}>{routing.displayRelay(relay)}</Anchor>
{#if showStatus}
<span
on:mouseout={() => {

View File

@ -6,6 +6,7 @@
import {normalizeRelayUrl, Tags, getAvgQuality} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {routing} from "src/system"
import {getUserReadRelays} from "src/agent/relays"
import network from "src/agent/network"
import {watch} from "src/agent/db"
@ -19,7 +20,8 @@
let search
let reviews = []
let knownRelays = watch("relays", t => t.all())
const knownRelays = watch(routing.relays, () => routing.relays.all())
$: ratings = mapValues(
events => getAvgQuality("review/relay", events),

View File

@ -1,11 +1,11 @@
<script lang="ts">
import {onMount} from "svelte"
import {between} from "hurdak/lib/hurdak"
import {displayRelay} from "src/util/nostr"
import {webSocketURLToPlainOrBase64} from "src/util/misc"
import {poll, stringToHue, hsl} from "src/util/misc"
import Rating from "src/partials/Rating.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {routing} from "src/system"
import pool from "src/agent/pool"
export let relay
@ -29,7 +29,7 @@
href={`/relays/${webSocketURLToPlainOrBase64(relay.url)}`}
class="border-b border-solid"
style={`border-color: ${hsl(stringToHue(relay.url))}`}>
{displayRelay(relay)}
{routing.displayRelay(relay)}
</Anchor>
<span
on:mouseout={() => {

View File

@ -6,11 +6,11 @@
import Anchor from "src/partials/Anchor.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import {cmd} from "src/system"
import user from "src/agent/user"
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
import network from "src/agent/network"
import {watch} from "src/agent/db"
import cmd from "src/agent/cmd"
export let entity

View File

@ -9,8 +9,8 @@
import Anchor from "src/partials/Anchor.svelte"
import {toast, modal} from "src/partials/state"
import {getUserWriteRelays} from "src/agent/relays"
import {cmd} from "src/system"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
import {publishWithToast} from "src/app/state"
export let room = {name: null, id: null, about: null, picture: null}

View File

@ -1,6 +1,6 @@
<script>
import {pluck, find} from "ramda"
import {Tags, displayRelay} from "src/util/nostr"
import {Tags} from "src/util/nostr"
import {modal, toast} from "src/partials/state"
import Heading from "src/partials/Heading.svelte"
import PersonBadge from "src/app/shared/PersonBadge.svelte"
@ -87,7 +87,7 @@
<strong>Relays</strong>
<MultiSelect search={_searchRelays} bind:value={values.relays}>
<div slot="item" let:item>
{displayRelay({url: item[1]})}
{routing.displayRelay({url: item[1]})}
</div>
</MultiSelect>
<p class="text-sm text-gray-4">

View File

@ -13,6 +13,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Modal from "src/partials/Modal.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {DEFAULT_RELAYS, routing} from "src/system"
import {watch} from "src/agent/db"
import network from "src/agent/network"
import user from "src/agent/user"
@ -26,11 +27,11 @@
let attemptedRelays = new Set()
let customRelays = []
let allRelays = []
let knownRelays = watch("relays", table =>
let knownRelays = watch(routing.relays, () =>
uniqBy(
prop("url"),
// Make sure our hardcoded urls are first, since they're more likely to find a match
pool.defaultUrls.map(objOf("url")).concat(shuffle(table.all()))
DEFAULT_RELAYS.map(objOf("url")).concat(shuffle(routing.relays.all()))
)
)

View File

@ -9,9 +9,8 @@
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
import {watch} from "src/agent/db"
import network from "src/agent/network"
import {keys, directory} from "src/system"
import {keys, directory, cmd} from "src/system"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
import {routes} from "src/app/state"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"

View File

@ -15,9 +15,8 @@
import RelayCard from "src/app/shared/RelayCard.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import RelaySearch from "src/app/shared/RelaySearch.svelte"
import {directory} from "src/system"
import {directory, cmd} from "src/system"
import {getUserWriteRelays, getRelayForPersonHint} from "src/agent/relays"
import cmd from "src/agent/cmd"
import user from "src/agent/user"
import {toast, modal} from "src/partials/state"
import {publishWithToast} from "src/app/state"

View File

@ -12,8 +12,7 @@
import {directory} from "src/system"
import {getEventPublishRelays} from "src/agent/relays"
import network from "src/agent/network"
import {keys, settings, nip57} from "src/system"
import cmd from "src/agent/cmd"
import {keys, cmd, settings, nip57} from "src/system"
export let note

View File

@ -14,8 +14,7 @@
import network from "src/agent/network"
import user from "src/agent/user"
import pool from "src/agent/pool"
import {keys, directory} from "src/system"
import cmd from "src/agent/cmd"
import {keys, directory, cmd} from "src/system"
import {loadAppData} from "src/app/state"
import {modal} from "src/partials/state"

View File

@ -7,6 +7,7 @@
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {routing} from "src/system"
import {watch} from "src/agent/db"
import user from "src/agent/user"
@ -15,7 +16,7 @@
let q = ""
let search
const knownRelays = watch("relays", t => t.all())
const knownRelays = watch(routing.relays, () => routing.relays.all())
$: joined = new Set(pluck("url", $relays))
$: search = fuzzy(

View File

@ -1,13 +1,13 @@
<script lang="ts">
import {batch, timedelta} from "src/util/misc"
import {displayRelay, normalizeRelayUrl, getAvgQuality} from "src/util/nostr"
import {normalizeRelayUrl, getAvgQuality} from "src/util/nostr"
import Content from "src/partials/Content.svelte"
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 {relays} from "src/agent/db"
import {routing} from "src/system"
export let url
@ -18,7 +18,7 @@
$: rating = getAvgQuality("review/relay", reviews)
const relay = relays.get(url) || {url}
const relay = routing.getRelay(url)
const tabs = ["reviews", "notes"]
const setActiveTab = tab => {
activeTab = tab
@ -28,7 +28,7 @@
reviews = reviews.concat(chunk)
})
document.title = displayRelay(relay)
document.title = routing.displayRelay(relay)
</script>
<Content>
@ -41,8 +41,8 @@
<Rating inert value={rating} />
</div>
{/if}
{#if relay.description}
<p>{relay.description}</p>
{#if relay.meta.description}
<p>{relay.meta.description}</p>
{/if}
<Tabs borderClass="border-gray-6" {tabs} {activeTab} {setActiveTab} />
{#if activeTab === "reviews"}

View File

@ -6,8 +6,8 @@
import Heading from "src/partials/Heading.svelte"
import Compose from "src/partials/Compose.svelte"
import Rating from "src/partials/Rating.svelte"
import {cmd} from "src/system"
import user from "src/agent/user"
import cmd from "src/agent/cmd"
export let url

View File

@ -8,9 +8,9 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {cmd} from "src/system"
import user from "src/agent/user"
import {getUserWriteRelays} from "src/agent/relays"
import cmd from "src/agent/cmd"
import {routes} from "src/app/state"
import {publishWithToast} from "src/app/state"

193
src/system/cmd.ts Normal file
View File

@ -0,0 +1,193 @@
import {last, pick, uniqBy} from "ramda"
import {get} from "svelte/store"
import {doPipe} from "hurdak/lib/hurdak"
import {Tags, roomAttrs, findRoot, findReply} from "src/util/nostr"
import {parseContent} from "src/util/notes"
import {getRelayForPersonHint, getRelayForEventHint} from "src/agent/relays"
export default ({keys, sync, pool, displayPubkey}) => {
const authenticate = (url, challenge) =>
new PublishableEvent(22242, {
tags: [
["challenge", challenge],
["relay", url],
],
})
const updateUser = updates => new PublishableEvent(0, {content: JSON.stringify(updates)})
const setRelays = newRelays =>
new PublishableEvent(10002, {
tags: newRelays.map(r => {
const t = ["r", r.url]
if (!r.write) {
t.push("read")
}
return t
}),
})
const setAppData = (d, content) => new PublishableEvent(30078, {content, tags: [["d", d]]})
const setPetnames = petnames => new PublishableEvent(3, {tags: petnames})
const setMutes = mutes => new PublishableEvent(10000, {tags: mutes})
const createList = list => new PublishableEvent(30001, {tags: list})
const createRoom = room =>
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
const updateRoom = ({id, ...room}) =>
new PublishableEvent(41, {content: JSON.stringify(pick(roomAttrs, room)), tags: [["e", id]]})
const createChatMessage = (roomId, content, url) =>
new PublishableEvent(42, {content, tags: [["e", roomId, url, "root"]]})
const createDirectMessage = (pubkey, content) =>
new PublishableEvent(4, {content, tags: [["p", pubkey]]})
const createNote = (content, tags = []) =>
new PublishableEvent(1, {content, tags: uniqTags(tagsFromContent(content, tags))})
const createReaction = (note, content) =>
new PublishableEvent(7, {content, tags: getReplyTags(note)})
const createReply = (note, content, tags = []) =>
new PublishableEvent(1, {
content,
tags: doPipe(tags, [
tags => tags.concat(getReplyTags(note, true)),
tags => tagsFromContent(content, tags),
uniqTags,
]),
})
const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => {
const tags = [
["relays", ...relays],
["amount", amount.toString()],
["lnurl", lnurl],
["p", pubkey],
]
if (eventId) {
tags.push(["e", eventId])
}
return new PublishableEvent(9734, {content, tags, tagClient: false})
}
const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id])})
const createLabel = payload => new PublishableEvent(1985, payload)
// Utils
const tagsFromContent = (content, tags) => {
const seen = new Set(Tags.wrap(tags).values().all())
for (const {type, value} of parseContent({content})) {
if (type === "topic") {
tags = tags.concat([["t", value]])
seen.add(value)
}
if (type.match(/nostr:(note|nevent)/) && !seen.has(value.id)) {
tags = tags.concat([["e", value.id, value.relays?.[0] || "", "mention"]])
seen.add(value.id)
}
if (type.match(/nostr:(nprofile|npub)/) && !seen.has(value.pubkey)) {
tags = tags.concat([mention(value.pubkey)])
seen.add(value.pubkey)
}
}
return tags
}
const mention = pubkey => {
const name = displayPubkey(pubkey)
const hint = getRelayForPersonHint(pubkey)
return ["p", pubkey, hint?.url || "", name]
}
const getReplyTags = (n, inherit = false) => {
const extra = inherit
? Tags.from(n)
.type("e")
.reject(t => last(t) === "mention")
.all()
.map(t => t.slice(0, 3))
: []
const pHint = getRelayForPersonHint(n.pubkey)
const eHint = getRelayForEventHint(n) || pHint
const reply = ["e", n.id, eHint?.url || "", "reply"]
const root = doPipe(findRoot(n) || findReply(n) || reply, [
t => (t.length < 3 ? t.concat(eHint?.url || "") : t),
t => t.slice(0, 3).concat("root"),
])
return [mention(n.pubkey), root, ...extra, reply]
}
const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
class PublishableEvent {
event: Record<string, any>
constructor(kind, {content = "", tags = [], tagClient = true}) {
const pubkey = get(keys.pubkey)
const createdAt = Math.round(new Date().valueOf() / 1000)
if (tagClient) {
tags = tags.filter(t => t[0] !== "client").concat([["client", "coracle"]])
}
this.event = {kind, content, tags, pubkey, created_at: createdAt}
}
getSignedEvent() {
try {
return keys.sign(this.event)
} catch (e) {
console.log(this.event)
throw e
}
}
async publish(relays, onProgress = null, verb = "EVENT") {
const event = await this.getSignedEvent()
// return console.log(event)
const promise = pool.publish({relays, event, onProgress, verb})
// Copy the event since loki mutates it to add metadata
sync.processEvents({...event, seen_on: []})
return [event, promise]
}
}
return {
mention,
authenticate,
updateUser,
setRelays,
setAppData,
setPetnames,
setMutes,
createList,
createRoom,
updateRoom,
createChatMessage,
createDirectMessage,
createNote,
createReaction,
createReply,
requestZap,
deleteEvent,
createLabel,
PublishableEvent,
}
}

View File

@ -1,4 +1,4 @@
import {sortBy, nth, prop, inc} from 'ramda'
import {sortBy, nth, inc} from "ramda"
import {fuzzy} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {Table, watch} from "src/agent/db"

View File

@ -7,22 +7,29 @@ import initDirectory from "src/system/directory"
import initNip05 from "src/system/nip05"
import initNip57 from "src/system/nip57"
import initContent from "src/system/content"
import initRouting from "src/system/routing"
import initCmd from "src/system/cmd"
import {getUserWriteRelays} from "src/agent/relays"
import {default as agentSync} from "src/agent/sync"
import pool from "src/agent/pool"
import cmd from "src/agent/cmd"
import user from "src/agent/user"
// Hacks for circular deps
const getCmd = () => cmd
// ===========================================================
// Initialize various components
const sync = initSync({keys})
const social = initSocial({keys, sync, cmd, getUserWriteRelays})
const settings = initSettings({keys, sync, cmd, getUserWriteRelays})
const social = initSocial({keys, sync, getCmd, getUserWriteRelays})
const settings = initSettings({keys, sync, getCmd, getUserWriteRelays})
const directory = initDirectory({sync, sortByGraph: social.sortByGraph})
const nip05 = initNip05({sync, sortByGraph: social.sortByGraph})
const nip57 = initNip57({sync, sortByGraph: social.sortByGraph})
const routing = initContent({sync, sortByGraph: social.sortByGraph})
const routing = initRouting({sync, sortByGraph: social.sortByGraph})
const content = initContent({sync})
const cmd = initCmd({keys, sync, pool, displayPubkey: directory.displayPubkey})
// Glue stuff together
@ -32,9 +39,17 @@ settings.store.subscribe($settings => {
pool.Config.multiplextrUrl = $settings.multiplextrUrl
})
cmd.ext.displayPubkey = directory.displayPubkey
user.ext.cmd = cmd
pool.ext.routing = routing
// ===========================================================
// Initialization
const initialize = () => {
routing.initialize()
}
// ===========================================================
// Exports
export {keys, sync, social, settings, directory, nip05, nip57, routing, content}
export {keys, sync, social, settings, directory, nip05, nip57, routing, content, cmd, initialize}

View File

@ -1,27 +1,25 @@
import {sortBy, nth, prop, inc} from 'ramda'
import {fuzzy} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {sortBy, last, inc} from "ramda"
import {fuzzy, tryJson, now, fetchJson} from "src/util/misc"
import {warn} from "src/util/logger"
import {normalizeRelayUrl, isShareableRelay} from "src/util/nostr"
import {DUFFLEPUD_URL, DEFAULT_RELAYS, FORCE_RELAYS} from "src/system/env"
import {Table, watch} from "src/agent/db"
export default ({sync, sortByGraph) => {
export default ({sync, sortByGraph}) => {
const relays = new Table("routing/relays", "url", {sort: sortBy(e => -e.count)})
const relaySelections = new Table("routing/relaySelections", "pubkey", {sort: sortByGraph})
const processTopics = e => {
const tagTopics = Tags.from(e).topics()
const contentTopics = Array.from(e.content.toLowerCase().matchAll(/#(\w{2,100})/g)).map(nth(1))
for (const name of tagTopics.concat(contentTopics)) {
const topic = topics.get(name)
topics.patch({name, count: inc(topic?.count || 0)})
}
}
const addRelay = url => {
const relay = relays.get(url)
relays.patch({url, count: inc(relay?.count || 0)})
relays.patch({
url,
count: inc(relay?.count || 0),
first_seen: relay?.first_seen || now(),
meta: {
last_checked: 0,
},
})
}
const addPolicies = ({pubkey, created_at}, policies) => {
@ -44,40 +42,70 @@ export default ({sync, sortByGraph) => {
})
sync.addHandler(3, e => {
addPolicies(e, tryJson(() => {
Object.entries(JSON.parse(e.content || ""))
.filter(([url]) => isShareableRelay(url))
.map(([url, conditions]) => {
const write = ![false, '!'].includes(conditions.write)
const read = ![false, '!'].includes(conditions.read)
addPolicies(
e,
tryJson(() => {
Object.entries(JSON.parse(e.content || ""))
.filter(([url]) => isShareableRelay(url))
.map(([url, conditions]) => {
// @ts-ignore
const write = ![false, "!"].includes(conditions.write)
// @ts-ignore
const read = ![false, "!"].includes(conditions.read)
return {url: normalizeRelayUrl(url), write, read}
})
}))
return {url: normalizeRelayUrl(url), write, read}
})
})
)
})
sync.addHandler(10002, e => {
addPolicies(e, e.tags.map(([_, url, mode]) => {
const write = mode === 'write'
const read = mode === 'read'
addPolicies(
e,
e.tags.map(([_, url, mode]) => {
const write = mode === "write"
const read = mode === "read"
if (!write && !read) {
warn(`Encountered unknown relay mode: ${mode}`)
}
if (!write && !read) {
warn(`Encountered unknown relay mode: ${mode}`)
}
return {url: normalizeRelayUrl(url), write, read}
}))
return {url: normalizeRelayUrl(url), write, read}
})
)
})
const getRelay = url => relays.get(url) || {url}
const getRelayMeta = url => relays.get(url)?.meta || {}
const searchRelays = watch(relays, () => fuzzy(relays.all(), {keys: ["url"]}))
const displayRelay = ({url}) => last(url.split("://"))
const initialize = async () => {
// Throw some hardcoded defaults in there
DEFAULT_RELAYS.forEach(addRelay)
// Load relays from nostr.watch via dufflepud
if (FORCE_RELAYS.length === 0) {
try {
const json = await fetchJson(DUFFLEPUD_URL + "/relay")
json.relays.filter(isShareableRelay).forEach(addRelay)
} catch (e) {
warn("Failed to fetch relays list", e)
}
}
}
return {
relays,
relaySelections,
getRelay,
getRelayMeta,
searchRelays,
displayRelay,
initialize,
}
}

View File

@ -3,7 +3,7 @@ import {synced, getter} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {DUFFLEPUD_URL, MULTIPLEXTR_URL} from "src/system/env"
export default ({keys, sync, cmd, getUserWriteRelays}) => {
export default ({keys, sync, getCmd, getUserWriteRelays}) => {
const store = synced("settings/store", {
lastUpdated: 0,
relayLimit: 20,
@ -38,7 +38,7 @@ export default ({keys, sync, cmd, getUserWriteRelays}) => {
const d = "coracle/settings/v1"
const v = await keys.encryptJson(settings)
return cmd.setAppData(d, v).publish(getUserWriteRelays())
return getCmd().setAppData(d, v).publish(getUserWriteRelays())
}
}

View File

@ -5,7 +5,7 @@ import {now} from "src/util/misc"
import {Tags} from "src/util/nostr"
import {Table} from "src/agent/db"
export default ({keys, sync, cmd, getUserWriteRelays}) => {
export default ({keys, sync, getCmd, getUserWriteRelays}) => {
// Don't delete the user's own info or those of direct follows
const sortByGraph = xs => {
const pubkey = keys.getPubkey()
@ -78,7 +78,7 @@ export default ({keys, sync, cmd, getUserWriteRelays}) => {
const updatePetnames = async $petnames => {
if (get(keys.canSign)) {
await cmd.setPetnames($petnames).publish(getUserWriteRelays())
await getCmd().setPetnames($petnames).publish(getUserWriteRelays())
} else {
graph.patch({
pubkey: getUserKey(),

View File

@ -116,8 +116,6 @@ export const findRoot = e => prop("root", findReplyAndRoot(e))
export const findRootId = e => findRoot(e)?.[1]
export const displayRelay = ({url}) => last(url.split("://"))
export const isLike = content => ["", "+", "🤙", "👍", "❤️", "😎", "🏅"].includes(content)
export const isNotification = (e, pubkey) => {

View File

@ -172,9 +172,11 @@ export const truncateContent = (content, {showEntire, maxLength, showMedia = fal
const truncateAt = maxLength * 0.6
content.every((part, i) => {
const isText = [TOPIC, TEXT].includes(part.type) || (part.type === LINK && !part.value.isMedia)
const isMedia = part.type === INVOICE || part.type.startsWith("nostr:") || part.value.isMedia
const textLength = part.value.url?.length || part.value.length
const isText =
[NOSTR_NPUB, NOSTR_NPROFILE, NOSTR_NADDR, TOPIC, TEXT].includes(part.type) ||
(part.type === LINK && !part.value.isMedia)
if (isText) {
length += textLength