mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 08:21:20 +00:00
Refactor again
This commit is contained in:
parent
8428c179c6
commit
faccf16f2e
@ -1,6 +1,7 @@
|
||||
# Current
|
||||
|
||||
- [ ] Refactor
|
||||
- [ ] Inject env into system
|
||||
- [ ] Delete messages when leaving a room
|
||||
- [ ] Add pagination to chat/dms
|
||||
- [ ] Fix scrolling to to when modal is open
|
||||
|
@ -2,8 +2,7 @@ import {max, mergeLeft, fromPairs, sortBy, assoc, uniqBy, prop, propEq, groupBy,
|
||||
import {findReplyId} from "src/util/nostr"
|
||||
import {chunk, ensurePlural} from "hurdak/lib/hurdak"
|
||||
import {batch, now, timedelta} from "src/util/misc"
|
||||
import {PubkeyLoader} from "src/system"
|
||||
import system, {ENABLE_ZAPS, user, routing, network} from "src/app/system"
|
||||
import {ENABLE_ZAPS, user, routing, pubkeyLoader, network} from "src/app/system"
|
||||
|
||||
class Cursor {
|
||||
relays: string[]
|
||||
@ -153,7 +152,7 @@ const streamContext = ({notes, onChunk, maxDepth = 2}) => {
|
||||
const pubkeys = pluck("pubkey", events)
|
||||
|
||||
// Load any people we should know about
|
||||
new PubkeyLoader(system).loadPubkeys(pubkeys)
|
||||
pubkeyLoader.loadPubkeys(pubkeys)
|
||||
|
||||
// Load data prior to now for our new ids
|
||||
chunk(256, newIds).forEach(ids => {
|
||||
|
@ -56,7 +56,7 @@
|
||||
seenChallenges.add(challenge)
|
||||
|
||||
const rawEvent = system.builder.authenticate(url, challenge)
|
||||
const [event] = await system.outbox.publish(rawEvent, [url], null, "AUTH")
|
||||
const [event] = await system.user.publish(rawEvent, [url], null, "AUTH")
|
||||
|
||||
return event
|
||||
}
|
||||
|
@ -2,12 +2,13 @@
|
||||
import cx from "classnames"
|
||||
import {theme, installPrompt} from "src/partials/state"
|
||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||
import {FORCE_RELAYS, user, directory, alerts} from "src/app/system"
|
||||
import {FORCE_RELAYS, user, directory, alerts, chat} from "src/app/system"
|
||||
import {watch} from "src/util/loki"
|
||||
import {routes, slowConnections, menuIsOpen} from "src/app/state"
|
||||
|
||||
const {canSign} = user.keys
|
||||
const {hasNewNotfications, hasNewChatMessages, hasNewDirectMessages} = alerts
|
||||
const {hasNewNotfications} = alerts
|
||||
const {hasNewChatMessages, hasNewDirectMessages} = chat
|
||||
const profile = watch(directory.profiles, () =>
|
||||
user.getPubkey() ? directory.getProfile(user.getPubkey()) : null
|
||||
)
|
||||
|
@ -2,10 +2,11 @@
|
||||
import {onMount} from "svelte"
|
||||
import {appName} from "src/partials/state"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {alerts} from "src/app/system"
|
||||
import {alerts, chat} from "src/app/system"
|
||||
import {menuIsOpen} from "src/app/state"
|
||||
|
||||
const {hasNewNotfications, hasNewChatMessages, hasNewDirectMessages} = alerts
|
||||
const {hasNewNotfications} = alerts
|
||||
const {hasNewChatMessages, hasNewDirectMessages} = chat
|
||||
const logoUrl = import.meta.env.VITE_LOGO_URL || "/images/logo.png"
|
||||
const toggleMenu = () => menuIsOpen.update(x => !x)
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
import CopyValue from "src/partials/CopyValue.svelte"
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import {ENABLE_ZAPS, FORCE_RELAYS, nip57, builder, routing, outbox, user} from "src/app/system"
|
||||
import {ENABLE_ZAPS, FORCE_RELAYS, nip57, builder, routing, user} from "src/app/system"
|
||||
|
||||
export let note
|
||||
export let reply
|
||||
@ -39,13 +39,16 @@
|
||||
const mute = () => user.mute("p", note.pubkey)
|
||||
|
||||
const react = async content => {
|
||||
const relays = routing.getPublishHints(3, note)
|
||||
const relays = routing.getPublishHints(3, note, user.getRelayUrls("write"))
|
||||
|
||||
like = first(await outbox.publish(builder.createReaction(note, content), relays))
|
||||
like = first(await user.publish(builder.createReaction(note, content), relays))
|
||||
}
|
||||
|
||||
const deleteReaction = e => {
|
||||
outbox.publish(builder.deleteEvents([e.id]), routing.getPublishHints(3, note))
|
||||
user.publish(
|
||||
builder.deleteEvents([e.id]),
|
||||
routing.getPublishHints(3, note, user.getRelayUrls("write"))
|
||||
)
|
||||
|
||||
like = null
|
||||
likes = reject(propEq("id", e.id), likes)
|
||||
|
@ -53,7 +53,7 @@
|
||||
|
||||
if (content) {
|
||||
const rawEvent = builder.createReply(note, content, data.mentions.map(builder.mention))
|
||||
const relays = routing.getPublishHints(3, note)
|
||||
const relays = routing.getPublishHints(3, note, user.getRelayUrls("write"))
|
||||
const [event, promise] = await publishWithToast(rawEvent, relays)
|
||||
|
||||
promise.then(({succeeded}) => {
|
||||
|
@ -5,8 +5,7 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonInfo from "src/app/shared/PersonInfo.svelte"
|
||||
import {social, routing, user, network} from "src/app/system"
|
||||
import {pubkeyLoader} from "src/app/state"
|
||||
import {social, routing, user, network, pubkeyLoader} from "src/app/system"
|
||||
|
||||
export let type
|
||||
export let pubkey
|
||||
|
@ -11,17 +11,16 @@ import {hash, timedelta, now, batch, shuffle, sleep, clamp} from "src/util/misc"
|
||||
import {userKinds, noteKinds} from "src/util/nostr"
|
||||
import {findReplyId} from "src/util/nostr"
|
||||
import {modal, toast} from "src/partials/state"
|
||||
import {PubkeyLoader} from "src/system"
|
||||
import system, {
|
||||
import {
|
||||
FORCE_RELAYS,
|
||||
DEFAULT_FOLLOWS,
|
||||
ENABLE_ZAPS,
|
||||
pubkeyLoader,
|
||||
alerts,
|
||||
cache,
|
||||
chat,
|
||||
meta,
|
||||
network,
|
||||
outbox,
|
||||
user,
|
||||
} from "src/app/system"
|
||||
|
||||
@ -86,8 +85,6 @@ export const logUsage = async name => {
|
||||
}
|
||||
}
|
||||
|
||||
export const pubkeyLoader = new PubkeyLoader(system)
|
||||
|
||||
// Synchronization from events to state
|
||||
|
||||
export const listen = async () => {
|
||||
@ -207,7 +204,7 @@ export const mergeParents = (notes: Array<DisplayEvent>) => {
|
||||
}
|
||||
|
||||
export const publishWithToast = (event, relays) =>
|
||||
outbox.publish(event, relays, ({completed, succeeded, failed, timeouts, pending}) => {
|
||||
user.publish(event, relays, ({completed, succeeded, failed, timeouts, pending}) => {
|
||||
let message = `Published to ${succeeded.size}/${relays.length} relays`
|
||||
|
||||
const extra = []
|
||||
|
@ -1,21 +1,55 @@
|
||||
export * from "src/system/env"
|
||||
import {System} from "src/system"
|
||||
import {identity} from "ramda"
|
||||
import {DefaultSystem} from "src/system"
|
||||
|
||||
const system = new System("coracle/system")
|
||||
export const DUFFLEPUD_URL = import.meta.env.VITE_DUFFLEPUD_URL
|
||||
|
||||
export const MULTIPLEXTR_URL = import.meta.env.VITE_MULTIPLEXTR_URL
|
||||
|
||||
export const FORCE_RELAYS = (import.meta.env.VITE_FORCE_RELAYS || "").split(",").filter(identity)
|
||||
|
||||
export const COUNT_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://rbr.bio"]
|
||||
|
||||
export const SEARCH_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://relay.nostr.band"]
|
||||
|
||||
export const DEFAULT_RELAYS =
|
||||
FORCE_RELAYS.length > 0
|
||||
? FORCE_RELAYS
|
||||
: [
|
||||
"wss://purplepag.es",
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relayable.org",
|
||||
"wss://nostr.wine",
|
||||
]
|
||||
|
||||
export const DEFAULT_FOLLOWS = (import.meta.env.VITE_DEFAULT_FOLLOWS || "")
|
||||
.split(",")
|
||||
.filter(identity)
|
||||
|
||||
export const ENABLE_ZAPS = JSON.parse(import.meta.env.VITE_ENABLE_ZAPS)
|
||||
|
||||
const system = new DefaultSystem("coracle/system", {
|
||||
DUFFLEPUD_URL,
|
||||
MULTIPLEXTR_URL,
|
||||
FORCE_RELAYS,
|
||||
COUNT_RELAYS,
|
||||
SEARCH_RELAYS,
|
||||
DEFAULT_RELAYS,
|
||||
})
|
||||
|
||||
export default system
|
||||
export const user = system.user
|
||||
export const sync = system.sync
|
||||
export const social = system.social
|
||||
export const network = system.network
|
||||
export const meta = system.meta
|
||||
export const user = system.user
|
||||
export const cache = system.cache
|
||||
export const content = system.content
|
||||
export const directory = system.directory
|
||||
export const nip05 = system.nip05
|
||||
export const nip57 = system.nip57
|
||||
export const social = system.social
|
||||
export const routing = system.routing
|
||||
export const cache = system.cache
|
||||
export const chat = system.chat
|
||||
export const alerts = system.alerts
|
||||
export const content = system.content
|
||||
export const network = system.network
|
||||
export const chat = system.chat
|
||||
export const builder = system.builder
|
||||
export const outbox = system.outbox
|
||||
export const meta = system.meta
|
||||
export const pubkeyLoader = system.pubkeyLoader
|
||||
|
@ -7,16 +7,15 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import NoteContent from "src/app/shared/NoteContent.svelte"
|
||||
import {builder, chat, user, routing, network, outbox} from "src/app/system"
|
||||
import {watch} from "src/util/loki"
|
||||
import {builder, chat, user, routing, network} from "src/app/system"
|
||||
|
||||
export let entity
|
||||
|
||||
const id = toHex(entity)
|
||||
const channel = watch(chat.channels, () => chat.channels.get(id) || {id})
|
||||
const channel = chat.channels.watch(() => chat.channels.get(id) || {id})
|
||||
const getRelays = () => routing.selectHints(user.getSetting("relay_limit"), $channel.hints || [])
|
||||
|
||||
chat.setLastChecked(id, now())
|
||||
user.setLastChecked(id, now())
|
||||
|
||||
const edit = () => {
|
||||
modal.push({type: "channel/edit", channel: $channel})
|
||||
@ -25,7 +24,7 @@
|
||||
const sendMessage = async content => {
|
||||
const [hint] = getRelays()
|
||||
|
||||
await outbox.publish(builder.createChatMessage(id, content, hint), getRelays())
|
||||
await user.publish(builder.createChatMessage(id, content, hint), getRelays())
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {toast, modal} from "src/partials/state"
|
||||
import {builder, chat, user} from "src/app/system"
|
||||
import {builder, user} from "src/app/system"
|
||||
import {publishWithToast} from "src/app/state"
|
||||
|
||||
export let channel = {name: null, id: null, about: null, picture: null}
|
||||
@ -43,7 +43,7 @@
|
||||
const [event] = await publishWithToast(builder.createChannel(channel), relays)
|
||||
|
||||
// Auto join the room the user just created
|
||||
await chat.joinChannel(event.id)
|
||||
await user.joinChannel(event.id)
|
||||
}
|
||||
|
||||
modal.pop()
|
||||
|
@ -12,9 +12,9 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import {DEFAULT_RELAYS, FORCE_RELAYS, routing, user, network} from "src/app/system"
|
||||
import {DEFAULT_RELAYS, FORCE_RELAYS, routing, user, pubkeyLoader, network} from "src/app/system"
|
||||
import {watch} from "src/util/loki"
|
||||
import {loadAppData, pubkeyLoader} from "src/app/state"
|
||||
import {loadAppData} from "src/app/state"
|
||||
|
||||
let modal = null
|
||||
let customRelayUrl = null
|
||||
|
@ -7,7 +7,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import NoteContent from "src/app/shared/NoteContent.svelte"
|
||||
import {watch} from "src/util/loki"
|
||||
import {user, routing, directory, builder, chat, network, outbox} from "src/app/system"
|
||||
import {user, routing, directory, builder, network} from "src/app/system"
|
||||
import {routes} from "src/app/state"
|
||||
import PersonCircle from "src/app/shared/PersonCircle.svelte"
|
||||
import PersonAbout from "src/app/shared/PersonAbout.svelte"
|
||||
@ -18,7 +18,7 @@
|
||||
const pubkey = toHex(entity)
|
||||
const profile = watch(directory.profiles, () => directory.getProfile(pubkey))
|
||||
|
||||
chat.setLastChecked(pubkey, now())
|
||||
user.setLastChecked(pubkey, now())
|
||||
|
||||
const getRelays = () =>
|
||||
routing.mergeHints(3, [
|
||||
@ -28,10 +28,7 @@
|
||||
|
||||
const sendMessage = async content => {
|
||||
const cyphertext = await user.crypt.encrypt(pubkey, content)
|
||||
const [event] = await outbox.publish(
|
||||
builder.createDirectMessage(pubkey, cyphertext),
|
||||
getRelays()
|
||||
)
|
||||
const [event] = await user.publish(builder.createDirectMessage(pubkey, cyphertext), getRelays())
|
||||
|
||||
// Return unencrypted content so we can display it immediately
|
||||
return {...event, content}
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import {directory, routing, user, network, outbox, builder, nip57} from "src/app/system"
|
||||
import {directory, routing, user, network, builder, nip57} from "src/app/system"
|
||||
|
||||
export let note
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
|
||||
const amount = zap.amount * 1000
|
||||
const zapper = nip57.zappers.get(note.pubkey)
|
||||
const relays = routing.getPublishHints(3, note)
|
||||
const event = await outbox.prep(
|
||||
const relays = routing.getPublishHints(3, note, user.getRelayUrls("write"))
|
||||
const event = await user.prepEvent(
|
||||
builder.requestZap(relays, zap.message, note.pubkey, note.id, amount, zapper.lnurl)
|
||||
)
|
||||
const eventString = encodeURI(JSON.stringify(event))
|
||||
|
@ -9,8 +9,8 @@
|
||||
import OnboardingRelays from "src/app/views/OnboardingRelays.svelte"
|
||||
import OnboardingFollows from "src/app/views/OnboardingFollows.svelte"
|
||||
import OnboardingNote from "src/app/views/OnboardingNote.svelte"
|
||||
import {DEFAULT_FOLLOWS, DEFAULT_RELAYS, builder, user} from "src/app/system"
|
||||
import {loadAppData, pubkeyLoader} from "src/app/state"
|
||||
import {DEFAULT_FOLLOWS, DEFAULT_RELAYS, pubkeyLoader, builder, user} from "src/app/system"
|
||||
import {loadAppData} from "src/app/state"
|
||||
import {modal} from "src/partials/state"
|
||||
|
||||
export let stage
|
||||
|
@ -1,82 +0,0 @@
|
||||
import {sortBy} from "ramda"
|
||||
import type {Writable, Readable} from "svelte/store"
|
||||
import {synced} from "src/util/misc"
|
||||
import {Tags, isLike, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {derived} from "svelte/store"
|
||||
import {Table, watch} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export class Alerts {
|
||||
system: System
|
||||
events: Table<Event>
|
||||
lastChecked: Writable<number>
|
||||
latestNotification: Writable<number>
|
||||
hasNewNotfications: Readable<boolean>
|
||||
hasNewDirectMessages: Readable<boolean>
|
||||
hasNewChatMessages: Readable<boolean>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
this.events = new Table(system.key("alerts/events"), "id", {sort: sortBy(e => -e.created_at)})
|
||||
this.lastChecked = synced(system.key("alerts/lastChecked"), 0)
|
||||
this.latestNotification = synced(system.key("alerts/latestNotification"), 0)
|
||||
|
||||
const isMention = e =>
|
||||
Tags.from(e).type("p").values().all().includes(system.user.getPubkey())
|
||||
const isReply = e => system.user.isUserEvent(findReplyId(e))
|
||||
const isDescendant = e => system.user.isUserEvent(findRootId(e))
|
||||
|
||||
const handleNotification = condition => e => {
|
||||
if (!system.user.getPubkey()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.pubkey === system.user.getPubkey()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!condition(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (system.user.isMuted(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.events.get(e.id)) {
|
||||
this.events.patch(e)
|
||||
}
|
||||
}
|
||||
|
||||
system.sync.addHandler(
|
||||
1,
|
||||
handleNotification(e => isMention(e) || isReply(e) || isDescendant(e))
|
||||
)
|
||||
|
||||
system.sync.addHandler(
|
||||
7,
|
||||
handleNotification(e => isLike(e.content) && isReply(e))
|
||||
)
|
||||
|
||||
system.sync.addHandler(9735, handleNotification(isReply))
|
||||
|
||||
this.hasNewNotfications = derived(
|
||||
[this.lastChecked, this.latestNotification],
|
||||
([$lastChecked, $latestNotification]) => $latestNotification > $lastChecked
|
||||
)
|
||||
|
||||
this.hasNewDirectMessages = watch(system.chat.channels, () => {
|
||||
const channels = system.chat.channels.all({type: "private", last_sent: {$type: "number"}})
|
||||
|
||||
return channels.filter(this.messageIsNew).length > 0
|
||||
})
|
||||
|
||||
this.hasNewChatMessages = watch(system.chat.channels, () => {
|
||||
const channels = system.chat.channels.all({type: "public", joined: true})
|
||||
|
||||
return channels.filter(this.messageIsNew).length > 0
|
||||
})
|
||||
}
|
||||
|
||||
messageIsNew = ({last_checked, last_received, last_sent}) =>
|
||||
last_received > Math.max(last_sent || 0, last_checked || 0)
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import type {Event as NostrToolsEvent} from "nostr-tools"
|
||||
import {sortBy} from "ramda"
|
||||
import {Table} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export type Event = NostrToolsEvent & {
|
||||
seen_on: string[]
|
||||
}
|
||||
|
||||
export class Cache {
|
||||
system: System
|
||||
events: Table<Event>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
this.events = new Table(system.key("cache/events"), "id", {
|
||||
max: 5000,
|
||||
sort: events => {
|
||||
const pubkey = system.user.getPubkey()
|
||||
const follows = system.user.getFollowsSet()
|
||||
|
||||
return sortBy(e => {
|
||||
if (e.pubkey === pubkey) return 0
|
||||
if (follows.has(e.pubkey)) return 1
|
||||
|
||||
return Number.MAX_SAFE_INTEGER - e.created_at
|
||||
}, events)
|
||||
},
|
||||
})
|
||||
|
||||
system.sync.addHandler(system.sync.ANY_KIND, e => {
|
||||
if (e.pubkey === system.user.getPubkey() && !this.events.get(e.id)) {
|
||||
this.events.patch(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,34 +1,37 @@
|
||||
import {Socket} from "paravel"
|
||||
import {now} from "src/util/misc"
|
||||
import {switcher} from "hurdak/lib/hurdak"
|
||||
import type {System} from "src/system/system"
|
||||
import type {RelayStat} from "src/system/types"
|
||||
import type {Network} from "src/system/components/Network"
|
||||
|
||||
export type MetaOpts = {
|
||||
network: Network
|
||||
}
|
||||
|
||||
export class Meta {
|
||||
system: System
|
||||
network: Network
|
||||
relayStats: Record<string, RelayStat>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
constructor({network}: MetaOpts) {
|
||||
this.network = network
|
||||
this.relayStats = {}
|
||||
|
||||
system.network.pool.on("open", ({url}) => {
|
||||
network.pool.on("open", ({url}) => {
|
||||
this.updateRelayStats(url, {last_opened: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
system.network.pool.on("close", ({url}) => {
|
||||
network.pool.on("close", ({url}) => {
|
||||
this.updateRelayStats(url, {last_closed: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
system.network.pool.on("error:set", (url, error) => {
|
||||
network.pool.on("error:set", (url, error) => {
|
||||
this.updateRelayStats(url, {error})
|
||||
})
|
||||
|
||||
system.network.pool.on("error:clear", url => {
|
||||
network.pool.on("error:clear", url => {
|
||||
this.updateRelayStats(url, {error: null})
|
||||
})
|
||||
|
||||
system.network.on("publish", urls => {
|
||||
network.on("publish", urls => {
|
||||
for (const url of urls) {
|
||||
this.updateRelayStats(url, {
|
||||
last_publish: now(),
|
||||
@ -37,7 +40,7 @@ export class Meta {
|
||||
}
|
||||
})
|
||||
|
||||
system.network.on("sub:open", urls => {
|
||||
network.on("sub:open", urls => {
|
||||
for (const url of urls) {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
@ -50,7 +53,7 @@ export class Meta {
|
||||
}
|
||||
})
|
||||
|
||||
system.network.on("sub:close", urls => {
|
||||
network.on("sub:close", urls => {
|
||||
for (const url of urls) {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
@ -61,7 +64,7 @@ export class Meta {
|
||||
}
|
||||
})
|
||||
|
||||
system.network.on("event", ({url}) => {
|
||||
network.on("event", ({url}) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
@ -70,7 +73,7 @@ export class Meta {
|
||||
})
|
||||
})
|
||||
|
||||
system.network.on("eose", (url, ms) => {
|
||||
network.on("eose", (url, ms) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
@ -80,7 +83,7 @@ export class Meta {
|
||||
})
|
||||
})
|
||||
|
||||
system.network.on("timeout", (url, ms) => {
|
||||
network.on("timeout", (url, ms) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
this.updateRelayStats(url, {
|
||||
@ -132,7 +135,7 @@ export class Meta {
|
||||
return [eoseQuality, "Connected"]
|
||||
}
|
||||
|
||||
if (this.system.network.pool.get(url).status === Socket.STATUS.READY) {
|
||||
if (this.network.pool.get(url).status === Socket.STATUS.READY) {
|
||||
return [1, "Connected"]
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import type {Filter} from "nostr-tools"
|
||||
import type {MyEvent} from "src/util/types"
|
||||
import {EventEmitter} from "events"
|
||||
import {verifySignature, matchFilters} from "nostr-tools"
|
||||
import {Pool, Plex, Relays, Executor, Socket} from "paravel"
|
||||
@ -7,13 +5,20 @@ 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 {FORCE_RELAYS, COUNT_RELAYS} from "src/system/env"
|
||||
import type {System} from "src/system/system"
|
||||
import type {Event, Filter, RelayInfo} from "src/system/types"
|
||||
|
||||
type SubscribeOpts = {
|
||||
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: MyEvent) => void
|
||||
onEvent?: (event: Event) => void
|
||||
onEose?: (url: string) => void
|
||||
shouldProcess?: boolean
|
||||
}
|
||||
@ -34,23 +39,21 @@ const getUrls = relays => {
|
||||
|
||||
export class Network extends EventEmitter {
|
||||
authHandler?: (url: string, challenge: string) => void
|
||||
system: System
|
||||
pool: Pool
|
||||
constructor(system) {
|
||||
constructor(readonly opts: NetworkOpts) {
|
||||
super()
|
||||
|
||||
this.authHandler = null
|
||||
this.system = system
|
||||
this.pool = new Pool()
|
||||
}
|
||||
getExecutor = (urls, {bypassBoot = false} = {}) => {
|
||||
if (FORCE_RELAYS.length > 0) {
|
||||
urls = FORCE_RELAYS
|
||||
if (this.opts.forceRelays.length > 0) {
|
||||
urls = this.opts.forceRelays
|
||||
}
|
||||
|
||||
let target
|
||||
|
||||
const muxUrl = this.system.user.getSetting("multiplextr_url")
|
||||
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
|
||||
@ -89,7 +92,7 @@ export class Network extends EventEmitter {
|
||||
|
||||
// Eagerly connect and handle AUTH
|
||||
executor.target.sockets.forEach(socket => {
|
||||
const {limitation} = this.system.routing.getRelayInfo(socket.url)
|
||||
const {limitation} = this.opts.getRelayInfo(socket.url)
|
||||
const waitForBoot = limitation?.payment_required || limitation?.auth_required
|
||||
|
||||
// This happens automatically, but kick it off anyway
|
||||
@ -219,7 +222,7 @@ export class Network extends EventEmitter {
|
||||
this.emit("event", {url, event})
|
||||
|
||||
if (shouldProcess) {
|
||||
this.system.sync.processEvents([event])
|
||||
this.opts.processEvents([event])
|
||||
}
|
||||
|
||||
onEvent(event)
|
||||
@ -262,7 +265,7 @@ export class Network extends EventEmitter {
|
||||
}: {
|
||||
relays: string[]
|
||||
filter: Filter | Filter[]
|
||||
onEvent?: (event: MyEvent) => void
|
||||
onEvent?: (event: Event) => void
|
||||
shouldProcess?: boolean
|
||||
timeout?: number
|
||||
}) => {
|
||||
@ -310,11 +313,11 @@ export class Network extends EventEmitter {
|
||||
attemptToComplete(false)
|
||||
},
|
||||
})
|
||||
}) as Promise<MyEvent[]>
|
||||
}) as Promise<Event[]>
|
||||
}
|
||||
count = async filter => {
|
||||
const filters = ensurePlural(filter)
|
||||
const executor = this.getExecutor(COUNT_RELAYS)
|
||||
const executor = this.getExecutor(this.opts.countRelays)
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = executor.count(filters, {
|
70
src/system/components/Sync.ts
Normal file
70
src/system/components/Sync.ts
Normal file
@ -0,0 +1,70 @@
|
||||
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,12 +1,30 @@
|
||||
import {nip19, nip04, getPublicKey, getSignature, generatePrivateKey} from "nostr-tools"
|
||||
import {
|
||||
nip19,
|
||||
nip04,
|
||||
getPublicKey,
|
||||
getSignature,
|
||||
getEventHash,
|
||||
generatePrivateKey,
|
||||
} from "nostr-tools"
|
||||
import NDK, {NDKEvent, NDKNip46Signer, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"
|
||||
import {when, whereEq, find, slice, reject, prop} from "ramda"
|
||||
import {switcherFn} from "hurdak/lib/hurdak"
|
||||
import {
|
||||
when,
|
||||
uniq,
|
||||
pluck,
|
||||
without,
|
||||
fromPairs,
|
||||
whereEq,
|
||||
find,
|
||||
slice,
|
||||
assoc,
|
||||
reject,
|
||||
prop,
|
||||
} from "ramda"
|
||||
import {switcherFn, doPipe} from "hurdak/lib/hurdak"
|
||||
import {derived} from "svelte/store"
|
||||
import {synced, now, tryJson, tryFunc, sleep, getter} from "src/util/misc"
|
||||
import {now, tryJson, tryFunc, sleep, getter} from "src/util/misc"
|
||||
import {Tags, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {DUFFLEPUD_URL, MULTIPLEXTR_URL} from "src/system/env"
|
||||
import type {System} from "src/system/system"
|
||||
import type {System} from "src/system/System"
|
||||
import type {UserSettings, KeyState} from "src/system/types"
|
||||
import type {Writable, Readable} from "svelte/store"
|
||||
|
||||
@ -25,13 +43,15 @@ export class Crypt {
|
||||
constructor(keys) {
|
||||
this.keys = keys
|
||||
}
|
||||
|
||||
async encrypt(pubkey, message) {
|
||||
const {method, privkey} = this.keys.getState()
|
||||
|
||||
return switcherFn(method, {
|
||||
extension: extension => withExtensionLock(() => {
|
||||
return getExtension().nip04.encrypt(pubkey, message)
|
||||
}),
|
||||
extension: extension =>
|
||||
withExtensionLock(() => {
|
||||
return getExtension().nip04.encrypt(pubkey, message)
|
||||
}),
|
||||
privkey: extension => nip04.encrypt(privkey, pubkey, message),
|
||||
bunker: async () => {
|
||||
const ndk = await this.keys.getNDK()
|
||||
@ -41,28 +61,30 @@ export class Crypt {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async decrypt(pubkey, message) {
|
||||
const {method, privkey} = this.keys.getState()
|
||||
|
||||
return switcherFn(method, {
|
||||
extension: () => withExtensionLock(() => {
|
||||
return new Promise(async resolve => {
|
||||
let result
|
||||
extension: () =>
|
||||
withExtensionLock(() => {
|
||||
return new Promise(async resolve => {
|
||||
let result
|
||||
|
||||
// Alby gives us a bunch of bogus errors, try multiple times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
result = await tryFunc(() => getExtension().nip04.decrypt(pubkey, message))
|
||||
// Alby gives us a bunch of bogus errors, try multiple times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
result = await tryFunc(() => getExtension().nip04.decrypt(pubkey, message))
|
||||
|
||||
if (result) {
|
||||
break
|
||||
if (result) {
|
||||
break
|
||||
}
|
||||
|
||||
await sleep(30)
|
||||
}
|
||||
|
||||
await sleep(30)
|
||||
}
|
||||
|
||||
resolve(result || `<Failed to decrypt message>`)
|
||||
})
|
||||
}),
|
||||
resolve(result || `<Failed to decrypt message>`)
|
||||
})
|
||||
}),
|
||||
privkey: () => {
|
||||
return (
|
||||
tryFunc(() => nip04.decrypt(privkey, pubkey, message)) || `<Failed to decrypt message>`
|
||||
@ -76,11 +98,13 @@ export class Crypt {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async encryptJson(data) {
|
||||
const {pubkey} = this.keys.getState()
|
||||
|
||||
return this.encrypt(pubkey, JSON.stringify(data))
|
||||
}
|
||||
|
||||
async decryptJson(data) {
|
||||
const {pubkey} = this.keys.getState()
|
||||
|
||||
@ -94,8 +118,9 @@ export class Keys {
|
||||
pubkey: Readable<string>
|
||||
canSign: Readable<boolean>
|
||||
ndk: NDK
|
||||
constructor(user) {
|
||||
this.keyState = synced(user.system.key("keys"), {})
|
||||
|
||||
constructor(sync) {
|
||||
this.keyState = sync.store("keys", {})
|
||||
this.getState = getter(this.keyState)
|
||||
this.pubkey = derived(this.keyState, prop("pubkey"))
|
||||
this.canSign = derived(this.keyState, ({method}) =>
|
||||
@ -103,6 +128,9 @@ export class Keys {
|
||||
)
|
||||
this.ndk = null
|
||||
}
|
||||
|
||||
getPubkey = () => this.getState().pubkey
|
||||
|
||||
isKeyValid = key => {
|
||||
// Validate the key before setting it to state by encoding it using bech32.
|
||||
// This will error if invalid (this works whether it's a public or a private key)
|
||||
@ -114,6 +142,7 @@ export class Keys {
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
prepareNdk = async (token?: string) => {
|
||||
const {pubkey, bunkerKey} = this.getState()
|
||||
const localSigner = new NDKPrivateKeySigner(bunkerKey)
|
||||
@ -131,6 +160,7 @@ export class Keys {
|
||||
await this.ndk.connect(5000)
|
||||
await this.ndk.signer.blockUntilReady()
|
||||
}
|
||||
|
||||
getNDK = async () => {
|
||||
if (!this.ndk) {
|
||||
await this.prepareNdk()
|
||||
@ -138,6 +168,7 @@ export class Keys {
|
||||
|
||||
return this.ndk
|
||||
}
|
||||
|
||||
login = (method, key) => {
|
||||
this.keyState.update($state => {
|
||||
let pubkey = null
|
||||
@ -159,9 +190,11 @@ export class Keys {
|
||||
return {method, pubkey, privkey, bunkerKey}
|
||||
})
|
||||
}
|
||||
|
||||
clear = () => {
|
||||
this.keyState.set({})
|
||||
}
|
||||
|
||||
sign = async event => {
|
||||
const {method, privkey} = this.getState()
|
||||
|
||||
@ -182,9 +215,10 @@ export class Keys {
|
||||
sig: getSignature(event, privkey),
|
||||
})
|
||||
},
|
||||
extension: () => withExtensionLock(() => {
|
||||
return getExtension().signEvent(event)
|
||||
}),
|
||||
extension: () =>
|
||||
withExtensionLock(() => {
|
||||
return getExtension().signEvent(event)
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -196,20 +230,21 @@ export class User {
|
||||
canSign: () => boolean
|
||||
settings: Writable<UserSettings>
|
||||
getSettings: () => UserSettings
|
||||
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
this.keys = new Keys(this)
|
||||
this.keys = new Keys(system.sync)
|
||||
this.crypt = new Crypt(this.keys)
|
||||
this.canSign = getter(this.keys.canSign)
|
||||
|
||||
this.settings = synced(system.key("settings/settings"), {
|
||||
this.settings = system.sync.store("settings/settings", {
|
||||
last_updated: 0,
|
||||
relay_limit: 10,
|
||||
default_zap: 21,
|
||||
show_media: true,
|
||||
report_analytics: true,
|
||||
dufflepud_url: DUFFLEPUD_URL,
|
||||
multiplextr_url: MULTIPLEXTR_URL,
|
||||
dufflepud_url: system.env.DUFFLEPUD_URL,
|
||||
multiplextr_url: system.env.MULTIPLEXTR_URL,
|
||||
})
|
||||
|
||||
this.getSettings = getter(this.settings)
|
||||
@ -236,7 +271,34 @@ export class User {
|
||||
|
||||
getStateKey = () => (this.canSign() ? this.getPubkey() : "anonymous")
|
||||
|
||||
publish = event => this.system.outbox.publish(event, this.getRelayUrls("write"))
|
||||
// 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
|
||||
|
||||
@ -366,4 +428,33 @@ export class User {
|
||||
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,28 +0,0 @@
|
||||
import {identity} from "ramda"
|
||||
|
||||
export const DUFFLEPUD_URL = import.meta.env.VITE_DUFFLEPUD_URL
|
||||
|
||||
export const MULTIPLEXTR_URL = import.meta.env.VITE_MULTIPLEXTR_URL
|
||||
|
||||
export const FORCE_RELAYS = (import.meta.env.VITE_FORCE_RELAYS || "").split(",").filter(identity)
|
||||
|
||||
export const COUNT_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://rbr.bio"]
|
||||
|
||||
export const SEARCH_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://relay.nostr.band"]
|
||||
|
||||
export const DEFAULT_RELAYS =
|
||||
FORCE_RELAYS.length > 0
|
||||
? FORCE_RELAYS
|
||||
: [
|
||||
"wss://purplepag.es",
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relayable.org",
|
||||
"wss://nostr.wine",
|
||||
]
|
||||
|
||||
export const DEFAULT_FOLLOWS = (import.meta.env.VITE_DEFAULT_FOLLOWS || "")
|
||||
.split(",")
|
||||
.filter(identity)
|
||||
|
||||
export const ENABLE_ZAPS = JSON.parse(import.meta.env.VITE_ENABLE_ZAPS)
|
@ -1,17 +1,17 @@
|
||||
export {Sync} from "src/system/sync"
|
||||
export {Social} from "src/system/social"
|
||||
export {Directory} from "src/system/directory"
|
||||
export {Nip05} from "src/system/nip05"
|
||||
export {Nip57} from "src/system/nip57"
|
||||
export {Content} from "src/system/content"
|
||||
export {Routing} from "src/system/routing"
|
||||
export {Cache} from "src/system/cache"
|
||||
export {Chat} from "src/system/chat"
|
||||
export {Alerts} from "src/system/alerts"
|
||||
export {Network} from "src/system/network"
|
||||
export {Outbox} from "src/system/outbox"
|
||||
export {Builder} from "src/system/builder"
|
||||
export {Meta} from "src/system/meta"
|
||||
export {Crypt, Keys, User} from "src/system/user"
|
||||
export {System} from "src/system/system"
|
||||
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,35 +0,0 @@
|
||||
import {assoc} from "ramda"
|
||||
import {getEventHash} from "nostr-tools"
|
||||
import {doPipe} from "hurdak/lib/hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export class Outbox {
|
||||
system: System
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
}
|
||||
|
||||
async prep(rawEvent) {
|
||||
return doPipe(rawEvent, [
|
||||
assoc("created_at", now()),
|
||||
assoc("pubkey", this.system.user.getPubkey()),
|
||||
e => ({...e, id: getEventHash(e)}),
|
||||
this.system.user.keys.sign,
|
||||
])
|
||||
}
|
||||
|
||||
async publish(event, relays, onProgress = null, verb = "EVENT") {
|
||||
if (!event.sig) {
|
||||
event = await this.prep(event)
|
||||
}
|
||||
|
||||
// return console.log(event)
|
||||
|
||||
const promise = this.system.network.publish({relays, event, onProgress, verb})
|
||||
|
||||
this.system.sync.processEvents(event)
|
||||
|
||||
return [event, promise]
|
||||
}
|
||||
}
|
64
src/system/stores/Alerts.ts
Normal file
64
src/system/stores/Alerts.ts
Normal file
@ -0,0 +1,64 @@
|
||||
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))
|
||||
}
|
||||
}
|
28
src/system/stores/Cache.ts
Normal file
28
src/system/stores/Cache.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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,40 +1,29 @@
|
||||
import {last, sortBy, pick, uniq, fromPairs, pluck, without} from "ramda"
|
||||
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 {Table} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Readable} from "svelte/store"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
import type {Crypt} from "src/system/components/User"
|
||||
import type {Channel, Message} from "src/system/types"
|
||||
|
||||
const getHints = e => pluck("url", Tags.from(e).relays())
|
||||
|
||||
export type Channel = {
|
||||
id: string
|
||||
type: "public" | "private"
|
||||
pubkey: string
|
||||
updated_at: number
|
||||
last_sent?: number
|
||||
last_received?: number
|
||||
last_checked?: number
|
||||
joined?: boolean
|
||||
hints: string[]
|
||||
}
|
||||
const messageIsNew = ({last_checked, last_received, last_sent}: Channel) =>
|
||||
last_received > Math.max(last_sent || 0, last_checked || 0)
|
||||
|
||||
export type Message = {
|
||||
id: string
|
||||
channel: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
content: string
|
||||
tags: string[][]
|
||||
export type ChatOpts = {
|
||||
getCrypt: () => Crypt
|
||||
}
|
||||
|
||||
export class Chat {
|
||||
system: System
|
||||
channels: Table<Channel>
|
||||
messages: Table<Message>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
this.channels = new Table(system.key("chat/channels"), "id", {
|
||||
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
|
||||
@ -43,7 +32,7 @@ export class Chat {
|
||||
}),
|
||||
})
|
||||
|
||||
this.messages = new Table(system.key("chat/messages"), "id", {
|
||||
this.messages = sync.table("chat/messages", "id", {
|
||||
sort: xs => {
|
||||
const channelIds = new Set(
|
||||
pluck("id", this.channels.all({$or: [{joined: true}, {type: "private"}]}))
|
||||
@ -53,7 +42,19 @@ export class Chat {
|
||||
},
|
||||
})
|
||||
|
||||
system.sync.addHandler(40, e => {
|
||||
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) {
|
||||
@ -76,7 +77,7 @@ export class Chat {
|
||||
})
|
||||
})
|
||||
|
||||
system.sync.addHandler(41, e => {
|
||||
sync.addHandler(41, e => {
|
||||
const channelId = Tags.from(e).getMeta("e")
|
||||
|
||||
if (!channelId) {
|
||||
@ -109,10 +110,10 @@ export class Chat {
|
||||
})
|
||||
})
|
||||
|
||||
system.sync.addHandler(30078, async e => {
|
||||
sync.addHandler(30078, async e => {
|
||||
if (Tags.from(e).getMeta("d") === "coracle/last_checked/v1") {
|
||||
await tryJson(async () => {
|
||||
const payload = await this.system.user.crypt.decryptJson(e.content)
|
||||
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
|
||||
@ -131,10 +132,10 @@ export class Chat {
|
||||
}
|
||||
})
|
||||
|
||||
system.sync.addHandler(30078, async e => {
|
||||
sync.addHandler(30078, async e => {
|
||||
if (Tags.from(e).getMeta("d") === "coracle/rooms_joined/v1") {
|
||||
await tryJson(async () => {
|
||||
const channelIds = await this.system.user.crypt.decryptJson(e.content)
|
||||
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)) {
|
||||
@ -152,15 +153,15 @@ export class Chat {
|
||||
}
|
||||
})
|
||||
|
||||
system.sync.addHandler(4, async e => {
|
||||
if (!this.system.user.canSign()) {
|
||||
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.system.user.getPubkey())) {
|
||||
if (![author, recipient].includes(this.opts.getCrypt().keys.getPubkey())) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -169,18 +170,18 @@ export class Chat {
|
||||
}
|
||||
|
||||
await tryFunc(async () => {
|
||||
const other = this.system.user.getPubkey() === author ? recipient : author
|
||||
const other = this.opts.getCrypt().keys.getPubkey() === author ? recipient : author
|
||||
|
||||
this.messages.patch({
|
||||
id: e.id,
|
||||
channel: other,
|
||||
pubkey: e.pubkey,
|
||||
created_at: e.created_at,
|
||||
content: await this.system.user.crypt.decrypt(other, e.content),
|
||||
content: await this.opts.getCrypt().decrypt(other, e.content),
|
||||
tags: e.tags,
|
||||
})
|
||||
|
||||
if (this.system.user.getPubkey() === author) {
|
||||
if (this.opts.getCrypt().keys.getPubkey() === author) {
|
||||
const channel = this.channels.get(recipient)
|
||||
|
||||
this.channels.patch({
|
||||
@ -202,7 +203,7 @@ export class Chat {
|
||||
})
|
||||
})
|
||||
|
||||
system.sync.addHandler(42, e => {
|
||||
sync.addHandler(42, e => {
|
||||
if (this.messages.get(e.id)) {
|
||||
return
|
||||
}
|
||||
@ -229,24 +230,4 @@ export class Chat {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setLastChecked = (channelId, timestamp) => {
|
||||
const lastChecked = fromPairs(
|
||||
this.channels.all({last_checked: {$type: "number"}}).map(r => [r.id, r.last_checked])
|
||||
)
|
||||
|
||||
return this.system.user.setAppData("last_checked/v1", {...lastChecked, [channelId]: timestamp})
|
||||
}
|
||||
|
||||
joinChannel = channelId => {
|
||||
const channelIds = uniq(pluck("id", this.channels.all({joined: true})).concat(channelId))
|
||||
|
||||
return this.system.user.setAppData("rooms_joined/v1", channelIds)
|
||||
}
|
||||
|
||||
leaveChannel = channelId => {
|
||||
const channelIds = without([channelId], pluck("id", this.channels.all({joined: true})))
|
||||
|
||||
return this.system.user.setAppData("rooms_joined/v1", channelIds)
|
||||
}
|
||||
}
|
@ -3,8 +3,8 @@ 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 {Table, watch} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
import type {Table} from "src/util/loki"
|
||||
import type {Sync} from "src/system/components/Sync"
|
||||
|
||||
export type Topic = {
|
||||
name: string
|
||||
@ -22,18 +22,15 @@ export type List = {
|
||||
}
|
||||
|
||||
export class Content {
|
||||
system: System
|
||||
topics: Table<Topic>
|
||||
lists: Table<List>
|
||||
searchTopics: Readable<(query: string) => Topic[]>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
constructor(sync: Sync) {
|
||||
this.topics = sync.table("content/topics", "name", {sort: sortBy(e => -e.count)})
|
||||
|
||||
this.topics = new Table(system.key("content/topics"), "name", {sort: sortBy(e => -e.count)})
|
||||
this.lists = sync.table("content/lists", "naddr")
|
||||
|
||||
this.lists = new Table(system.key("content/lists"), "naddr")
|
||||
|
||||
this.searchTopics = watch(this.topics, () =>
|
||||
this.searchTopics = this.topics.watch(() =>
|
||||
fuzzy(this.topics.all(), {keys: ["name"], threshold: 0.3})
|
||||
)
|
||||
|
||||
@ -50,10 +47,10 @@ export class Content {
|
||||
}
|
||||
}
|
||||
|
||||
system.sync.addHandler(1, processTopics)
|
||||
system.sync.addHandler(42, processTopics)
|
||||
sync.addHandler(1, processTopics)
|
||||
sync.addHandler(42, processTopics)
|
||||
|
||||
system.sync.addHandler(30001, e => {
|
||||
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})
|
||||
@ -75,7 +72,7 @@ export class Content {
|
||||
})
|
||||
})
|
||||
|
||||
system.sync.addHandler(5, e => {
|
||||
sync.addHandler(5, e => {
|
||||
Tags.from(e)
|
||||
.type("a")
|
||||
.values()
|
@ -1,31 +1,28 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ellipsize} from "hurdak/lib/hurdak"
|
||||
import {tryJson, now, fuzzy} from "src/util/misc"
|
||||
import {Table, watch} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
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 {
|
||||
system: System
|
||||
profiles: Table<Profile>
|
||||
searchProfiles: Readable<(q: string) => Record<string, any>[]>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
this.profiles = new Table(this.system.key("directory/profiles"), "pubkey", {
|
||||
constructor(sync: Sync) {
|
||||
this.profiles = sync.table("directory/profiles", "pubkey", {
|
||||
max: 5000,
|
||||
sort: this.system.sortByGraph,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
this.searchProfiles = watch(this.profiles, () => {
|
||||
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,
|
||||
})
|
||||
})
|
||||
|
||||
this.system.sync.addHandler(0, e => {
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(() => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const profile = this.profiles.get(e.pubkey)
|
@ -1,29 +1,19 @@
|
||||
import {last} from "ramda"
|
||||
import {nip05} from "nostr-tools"
|
||||
import {tryFunc, now, tryJson} from "src/util/misc"
|
||||
import {Table} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export type Handle = {
|
||||
profile: Record<string, any>
|
||||
pubkey: string
|
||||
address: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
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 {
|
||||
system: System
|
||||
handles: Table<Handle>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
this.handles = new Table(system.key("nip05/handles"), "pubkey", {
|
||||
sort: system.sortByGraph,
|
||||
constructor(sync: Sync) {
|
||||
this.handles = sync.table("nip05/handles", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
system.sync.addHandler(0, e => {
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const handle = this.handles.get(e.pubkey)
|
@ -1,8 +1,9 @@
|
||||
import {fetchJson, now, tryFunc, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
||||
import {invoiceAmount} from "src/util/lightning"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import {Table} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
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
|
||||
@ -20,29 +21,15 @@ const getLnUrl = address => {
|
||||
}
|
||||
}
|
||||
|
||||
export type Zapper = {
|
||||
pubkey: string
|
||||
lnurl: string
|
||||
callback: string
|
||||
minSendable: number
|
||||
maxSendable: number
|
||||
nostrPubkey: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export class Nip57 {
|
||||
system: System
|
||||
zappers: Table<Zapper>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
|
||||
this.zappers = new Table(system.key("niip57/zappers"), "pubkey", {
|
||||
sort: system.sortByGraph,
|
||||
constructor(sync: Sync) {
|
||||
this.zappers = sync.table("niip57/zappers", "pubkey", {
|
||||
max: 5000,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
system.sync.addHandler(0, e => {
|
||||
sync.addHandler(0, e => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const zapper = this.zappers.get(e.pubkey)
|
@ -1,35 +1,35 @@
|
||||
import {sortBy, pluck, uniq, nth, uniqBy, prop, last, inc} from "ramda"
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
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 {DUFFLEPUD_URL, DEFAULT_RELAYS, SEARCH_RELAYS, FORCE_RELAYS} from "src/system/env"
|
||||
import {Table, watch} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
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 {
|
||||
system: System
|
||||
sync: Sync
|
||||
relays: Table<Relay>
|
||||
policies: Table<RelayPolicy>
|
||||
searchRelays: Readable<(query: string) => Relay[]>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
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"]}))
|
||||
|
||||
this.relays = new Table(system.key("routing/relays"), "url", {sort: sortBy(e => -e.count)})
|
||||
|
||||
this.policies = new Table(system.key("routing/policies"), "pubkey", {sort: system.sortByGraph})
|
||||
|
||||
this.searchRelays = watch(this.relays, () => fuzzy(this.relays.all(), {keys: ["url"]}))
|
||||
|
||||
system.sync.addHandler(2, e => {
|
||||
sync.addHandler(2, e => {
|
||||
if (isShareableRelay(e.content)) {
|
||||
this.addRelay(normalizeRelayUrl(e.content))
|
||||
}
|
||||
})
|
||||
|
||||
system.sync.addHandler(3, e => {
|
||||
sync.addHandler(3, e => {
|
||||
this.setPolicy(
|
||||
e,
|
||||
tryJson(() => {
|
||||
@ -47,7 +47,7 @@ export class Routing {
|
||||
)
|
||||
})
|
||||
|
||||
system.sync.addHandler(10002, e => {
|
||||
sync.addHandler(10002, e => {
|
||||
this.setPolicy(
|
||||
e,
|
||||
Tags.from(e)
|
||||
@ -66,11 +66,13 @@ export class Routing {
|
||||
)
|
||||
})
|
||||
;(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) {
|
||||
if (FORCE_RELAYS.length === 0 && DUFFLEPUD_URL) {
|
||||
try {
|
||||
const json = await fetchJson(DUFFLEPUD_URL + "/relay")
|
||||
|
||||
@ -126,7 +128,7 @@ export class Routing {
|
||||
this.relays.all({"info.supported_nips": {$contains: 50}})
|
||||
)
|
||||
|
||||
return uniq(SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8)
|
||||
return uniq(this.sync.env.SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8)
|
||||
}
|
||||
|
||||
getPubkeyRelays = (pubkey, mode = null) => {
|
||||
@ -156,7 +158,7 @@ export class Routing {
|
||||
const ok = []
|
||||
const bad = []
|
||||
|
||||
for (const url of chain(hints, this.system.user.getRelayUrls(), DEFAULT_RELAYS)) {
|
||||
for (const url of chain(hints, this.opts.getDefaultRelays())) {
|
||||
if (seen.has(url)) {
|
||||
continue
|
||||
}
|
||||
@ -166,9 +168,7 @@ export class Routing {
|
||||
// Filter out relays that appear to be broken or slow
|
||||
if (!isShareableRelay(url)) {
|
||||
bad.push(url)
|
||||
} else if (this.system.network.pool.get(url, {autoConnect: false})?.error) {
|
||||
bad.push(url)
|
||||
} else if (first(this.system.meta.getRelayQuality(url)) < 0.5) {
|
||||
} else if (this.opts.relayHasError(url)) {
|
||||
bad.push(url)
|
||||
} else {
|
||||
ok.push(url)
|
||||
@ -191,8 +191,8 @@ export class Routing {
|
||||
getPubkeyHints = this.hintSelector(function* (pubkey, mode = "write") {
|
||||
const other = mode === "write" ? "read" : "write"
|
||||
|
||||
yield* this.system.routing.getPubkeyRelayUrls(pubkey, mode)
|
||||
yield* this.system.routing.getPubkeyRelayUrls(pubkey, other)
|
||||
yield* this.getPubkeyRelayUrls(pubkey, mode)
|
||||
yield* this.getPubkeyRelayUrls(pubkey, other)
|
||||
})
|
||||
|
||||
getEventHints = this.hintSelector(function* (event) {
|
||||
@ -205,9 +205,9 @@ export class Routing {
|
||||
// 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.system.routing.getPubkeyRelayUrls(event.pubkey, "write")
|
||||
yield* this.getPubkeyRelayUrls(event.pubkey, "write")
|
||||
yield* event.seen_on || []
|
||||
yield* this.system.routing.getPubkeyRelayUrls(event.pubkey, "read")
|
||||
yield* this.getPubkeyRelayUrls(event.pubkey, "read")
|
||||
})
|
||||
|
||||
// If we're looking for an event's parent, tags are the most reliable hint,
|
||||
@ -224,17 +224,12 @@ export class Routing {
|
||||
// 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) => {
|
||||
getPublishHints = (limit, event, extraRelays = []) => {
|
||||
const tags = Tags.from(event)
|
||||
const pubkeys = tags.type("p").values().all().concat(event.pubkey)
|
||||
const hints = this.mergeHints(
|
||||
limit,
|
||||
pubkeys.map(pubkey => this.getPubkeyHints(3, pubkey, "read"))
|
||||
)
|
||||
const hintGroups = pubkeys.map(pubkey => this.getPubkeyHints(3, pubkey, "read"))
|
||||
|
||||
return uniq(
|
||||
hints.concat(this.system.routing.getPubkeyRelayUrls(this.system.user.getStateKey(), "write"))
|
||||
)
|
||||
return this.mergeHints(limit, hintGroups.concat([extraRelays]))
|
||||
}
|
||||
|
||||
mergeHints = (limit, groups) => {
|
@ -1,22 +1,22 @@
|
||||
import {ensurePlural} from "hurdak/lib/hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import {Table} from "src/util/loki"
|
||||
import type {System} from "src/system/system"
|
||||
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 {
|
||||
system: System
|
||||
sync: Sync
|
||||
graph: Table<GraphEntry>
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
constructor(sync) {
|
||||
this.sync = sync
|
||||
|
||||
this.graph = new Table(this.system.key("social/graph"), "pubkey", {
|
||||
this.graph = sync.table("social/graph", "pubkey", {
|
||||
max: 5000,
|
||||
sort: this.system.sortByGraph,
|
||||
sort: sync.sortByPubkeyWhitelist,
|
||||
})
|
||||
|
||||
this.system.sync.addHandler(3, e => {
|
||||
sync.addHandler(3, e => {
|
||||
const entry = this.graph.get(e.pubkey)
|
||||
|
||||
if (e.created_at < entry?.petnames_updated_at) {
|
||||
@ -31,7 +31,7 @@ export class Social {
|
||||
})
|
||||
})
|
||||
|
||||
this.system.sync.addHandler(10000, e => {
|
||||
sync.addHandler(10000, e => {
|
||||
const entry = this.graph.get(e.pubkey)
|
||||
|
||||
if (e.created_at < entry?.mutes_updated_at) {
|
@ -1,41 +0,0 @@
|
||||
import {identity} from "ramda"
|
||||
import {ensurePlural, chunk} from "hurdak/lib/hurdak"
|
||||
import {sleep} from "src/util/misc"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export class Sync {
|
||||
handlers = {}
|
||||
system: System
|
||||
ANY_KIND: string
|
||||
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
this.ANY_KIND = this.system.key("ANY_KIND")
|
||||
}
|
||||
|
||||
addHandler(kind, f) {
|
||||
this.handlers[kind] = this.handlers[kind] || []
|
||||
this.handlers[kind].push(f)
|
||||
}
|
||||
|
||||
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,71 +1,181 @@
|
||||
import {prop, nth, sortBy} from "ramda"
|
||||
import {User} from "src/system/user"
|
||||
import {Sync} from "src/system/sync"
|
||||
import {Social} from "src/system/social"
|
||||
import {Directory} from "src/system/directory"
|
||||
import {Nip05} from "src/system/nip05"
|
||||
import {Nip57} from "src/system/nip57"
|
||||
import {Content} from "src/system/content"
|
||||
import {Routing} from "src/system/routing"
|
||||
import {Cache} from "src/system/cache"
|
||||
import {Chat} from "src/system/chat"
|
||||
import {Alerts} from "src/system/alerts"
|
||||
import {Network} from "src/system/network"
|
||||
import {Outbox} from "src/system/outbox"
|
||||
import {Builder} from "src/system/builder"
|
||||
import {Meta} from "src/system/meta"
|
||||
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 class System {
|
||||
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
|
||||
user: User
|
||||
env: SystemEnv
|
||||
sync: Sync
|
||||
social: Social
|
||||
network: Network
|
||||
meta: Meta
|
||||
user: User
|
||||
cache: Cache
|
||||
content: Content
|
||||
directory: Directory
|
||||
nip05: Nip05
|
||||
nip57: Nip57
|
||||
social: Social
|
||||
routing: Routing
|
||||
cache: Cache
|
||||
chat: Chat
|
||||
alerts: Alerts
|
||||
content: Content
|
||||
network: Network
|
||||
chat: Chat
|
||||
builder: Builder
|
||||
outbox: Outbox
|
||||
meta: Meta
|
||||
|
||||
constructor(ns) {
|
||||
this.ns = ns
|
||||
this.sync = new Sync(this)
|
||||
this.user = new User(this)
|
||||
this.social = new Social(this)
|
||||
this.directory = new Directory(this)
|
||||
this.nip05 = new Nip05(this)
|
||||
this.nip57 = new Nip57(this)
|
||||
this.routing = new Routing(this)
|
||||
this.cache = new Cache(this)
|
||||
this.chat = new Chat(this)
|
||||
this.alerts = new Alerts(this)
|
||||
this.content = new Content(this)
|
||||
this.network = new Network(this)
|
||||
this.builder = new Builder(this)
|
||||
this.outbox = new Outbox(this)
|
||||
this.meta = new Meta(this)
|
||||
}
|
||||
|
||||
key = key => `${this.ns}/${key}`
|
||||
|
||||
// For use with table.sort to avoid deleting the user's own info or those of
|
||||
// direct follows
|
||||
sortByGraph = xs => {
|
||||
const pubkey = this.user.getPubkey()
|
||||
|
||||
if (pubkey) {
|
||||
const follows = this.social.graph.get(pubkey).petnames || []
|
||||
const whitelist = new Set(follows.map(nth(1)).concat(pubkey))
|
||||
|
||||
return sortBy(x => (whitelist.has(x.pubkey) ? 0 : x.updated_at), xs)
|
||||
} else {
|
||||
return sortBy(prop("updated_at"))
|
||||
}
|
||||
}
|
||||
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}),
|
||||
})
|
||||
}
|
||||
|
@ -15,6 +15,25 @@ export type Filter = {
|
||||
[key: `#${string}`]: string[]
|
||||
}
|
||||
|
||||
export type Zapper = {
|
||||
pubkey: string
|
||||
lnurl: string
|
||||
callback: string
|
||||
minSendable: number
|
||||
maxSendable: number
|
||||
nostrPubkey: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type Handle = {
|
||||
profile: Record<string, any>
|
||||
pubkey: string
|
||||
address: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type RelayInfo = {
|
||||
contact?: string
|
||||
description?: string
|
||||
@ -92,3 +111,24 @@ export type KeyState = {
|
||||
privkey?: string
|
||||
bunkerKey?: string
|
||||
}
|
||||
|
||||
export type Channel = {
|
||||
id: string
|
||||
type: "public" | "private"
|
||||
pubkey: string
|
||||
updated_at: number
|
||||
last_sent?: number
|
||||
last_received?: number
|
||||
last_checked?: number
|
||||
joined?: boolean
|
||||
hints: string[]
|
||||
}
|
||||
|
||||
export type Message = {
|
||||
id: string
|
||||
channel: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
content: string
|
||||
tags: string[][]
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
import {last, pick, uniqBy} from "ramda"
|
||||
import {doPipe, first} from "hurdak/lib/hurdak"
|
||||
import {doPipe} from "hurdak/lib/hurdak"
|
||||
import {Tags, channelAttrs, findRoot, findReply} from "src/util/nostr"
|
||||
import {parseContent} from "src/util/notes"
|
||||
import type {System} from "src/system/system"
|
||||
|
||||
export type BuilderOpts = {
|
||||
getPubkeyPetname: (pubkey: string) => string
|
||||
getPubkeyHint: (pubkey: string) => string
|
||||
getEventHint: (event: Event) => string
|
||||
}
|
||||
|
||||
const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
|
||||
|
||||
@ -15,16 +20,19 @@ const buildEvent = (kind, {content = "", tags = [], tagClient = true}) => {
|
||||
}
|
||||
|
||||
export class Builder {
|
||||
system: System
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
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 profile = this.system.directory.getProfile(pubkey)
|
||||
const name = profile ? this.system.directory.displayProfile(profile) : ""
|
||||
const hint = first(this.system.routing.getPubkeyHints(1, pubkey)) || ""
|
||||
const hint = this.getPubkeyHint(pubkey)
|
||||
const petname = this.getPubkeyPetname(pubkey)
|
||||
|
||||
return ["p", pubkey, hint, name]
|
||||
return ["p", pubkey, hint, petname]
|
||||
}
|
||||
tagsFromContent(content, tags) {
|
||||
const seen = new Set(Tags.wrap(tags).values().all())
|
||||
@ -56,7 +64,7 @@ export class Builder {
|
||||
.all()
|
||||
.map(t => t.slice(0, 3))
|
||||
: []
|
||||
const eHint = first(this.system.routing.getEventHints(1, n)) || ""
|
||||
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),
|
@ -3,7 +3,12 @@ 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 {System} from "src/system/system"
|
||||
|
||||
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[]
|
||||
@ -12,12 +17,16 @@ export type LoadPeopleOpts = {
|
||||
}
|
||||
|
||||
export class PubkeyLoader {
|
||||
system: System
|
||||
attemptedPubkeys: Set<string>
|
||||
getLastUpdated: PubkeyLoaderOpts["getLastUpdated"]
|
||||
getChunkRelays: PubkeyLoaderOpts["getChunkRelays"]
|
||||
loadChunk: PubkeyLoaderOpts["loadChunk"]
|
||||
|
||||
constructor(system) {
|
||||
this.system = system
|
||||
constructor({getLastUpdated, getChunkRelays, loadChunk}: PubkeyLoaderOpts) {
|
||||
this.attemptedPubkeys = new Set()
|
||||
this.getLastUpdated = getLastUpdated
|
||||
this.getChunkRelays = getChunkRelays
|
||||
this.loadChunk = loadChunk
|
||||
}
|
||||
|
||||
getStalePubkeys = pubkeys => {
|
||||
@ -31,9 +40,7 @@ export class PubkeyLoader {
|
||||
|
||||
this.attemptedPubkeys.add(pubkey)
|
||||
|
||||
const profile = this.system.directory.profiles.get(pubkey)
|
||||
|
||||
if (profile?.updated_at > since) {
|
||||
if (this.getLastUpdated(pubkey) > since) {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -44,7 +51,6 @@ export class PubkeyLoader {
|
||||
}
|
||||
|
||||
loadPubkeys = async (rawPubkeys, {relays, force, kinds = personKinds}: LoadPeopleOpts = {}) => {
|
||||
const {network, routing, user} = this.system
|
||||
const pubkeys = force ? uniq(rawPubkeys) : this.getStalePubkeys(rawPubkeys)
|
||||
|
||||
const getChunkRelays = chunk => {
|
||||
@ -52,10 +58,7 @@ export class PubkeyLoader {
|
||||
return relays
|
||||
}
|
||||
|
||||
return routing.mergeHints(
|
||||
user.getSetting("relay_limit"),
|
||||
chunk.map(pubkey => routing.getPubkeyHints(3, pubkey))
|
||||
)
|
||||
return this.getChunkRelays(chunk)
|
||||
}
|
||||
|
||||
const getChunkFilter = chunk => {
|
||||
@ -74,7 +77,7 @@ export class PubkeyLoader {
|
||||
|
||||
await Promise.all(
|
||||
chunk(256, pubkeys).map(async chunk => {
|
||||
await network.load({
|
||||
await this.loadChunk({
|
||||
relays: getChunkRelays(chunk),
|
||||
filter: getChunkFilter(chunk),
|
||||
})
|
||||
|
@ -138,6 +138,9 @@ export class Table<T> {
|
||||
max(k): number {
|
||||
return this._coll.max(k)
|
||||
}
|
||||
watch(f) {
|
||||
return watch(this, f)
|
||||
}
|
||||
}
|
||||
|
||||
const listener = (() => {
|
||||
|
Loading…
Reference in New Issue
Block a user