mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 08:21:20 +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 {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"
|
||||
import {collection, derived} from "../util/store"
|
||||
|
||||
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,
|
||||
})
|
||||
export function contributeState() {
|
||||
const profiles = collection<Profile>()
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
return {profiles}
|
||||
}
|
||||
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(() => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const profile = this.profiles.get(e.pubkey)
|
||||
export function contributeSelectors({Directory}) {
|
||||
const getProfile = (pubkey: string): Profile => Directory.profiles.getKey(pubkey) || {pubkey}
|
||||
|
||||
if (e.created_at < profile?.created_at) {
|
||||
return
|
||||
}
|
||||
const getNamedProfiles = () =>
|
||||
Directory.profiles.all().filter(p => p.name || p.nip05 || p.display_name)
|
||||
|
||||
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) => {
|
||||
const displayProfile = ({display_name, name, pubkey}: Profile) => {
|
||||
if (display_name) {
|
||||
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 {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"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
const getLnUrl = address => {
|
||||
// Try to parse it as a lud06 LNURL
|
||||
@ -21,52 +20,15 @@ const getLnUrl = address => {
|
||||
}
|
||||
}
|
||||
|
||||
export class Nip57 {
|
||||
zappers: Table<Zapper>
|
||||
constructor(sync: Sync) {
|
||||
this.zappers = sync.table("niip57/zappers", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
export function contributeState() {
|
||||
const zappers = collection<Zapper>()
|
||||
|
||||
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()
|
||||
return {zappers}
|
||||
}
|
||||
|
||||
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)
|
||||
export function contributeActions({Nip57}) {
|
||||
const processZaps = (zaps, pubkey) => {
|
||||
const zapper = Nip57.zappers.getKey(pubkey)
|
||||
|
||||
if (!zapper) {
|
||||
return []
|
||||
@ -113,4 +75,43 @@ export class Nip57 {
|
||||
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 {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"
|
||||
import {collection} from "../util/store"
|
||||
|
||||
export class Social {
|
||||
sync: Sync
|
||||
graph: Table<GraphEntry>
|
||||
constructor(sync) {
|
||||
this.sync = sync
|
||||
export function contributeState() {
|
||||
const graph = collection<GraphEntry>()
|
||||
|
||||
this.graph = sync.table("social/graph", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
return {graph}
|
||||
}
|
||||
|
||||
sync.addHandler(3, e => {
|
||||
const entry = this.graph.get(e.pubkey)
|
||||
export function contributeActions({Social}) {
|
||||
const getPetnames = pubkey => Social.graph.getKey(pubkey)?.petnames || []
|
||||
|
||||
if (e.created_at < entry?.petnames_updated_at) {
|
||||
return
|
||||
}
|
||||
const getMutedTags = pubkey => Social.graph.getKey(pubkey)?.mutes || []
|
||||
|
||||
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 getFollowsSet = pubkeys => {
|
||||
const follows = new Set()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of this.getPetnames(pubkey)) {
|
||||
for (const tag of getPetnames(pubkey)) {
|
||||
follows.add(tag[1])
|
||||
}
|
||||
}
|
||||
@ -63,11 +27,11 @@ export class Social {
|
||||
return follows
|
||||
}
|
||||
|
||||
getMutesSet = pubkeys => {
|
||||
const getMutesSet = pubkeys => {
|
||||
const mutes = new Set()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of this.getMutedTags(pubkey)) {
|
||||
for (const tag of getMutedTags(pubkey)) {
|
||||
mutes.add(tag[1])
|
||||
}
|
||||
}
|
||||
@ -75,15 +39,15 @@ export class Social {
|
||||
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 follows = this.getFollowsSet(pubkeys)
|
||||
const getNetworkSet = (pubkeys, includeFollows = false) => {
|
||||
const follows = getFollowsSet(pubkeys)
|
||||
const network = includeFollows ? follows : new Set()
|
||||
|
||||
for (const pubkey of this.getFollows(follows)) {
|
||||
for (const pubkey of getFollows(follows)) {
|
||||
if (!follows.has(pubkey)) {
|
||||
network.add(pubkey)
|
||||
}
|
||||
@ -92,9 +56,54 @@ export class Social {
|
||||
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 Builder from "./components/Builder"
|
||||
import * as Chat from "./components/Chat"
|
||||
import * as Content from "./components/Content"
|
||||
import * as Crypt from "./components/Crypt"
|
||||
import * as Directory from "./components/Directory"
|
||||
import * as Events from "./components/Events"
|
||||
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 Nip57 from "./components/Nip57"
|
||||
import * as PubkeyLoader from "./components/PubkeyLoader"
|
||||
import * as Routing from "./components/Routing"
|
||||
import * as Social from "./components/Social"
|
||||
import * as User from "./components/User"
|
||||
|
||||
export const createEngine = (engine, components) => {
|
||||
for (const component of components) {
|
||||
engine[component.name] = component.contributeState?.()
|
||||
const componentState = components.map(c => [c, c.contributeState?.(engine)])
|
||||
|
||||
for (const [component, state] of componentState) {
|
||||
Object.assign(engine[component.name], state)
|
||||
}
|
||||
|
||||
const componentSelectors = components.map(c => [c, c.contributeSelectors?.(engine)])
|
||||
@ -36,16 +46,21 @@ export const createDefaultEngine = Env => {
|
||||
{Env},
|
||||
{
|
||||
Alerts,
|
||||
Builder,
|
||||
Chat,
|
||||
Content,
|
||||
Crypt,
|
||||
// Directory,
|
||||
Directory,
|
||||
Events,
|
||||
Keys,
|
||||
Meta,
|
||||
Network,
|
||||
Nip05,
|
||||
// Nip57,
|
||||
Nip57,
|
||||
PubkeyLoader,
|
||||
Routing,
|
||||
// Social,
|
||||
Social,
|
||||
User,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -127,8 +127,8 @@ export const key = <T>(baseStore, k): Key<T> => {
|
||||
return keyStore
|
||||
}
|
||||
|
||||
export const collection = <T>(): Collection<T> => {
|
||||
const baseStore = writable<Map<any, T>>(new Map())
|
||||
export const collection = <T>(defaults = {}): Collection<T> => {
|
||||
const baseStore = writable<Map<any, T>>(new Map(Object.entries(defaults)))
|
||||
|
||||
return {
|
||||
...baseStore,
|
||||
|
@ -4,10 +4,19 @@ 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 {UserSettings} from "src/system/types"
|
||||
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
|
||||
|
@ -95,16 +95,6 @@ export type Profile = {
|
||||
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 = {
|
||||
id: string
|
||||
type: "public" | "private"
|
||||
|
Loading…
Reference in New Issue
Block a user