mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 08:21:20 +00:00
Delete system
This commit is contained in:
parent
8de6980802
commit
1f3fee870a
@ -1,6 +1,6 @@
|
||||
import {Tags, isLike, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {collection, writable, derived} from "../util/store"
|
||||
import type {Event} from "src/system/types"
|
||||
import type {Event} from "src/engine/types"
|
||||
|
||||
export function contributeState() {
|
||||
const events = collection<Event>()
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {last, propEq, pick, uniq, pluck} from "ramda"
|
||||
import {tryJson, now, tryFunc} from "src/util/misc"
|
||||
import {Tags, channelAttrs} from "src/util/nostr"
|
||||
import type {Channel, Message} from "src/system/types"
|
||||
import type {Channel, Message} from "src/engine/types"
|
||||
import {collection, derived} from "../util/store"
|
||||
|
||||
const getHints = e => pluck("url", Tags.from(e).relays())
|
||||
|
@ -2,7 +2,7 @@ import {nip19} from "nostr-tools"
|
||||
import {always, nth, inc} from "ramda"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Topic, List} from "src/system/types"
|
||||
import type {Topic, List} from "src/engine/types"
|
||||
import {derived, collection} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ellipsize} from "hurdak/lib/hurdak"
|
||||
import {tryJson, now, fuzzy} from "src/util/misc"
|
||||
import type {Profile} from "src/system/types"
|
||||
import type {Profile} from "src/engine/types"
|
||||
import {collection, derived} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Socket} from "paravel"
|
||||
import {now} from "src/util/misc"
|
||||
import {switcher} from "hurdak/lib/hurdak"
|
||||
import type {RelayStat} from "src/system/types"
|
||||
import type {RelayStat} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -5,7 +5,7 @@ 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"
|
||||
import type {Event, Filter} from "src/engine/types"
|
||||
|
||||
export type SubscribeOpts = {
|
||||
relays: string[]
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {last} from "ramda"
|
||||
import {nip05} from "nostr-tools"
|
||||
import {tryFunc, now, tryJson} from "src/util/misc"
|
||||
import type {Handle} from "src/system/types"
|
||||
import type {Handle} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {fetchJson, now, tryFunc, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
||||
import {invoiceAmount} from "src/util/lightning"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Zapper} from "src/system/types"
|
||||
import type {Zapper} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
const getLnUrl = address => {
|
||||
|
@ -2,7 +2,7 @@ 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"
|
||||
import type {Filter} from "src/engine/types"
|
||||
|
||||
export type LoadPeopleOpts = {
|
||||
relays?: string[]
|
||||
|
@ -2,7 +2,7 @@ import {sortBy, pluck, uniq, nth, uniqBy, prop, last, inc} from "ramda"
|
||||
import {fuzzy, chain, tryJson, now, fetchJson} from "src/util/misc"
|
||||
import {warn} from "src/util/logger"
|
||||
import {normalizeRelayUrl, findReplyId, isShareableRelay, Tags} from "src/util/nostr"
|
||||
import type {Relay, RelayInfo, RelayPolicy} from "src/system/types"
|
||||
import type {Relay, RelayInfo, RelayPolicy} from "src/engine/types"
|
||||
import {derived, collection} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {ensurePlural} from "hurdak/lib/hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {GraphEntry} from "src/system/types"
|
||||
import type {GraphEntry} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
export function contributeState() {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type {Event} from "src/system/types"
|
||||
import type {Event} from "src/engine/types"
|
||||
import {pushToKey} from "src/util/misc"
|
||||
import {queue} from "../util/queue"
|
||||
import {collection} from "../util/store"
|
||||
|
@ -1,144 +0,0 @@
|
||||
import {Socket} from "paravel"
|
||||
import {now} from "src/util/misc"
|
||||
import {switcher} from "hurdak/lib/hurdak"
|
||||
import type {RelayStat} from "src/system/types"
|
||||
import type {Network} from "src/system/components/Network"
|
||||
|
||||
export type MetaOpts = {
|
||||
network: Network
|
||||
}
|
||||
|
||||
export class Meta {
|
||||
network: Network
|
||||
relayStats: Record<string, RelayStat>
|
||||
constructor({network}: MetaOpts) {
|
||||
this.network = network
|
||||
this.relayStats = {}
|
||||
|
||||
network.pool.on("open", ({url}) => {
|
||||
this.updateRelayStats(url, {last_opened: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
network.pool.on("close", ({url}) => {
|
||||
this.updateRelayStats(url, {last_closed: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
network.pool.on("error:set", (url, error) => {
|
||||
this.updateRelayStats(url, {error})
|
||||
})
|
||||
|
||||
network.pool.on("error:clear", url => {
|
||||
this.updateRelayStats(url, {error: null})
|
||||
})
|
||||
|
||||
network.on("publish", urls => {
|
||||
for (const url of urls) {
|
||||
this.updateRelayStats(url, {
|
||||
last_publish: now(),
|
||||
last_activity: now(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
network.on("sub:open", urls => {
|
||||
for (const url of urls) {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
last_sub: now(),
|
||||
last_activity: now(),
|
||||
total_subs: (stats?.total_subs || 0) + 1,
|
||||
active_subs: (stats?.active_subs || 0) + 1,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
network.on("sub:close", urls => {
|
||||
for (const url of urls) {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
last_activity: now(),
|
||||
active_subs: stats ? stats.active_subs - 1 : 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
network.on("event", ({url}) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
last_activity: now(),
|
||||
events_count: (stats.events_count || 0) + 1,
|
||||
})
|
||||
})
|
||||
|
||||
network.on("eose", (url, ms) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
last_activity: now(),
|
||||
eose_count: (stats.eose_count || 0) + 1,
|
||||
eose_timer: (stats.eose_timer || 0) + ms,
|
||||
})
|
||||
})
|
||||
|
||||
network.on("timeout", (url, ms) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
last_activity: now(),
|
||||
timeouts: (stats.timeouts || 0) + 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getRelayStats = url => this.relayStats[url]
|
||||
|
||||
updateRelayStats = (url, updates) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.relayStats[url] = {...stats, ...updates}
|
||||
}
|
||||
|
||||
getRelayQuality = url => {
|
||||
const stats = this.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 (this.network.pool.get(url).status === Socket.STATUS.READY) {
|
||||
return [1, "Connected"]
|
||||
}
|
||||
|
||||
return [0.5, "Not Connected"]
|
||||
}
|
||||
}
|
@ -1,334 +0,0 @@
|
||||
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, RelayInfo} from "src/system/types"
|
||||
|
||||
export type NetworkOpts = {
|
||||
getMultiplextrUrl: () => string | null
|
||||
processEvents: (events: Event[]) => void
|
||||
getRelayInfo: (url: string) => RelayInfo | null
|
||||
forceRelays: string[]
|
||||
countRelays: string[]
|
||||
}
|
||||
|
||||
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 class Network extends EventEmitter {
|
||||
authHandler?: (url: string, challenge: string) => void
|
||||
pool: Pool
|
||||
constructor(readonly opts: NetworkOpts) {
|
||||
super()
|
||||
|
||||
this.authHandler = null
|
||||
this.pool = new Pool()
|
||||
}
|
||||
getExecutor = (urls, {bypassBoot = false} = {}) => {
|
||||
if (this.opts.forceRelays.length > 0) {
|
||||
urls = this.opts.forceRelays
|
||||
}
|
||||
|
||||
let target
|
||||
|
||||
const muxUrl = this.opts.getMultiplextrUrl()
|
||||
|
||||
// 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 || this.pool.has(muxUrl))) {
|
||||
const socket = this.pool.get(muxUrl)
|
||||
|
||||
if (!socket.error) {
|
||||
target = new Plex(urls, socket)
|
||||
}
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
target = new Relays(urls.map(url => this.pool.get(url)))
|
||||
}
|
||||
|
||||
const executor = new Executor(target)
|
||||
|
||||
executor.handleAuth({
|
||||
onAuth(url, challenge) {
|
||||
this.emit("error:set", url, "unauthorized")
|
||||
|
||||
return this.authHandler?.(url, challenge)
|
||||
},
|
||||
onOk(url, id, ok, message) {
|
||||
this.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) {
|
||||
this.pool.get(url)
|
||||
this.pool.booted = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Eagerly connect and handle AUTH
|
||||
executor.target.sockets.forEach(socket => {
|
||||
const {limitation} = this.opts.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
|
||||
}
|
||||
publish = ({relays, event, onProgress, timeout = 3000, verb = "EVENT"}) => {
|
||||
const urls = getUrls(relays)
|
||||
const executor = this.getExecutor(urls, {bypassBoot: verb === "AUTH"})
|
||||
|
||||
this.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()
|
||||
})
|
||||
}
|
||||
subscribe = ({relays, filter, onEvent, onEose, shouldProcess = true}: SubscribeOpts) => {
|
||||
const urls = getUrls(relays)
|
||||
const executor = this.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})
|
||||
|
||||
this.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
|
||||
}
|
||||
|
||||
this.emit("event", {url, event})
|
||||
|
||||
if (shouldProcess) {
|
||||
this.opts.processEvents([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)) {
|
||||
this.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()
|
||||
|
||||
this.emit("sub:close", urls)
|
||||
|
||||
closed = true
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
this.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 = this.subscribe({
|
||||
relays,
|
||||
filter,
|
||||
shouldProcess,
|
||||
onEvent: event => {
|
||||
onEvent?.(event)
|
||||
allEvents.push(event)
|
||||
},
|
||||
onEose: url => {
|
||||
eose.add(url)
|
||||
attemptToComplete(false)
|
||||
},
|
||||
})
|
||||
}) as Promise<Event[]>
|
||||
}
|
||||
count = async filter => {
|
||||
const filters = ensurePlural(filter)
|
||||
const executor = this.getExecutor(this.opts.countRelays)
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = executor.count(filters, {
|
||||
onCount: (url, {count}) => resolve(count),
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(0)
|
||||
sub.unsubscribe()
|
||||
executor.target.cleanup()
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import {identity, sortBy, prop} from "ramda"
|
||||
import {ensurePlural, chunk} from "hurdak/lib/hurdak"
|
||||
import {Table} from "src/util/loki"
|
||||
import {sleep, synced} from "src/util/misc"
|
||||
import type {SystemEnv} from "src/system/System"
|
||||
|
||||
type SyncOpts = {
|
||||
getUserPubkey: () => null | string
|
||||
getPubkeyWhitelist: () => null | Set<string>
|
||||
}
|
||||
|
||||
export class Sync {
|
||||
ns: string
|
||||
env: SystemEnv
|
||||
stores = {}
|
||||
handlers = {}
|
||||
ANY_KIND: string
|
||||
getUserPubkey: SyncOpts["getUserPubkey"]
|
||||
getPubkeyWhitelist: SyncOpts["getPubkeyWhitelist"]
|
||||
|
||||
constructor(system, {getUserPubkey, getPubkeyWhitelist}: SyncOpts) {
|
||||
this.ns = system.ns
|
||||
this.env = system.env
|
||||
this.ANY_KIND = this.key(this.key("ANY_KIND"))
|
||||
this.getUserPubkey = getUserPubkey
|
||||
this.getPubkeyWhitelist = getPubkeyWhitelist
|
||||
}
|
||||
|
||||
key = key => `${this.ns}/${key}`
|
||||
|
||||
table = <T>(name, pk, opts = {}) => new Table<T>(this.key(name), pk, opts)
|
||||
|
||||
store = (name, defaultValue) => synced(this.key(name), defaultValue)
|
||||
|
||||
addHandler(kind, f) {
|
||||
this.handlers[kind] = this.handlers[kind] || []
|
||||
this.handlers[kind].push(f)
|
||||
}
|
||||
|
||||
sortByPubkeyWhitelist = xs => {
|
||||
const whitelist = this.getPubkeyWhitelist()
|
||||
|
||||
const sort = sortBy(
|
||||
whitelist ? x => (whitelist.has(x.pubkey) ? 0 : x.updated_at) : prop("updated_at")
|
||||
)
|
||||
|
||||
return sort(xs)
|
||||
}
|
||||
|
||||
async processEvents(events) {
|
||||
const chunks = chunk(100, ensurePlural(events).filter(identity))
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
for (const event of chunks[i]) {
|
||||
for (const handler of this.handlers[this.ANY_KIND] || []) {
|
||||
await handler(event)
|
||||
}
|
||||
|
||||
for (const handler of this.handlers[event.kind] || []) {
|
||||
await handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Don't lock up the ui when processing a lot of events
|
||||
if (i < chunks.length - 1) {
|
||||
await sleep(30)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
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, getter} from "src/util/misc"
|
||||
import {Tags, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
||||
import type {System} from "src/system/System"
|
||||
import type {Writable} from "svelte/store"
|
||||
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 {
|
||||
keys: typeof engine.keys
|
||||
crypt: typeof engine.crypt
|
||||
system: System
|
||||
canSign: () => boolean
|
||||
settings: Writable<UserSettings>
|
||||
getSettings: () => UserSettings
|
||||
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
this.keys = engine.keys
|
||||
this.crypt = engine.crypt
|
||||
this.canSign = getter(this.keys.canSign)
|
||||
|
||||
this.settings = system.sync.store("settings/settings", {
|
||||
last_updated: 0,
|
||||
relay_limit: 10,
|
||||
default_zap: 21,
|
||||
show_media: true,
|
||||
report_analytics: true,
|
||||
dufflepud_url: system.env.DUFFLEPUD_URL,
|
||||
multiplextr_url: system.env.MULTIPLEXTR_URL,
|
||||
})
|
||||
|
||||
this.getSettings = getter(this.settings)
|
||||
|
||||
system.sync.addHandler(30078, async e => {
|
||||
if (
|
||||
Tags.from(e).getMeta("d") === "coracle/settings/v1" &&
|
||||
e.created_at > this.getSetting("last_updated")
|
||||
) {
|
||||
const updates = await this.crypt.decryptJson(e.content)
|
||||
|
||||
if (updates) {
|
||||
this.settings.set({...this.getSettings(), ...updates, lastUpdated: e.created_at})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getPubkey = () => this.keys.pubkey.get()
|
||||
|
||||
getProfile = () => this.system.directory.getProfile(this.getPubkey())
|
||||
|
||||
isUserEvent = id => this.system.cache.events.get(id)?.pubkey === this.getPubkey()
|
||||
|
||||
getStateKey = () => (this.canSign() ? this.getPubkey() : "anonymous")
|
||||
|
||||
// Publish
|
||||
|
||||
async prepEvent(rawEvent) {
|
||||
return doPipe(rawEvent, [
|
||||
assoc("created_at", now()),
|
||||
assoc("pubkey", this.getPubkey()),
|
||||
e => ({...e, id: getEventHash(e)}),
|
||||
this.keys.sign,
|
||||
])
|
||||
}
|
||||
|
||||
async publish(event, relays = null, onProgress = null, verb = "EVENT") {
|
||||
if (!event.sig) {
|
||||
event = await this.prepEvent(event)
|
||||
}
|
||||
|
||||
if (!relays) {
|
||||
relays = this.getRelayUrls("write")
|
||||
}
|
||||
|
||||
// return console.log(event)
|
||||
|
||||
const promise = this.system.network.publish({event, relays, onProgress, verb})
|
||||
|
||||
this.system.sync.processEvents(event)
|
||||
|
||||
return [event, promise]
|
||||
}
|
||||
|
||||
// Settings
|
||||
|
||||
getSetting = k => this.getSettings()[k]
|
||||
|
||||
dufflepud = path => `${this.getSetting("dufflepud_url")}/${path}`
|
||||
|
||||
setSettings = async settings => {
|
||||
this.settings.update($settings => ({...$settings, ...settings}))
|
||||
|
||||
if (this.canSign()) {
|
||||
const d = "coracle/settings/v1"
|
||||
const v = await this.crypt.encryptJson(settings)
|
||||
|
||||
return this.publish(this.system.builder.setAppData(d, v))
|
||||
}
|
||||
}
|
||||
|
||||
async setAppData(key, content) {
|
||||
if (this.canSign()) {
|
||||
const d = `coracle/${key}`
|
||||
const v = await this.crypt.encryptJson(content)
|
||||
|
||||
return this.publish(this.system.builder.setAppData(d, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Routing
|
||||
|
||||
getRelays = (mode?: string) => this.system.routing.getPubkeyRelays(this.getStateKey(), mode)
|
||||
|
||||
getRelayUrls = (mode?: string) => this.system.routing.getPubkeyRelayUrls(this.getStateKey(), mode)
|
||||
|
||||
setRelays = relays => {
|
||||
if (this.canSign()) {
|
||||
return this.publish(this.system.builder.setRelays(relays))
|
||||
} else {
|
||||
this.system.routing.setPolicy({pubkey: this.getStateKey(), created_at: now()}, relays)
|
||||
}
|
||||
}
|
||||
|
||||
addRelay = url => this.setRelays(this.getRelays().concat({url, read: true, write: true}))
|
||||
|
||||
removeRelay = url =>
|
||||
this.setRelays(reject(whereEq({url: normalizeRelayUrl(url)}), this.getRelays()))
|
||||
|
||||
setRelayPolicy = (url, policy) =>
|
||||
this.setRelays(this.getRelays().map(when(whereEq({url}), p => ({...p, ...policy}))))
|
||||
|
||||
// Social
|
||||
|
||||
getPetnames = () => this.system.social.getPetnames(this.getStateKey())
|
||||
|
||||
getMutedTags = () => this.system.social.getMutedTags(this.getStateKey())
|
||||
|
||||
getFollowsSet = () => this.system.social.getFollowsSet(this.getStateKey())
|
||||
|
||||
getMutesSet = () => this.system.social.getMutesSet(this.getStateKey())
|
||||
|
||||
getFollows = () => this.system.social.getFollows(this.getStateKey())
|
||||
|
||||
getMutes = () => this.system.social.getMutes(this.getStateKey())
|
||||
|
||||
getNetworkSet = () => this.system.social.getNetworkSet(this.getStateKey())
|
||||
|
||||
getNetwork = () => this.system.social.getNetwork(this.getStateKey())
|
||||
|
||||
isFollowing = pubkey => this.system.social.isFollowing(this.getStateKey(), pubkey)
|
||||
|
||||
isIgnoring = pubkeyOrEventId => this.system.social.isIgnoring(this.getStateKey(), pubkeyOrEventId)
|
||||
|
||||
setProfile = $profile => this.publish(this.system.builder.setProfile($profile))
|
||||
|
||||
setPetnames = async $petnames => {
|
||||
if (this.canSign()) {
|
||||
await this.publish(this.system.builder.setPetnames($petnames))
|
||||
} else {
|
||||
this.system.social.graph.patch({
|
||||
pubkey: this.getStateKey(),
|
||||
updated_at: now(),
|
||||
petnames_updated_at: now(),
|
||||
petnames: $petnames,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
follow = pubkey =>
|
||||
this.setPetnames(
|
||||
this.getPetnames()
|
||||
.filter(t => t[1] !== pubkey)
|
||||
.concat([this.system.builder.mention(pubkey)])
|
||||
)
|
||||
|
||||
unfollow = pubkey => this.setPetnames(reject(t => t[1] === pubkey, this.getPetnames()))
|
||||
|
||||
isMuted = e => {
|
||||
const m = this.getMutesSet()
|
||||
|
||||
return find(t => m.has(t), [e.id, e.pubkey, findReplyId(e), findRootId(e)])
|
||||
}
|
||||
|
||||
applyMutes = events => reject(this.isMuted, events)
|
||||
|
||||
setMutes = async $mutes => {
|
||||
if (this.canSign()) {
|
||||
await this.publish(this.system.builder.setMutes($mutes.map(slice(0, 2))))
|
||||
} else {
|
||||
this.system.social.graph.patch({
|
||||
pubkey: this.getStateKey(),
|
||||
updated_at: now(),
|
||||
mutes_updated_at: now(),
|
||||
mutes: $mutes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mute = (type, value) =>
|
||||
this.setMutes(reject(t => t[1] === value, this.getMutes()).concat([[type, value]]))
|
||||
|
||||
unmute = target => this.setMutes(reject(t => t[1] === target, this.getMutes()))
|
||||
|
||||
// Content
|
||||
|
||||
getLists = (spec = null) => this.system.content.getLists({...spec, pubkey: this.getStateKey()})
|
||||
|
||||
putList = (name, params, relays) =>
|
||||
this.publish(this.system.builder.createList([["d", name]].concat(params).concat(relays)))
|
||||
|
||||
removeList = naddr => this.publish(this.system.builder.deleteNaddrs([naddr]))
|
||||
|
||||
// Chat
|
||||
|
||||
setLastChecked = (channelId, timestamp) => {
|
||||
const lastChecked = fromPairs(
|
||||
this.system.chat.channels
|
||||
.all({last_checked: {$type: "number"}})
|
||||
.map(r => [r.id, r.last_checked])
|
||||
)
|
||||
|
||||
return this.setAppData("last_checked/v1", {...lastChecked, [channelId]: timestamp})
|
||||
}
|
||||
|
||||
joinChannel = channelId => {
|
||||
const channelIds = uniq(
|
||||
pluck("id", this.system.chat.channels.all({joined: true})).concat(channelId)
|
||||
)
|
||||
|
||||
return this.setAppData("rooms_joined/v1", channelIds)
|
||||
}
|
||||
|
||||
leaveChannel = channelId => {
|
||||
const channelIds = without(
|
||||
[channelId],
|
||||
pluck("id", this.system.chat.channels.all({joined: true}))
|
||||
)
|
||||
|
||||
return this.setAppData("rooms_joined/v1", channelIds)
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
export type * from "src/system/types"
|
||||
export {Directory} from "src/system/stores/Directory"
|
||||
export {Routing} from "src/system/stores/Routing"
|
||||
export {Content} from "src/system/stores/Content"
|
||||
export {Nip57} from "src/system/stores/Nip57"
|
||||
export {Chat} from "src/system/stores/Chat"
|
||||
export {Nip05} from "src/system/stores/Nip05"
|
||||
export {Cache} from "src/system/stores/Cache"
|
||||
export {Social} from "src/system/stores/Social"
|
||||
export {Alerts} from "src/system/stores/Alerts"
|
||||
export {Meta} from "src/system/components/Meta"
|
||||
export {Network} from "src/system/components/Network"
|
||||
export {User} from "src/system/components/User"
|
||||
export {Sync} from "src/system/components/Sync"
|
||||
export {PubkeyLoader} from "src/system/util/PubkeyLoader"
|
||||
export {Builder} from "src/system/util/Builder"
|
||||
export {DefaultSystem} from "src/system/System"
|
@ -1,64 +0,0 @@
|
||||
import {sortBy} from "ramda"
|
||||
import type {Writable, Readable} from "svelte/store"
|
||||
import {Tags, isLike, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {derived} from "svelte/store"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
import type {Event} from "src/system/types"
|
||||
|
||||
export type AlertsOpts = {
|
||||
getUserPubkey: () => null | string
|
||||
isUserEvent: (e: Event) => boolean
|
||||
isMuted: (e: Event) => boolean
|
||||
}
|
||||
|
||||
export class Alerts {
|
||||
events: Table<Event>
|
||||
lastChecked: Writable<number>
|
||||
latestNotification: Writable<number>
|
||||
hasNewNotfications: Readable<boolean>
|
||||
constructor(sync: Sync, readonly opts: AlertsOpts) {
|
||||
this.events = sync.table("alerts/events", "id", {sort: sortBy(e => -e.created_at)})
|
||||
this.lastChecked = sync.store("alerts/lastChecked", 0)
|
||||
this.latestNotification = sync.store("alerts/latestNotification", 0)
|
||||
|
||||
this.hasNewNotfications = derived(
|
||||
[this.lastChecked, this.latestNotification],
|
||||
([$lastChecked, $latestNotification]) => $latestNotification > $lastChecked
|
||||
)
|
||||
|
||||
const isMention = e => Tags.from(e).pubkeys().includes(this.opts.getUserPubkey())
|
||||
const isDescendant = e => this.opts.isUserEvent(findRootId(e))
|
||||
const isReply = e => this.opts.isUserEvent(findReplyId(e))
|
||||
|
||||
const handleNotification = condition => e => {
|
||||
if (!this.opts.getUserPubkey() || e.pubkey === this.opts.getUserPubkey()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!condition(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.opts.isMuted(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.events.get(e.id)) {
|
||||
this.events.patch(e)
|
||||
}
|
||||
}
|
||||
|
||||
sync.addHandler(
|
||||
1,
|
||||
handleNotification(e => isMention(e) || isReply(e) || isDescendant(e))
|
||||
)
|
||||
|
||||
sync.addHandler(
|
||||
7,
|
||||
handleNotification(e => isLike(e.content) && isReply(e))
|
||||
)
|
||||
|
||||
sync.addHandler(9735, handleNotification(isReply))
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import type {Event as NostrToolsEvent} from "nostr-tools"
|
||||
import {sortBy} from "ramda"
|
||||
import type {Table} from "src/util/loki"
|
||||
|
||||
export type Event = NostrToolsEvent & {
|
||||
seen_on: string[]
|
||||
}
|
||||
|
||||
export class Cache {
|
||||
events: Table<Event>
|
||||
constructor(sync) {
|
||||
this.events = sync.table("cache/events", "id", {
|
||||
max: 5000,
|
||||
sort: events => {
|
||||
const sortByPubkeyWhitelist = e =>
|
||||
sync.getUserPubkey() === e.pubkey ? 0 : Number.MAX_SAFE_INTEGER - e.created_at
|
||||
|
||||
return sortBy(sortByPubkeyWhitelist, events)
|
||||
},
|
||||
})
|
||||
|
||||
sync.addHandler(sync.ANY_KIND, e => {
|
||||
if (e.pubkey === sync.getUserPubkey() && !this.events.get(e.id)) {
|
||||
this.events.patch(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,233 +0,0 @@
|
||||
import {last, sortBy, pick, uniq, pluck} from "ramda"
|
||||
import {get} from "svelte/store"
|
||||
import {tryJson, now, tryFunc} from "src/util/misc"
|
||||
import {Tags, channelAttrs} from "src/util/nostr"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Readable} from "svelte/store"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
import type {Channel, Message} from "src/system/types"
|
||||
import type engine from "src/app/system"
|
||||
|
||||
const getHints = e => pluck("url", Tags.from(e).relays())
|
||||
|
||||
const messageIsNew = ({last_checked, last_received, last_sent}: Channel) =>
|
||||
last_received > Math.max(last_sent || 0, last_checked || 0)
|
||||
|
||||
export type ChatOpts = {
|
||||
getCrypt: () => typeof engine.crypt
|
||||
}
|
||||
|
||||
export class Chat {
|
||||
channels: Table<Channel>
|
||||
messages: Table<Message>
|
||||
hasNewDirectMessages: Readable<boolean>
|
||||
hasNewChatMessages: Readable<boolean>
|
||||
constructor(sync: Sync, readonly opts: ChatOpts) {
|
||||
this.channels = sync.table("chat/channels", "id", {
|
||||
sort: sortBy(x => {
|
||||
if (x.joined || x.type === "private") return 0
|
||||
if (!x.name || x.name.match(/test/i)) return Infinity
|
||||
|
||||
return x.updated_at
|
||||
}),
|
||||
})
|
||||
|
||||
this.messages = sync.table("chat/messages", "id", {
|
||||
sort: xs => {
|
||||
const channelIds = new Set(
|
||||
pluck("id", this.channels.all({$or: [{joined: true}, {type: "private"}]}))
|
||||
)
|
||||
|
||||
return sortBy(x => (channelIds.has(x.id) ? 0 : x.created_at), xs)
|
||||
},
|
||||
})
|
||||
|
||||
this.hasNewDirectMessages = this.channels.watch(() => {
|
||||
const channels = this.channels.all({type: "private", last_sent: {$type: "number"}})
|
||||
|
||||
return channels.filter(e => messageIsNew(e)).length > 0
|
||||
})
|
||||
|
||||
this.hasNewChatMessages = this.channels.watch(() => {
|
||||
const channels = this.channels.all({type: "public", joined: true})
|
||||
|
||||
return channels.filter(e => messageIsNew(e)).length > 0
|
||||
})
|
||||
|
||||
sync.addHandler(40, e => {
|
||||
const channel = this.channels.get(e.id)
|
||||
|
||||
if (e.created_at < channel?.updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content)))
|
||||
|
||||
if (!content?.name) {
|
||||
return
|
||||
}
|
||||
|
||||
this.channels.patch({
|
||||
id: e.id,
|
||||
type: "public",
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
hints: getHints(e),
|
||||
...content,
|
||||
})
|
||||
})
|
||||
|
||||
sync.addHandler(41, e => {
|
||||
const channelId = Tags.from(e).getMeta("e")
|
||||
|
||||
if (!channelId) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = this.channels.get(channelId)
|
||||
|
||||
if (e.created_at < channel?.updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.pubkey !== channel?.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content)))
|
||||
|
||||
if (!content?.name) {
|
||||
return
|
||||
}
|
||||
|
||||
this.channels.patch({
|
||||
id: channelId,
|
||||
type: "public",
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
hints: getHints(e),
|
||||
...content,
|
||||
})
|
||||
})
|
||||
|
||||
sync.addHandler(30078, async e => {
|
||||
if (Tags.from(e).getMeta("d") === "coracle/last_checked/v1") {
|
||||
await tryJson(async () => {
|
||||
const payload = await this.opts.getCrypt().decryptJson(e.content)
|
||||
|
||||
for (const key of Object.keys(payload)) {
|
||||
// Backwards compat from when we used to prefix id/pubkey
|
||||
const channelId = last(key.split("/"))
|
||||
const channel = this.channels.get(channelId)
|
||||
const last_checked = Math.max(payload[channelId], channel?.last_checked || 0)
|
||||
|
||||
// A bunch of junk got added to this setting. Integer keys, settings, etc
|
||||
if (isNaN(last_checked) || last_checked < 1577836800) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.channels.patch({id: channelId, last_checked})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
sync.addHandler(30078, async e => {
|
||||
if (Tags.from(e).getMeta("d") === "coracle/rooms_joined/v1") {
|
||||
await tryJson(async () => {
|
||||
const channelIds = await this.opts.getCrypt().decryptJson(e.content)
|
||||
|
||||
// Just a bug from when I was building the feature, remove someday
|
||||
if (!Array.isArray(channelIds)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.channels.all({type: "public"}).forEach(channel => {
|
||||
if (channel.joined && !channelIds.includes(channel.id)) {
|
||||
this.channels.patch({id: channel.id, joined: false})
|
||||
} else if (!channel.joined && channelIds.includes(channel.id)) {
|
||||
this.channels.patch({id: channel.id, joined: true})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
sync.addHandler(4, async e => {
|
||||
if (!get(this.opts.getCrypt().keys.canSign)) {
|
||||
return
|
||||
}
|
||||
|
||||
const author = e.pubkey
|
||||
const recipient = Tags.from(e).type("p").values().first()
|
||||
|
||||
if (![author, recipient].includes(this.opts.getCrypt().keys.pubkey.get())) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.messages.get(e.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
await tryFunc(async () => {
|
||||
const other = this.opts.getCrypt().keys.pubkey.get() === author ? recipient : author
|
||||
|
||||
this.messages.patch({
|
||||
id: e.id,
|
||||
channel: other,
|
||||
pubkey: e.pubkey,
|
||||
created_at: e.created_at,
|
||||
content: await this.opts.getCrypt().decrypt(other, e.content),
|
||||
tags: e.tags,
|
||||
})
|
||||
|
||||
if (this.opts.getCrypt().keys.pubkey.get() === author) {
|
||||
const channel = this.channels.get(recipient)
|
||||
|
||||
this.channels.patch({
|
||||
id: recipient,
|
||||
type: "private",
|
||||
last_sent: e.created_at,
|
||||
hints: uniq(getHints(e).concat(channel?.hints || [])),
|
||||
})
|
||||
} else {
|
||||
const channel = this.channels.get(author)
|
||||
|
||||
this.channels.patch({
|
||||
id: author,
|
||||
type: "private",
|
||||
last_received: e.created_at,
|
||||
hints: uniq(getHints(e).concat(channel?.hints || [])),
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
sync.addHandler(42, e => {
|
||||
if (this.messages.get(e.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
const tags = Tags.from(e)
|
||||
const channelId = tags.getMeta("e")
|
||||
const channel = this.channels.get(channelId)
|
||||
const hints = uniq(pluck("url", tags.relays()).concat(channel?.hints || []))
|
||||
|
||||
this.messages.patch({
|
||||
id: e.id,
|
||||
channel: channelId,
|
||||
pubkey: e.pubkey,
|
||||
created_at: e.created_at,
|
||||
content: e.content,
|
||||
tags: e.tags,
|
||||
})
|
||||
|
||||
this.channels.patch({
|
||||
id: channelId,
|
||||
type: "public",
|
||||
last_sent: e.created_at,
|
||||
hints,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {sortBy, nth, inc} from "ramda"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
|
||||
export type Topic = {
|
||||
name: string
|
||||
count?: number
|
||||
}
|
||||
|
||||
export type List = {
|
||||
name: string
|
||||
naddr: string
|
||||
pubkey: string
|
||||
tags: string[][]
|
||||
updated_at: number
|
||||
created_at: number
|
||||
deleted_at?: number
|
||||
}
|
||||
|
||||
export class Content {
|
||||
topics: Table<Topic>
|
||||
lists: Table<List>
|
||||
searchTopics: Readable<(query: string) => Topic[]>
|
||||
constructor(sync: Sync) {
|
||||
this.topics = sync.table("content/topics", "name", {sort: sortBy(e => -e.count)})
|
||||
|
||||
this.lists = sync.table("content/lists", "naddr")
|
||||
|
||||
this.searchTopics = this.topics.watch(() =>
|
||||
fuzzy(this.topics.all(), {keys: ["name"], threshold: 0.3})
|
||||
)
|
||||
|
||||
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 = this.topics.get(name)
|
||||
|
||||
this.topics.patch({name, count: inc(topic?.count || 0)})
|
||||
}
|
||||
}
|
||||
|
||||
sync.addHandler(1, processTopics)
|
||||
sync.addHandler(42, processTopics)
|
||||
|
||||
sync.addHandler(30001, e => {
|
||||
const {pubkey, kind, created_at} = e
|
||||
const name = Tags.from(e).getMeta("d")
|
||||
const naddr = nip19.naddrEncode({identifier: name, pubkey, kind})
|
||||
const list = this.lists.get(naddr)
|
||||
|
||||
if (created_at < list?.updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
this.lists.patch({
|
||||
...list,
|
||||
name,
|
||||
naddr,
|
||||
pubkey,
|
||||
tags: e.tags,
|
||||
updated_at: created_at,
|
||||
created_at: list?.created_at || created_at,
|
||||
deleted_at: null,
|
||||
})
|
||||
})
|
||||
|
||||
sync.addHandler(5, e => {
|
||||
Tags.from(e)
|
||||
.type("a")
|
||||
.values()
|
||||
.all()
|
||||
.forEach(naddr => {
|
||||
const list = this.lists.get(naddr)
|
||||
|
||||
if (list) {
|
||||
this.lists.patch({
|
||||
naddr: list.naddr,
|
||||
deleted_at: e.created_at,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getLists = (spec = null) => this.lists.all({...spec, deleted_at: {$eq: null}})
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ellipsize} from "hurdak/lib/hurdak"
|
||||
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"
|
||||
|
||||
export class Directory {
|
||||
profiles: Table<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 fuzzy(this.getNamedProfiles(), {
|
||||
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
||||
threshold: 0.3,
|
||||
})
|
||||
})
|
||||
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(() => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const profile = this.profiles.get(e.pubkey)
|
||||
|
||||
if (e.created_at < profile?.created_at) {
|
||||
return
|
||||
}
|
||||
|
||||
this.profiles.patch({
|
||||
...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) {
|
||||
return ellipsize(display_name, 60)
|
||||
}
|
||||
|
||||
if (name) {
|
||||
return ellipsize(name, 60)
|
||||
}
|
||||
|
||||
try {
|
||||
return nip19.npubEncode(pubkey).slice(-8)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
displayPubkey = pubkey => this.displayProfile(this.getProfile(pubkey))
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import {last} from "ramda"
|
||||
import {nip05} from "nostr-tools"
|
||||
import {tryFunc, now, tryJson} from "src/util/misc"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
import type {Handle} from "src/system/types"
|
||||
|
||||
export class Nip05 {
|
||||
handles: Table<Handle>
|
||||
constructor(sync: Sync) {
|
||||
this.handles = sync.table("nip05/handles", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const handle = this.handles.get(e.pubkey)
|
||||
|
||||
if (!kind0.nip05 || e.created_at < handle?.created_at) {
|
||||
return
|
||||
}
|
||||
|
||||
const profile = await tryFunc(() => nip05.queryProfile(kind0.nip05), true)
|
||||
|
||||
if (profile?.pubkey !== e.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
this.handles.patch({
|
||||
profile: profile,
|
||||
pubkey: e.pubkey,
|
||||
address: kind0.nip05,
|
||||
created_at: e.created_at,
|
||||
updated_at: now(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getHandle = pubkey => this.handles.get(pubkey)
|
||||
|
||||
displayHandle = handle =>
|
||||
handle.address.startsWith("_@") ? last(handle.address.split("@")) : handle.address
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import {fetchJson, now, tryFunc, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
||||
import {invoiceAmount} from "src/util/lightning"
|
||||
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"
|
||||
|
||||
const getLnUrl = address => {
|
||||
// Try to parse it as a lud06 LNURL
|
||||
if (address.startsWith("lnurl1")) {
|
||||
return tryFunc(() => bech32ToHex(address))
|
||||
}
|
||||
|
||||
// Try to parse it as a lud16 address
|
||||
if (address.includes("@")) {
|
||||
const [name, domain] = address.split("@")
|
||||
|
||||
if (domain && name) {
|
||||
return `https://${domain}/.well-known/lnurlp/${name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Nip57 {
|
||||
zappers: Table<Zapper>
|
||||
constructor(sync: Sync) {
|
||||
this.zappers = sync.table("niip57/zappers", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
sync.addHandler(0, e => {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
return []
|
||||
}
|
||||
|
||||
return zaps
|
||||
.map(zap => {
|
||||
const zapMeta = Tags.from(zap).asMeta()
|
||||
|
||||
return tryJson(() => ({
|
||||
...zap,
|
||||
invoiceAmount: invoiceAmount(zapMeta.bolt11),
|
||||
request: JSON.parse(zapMeta.description),
|
||||
}))
|
||||
})
|
||||
.filter(zap => {
|
||||
if (!zap) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't count zaps that the user sent himself
|
||||
if (zap.request.pubkey === pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const {invoiceAmount, request} = zap
|
||||
const reqMeta = Tags.from(request).asMeta()
|
||||
|
||||
// Verify that the zapper actually sent the requested amount (if it was supplied)
|
||||
if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the sending client provided an lnurl tag, verify that too
|
||||
if (reqMeta.lnurl && reqMeta.lnurl !== zapper.lnurl) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that the zap note actually came from the recipient's zapper
|
||||
if (zapper.nostrPubkey !== zap.pubkey) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
import {sortBy, pluck, uniq, nth, uniqBy, prop, last, inc} from "ramda"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {fuzzy, chain, tryJson, now, fetchJson} from "src/util/misc"
|
||||
import {warn} from "src/util/logger"
|
||||
import {normalizeRelayUrl, findReplyId, isShareableRelay, Tags} from "src/util/nostr"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
import type {Relay, RelayInfo, RelayPolicy} from "src/system/types"
|
||||
|
||||
export type RoutingOpts = {
|
||||
getDefaultRelays: () => string[]
|
||||
relayHasError: (url: string) => boolean
|
||||
}
|
||||
|
||||
export class Routing {
|
||||
sync: Sync
|
||||
relays: Table<Relay>
|
||||
policies: Table<RelayPolicy>
|
||||
searchRelays: Readable<(query: string) => Relay[]>
|
||||
constructor(sync, readonly opts: RoutingOpts) {
|
||||
this.sync = sync
|
||||
this.relays = sync.table("routing/relays", "url", {sort: sortBy(e => -e.count)})
|
||||
this.policies = sync.table("routing/policies", "pubkey", {sort: sync.sortByPubkeyWhitelist})
|
||||
this.searchRelays = this.relays.watch(() => fuzzy(this.relays.all(), {keys: ["url"]}))
|
||||
|
||||
sync.addHandler(2, e => {
|
||||
if (isShareableRelay(e.content)) {
|
||||
this.addRelay(normalizeRelayUrl(e.content))
|
||||
}
|
||||
})
|
||||
|
||||
sync.addHandler(3, e => {
|
||||
this.setPolicy(
|
||||
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}
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
sync.addHandler(10002, e => {
|
||||
this.setPolicy(
|
||||
e,
|
||||
Tags.from(e)
|
||||
.type("r")
|
||||
.all()
|
||||
.map(([_, url, mode]) => {
|
||||
const write = !mode || mode === "write"
|
||||
const read = !mode || mode === "read"
|
||||
|
||||
if (!write && !read) {
|
||||
warn(`Encountered unknown relay mode: ${mode}`)
|
||||
}
|
||||
|
||||
return {url: normalizeRelayUrl(url), write, read}
|
||||
})
|
||||
)
|
||||
})
|
||||
;(async () => {
|
||||
const {DEFAULT_RELAYS, FORCE_RELAYS, DUFFLEPUD_URL} = this.sync.env
|
||||
|
||||
// Throw some hardcoded defaults in there
|
||||
DEFAULT_RELAYS.forEach(this.addRelay)
|
||||
|
||||
// Load relays from nostr.watch via dufflepud
|
||||
if (FORCE_RELAYS.length === 0 && DUFFLEPUD_URL) {
|
||||
try {
|
||||
const json = await fetchJson(DUFFLEPUD_URL + "/relay")
|
||||
|
||||
json.relays.filter(isShareableRelay).forEach(this.addRelay)
|
||||
} catch (e) {
|
||||
warn("Failed to fetch relays list", e)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
addRelay = url => {
|
||||
const relay = this.relays.get(url)
|
||||
|
||||
this.relays.patch({
|
||||
url,
|
||||
count: inc(relay?.count || 0),
|
||||
first_seen: relay?.first_seen || now(),
|
||||
info: {
|
||||
last_checked: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
setPolicy = ({pubkey, created_at}, relays) => {
|
||||
if (relays?.length > 0) {
|
||||
if (created_at < this.policies.get(pubkey)?.created_at) {
|
||||
return
|
||||
}
|
||||
|
||||
this.policies.patch({
|
||||
pubkey,
|
||||
created_at,
|
||||
updated_at: now(),
|
||||
relays: uniqBy(prop("url"), relays).map(relay => {
|
||||
this.addRelay(relay.url)
|
||||
|
||||
return {read: true, write: true, ...relay}
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getRelay = (url: string): Relay => this.relays.get(url) || {url}
|
||||
|
||||
getRelayInfo = (url: string): RelayInfo => this.relays.get(url)?.info || {}
|
||||
|
||||
displayRelay = ({url}) => last(url.split("://"))
|
||||
|
||||
getSearchRelays = () => {
|
||||
const searchableRelayUrls = pluck(
|
||||
"url",
|
||||
this.relays.all({"info.supported_nips": {$contains: 50}})
|
||||
)
|
||||
|
||||
return uniq(this.sync.env.SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8)
|
||||
}
|
||||
|
||||
getPubkeyRelays = (pubkey, mode = null) => {
|
||||
const relays = this.policies.get(pubkey)?.relays || []
|
||||
|
||||
return mode ? relays.filter(prop(mode)) : relays
|
||||
}
|
||||
|
||||
getPubkeyRelayUrls = (pubkey, mode = null) => pluck("url", this.getPubkeyRelays(pubkey, mode))
|
||||
|
||||
// Smart relay selection
|
||||
//
|
||||
// From Mike Dilger:
|
||||
// 1) Other people's write relays — pull events from people you follow,
|
||||
// including their contact lists
|
||||
// 2) Other people's read relays — push events that tag them (replies or just tagging).
|
||||
// However, these may be authenticated, use with caution
|
||||
// 3) Your write relays —- write events you post to your microblog feed for the
|
||||
// world to see. ALSO write your contact list. ALSO read back your own contact list.
|
||||
// 4) Your read relays —- read events that tag you. ALSO both write and read
|
||||
// client-private data like client configuration events or anything that the world
|
||||
// doesn't need to see.
|
||||
// 5) Advertise relays — write and read back your own relay list
|
||||
|
||||
selectHints = (limit, hints) => {
|
||||
const seen = new Set()
|
||||
const ok = []
|
||||
const bad = []
|
||||
|
||||
for (const url of chain(hints, this.opts.getDefaultRelays())) {
|
||||
if (seen.has(url)) {
|
||||
continue
|
||||
}
|
||||
|
||||
seen.add(url)
|
||||
|
||||
// Filter out relays that appear to be broken or slow
|
||||
if (!isShareableRelay(url)) {
|
||||
bad.push(url)
|
||||
} else if (this.opts.relayHasError(url)) {
|
||||
bad.push(url)
|
||||
} else {
|
||||
ok.push(url)
|
||||
}
|
||||
|
||||
if (ok.length > limit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have enough hints, use the broken ones
|
||||
return ok.concat(bad).slice(0, limit)
|
||||
}
|
||||
|
||||
hintSelector =
|
||||
generateHints =>
|
||||
(limit, ...args) =>
|
||||
this.selectHints(limit, generateHints.call(this, ...args))
|
||||
|
||||
getPubkeyHints = this.hintSelector(function* (pubkey, mode = "write") {
|
||||
const other = mode === "write" ? "read" : "write"
|
||||
|
||||
yield* this.getPubkeyRelayUrls(pubkey, mode)
|
||||
yield* this.getPubkeyRelayUrls(pubkey, other)
|
||||
})
|
||||
|
||||
getEventHints = this.hintSelector(function* (event) {
|
||||
yield* event.seen_on || []
|
||||
yield* this.getPubkeyHints(null, event.pubkey)
|
||||
})
|
||||
|
||||
// If we're looking for an event's children, the read relays the author has
|
||||
// advertised would be the most reliable option, since well-behaved clients
|
||||
// will write replies there. However, this may include spam, so we may want
|
||||
// to read from the current user's network's read relays instead.
|
||||
getReplyHints = this.hintSelector(function* (event) {
|
||||
yield* this.getPubkeyRelayUrls(event.pubkey, "write")
|
||||
yield* event.seen_on || []
|
||||
yield* this.getPubkeyRelayUrls(event.pubkey, "read")
|
||||
})
|
||||
|
||||
// If we're looking for an event's parent, tags are the most reliable hint,
|
||||
// but we can also look at where the author of the note reads from
|
||||
getParentHints = this.hintSelector(function* (event) {
|
||||
const parentId = findReplyId(event)
|
||||
|
||||
yield* Tags.from(event).equals(parentId).relays()
|
||||
yield* event.seen_on || []
|
||||
yield* this.getPubkeyHints(null, event.pubkey, "read")
|
||||
})
|
||||
|
||||
// If we're replying or reacting to an event, we want the author to know, as well as
|
||||
// anyone else who is tagged in the original event or the reply. Get everyone's read
|
||||
// relays. Limit how many per pubkey we publish to though. We also want to advertise
|
||||
// our content to our followers, so publish to our write relays as well.
|
||||
getPublishHints = (limit, event, extraRelays = []) => {
|
||||
const tags = Tags.from(event)
|
||||
const pubkeys = tags.type("p").values().all().concat(event.pubkey)
|
||||
const hintGroups = pubkeys.map(pubkey => this.getPubkeyHints(3, pubkey, "read"))
|
||||
|
||||
return this.mergeHints(limit, hintGroups.concat([extraRelays]))
|
||||
}
|
||||
|
||||
mergeHints = (limit, groups) => {
|
||||
const scores = {} as Record<string, any>
|
||||
|
||||
for (const hints of groups) {
|
||||
hints.forEach((hint, i) => {
|
||||
const score = 1 / (i + 1) / hints.length
|
||||
|
||||
if (!scores[hint]) {
|
||||
scores[hint] = {score: 0, count: 0}
|
||||
}
|
||||
|
||||
scores[hint].score += score
|
||||
scores[hint].count += 1
|
||||
})
|
||||
}
|
||||
|
||||
// Use the log-sum-exp and a weighted sum
|
||||
for (const score of Object.values(scores)) {
|
||||
const weight = Math.log(groups.length / score.count)
|
||||
|
||||
score.score = weight + Math.log1p(Math.exp(score.score - score.count))
|
||||
}
|
||||
|
||||
return sortBy(([hint, {score}]) => -score, Object.entries(scores))
|
||||
.map(nth(0))
|
||||
.slice(0, limit)
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import {ensurePlural} from "hurdak/lib/hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
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"
|
||||
|
||||
export class Social {
|
||||
sync: Sync
|
||||
graph: Table<GraphEntry>
|
||||
constructor(sync) {
|
||||
this.sync = sync
|
||||
|
||||
this.graph = sync.table("social/graph", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
sync.addHandler(3, e => {
|
||||
const entry = this.graph.get(e.pubkey)
|
||||
|
||||
if (e.created_at < entry?.petnames_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
this.graph.patch({
|
||||
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()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of this.getPetnames(pubkey)) {
|
||||
follows.add(tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
return follows
|
||||
}
|
||||
|
||||
getMutesSet = pubkeys => {
|
||||
const mutes = new Set()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of this.getMutedTags(pubkey)) {
|
||||
mutes.add(tag[1])
|
||||
}
|
||||
}
|
||||
|
||||
return mutes
|
||||
}
|
||||
|
||||
getFollows = pubkeys => Array.from(this.getFollowsSet(pubkeys))
|
||||
|
||||
getMutes = pubkeys => Array.from(this.getMutesSet(pubkeys))
|
||||
|
||||
getNetworkSet = (pubkeys, includeFollows = false) => {
|
||||
const follows = this.getFollowsSet(pubkeys)
|
||||
const network = includeFollows ? follows : new Set()
|
||||
|
||||
for (const pubkey of this.getFollows(follows)) {
|
||||
if (!follows.has(pubkey)) {
|
||||
network.add(pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return network
|
||||
}
|
||||
|
||||
getNetwork = pubkeys => Array.from(this.getNetworkSet(pubkeys))
|
||||
|
||||
isFollowing = (a, b) => this.getFollowsSet(a).has(b)
|
||||
|
||||
isIgnoring = (a, b) => this.getMutesSet(a).has(b)
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
import {Sync} from "src/system/components/Sync"
|
||||
import {Network} from "src/system/components/Network"
|
||||
import {Meta} from "src/system/components/Meta"
|
||||
import {User} from "src/system/components/User"
|
||||
import {Directory} from "src/system/stores/Directory"
|
||||
import {Routing} from "src/system/stores/Routing"
|
||||
import {Content} from "src/system/stores/Content"
|
||||
import {Nip57} from "src/system/stores/Nip57"
|
||||
import {Chat} from "src/system/stores/Chat"
|
||||
import {Nip05} from "src/system/stores/Nip05"
|
||||
import {Cache} from "src/system/stores/Cache"
|
||||
import {Social} from "src/system/stores/Social"
|
||||
import {Alerts} from "src/system/stores/Alerts"
|
||||
import {PubkeyLoader} from "src/system/util/PubkeyLoader"
|
||||
import {Builder} from "src/system/util/Builder"
|
||||
|
||||
export type SystemEnv = {
|
||||
DUFFLEPUD_URL?: string
|
||||
MULTIPLEXTR_URL?: string
|
||||
FORCE_RELAYS?: string[]
|
||||
COUNT_RELAYS: string[]
|
||||
SEARCH_RELAYS: string[]
|
||||
DEFAULT_RELAYS: string[]
|
||||
}
|
||||
|
||||
export interface System {
|
||||
ns: string
|
||||
env: SystemEnv
|
||||
sync: Sync
|
||||
network: Network
|
||||
meta: Meta
|
||||
user: User
|
||||
cache: Cache
|
||||
content: Content
|
||||
directory: Directory
|
||||
nip05: Nip05
|
||||
nip57: Nip57
|
||||
social: Social
|
||||
routing: Routing
|
||||
alerts: Alerts
|
||||
chat: Chat
|
||||
builder: Builder
|
||||
pubkeyLoader: PubkeyLoader
|
||||
}
|
||||
|
||||
export class DefaultSystem implements System {
|
||||
ns: string
|
||||
env: SystemEnv
|
||||
sync: Sync
|
||||
network: Network
|
||||
meta: Meta
|
||||
user: User
|
||||
cache: Cache
|
||||
content: Content
|
||||
directory: Directory
|
||||
nip05: Nip05
|
||||
nip57: Nip57
|
||||
social: Social
|
||||
routing: Routing
|
||||
alerts: Alerts
|
||||
chat: Chat
|
||||
builder: Builder
|
||||
pubkeyLoader: PubkeyLoader
|
||||
|
||||
constructor(ns, env) {
|
||||
this.ns = ns
|
||||
this.env = env
|
||||
|
||||
// Core components
|
||||
this.sync = DefaultSystem.getSync(this)
|
||||
this.network = DefaultSystem.getNetwork(this)
|
||||
this.meta = DefaultSystem.getMeta(this)
|
||||
this.user = DefaultSystem.getUser(this)
|
||||
|
||||
// Data stores
|
||||
this.cache = DefaultSystem.getCache(this)
|
||||
this.content = DefaultSystem.getContent(this)
|
||||
this.directory = DefaultSystem.getDirectory(this)
|
||||
this.nip05 = DefaultSystem.getNip05(this)
|
||||
this.nip57 = DefaultSystem.getNip57(this)
|
||||
this.social = DefaultSystem.getSocial(this)
|
||||
this.routing = DefaultSystem.getRouting(this)
|
||||
this.alerts = DefaultSystem.getAlerts(this)
|
||||
this.chat = DefaultSystem.getChat(this)
|
||||
|
||||
// Extra utils
|
||||
this.builder = DefaultSystem.getBuilder(this)
|
||||
this.pubkeyLoader = DefaultSystem.getPubkeyLoader(this)
|
||||
}
|
||||
|
||||
static getSync = system =>
|
||||
new Sync(system, {
|
||||
getUserPubkey: () => system.user.getPubkey(),
|
||||
getPubkeyWhitelist: () => {
|
||||
const pubkey = system.user.getPubkey()
|
||||
|
||||
if (!pubkey) {
|
||||
return null
|
||||
}
|
||||
|
||||
const follows = system.user.getFollowsSet()
|
||||
|
||||
follows.add(pubkey)
|
||||
|
||||
return follows
|
||||
},
|
||||
})
|
||||
|
||||
static getNetwork = system =>
|
||||
new Network({
|
||||
getMultiplextrUrl: () => system.user.getSetting("multiplextr_url"),
|
||||
processEvents: events => system.sync.processEvents(events),
|
||||
getRelayInfo: url => system.routing.getRelayInfo(url),
|
||||
forceRelays: system.env.FORCE_RELAYS,
|
||||
countRelays: system.env.COUNT_RELAYS,
|
||||
})
|
||||
|
||||
static getMeta = system =>
|
||||
new Meta({
|
||||
network: system.network,
|
||||
})
|
||||
|
||||
static getUser = system => new User(system)
|
||||
|
||||
static getCache = system => new Cache(system.sync)
|
||||
|
||||
static getContent = system => new Content(system.sync)
|
||||
|
||||
static getDirectory = system => new Directory(system.sync)
|
||||
|
||||
static getNip05 = system => new Nip05(system.sync)
|
||||
|
||||
static getNip57 = system => new Nip57(system.sync)
|
||||
|
||||
static getSocial = system => new Social(system.sync)
|
||||
|
||||
static getRouting = system =>
|
||||
new Routing(system.sync, {
|
||||
getDefaultRelays: () => system.user.getRelayUrls().concat(system.env.DEFAULT_RELAYS),
|
||||
relayHasError: url =>
|
||||
Boolean(
|
||||
system.network.pool.get(url, {autoConnect: false})?.error ||
|
||||
first(system.meta.getRelayQuality(url)) < 0.5
|
||||
),
|
||||
})
|
||||
|
||||
static getAlerts = system =>
|
||||
new Alerts(system.sync, {
|
||||
getUserPubkey: () => system.user.getPubkey(),
|
||||
isUserEvent: e => system.user.isUserEvent(e),
|
||||
isMuted: e => system.user.isMuted(e),
|
||||
})
|
||||
|
||||
static getChat = system =>
|
||||
new Chat(system.sync, {
|
||||
getCrypt: () => system.user.crypt,
|
||||
})
|
||||
|
||||
static getBuilder = system =>
|
||||
new Builder({
|
||||
getEventHint: event => first(system.routing.getEventHints(1, event)) || "",
|
||||
getPubkeyHint: pubkey => first(system.routing.getPubkeyHints(1, pubkey)) || "",
|
||||
getPubkeyPetname: pubkey => {
|
||||
const profile = system.directory.getProfile(pubkey)
|
||||
|
||||
return profile ? system.directory.displayProfile(profile) : ""
|
||||
},
|
||||
})
|
||||
|
||||
static getPubkeyLoader = system =>
|
||||
new PubkeyLoader({
|
||||
getLastUpdated: pubkey => system.directory.profiles.get(pubkey)?.updated_at || 0,
|
||||
getChunkRelays: pubkeys =>
|
||||
system.routing.mergeHints(
|
||||
system.user.getSetting("relay_limit"),
|
||||
pubkeys.map(pubkey => system.routing.getPubkeyHints(3, pubkey))
|
||||
),
|
||||
loadChunk: ({filter, relays}) => system.network.load({filter, relays}),
|
||||
})
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
import {last, pick, uniqBy} from "ramda"
|
||||
import {doPipe} from "hurdak/lib/hurdak"
|
||||
import {Tags, channelAttrs, findRoot, findReply} from "src/util/nostr"
|
||||
import {parseContent} from "src/util/notes"
|
||||
|
||||
export type BuilderOpts = {
|
||||
getPubkeyPetname: (pubkey: string) => string
|
||||
getPubkeyHint: (pubkey: string) => string
|
||||
getEventHint: (event: Event) => string
|
||||
}
|
||||
|
||||
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 class Builder {
|
||||
getPubkeyPetname: BuilderOpts["getPubkeyPetname"]
|
||||
getPubkeyHint: BuilderOpts["getPubkeyHint"]
|
||||
getEventHint: BuilderOpts["getEventHint"]
|
||||
constructor({getPubkeyPetname, getPubkeyHint, getEventHint}: BuilderOpts) {
|
||||
this.getPubkeyPetname = getPubkeyPetname
|
||||
this.getPubkeyHint = getPubkeyHint
|
||||
this.getEventHint = getEventHint
|
||||
}
|
||||
mention = pubkey => {
|
||||
const hint = this.getPubkeyHint(pubkey)
|
||||
const petname = this.getPubkeyPetname(pubkey)
|
||||
|
||||
return ["p", pubkey, hint, petname]
|
||||
}
|
||||
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([this.mention(value.pubkey)])
|
||||
seen.add(value.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
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 = this.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 [this.mention(n.pubkey), root, ...extra, reply]
|
||||
}
|
||||
authenticate(url, challenge) {
|
||||
return buildEvent(22242, {
|
||||
tags: [
|
||||
["challenge", challenge],
|
||||
["relay", url],
|
||||
],
|
||||
})
|
||||
}
|
||||
setProfile(profile) {
|
||||
return buildEvent(0, {content: JSON.stringify(profile)})
|
||||
}
|
||||
setRelays(relays) {
|
||||
return buildEvent(10002, {
|
||||
tags: relays.map(r => {
|
||||
const t = ["r", r.url]
|
||||
|
||||
if (!r.write) {
|
||||
t.push("read")
|
||||
}
|
||||
|
||||
return t
|
||||
}),
|
||||
})
|
||||
}
|
||||
setAppData(d, content = "") {
|
||||
return buildEvent(30078, {content, tags: [["d", d]]})
|
||||
}
|
||||
setPetnames(petnames) {
|
||||
return buildEvent(3, {tags: petnames})
|
||||
}
|
||||
setMutes(mutes) {
|
||||
return buildEvent(10000, {tags: mutes})
|
||||
}
|
||||
createList(list) {
|
||||
return buildEvent(30001, {tags: list})
|
||||
}
|
||||
createChannel(channel) {
|
||||
return buildEvent(40, {content: JSON.stringify(pick(channelAttrs, channel))})
|
||||
}
|
||||
updateChannel({id, ...channel}) {
|
||||
return buildEvent(41, {
|
||||
content: JSON.stringify(pick(channelAttrs, channel)),
|
||||
tags: [["e", id]],
|
||||
})
|
||||
}
|
||||
createChatMessage(channelId, content, url) {
|
||||
return buildEvent(42, {content, tags: [["e", channelId, url, "root"]]})
|
||||
}
|
||||
createDirectMessage(pubkey, content) {
|
||||
return buildEvent(4, {content, tags: [["p", pubkey]]})
|
||||
}
|
||||
createNote(content, tags = []) {
|
||||
return buildEvent(1, {content, tags: uniqTags(this.tagsFromContent(content, tags))})
|
||||
}
|
||||
createReaction(note, content) {
|
||||
return buildEvent(7, {content, tags: this.getReplyTags(note)})
|
||||
}
|
||||
createReply(note, content, tags = []) {
|
||||
return buildEvent(1, {
|
||||
content,
|
||||
tags: doPipe(tags, [
|
||||
tags => tags.concat(this.getReplyTags(note, true)),
|
||||
tags => this.tagsFromContent(content, tags),
|
||||
uniqTags,
|
||||
]),
|
||||
})
|
||||
}
|
||||
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})
|
||||
}
|
||||
deleteEvents(ids) {
|
||||
return buildEvent(5, {tags: ids.map(id => ["e", id])})
|
||||
}
|
||||
deleteNaddrs(naddrs) {
|
||||
return buildEvent(5, {tags: naddrs.map(naddr => ["a", naddr])})
|
||||
}
|
||||
createLabel(payload) {
|
||||
return buildEvent(1985, payload)
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
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 PubkeyLoaderOpts = {
|
||||
getLastUpdated: (pubkey: string) => number
|
||||
getChunkRelays: (pubkeys: string[]) => string[]
|
||||
loadChunk: (args: {filter: Filter | Filter[]; relays: string[]}) => Promise<void>
|
||||
}
|
||||
|
||||
export type LoadPeopleOpts = {
|
||||
relays?: string[]
|
||||
kinds?: number[]
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export class PubkeyLoader {
|
||||
attemptedPubkeys: Set<string>
|
||||
getLastUpdated: PubkeyLoaderOpts["getLastUpdated"]
|
||||
getChunkRelays: PubkeyLoaderOpts["getChunkRelays"]
|
||||
loadChunk: PubkeyLoaderOpts["loadChunk"]
|
||||
|
||||
constructor({getLastUpdated, getChunkRelays, loadChunk}: PubkeyLoaderOpts) {
|
||||
this.attemptedPubkeys = new Set()
|
||||
this.getLastUpdated = getLastUpdated
|
||||
this.getChunkRelays = getChunkRelays
|
||||
this.loadChunk = loadChunk
|
||||
}
|
||||
|
||||
getStalePubkeys = pubkeys => {
|
||||
const stale = new Set()
|
||||
const since = now() - timedelta(3, "hours")
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
if (stale.has(pubkey) || this.attemptedPubkeys.has(pubkey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.attemptedPubkeys.add(pubkey)
|
||||
|
||||
if (this.getLastUpdated(pubkey) > since) {
|
||||
continue
|
||||
}
|
||||
|
||||
stale.add(pubkey)
|
||||
}
|
||||
|
||||
return stale
|
||||
}
|
||||
|
||||
loadPubkeys = async (rawPubkeys, {relays, force, kinds = personKinds}: LoadPeopleOpts = {}) => {
|
||||
const pubkeys = force ? uniq(rawPubkeys) : this.getStalePubkeys(rawPubkeys)
|
||||
|
||||
const getChunkRelays = chunk => {
|
||||
if (relays?.length > 0) {
|
||||
return relays
|
||||
}
|
||||
|
||||
return this.getChunkRelays(chunk)
|
||||
}
|
||||
|
||||
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 this.loadChunk({
|
||||
relays: getChunkRelays(chunk),
|
||||
filter: getChunkFilter(chunk),
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user