mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 16:31:04 +00:00
Move the rest over to engine
This commit is contained in:
parent
d5630e7ab7
commit
8de6980802
178
src/engine/components/Builder.ts
Normal file
178
src/engine/components/Builder.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import {last, pick, uniqBy} from "ramda"
|
||||||
|
import {doPipe, first} from "hurdak/lib/hurdak"
|
||||||
|
import {Tags, channelAttrs, findRoot, findReply} from "src/util/nostr"
|
||||||
|
import {parseContent} from "src/util/notes"
|
||||||
|
|
||||||
|
const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
|
||||||
|
|
||||||
|
const buildEvent = (kind, {content = "", tags = [], tagClient = true}) => {
|
||||||
|
if (tagClient) {
|
||||||
|
tags = tags.filter(t => t[0] !== "client").concat([["client", "coracle"]])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {kind, content, tags}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeActions({Routing, Directory}) {
|
||||||
|
const getEventHint = event => first(Routing.getEventHints(1, event)) || ""
|
||||||
|
|
||||||
|
const getPubkeyHint = pubkey => first(Routing.getPubkeyHints(1, pubkey)) || ""
|
||||||
|
|
||||||
|
const getPubkeyPetname = pubkey => {
|
||||||
|
const profile = Directory.getProfile(pubkey)
|
||||||
|
|
||||||
|
return profile ? Directory.displayProfile(profile) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const mention = pubkey => {
|
||||||
|
const hint = getPubkeyHint(pubkey)
|
||||||
|
const petname = getPubkeyPetname(pubkey)
|
||||||
|
|
||||||
|
return ["p", pubkey, hint, petname]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 eHint = getEventHint(n)
|
||||||
|
const reply = ["e", n.id, eHint, "reply"]
|
||||||
|
const root = doPipe(findRoot(n) || findReply(n) || reply, [
|
||||||
|
t => (t.length < 3 ? t.concat(eHint) : t),
|
||||||
|
t => t.slice(0, 3).concat("root"),
|
||||||
|
])
|
||||||
|
|
||||||
|
return [mention(n.pubkey), root, ...extra, reply]
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticate = (url, challenge) =>
|
||||||
|
buildEvent(22242, {
|
||||||
|
tags: [
|
||||||
|
["challenge", challenge],
|
||||||
|
["relay", url],
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const setProfile = profile => buildEvent(0, {content: JSON.stringify(profile)})
|
||||||
|
|
||||||
|
const setRelays = relays =>
|
||||||
|
buildEvent(10002, {
|
||||||
|
tags: relays.map(r => {
|
||||||
|
const t = ["r", r.url]
|
||||||
|
|
||||||
|
if (!r.write) {
|
||||||
|
t.push("read")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const setAppData = (d, content = "") => buildEvent(30078, {content, tags: [["d", d]]})
|
||||||
|
|
||||||
|
const setPetnames = petnames => buildEvent(3, {tags: petnames})
|
||||||
|
|
||||||
|
const setMutes = mutes => buildEvent(10000, {tags: mutes})
|
||||||
|
|
||||||
|
const createList = list => buildEvent(30001, {tags: list})
|
||||||
|
|
||||||
|
const createChannel = channel =>
|
||||||
|
buildEvent(40, {content: JSON.stringify(pick(channelAttrs, channel))})
|
||||||
|
|
||||||
|
const updateChannel = ({id, ...channel}) =>
|
||||||
|
buildEvent(41, {
|
||||||
|
content: JSON.stringify(pick(channelAttrs, channel)),
|
||||||
|
tags: [["e", id]],
|
||||||
|
})
|
||||||
|
|
||||||
|
const createChatMessage = (channelId, content, url) =>
|
||||||
|
buildEvent(42, {content, tags: [["e", channelId, url, "root"]]})
|
||||||
|
|
||||||
|
const createDirectMessage = (pubkey, content) => buildEvent(4, {content, tags: [["p", pubkey]]})
|
||||||
|
|
||||||
|
const createNote = (content, tags = []) =>
|
||||||
|
buildEvent(1, {content, tags: uniqTags(tagsFromContent(content, tags))})
|
||||||
|
|
||||||
|
const createReaction = (note, content) => buildEvent(7, {content, tags: getReplyTags(note)})
|
||||||
|
|
||||||
|
const createReply = (note, content, tags = []) =>
|
||||||
|
buildEvent(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 buildEvent(9734, {content, tags, tagClient: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteEvents = ids => buildEvent(5, {tags: ids.map(id => ["e", id])})
|
||||||
|
|
||||||
|
const deleteNaddrs = naddrs => buildEvent(5, {tags: naddrs.map(naddr => ["a", naddr])})
|
||||||
|
|
||||||
|
const createLabel = payload => buildEvent(1985, payload)
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagsFromContent,
|
||||||
|
getReplyTags,
|
||||||
|
authenticate,
|
||||||
|
setProfile,
|
||||||
|
setRelays,
|
||||||
|
setAppData,
|
||||||
|
setPetnames,
|
||||||
|
setMutes,
|
||||||
|
createList,
|
||||||
|
createChannel,
|
||||||
|
updateChannel,
|
||||||
|
createChatMessage,
|
||||||
|
createDirectMessage,
|
||||||
|
createNote,
|
||||||
|
createReaction,
|
||||||
|
createReply,
|
||||||
|
requestZap,
|
||||||
|
deleteEvents,
|
||||||
|
deleteNaddrs,
|
||||||
|
createLabel,
|
||||||
|
}
|
||||||
|
}
|
@ -1,58 +1,22 @@
|
|||||||
import {nip19} from "nostr-tools"
|
import {nip19} from "nostr-tools"
|
||||||
import {ellipsize} from "hurdak/lib/hurdak"
|
import {ellipsize} from "hurdak/lib/hurdak"
|
||||||
import {tryJson, now, fuzzy} from "src/util/misc"
|
import {tryJson, now, fuzzy} from "src/util/misc"
|
||||||
import type {Table} from "src/util/loki"
|
|
||||||
import type {Readable} from "svelte/store"
|
|
||||||
import type {Sync} from "src/system/components/Sync"
|
|
||||||
import type {Profile} from "src/system/types"
|
import type {Profile} from "src/system/types"
|
||||||
|
import {collection, derived} from "../util/store"
|
||||||
|
|
||||||
export class Directory {
|
export function contributeState() {
|
||||||
profiles: Table<Profile>
|
const profiles = collection<Profile>()
|
||||||
searchProfiles: Readable<(q: string) => Record<string, any>[]>
|
|
||||||
constructor(sync: Sync) {
|
|
||||||
this.profiles = sync.table("directory/profiles", "pubkey", {
|
|
||||||
max: 5000,
|
|
||||||
sort: sync.sortByPubkeyWhitelist,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.searchProfiles = this.profiles.watch(() => {
|
return {profiles}
|
||||||
return fuzzy(this.getNamedProfiles(), {
|
}
|
||||||
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
|
||||||
threshold: 0.3,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sync.addHandler(0, e => {
|
export function contributeSelectors({Directory}) {
|
||||||
tryJson(() => {
|
const getProfile = (pubkey: string): Profile => Directory.profiles.getKey(pubkey) || {pubkey}
|
||||||
const kind0 = JSON.parse(e.content)
|
|
||||||
const profile = this.profiles.get(e.pubkey)
|
|
||||||
|
|
||||||
if (e.created_at < profile?.created_at) {
|
const getNamedProfiles = () =>
|
||||||
return
|
Directory.profiles.all().filter(p => p.name || p.nip05 || p.display_name)
|
||||||
}
|
|
||||||
|
|
||||||
this.profiles.patch({
|
const displayProfile = ({display_name, name, pubkey}: Profile) => {
|
||||||
...kind0,
|
|
||||||
pubkey: e.pubkey,
|
|
||||||
created_at: e.created_at,
|
|
||||||
updated_at: now(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getProfile = (pubkey: string): Profile => this.profiles.get(pubkey) || {pubkey}
|
|
||||||
|
|
||||||
getNamedProfiles = () =>
|
|
||||||
this.profiles.all({
|
|
||||||
$or: [
|
|
||||||
{name: {$type: "string"}},
|
|
||||||
{nip05: {$type: "string"}},
|
|
||||||
{display_name: {$type: "string"}},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
displayProfile = ({display_name, name, pubkey}: Profile) => {
|
|
||||||
if (display_name) {
|
if (display_name) {
|
||||||
return ellipsize(display_name, 60)
|
return ellipsize(display_name, 60)
|
||||||
}
|
}
|
||||||
@ -70,5 +34,34 @@ export class Directory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
displayPubkey = pubkey => this.displayProfile(this.getProfile(pubkey))
|
const displayPubkey = pubkey => displayProfile(getProfile(pubkey))
|
||||||
|
|
||||||
|
const searchProfiles = derived(Directory.profiles, $profiles => {
|
||||||
|
return fuzzy(getNamedProfiles(), {
|
||||||
|
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
||||||
|
threshold: 0.3,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return {getProfile, getNamedProfiles, displayProfile, displayPubkey, searchProfiles}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize({Events, Directory}) {
|
||||||
|
Events.addHandler(0, e => {
|
||||||
|
tryJson(() => {
|
||||||
|
const kind0 = JSON.parse(e.content)
|
||||||
|
const profile = Directory.profiles.getKey(e.pubkey)
|
||||||
|
|
||||||
|
if (e.created_at < profile?.created_at) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.profiles.mergeKey(e.pubkey, {
|
||||||
|
...kind0,
|
||||||
|
pubkey: e.pubkey,
|
||||||
|
created_at: e.created_at,
|
||||||
|
updated_at: now(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
137
src/engine/components/Meta.ts
Normal file
137
src/engine/components/Meta.ts
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import {Socket} from "paravel"
|
||||||
|
import {now} from "src/util/misc"
|
||||||
|
import {switcher} from "hurdak/lib/hurdak"
|
||||||
|
import type {RelayStat} from "src/system/types"
|
||||||
|
import {collection} from "../util/store"
|
||||||
|
|
||||||
|
export function contributeState() {
|
||||||
|
const relayStats = collection<RelayStat>()
|
||||||
|
|
||||||
|
return {relayStats}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeActions({Meta, Network}) {
|
||||||
|
const getRelayStats = url => Meta.relayStats.getKey(url)
|
||||||
|
|
||||||
|
const getRelayQuality = url => {
|
||||||
|
const stats = getRelayStats(url)
|
||||||
|
|
||||||
|
if (!stats) {
|
||||||
|
return [0.5, "Not Connected"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.error) {
|
||||||
|
return [
|
||||||
|
0,
|
||||||
|
switcher(stats.error, {
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
unauthorized: "Logging in",
|
||||||
|
forbidden: "Failed to log in",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {timeouts, total_subs: totalSubs, eose_timer: eoseTimer, eose_count: eoseCount} = stats
|
||||||
|
const timeoutRate = timeouts > 0 ? timeouts / totalSubs : null
|
||||||
|
const eoseQuality = eoseCount > 0 ? Math.max(1, 500 / (eoseTimer / eoseCount)) : null
|
||||||
|
|
||||||
|
if (timeoutRate && timeoutRate > 0.5) {
|
||||||
|
return [1 - timeoutRate, "Slow connection"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eoseQuality && eoseQuality < 0.7) {
|
||||||
|
return [eoseQuality, "Slow connection"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eoseQuality) {
|
||||||
|
return [eoseQuality, "Connected"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Network.pool.get(url).status === Socket.STATUS.READY) {
|
||||||
|
return [1, "Connected"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [0.5, "Not Connected"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {getRelayStats, getRelayQuality}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize({Network, Meta}) {
|
||||||
|
Network.pool.on("open", ({url}) => {
|
||||||
|
Meta.relayStats.mergeKey(url, {last_opened: now(), last_activity: now()})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.pool.on("close", ({url}) => {
|
||||||
|
Meta.relayStats.mergeKey(url, {last_closed: now(), last_activity: now()})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.pool.on("error:set", (url, error) => {
|
||||||
|
Meta.relayStats.mergeKey(url, {error})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.pool.on("error:clear", url => {
|
||||||
|
Meta.relayStats.mergeKey(url, {error: null})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("publish", urls => {
|
||||||
|
for (const url of urls) {
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_publish: now(),
|
||||||
|
last_activity: now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("sub:open", urls => {
|
||||||
|
for (const url of urls) {
|
||||||
|
const stats = Meta.getRelayStats(url)
|
||||||
|
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_sub: now(),
|
||||||
|
last_activity: now(),
|
||||||
|
total_subs: (stats?.total_subs || 0) + 1,
|
||||||
|
active_subs: (stats?.active_subs || 0) + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("sub:close", urls => {
|
||||||
|
for (const url of urls) {
|
||||||
|
const stats = Meta.getRelayStats(url)
|
||||||
|
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_activity: now(),
|
||||||
|
active_subs: stats ? stats.active_subs - 1 : 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("event", ({url}) => {
|
||||||
|
const stats = Meta.getRelayStats(url)
|
||||||
|
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_activity: now(),
|
||||||
|
events_count: (stats.events_count || 0) + 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("eose", (url, ms) => {
|
||||||
|
const stats = Meta.getRelayStats(url)
|
||||||
|
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_activity: now(),
|
||||||
|
eose_count: (stats.eose_count || 0) + 1,
|
||||||
|
eose_timer: (stats.eose_timer || 0) + ms,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Network.emitter.on("timeout", (url, ms) => {
|
||||||
|
const stats = Meta.getRelayStats(url)
|
||||||
|
|
||||||
|
Meta.relayStats.mergeKey(url, {
|
||||||
|
last_activity: now(),
|
||||||
|
timeouts: (stats.timeouts || 0) + 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
332
src/engine/components/Network.ts
Normal file
332
src/engine/components/Network.ts
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import {EventEmitter} from "events"
|
||||||
|
import {verifySignature, matchFilters} from "nostr-tools"
|
||||||
|
import {Pool, Plex, Relays, Executor, Socket} from "paravel"
|
||||||
|
import {ensurePlural} from "hurdak/lib/hurdak"
|
||||||
|
import {union, difference} from "src/util/misc"
|
||||||
|
import {warn, error, log} from "src/util/logger"
|
||||||
|
import {normalizeRelayUrl} from "src/util/nostr"
|
||||||
|
import type {Event, Filter} from "src/system/types"
|
||||||
|
|
||||||
|
export type SubscribeOpts = {
|
||||||
|
relays: string[]
|
||||||
|
filter: Filter[] | Filter
|
||||||
|
onEvent?: (event: Event) => void
|
||||||
|
onEose?: (url: string) => void
|
||||||
|
shouldProcess?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUrls = relays => {
|
||||||
|
if (relays.length === 0) {
|
||||||
|
error(`Attempted to connect to zero urls`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = new Set(relays.map(normalizeRelayUrl))
|
||||||
|
|
||||||
|
if (urls.size !== relays.length) {
|
||||||
|
warn(`Attempted to connect to non-unique relays`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeState() {
|
||||||
|
const authHandler = null
|
||||||
|
const emitter = new EventEmitter()
|
||||||
|
const pool = new Pool()
|
||||||
|
|
||||||
|
return {authHandler, emitter, pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeActions({Network, User, Events, Routing, Env}) {
|
||||||
|
const getExecutor = (urls, {bypassBoot = false} = {}) => {
|
||||||
|
if (Env.FORCE_RELAYS?.length > 0) {
|
||||||
|
urls = Env.FORCE_RELAYS
|
||||||
|
}
|
||||||
|
|
||||||
|
let target
|
||||||
|
|
||||||
|
const muxUrl = User.getSetting("multiplextr_url")
|
||||||
|
|
||||||
|
// Try to use our multiplexer, but if it fails to connect fall back to relays. If
|
||||||
|
// we're only connecting to a single relay, just do it directly, unless we already
|
||||||
|
// have a connection to the multiplexer open, in which case we're probably doing
|
||||||
|
// AUTH with a single relay.
|
||||||
|
if (muxUrl && (urls.length > 1 || Network.pool.has(muxUrl))) {
|
||||||
|
const socket = Network.pool.get(muxUrl)
|
||||||
|
|
||||||
|
if (!socket.error) {
|
||||||
|
target = new Plex(urls, socket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
target = new Relays(urls.map(url => Network.pool.get(url)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const executor = new Executor(target)
|
||||||
|
|
||||||
|
executor.handleAuth({
|
||||||
|
onAuth(url, challenge) {
|
||||||
|
Network.emitter.emit("error:set", url, "unauthorized")
|
||||||
|
|
||||||
|
return Network.authHandler?.(url, challenge)
|
||||||
|
},
|
||||||
|
onOk(url, id, ok, message) {
|
||||||
|
Network.emitter.emit("error:clear", url, ok ? null : "forbidden")
|
||||||
|
|
||||||
|
// Once we get a good auth response don't wait to send stuff to the relay
|
||||||
|
if (ok) {
|
||||||
|
Network.pool.get(url)
|
||||||
|
Network.pool.booted = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Eagerly connect and handle AUTH
|
||||||
|
executor.target.sockets.forEach(socket => {
|
||||||
|
const {limitation} = Routing.getRelayInfo(socket.url)
|
||||||
|
const waitForBoot = limitation?.payment_required || limitation?.auth_required
|
||||||
|
|
||||||
|
// This happens automatically, but kick it off anyway
|
||||||
|
socket.connect()
|
||||||
|
|
||||||
|
// Delay REQ/EVENT until AUTH flow happens. Highly hacky, as this relies on
|
||||||
|
// overriding the `shouldDeferWork` property of the socket. We do it this way
|
||||||
|
// so that we're not blocking sending to all the other public relays
|
||||||
|
if (!bypassBoot && waitForBoot && socket.status === Socket.STATUS.PENDING) {
|
||||||
|
socket.shouldDeferWork = () => {
|
||||||
|
return socket.booted && socket.status !== Socket.STATUS.READY
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => Object.assign(socket, {booted: true}), 2000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return executor
|
||||||
|
}
|
||||||
|
|
||||||
|
const publish = ({relays, event, onProgress, timeout = 3000, verb = "EVENT"}) => {
|
||||||
|
const urls = getUrls(relays)
|
||||||
|
const executor = getExecutor(urls, {bypassBoot: verb === "AUTH"})
|
||||||
|
|
||||||
|
Network.emitter.emit("publish", urls)
|
||||||
|
|
||||||
|
log(`Publishing to ${urls.length} relays`, event, urls)
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const timeouts = new Set()
|
||||||
|
const succeeded = new Set()
|
||||||
|
const failed = new Set()
|
||||||
|
|
||||||
|
const getProgress = () => {
|
||||||
|
const completed = union(timeouts, succeeded, failed)
|
||||||
|
const pending = difference(urls, completed)
|
||||||
|
|
||||||
|
return {succeeded, failed, timeouts, completed, pending}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attemptToResolve = () => {
|
||||||
|
const progress = getProgress()
|
||||||
|
|
||||||
|
if (progress.pending.size === 0) {
|
||||||
|
log(`Finished publishing to ${urls.length} relays`, event, progress)
|
||||||
|
resolve(progress)
|
||||||
|
sub.unsubscribe()
|
||||||
|
executor.target.cleanup()
|
||||||
|
} else if (onProgress) {
|
||||||
|
onProgress(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const url of urls) {
|
||||||
|
if (!succeeded.has(url) && !failed.has(url)) {
|
||||||
|
timeouts.add(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptToResolve()
|
||||||
|
}, timeout)
|
||||||
|
|
||||||
|
const sub = executor.publish(event, {
|
||||||
|
verb,
|
||||||
|
onOk: url => {
|
||||||
|
succeeded.add(url)
|
||||||
|
timeouts.delete(url)
|
||||||
|
failed.delete(url)
|
||||||
|
attemptToResolve()
|
||||||
|
},
|
||||||
|
onError: url => {
|
||||||
|
failed.add(url)
|
||||||
|
timeouts.delete(url)
|
||||||
|
attemptToResolve()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Report progress to start
|
||||||
|
attemptToResolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribe = ({relays, filter, onEvent, onEose, shouldProcess = true}: SubscribeOpts) => {
|
||||||
|
const urls = getUrls(relays)
|
||||||
|
const executor = getExecutor(urls)
|
||||||
|
const filters = ensurePlural(filter)
|
||||||
|
const now = Date.now()
|
||||||
|
const seen = new Map()
|
||||||
|
const eose = new Set()
|
||||||
|
|
||||||
|
log(`Starting subscription with ${relays.length} relays`, {filters, relays})
|
||||||
|
|
||||||
|
Network.emitter.emit("sub:open", urls)
|
||||||
|
|
||||||
|
const sub = executor.subscribe(filters, {
|
||||||
|
onEvent: (url, event) => {
|
||||||
|
const seen_on = seen.get(event.id)
|
||||||
|
|
||||||
|
if (seen_on) {
|
||||||
|
if (!seen_on.includes(url)) {
|
||||||
|
seen_on.push(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(event, {
|
||||||
|
seen_on: [url],
|
||||||
|
content: event.content || "",
|
||||||
|
})
|
||||||
|
|
||||||
|
seen.set(event.id, event.seen_on)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!verifySignature(event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFilters(filters, event)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Network.emitter.emit("event", {url, event})
|
||||||
|
|
||||||
|
if (shouldProcess) {
|
||||||
|
Events.queue.push(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
onEvent(event)
|
||||||
|
},
|
||||||
|
onEose: url => {
|
||||||
|
onEose?.(url)
|
||||||
|
|
||||||
|
// Keep track of relay timing stats, but only for the first eose we get
|
||||||
|
if (!eose.has(url)) {
|
||||||
|
Network.emitter.emit("eose", url, Date.now() - now)
|
||||||
|
}
|
||||||
|
|
||||||
|
eose.add(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
let closed = false
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (closed) {
|
||||||
|
error("Closed subscription twice", filters)
|
||||||
|
} else {
|
||||||
|
log(`Closing subscription`, filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub.unsubscribe()
|
||||||
|
executor.target.cleanup()
|
||||||
|
|
||||||
|
Network.emitter.emit("sub:close", urls)
|
||||||
|
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = ({
|
||||||
|
relays,
|
||||||
|
filter,
|
||||||
|
onEvent = null,
|
||||||
|
shouldProcess = true,
|
||||||
|
timeout = 5000,
|
||||||
|
}: {
|
||||||
|
relays: string[]
|
||||||
|
filter: Filter | Filter[]
|
||||||
|
onEvent?: (event: Event) => void
|
||||||
|
shouldProcess?: boolean
|
||||||
|
timeout?: number
|
||||||
|
}) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let completed = false
|
||||||
|
const eose = new Set()
|
||||||
|
const allEvents = []
|
||||||
|
|
||||||
|
const attemptToComplete = force => {
|
||||||
|
// If we've already unsubscribed we're good
|
||||||
|
if (completed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDone = eose.size === relays.length
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
relays.forEach(url => {
|
||||||
|
if (!eose.has(url)) {
|
||||||
|
Network.pool.emit("timeout", url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDone || force) {
|
||||||
|
unsubscribe()
|
||||||
|
resolve(allEvents)
|
||||||
|
completed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a relay takes too long, give up
|
||||||
|
setTimeout(() => attemptToComplete(true), timeout)
|
||||||
|
|
||||||
|
const unsubscribe = subscribe({
|
||||||
|
relays,
|
||||||
|
filter,
|
||||||
|
shouldProcess,
|
||||||
|
onEvent: event => {
|
||||||
|
onEvent?.(event)
|
||||||
|
allEvents.push(event)
|
||||||
|
},
|
||||||
|
onEose: url => {
|
||||||
|
eose.add(url)
|
||||||
|
attemptToComplete(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}) as Promise<Event[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = async filter => {
|
||||||
|
const filters = ensurePlural(filter)
|
||||||
|
const executor = getExecutor(Env.COUNT_RELAYS)
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const sub = executor.count(filters, {
|
||||||
|
onCount: (url, {count}) => resolve(count),
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(0)
|
||||||
|
sub.unsubscribe()
|
||||||
|
executor.target.cleanup()
|
||||||
|
}, 3000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {subscribe, publish, load, count}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import {fetchJson, now, tryFunc, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
import {fetchJson, now, tryFunc, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
||||||
import {invoiceAmount} from "src/util/lightning"
|
import {invoiceAmount} from "src/util/lightning"
|
||||||
import {Tags} from "src/util/nostr"
|
import {Tags} from "src/util/nostr"
|
||||||
import type {Table} from "src/util/loki"
|
|
||||||
import type {Sync} from "src/system/components/Sync"
|
|
||||||
import type {Zapper} from "src/system/types"
|
import type {Zapper} from "src/system/types"
|
||||||
|
import {collection} from "../util/store"
|
||||||
|
|
||||||
const getLnUrl = address => {
|
const getLnUrl = address => {
|
||||||
// Try to parse it as a lud06 LNURL
|
// Try to parse it as a lud06 LNURL
|
||||||
@ -21,52 +20,15 @@ const getLnUrl = address => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Nip57 {
|
export function contributeState() {
|
||||||
zappers: Table<Zapper>
|
const zappers = collection<Zapper>()
|
||||||
constructor(sync: Sync) {
|
|
||||||
this.zappers = sync.table("niip57/zappers", "pubkey", {
|
|
||||||
max: 5000,
|
|
||||||
sort: sync.sortByPubkeyWhitelist,
|
|
||||||
})
|
|
||||||
|
|
||||||
sync.addHandler(0, e => {
|
return {zappers}
|
||||||
tryJson(async () => {
|
}
|
||||||
const kind0 = JSON.parse(e.content)
|
|
||||||
const zapper = this.zappers.get(e.pubkey)
|
|
||||||
const address = (kind0.lud16 || kind0.lud06 || "").toLowerCase()
|
|
||||||
|
|
||||||
if (!address || e.created_at < zapper?.created_at) {
|
export function contributeActions({Nip57}) {
|
||||||
return
|
const processZaps = (zaps, pubkey) => {
|
||||||
}
|
const zapper = Nip57.zappers.getKey(pubkey)
|
||||||
|
|
||||||
const url = getLnUrl(address)
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await tryFunc(() => fetchJson(url), true)
|
|
||||||
|
|
||||||
if (!result?.allowsNostr || !result?.nostrPubkey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zappers.patch({
|
|
||||||
pubkey: e.pubkey,
|
|
||||||
lnurl: hexToBech32("lnurl", url),
|
|
||||||
callback: result.callback,
|
|
||||||
minSendable: result.minSendable,
|
|
||||||
maxSendable: result.maxSendable,
|
|
||||||
nostrPubkey: result.nostrPubkey,
|
|
||||||
created_at: e.created_at,
|
|
||||||
updated_at: now(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
processZaps = (zaps, pubkey) => {
|
|
||||||
const zapper = this.zappers.get(pubkey)
|
|
||||||
|
|
||||||
if (!zapper) {
|
if (!zapper) {
|
||||||
return []
|
return []
|
||||||
@ -113,4 +75,43 @@ export class Nip57 {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {processZaps}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize({Events, Nip57}) {
|
||||||
|
Events.addHandler(0, e => {
|
||||||
|
tryJson(async () => {
|
||||||
|
const kind0 = JSON.parse(e.content)
|
||||||
|
const zapper = Nip57.zappers.getKey(e.pubkey)
|
||||||
|
const address = (kind0.lud16 || kind0.lud06 || "").toLowerCase()
|
||||||
|
|
||||||
|
if (!address || e.created_at < zapper?.created_at) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = getLnUrl(address)
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tryFunc(() => fetchJson(url), true)
|
||||||
|
|
||||||
|
if (!result?.allowsNostr || !result?.nostrPubkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Nip57.zappers.setKey(e.pubkey, {
|
||||||
|
pubkey: e.pubkey,
|
||||||
|
lnurl: hexToBech32("lnurl", url),
|
||||||
|
callback: result.callback,
|
||||||
|
minSendable: result.minSendable,
|
||||||
|
maxSendable: result.maxSendable,
|
||||||
|
nostrPubkey: result.nostrPubkey,
|
||||||
|
created_at: e.created_at,
|
||||||
|
updated_at: now(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
79
src/engine/components/PubkeyLoader.ts
Normal file
79
src/engine/components/PubkeyLoader.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import {without, uniq} from "ramda"
|
||||||
|
import {chunk} from "hurdak/lib/hurdak"
|
||||||
|
import {personKinds, appDataKeys} from "src/util/nostr"
|
||||||
|
import {now, timedelta} from "src/util/misc"
|
||||||
|
import type {Filter} from "src/system/types"
|
||||||
|
|
||||||
|
export type LoadPeopleOpts = {
|
||||||
|
relays?: string[]
|
||||||
|
kinds?: number[]
|
||||||
|
force?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeActions({Directory, Routing, User, Network}) {
|
||||||
|
const attemptedPubkeys = new Set()
|
||||||
|
|
||||||
|
const getStalePubkeys = pubkeys => {
|
||||||
|
const stale = new Set()
|
||||||
|
const since = now() - timedelta(3, "hours")
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
if (stale.has(pubkey) || attemptedPubkeys.has(pubkey)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptedPubkeys.add(pubkey)
|
||||||
|
|
||||||
|
if (Directory.profiles.getKey(pubkey)?.updated_at || 0 > since) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stale.add(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stale
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPubkeys = async (
|
||||||
|
rawPubkeys,
|
||||||
|
{relays, force, kinds = personKinds}: LoadPeopleOpts = {}
|
||||||
|
) => {
|
||||||
|
const pubkeys = force ? uniq(rawPubkeys) : getStalePubkeys(rawPubkeys)
|
||||||
|
|
||||||
|
const getChunkRelays = chunk => {
|
||||||
|
if (relays?.length > 0) {
|
||||||
|
return relays
|
||||||
|
}
|
||||||
|
|
||||||
|
return Routing.mergeHints(
|
||||||
|
User.getSetting("relay_limit"),
|
||||||
|
chunk.map(pubkey => Routing.getPubkeyHints(3, pubkey))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChunkFilter = chunk => {
|
||||||
|
const filter = [] as Filter[]
|
||||||
|
|
||||||
|
filter.push({kinds: without([30078], kinds), authors: chunk})
|
||||||
|
|
||||||
|
// Add a separate filter for app data so we're not pulling down other people's stuff,
|
||||||
|
// or obsolete events of our own.
|
||||||
|
if (kinds.includes(30078)) {
|
||||||
|
filter.push({kinds: [30078], authors: chunk, "#d": appDataKeys})
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
chunk(256, pubkeys).map(async chunk => {
|
||||||
|
await Network.load({
|
||||||
|
relays: getChunkRelays(chunk),
|
||||||
|
filter: getChunkFilter(chunk),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {loadPubkeys}
|
||||||
|
}
|
@ -1,61 +1,25 @@
|
|||||||
import {ensurePlural} from "hurdak/lib/hurdak"
|
import {ensurePlural} from "hurdak/lib/hurdak"
|
||||||
import {now} from "src/util/misc"
|
import {now} from "src/util/misc"
|
||||||
import {Tags} from "src/util/nostr"
|
import {Tags} from "src/util/nostr"
|
||||||
import type {Table} from "src/util/loki"
|
|
||||||
import type {Sync} from "src/system/components/Sync"
|
|
||||||
import type {GraphEntry} from "src/system/types"
|
import type {GraphEntry} from "src/system/types"
|
||||||
|
import {collection} from "../util/store"
|
||||||
|
|
||||||
export class Social {
|
export function contributeState() {
|
||||||
sync: Sync
|
const graph = collection<GraphEntry>()
|
||||||
graph: Table<GraphEntry>
|
|
||||||
constructor(sync) {
|
|
||||||
this.sync = sync
|
|
||||||
|
|
||||||
this.graph = sync.table("social/graph", "pubkey", {
|
return {graph}
|
||||||
max: 5000,
|
}
|
||||||
sort: sync.sortByPubkeyWhitelist,
|
|
||||||
})
|
|
||||||
|
|
||||||
sync.addHandler(3, e => {
|
export function contributeActions({Social}) {
|
||||||
const entry = this.graph.get(e.pubkey)
|
const getPetnames = pubkey => Social.graph.getKey(pubkey)?.petnames || []
|
||||||
|
|
||||||
if (e.created_at < entry?.petnames_updated_at) {
|
const getMutedTags = pubkey => Social.graph.getKey(pubkey)?.mutes || []
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.graph.patch({
|
const getFollowsSet = pubkeys => {
|
||||||
pubkey: e.pubkey,
|
|
||||||
updated_at: now(),
|
|
||||||
petnames_updated_at: e.created_at,
|
|
||||||
petnames: Tags.from(e).type("p").all(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
sync.addHandler(10000, e => {
|
|
||||||
const entry = this.graph.get(e.pubkey)
|
|
||||||
|
|
||||||
if (e.created_at < entry?.mutes_updated_at) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.graph.patch({
|
|
||||||
pubkey: e.pubkey,
|
|
||||||
updated_at: now(),
|
|
||||||
mutes_updated_at: e.created_at,
|
|
||||||
mutes: Tags.from(e).type("p").all(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getPetnames = pubkey => this.graph.get(pubkey)?.petnames || []
|
|
||||||
|
|
||||||
getMutedTags = pubkey => this.graph.get(pubkey)?.mutes || []
|
|
||||||
|
|
||||||
getFollowsSet = pubkeys => {
|
|
||||||
const follows = new Set()
|
const follows = new Set()
|
||||||
|
|
||||||
for (const pubkey of ensurePlural(pubkeys)) {
|
for (const pubkey of ensurePlural(pubkeys)) {
|
||||||
for (const tag of this.getPetnames(pubkey)) {
|
for (const tag of getPetnames(pubkey)) {
|
||||||
follows.add(tag[1])
|
follows.add(tag[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,11 +27,11 @@ export class Social {
|
|||||||
return follows
|
return follows
|
||||||
}
|
}
|
||||||
|
|
||||||
getMutesSet = pubkeys => {
|
const getMutesSet = pubkeys => {
|
||||||
const mutes = new Set()
|
const mutes = new Set()
|
||||||
|
|
||||||
for (const pubkey of ensurePlural(pubkeys)) {
|
for (const pubkey of ensurePlural(pubkeys)) {
|
||||||
for (const tag of this.getMutedTags(pubkey)) {
|
for (const tag of getMutedTags(pubkey)) {
|
||||||
mutes.add(tag[1])
|
mutes.add(tag[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,15 +39,15 @@ export class Social {
|
|||||||
return mutes
|
return mutes
|
||||||
}
|
}
|
||||||
|
|
||||||
getFollows = pubkeys => Array.from(this.getFollowsSet(pubkeys))
|
const getFollows = pubkeys => Array.from(getFollowsSet(pubkeys))
|
||||||
|
|
||||||
getMutes = pubkeys => Array.from(this.getMutesSet(pubkeys))
|
const getMutes = pubkeys => Array.from(getMutesSet(pubkeys))
|
||||||
|
|
||||||
getNetworkSet = (pubkeys, includeFollows = false) => {
|
const getNetworkSet = (pubkeys, includeFollows = false) => {
|
||||||
const follows = this.getFollowsSet(pubkeys)
|
const follows = getFollowsSet(pubkeys)
|
||||||
const network = includeFollows ? follows : new Set()
|
const network = includeFollows ? follows : new Set()
|
||||||
|
|
||||||
for (const pubkey of this.getFollows(follows)) {
|
for (const pubkey of getFollows(follows)) {
|
||||||
if (!follows.has(pubkey)) {
|
if (!follows.has(pubkey)) {
|
||||||
network.add(pubkey)
|
network.add(pubkey)
|
||||||
}
|
}
|
||||||
@ -92,9 +56,54 @@ export class Social {
|
|||||||
return network
|
return network
|
||||||
}
|
}
|
||||||
|
|
||||||
getNetwork = pubkeys => Array.from(this.getNetworkSet(pubkeys))
|
const getNetwork = pubkeys => Array.from(getNetworkSet(pubkeys))
|
||||||
|
|
||||||
isFollowing = (a, b) => this.getFollowsSet(a).has(b)
|
const isFollowing = (a, b) => getFollowsSet(a).has(b)
|
||||||
|
|
||||||
isIgnoring = (a, b) => this.getMutesSet(a).has(b)
|
const isIgnoring = (a, b) => getMutesSet(a).has(b)
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPetnames,
|
||||||
|
getMutedTags,
|
||||||
|
getFollowsSet,
|
||||||
|
getMutesSet,
|
||||||
|
getFollows,
|
||||||
|
getMutes,
|
||||||
|
getNetworkSet,
|
||||||
|
getNetwork,
|
||||||
|
isFollowing,
|
||||||
|
isIgnoring,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize({Events, Social}) {
|
||||||
|
Events.addHandler(3, e => {
|
||||||
|
const entry = Social.graph.getKey(e.pubkey)
|
||||||
|
|
||||||
|
if (e.created_at < entry?.petnames_updated_at) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Social.graph.mergeKey(e.pubkey, {
|
||||||
|
pubkey: e.pubkey,
|
||||||
|
updated_at: now(),
|
||||||
|
petnames_updated_at: e.created_at,
|
||||||
|
petnames: Tags.from(e).type("p").all(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Events.addHandler(10000, e => {
|
||||||
|
const entry = Social.graph.getKey(e.pubkey)
|
||||||
|
|
||||||
|
if (e.created_at < entry?.mutes_updated_at) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Social.graph.mergeKey(e.pubkey, {
|
||||||
|
pubkey: e.pubkey,
|
||||||
|
updated_at: now(),
|
||||||
|
mutes_updated_at: e.created_at,
|
||||||
|
mutes: Tags.from(e).type("p").all(),
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
288
src/engine/components/User.ts
Normal file
288
src/engine/components/User.ts
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
import {getEventHash} from "nostr-tools"
|
||||||
|
import {when, uniq, pluck, without, fromPairs, whereEq, find, slice, assoc, reject} from "ramda"
|
||||||
|
import {doPipe} from "hurdak/lib/hurdak"
|
||||||
|
import {now} from "src/util/misc"
|
||||||
|
import {Tags, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
||||||
|
import {collection} from "../util/store"
|
||||||
|
|
||||||
|
export function contributeState({Env}) {
|
||||||
|
const settings = collection<any>({
|
||||||
|
last_updated: 0,
|
||||||
|
relay_limit: 10,
|
||||||
|
default_zap: 21,
|
||||||
|
show_media: true,
|
||||||
|
report_analytics: true,
|
||||||
|
dufflepud_url: Env.DUFFLEPUD_URL,
|
||||||
|
multiplextr_url: Env.MULTIPLEXTR_URL,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {settings}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contributeActions({
|
||||||
|
Keys,
|
||||||
|
Directory,
|
||||||
|
Events,
|
||||||
|
Network,
|
||||||
|
Crypt,
|
||||||
|
Builder,
|
||||||
|
Social,
|
||||||
|
Routing,
|
||||||
|
User,
|
||||||
|
Chat,
|
||||||
|
Content,
|
||||||
|
}) {
|
||||||
|
const getPubkey = () => Keys.pubkey.get()
|
||||||
|
|
||||||
|
const getProfile = () => Directory.getProfile(getPubkey())
|
||||||
|
|
||||||
|
const isUserEvent = id => Events.cache.getKey(id)?.pubkey === getPubkey()
|
||||||
|
|
||||||
|
const getStateKey = () => (Keys.canSign.get() ? getPubkey() : "anonymous")
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
|
||||||
|
const prepEvent = async rawEvent => {
|
||||||
|
return doPipe(rawEvent, [
|
||||||
|
assoc("created_at", now()),
|
||||||
|
assoc("pubkey", getPubkey()),
|
||||||
|
e => ({...e, id: getEventHash(e)}),
|
||||||
|
Keys.sign,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const publish = async (event, relays = null, onProgress = null, verb = "EVENT") => {
|
||||||
|
if (!event.sig) {
|
||||||
|
event = await prepEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relays) {
|
||||||
|
relays = getRelayUrls("write")
|
||||||
|
}
|
||||||
|
|
||||||
|
// return console.log(event)
|
||||||
|
|
||||||
|
const promise = Network.publish({event, relays, onProgress, verb})
|
||||||
|
|
||||||
|
Events.queue.push(event)
|
||||||
|
|
||||||
|
return [event, promise]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
|
||||||
|
const getSetting = k => User.settings.getKey(k)
|
||||||
|
|
||||||
|
const dufflepud = path => `${getSetting("dufflepud_url")}/${path}`
|
||||||
|
|
||||||
|
const setSettings = async settings => {
|
||||||
|
User.settings.update($settings => ({...$settings, ...settings}))
|
||||||
|
|
||||||
|
if (Keys.canSign.get()) {
|
||||||
|
const d = "coracle/settings/v1"
|
||||||
|
const v = await Crypt.encryptJson(settings)
|
||||||
|
|
||||||
|
return publish(Builder.setAppData(d, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAppData = async (key, content) => {
|
||||||
|
if (Keys.canSign.get()) {
|
||||||
|
const d = `coracle/${key}`
|
||||||
|
const v = await Crypt.encryptJson(content)
|
||||||
|
|
||||||
|
return publish(Builder.setAppData(d, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routing
|
||||||
|
|
||||||
|
const getRelays = (mode?: string) => Routing.getPubkeyRelays(getStateKey(), mode)
|
||||||
|
|
||||||
|
const getRelayUrls = (mode?: string) => Routing.getPubkeyRelayUrls(getStateKey(), mode)
|
||||||
|
|
||||||
|
const setRelays = relays => {
|
||||||
|
if (Keys.canSign.get()) {
|
||||||
|
return publish(Builder.setRelays(relays))
|
||||||
|
} else {
|
||||||
|
Routing.setPolicy({pubkey: getStateKey(), created_at: now()}, relays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addRelay = url => setRelays(getRelays().concat({url, read: true, write: true}))
|
||||||
|
|
||||||
|
const removeRelay = url => setRelays(reject(whereEq({url: normalizeRelayUrl(url)}), getRelays()))
|
||||||
|
|
||||||
|
const setRelayPolicy = (url, policy) =>
|
||||||
|
setRelays(getRelays().map(when(whereEq({url}), p => ({...p, ...policy}))))
|
||||||
|
|
||||||
|
// Social
|
||||||
|
|
||||||
|
const getPetnames = () => Social.getPetnames(getStateKey())
|
||||||
|
|
||||||
|
const getMutedTags = () => Social.getMutedTags(getStateKey())
|
||||||
|
|
||||||
|
const getFollowsSet = () => Social.getFollowsSet(getStateKey())
|
||||||
|
|
||||||
|
const getMutesSet = () => Social.getMutesSet(getStateKey())
|
||||||
|
|
||||||
|
const getFollows = () => Social.getFollows(getStateKey())
|
||||||
|
|
||||||
|
const getMutes = () => Social.getMutes(getStateKey())
|
||||||
|
|
||||||
|
const getNetworkSet = () => Social.getNetworkSet(getStateKey())
|
||||||
|
|
||||||
|
const getNetwork = () => Social.getNetwork(getStateKey())
|
||||||
|
|
||||||
|
const isFollowing = pubkey => Social.isFollowing(getStateKey(), pubkey)
|
||||||
|
|
||||||
|
const isIgnoring = pubkeyOrEventId => Social.isIgnoring(getStateKey(), pubkeyOrEventId)
|
||||||
|
|
||||||
|
const setProfile = $profile => publish(Builder.setProfile($profile))
|
||||||
|
|
||||||
|
const setPetnames = async $petnames => {
|
||||||
|
if (Keys.canSign.get()) {
|
||||||
|
await publish(Builder.setPetnames($petnames))
|
||||||
|
} else {
|
||||||
|
Social.graph.mergeKey(getStateKey(), {
|
||||||
|
pubkey: getStateKey(),
|
||||||
|
updated_at: now(),
|
||||||
|
petnames_updated_at: now(),
|
||||||
|
petnames: $petnames,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const follow = pubkey =>
|
||||||
|
setPetnames(
|
||||||
|
getPetnames()
|
||||||
|
.filter(t => t[1] !== pubkey)
|
||||||
|
.concat([Builder.mention(pubkey)])
|
||||||
|
)
|
||||||
|
|
||||||
|
const unfollow = pubkey => setPetnames(reject(t => t[1] === pubkey, getPetnames()))
|
||||||
|
|
||||||
|
const isMuted = e => {
|
||||||
|
const m = getMutesSet()
|
||||||
|
|
||||||
|
return find(t => m.has(t), [e.id, e.pubkey, findReplyId(e), findRootId(e)])
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyMutes = events => reject(isMuted, events)
|
||||||
|
|
||||||
|
const setMutes = async $mutes => {
|
||||||
|
if (Keys.canSign.get()) {
|
||||||
|
await publish(Builder.setMutes($mutes.map(slice(0, 2))))
|
||||||
|
} else {
|
||||||
|
Social.graph.mergeKey(getStateKey(), {
|
||||||
|
pubkey: getStateKey(),
|
||||||
|
updated_at: now(),
|
||||||
|
mutes_updated_at: now(),
|
||||||
|
mutes: $mutes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mute = (type, value) =>
|
||||||
|
setMutes(reject(t => t[1] === value, getMutes()).concat([[type, value]]))
|
||||||
|
|
||||||
|
const unmute = target => setMutes(reject(t => t[1] === target, getMutes()))
|
||||||
|
|
||||||
|
// Content
|
||||||
|
|
||||||
|
const getLists = (spec = null) => Content.getLists({...spec, pubkey: getStateKey()})
|
||||||
|
|
||||||
|
const putList = (name, params, relays) =>
|
||||||
|
publish(Builder.createList([["d", name]].concat(params).concat(relays)))
|
||||||
|
|
||||||
|
const removeList = naddr => publish(Builder.deleteNaddrs([naddr]))
|
||||||
|
|
||||||
|
// Chat
|
||||||
|
|
||||||
|
const setLastChecked = (channelId, timestamp) => {
|
||||||
|
const lastChecked = fromPairs(
|
||||||
|
Chat.channels.all({last_checked: {$type: "number"}}).map(r => [r.id, r.last_checked])
|
||||||
|
)
|
||||||
|
|
||||||
|
return setAppData("last_checked/v1", {...lastChecked, [channelId]: timestamp})
|
||||||
|
}
|
||||||
|
|
||||||
|
const joinChannel = channelId => {
|
||||||
|
const channelIds = uniq(pluck("id", Chat.channels.all({joined: true})).concat(channelId))
|
||||||
|
|
||||||
|
return setAppData("rooms_joined/v1", channelIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaveChannel = channelId => {
|
||||||
|
const channelIds = without([channelId], pluck("id", Chat.channels.all({joined: true})))
|
||||||
|
|
||||||
|
return setAppData("rooms_joined/v1", channelIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
getPubkey,
|
||||||
|
|
||||||
|
getProfile,
|
||||||
|
|
||||||
|
isUserEvent,
|
||||||
|
|
||||||
|
getStateKey,
|
||||||
|
|
||||||
|
getSetting,
|
||||||
|
|
||||||
|
dufflepud,
|
||||||
|
|
||||||
|
setSettings,
|
||||||
|
|
||||||
|
getRelays,
|
||||||
|
getRelayUrls,
|
||||||
|
setRelays,
|
||||||
|
addRelay,
|
||||||
|
removeRelay,
|
||||||
|
setRelayPolicy,
|
||||||
|
getPetnames,
|
||||||
|
getMutedTags,
|
||||||
|
getFollowsSet,
|
||||||
|
getMutesSet,
|
||||||
|
getFollows,
|
||||||
|
getMutes,
|
||||||
|
getNetworkSet,
|
||||||
|
getNetwork,
|
||||||
|
isFollowing,
|
||||||
|
isIgnoring,
|
||||||
|
setProfile,
|
||||||
|
setPetnames,
|
||||||
|
follow,
|
||||||
|
unfollow,
|
||||||
|
isMuted,
|
||||||
|
applyMutes,
|
||||||
|
setMutes,
|
||||||
|
mute,
|
||||||
|
unmute,
|
||||||
|
getLists,
|
||||||
|
putList,
|
||||||
|
removeList,
|
||||||
|
setLastChecked,
|
||||||
|
joinChannel,
|
||||||
|
leaveChannel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialize({Events, Crypt, User}) {
|
||||||
|
Events.addHandler(30078, async e => {
|
||||||
|
if (
|
||||||
|
Tags.from(e).getMeta("d") === "coracle/settings/v1" &&
|
||||||
|
e.created_at > User.settings.getKey("last_updated")
|
||||||
|
) {
|
||||||
|
const updates = await Crypt.decryptJson(e.content)
|
||||||
|
|
||||||
|
if (updates) {
|
||||||
|
User.settings.update($settings => ({
|
||||||
|
...$settings,
|
||||||
|
...updates,
|
||||||
|
last_updated: e.created_at,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -1,15 +1,25 @@
|
|||||||
import * as Alerts from "./components/Alerts"
|
import * as Alerts from "./components/Alerts"
|
||||||
|
import * as Builder from "./components/Builder"
|
||||||
import * as Chat from "./components/Chat"
|
import * as Chat from "./components/Chat"
|
||||||
import * as Content from "./components/Content"
|
import * as Content from "./components/Content"
|
||||||
import * as Crypt from "./components/Crypt"
|
import * as Crypt from "./components/Crypt"
|
||||||
|
import * as Directory from "./components/Directory"
|
||||||
import * as Events from "./components/Events"
|
import * as Events from "./components/Events"
|
||||||
import * as Keys from "./components/Keys"
|
import * as Keys from "./components/Keys"
|
||||||
|
import * as Meta from "./components/Meta"
|
||||||
|
import * as Network from "./components/Network"
|
||||||
import * as Nip05 from "./components/Nip05"
|
import * as Nip05 from "./components/Nip05"
|
||||||
|
import * as Nip57 from "./components/Nip57"
|
||||||
|
import * as PubkeyLoader from "./components/PubkeyLoader"
|
||||||
import * as Routing from "./components/Routing"
|
import * as Routing from "./components/Routing"
|
||||||
|
import * as Social from "./components/Social"
|
||||||
|
import * as User from "./components/User"
|
||||||
|
|
||||||
export const createEngine = (engine, components) => {
|
export const createEngine = (engine, components) => {
|
||||||
for (const component of components) {
|
const componentState = components.map(c => [c, c.contributeState?.(engine)])
|
||||||
engine[component.name] = component.contributeState?.()
|
|
||||||
|
for (const [component, state] of componentState) {
|
||||||
|
Object.assign(engine[component.name], state)
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentSelectors = components.map(c => [c, c.contributeSelectors?.(engine)])
|
const componentSelectors = components.map(c => [c, c.contributeSelectors?.(engine)])
|
||||||
@ -36,16 +46,21 @@ export const createDefaultEngine = Env => {
|
|||||||
{Env},
|
{Env},
|
||||||
{
|
{
|
||||||
Alerts,
|
Alerts,
|
||||||
|
Builder,
|
||||||
Chat,
|
Chat,
|
||||||
Content,
|
Content,
|
||||||
Crypt,
|
Crypt,
|
||||||
// Directory,
|
Directory,
|
||||||
Events,
|
Events,
|
||||||
Keys,
|
Keys,
|
||||||
|
Meta,
|
||||||
|
Network,
|
||||||
Nip05,
|
Nip05,
|
||||||
// Nip57,
|
Nip57,
|
||||||
|
PubkeyLoader,
|
||||||
Routing,
|
Routing,
|
||||||
// Social,
|
Social,
|
||||||
|
User,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -127,8 +127,8 @@ export const key = <T>(baseStore, k): Key<T> => {
|
|||||||
return keyStore
|
return keyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export const collection = <T>(): Collection<T> => {
|
export const collection = <T>(defaults = {}): Collection<T> => {
|
||||||
const baseStore = writable<Map<any, T>>(new Map())
|
const baseStore = writable<Map<any, T>>(new Map(Object.entries(defaults)))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...baseStore,
|
...baseStore,
|
||||||
|
@ -4,10 +4,19 @@ import {doPipe} from "hurdak/lib/hurdak"
|
|||||||
import {now, getter} from "src/util/misc"
|
import {now, getter} from "src/util/misc"
|
||||||
import {Tags, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
import {Tags, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
||||||
import type {System} from "src/system/System"
|
import type {System} from "src/system/System"
|
||||||
import type {UserSettings} from "src/system/types"
|
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import engine from "src/app/system"
|
import engine from "src/app/system"
|
||||||
|
|
||||||
|
export type UserSettings = {
|
||||||
|
last_updated: number
|
||||||
|
relay_limit: number
|
||||||
|
default_zap: number
|
||||||
|
show_media: boolean
|
||||||
|
report_analytics: boolean
|
||||||
|
dufflepud_url: string
|
||||||
|
multiplextr_url: string
|
||||||
|
}
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
keys: typeof engine.keys
|
keys: typeof engine.keys
|
||||||
crypt: typeof engine.crypt
|
crypt: typeof engine.crypt
|
||||||
|
@ -95,16 +95,6 @@ export type Profile = {
|
|||||||
display_name?: string
|
display_name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserSettings = {
|
|
||||||
last_updated: number
|
|
||||||
relay_limit: number
|
|
||||||
default_zap: number
|
|
||||||
show_media: boolean
|
|
||||||
report_analytics: boolean
|
|
||||||
dufflepud_url: string
|
|
||||||
multiplextr_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Channel = {
|
export type Channel = {
|
||||||
id: string
|
id: string
|
||||||
type: "public" | "private"
|
type: "public" | "private"
|
||||||
|
Loading…
Reference in New Issue
Block a user