mirror of
https://github.com/coracle-social/coracle.git
synced 2024-10-02 18:00:52 +00:00
Start converting components again
This commit is contained in:
parent
479f9d6681
commit
a7cae60ca9
@ -1,7 +1,8 @@
|
||||
# Current
|
||||
|
||||
- [ ] Refactor
|
||||
- [ ] Fix load new notes button
|
||||
- [ ] Move pubkey loader to utils
|
||||
- [ ] Have meta intercept subscribe/publish rather than listen to specific events
|
||||
- [ ] Speed up note detail
|
||||
- [ ] Fix feed control state
|
||||
- [ ] Remove external dependencies from engine, open source it?
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {identity} from "ramda"
|
||||
import {createDefaultEngine} from "src/engine"
|
||||
import {Engine} from "src/engine"
|
||||
|
||||
export const DUFFLEPUD_URL = import.meta.env.VITE_DUFFLEPUD_URL
|
||||
|
||||
@ -28,7 +28,7 @@ export const DEFAULT_FOLLOWS = (import.meta.env.VITE_DEFAULT_FOLLOWS || "")
|
||||
|
||||
export const ENABLE_ZAPS = JSON.parse(import.meta.env.VITE_ENABLE_ZAPS)
|
||||
|
||||
const engine = createDefaultEngine({
|
||||
const engine = new Engine({
|
||||
DUFFLEPUD_URL,
|
||||
MULTIPLEXTR_URL,
|
||||
FORCE_RELAYS,
|
||||
@ -39,40 +39,40 @@ const engine = createDefaultEngine({
|
||||
})
|
||||
|
||||
export default engine
|
||||
export const Alerts = engine.Alerts
|
||||
export const Builder = engine.Builder
|
||||
export const Content = engine.Content
|
||||
export const Crypt = engine.Crypt
|
||||
export const Directory = engine.Directory
|
||||
export const Events = engine.Events
|
||||
export const Keys = engine.Keys
|
||||
export const Meta = engine.Meta
|
||||
export const Network = engine.Network
|
||||
export const Nip02 = engine.Nip02
|
||||
export const Nip04 = engine.Nip04
|
||||
export const Nip05 = engine.Nip05
|
||||
export const Nip28 = engine.Nip28
|
||||
export const Nip57 = engine.Nip57
|
||||
export const Nip65 = engine.Nip65
|
||||
export const Outbox = engine.Outbox
|
||||
export const PubkeyLoader = engine.PubkeyLoader
|
||||
export const Storage = engine.Storage
|
||||
export const User = engine.User
|
||||
export const alerts = engine.Alerts
|
||||
export const builder = engine.Builder
|
||||
export const content = engine.Content
|
||||
export const directory = engine.Directory
|
||||
export const events = engine.Events
|
||||
export const keys = engine.Keys
|
||||
export const meta = engine.Meta
|
||||
export const network = engine.Network
|
||||
export const nip02 = engine.Nip02
|
||||
export const nip04 = engine.Nip04
|
||||
export const nip05 = engine.Nip05
|
||||
export const nip28 = engine.Nip28
|
||||
export const nip57 = engine.Nip57
|
||||
export const nip65 = engine.Nip65
|
||||
export const outbox = engine.Outbox
|
||||
export const pubkeyLoader = engine.PubkeyLoader
|
||||
export const storage = engine.Storage
|
||||
export const user = engine.User
|
||||
export const Alerts = engine.components.Alerts
|
||||
export const Builder = engine.components.Builder
|
||||
export const Content = engine.components.Content
|
||||
export const Crypt = engine.components.Crypt
|
||||
export const Directory = engine.components.Directory
|
||||
export const Events = engine.components.Events
|
||||
export const Keys = engine.components.Keys
|
||||
export const Meta = engine.components.Meta
|
||||
export const Network = engine.components.Network
|
||||
export const Nip02 = engine.components.Nip02
|
||||
export const Nip04 = engine.components.Nip04
|
||||
export const Nip05 = engine.components.Nip05
|
||||
export const Nip28 = engine.components.Nip28
|
||||
export const Nip57 = engine.components.Nip57
|
||||
export const Nip65 = engine.components.Nip65
|
||||
export const Outbox = engine.components.Outbox
|
||||
export const PubkeyLoader = engine.components.PubkeyLoader
|
||||
export const Storage = engine.components.Storage
|
||||
export const User = engine.components.User
|
||||
export const alerts = engine.components.Alerts
|
||||
export const builder = engine.components.Builder
|
||||
export const content = engine.components.Content
|
||||
export const directory = engine.components.Directory
|
||||
export const events = engine.components.Events
|
||||
export const keys = engine.components.Keys
|
||||
export const meta = engine.components.Meta
|
||||
export const network = engine.components.Network
|
||||
export const nip02 = engine.components.Nip02
|
||||
export const nip04 = engine.components.Nip04
|
||||
export const nip05 = engine.components.Nip05
|
||||
export const nip28 = engine.components.Nip28
|
||||
export const nip57 = engine.components.Nip57
|
||||
export const nip65 = engine.components.Nip65
|
||||
export const outbox = engine.components.Outbox
|
||||
export const pubkeyLoader = engine.components.PubkeyLoader
|
||||
export const storage = engine.components.Storage
|
||||
export const user = engine.components.User
|
||||
|
@ -4,6 +4,8 @@
|
||||
import {stringToHue, hsl} from "src/util/misc"
|
||||
import ImageCircle from "src/partials/ImageCircle.svelte"
|
||||
import LogoSvg from "src/partials/LogoSvg.svelte"
|
||||
import type {Readable} from 'src/engine/util/store'
|
||||
import type {Profile} from 'src/engine/types'
|
||||
import {directory} from "src/app/engine"
|
||||
|
||||
export let pubkey
|
||||
@ -12,7 +14,7 @@
|
||||
const hue = stringToHue(pubkey)
|
||||
const primary = hsl(hue, {lightness: 80})
|
||||
const secondary = hsl(hue, {saturation: 30, lightness: 30})
|
||||
const profile = directory.profiles.key(pubkey).derived(defaultTo({pubkey}))
|
||||
const profile = directory.profiles.key(pubkey).derived(defaultTo({pubkey})) as Readable<Profile>
|
||||
</script>
|
||||
|
||||
{#if $profile.picture}
|
||||
|
@ -10,6 +10,7 @@ import {warn} from "src/util/logger"
|
||||
import {now} from "src/util/misc"
|
||||
import {userKinds, noteKinds} from "src/util/nostr"
|
||||
import {modal, toast} from "src/partials/state"
|
||||
import type {Event} from 'src/engine/types'
|
||||
import {
|
||||
FORCE_RELAYS,
|
||||
DEFAULT_FOLLOWS,
|
||||
@ -26,10 +27,11 @@ import {
|
||||
// Routing
|
||||
|
||||
export const routes = {
|
||||
person: (pubkey, tab = "notes") => `/people/${nip19.npubEncode(pubkey)}/${tab}`,
|
||||
person: (pubkey: string, tab = "notes") => `/people/${nip19.npubEncode(pubkey)}/${tab}`,
|
||||
}
|
||||
|
||||
export const addToList = (type, value) => modal.push({type: "list/select", item: {type, value}})
|
||||
export const addToList = (type: string, value: string) =>
|
||||
modal.push({type: "list/select", item: {type, value}})
|
||||
|
||||
// Menu
|
||||
|
||||
@ -38,7 +40,7 @@ export const menuIsOpen = writable(false)
|
||||
// Redact long strings, especially hex and bech32 keys which are 64 and 63
|
||||
// characters long, respectively. Put the threshold a little lower in case
|
||||
// someone accidentally enters a key with the last few digits missing
|
||||
const redactErrorInfo = info =>
|
||||
const redactErrorInfo = (info: any) =>
|
||||
JSON.parse(JSON.stringify(info || null).replace(/\w{60}\w+/g, "[REDACTED]"))
|
||||
|
||||
// Wait for bugsnag to be started in main
|
||||
@ -67,7 +69,7 @@ setTimeout(() => {
|
||||
|
||||
const session = Math.random().toString().slice(2)
|
||||
|
||||
export const logUsage = async name => {
|
||||
export const logUsage = async (name: string) => {
|
||||
// Hash the user's pubkey so we can identify unique users without knowing
|
||||
// anything about them
|
||||
const pubkey = Keys.pubkey.get()
|
||||
@ -101,7 +103,7 @@ setInterval(() => {
|
||||
|
||||
if (stats.last_activity < now() - 60) {
|
||||
Network.pool.remove(url)
|
||||
} else if (userRelays.has(url) && first(Meta.getRelayQuality(url)) < 0.3) {
|
||||
} else if (userRelays.has(url) && Meta.getRelayQuality(url)[0] < 0.3) {
|
||||
$slowConnections.push(url)
|
||||
}
|
||||
}
|
||||
@ -117,9 +119,9 @@ export const listenForNotifications = async () => {
|
||||
|
||||
const channelIds = pluck("id", Nip28.channels.get().filter(whereEq({joined: true})))
|
||||
|
||||
const eventIds = doPipe(Events.cache.get(), [
|
||||
filter(e => noteKinds.includes(e.kind)),
|
||||
sortBy(e => -e.created_at),
|
||||
const eventIds: string[] = doPipe(Events.cache.get(), [
|
||||
filter((e: Event) => noteKinds.includes(e.kind)),
|
||||
sortBy((e: Event) => -e.created_at),
|
||||
slice(0, 256),
|
||||
pluck("id"),
|
||||
])
|
||||
@ -166,7 +168,7 @@ export const loadAppData = async () => {
|
||||
listenForNotifications()
|
||||
}
|
||||
|
||||
export const login = async (method, key) => {
|
||||
export const login = async (method: string, key: string) => {
|
||||
Keys.login(method, key)
|
||||
|
||||
if (FORCE_RELAYS.length > 0) {
|
||||
@ -188,7 +190,7 @@ export const login = async (method, key) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const publishWithToast = (event, relays) =>
|
||||
export const publishWithToast = (event: Event, relays: string[]) =>
|
||||
Outbox.publish(event, relays, ({completed, succeeded, failed, timeouts, pending}) => {
|
||||
let message = `Published to ${succeeded.size}/${relays.length} relays`
|
||||
|
||||
@ -215,8 +217,8 @@ export const publishWithToast = (event, relays) =>
|
||||
// Feeds
|
||||
|
||||
export const compileFilter = (filter: DynamicFilter): Filter => {
|
||||
const getAuthors = pubkeys =>
|
||||
shuffle(pubkeys.length > 0 ? pubkeys : DEFAULT_FOLLOWS).slice(0, 256)
|
||||
const getAuthors = (pubkeys: string[]) =>
|
||||
shuffle(pubkeys.length > 0 ? pubkeys : DEFAULT_FOLLOWS as string[]).slice(0, 256)
|
||||
|
||||
if (filter.authors === "global") {
|
||||
filter = omit(["authors"], filter)
|
||||
|
53
src/engine/Engine.ts
Normal file
53
src/engine/Engine.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type {Env} from "./types"
|
||||
import {Alerts} from "./components/Alerts"
|
||||
import {Builder} from "./components/Builder"
|
||||
import {Content} from "./components/Content"
|
||||
import {Crypt} from "./components/Crypt"
|
||||
import {Directory} from "./components/Directory"
|
||||
import {Events} from "./components/Events"
|
||||
import {Keys} from "./components/Keys"
|
||||
import {Meta} from "./components/Meta"
|
||||
import {Network} from "./components/Network"
|
||||
import {Nip02} from "./components/Nip02"
|
||||
import {Nip04} from "./components/Nip04"
|
||||
import {Nip05} from "./components/Nip05"
|
||||
import {Nip28} from "./components/Nip28"
|
||||
import {Nip57} from "./components/Nip57"
|
||||
import {Nip65} from "./components/Nip65"
|
||||
import {Outbox} from "./components/Outbox"
|
||||
import {PubkeyLoader} from "./components/PubkeyLoader"
|
||||
import {Storage} from "./components/Storage"
|
||||
import {User} from "./components/User"
|
||||
|
||||
export class Engine {
|
||||
Env: Env
|
||||
components = {
|
||||
Alerts: new Alerts(),
|
||||
Builder: new Builder(),
|
||||
Content: new Content(),
|
||||
Crypt: new Crypt(),
|
||||
Directory: new Directory(),
|
||||
Events: new Events(),
|
||||
Keys: new Keys(),
|
||||
Meta: new Meta(),
|
||||
Network: new Network(),
|
||||
Nip02: new Nip02(),
|
||||
Nip04: new Nip04(),
|
||||
Nip05: new Nip05(),
|
||||
Nip28: new Nip28(),
|
||||
Nip57: new Nip57(),
|
||||
Nip65: new Nip65(),
|
||||
Outbox: new Outbox(),
|
||||
PubkeyLoader: new PubkeyLoader(),
|
||||
Storage: new Storage(),
|
||||
User: new User(),
|
||||
}
|
||||
|
||||
constructor(Env: Env) {
|
||||
this.Env = Env
|
||||
|
||||
for (const component of Object.values(this.components)) {
|
||||
component.initialize?.(this)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +1,31 @@
|
||||
import {reduce} from "ramda"
|
||||
import {Tags, noteKinds, isLike, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {collection, writable, derived} from "../util/store"
|
||||
import {collection, writable, derived} from "src/engine/util/store"
|
||||
import type {Readable} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Event} from "src/engine/types"
|
||||
|
||||
export class Alerts {
|
||||
static contributeState() {
|
||||
const events = collection<Event>("id")
|
||||
events = collection<Event>("id")
|
||||
lastChecked = writable(0)
|
||||
latestNotification = this.events.derived(reduce((n, e) => Math.max(n, e.created_at), 0))
|
||||
hasNewNotfications = derived([this.lastChecked, this.latestNotification], ([c, n]) => n > c)
|
||||
|
||||
const lastChecked = writable(0)
|
||||
initialize(engine: Engine) {
|
||||
const {Alerts, Events, Keys, User} = engine.components
|
||||
|
||||
const latestNotification = events.derived(reduce((n, e) => Math.max(n, e.created_at), 0))
|
||||
const isMention = (e: Event) => Tags.from(e).pubkeys().includes(Keys.pubkey.get())
|
||||
|
||||
const hasNewNotfications = derived(
|
||||
[lastChecked, latestNotification],
|
||||
([$lastChecked, $latestNotification]) => $latestNotification > $lastChecked
|
||||
)
|
||||
const isUserEvent = (id: string) => Events.cache.key(id).get()?.pubkey === Keys.pubkey.get()
|
||||
|
||||
return {events, lastChecked, latestNotification, hasNewNotfications}
|
||||
}
|
||||
const isDescendant = (e: Event) => isUserEvent(findRootId(e))
|
||||
|
||||
static initialize({Alerts, Events, Keys, User}) {
|
||||
const isMention = e => Tags.from(e).pubkeys().includes(Keys.pubkey.get())
|
||||
const isReply = (e: Event) => isUserEvent(findReplyId(e))
|
||||
|
||||
const isUserEvent = id => Events.cache.key(id).get()?.pubkey === Keys.pubkey.get()
|
||||
|
||||
const isDescendant = e => isUserEvent(findRootId(e))
|
||||
|
||||
const isReply = e => isUserEvent(findReplyId(e))
|
||||
|
||||
const handleNotification = condition => e => {
|
||||
const handleNotification = (e: Event) => {
|
||||
const pubkey = Keys.pubkey.get()
|
||||
|
||||
if (!pubkey || e.pubkey === pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!condition(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (User.isMuted(e)) {
|
||||
if (!pubkey || e.pubkey === pubkey || User.isMuted(e)) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -47,17 +33,23 @@ export class Alerts {
|
||||
}
|
||||
|
||||
noteKinds.forEach(kind => {
|
||||
Events.addHandler(
|
||||
kind,
|
||||
handleNotification(e => isMention(e) || isReply(e) || isDescendant(e))
|
||||
)
|
||||
Events.addHandler(kind, (e: Event) => {
|
||||
if (isMention(e) || isReply(e) || isDescendant(e)) {
|
||||
handleNotification(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Events.addHandler(
|
||||
7,
|
||||
handleNotification(e => isLike(e.content) && isReply(e))
|
||||
)
|
||||
Events.addHandler(7, (e: Event) => {
|
||||
if (isLike(e.content) && isReply(e)) {
|
||||
handleNotification(e)
|
||||
}
|
||||
})
|
||||
|
||||
Events.addHandler(9735, handleNotification(isReply))
|
||||
Events.addHandler(9735, (e: Event) => {
|
||||
if (isReply(e)) {
|
||||
handleNotification(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -2,40 +2,50 @@ import {last, pick, uniqBy} from "ramda"
|
||||
import {doPipe, first} from "hurdak"
|
||||
import {Tags, channelAttrs, findRoot, findReply} from "src/util/nostr"
|
||||
import {parseContent} from "src/util/notes"
|
||||
import type {Event, RelayPolicy, RelayPolicyEntry} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
const uniqTags = uniqBy(t => t.slice(0, 2).join(":"))
|
||||
type EventOpts = {
|
||||
content?: string
|
||||
tags?: string[][]
|
||||
tagClient?: boolean
|
||||
}
|
||||
|
||||
const buildEvent = (kind, {content = "", tags = [], tagClient = true}) => {
|
||||
const uniqTags = uniqBy((t: string[]) => t.slice(0, 2).join(":"))
|
||||
|
||||
const buildEvent = (kind: number, {content = "", tags = [], tagClient = true}: EventOpts) => {
|
||||
if (tagClient) {
|
||||
tags = tags.filter(t => t[0] !== "client").concat([["client", "coracle"]])
|
||||
tags = tags.filter((t: string[]) => t[0] !== "client").concat([["client", "coracle"]])
|
||||
}
|
||||
|
||||
return {kind, content, tags}
|
||||
}
|
||||
|
||||
export class Builder {
|
||||
static contributeActions({Nip65, Directory}) {
|
||||
const getEventHint = event => first(Nip65.getEventHints(1, event)) || ""
|
||||
engine: Engine
|
||||
|
||||
const getPubkeyHint = pubkey => first(Nip65.getPubkeyHints(1, pubkey)) || ""
|
||||
getEventHint = (event: Event) => first(this.engine.components.Nip65.getEventHints(1, event)) || ""
|
||||
|
||||
const getPubkeyPetname = pubkey => {
|
||||
const profile = Directory.getProfile(pubkey)
|
||||
getPubkeyHint = (pubkey: string): string =>
|
||||
first(this.engine.components.Nip65.getPubkeyHints(1, pubkey)) || ""
|
||||
|
||||
return profile ? Directory.displayProfile(profile) : ""
|
||||
getPubkeyPetname = (pubkey: string) => {
|
||||
const profile = this.engine.components.Directory.getProfile(pubkey)
|
||||
|
||||
return profile ? this.engine.components.Directory.displayProfile(profile) : ""
|
||||
}
|
||||
|
||||
const mention = pubkey => {
|
||||
const hint = getPubkeyHint(pubkey)
|
||||
const petname = getPubkeyPetname(pubkey)
|
||||
mention = (pubkey: string): string[] => {
|
||||
const hint = this.getPubkeyHint(pubkey)
|
||||
const petname = this.getPubkeyPetname(pubkey)
|
||||
|
||||
return ["p", pubkey, hint, petname]
|
||||
}
|
||||
|
||||
const tagsFromContent = (content, tags) => {
|
||||
tagsFromContent = (content: string, tags: string[][]) => {
|
||||
const seen = new Set(Tags.wrap(tags).values().all())
|
||||
|
||||
for (const {type, value} of parseContent({content})) {
|
||||
for (const {type, value} of parseContent({content, tags: []})) {
|
||||
if (type === "topic") {
|
||||
tags = tags.concat([["t", value]])
|
||||
seen.add(value)
|
||||
@ -47,7 +57,7 @@ export class Builder {
|
||||
}
|
||||
|
||||
if (type.match(/nostr:(nprofile|npub)/) && !seen.has(value.pubkey)) {
|
||||
tags = tags.concat([mention(value.pubkey)])
|
||||
tags = tags.concat([this.mention(value.pubkey)])
|
||||
seen.add(value.pubkey)
|
||||
}
|
||||
}
|
||||
@ -55,7 +65,7 @@ export class Builder {
|
||||
return tags
|
||||
}
|
||||
|
||||
const getReplyTags = (n, inherit = false) => {
|
||||
getReplyTags = (n: Event, inherit = false) => {
|
||||
const extra = inherit
|
||||
? Tags.from(n)
|
||||
.type("e")
|
||||
@ -63,17 +73,17 @@ export class Builder {
|
||||
.all()
|
||||
.map(t => t.slice(0, 3))
|
||||
: []
|
||||
const eHint = getEventHint(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),
|
||||
t => t.slice(0, 3).concat("root"),
|
||||
])
|
||||
|
||||
return [mention(n.pubkey), root, ...extra, reply]
|
||||
return [this.mention(n.pubkey), root, ...extra, reply]
|
||||
}
|
||||
|
||||
const authenticate = (url, challenge) =>
|
||||
authenticate = (url: string, challenge: string) =>
|
||||
buildEvent(22242, {
|
||||
tags: [
|
||||
["challenge", challenge],
|
||||
@ -81,9 +91,9 @@ export class Builder {
|
||||
],
|
||||
})
|
||||
|
||||
const setProfile = profile => buildEvent(0, {content: JSON.stringify(profile)})
|
||||
setProfile = (profile: Record<string, any>) => buildEvent(0, {content: JSON.stringify(profile)})
|
||||
|
||||
const setRelays = relays =>
|
||||
setRelays = (relays: RelayPolicyEntry[]) =>
|
||||
buildEvent(10002, {
|
||||
tags: relays.map(r => {
|
||||
const t = ["r", r.url]
|
||||
@ -96,44 +106,53 @@ export class Builder {
|
||||
}),
|
||||
})
|
||||
|
||||
const setAppData = (d, content = "") => buildEvent(30078, {content, tags: [["d", d]]})
|
||||
setAppData = (d: string, content = "") => buildEvent(30078, {content, tags: [["d", d]]})
|
||||
|
||||
const setPetnames = petnames => buildEvent(3, {tags: petnames})
|
||||
setPetnames = (petnames: string[][]) => buildEvent(3, {tags: petnames})
|
||||
|
||||
const setMutes = mutes => buildEvent(10000, {tags: mutes})
|
||||
setMutes = (mutes: string[][]) => buildEvent(10000, {tags: mutes})
|
||||
|
||||
const createList = list => buildEvent(30001, {tags: list})
|
||||
createList = (list: string[][]) => buildEvent(30001, {tags: list})
|
||||
|
||||
const createChannel = channel =>
|
||||
createChannel = (channel: Record<string, any>) =>
|
||||
buildEvent(40, {content: JSON.stringify(pick(channelAttrs, channel))})
|
||||
|
||||
const updateChannel = ({id, ...channel}) =>
|
||||
updateChannel = ({id, ...channel}: Record<string, any>) =>
|
||||
buildEvent(41, {
|
||||
content: JSON.stringify(pick(channelAttrs, channel)),
|
||||
tags: [["e", id]],
|
||||
})
|
||||
|
||||
const createChatMessage = (channelId, content, url) =>
|
||||
createChatMessage = (channelId: string, content: string, url: string) =>
|
||||
buildEvent(42, {content, tags: [["e", channelId, url, "root"]]})
|
||||
|
||||
const createDirectMessage = (pubkey, content) => buildEvent(4, {content, tags: [["p", pubkey]]})
|
||||
createDirectMessage = (pubkey: string, content: string) =>
|
||||
buildEvent(4, {content, tags: [["p", pubkey]]})
|
||||
|
||||
const createNote = (content, tags = []) =>
|
||||
buildEvent(1, {content, tags: uniqTags(tagsFromContent(content, tags))})
|
||||
createNote = (content: string, tags: string[][] = []) =>
|
||||
buildEvent(1, {content, tags: uniqTags(this.tagsFromContent(content, tags))})
|
||||
|
||||
const createReaction = (note, content) => buildEvent(7, {content, tags: getReplyTags(note)})
|
||||
createReaction = (note: Event, content: string) =>
|
||||
buildEvent(7, {content, tags: this.getReplyTags(note)})
|
||||
|
||||
const createReply = (note, content, tags = []) =>
|
||||
createReply = (note: Event, content: string, tags: string[][] = []) =>
|
||||
buildEvent(1, {
|
||||
content,
|
||||
tags: doPipe(tags, [
|
||||
tags => tags.concat(getReplyTags(note, true)),
|
||||
tags => tagsFromContent(content, tags),
|
||||
tags => tags.concat(this.getReplyTags(note, true)),
|
||||
tags => this.tagsFromContent(content, tags),
|
||||
uniqTags,
|
||||
]),
|
||||
})
|
||||
|
||||
const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => {
|
||||
requestZap = (
|
||||
relays: string[],
|
||||
content: string,
|
||||
pubkey: string,
|
||||
eventId: string,
|
||||
amount: number,
|
||||
lnurl: string
|
||||
) => {
|
||||
const tags = [
|
||||
["relays", ...relays],
|
||||
["amount", amount.toString()],
|
||||
@ -148,34 +167,13 @@ export class Builder {
|
||||
return buildEvent(9734, {content, tags, tagClient: false})
|
||||
}
|
||||
|
||||
const deleteEvents = ids => buildEvent(5, {tags: ids.map(id => ["e", id])})
|
||||
deleteEvents = (ids: string[]) => buildEvent(5, {tags: ids.map(id => ["e", id])})
|
||||
|
||||
const deleteNaddrs = naddrs => buildEvent(5, {tags: naddrs.map(naddr => ["a", naddr])})
|
||||
deleteNaddrs = (naddrs: string[]) => buildEvent(5, {tags: naddrs.map(naddr => ["a", naddr])})
|
||||
|
||||
const createLabel = payload => buildEvent(1985, payload)
|
||||
createLabel = (payload: {content: string; tags: string[][]}) => buildEvent(1985, payload)
|
||||
|
||||
return {
|
||||
mention,
|
||||
tagsFromContent,
|
||||
getReplyTags,
|
||||
authenticate,
|
||||
setProfile,
|
||||
setRelays,
|
||||
setAppData,
|
||||
setPetnames,
|
||||
setMutes,
|
||||
createList,
|
||||
createChannel,
|
||||
updateChannel,
|
||||
createChatMessage,
|
||||
createDirectMessage,
|
||||
createNote,
|
||||
createReaction,
|
||||
createReply,
|
||||
requestZap,
|
||||
deleteEvents,
|
||||
deleteNaddrs,
|
||||
createLabel,
|
||||
}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
||||
|
@ -3,29 +3,22 @@ import {nth, inc} from "ramda"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Topic, List} from "src/engine/types"
|
||||
import {derived, collection} from "../util/store"
|
||||
import {derived, collection} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Event} from "src/engine/types"
|
||||
|
||||
export class Content {
|
||||
static contributeState() {
|
||||
const topics = collection<Topic>("name")
|
||||
|
||||
const lists = collection<List>("naddr")
|
||||
|
||||
return {topics, lists}
|
||||
}
|
||||
|
||||
static contributeSelectors({Content}) {
|
||||
const getLists = f => Content.lists.get().filter(l => !l.deleted_at && (f ? f(l) : true))
|
||||
|
||||
const searchTopics = derived(Content.topics, $topics =>
|
||||
topics = collection<Topic>("name")
|
||||
lists = collection<List>("naddr")
|
||||
searchTopics = derived(this.topics, $topics =>
|
||||
fuzzy($topics.values(), {keys: ["name"], threshold: 0.3})
|
||||
)
|
||||
|
||||
return {getLists, searchTopics}
|
||||
}
|
||||
getLists = (f: (l: List) => boolean) =>
|
||||
this.lists.get().filter(l => !l.deleted_at && (f ? f(l) : true))
|
||||
|
||||
static initialize({Events, Content}) {
|
||||
const processTopics = e => {
|
||||
initialize(engine: Engine) {
|
||||
const processTopics = (e: Event) => {
|
||||
const tagTopics = Tags.from(e).topics()
|
||||
const contentTopics = Array.from(e.content.toLowerCase().matchAll(/#(\w{2,100})/g)).map(
|
||||
nth(1)
|
||||
@ -33,45 +26,45 @@ export class Content {
|
||||
|
||||
for (const name of tagTopics.concat(contentTopics)) {
|
||||
if (name) {
|
||||
const topic = Content.topics.key(name).get()
|
||||
const topic = this.topics.key(name).get()
|
||||
|
||||
Content.topics.key(name).merge({count: inc(topic?.count || 0)})
|
||||
this.topics.key(name).merge({count: inc(topic?.count || 0)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Events.addHandler(1, processTopics)
|
||||
engine.components.Events.addHandler(1, processTopics)
|
||||
|
||||
Events.addHandler(42, processTopics)
|
||||
engine.components.Events.addHandler(42, processTopics)
|
||||
|
||||
Events.addHandler(30001, e => {
|
||||
engine.components.Events.addHandler(30001, (e: Event) => {
|
||||
const {pubkey, kind, created_at} = e
|
||||
const name = Tags.from(e).getMeta("d")
|
||||
const naddr = nip19.naddrEncode({identifier: name, pubkey, kind})
|
||||
const list = Content.lists.key(naddr).get()
|
||||
const list = this.lists.key(naddr).get()
|
||||
|
||||
if (created_at < list?.updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
Content.lists.key(naddr).merge({
|
||||
this.lists.key(naddr).merge({
|
||||
...list,
|
||||
name,
|
||||
pubkey,
|
||||
tags: e.tags,
|
||||
updated_at: created_at,
|
||||
created_at: list?.created_at || created_at,
|
||||
deleted_at: null,
|
||||
deleted_at: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
Events.addHandler(5, e => {
|
||||
engine.components.Events.addHandler(5, (e: Event) => {
|
||||
Tags.from(e)
|
||||
.type("a")
|
||||
.values()
|
||||
.all()
|
||||
.forEach(naddr => {
|
||||
const list = Content.lists.key(naddr)
|
||||
const list = this.lists.key(naddr)
|
||||
|
||||
if (list.exists()) {
|
||||
list.merge({deleted_at: e.created_at})
|
||||
|
@ -1,17 +1,21 @@
|
||||
import {nip04} from "nostr-tools"
|
||||
import {switcherFn, sleep, tryFunc} from "hurdak"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {KeyState} from "src/engine/types"
|
||||
|
||||
export class Crypt {
|
||||
static contributeActions({Keys}, emit) {
|
||||
async function encrypt(pubkey, message) {
|
||||
const {method, privkey} = Keys.current.get()
|
||||
engine: Engine
|
||||
|
||||
async encrypt(pubkey: string, message: string) {
|
||||
const {method, privkey} = this.engine.components.Keys.current.get() as KeyState
|
||||
|
||||
return switcherFn(method, {
|
||||
extension: () => Keys.withExtension(ext => ext.nip04.encrypt(pubkey, message)),
|
||||
privkey: () => nip04.encrypt(privkey, pubkey, message),
|
||||
extension: () =>
|
||||
this.engine.components.Keys.withExtension((ext: any) => ext.nip04.encrypt(pubkey, message)),
|
||||
privkey: () => nip04.encrypt(privkey as string, pubkey, message),
|
||||
bunker: async () => {
|
||||
const ndk = await Keys.getNDK()
|
||||
const ndk = await this.engine.components.Keys.getNDK()
|
||||
const user = ndk.getUser({hexpubkey: pubkey})
|
||||
|
||||
return ndk.signer.encrypt(user, message)
|
||||
@ -19,12 +23,12 @@ export class Crypt {
|
||||
})
|
||||
}
|
||||
|
||||
async function decrypt(pubkey, message) {
|
||||
const {method, privkey} = Keys.current.get()
|
||||
async decrypt(pubkey: string, message: string) {
|
||||
const {method, privkey} = this.engine.components.Keys.current.get() as KeyState
|
||||
|
||||
return switcherFn(method, {
|
||||
extension: () =>
|
||||
Keys.withExtension(ext => {
|
||||
this.engine.components.Keys.withExtension((ext: any) => {
|
||||
return new Promise(async resolve => {
|
||||
let result
|
||||
|
||||
@ -44,11 +48,12 @@ export class Crypt {
|
||||
}),
|
||||
privkey: () => {
|
||||
return (
|
||||
tryFunc(() => nip04.decrypt(privkey, pubkey, message)) || `<Failed to decrypt message>`
|
||||
tryFunc(() => nip04.decrypt(privkey as string, pubkey, message)) ||
|
||||
`<Failed to decrypt message>`
|
||||
)
|
||||
},
|
||||
bunker: async () => {
|
||||
const ndk = await Keys.getNDK()
|
||||
const ndk = await this.engine.components.Keys.getNDK()
|
||||
const user = ndk.getUser({hexpubkey: pubkey})
|
||||
|
||||
return ndk.signer.decrypt(user, message)
|
||||
@ -56,18 +61,19 @@ export class Crypt {
|
||||
})
|
||||
}
|
||||
|
||||
async function encryptJson(data) {
|
||||
const {pubkey} = Keys.current.get()
|
||||
async encryptJson(data: any) {
|
||||
const {pubkey} = this.engine.components.Keys.current.get() as KeyState
|
||||
|
||||
return encrypt(pubkey, JSON.stringify(data))
|
||||
return this.encrypt(pubkey, JSON.stringify(data))
|
||||
}
|
||||
|
||||
async function decryptJson(data) {
|
||||
const {pubkey} = Keys.current.get()
|
||||
async decryptJson(data: string) {
|
||||
const {pubkey} = this.engine.components.Keys.current.get() as KeyState
|
||||
|
||||
return tryJson(async () => JSON.parse(await decrypt(pubkey, data)))
|
||||
return tryJson(async () => JSON.parse(await this.decrypt(pubkey, data)))
|
||||
}
|
||||
|
||||
return {encrypt, decrypt, encryptJson, decryptJson}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,18 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ellipsize} from "hurdak"
|
||||
import {tryJson, now, fuzzy} from "src/util/misc"
|
||||
import type {Profile} from "src/engine/types"
|
||||
import {collection, derived} from "../util/store"
|
||||
import {collection, derived} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Event, Profile} from "src/engine/types"
|
||||
|
||||
export class Directory {
|
||||
static contributeState() {
|
||||
const profiles = collection<Profile>("pubkey")
|
||||
profiles = collection<Profile>("pubkey")
|
||||
|
||||
return {profiles}
|
||||
}
|
||||
getProfile = (pubkey: string): Profile => this.profiles.key(pubkey).get() || {pubkey}
|
||||
|
||||
static contributeSelectors({Directory}) {
|
||||
const getProfile = (pubkey: string): Profile => Directory.profiles.key(pubkey).get() || {pubkey}
|
||||
getNamedProfiles = () => this.profiles.get().filter(p => p.name || p.nip05 || p.display_name)
|
||||
|
||||
const getNamedProfiles = () =>
|
||||
Directory.profiles.get().filter(p => p.name || p.nip05 || p.display_name)
|
||||
|
||||
const displayProfile = ({display_name, name, pubkey}: Profile) => {
|
||||
displayProfile = ({display_name, name, pubkey}: Profile) => {
|
||||
if (display_name) {
|
||||
return ellipsize(display_name, 60)
|
||||
}
|
||||
@ -35,25 +30,22 @@ export class Directory {
|
||||
}
|
||||
}
|
||||
|
||||
const displayPubkey = pubkey => displayProfile(getProfile(pubkey))
|
||||
displayPubkey = (pubkey: string) => this.displayProfile(this.getProfile(pubkey))
|
||||
|
||||
const searchProfiles = derived(Directory.profiles, $profiles => {
|
||||
return fuzzy(getNamedProfiles(), {
|
||||
searchProfiles = derived(this.profiles, $profiles => {
|
||||
return fuzzy(this.getNamedProfiles(), {
|
||||
keys: ["name", "display_name", {name: "nip05", weight: 0.5}, {name: "about", weight: 0.1}],
|
||||
threshold: 0.3,
|
||||
})
|
||||
})
|
||||
|
||||
return {getProfile, getNamedProfiles, displayProfile, displayPubkey, searchProfiles}
|
||||
}
|
||||
|
||||
static initialize({Events, Directory}) {
|
||||
Events.addHandler(0, e => {
|
||||
initialize(engine: Engine) {
|
||||
engine.components.Events.addHandler(0, (e: Event) => {
|
||||
tryJson(() => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const profile = Directory.profiles.key(e.pubkey)
|
||||
const profile = this.profiles.key(e.pubkey)
|
||||
|
||||
if (e.created_at < profile.get()?.created_at) {
|
||||
if (e.created_at < (profile.get()?.created_at || Infinity)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,28 @@
|
||||
import type {Event} from "src/engine/types"
|
||||
import {pushToKey} from "src/util/misc"
|
||||
import {Worker} from "../util/Worker"
|
||||
import {collection} from "../util/store"
|
||||
import {Worker} from "src/engine/util/Worker"
|
||||
import {collection} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
export const ANY_KIND = "Events/ANY_KIND"
|
||||
|
||||
export class Events {
|
||||
static contributeState() {
|
||||
return {
|
||||
queue: new Worker<Event>(),
|
||||
cache: collection<Event>("id"),
|
||||
handlers: {},
|
||||
}
|
||||
handlers = {} as Record<string, Array<(e: Event) => void>>
|
||||
queue = new Worker<Event>()
|
||||
cache = collection<Event>("id")
|
||||
addHandler = (kind: number, f: (e: Event) => void) => pushToKey(this.handlers, kind.toString(), f)
|
||||
|
||||
initialize(engine: Engine) {
|
||||
this.queue.listen(async event => {
|
||||
if (event.pubkey === engine.components.Keys.pubkey.get()) {
|
||||
this.cache.key(event.id).set(event)
|
||||
}
|
||||
|
||||
static contributeActions({Events}) {
|
||||
const addHandler = (kind, f) => pushToKey(Events.handlers, kind, f)
|
||||
|
||||
return {addHandler}
|
||||
}
|
||||
|
||||
static initialize({Events, Keys}) {
|
||||
Events.queue.listen(async event => {
|
||||
if (event.pubkey === Keys.pubkey.get()) {
|
||||
Events.cache.key(event.id).set(event)
|
||||
}
|
||||
|
||||
for (const handler of Events.handlers[ANY_KIND] || []) {
|
||||
for (const handler of this.handlers[ANY_KIND] || []) {
|
||||
await handler(event)
|
||||
}
|
||||
|
||||
for (const handler of Events.handlers[event.kind] || []) {
|
||||
for (const handler of this.handlers[event.kind.toString()] || []) {
|
||||
await handler(event)
|
||||
}
|
||||
})
|
||||
|
@ -2,52 +2,39 @@ import {propEq, find, reject} from "ramda"
|
||||
import {nip19, getPublicKey, getSignature, generatePrivateKey} from "nostr-tools"
|
||||
import NDK, {NDKEvent, NDKNip46Signer, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"
|
||||
import {switcherFn} from "hurdak"
|
||||
import {writable, derived} from "../util/store"
|
||||
|
||||
export type LoginMethod = "bunker" | "pubkey" | "privkey" | "extension"
|
||||
|
||||
export type KeyState = {
|
||||
method: LoginMethod
|
||||
pubkey: string
|
||||
privkey: string | null
|
||||
bunkerKey: string | null
|
||||
}
|
||||
import {writable, derived} from "src/engine/util/store"
|
||||
import type {KeyState, Event} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
export class Keys {
|
||||
static contributeState() {
|
||||
const pubkey = writable<string | null>()
|
||||
|
||||
const keyState = writable<KeyState[]>([])
|
||||
|
||||
const getKeyState = k => find(propEq("pubkey", k), keyState.get())
|
||||
|
||||
const setKeyState = v => keyState.update(s => reject(propEq("pubkey", v.pubkey), s).concat(v))
|
||||
|
||||
const removeKeyState = k => keyState.update(s => reject(propEq("pubkey", k), s))
|
||||
|
||||
const current = derived<KeyState | null>(pubkey, k => getKeyState(k))
|
||||
|
||||
const canSign = derived(current, keyState =>
|
||||
pubkey = writable<string | null>(null)
|
||||
keyState = writable<KeyState[]>([])
|
||||
current = derived(this.pubkey, k => this.getKeyState(k))
|
||||
canSign = derived(this.current, keyState =>
|
||||
["bunker", "privkey", "extension"].includes(keyState?.method)
|
||||
)
|
||||
|
||||
return {pubkey, keyState, getKeyState, setKeyState, removeKeyState, current, canSign}
|
||||
}
|
||||
getKeyState = (k: string) => find(propEq("pubkey", k), this.keyState.get())
|
||||
|
||||
static contributeSelectors({Keys}) {
|
||||
const {current} = Keys
|
||||
setKeyState = (v: KeyState) =>
|
||||
this.keyState.update((s: KeyState[]) => reject(propEq("pubkey", v.pubkey), s).concat(v))
|
||||
|
||||
removeKeyState = (k: string) =>
|
||||
this.keyState.update((s: KeyState[]) => reject(propEq("pubkey", k), s))
|
||||
|
||||
withExtension = (() => {
|
||||
let extensionLock = Promise.resolve()
|
||||
|
||||
const getExtension = () => (window as {nostr?: any}).nostr
|
||||
|
||||
const withExtension = f => {
|
||||
return (f: (ext: any) => void) => {
|
||||
extensionLock = extensionLock.catch(e => console.error(e)).then(() => f(getExtension()))
|
||||
|
||||
return extensionLock
|
||||
}
|
||||
})()
|
||||
|
||||
const isKeyValid = key => {
|
||||
isKeyValid = (key: string) => {
|
||||
// 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)
|
||||
try {
|
||||
@ -59,10 +46,11 @@ export class Keys {
|
||||
return true
|
||||
}
|
||||
|
||||
getNDK = (() => {
|
||||
const ndkInstances = new Map()
|
||||
|
||||
const prepareNDK = async (token?: string) => {
|
||||
const {pubkey, bunkerKey} = current.get()
|
||||
const {pubkey, bunkerKey} = this.current.get() as KeyState
|
||||
const localSigner = new NDKPrivateKeySigner(bunkerKey)
|
||||
|
||||
const ndk = new NDK({
|
||||
@ -81,43 +69,40 @@ export class Keys {
|
||||
ndkInstances.set(pubkey, ndk)
|
||||
}
|
||||
|
||||
const getNDK = async () => {
|
||||
const {pubkey} = current.get()
|
||||
return async (token?: string) => {
|
||||
const {pubkey} = this.current.get() as KeyState
|
||||
|
||||
if (!ndkInstances.has(pubkey)) {
|
||||
await prepareNDK()
|
||||
await prepareNDK(token)
|
||||
}
|
||||
|
||||
return ndkInstances.get(pubkey)
|
||||
}
|
||||
})()
|
||||
|
||||
return {withExtension, isKeyValid, getNDK}
|
||||
}
|
||||
|
||||
static contributeActions({Keys}) {
|
||||
const login = (method, key) => {
|
||||
login = (method: string, key: string | {pubkey: string; token: string}) => {
|
||||
let pubkey = null
|
||||
let privkey = null
|
||||
let bunkerKey = null
|
||||
|
||||
if (method === "privkey") {
|
||||
privkey = key
|
||||
pubkey = getPublicKey(key)
|
||||
privkey = key as string
|
||||
pubkey = getPublicKey(privkey)
|
||||
} else if (["pubkey", "extension"].includes(method)) {
|
||||
pubkey = key
|
||||
pubkey = key as string
|
||||
} else if (method === "bunker") {
|
||||
pubkey = key.pubkey
|
||||
pubkey = (key as {pubkey: string}).pubkey
|
||||
bunkerKey = generatePrivateKey()
|
||||
|
||||
Keys.getNDK(key.token)
|
||||
this.getNDK((key as {token: string}).token)
|
||||
}
|
||||
|
||||
Keys.setKeyState({method, pubkey, privkey, bunkerKey})
|
||||
Keys.pubkey.set(pubkey)
|
||||
this.setKeyState({method, pubkey, privkey, bunkerKey})
|
||||
this.pubkey.set(pubkey)
|
||||
}
|
||||
|
||||
const sign = async event => {
|
||||
const {method, privkey} = Keys.current.get()
|
||||
sign = async (event: Event) => {
|
||||
const {method, privkey} = this.current.get()
|
||||
|
||||
console.assert(event.id)
|
||||
console.assert(event.pubkey)
|
||||
@ -125,7 +110,7 @@ export class Keys {
|
||||
|
||||
return switcherFn(method, {
|
||||
bunker: async () => {
|
||||
const ndk = await Keys.getNDK()
|
||||
const ndk = await this.getNDK()
|
||||
const ndkEvent = new NDKEvent(ndk, event)
|
||||
|
||||
await ndkEvent.sign(ndk.signer)
|
||||
@ -137,17 +122,21 @@ export class Keys {
|
||||
sig: getSignature(event, privkey),
|
||||
})
|
||||
},
|
||||
extension: () => Keys.withExtension(ext => ext.signEvent(event)),
|
||||
extension: () => this.withExtension(ext => ext.signEvent(event)),
|
||||
})
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
const $pubkey = Keys.pubkey.get()
|
||||
clear = () => {
|
||||
const $pubkey = this.pubkey.get()
|
||||
|
||||
Keys.pubkey.set(null)
|
||||
Keys.removeKeyState($pubkey)
|
||||
this.pubkey.set(null)
|
||||
|
||||
if ($pubkey) {
|
||||
this.removeKeyState($pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return {login, sign, clear}
|
||||
initialize(engine: Engine) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,20 @@
|
||||
import {Socket} from "paravel"
|
||||
import {now} from "src/util/misc"
|
||||
import {switcher} from "hurdak"
|
||||
import {collection} from "src/engine/util/store"
|
||||
import type {RelayStat} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Network} from "src/engine/components/Network"
|
||||
|
||||
export class Meta {
|
||||
static contributeState() {
|
||||
const relayStats = collection<RelayStat>("url")
|
||||
Network: Network
|
||||
|
||||
return {relayStats}
|
||||
}
|
||||
relayStats = collection<RelayStat>("url")
|
||||
|
||||
static contributeSelectors({Meta, Network}) {
|
||||
const getRelayStats = url => Meta.relayStats.key(url).get()
|
||||
getRelayStats = (url: string) => this.relayStats.key(url).get()
|
||||
|
||||
const getRelayQuality = url => {
|
||||
const stats = getRelayStats(url)
|
||||
getRelayQuality = (url: string): [number, string] => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
if (!stats) {
|
||||
return [0.5, "Not Connected"]
|
||||
@ -48,47 +47,46 @@ export class Meta {
|
||||
return [eoseQuality, "Connected"]
|
||||
}
|
||||
|
||||
if (Network.pool.get(url).status === Socket.STATUS.READY) {
|
||||
if (this.Network.pool.get(url).status === Socket.STATUS.READY) {
|
||||
return [1, "Connected"]
|
||||
}
|
||||
|
||||
return [0.5, "Not Connected"]
|
||||
}
|
||||
|
||||
return {getRelayStats, getRelayQuality}
|
||||
}
|
||||
initialize(engine: Engine) {
|
||||
this.Network = engine.components.Network
|
||||
|
||||
static initialize({Network, Meta}) {
|
||||
Network.pool.on("open", ({url}) => {
|
||||
Meta.relayStats.key(url).merge({last_opened: now(), last_activity: now()})
|
||||
this.Network.pool.on("open", ({url}: {url: string}) => {
|
||||
this.relayStats.key(url).merge({last_opened: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
Network.pool.on("close", ({url}) => {
|
||||
Meta.relayStats.key(url).merge({last_closed: now(), last_activity: now()})
|
||||
this.Network.pool.on("close", ({url}: {url: string}) => {
|
||||
this.relayStats.key(url).merge({last_closed: now(), last_activity: now()})
|
||||
})
|
||||
|
||||
Network.pool.on("error:set", (url, error) => {
|
||||
Meta.relayStats.key(url).merge({error})
|
||||
this.Network.pool.on("error:set", (url: string, error: string) => {
|
||||
this.relayStats.key(url).merge({error})
|
||||
})
|
||||
|
||||
Network.pool.on("error:clear", url => {
|
||||
Meta.relayStats.key(url).merge({error: null})
|
||||
this.Network.pool.on("error:clear", (url: string) => {
|
||||
this.relayStats.key(url).merge({error: null})
|
||||
})
|
||||
|
||||
Network.emitter.on("publish", urls => {
|
||||
this.Network.emitter.on("publish", (urls: string[]) => {
|
||||
for (const url of urls) {
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
last_publish: now(),
|
||||
last_activity: now(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Network.emitter.on("sub:open", urls => {
|
||||
this.Network.emitter.on("sub:open", (urls: string[]) => {
|
||||
for (const url of urls) {
|
||||
const stats = Meta.getRelayStats(url)
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
last_sub: now(),
|
||||
last_activity: now(),
|
||||
total_subs: (stats?.total_subs || 0) + 1,
|
||||
@ -97,40 +95,40 @@ export class Meta {
|
||||
}
|
||||
})
|
||||
|
||||
Network.emitter.on("sub:close", urls => {
|
||||
this.Network.emitter.on("sub:close", (urls: string[]) => {
|
||||
for (const url of urls) {
|
||||
const stats = Meta.getRelayStats(url)
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
last_activity: now(),
|
||||
active_subs: stats ? stats.active_subs - 1 : 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Network.emitter.on("event", ({url}) => {
|
||||
const stats = Meta.getRelayStats(url)
|
||||
this.Network.emitter.on("event", ({url}: {url: string}) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
last_activity: now(),
|
||||
events_count: (stats.events_count || 0) + 1,
|
||||
})
|
||||
})
|
||||
|
||||
Network.emitter.on("eose", (url, ms) => {
|
||||
const stats = Meta.getRelayStats(url)
|
||||
this.Network.emitter.on("eose", (url: string, ms: number) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
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)
|
||||
this.Network.emitter.on("timeout", (url: string, ms: number) => {
|
||||
const stats = this.getRelayStats(url)
|
||||
|
||||
Meta.relayStats.key(url).merge({
|
||||
this.relayStats.key(url).merge({
|
||||
last_activity: now(),
|
||||
timeouts: (stats.timeouts || 0) + 1,
|
||||
})
|
||||
|
@ -5,10 +5,29 @@ import {ensurePlural, union, difference} from "hurdak"
|
||||
import {warn, error, log} from "src/util/logger"
|
||||
import {normalizeRelayUrl} from "src/util/nostr"
|
||||
import type {Event, Filter} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {CursorOpts} from "src/engine/util/Cursor"
|
||||
import type {FeedOpts} from "src/engine/util/Feed"
|
||||
import {Cursor, MultiCursor} from "src/engine/util/Cursor"
|
||||
import {Subscription} from "src/engine/util/Subscription"
|
||||
import {Feed} from "src/engine/util/Feed"
|
||||
|
||||
export type Progress = {
|
||||
succeeded: Set<string>
|
||||
failed: Set<string>
|
||||
timeouts: Set<string>
|
||||
completed: Set<string>
|
||||
pending: Set<string>
|
||||
}
|
||||
|
||||
export type PublishOpts = {
|
||||
relays: string[]
|
||||
event: Event
|
||||
onProgress: (p: Progress) => void
|
||||
timeout?: number
|
||||
verb?: string
|
||||
}
|
||||
|
||||
export type SubscribeOpts = {
|
||||
relays: string[]
|
||||
filter: Filter[] | Filter
|
||||
@ -19,7 +38,7 @@ export type SubscribeOpts = {
|
||||
shouldProcess?: boolean
|
||||
}
|
||||
|
||||
const getUrls = relays => {
|
||||
const getUrls = (relays: string[]) => {
|
||||
if (relays.length === 0) {
|
||||
error(`Attempted to connect to zero urls`)
|
||||
}
|
||||
@ -34,38 +53,28 @@ const getUrls = relays => {
|
||||
}
|
||||
|
||||
export class Network {
|
||||
static contributeState() {
|
||||
const authHandler = null
|
||||
const emitter = new EventEmitter()
|
||||
const pool = new Pool()
|
||||
engine: Engine
|
||||
pool = new Pool()
|
||||
authHandler: (url: string, challenge: string) => void
|
||||
emitter = new EventEmitter()
|
||||
|
||||
return {authHandler, emitter, pool}
|
||||
}
|
||||
relayHasError = (url: string) => Boolean(this.pool.get(url, {autoConnect: false})?.error)
|
||||
|
||||
static contributeSelectors({Network}) {
|
||||
const relayHasError = url => Boolean(Network.pool.get(url, {autoConnect: false})?.error)
|
||||
|
||||
return {relayHasError}
|
||||
}
|
||||
|
||||
static contributeActions(engine) {
|
||||
const {Network, User, Events, Nip65, Env} = engine
|
||||
|
||||
const getExecutor = (urls, {bypassBoot = false} = {}) => {
|
||||
if (Env.FORCE_RELAYS?.length > 0) {
|
||||
urls = Env.FORCE_RELAYS
|
||||
getExecutor = (urls: string[], {bypassBoot = false} = {}) => {
|
||||
if (this.engine.Env.FORCE_RELAYS?.length > 0) {
|
||||
urls = this.engine.Env.FORCE_RELAYS
|
||||
}
|
||||
|
||||
let target
|
||||
|
||||
const muxUrl = User.getSetting("multiplextr_url")
|
||||
const muxUrl = this.engine.components.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 (muxUrl && (urls.length > 1 || this.pool.has(muxUrl))) {
|
||||
const socket = this.pool.get(muxUrl)
|
||||
|
||||
if (!socket.error) {
|
||||
target = new Plex(urls, socket)
|
||||
@ -73,31 +82,31 @@ export class Network {
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
target = new Relays(urls.map(url => Network.pool.get(url)))
|
||||
target = new Relays(urls.map(url => this.pool.get(url)))
|
||||
}
|
||||
|
||||
const executor = new Executor(target)
|
||||
|
||||
executor.handleAuth({
|
||||
onAuth(url, challenge) {
|
||||
Network.emitter.emit("error:set", url, "unauthorized")
|
||||
onAuth(url: string, challenge: string) {
|
||||
this.emitter.emit("error:set", url, "unauthorized")
|
||||
|
||||
return Network.authHandler?.(url, challenge)
|
||||
return this.authHandler?.(url, challenge)
|
||||
},
|
||||
onOk(url, id, ok, message) {
|
||||
Network.emitter.emit("error:clear", url, ok ? null : "forbidden")
|
||||
onOk(url: string, id: string, ok: boolean, message: string) {
|
||||
this.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
|
||||
this.pool.get(url)
|
||||
this.pool.booted = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Eagerly connect and handle AUTH
|
||||
executor.target.sockets.forEach(socket => {
|
||||
const {limitation} = Nip65.getRelayInfo(socket.url)
|
||||
executor.target.sockets.forEach((socket: any) => {
|
||||
const {limitation} = this.engine.components.Nip65.getRelayInfo(socket.url)
|
||||
const waitForBoot = limitation?.payment_required || limitation?.auth_required
|
||||
|
||||
// This happens automatically, but kick it off anyway
|
||||
@ -118,22 +127,22 @@ export class Network {
|
||||
return executor
|
||||
}
|
||||
|
||||
const publish = ({relays, event, onProgress, timeout = 3000, verb = "EVENT"}) => {
|
||||
publish = ({relays, event, onProgress, timeout = 3000, verb = "EVENT"}: PublishOpts) => {
|
||||
const urls = getUrls(relays)
|
||||
const executor = getExecutor(urls, {bypassBoot: verb === "AUTH"})
|
||||
const executor = this.getExecutor(urls, {bypassBoot: verb === "AUTH"})
|
||||
|
||||
Network.emitter.emit("publish", urls)
|
||||
this.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 timeouts = new Set<string>()
|
||||
const succeeded = new Set<string>()
|
||||
const failed = new Set<string>()
|
||||
|
||||
const getProgress = () => {
|
||||
const completed = union(timeouts, succeeded, failed)
|
||||
const pending = difference(urls, completed)
|
||||
const pending = difference(new Set(urls), completed)
|
||||
|
||||
return {succeeded, failed, timeouts, completed, pending}
|
||||
}
|
||||
@ -163,13 +172,13 @@ export class Network {
|
||||
|
||||
const sub = executor.publish(event, {
|
||||
verb,
|
||||
onOk: url => {
|
||||
onOk: (url: string) => {
|
||||
succeeded.add(url)
|
||||
timeouts.delete(url)
|
||||
failed.delete(url)
|
||||
attemptToResolve()
|
||||
},
|
||||
onError: url => {
|
||||
onError: (url: string) => {
|
||||
failed.add(url)
|
||||
timeouts.delete(url)
|
||||
attemptToResolve()
|
||||
@ -181,7 +190,7 @@ export class Network {
|
||||
})
|
||||
}
|
||||
|
||||
const subscribe = ({
|
||||
subscribe = ({
|
||||
relays,
|
||||
filter,
|
||||
onEose,
|
||||
@ -191,7 +200,7 @@ export class Network {
|
||||
shouldProcess = true,
|
||||
}: SubscribeOpts) => {
|
||||
const urls = getUrls(relays)
|
||||
const executor = getExecutor(urls)
|
||||
const executor = this.getExecutor(urls)
|
||||
const filters = ensurePlural(filter)
|
||||
const subscription = new Subscription()
|
||||
const now = Date.now()
|
||||
@ -200,12 +209,12 @@ export class Network {
|
||||
|
||||
log(`Starting subscription with ${relays.length} relays`, {filters, relays})
|
||||
|
||||
Network.emitter.emit("sub:open", urls)
|
||||
this.emitter.emit("sub:open", urls)
|
||||
|
||||
subscription.on("close", () => {
|
||||
sub.unsubscribe()
|
||||
executor.target.cleanup()
|
||||
Network.emitter.emit("sub:close", urls)
|
||||
this.emitter.emit("sub:close", urls)
|
||||
onClose?.()
|
||||
})
|
||||
|
||||
@ -214,7 +223,7 @@ export class Network {
|
||||
}
|
||||
|
||||
const sub = executor.subscribe(filters, {
|
||||
onEvent: (url, event) => {
|
||||
onEvent: (url: string, event: Event) => {
|
||||
const seen_on = seen.get(event.id)
|
||||
|
||||
if (seen_on) {
|
||||
@ -246,20 +255,20 @@ export class Network {
|
||||
return
|
||||
}
|
||||
|
||||
Network.emitter.emit("event", {url, event})
|
||||
this.emitter.emit("event", {url, event})
|
||||
|
||||
if (shouldProcess) {
|
||||
Events.queue.push(event)
|
||||
this.engine.components.Events.queue.push(event)
|
||||
}
|
||||
|
||||
onEvent?.(event)
|
||||
},
|
||||
onEose: url => {
|
||||
onEose: (url: string) => {
|
||||
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)
|
||||
this.emitter.emit("eose", url, Date.now() - now)
|
||||
}
|
||||
|
||||
eose.add(url)
|
||||
@ -273,13 +282,13 @@ export class Network {
|
||||
return subscription
|
||||
}
|
||||
|
||||
const count = async filter => {
|
||||
count = async (filter: Filter | Filter[]) => {
|
||||
const filters = ensurePlural(filter)
|
||||
const executor = getExecutor(Env.COUNT_RELAYS)
|
||||
const executor = this.getExecutor(this.engine.Env.COUNT_RELAYS)
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = executor.count(filters, {
|
||||
onCount: (url, {count}) => resolve(count),
|
||||
onCount: (url: string, {count}: {count: number}) => resolve(count),
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
@ -290,13 +299,14 @@ export class Network {
|
||||
})
|
||||
}
|
||||
|
||||
const cursor = opts => new Cursor({...opts, subscribe})
|
||||
cursor = (opts: Partial<CursorOpts>) => new Cursor({...opts, Network: this} as CursorOpts)
|
||||
|
||||
const multiCursor = ({relays, ...opts}) =>
|
||||
new MultiCursor(relays.map(relay => cursor({relay, ...opts})))
|
||||
multiCursor = ({relays, ...opts}: Partial<CursorOpts> & {relays: string[]}) =>
|
||||
new MultiCursor(relays.map((relay: string) => this.cursor({relay, ...opts} as CursorOpts)))
|
||||
|
||||
const feed = opts => new Feed({engine, ...opts})
|
||||
feed = (opts: Partial<FeedOpts>) => new Feed({engine: this.engine, ...opts} as FeedOpts)
|
||||
|
||||
return {subscribe, publish, count, cursor, multiCursor, feed}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
||||
|
@ -2,25 +2,21 @@ import {ensurePlural} from "hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {GraphEntry} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {collection} from "src/engine/util/store"
|
||||
|
||||
export class Nip02 {
|
||||
static contributeState() {
|
||||
const graph = collection<GraphEntry>("pubkey")
|
||||
graph = collection<GraphEntry>("pubkey")
|
||||
|
||||
return {graph}
|
||||
}
|
||||
getPetnames = (pubkey: string) => this.graph.key(pubkey).get()?.petnames || []
|
||||
|
||||
static contributeActions({Nip02}) {
|
||||
const getPetnames = pubkey => Nip02.graph.key(pubkey).get()?.petnames || []
|
||||
getMutedTags = (pubkey: string) => this.graph.key(pubkey).get()?.mutes || []
|
||||
|
||||
const getMutedTags = pubkey => Nip02.graph.key(pubkey).get()?.mutes || []
|
||||
|
||||
const getFollowsSet = pubkeys => {
|
||||
const follows = new Set()
|
||||
getFollowsSet = (pubkeys: string | string[]) => {
|
||||
const follows = new Set<string>()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of getPetnames(pubkey)) {
|
||||
for (const tag of this.getPetnames(pubkey)) {
|
||||
follows.add(tag[1])
|
||||
}
|
||||
}
|
||||
@ -28,11 +24,11 @@ export class Nip02 {
|
||||
return follows
|
||||
}
|
||||
|
||||
const getMutesSet = pubkeys => {
|
||||
const mutes = new Set()
|
||||
getMutesSet = (pubkeys: string | string[]) => {
|
||||
const mutes = new Set<string>()
|
||||
|
||||
for (const pubkey of ensurePlural(pubkeys)) {
|
||||
for (const tag of getMutedTags(pubkey)) {
|
||||
for (const tag of this.getMutedTags(pubkey)) {
|
||||
mutes.add(tag[1])
|
||||
}
|
||||
}
|
||||
@ -40,15 +36,15 @@ export class Nip02 {
|
||||
return mutes
|
||||
}
|
||||
|
||||
const getFollows = pubkeys => Array.from(getFollowsSet(pubkeys))
|
||||
getFollows = (pubkeys: string | string[]) => Array.from(this.getFollowsSet(pubkeys))
|
||||
|
||||
const getMutes = pubkeys => Array.from(getMutesSet(pubkeys))
|
||||
getMutes = (pubkeys: string | string[]) => Array.from(this.getMutesSet(pubkeys))
|
||||
|
||||
const getNetworkSet = (pubkeys, includeFollows = false) => {
|
||||
const follows = getFollowsSet(pubkeys)
|
||||
const network = includeFollows ? follows : new Set()
|
||||
getNetworkSet = (pubkeys: string | string[], includeFollows = false) => {
|
||||
const follows = this.getFollowsSet(pubkeys)
|
||||
const network = includeFollows ? follows : new Set<string>()
|
||||
|
||||
for (const pubkey of getFollows(follows)) {
|
||||
for (const pubkey of this.getFollows(Array.from(follows))) {
|
||||
if (!follows.has(pubkey)) {
|
||||
network.add(pubkey)
|
||||
}
|
||||
@ -57,49 +53,35 @@ export class Nip02 {
|
||||
return network
|
||||
}
|
||||
|
||||
const getNetwork = pubkeys => Array.from(getNetworkSet(pubkeys))
|
||||
getNetwork = (pubkeys: string | string[]) => Array.from(this.getNetworkSet(pubkeys))
|
||||
|
||||
const isFollowing = (a, b) => getFollowsSet(a).has(b)
|
||||
isFollowing = (a: string, b: string) => this.getFollowsSet(a).has(b)
|
||||
|
||||
const isIgnoring = (a, b) => getMutesSet(a).has(b)
|
||||
isIgnoring = (a: string, b: string) => this.getMutesSet(a).has(b)
|
||||
|
||||
return {
|
||||
getPetnames,
|
||||
getMutedTags,
|
||||
getFollowsSet,
|
||||
getMutesSet,
|
||||
getFollows,
|
||||
getMutes,
|
||||
getNetworkSet,
|
||||
getNetwork,
|
||||
isFollowing,
|
||||
isIgnoring,
|
||||
}
|
||||
}
|
||||
|
||||
static initialize({Events, Nip02}) {
|
||||
Events.addHandler(3, e => {
|
||||
const entry = Nip02.graph.key(e.pubkey).get()
|
||||
initialize(engine: Engine) {
|
||||
engine.components.Events.addHandler(3, e => {
|
||||
const entry = this.graph.key(e.pubkey).get()
|
||||
|
||||
if (e.created_at < entry?.petnames_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip02.graph.key(e.pubkey).merge({
|
||||
this.graph.key(e.pubkey).merge({
|
||||
updated_at: now(),
|
||||
petnames_updated_at: e.created_at,
|
||||
petnames: Tags.from(e).type("p").all(),
|
||||
})
|
||||
})
|
||||
|
||||
Events.addHandler(10000, e => {
|
||||
const entry = Nip02.graph.key(e.pubkey).get()
|
||||
engine.components.Events.addHandler(10000, e => {
|
||||
const entry = this.graph.key(e.pubkey).get()
|
||||
|
||||
if (e.created_at < entry?.mutes_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip02.graph.key(e.pubkey).merge({
|
||||
this.graph.key(e.pubkey).merge({
|
||||
updated_at: now(),
|
||||
mutes_updated_at: e.created_at,
|
||||
mutes: Tags.from(e).type(["e", "p"]).all(),
|
||||
|
@ -2,51 +2,47 @@ import {tryFunc} from "hurdak"
|
||||
import {find, last, uniq, pluck} from "ramda"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import {Tags, appDataKeys} from "src/util/nostr"
|
||||
import type {Contact, Message} from "src/engine/types"
|
||||
import {collection, derived} from "../util/store"
|
||||
import type {Contact, Profile, Message, Event} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {collection, derived} from "src/engine/util/store"
|
||||
|
||||
const getHints = e => pluck("url", Tags.from(e).relays())
|
||||
const getHints = (e: Event) => pluck("url", Tags.from(e).relays())
|
||||
|
||||
const messageIsNew = ({last_checked, last_received, last_sent}: Contact) =>
|
||||
last_received > Math.max(last_sent || 0, last_checked || 0)
|
||||
|
||||
export class Nip04 {
|
||||
static contributeState() {
|
||||
const contacts = collection<Contact>("pubkey")
|
||||
const messages = collection<Message>("id")
|
||||
engine: Engine
|
||||
contacts = collection<Contact>("pubkey")
|
||||
messages = collection<Message>("id")
|
||||
|
||||
const hasNewMessages = derived(
|
||||
contacts,
|
||||
find(e => e.last_sent > 0 && messageIsNew(e))
|
||||
hasNewMessages = derived(
|
||||
this.contacts,
|
||||
find((e: Contact) => e.last_sent > 0 && messageIsNew(e))
|
||||
)
|
||||
|
||||
return {contacts, messages, hasNewMessages}
|
||||
}
|
||||
|
||||
static contributeSelectors({Nip04, Directory}) {
|
||||
const searchContacts = Nip04.messages.derived($messages => {
|
||||
searchContacts = this.messages.derived($messages => {
|
||||
const pubkeySet = new Set(pluck("pubkey", $messages))
|
||||
const searchProfiles = Directory.searchProfiles.get()
|
||||
const searchProfiles = this.engine.components.Directory.searchProfiles.get()
|
||||
|
||||
return q =>
|
||||
return (q: string) =>
|
||||
searchProfiles(q)
|
||||
.filter(p => pubkeySet.has(p.pubkey))
|
||||
.map(p => Nip04.contacts.key(p.pubkey).get())
|
||||
.filter((p: Profile) => pubkeySet.has(p.pubkey))
|
||||
.map((p: Profile) => this.contacts.key(p.pubkey).get())
|
||||
})
|
||||
|
||||
return {messageIsNew, searchContacts}
|
||||
}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
|
||||
static initialize({Events, Nip04, Keys, Crypt}) {
|
||||
Events.addHandler(30078, async e => {
|
||||
engine.components.Events.addHandler(30078, async e => {
|
||||
if (Tags.from(e).getMeta("d") === appDataKeys.NIP04_LAST_CHECKED) {
|
||||
await tryJson(async () => {
|
||||
const payload = await Crypt.decryptJson(e.content)
|
||||
const payload = await engine.components.Crypt.decryptJson(e.content)
|
||||
|
||||
for (const key of Object.keys(payload)) {
|
||||
// Backwards compat from when we used to prefix id/pubkey
|
||||
const pubkey = last(key.split("/"))
|
||||
const contact = Nip04.contacts.key(pubkey).get()
|
||||
const contact = this.contacts.key(pubkey).get()
|
||||
const last_checked = Math.max(payload[pubkey], contact?.last_checked || 0)
|
||||
|
||||
// A bunch of junk got added to this setting. Integer keys, settings, etc
|
||||
@ -54,50 +50,51 @@ export class Nip04 {
|
||||
continue
|
||||
}
|
||||
|
||||
Nip04.contacts.key(pubkey).merge({last_checked})
|
||||
this.contacts.key(pubkey).merge({last_checked})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Events.addHandler(4, async e => {
|
||||
if (!Keys.canSign.get()) {
|
||||
engine.components.Events.addHandler(4, async e => {
|
||||
if (!engine.components.Keys.canSign.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
const author = e.pubkey
|
||||
const recipient = Tags.from(e).type("p").values().first()
|
||||
|
||||
if (![author, recipient].includes(Keys.pubkey.get())) {
|
||||
if (![author, recipient].includes(engine.components.Keys.pubkey.get())) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Nip04.messages.key(e.id).get()) {
|
||||
if (this.messages.key(e.id).get()) {
|
||||
return
|
||||
}
|
||||
|
||||
await tryFunc(async () => {
|
||||
const other = Keys.pubkey.get() === author ? recipient : author
|
||||
const other = engine.components.Keys.pubkey.get() === author ? recipient : author
|
||||
|
||||
Nip04.messages.key(e.id).set({
|
||||
this.messages.key(e.id).set({
|
||||
id: e.id,
|
||||
contact: other,
|
||||
pubkey: e.pubkey,
|
||||
created_at: e.created_at,
|
||||
content: await Crypt.decrypt(other, e.content),
|
||||
content: await engine.components.Crypt.decrypt(other, e.content),
|
||||
tags: e.tags,
|
||||
})
|
||||
|
||||
if (Keys.pubkey.get() === author) {
|
||||
const contact = Nip04.contacts.key(recipient).get()
|
||||
if (engine.components.Keys.pubkey.get() === author) {
|
||||
const contact = this.contacts.key(recipient).get()
|
||||
|
||||
Nip04.contacts.key(recipient).merge({
|
||||
this.contacts.key(recipient).merge({
|
||||
last_sent: e.created_at,
|
||||
hints: uniq(getHints(e).concat(contact?.hints || [])),
|
||||
})
|
||||
} else {
|
||||
const contact = Nip04.contacts.key(author).get()
|
||||
const contact = this.contacts.key(author).get()
|
||||
|
||||
Nip04.contacts.key(author).merge({
|
||||
this.contacts.key(author).merge({
|
||||
last_received: e.created_at,
|
||||
hints: uniq(getHints(e).concat(contact?.hints || [])),
|
||||
})
|
||||
|
@ -3,35 +3,30 @@ import {nip05} from "nostr-tools"
|
||||
import {tryFunc} from "hurdak"
|
||||
import {now, tryJson} from "src/util/misc"
|
||||
import type {Handle} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {collection} from "src/engine/util/store"
|
||||
|
||||
export class Nip05 {
|
||||
static contributeState() {
|
||||
return {
|
||||
handles: collection<Handle>("pubkey"),
|
||||
}
|
||||
}
|
||||
handles = collection<Handle>("pubkey")
|
||||
|
||||
static contributeSelectors({Nip05}) {
|
||||
const getHandle = pubkey => Nip05.handles.key(pubkey).get()
|
||||
getHandle = (pubkey: string) => this.handles.key(pubkey).get()
|
||||
|
||||
const displayHandle = handle =>
|
||||
displayHandle = (handle: Handle) =>
|
||||
handle.address.startsWith("_@") ? last(handle.address.split("@")) : handle.address
|
||||
|
||||
return {getHandle, displayHandle}
|
||||
}
|
||||
|
||||
static initialize({Events, Nip05}) {
|
||||
Events.addHandler(0, e => {
|
||||
initialize(engine: Engine) {
|
||||
engine.components.Events.addHandler(0, e => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const handle = Nip05.handles.key(e.pubkey)
|
||||
const handle = this.handles.key(e.pubkey)
|
||||
|
||||
if (!kind0.nip05 || e.created_at < handle.get()?.created_at) {
|
||||
if (!kind0.nip05 || e.created_at < (handle.get()?.created_at || Infinity)) {
|
||||
return
|
||||
}
|
||||
|
||||
const profile = await tryFunc(() => nip05.queryProfile(kind0.nip05), true)
|
||||
const profile = (await tryFunc(() => nip05.queryProfile(kind0.nip05))) as null | {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
if (profile?.pubkey !== e.pubkey) {
|
||||
return
|
||||
|
@ -1,29 +1,24 @@
|
||||
import {find, last, pick, uniq} from "ramda"
|
||||
import {tryJson, fuzzy, now} from "src/util/misc"
|
||||
import {Tags, appDataKeys, channelAttrs} from "src/util/nostr"
|
||||
import type {Channel, Message} from "src/engine/types"
|
||||
import {collection, derived} from "../util/store"
|
||||
import type {Channel, Event, Message} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {collection, derived} from "src/engine/util/store"
|
||||
import type {Readable} from "src/engine/util/store"
|
||||
|
||||
const messageIsNew = ({last_checked, last_received, last_sent}: Channel) =>
|
||||
last_received > Math.max(last_sent || 0, last_checked || 0)
|
||||
|
||||
export class Nip28 {
|
||||
static contributeState() {
|
||||
const channels = collection<Channel>("id")
|
||||
const messages = collection<Message>("id")
|
||||
channels = collection<Channel>("id")
|
||||
messages = collection<Message>("id")
|
||||
|
||||
const hasNewMessages = derived(
|
||||
channels,
|
||||
find(e => {
|
||||
return e.type === "public" && e.joined > 0 && messageIsNew(e)
|
||||
})
|
||||
hasNewMessages = derived(
|
||||
this.channels,
|
||||
find((c: Channel) => c.joined && messageIsNew(c))
|
||||
)
|
||||
|
||||
return {channels, messages, hasNewMessages}
|
||||
}
|
||||
|
||||
static contributeSelectors({Nip28}) {
|
||||
const getSearchChannels = channels =>
|
||||
getSearchChannels = (channels: Readable<Channel[]>) =>
|
||||
channels.derived($channels => {
|
||||
return fuzzy($channels, {
|
||||
keys: ["name", {name: "about", weight: 0.5}],
|
||||
@ -31,42 +26,38 @@ export class Nip28 {
|
||||
})
|
||||
})
|
||||
|
||||
const searchChannels = getSearchChannels(Nip28.channels)
|
||||
searchChannels = this.getSearchChannels(this.channels)
|
||||
|
||||
return {messageIsNew, getSearchChannels, searchChannels}
|
||||
}
|
||||
|
||||
static initialize({Events, Nip28, Keys, Crypt}) {
|
||||
Events.addHandler(40, e => {
|
||||
const channel = Nip28.channels.key(e.id).get()
|
||||
initialize(engine: Engine) {
|
||||
engine.components.Events.addHandler(40, (e: Event) => {
|
||||
const channel = this.channels.key(e.id).get()
|
||||
|
||||
if (e.created_at < channel?.updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content)))
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content))) as Partial<Channel>
|
||||
|
||||
if (!content?.name) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip28.channels.key(e.id).merge({
|
||||
type: "public",
|
||||
this.channels.key(e.id).merge({
|
||||
...content,
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
hints: Tags.from(e).relays(),
|
||||
...content,
|
||||
})
|
||||
})
|
||||
|
||||
Events.addHandler(41, e => {
|
||||
engine.components.Events.addHandler(41, (e: Event) => {
|
||||
const channelId = Tags.from(e).getMeta("e")
|
||||
|
||||
if (!channelId) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = Nip28.channels.key(channelId).get()
|
||||
const channel = this.channels.key(channelId).get()
|
||||
|
||||
if (e.created_at < channel?.updated_at) {
|
||||
return
|
||||
@ -76,29 +67,29 @@ export class Nip28 {
|
||||
return
|
||||
}
|
||||
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content)))
|
||||
const content = tryJson(() => pick(channelAttrs, JSON.parse(e.content))) as Partial<Channel>
|
||||
|
||||
if (!content?.name) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip28.channels.key(channelId).merge({
|
||||
this.channels.key(channelId).merge({
|
||||
...content,
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
hints: Tags.from(e).relays(),
|
||||
...content,
|
||||
})
|
||||
})
|
||||
|
||||
Events.addHandler(30078, async e => {
|
||||
engine.components.Events.addHandler(30078, async (e: Event) => {
|
||||
if (Tags.from(e).getMeta("d") === appDataKeys.NIP28_LAST_CHECKED) {
|
||||
await tryJson(async () => {
|
||||
const payload = await Crypt.decryptJson(e.content)
|
||||
const payload = await engine.components.Crypt.decryptJson(e.content)
|
||||
|
||||
for (const key of Object.keys(payload)) {
|
||||
// Backwards compat from when we used to prefix id/pubkey
|
||||
const id = last(key.split("/"))
|
||||
const channel = Nip28.channels.key(id).get()
|
||||
const channel = this.channels.key(id).get()
|
||||
const last_checked = Math.max(payload[id], channel?.last_checked || 0)
|
||||
|
||||
// A bunch of junk got added to this setting. Integer keys, settings, etc
|
||||
@ -106,35 +97,35 @@ export class Nip28 {
|
||||
continue
|
||||
}
|
||||
|
||||
Nip28.channels.key(id).merge({last_checked})
|
||||
this.channels.key(id).merge({last_checked})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Events.addHandler(30078, async e => {
|
||||
engine.components.Events.addHandler(30078, async (e: Event) => {
|
||||
if (Tags.from(e).getMeta("d") === appDataKeys.NIP28_ROOMS_JOINED) {
|
||||
await tryJson(async () => {
|
||||
const channelIds = await Crypt.decryptJson(e.content)
|
||||
const channelIds = await engine.components.Crypt.decryptJson(e.content)
|
||||
|
||||
// Just a bug from when I was building the feature, remove someday
|
||||
if (!Array.isArray(channelIds)) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip28.channels.get().forEach(channel => {
|
||||
this.channels.get().forEach(channel => {
|
||||
if (channel.joined && !channelIds.includes(channel.id)) {
|
||||
Nip28.channels.key(channel.id).merge({joined: false})
|
||||
this.channels.key(channel.id).merge({joined: false})
|
||||
} else if (!channel.joined && channelIds.includes(channel.id)) {
|
||||
Nip28.channels.key(channel.id).merge({joined: true})
|
||||
this.channels.key(channel.id).merge({joined: true})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Events.addHandler(42, e => {
|
||||
if (Nip28.messages.key(e.id).exists()) {
|
||||
engine.components.Events.addHandler(42, (e: Event) => {
|
||||
if (this.messages.key(e.id).exists()) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -145,10 +136,10 @@ export class Nip28 {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = Nip28.channels.key(channelId).get()
|
||||
const channel = this.channels.key(channelId).get()
|
||||
const hints = uniq(tags.relays().concat(channel?.hints || []))
|
||||
|
||||
Nip28.messages.key(e.id).merge({
|
||||
this.messages.key(e.id).merge({
|
||||
channel: channelId,
|
||||
pubkey: e.pubkey,
|
||||
created_at: e.created_at,
|
||||
@ -156,10 +147,10 @@ export class Nip28 {
|
||||
tags: e.tags,
|
||||
})
|
||||
|
||||
if (e.pubkey === Keys.pubkey.get()) {
|
||||
Nip28.channels.key(channelId).merge({last_sent: e.created_at, hints})
|
||||
if (e.pubkey === engine.components.Keys.pubkey.get()) {
|
||||
this.channels.key(channelId).merge({last_sent: e.created_at, hints})
|
||||
} else {
|
||||
Nip28.channels.key(channelId).merge({last_received: e.created_at, hints})
|
||||
this.channels.key(channelId).merge({last_received: e.created_at, hints})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -2,13 +2,19 @@ import {Fetch, tryFunc} from "hurdak"
|
||||
import {now, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
|
||||
import {invoiceAmount} from "src/util/lightning"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Zapper} from "src/engine/types"
|
||||
import {collection} from "../util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Zapper, Event} from "src/engine/types"
|
||||
import {collection} from "src/engine/util/store"
|
||||
|
||||
const getLnUrl = address => {
|
||||
type ZapEvent = Event & {
|
||||
invoiceAmount: number
|
||||
request: Event
|
||||
}
|
||||
|
||||
const getLnUrl = (address: string): string => {
|
||||
// Try to parse it as a lud06 LNURL
|
||||
if (address.startsWith("lnurl1")) {
|
||||
return tryFunc(() => bech32ToHex(address))
|
||||
return tryFunc(() => bech32ToHex(address)) as string
|
||||
}
|
||||
|
||||
// Try to parse it as a lud16 address
|
||||
@ -22,31 +28,29 @@ const getLnUrl = address => {
|
||||
}
|
||||
|
||||
export class Nip57 {
|
||||
static contributeState() {
|
||||
const zappers = collection<Zapper>("pubkey")
|
||||
zappers = collection<Zapper>("pubkey")
|
||||
|
||||
return {zappers}
|
||||
}
|
||||
|
||||
static contributeActions({Nip57}) {
|
||||
const processZaps = (zaps, pubkey) => {
|
||||
const zapper = Nip57.zappers.key(pubkey).get()
|
||||
processZaps = (zaps: Event[], pubkey: string) => {
|
||||
const zapper = this.zappers.key(pubkey).get()
|
||||
|
||||
if (!zapper) {
|
||||
return []
|
||||
}
|
||||
|
||||
return zaps
|
||||
.map(zap => {
|
||||
const zapMeta = Tags.from(zap).asMeta()
|
||||
.map((zap: Event) => {
|
||||
const zapMeta = Tags.from(zap).asMeta() as {
|
||||
bolt11: string
|
||||
description: string
|
||||
}
|
||||
|
||||
return tryJson(() => ({
|
||||
...zap,
|
||||
invoiceAmount: invoiceAmount(zapMeta.bolt11),
|
||||
request: JSON.parse(zapMeta.description),
|
||||
}))
|
||||
})) as ZapEvent
|
||||
})
|
||||
.filter(zap => {
|
||||
.filter((zap: ZapEvent) => {
|
||||
if (!zap) {
|
||||
return false
|
||||
}
|
||||
@ -57,7 +61,10 @@ export class Nip57 {
|
||||
}
|
||||
|
||||
const {invoiceAmount, request} = zap
|
||||
const reqMeta = Tags.from(request).asMeta()
|
||||
const reqMeta = Tags.from(request).asMeta() as {
|
||||
amount?: string
|
||||
lnurl?: string
|
||||
}
|
||||
|
||||
// Verify that the zapper actually sent the requested amount (if it was supplied)
|
||||
if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
|
||||
@ -78,14 +85,11 @@ export class Nip57 {
|
||||
})
|
||||
}
|
||||
|
||||
return {processZaps}
|
||||
}
|
||||
|
||||
static initialize({Events, Nip57}) {
|
||||
Events.addHandler(0, e => {
|
||||
initialize(engine: Engine) {
|
||||
engine.components.Events.addHandler(0, (e: Event) => {
|
||||
tryJson(async () => {
|
||||
const kind0 = JSON.parse(e.content)
|
||||
const zapper = Nip57.zappers.key(e.pubkey)
|
||||
const zapper = this.zappers.key(e.pubkey)
|
||||
const address = (kind0.lud16 || kind0.lud06 || "").toLowerCase()
|
||||
|
||||
if (!address || e.created_at < zapper.get()?.created_at) {
|
||||
@ -98,7 +102,7 @@ export class Nip57 {
|
||||
return
|
||||
}
|
||||
|
||||
const result = await tryFunc(() => Fetch.fetchJson(url), true)
|
||||
const result = (await tryFunc(() => Fetch.fetchJson(url))) as any
|
||||
|
||||
if (!result?.allowsNostr || !result?.nostrPubkey) {
|
||||
return
|
||||
|
@ -3,23 +3,20 @@ import {first, chain, Fetch} from "hurdak"
|
||||
import {fuzzy, tryJson, now} from "src/util/misc"
|
||||
import {warn} from "src/util/logger"
|
||||
import {normalizeRelayUrl, findReplyId, isShareableRelay, Tags} from "src/util/nostr"
|
||||
import type {Relay, RelayInfo, RelayPolicy} from "src/engine/types"
|
||||
import {derived, collection} from "../util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Event, Relay, RelayInfo, RelayPolicy, RelayPolicyEntry} from "src/engine/types"
|
||||
import {derived, collection} from "src/engine/util/store"
|
||||
|
||||
export class Nip65 {
|
||||
static contributeState() {
|
||||
const relays = collection<Relay>("url")
|
||||
const policies = collection<RelayPolicy>("pubkey")
|
||||
engine: Engine
|
||||
relays = collection<Relay>("url")
|
||||
policies = collection<RelayPolicy>("pubkey")
|
||||
|
||||
return {relays, policies}
|
||||
}
|
||||
|
||||
static contributeActions({Env, Nip65, Network, Meta, User}) {
|
||||
const addRelay = url => {
|
||||
addRelay = (url: string) => {
|
||||
if (isShareableRelay(url)) {
|
||||
const relay = Nip65.relays.key(url).get()
|
||||
const relay = this.relays.key(url).get()
|
||||
|
||||
Nip65.relays.key(url).merge({
|
||||
this.relays.key(url).merge({
|
||||
count: inc(relay?.count || 0),
|
||||
first_seen: relay?.first_seen || now(),
|
||||
info: {
|
||||
@ -29,17 +26,17 @@ export class Nip65 {
|
||||
}
|
||||
}
|
||||
|
||||
const setPolicy = ({pubkey, created_at}, relays) => {
|
||||
setPolicy = ({pubkey, created_at}: {pubkey: string, created_at: number}, relays: RelayPolicyEntry[]) => {
|
||||
if (relays?.length > 0) {
|
||||
if (created_at < Nip65.policies.key(pubkey).get()?.created_at) {
|
||||
if (created_at < this.policies.key(pubkey).get()?.created_at) {
|
||||
return
|
||||
}
|
||||
|
||||
Nip65.policies.key(pubkey).merge({
|
||||
this.policies.key(pubkey).merge({
|
||||
created_at,
|
||||
updated_at: now(),
|
||||
relays: uniqBy(prop("url"), relays).map(relay => {
|
||||
addRelay(relay.url)
|
||||
relays: uniqBy(prop("url"), relays).map((relay: RelayPolicyEntry) => {
|
||||
this.addRelay(relay.url)
|
||||
|
||||
return {read: true, write: true, ...relay}
|
||||
}),
|
||||
@ -47,30 +44,31 @@ export class Nip65 {
|
||||
}
|
||||
}
|
||||
|
||||
const getRelay = (url: string): Relay => Nip65.relays.key(url).get() || {url}
|
||||
getRelay = (url: string): Relay => this.relays.key(url).get() || {url}
|
||||
|
||||
const getRelayInfo = (url: string): RelayInfo => getRelay(url)?.info || {}
|
||||
getRelayInfo = (url: string): RelayInfo => this.getRelay(url)?.info || {}
|
||||
|
||||
const displayRelay = ({url}) => last(url.split("://"))
|
||||
displayRelay = ({url}: Relay) => last(url.split("://"))
|
||||
|
||||
const searchRelays = derived(Nip65.relays, $relays => fuzzy($relays.values(), {keys: ["url"]}))
|
||||
searchRelays = derived(this.relays, $relays => fuzzy($relays.values(), {keys: ["url"]}))
|
||||
|
||||
const getSearchRelays = () => {
|
||||
const searchableRelayUrls = Nip65.relays
|
||||
getSearchRelays = () => {
|
||||
const searchableRelayUrls = this.relays
|
||||
.get()
|
||||
.filter(r => (r.info?.supported_nips || []).includes(50))
|
||||
.map(prop("url"))
|
||||
|
||||
return uniq(Env.SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8)
|
||||
return uniq(this.engine.Env.SEARCH_RELAYS.concat(searchableRelayUrls)).slice(0, 8)
|
||||
}
|
||||
|
||||
const getPubkeyRelays = (pubkey, mode = null) => {
|
||||
const relays = Nip65.policies.key(pubkey).get()?.relays || []
|
||||
getPubkeyRelays = (pubkey: string, mode: string = null) => {
|
||||
const relays = this.policies.key(pubkey).get()?.relays || []
|
||||
|
||||
return mode ? relays.filter(prop(mode)) : relays
|
||||
}
|
||||
|
||||
const getPubkeyRelayUrls = (pubkey, mode = null) => pluck("url", getPubkeyRelays(pubkey, mode))
|
||||
getPubkeyRelayUrls = (pubkey: string, mode: string = null) =>
|
||||
pluck("url", this.getPubkeyRelays(pubkey, mode))
|
||||
|
||||
// Smart relay selection
|
||||
//
|
||||
@ -86,12 +84,16 @@ export class Nip65 {
|
||||
// doesn't need to see.
|
||||
// 5) Advertise relays — write and read back your own relay list
|
||||
|
||||
const selectHints = (limit, hints) => {
|
||||
selectHints = (limit: number, hints: Iterable<string>) => {
|
||||
const seen = new Set()
|
||||
const ok = []
|
||||
const bad = []
|
||||
|
||||
for (const url of chain(hints, User.getRelayUrls("write"), Env.DEFAULT_RELAYS)) {
|
||||
for (const url of chain(
|
||||
hints,
|
||||
this.engine.components.User.getRelayUrls("write"),
|
||||
this.engine.Env.DEFAULT_RELAYS
|
||||
)) {
|
||||
if (seen.has(url)) {
|
||||
continue
|
||||
}
|
||||
@ -101,7 +103,10 @@ export class Nip65 {
|
||||
// Filter out relays that appear to be broken or slow
|
||||
if (!isShareableRelay(url)) {
|
||||
bad.push(url)
|
||||
} else if (Network.relayHasError(url) || first(Meta.getRelayQuality(url)) < 0.5) {
|
||||
} else if (
|
||||
this.engine.components.Network.relayHasError(url) ||
|
||||
this.engine.components.Meta.getRelayQuality(url)[0] < 0.5
|
||||
) {
|
||||
bad.push(url)
|
||||
} else {
|
||||
ok.push(url)
|
||||
@ -116,56 +121,56 @@ export class Nip65 {
|
||||
return ok.concat(bad).slice(0, limit)
|
||||
}
|
||||
|
||||
const hintSelector =
|
||||
generateHints =>
|
||||
(limit, ...args) =>
|
||||
selectHints(limit, generateHints(...args))
|
||||
hintSelector =
|
||||
(generateHints: (...args: any[]) => Iterable<string>) =>
|
||||
(limit: number, ...args: any[]) =>
|
||||
this.selectHints(limit, generateHints.call(this, ...args))
|
||||
|
||||
const getPubkeyHints = hintSelector(function* (pubkey, mode = "write") {
|
||||
getPubkeyHints = this.hintSelector(function* (this: Nip65, pubkey: string, mode = "write") {
|
||||
const other = mode === "write" ? "read" : "write"
|
||||
|
||||
yield* getPubkeyRelayUrls(pubkey, mode)
|
||||
yield* getPubkeyRelayUrls(pubkey, other)
|
||||
yield* this.getPubkeyRelayUrls(pubkey, mode)
|
||||
yield* this.getPubkeyRelayUrls(pubkey, other)
|
||||
})
|
||||
|
||||
const getEventHints = hintSelector(function* (event) {
|
||||
getEventHints = this.hintSelector(function* (this: Nip65, event: Event) {
|
||||
yield* event.seen_on || []
|
||||
yield* getPubkeyHints(null, event.pubkey)
|
||||
yield* this.getPubkeyHints(null, event.pubkey)
|
||||
})
|
||||
|
||||
// If we're looking for an event's children, the read relays the author has
|
||||
// advertised would be the most reliable option, since well-behaved clients
|
||||
// will write replies there. However, this may include spam, so we may want
|
||||
// to read from the current user's network's read relays instead.
|
||||
const getReplyHints = hintSelector(function* (event) {
|
||||
yield* getPubkeyRelayUrls(event.pubkey, "write")
|
||||
getReplyHints = this.hintSelector(function* (this: Nip65, event) {
|
||||
yield* this.getPubkeyRelayUrls(event.pubkey, "write")
|
||||
yield* event.seen_on || []
|
||||
yield* 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,
|
||||
// but we can also look at where the author of the note reads from
|
||||
const getParentHints = hintSelector(function* (event) {
|
||||
getParentHints = this.hintSelector(function* (this: Nip65, event) {
|
||||
const parentId = findReplyId(event)
|
||||
|
||||
yield* Tags.from(event).equals(parentId).relays()
|
||||
yield* event.seen_on || []
|
||||
yield* getPubkeyHints(null, event.pubkey, "read")
|
||||
yield* this.getPubkeyHints(null, event.pubkey, "read")
|
||||
})
|
||||
|
||||
// If we're replying or reacting to an event, we want the author to know, as well as
|
||||
// anyone else who is tagged in the original event or the reply. Get everyone's read
|
||||
// relays. Limit how many per pubkey we publish to though. We also want to advertise
|
||||
// our content to our followers, so publish to our write relays as well.
|
||||
const getPublishHints = (limit, event, extraRelays = []) => {
|
||||
getPublishHints = (limit: number, event: Event, extraRelays: string[] = []) => {
|
||||
const tags = Tags.from(event)
|
||||
const pubkeys = tags.type("p").values().all().concat(event.pubkey)
|
||||
const hintGroups = pubkeys.map(pubkey => getPubkeyHints(3, pubkey, "read"))
|
||||
const hintGroups = pubkeys.map(pubkey => this.getPubkeyHints(3, pubkey, "read"))
|
||||
|
||||
return mergeHints(limit, hintGroups.concat([extraRelays]))
|
||||
return this.mergeHints(limit, hintGroups.concat([extraRelays]))
|
||||
}
|
||||
|
||||
const mergeHints = (limit, groups) => {
|
||||
mergeHints = (limit: number, groups: string[][]) => {
|
||||
const scores = {} as Record<string, any>
|
||||
|
||||
for (const hints of groups) {
|
||||
@ -193,39 +198,20 @@ export class Nip65 {
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
return {
|
||||
addRelay,
|
||||
setPolicy,
|
||||
getRelay,
|
||||
getRelayInfo,
|
||||
displayRelay,
|
||||
searchRelays,
|
||||
getSearchRelays,
|
||||
getPubkeyRelays,
|
||||
getPubkeyRelayUrls,
|
||||
selectHints,
|
||||
hintSelector,
|
||||
getPubkeyHints,
|
||||
getEventHints,
|
||||
getReplyHints,
|
||||
getParentHints,
|
||||
getPublishHints,
|
||||
mergeHints,
|
||||
}
|
||||
}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
|
||||
static initialize({Env, Events, Nip65}) {
|
||||
Events.addHandler(2, e => {
|
||||
engine.components.Events.addHandler(2, e => {
|
||||
if (isShareableRelay(e.content)) {
|
||||
Nip65.addRelay(normalizeRelayUrl(e.content))
|
||||
this.addRelay(normalizeRelayUrl(e.content))
|
||||
}
|
||||
})
|
||||
|
||||
Events.addHandler(3, e => {
|
||||
Nip65.setPolicy(
|
||||
engine.components.Events.addHandler(3, e => {
|
||||
this.setPolicy(
|
||||
e,
|
||||
tryJson(() => {
|
||||
Object.entries(JSON.parse(e.content || ""))
|
||||
tryJson<RelayPolicyEntry[]>(() => {
|
||||
return Object.entries(JSON.parse(e.content || ""))
|
||||
.filter(([url]) => isShareableRelay(url))
|
||||
.map(([url, conditions]) => {
|
||||
// @ts-ignore
|
||||
@ -235,12 +221,12 @@ export class Nip65 {
|
||||
|
||||
return {url: normalizeRelayUrl(url), write, read}
|
||||
})
|
||||
})
|
||||
}) as RelayPolicyEntry[]
|
||||
)
|
||||
})
|
||||
|
||||
Events.addHandler(10002, e => {
|
||||
Nip65.setPolicy(
|
||||
engine.components.Events.addHandler(10002, e => {
|
||||
this.setPolicy(
|
||||
e,
|
||||
Tags.from(e)
|
||||
.type("r")
|
||||
@ -258,17 +244,17 @@ export class Nip65 {
|
||||
)
|
||||
})
|
||||
;(async () => {
|
||||
const {DEFAULT_RELAYS, FORCE_RELAYS, DUFFLEPUD_URL} = Env
|
||||
const {DEFAULT_RELAYS, FORCE_RELAYS, DUFFLEPUD_URL} = engine.Env
|
||||
|
||||
// Throw some hardcoded defaults in there
|
||||
DEFAULT_RELAYS.forEach(Nip65.addRelay)
|
||||
DEFAULT_RELAYS.forEach(this.addRelay)
|
||||
|
||||
// Load relays from nostr.watch via dufflepud
|
||||
if (FORCE_RELAYS.length === 0 && DUFFLEPUD_URL) {
|
||||
try {
|
||||
const json = await Fetch.fetchJson(DUFFLEPUD_URL + "/relay")
|
||||
|
||||
json.relays.filter(isShareableRelay).forEach(Nip65.addRelay)
|
||||
json.relays.filter(isShareableRelay).forEach(this.addRelay)
|
||||
} catch (e) {
|
||||
warn("Failed to fetch relays list", e)
|
||||
}
|
||||
|
@ -1,49 +1,51 @@
|
||||
import type {Event} from "src/engine/types"
|
||||
import {getEventHash} from "nostr-tools"
|
||||
import type {UnsignedEvent} from "nostr-tools"
|
||||
import {assoc} from "ramda"
|
||||
import {doPipe} from "hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import {Worker} from "../util/Worker"
|
||||
import type {Progress} from "src/engine/components/Network"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import type {Event} from "src/engine/types"
|
||||
|
||||
export class Outbox {
|
||||
static contributeState() {
|
||||
return {
|
||||
queue: new Worker<Event>(),
|
||||
}
|
||||
engine: Engine
|
||||
|
||||
prepEvent = async (rawEvent: Partial<Event>): Promise<Event> => {
|
||||
if (rawEvent.sig) {
|
||||
return rawEvent as Event
|
||||
}
|
||||
|
||||
static contributeActions({Keys, Network, User, Events}) {
|
||||
const prepEvent = async rawEvent => {
|
||||
return await doPipe(rawEvent, [
|
||||
assoc("created_at", now()),
|
||||
assoc("pubkey", Keys.pubkey.get()),
|
||||
e => ({...e, id: getEventHash(e)}),
|
||||
Keys.sign,
|
||||
])
|
||||
const event = {
|
||||
...rawEvent,
|
||||
created_at: now(),
|
||||
pubkey: this.engine.components.Keys.pubkey.get(),
|
||||
}
|
||||
|
||||
const publish = async (event, relays = null, onProgress = null, verb = "EVENT") => {
|
||||
if (!event.sig) {
|
||||
event = await prepEvent(event)
|
||||
event.id = getEventHash(event as UnsignedEvent)
|
||||
|
||||
return this.engine.components.Keys.sign(event as Event)
|
||||
}
|
||||
|
||||
publish = async (
|
||||
rawEvent: Partial<Event>,
|
||||
relays: string[] = null,
|
||||
onProgress: (p: Progress) => void = null,
|
||||
verb = "EVENT"
|
||||
) => {
|
||||
const event = rawEvent.sig ? (rawEvent as Event) : await this.prepEvent(rawEvent)
|
||||
|
||||
if (!relays) {
|
||||
relays = User.getRelayUrls("write")
|
||||
relays = this.engine.components.User.getRelayUrls("write")
|
||||
}
|
||||
|
||||
// return console.log(event)
|
||||
|
||||
const promise = Network.publish({event, relays, onProgress, verb})
|
||||
this.engine.components.Events.queue.push(event)
|
||||
|
||||
Events.queue.push(event)
|
||||
|
||||
return [event, promise]
|
||||
return [event, this.engine.components.Network.publish({event, relays, onProgress, verb})]
|
||||
}
|
||||
|
||||
return {prepEvent, publish}
|
||||
}
|
||||
|
||||
static initialize({Outbox}) {
|
||||
Outbox.queue.listen(({event}) => Outbox.publish(event))
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import {chunk, seconds, ensurePlural} from "hurdak"
|
||||
import {personKinds, appDataKeys} from "src/util/nostr"
|
||||
import {now} from "src/util/misc"
|
||||
import type {Filter} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
export type LoadPeopleOpts = {
|
||||
relays?: string[]
|
||||
@ -11,49 +12,50 @@ export type LoadPeopleOpts = {
|
||||
}
|
||||
|
||||
export class PubkeyLoader {
|
||||
static contributeActions({Directory, Nip65, User, Network}) {
|
||||
const attemptedPubkeys = new Set()
|
||||
engine: Engine
|
||||
|
||||
const getStalePubkeys = pubkeys => {
|
||||
attemptedPubkeys = new Set()
|
||||
|
||||
getStalePubkeys = (pubkeys: string[]) => {
|
||||
const stale = new Set()
|
||||
const since = now() - seconds(3, "hour")
|
||||
|
||||
for (const pubkey of pubkeys) {
|
||||
if (stale.has(pubkey) || attemptedPubkeys.has(pubkey)) {
|
||||
if (stale.has(pubkey) || this.attemptedPubkeys.has(pubkey)) {
|
||||
continue
|
||||
}
|
||||
|
||||
attemptedPubkeys.add(pubkey)
|
||||
this.attemptedPubkeys.add(pubkey)
|
||||
|
||||
if (Directory.profiles.key(pubkey).get()?.updated_at || 0 > since) {
|
||||
if (this.engine.components.Directory.profiles.key(pubkey).get()?.updated_at || 0 > since) {
|
||||
continue
|
||||
}
|
||||
|
||||
stale.add(pubkey)
|
||||
}
|
||||
|
||||
return stale
|
||||
return Array.from(stale)
|
||||
}
|
||||
|
||||
const load = async (
|
||||
pubkeyGroups,
|
||||
load = async (
|
||||
pubkeyGroups: string | string[],
|
||||
{relays, force, kinds = personKinds}: LoadPeopleOpts = {}
|
||||
) => {
|
||||
const rawPubkeys = ensurePlural(pubkeyGroups).reduce((a, b) => a.concat(b), [])
|
||||
const pubkeys = force ? uniq(rawPubkeys) : getStalePubkeys(rawPubkeys)
|
||||
const pubkeys = force ? uniq(rawPubkeys) : this.getStalePubkeys(rawPubkeys)
|
||||
|
||||
const getChunkRelays = chunk => {
|
||||
const getChunkRelays = (chunk: string[]) => {
|
||||
if (relays?.length > 0) {
|
||||
return relays
|
||||
}
|
||||
|
||||
return Nip65.mergeHints(
|
||||
User.getSetting("relay_limit"),
|
||||
chunk.map(pubkey => Nip65.getPubkeyHints(3, pubkey))
|
||||
return this.engine.components.Nip65.mergeHints(
|
||||
this.engine.components.User.getSetting("relay_limit"),
|
||||
chunk.map(pubkey => this.engine.components.Nip65.getPubkeyHints(3, pubkey))
|
||||
)
|
||||
}
|
||||
|
||||
const getChunkFilter = chunk => {
|
||||
const getChunkFilter = (chunk: string[]) => {
|
||||
const filter = [] as Filter[]
|
||||
|
||||
filter.push({kinds: without([30078], kinds), authors: chunk})
|
||||
@ -70,8 +72,8 @@ export class PubkeyLoader {
|
||||
await Promise.all(
|
||||
pluck(
|
||||
"complete",
|
||||
chunk(256, pubkeys).map(chunk =>
|
||||
Network.subscribe({
|
||||
chunk(256, pubkeys).map((chunk: string[]) =>
|
||||
this.engine.components.Network.subscribe({
|
||||
relays: getChunkRelays(chunk),
|
||||
filter: getChunkFilter(chunk),
|
||||
timeout: 10_000,
|
||||
@ -81,6 +83,7 @@ export class PubkeyLoader {
|
||||
)
|
||||
}
|
||||
|
||||
return {load}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
}
|
||||
}
|
||||
|
@ -1,118 +1,53 @@
|
||||
import {prop, pluck, splitAt, path as getPath, sortBy} from "ramda"
|
||||
import {sleep, defer, chunk, randomInt, throttle} from "hurdak"
|
||||
import {Storage as LocalStorage} from "hurdak"
|
||||
import {writable} from "../util/store"
|
||||
import {IndexedDB} from "../util/indexeddb"
|
||||
import type {Channel, Contact} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
import {writable} from "src/engine/util/store"
|
||||
import type {Writable, Collection} from "src/engine/util/store"
|
||||
import {IndexedDB} from "src/engine/util/indexeddb"
|
||||
|
||||
const localStorageKeys = ["Alerts.lastChecked", "Keys.pubkey", "Keys.keyState", "User.settings"]
|
||||
|
||||
const policy = (key, max, sort) => ({key, max, sort})
|
||||
const sortChannels = sortBy((e: Channel) =>
|
||||
e.joined ? 0 : -Math.max(e.last_checked || 0, e.last_sent || 0)
|
||||
)
|
||||
|
||||
const sortChannels = sortBy(e => (e.joined ? 0 : -Math.max(e.last_checked || 0, e.last_sent || 0)))
|
||||
const sortContacts = sortBy((e: Contact) => -Math.max(e.last_checked || 0, e.last_sent || 0))
|
||||
|
||||
const sortContacts = sortBy(e => -Math.max(e.last_checked || 0, e.last_sent || 0))
|
||||
const policy = (key: string, max: number, sort: (xs: any[]) => any[]) => ({key, max, sort})
|
||||
|
||||
const getCollectionPolicies = ({Storage}) => [
|
||||
policy("Alerts.events", 500, sortBy(prop("created_at"))),
|
||||
policy("Nip28.channels", 1000, sortChannels),
|
||||
policy("Nip28.messages", 10000, sortBy(prop("created_at"))),
|
||||
policy("Nip04.contacts", 1000, sortContacts),
|
||||
policy("Nip04.messages", 10000, sortBy(prop("created_at"))),
|
||||
policy("Content.topics", 1000, sortBy(prop("count"))),
|
||||
policy("Content.lists", 500, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Directory.profiles", 5000, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Events.cache", 5000, Storage.sortByPubkeyWhitelist(prop("created_at"))),
|
||||
policy("Nip02.graph", 5000, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip05.handles", 5000, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip57.zappers", 5000, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip65.relays", 2000, prop("count")),
|
||||
policy("Nip65.policies", 5000, Storage.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
]
|
||||
|
||||
// Sync helpers
|
||||
|
||||
const syncScalars = (engine, keys) => {
|
||||
for (const key of keys) {
|
||||
const store = getPath(key.split("."), engine)
|
||||
|
||||
if (Object.hasOwn(localStorage, key)) {
|
||||
store.set(LocalStorage.getJson(key))
|
||||
}
|
||||
|
||||
store.subscribe(throttle(300, $value => LocalStorage.setJson(key, $value)))
|
||||
}
|
||||
}
|
||||
|
||||
const syncCollections = async (engine, policies) => {
|
||||
for (const {key} of policies) {
|
||||
const store = getPath(key.split("."), engine)
|
||||
|
||||
store.set(await engine.Storage.db.getAll(key))
|
||||
|
||||
store.subscribe(
|
||||
throttle(randomInt(3000, 5000), async rows => {
|
||||
if (engine.Storage.dead.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Do it in small steps to avoid clogging stuff up
|
||||
for (const records of chunk(100, rows)) {
|
||||
await engine.Storage.db.bulkPut(key, records)
|
||||
await sleep(50)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Every so often randomly prune a store
|
||||
setInterval(() => {
|
||||
const {key, max, sort} = policies[Math.floor(policies.length * Math.random())]
|
||||
const store = getPath(key.split("."), engine)
|
||||
const data = store.get()
|
||||
|
||||
if (data.length < max * 1.1) {
|
||||
return
|
||||
}
|
||||
|
||||
const [discard, keep] = splitAt(max, sort(data))
|
||||
|
||||
store.set(keep)
|
||||
engine.Storage.db.bulkDelete(key, pluck(store.pk, discard))
|
||||
}, 30_000)
|
||||
}
|
||||
const getStore = (key: string, engine: Engine) =>
|
||||
getPath(key.split("."), engine.components) as Collection<any>
|
||||
|
||||
export class Storage {
|
||||
static contributeState() {
|
||||
const ready = defer()
|
||||
engine: Engine
|
||||
db: IndexedDB
|
||||
ready = defer()
|
||||
dead = writable(false)
|
||||
|
||||
const dead = writable(false)
|
||||
close = () => {
|
||||
this.dead.set(true)
|
||||
|
||||
return {db: null, ready, dead}
|
||||
return this.db?.close()
|
||||
}
|
||||
|
||||
static contributeActions({Storage, Nip02, Keys}) {
|
||||
const close = () => {
|
||||
Storage.dead.set(true)
|
||||
|
||||
return Storage.db?.close()
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
Storage.dead.set(true)
|
||||
clear = () => {
|
||||
this.dead.set(true)
|
||||
|
||||
localStorage.clear()
|
||||
|
||||
return Storage.db?.delete()
|
||||
return this.db?.delete()
|
||||
}
|
||||
|
||||
const getPubkeyWhitelist = () => {
|
||||
const pubkeys = Keys.keyState.get().map(prop("pubkey"))
|
||||
getPubkeyWhitelist = () => {
|
||||
const pubkeys = this.engine.components.Keys.keyState.get().map(prop("pubkey"))
|
||||
|
||||
return [new Set(pubkeys), Nip02.getFollowsSet(pubkeys)]
|
||||
return [new Set(pubkeys), this.engine.components.Nip02.getFollowsSet(pubkeys)]
|
||||
}
|
||||
|
||||
const sortByPubkeyWhitelist = fallback => rows => {
|
||||
const [pubkeys, follows] = getPubkeyWhitelist()
|
||||
sortByPubkeyWhitelist = (fallback: (x: any) => number) => (rows: Record<string, any>[]) => {
|
||||
const [pubkeys, follows] = this.getPubkeyWhitelist()
|
||||
|
||||
return sortBy(x => {
|
||||
if (pubkeys.has(x.pubkey)) {
|
||||
@ -127,17 +62,42 @@ export class Storage {
|
||||
}, rows)
|
||||
}
|
||||
|
||||
return {close, clear, getPubkeyWhitelist, sortByPubkeyWhitelist}
|
||||
async initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
|
||||
for (const key of localStorageKeys) {
|
||||
const store = getStore(key, engine)
|
||||
|
||||
if (Object.hasOwn(localStorage, key)) {
|
||||
store.set(LocalStorage.getJson(key))
|
||||
}
|
||||
|
||||
static async initialize(engine) {
|
||||
syncScalars(engine, localStorageKeys)
|
||||
store.subscribe(throttle(300, $value => LocalStorage.setJson(key, $value)))
|
||||
}
|
||||
|
||||
if (window.indexedDB) {
|
||||
const policies = getCollectionPolicies(engine)
|
||||
const policies = [
|
||||
policy("Alerts.events", 500, sortBy(prop("created_at"))),
|
||||
policy("Nip28.channels", 1000, sortChannels),
|
||||
policy("Nip28.messages", 10000, sortBy(prop("created_at"))),
|
||||
policy("Nip04.contacts", 1000, sortContacts),
|
||||
policy("Nip04.messages", 10000, sortBy(prop("created_at"))),
|
||||
policy("Content.topics", 1000, sortBy(prop("count"))),
|
||||
policy("Content.lists", 500, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Directory.profiles", 5000, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Events.cache", 5000, this.sortByPubkeyWhitelist(prop("created_at"))),
|
||||
policy("Nip02.graph", 5000, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip05.handles", 5000, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip57.zappers", 5000, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
policy("Nip65.relays", 2000, prop("count")),
|
||||
policy("Nip65.policies", 5000, this.sortByPubkeyWhitelist(prop("updated_at"))),
|
||||
]
|
||||
|
||||
const indexedDBStores = policies.map(({key}) => {
|
||||
const store = getPath(key.split("."), engine)
|
||||
this.db = new IndexedDB(
|
||||
"nostr-engine/Storage",
|
||||
1,
|
||||
policies.map(({key}) => {
|
||||
const store = getStore(key, engine)
|
||||
|
||||
return {
|
||||
name: key,
|
||||
@ -146,16 +106,49 @@ export class Storage {
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
engine.Storage.db = new IndexedDB("nostr-engine/Storage", 1, indexedDBStores)
|
||||
window.addEventListener("beforeunload", () => this.close())
|
||||
|
||||
window.addEventListener("beforeunload", () => engine.Storage.close())
|
||||
await this.db.open()
|
||||
|
||||
await engine.Storage.db.open()
|
||||
for (const {key} of policies) {
|
||||
const store = getStore(key, engine)
|
||||
|
||||
await syncCollections(engine, policies)
|
||||
store.set(await this.db.getAll(key))
|
||||
|
||||
store.subscribe(
|
||||
throttle(randomInt(3000, 5000), async <T>(rows: T) => {
|
||||
if (this.dead.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
engine.Storage.ready.resolve()
|
||||
// Do it in small steps to avoid clogging stuff up
|
||||
for (const records of chunk(100, rows as any[])) {
|
||||
await this.db.bulkPut(key, records)
|
||||
await sleep(50)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Every so often randomly prune a store
|
||||
setInterval(() => {
|
||||
const {key, max, sort} = policies[Math.floor(policies.length * Math.random())]
|
||||
const store = getStore(key, engine)
|
||||
const data = store.get()
|
||||
|
||||
if (data.length < max * 1.1) {
|
||||
return
|
||||
}
|
||||
|
||||
const [discard, keep] = splitAt(max, sort(data))
|
||||
|
||||
store.set(keep)
|
||||
this.db.bulkDelete(key, pluck(store.pk, discard))
|
||||
}, 30_000)
|
||||
}
|
||||
|
||||
this.ready.resolve()
|
||||
}
|
||||
}
|
||||
|
@ -1,116 +1,102 @@
|
||||
import {when, prop, uniq, pluck, fromPairs, whereEq, find, slice, reject} from "ramda"
|
||||
import {now} from "src/util/misc"
|
||||
import {Tags, appDataKeys, normalizeRelayUrl, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {writable} from "../util/store"
|
||||
import type {RelayPolicyEntry, List, Event} from "src/engine/types"
|
||||
import {writable} from "src/engine/util/store"
|
||||
import type {Writable} from "src/engine/util/store"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
export class User {
|
||||
static contributeState({Env}) {
|
||||
const settings = writable<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,
|
||||
})
|
||||
engine: Engine
|
||||
settings: Writable<Record<string, any>>
|
||||
|
||||
return {settings}
|
||||
}
|
||||
getPubkey = () => this.engine.components.Keys.pubkey.get()
|
||||
|
||||
static contributeActions({
|
||||
Builder,
|
||||
Content,
|
||||
Crypt,
|
||||
Directory,
|
||||
Events,
|
||||
Keys,
|
||||
Network,
|
||||
Outbox,
|
||||
Nip02,
|
||||
Nip04,
|
||||
Nip28,
|
||||
Nip65,
|
||||
User,
|
||||
}) {
|
||||
const getPubkey = () => Keys.pubkey.get()
|
||||
|
||||
const getStateKey = () => (Keys.canSign.get() ? getPubkey() : "anonymous")
|
||||
getStateKey = () => (this.engine.components.Keys.canSign.get() ? this.getPubkey() : "anonymous")
|
||||
|
||||
// Settings
|
||||
|
||||
const getSetting = k => User.settings.get()[k]
|
||||
getSetting = (k: string) => this.settings.get()[k]
|
||||
|
||||
const dufflepud = path => `${getSetting("dufflepud_url")}/${path}`
|
||||
dufflepud = (path: string) => `${this.getSetting("dufflepud_url")}/${path}`
|
||||
|
||||
const setSettings = async settings => {
|
||||
User.settings.update($settings => ({...$settings, ...settings}))
|
||||
setSettings = async (settings: Record<string, any>) => {
|
||||
this.settings.update($settings => ({...$settings, ...settings}))
|
||||
|
||||
if (Keys.canSign.get()) {
|
||||
if (this.engine.components.Keys.canSign.get()) {
|
||||
const d = appDataKeys.USER_SETTINGS
|
||||
const v = await Crypt.encryptJson(settings)
|
||||
const v = await this.engine.components.Crypt.encryptJson(settings)
|
||||
|
||||
return Outbox.queue.push({event: Builder.setAppData(d, v)})
|
||||
return this.engine.components.Outbox.publish(this.engine.components.Builder.setAppData(d, v))
|
||||
}
|
||||
}
|
||||
|
||||
const setAppData = async (d, content) => {
|
||||
const v = await Crypt.encryptJson(content)
|
||||
setAppData = async (d: string, content: any) => {
|
||||
const v = await this.engine.components.Crypt.encryptJson(content)
|
||||
|
||||
return Outbox.queue.push({event: Builder.setAppData(d, v)})
|
||||
return this.engine.components.Outbox.publish(this.engine.components.Builder.setAppData(d, v))
|
||||
}
|
||||
|
||||
// Nip65
|
||||
|
||||
const getRelays = (mode?: string) => Nip65.getPubkeyRelays(getStateKey(), mode)
|
||||
getRelays = (mode?: string) =>
|
||||
this.engine.components.Nip65.getPubkeyRelays(this.getStateKey(), mode)
|
||||
|
||||
const getRelayUrls = (mode?: string) => Nip65.getPubkeyRelayUrls(getStateKey(), mode)
|
||||
getRelayUrls = (mode?: string) =>
|
||||
this.engine.components.Nip65.getPubkeyRelayUrls(this.getStateKey(), mode)
|
||||
|
||||
const setRelays = relays => {
|
||||
if (Keys.canSign.get()) {
|
||||
return Outbox.queue.push({event: Builder.setRelays(relays)})
|
||||
setRelays = (relays: RelayPolicyEntry[]) => {
|
||||
if (this.engine.components.Keys.canSign.get()) {
|
||||
return this.engine.components.Outbox.publish(this.engine.components.Builder.setRelays(relays))
|
||||
} else {
|
||||
Nip65.setPolicy({pubkey: getStateKey(), created_at: now()}, relays)
|
||||
this.engine.components.Nip65.setPolicy(
|
||||
{pubkey: this.getStateKey(), created_at: now()},
|
||||
relays
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const addRelay = url => setRelays(getRelays().concat({url, read: true, write: true}))
|
||||
addRelay = (url: string) => this.setRelays(this.getRelays().concat({url, read: true, write: true}))
|
||||
|
||||
const removeRelay = url =>
|
||||
setRelays(reject(whereEq({url: normalizeRelayUrl(url)}), getRelays()))
|
||||
removeRelay = (url: string) =>
|
||||
this.setRelays(reject(whereEq({url: normalizeRelayUrl(url)}), this.getRelays()))
|
||||
|
||||
const setRelayPolicy = (url, policy) =>
|
||||
setRelays(getRelays().map(when(whereEq({url}), p => ({...p, ...policy}))))
|
||||
setRelayPolicy = (url: string, policy: Partial<RelayPolicyEntry>) =>
|
||||
this.setRelays(this.getRelays().map(when(whereEq({url}), p => ({...p, ...policy}))))
|
||||
|
||||
// Nip02
|
||||
|
||||
const getPetnames = () => Nip02.getPetnames(getStateKey())
|
||||
getPetnames = () => this.engine.components.Nip02.getPetnames(this.getStateKey())
|
||||
|
||||
const getMutedTags = () => Nip02.getMutedTags(getStateKey())
|
||||
getMutedTags = () => this.engine.components.Nip02.getMutedTags(this.getStateKey())
|
||||
|
||||
const getFollowsSet = () => Nip02.getFollowsSet(getStateKey())
|
||||
getFollowsSet = () => this.engine.components.Nip02.getFollowsSet(this.getStateKey())
|
||||
|
||||
const getMutesSet = () => Nip02.getMutesSet(getStateKey())
|
||||
getMutesSet = () => this.engine.components.Nip02.getMutesSet(this.getStateKey())
|
||||
|
||||
const getFollows = () => Nip02.getFollows(getStateKey())
|
||||
getFollows = () => this.engine.components.Nip02.getFollows(this.getStateKey())
|
||||
|
||||
const getMutes = () => Nip02.getMutes(getStateKey())
|
||||
getMutes = () => this.engine.components.Nip02.getMutes(this.getStateKey())
|
||||
|
||||
const getNetworkSet = () => Nip02.getNetworkSet(getStateKey())
|
||||
getNetworkSet = () => this.engine.components.Nip02.getNetworkSet(this.getStateKey())
|
||||
|
||||
const getNetwork = () => Nip02.getNetwork(getStateKey())
|
||||
getNetwork = () => this.engine.components.Nip02.getNetwork(this.getStateKey())
|
||||
|
||||
const isFollowing = pubkey => Nip02.isFollowing(getStateKey(), pubkey)
|
||||
isFollowing = (pubkey: string) => this.engine.components.Nip02.isFollowing(this.getStateKey(), pubkey)
|
||||
|
||||
const isIgnoring = pubkeyOrEventId => Nip02.isIgnoring(getStateKey(), pubkeyOrEventId)
|
||||
isIgnoring = (pubkeyOrEventId: string) =>
|
||||
this.engine.components.Nip02.isIgnoring(this.getStateKey(), pubkeyOrEventId)
|
||||
|
||||
const setProfile = $profile => Outbox.queue.push({event: Builder.setProfile($profile)})
|
||||
setProfile = ($profile: Record<string, any>) =>
|
||||
this.engine.components.Outbox.publish(this.engine.components.Builder.setProfile($profile))
|
||||
|
||||
const setPetnames = async $petnames => {
|
||||
if (Keys.canSign.get()) {
|
||||
await Outbox.queue.push({event: Builder.setPetnames($petnames)})
|
||||
setPetnames = async ($petnames: string[][]) => {
|
||||
if (this.engine.components.Keys.canSign.get()) {
|
||||
await this.engine.components.Outbox.publish(
|
||||
this.engine.components.Builder.setPetnames($petnames)
|
||||
)
|
||||
} else {
|
||||
Nip02.graph.key(getStateKey()).merge({
|
||||
this.engine.components.Nip02.graph.key(this.getStateKey()).merge({
|
||||
updated_at: now(),
|
||||
petnames_updated_at: now(),
|
||||
petnames: $petnames,
|
||||
@ -118,28 +104,31 @@ export class User {
|
||||
}
|
||||
}
|
||||
|
||||
const follow = pubkey =>
|
||||
setPetnames(
|
||||
getPetnames()
|
||||
follow = (pubkey: string) =>
|
||||
this.setPetnames(
|
||||
this.getPetnames()
|
||||
.filter(t => t[1] !== pubkey)
|
||||
.concat([Builder.mention(pubkey)])
|
||||
.concat([this.engine.components.Builder.mention(pubkey)])
|
||||
)
|
||||
|
||||
const unfollow = pubkey => setPetnames(reject(t => t[1] === pubkey, getPetnames()))
|
||||
unfollow = (pubkey: string) =>
|
||||
this.setPetnames(reject((t: string[]) => t[1] === pubkey, this.getPetnames()))
|
||||
|
||||
const isMuted = e => {
|
||||
const m = getMutesSet()
|
||||
isMuted = (e: Event) => {
|
||||
const m = this.getMutesSet()
|
||||
|
||||
return find(t => m.has(t), [e.id, e.pubkey, findReplyId(e), findRootId(e)])
|
||||
}
|
||||
|
||||
const applyMutes = events => reject(isMuted, events)
|
||||
applyMutes = (events: Event[]) => reject(this.isMuted, events)
|
||||
|
||||
const setMutes = async $mutes => {
|
||||
if (Keys.canSign.get()) {
|
||||
await Outbox.queue.push({event: Builder.setMutes($mutes.map(slice(0, 2)))})
|
||||
setMutes = async ($mutes: string[][]) => {
|
||||
if (this.engine.components.Keys.canSign.get()) {
|
||||
await this.engine.components.Outbox.publish(
|
||||
this.engine.components.Builder.setMutes($mutes.map(t => t.slice(0, 2)))
|
||||
)
|
||||
} else {
|
||||
Nip02.graph.key(getStateKey()).merge({
|
||||
this.engine.components.Nip02.graph.key(this.getStateKey()).merge({
|
||||
updated_at: now(),
|
||||
mutes_updated_at: now(),
|
||||
mutes: $mutes,
|
||||
@ -147,127 +136,101 @@ export class User {
|
||||
}
|
||||
}
|
||||
|
||||
const mute = (type, value) =>
|
||||
setMutes(reject(t => t[1] === value, getMutedTags()).concat([[type, value]]))
|
||||
mute = (type: string, value: string) =>
|
||||
this.setMutes(reject((t: string[]) => t[1] === value, this.getMutedTags()).concat([[type, value]]))
|
||||
|
||||
const unmute = target => setMutes(reject(t => t[1] === target, getMutedTags()))
|
||||
unmute = (target: string) => this.setMutes(reject((t: string[]) => t[1] === target, this.getMutedTags()))
|
||||
|
||||
// Content
|
||||
// Lists
|
||||
|
||||
const getLists = f => Content.getLists(l => l.pubkey === getStateKey() && (f ? f(l) : true))
|
||||
getLists = (f?: (l: List) => boolean) =>
|
||||
this.engine.components.Content.getLists(
|
||||
l => l.pubkey === this.getStateKey() && (f ? f(l) : true)
|
||||
)
|
||||
|
||||
const putList = (name, params, relays) =>
|
||||
Outbox.queue.push({
|
||||
event: Builder.createList([["d", name]].concat(params).concat(relays)),
|
||||
})
|
||||
putList = (name: string, params: string[][], relays: string[]) =>
|
||||
this.engine.components.Outbox.publish(
|
||||
this.engine.components.Builder.createList([["d", name]].concat(params).concat(relays))
|
||||
)
|
||||
|
||||
const removeList = naddr => Outbox.queue.push({event: Builder.deleteNaddrs([naddr])})
|
||||
removeList = (naddr: string) =>
|
||||
this.engine.components.Outbox.publish(this.engine.components.Builder.deleteNaddrs([naddr]))
|
||||
|
||||
// Messages
|
||||
|
||||
const markAllMessagesRead = () => {
|
||||
markAllMessagesRead = () => {
|
||||
const lastChecked = fromPairs(
|
||||
uniq(pluck("contact", Nip04.messages.get())).map(k => [k, now()])
|
||||
uniq(pluck("contact", this.engine.components.Nip04.messages.get())).map(k => [k, now()])
|
||||
)
|
||||
|
||||
return setAppData(appDataKeys.NIP04_LAST_CHECKED, lastChecked)
|
||||
return this.setAppData(appDataKeys.NIP04_LAST_CHECKED, lastChecked)
|
||||
}
|
||||
|
||||
const setContactLastChecked = pubkey => {
|
||||
setContactLastChecked = (pubkey: string) => {
|
||||
const lastChecked = fromPairs(
|
||||
Nip04.contacts
|
||||
this.engine.components.Nip04.contacts
|
||||
.get()
|
||||
.filter(prop("last_checked"))
|
||||
.map(r => [r.id, r.last_checked])
|
||||
)
|
||||
|
||||
return setAppData(appDataKeys.NIP04_LAST_CHECKED, {...lastChecked, [pubkey]: now()})
|
||||
return this.setAppData(appDataKeys.NIP04_LAST_CHECKED, {...lastChecked, [pubkey]: now()})
|
||||
}
|
||||
|
||||
// Nip28
|
||||
// Channels
|
||||
|
||||
const setChannelLastChecked = id => {
|
||||
setChannelLastChecked = (id: string) => {
|
||||
const lastChecked = fromPairs(
|
||||
Nip28.channels
|
||||
this.engine.components.Nip28.channels
|
||||
.get()
|
||||
.filter(prop("last_checked"))
|
||||
.map(r => [r.id, r.last_checked])
|
||||
)
|
||||
|
||||
return setAppData(appDataKeys.NIP28_LAST_CHECKED, {...lastChecked, [id]: now()})
|
||||
return this.setAppData(appDataKeys.NIP28_LAST_CHECKED, {...lastChecked, [id]: now()})
|
||||
}
|
||||
|
||||
const saveChannels = () =>
|
||||
setAppData(
|
||||
saveChannels = () =>
|
||||
this.setAppData(
|
||||
appDataKeys.NIP28_ROOMS_JOINED,
|
||||
pluck("id", Nip28.channels.get().filter(whereEq({joined: true})))
|
||||
pluck("id", this.engine.components.Nip28.channels.get().filter(whereEq({joined: true})))
|
||||
)
|
||||
|
||||
const joinChannel = id => {
|
||||
Nip28.channels.key(id).merge({joined: false})
|
||||
joinChannel = (id: string) => {
|
||||
this.engine.components.Nip28.channels.key(id).merge({joined: false})
|
||||
|
||||
return saveChannels()
|
||||
return this.saveChannels()
|
||||
}
|
||||
|
||||
const leaveChannel = id => {
|
||||
Nip28.channels.key(id).merge({joined: false})
|
||||
Nip28.messages.reject(m => m.channel === id)
|
||||
leaveChannel = (id: string) => {
|
||||
this.engine.components.Nip28.channels.key(id).merge({joined: false})
|
||||
this.engine.components.Nip28.messages.reject(m => m.channel === id)
|
||||
|
||||
return saveChannels()
|
||||
return this.saveChannels()
|
||||
}
|
||||
|
||||
return {
|
||||
getPubkey,
|
||||
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,
|
||||
markAllMessagesRead,
|
||||
setContactLastChecked,
|
||||
setChannelLastChecked,
|
||||
joinChannel,
|
||||
leaveChannel,
|
||||
}
|
||||
}
|
||||
initialize(engine: Engine) {
|
||||
this.engine = engine
|
||||
|
||||
static initialize({Events, Crypt, User}) {
|
||||
Events.addHandler(30078, async e => {
|
||||
this.settings = writable<Record<string, any>>({
|
||||
last_updated: 0,
|
||||
relay_limit: 10,
|
||||
default_zap: 21,
|
||||
show_media: true,
|
||||
report_analytics: true,
|
||||
dufflepud_url: engine.Env.DUFFLEPUD_URL,
|
||||
multiplextr_url: engine.Env.MULTIPLEXTR_URL,
|
||||
})
|
||||
|
||||
engine.components.Events.addHandler(30078, async e => {
|
||||
if (
|
||||
Tags.from(e).getMeta("d") === "coracle/settings/v1" &&
|
||||
e.created_at > User.getSetting("last_updated")
|
||||
e.created_at > this.getSetting("last_updated")
|
||||
) {
|
||||
const updates = await Crypt.decryptJson(e.content)
|
||||
const updates = await engine.components.Crypt.decryptJson(e.content)
|
||||
|
||||
if (updates) {
|
||||
User.settings.update($settings => ({
|
||||
this.settings.update($settings => ({
|
||||
...$settings,
|
||||
...updates,
|
||||
last_updated: e.created_at,
|
||||
|
@ -1,73 +1,21 @@
|
||||
import {Alerts} from "./components/Alerts"
|
||||
import {Builder} from "./components/Builder"
|
||||
import {Content} from "./components/Content"
|
||||
import {Crypt} from "./components/Crypt"
|
||||
import {Directory} from "./components/Directory"
|
||||
import {Events} from "./components/Events"
|
||||
import {Keys} from "./components/Keys"
|
||||
import {Meta} from "./components/Meta"
|
||||
import {Network} from "./components/Network"
|
||||
import {Nip02} from "./components/Nip02"
|
||||
import {Nip04} from "./components/Nip04"
|
||||
import {Nip05} from "./components/Nip05"
|
||||
import {Nip28} from "./components/Nip28"
|
||||
import {Nip57} from "./components/Nip57"
|
||||
import {Nip65} from "./components/Nip65"
|
||||
import {Outbox} from "./components/Outbox"
|
||||
import {PubkeyLoader} from "./components/PubkeyLoader"
|
||||
import {Storage} from "./components/Storage"
|
||||
import {User} from "./components/User"
|
||||
|
||||
export const createEngine = (engine, components) => {
|
||||
for (const component of components) {
|
||||
engine[component.name] = {}
|
||||
}
|
||||
|
||||
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)])
|
||||
|
||||
for (const [component, selectors] of componentSelectors) {
|
||||
Object.assign(engine[component.name], selectors)
|
||||
}
|
||||
|
||||
const componentActions = components.map(c => [c, c.contributeActions?.(engine)])
|
||||
|
||||
for (const [component, actions] of componentActions) {
|
||||
Object.assign(engine[component.name], actions)
|
||||
}
|
||||
|
||||
for (const component of components) {
|
||||
component.initialize?.(engine)
|
||||
}
|
||||
|
||||
return engine
|
||||
}
|
||||
|
||||
export const createDefaultEngine = Env => {
|
||||
return createEngine({Env}, [
|
||||
Alerts,
|
||||
Builder,
|
||||
Content,
|
||||
Crypt,
|
||||
Directory,
|
||||
Events,
|
||||
Keys,
|
||||
Meta,
|
||||
Network,
|
||||
Nip02,
|
||||
Nip04,
|
||||
Nip05,
|
||||
Nip28,
|
||||
Nip57,
|
||||
Nip65,
|
||||
Outbox,
|
||||
PubkeyLoader,
|
||||
Storage,
|
||||
User,
|
||||
])
|
||||
}
|
||||
export * from "./types"
|
||||
export {Engine} from "./Engine"
|
||||
export {Alerts} from "./components/Alerts"
|
||||
export {Builder} from "./components/Builder"
|
||||
export {Content} from "./components/Content"
|
||||
export {Crypt} from "./components/Crypt"
|
||||
export {Directory} from "./components/Directory"
|
||||
export {Events} from "./components/Events"
|
||||
export {Keys} from "./components/Keys"
|
||||
export {Meta} from "./components/Meta"
|
||||
export {Network} from "./components/Network"
|
||||
export {Nip02} from "./components/Nip02"
|
||||
export {Nip04} from "./components/Nip04"
|
||||
export {Nip05} from "./components/Nip05"
|
||||
export {Nip28} from "./components/Nip28"
|
||||
export {Nip57} from "./components/Nip57"
|
||||
export {Nip65} from "./components/Nip65"
|
||||
export {Outbox} from "./components/Outbox"
|
||||
export {PubkeyLoader} from "./components/PubkeyLoader"
|
||||
export {Storage} from "./components/Storage"
|
||||
export {User} from "./components/User"
|
||||
|
@ -6,7 +6,7 @@ export type Event = NostrToolsEvent & {
|
||||
|
||||
export type DisplayEvent = Event & {
|
||||
zaps: Event[]
|
||||
replies: Event[]
|
||||
replies: DisplayEvent[]
|
||||
reactions: Event[]
|
||||
matchesFilter?: boolean
|
||||
}
|
||||
@ -47,6 +47,7 @@ export type RelayInfo = {
|
||||
contact?: string
|
||||
description?: string
|
||||
last_checked?: number
|
||||
supported_nips?: number[]
|
||||
limitation?: {
|
||||
payment_required?: boolean
|
||||
auth_required?: boolean
|
||||
@ -60,17 +61,24 @@ export type Relay = {
|
||||
info?: RelayInfo
|
||||
}
|
||||
|
||||
export type RelayPolicyEntry = {
|
||||
url: string
|
||||
read: boolean
|
||||
write: boolean
|
||||
}
|
||||
|
||||
export type RelayPolicy = {
|
||||
pubkey: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
relays: {url: string; read: boolean; write: boolean}[]
|
||||
relays: RelayPolicyEntry[]
|
||||
}
|
||||
|
||||
export type RelayStat = {
|
||||
url: string
|
||||
error?: string
|
||||
last_opened?: number
|
||||
last_closed?: number
|
||||
last_activity?: number
|
||||
last_publish?: number
|
||||
last_sub?: number
|
||||
@ -106,6 +114,7 @@ export type Profile = {
|
||||
|
||||
export type Channel = {
|
||||
id: string
|
||||
name?: string
|
||||
pubkey: string
|
||||
updated_at: number
|
||||
last_sent?: number
|
||||
@ -127,7 +136,8 @@ export type Contact = {
|
||||
|
||||
export type Message = {
|
||||
id: string
|
||||
channel: string
|
||||
contact?: string
|
||||
channel?: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
content: string
|
||||
@ -148,3 +158,20 @@ export type List = {
|
||||
created_at: number
|
||||
deleted_at?: number
|
||||
}
|
||||
|
||||
export type Env = {
|
||||
DUFFLEPUD_URL: string
|
||||
MULTIPLEXTR_URL: string
|
||||
FORCE_RELAYS: string[]
|
||||
COUNT_RELAYS: string[]
|
||||
SEARCH_RELAYS: string[]
|
||||
DEFAULT_RELAYS: string[]
|
||||
ENABLE_ZAPS: boolean
|
||||
}
|
||||
|
||||
export type KeyState = {
|
||||
method: string
|
||||
pubkey: string
|
||||
privkey: string | null
|
||||
bunkerKey: string | null
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import {all, prop, mergeLeft, identity, sortBy} from "ramda"
|
||||
import {ensurePlural, first} from "hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import type {Filter, Event} from "../types"
|
||||
import type {Filter, Event} from "src/engine/types"
|
||||
import type {Subscription} from "src/engine/util/Subscription"
|
||||
import type {Network} from "src/engine/components/Network"
|
||||
|
||||
export type CursorOpts = {
|
||||
relay: string
|
||||
filter: Filter | Filter[]
|
||||
subscribe: (opts: any) => void
|
||||
Network: Network
|
||||
onEvent?: (e: Event) => void
|
||||
}
|
||||
|
||||
@ -23,7 +25,7 @@ export class Cursor {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
load(n) {
|
||||
load(n: number) {
|
||||
const limit = n - this.buffer.length
|
||||
|
||||
// If we're already loading, or we have enough buffered, do nothing
|
||||
@ -38,11 +40,11 @@ export class Cursor {
|
||||
|
||||
let count = 0
|
||||
|
||||
return this.opts.subscribe({
|
||||
return this.opts.Network.subscribe({
|
||||
timeout: 4000,
|
||||
relays: [relay],
|
||||
filter: ensurePlural(filter).map(mergeLeft({until, limit})),
|
||||
onEvent: event => {
|
||||
onEvent: (event: Event) => {
|
||||
this.until = Math.min(until, event.created_at)
|
||||
this.buffer.push(event)
|
||||
|
||||
@ -87,7 +89,7 @@ export class MultiCursor {
|
||||
this.#cursors = cursors
|
||||
}
|
||||
|
||||
load(limit) {
|
||||
load(limit: number) {
|
||||
return this.#cursors.map(c => c.load(limit)).filter(identity)
|
||||
}
|
||||
|
||||
@ -99,7 +101,7 @@ export class MultiCursor {
|
||||
return this.#cursors.reduce((n, c) => n + c.buffer.length, 0)
|
||||
}
|
||||
|
||||
take(n) {
|
||||
take(n: number): [Subscription[], Event[]] {
|
||||
const events = []
|
||||
|
||||
while (events.length < n) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {matchFilters} from "nostr-tools"
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {
|
||||
map,
|
||||
omit,
|
||||
pick,
|
||||
pluck,
|
||||
@ -17,14 +18,17 @@ import {
|
||||
reject,
|
||||
} from "ramda"
|
||||
import {ensurePlural, seconds, sleep, batch, union, chunk, doPipe} from "hurdak"
|
||||
import {now} from "src/util/misc"
|
||||
import {now, pushToKey} from "src/util/misc"
|
||||
import {findReplyId, Tags, noteKinds} from "src/util/nostr"
|
||||
import {collection} from "./store"
|
||||
import type {Collection} from "./store"
|
||||
import {Cursor, MultiCursor} from "./Cursor"
|
||||
import type {Event, DisplayEvent, Filter} from "../types"
|
||||
import {Cursor, MultiCursor} from "src/engine/util/Cursor"
|
||||
import type {Collection} from "src/engine/util/store"
|
||||
import type {Subscription} from "src/engine/util/Subscription"
|
||||
import type {Event, DisplayEvent, Filter} from "src/engine/types"
|
||||
import type {Engine} from "src/engine/Engine"
|
||||
|
||||
const fromDisplayEvent = omit(["zaps", "likes", "replies", "matchesFilter"])
|
||||
const fromDisplayEvent = (e: DisplayEvent): Event =>
|
||||
omit(["zaps", "likes", "replies", "matchesFilter"], e)
|
||||
|
||||
export type FeedOpts = {
|
||||
limit?: number
|
||||
@ -33,7 +37,7 @@ export type FeedOpts = {
|
||||
filter: Filter | Filter[]
|
||||
onEvent?: (e: Event) => void
|
||||
shouldLoadParents?: boolean
|
||||
engine: any
|
||||
engine: Engine
|
||||
}
|
||||
|
||||
export class Feed {
|
||||
@ -67,7 +71,7 @@ export class Feed {
|
||||
|
||||
// Utils
|
||||
|
||||
addSubs(key, subs) {
|
||||
addSubs(key: string, subs: Array<Subscription>) {
|
||||
for (const sub of ensurePlural(subs)) {
|
||||
this.subs[key].push(sub)
|
||||
|
||||
@ -77,7 +81,7 @@ export class Feed {
|
||||
}
|
||||
}
|
||||
|
||||
getAllSubs(only = null) {
|
||||
getAllSubs(only: string[] = []) {
|
||||
return flatten(Object.values(only ? pick(only, this.subs) : this.subs))
|
||||
}
|
||||
|
||||
@ -85,24 +89,24 @@ export class Feed {
|
||||
return this.opts.engine.Env.ENABLE_ZAPS ? [1, 7, 9735] : [1, 7]
|
||||
}
|
||||
|
||||
matchFilters(e) {
|
||||
matchFilters(e: Event) {
|
||||
return matchFilters(ensurePlural(this.opts.filter), e)
|
||||
}
|
||||
|
||||
isTextNote(e) {
|
||||
isTextNote(e: Event) {
|
||||
return noteKinds.includes(e.kind)
|
||||
}
|
||||
|
||||
isMissingParent = e => {
|
||||
isMissingParent = (e: Event) => {
|
||||
const parentId = findReplyId(e)
|
||||
|
||||
return parentId && this.matchFilters(e) && !this.context.key(parentId).exists()
|
||||
}
|
||||
|
||||
preprocessEvents = events => {
|
||||
const {User} = this.opts.engine
|
||||
preprocessEvents = (events: Event[]) => {
|
||||
const {User} = this.opts.engine.components
|
||||
|
||||
events = reject(e => this.seen.has(e.id) || User.isMuted(e), events)
|
||||
events = reject((e: Event) => this.seen.has(e.id) || User.isMuted(e), events)
|
||||
|
||||
for (const event of events) {
|
||||
this.seen.add(event.id)
|
||||
@ -111,19 +115,19 @@ export class Feed {
|
||||
return events
|
||||
}
|
||||
|
||||
mergeHints(groups) {
|
||||
const {Nip65, User} = this.opts.engine
|
||||
mergeHints(groups: string[][]) {
|
||||
const {Nip65, User} = this.opts.engine.components
|
||||
|
||||
return Nip65.mergeHints(User.getSetting("relay_limit"), groups)
|
||||
}
|
||||
|
||||
applyContext(notes, context, substituteParents = false) {
|
||||
applyContext(notes: Event[], context: Event[], substituteParents = false) {
|
||||
const parentIds = new Set(notes.map(findReplyId).filter(identity))
|
||||
const forceShow = union(new Set(pluck("id", notes)), parentIds)
|
||||
const contextById = {}
|
||||
const zapsByParentId = {}
|
||||
const reactionsByParentId = {}
|
||||
const repliesByParentId = {}
|
||||
const contextById = {} as Record<string, Event>
|
||||
const zapsByParentId = {} as Record<string, Event[]>
|
||||
const reactionsByParentId = {} as Record<string, Event[]>
|
||||
const repliesByParentId = {} as Record<string, Event[]>
|
||||
|
||||
for (const event of context.concat(notes)) {
|
||||
const parentId = findReplyId(event)
|
||||
@ -135,28 +139,25 @@ export class Feed {
|
||||
contextById[event.id] = event
|
||||
|
||||
if (event.kind === 9735) {
|
||||
zapsByParentId[parentId] = zapsByParentId[parentId] || []
|
||||
zapsByParentId[parentId].push(event)
|
||||
pushToKey(zapsByParentId, parentId, event)
|
||||
} else if (event.kind === 7) {
|
||||
reactionsByParentId[parentId] = reactionsByParentId[parentId] || []
|
||||
reactionsByParentId[parentId].push(event)
|
||||
pushToKey(reactionsByParentId, parentId, event)
|
||||
} else {
|
||||
repliesByParentId[parentId] = repliesByParentId[parentId] || []
|
||||
repliesByParentId[parentId].push(event)
|
||||
pushToKey(repliesByParentId, parentId, event)
|
||||
}
|
||||
}
|
||||
|
||||
const annotate = (note: DisplayEvent) => {
|
||||
const {replies = [], reactions = [], zaps = []} = note
|
||||
const annotate = (note: Event): DisplayEvent => {
|
||||
const {replies = [], reactions = [], zaps = []} = note as DisplayEvent
|
||||
const combinedZaps = zaps.concat(zapsByParentId[note.id] || [])
|
||||
const combinedReactions = reactions.concat(reactionsByParentId[note.id] || [])
|
||||
const combinedReplies = replies.concat(repliesByParentId[note.id] || [])
|
||||
const combinedReplies = replies.concat(map(annotate, repliesByParentId[note.id] || []))
|
||||
|
||||
return {
|
||||
...note,
|
||||
zaps: uniqBy(prop("id"), combinedZaps),
|
||||
reactions: uniqBy(prop("id"), combinedReactions),
|
||||
replies: sortBy(e => -e.created_at, uniqBy(prop("id"), combinedReplies.map(annotate))),
|
||||
replies: sortBy((e: Event) => -e.created_at, uniqBy(prop("id"), combinedReplies)),
|
||||
matchesFilter: forceShow.has(note.id) || this.matchFilters(note),
|
||||
}
|
||||
}
|
||||
@ -180,17 +181,17 @@ export class Feed {
|
||||
|
||||
// Context loaders
|
||||
|
||||
loadPubkeys = events => {
|
||||
this.opts.engine.PubkeyLoader.load(
|
||||
events.filter(this.isTextNote).flatMap(e => Tags.from(e).pubkeys().concat(e.pubkey))
|
||||
loadPubkeys = (events: Event[]) => {
|
||||
this.opts.engine.components.PubkeyLoader.load(
|
||||
events.filter(this.isTextNote).flatMap((e: Event) => Tags.from(e).pubkeys().concat(e.pubkey))
|
||||
)
|
||||
}
|
||||
|
||||
loadParents = events => {
|
||||
const {Network, Nip65} = this.opts.engine
|
||||
loadParents = (events: Event[]) => {
|
||||
const {Network, Nip65} = this.opts.engine.components
|
||||
const parentsInfo = events
|
||||
.map(e => ({id: findReplyId(e), hints: Nip65.getParentHints(10, e)}))
|
||||
.filter(({id}) => id && !this.seen.has(id))
|
||||
.map((e: Event) => ({id: findReplyId(e), hints: Nip65.getParentHints(10, e)}))
|
||||
.filter(({id}: any) => id && !this.seen.has(id))
|
||||
|
||||
if (parentsInfo.length > 0) {
|
||||
this.addSubs("context", [
|
||||
@ -198,14 +199,14 @@ export class Feed {
|
||||
timeout: 3000,
|
||||
filter: {ids: pluck("id", parentsInfo)},
|
||||
relays: this.mergeHints(pluck("hints", parentsInfo)),
|
||||
onEvent: batch(100, context => this.addContext(context, {depth: 2})),
|
||||
onEvent: batch(100, (context: Event[]) => this.addContext(context, {depth: 2})),
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
loadContext = batch(300, eventGroups => {
|
||||
const {Network, Nip65} = this.opts.engine
|
||||
loadContext = batch(300, (eventGroups: any) => {
|
||||
const {Network, Nip65} = this.opts.engine.components
|
||||
const groupsByDepth = groupBy(prop("depth"), eventGroups)
|
||||
|
||||
for (const [depthStr, groups] of Object.entries(groupsByDepth)) {
|
||||
@ -215,21 +216,22 @@ export class Feed {
|
||||
continue
|
||||
}
|
||||
|
||||
const events = flatten(pluck("events", groups)).filter(this.isTextNote)
|
||||
const events = flatten(pluck("events", groups as any[])).filter(this.isTextNote) as Event[]
|
||||
|
||||
for (const c of chunk(256, events)) {
|
||||
Network.subscribe({
|
||||
timeout: 3000,
|
||||
relays: this.mergeHints(c.map(e => Nip65.getReplyHints(10, e))),
|
||||
filter: {kinds: this.getReplyKinds(), "#e": pluck("id", c)},
|
||||
onEvent: batch(100, context => this.addContext(context, {depth: depth - 1})),
|
||||
filter: {kinds: this.getReplyKinds(), "#e": pluck("id", c as Event[])},
|
||||
|
||||
onEvent: batch(100, (context: Event[]) => this.addContext(context, {depth: depth - 1})),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
listenForContext = throttle(5000, () => {
|
||||
const {Network, Nip65} = this.opts.engine
|
||||
const {Network, Nip65} = this.opts.engine.components
|
||||
|
||||
if (this.stopped) {
|
||||
return
|
||||
@ -239,7 +241,7 @@ export class Feed {
|
||||
|
||||
const contextByParentId = groupBy(findReplyId, this.context.get())
|
||||
|
||||
const findNotes = events =>
|
||||
const findNotes = (events: Event[]): Event[] =>
|
||||
events
|
||||
.filter(this.isTextNote)
|
||||
.flatMap(e => findNotes(contextByParentId[e.id] || []).concat(e))
|
||||
@ -249,7 +251,7 @@ export class Feed {
|
||||
Network.subscribe({
|
||||
relays: this.mergeHints(c.map(e => Nip65.getReplyHints(10, e))),
|
||||
filter: {kinds: this.getReplyKinds(), "#e": pluck("id", c), since: now()},
|
||||
onEvent: batch(100, context => this.addContext(context, {depth: 2})),
|
||||
onEvent: batch(100, (context: Event[]) => this.addContext(context, {depth: 2})),
|
||||
}),
|
||||
])
|
||||
}
|
||||
@ -257,7 +259,7 @@ export class Feed {
|
||||
|
||||
// Adders
|
||||
|
||||
addContext = (newEvents, {shouldLoadParents = false, depth = 0}) => {
|
||||
addContext = (newEvents: Event[], {shouldLoadParents = false, depth = 0}) => {
|
||||
const events = this.preprocessEvents(newEvents)
|
||||
|
||||
if (this.opts.onEvent) {
|
||||
@ -288,12 +290,12 @@ export class Feed {
|
||||
const {relays, filter, engine, depth} = this.opts
|
||||
|
||||
// No point in subscribing if we have an end date
|
||||
if (!any(prop("until"), ensurePlural(filter))) {
|
||||
if (!any(prop("until"), ensurePlural(filter) as any[])) {
|
||||
this.addSubs("main", [
|
||||
engine.Network.subscribe({
|
||||
engine.components.Network.subscribe({
|
||||
relays,
|
||||
filter: ensurePlural(filter).map(assoc("since", since)),
|
||||
onEvent: batch(1000, context =>
|
||||
onEvent: batch(1000, (context: Event[]) =>
|
||||
this.addContext(context, {shouldLoadParents: true, depth})
|
||||
),
|
||||
}),
|
||||
@ -306,8 +308,8 @@ export class Feed {
|
||||
new Cursor({
|
||||
relay,
|
||||
filter,
|
||||
subscribe: engine.Network.subscribe,
|
||||
onEvent: batch(100, context =>
|
||||
Network: engine.components.Network,
|
||||
onEvent: batch(100, (context: Event[]) =>
|
||||
this.addContext(context, {shouldLoadParents: true, depth})
|
||||
),
|
||||
})
|
||||
@ -325,24 +327,22 @@ export class Feed {
|
||||
}
|
||||
}
|
||||
|
||||
hydrate(feed) {
|
||||
hydrate(feed: DisplayEvent[]) {
|
||||
const {depth} = this.opts
|
||||
const notes = []
|
||||
const context = []
|
||||
const notes: DisplayEvent[] = []
|
||||
const context: Event[] = []
|
||||
|
||||
const addContext = ({zaps, replies, reactions, ...note}) => {
|
||||
const addContext = (note: DisplayEvent) => {
|
||||
context.push(fromDisplayEvent(note))
|
||||
|
||||
zaps.map(zap => context.push(zap))
|
||||
reactions.map(reaction => context.push(reaction))
|
||||
|
||||
replies.map(addContext)
|
||||
note.zaps.forEach(zap => context.push(zap))
|
||||
note.reactions.forEach(reaction => context.push(reaction))
|
||||
note.replies.forEach(reply => addContext(reply))
|
||||
}
|
||||
|
||||
feed.forEach(note => {
|
||||
addContext(note)
|
||||
|
||||
notes.push(fromDisplayEvent(note))
|
||||
notes.push(note)
|
||||
})
|
||||
|
||||
this.feed.set(notes)
|
||||
@ -401,7 +401,7 @@ export class Feed {
|
||||
}
|
||||
}
|
||||
|
||||
deferReactions = notes => {
|
||||
deferReactions = (notes: Event[]) => {
|
||||
const [defer, ok] = partition(e => !this.isTextNote(e) && this.isMissingParent(e), notes)
|
||||
|
||||
setTimeout(() => {
|
||||
@ -415,7 +415,7 @@ export class Feed {
|
||||
return ok
|
||||
}
|
||||
|
||||
deferOrphans = notes => {
|
||||
deferOrphans = (notes: Event[]) => {
|
||||
// If something has a parent id but we haven't found the parent yet, skip it until we have it.
|
||||
const [defer, ok] = partition(e => this.isTextNote(e) && this.isMissingParent(e), notes)
|
||||
|
||||
@ -424,7 +424,7 @@ export class Feed {
|
||||
return ok
|
||||
}
|
||||
|
||||
deferAncient = notes => {
|
||||
deferAncient = (notes: Event[]) => {
|
||||
// Sometimes relays send very old data very quickly. Pop these off the queue and re-add
|
||||
// them after we have more timely data. They still might be relevant, but order will still
|
||||
// be maintained since everything before the cutoff will be deferred the same way.
|
||||
@ -436,7 +436,7 @@ export class Feed {
|
||||
return ok
|
||||
}
|
||||
|
||||
addToFeed(notes) {
|
||||
addToFeed(notes: Event[]) {
|
||||
const context = this.context.get()
|
||||
const applied = this.applyContext(notes, context, true)
|
||||
const sorted = sortBy(e => -e.created_at, applied)
|
||||
|
@ -2,15 +2,8 @@ import EventEmitter from "events"
|
||||
import {defer} from "hurdak"
|
||||
|
||||
export class Subscription extends EventEmitter {
|
||||
closed: boolean
|
||||
complete: ReturnType<typeof defer>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.closed = false
|
||||
this.complete = defer()
|
||||
}
|
||||
closed = false
|
||||
complete = defer()
|
||||
|
||||
close = () => {
|
||||
if (!this.closed) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
export class Worker<T> {
|
||||
buffer: T[]
|
||||
handlers: Array<(x: T) => void>
|
||||
timeout: NodeJS.Timeout
|
||||
timeout: NodeJS.Timeout | undefined
|
||||
|
||||
constructor() {
|
||||
this.buffer = []
|
||||
@ -26,12 +26,12 @@ export class Worker<T> {
|
||||
}
|
||||
}
|
||||
|
||||
push = message => {
|
||||
push = (message: T) => {
|
||||
this.buffer.push(message)
|
||||
this.#enqueueWork()
|
||||
}
|
||||
|
||||
listen = handler => {
|
||||
listen = (handler: (x: T) => void) => {
|
||||
this.handlers.push(handler)
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ type R = Record<string, any>
|
||||
type M<T> = Map<string, T>
|
||||
|
||||
export interface Readable<T> {
|
||||
get: () => T | undefined
|
||||
get: () => T
|
||||
subscribe: (f: Subscriber) => () => void
|
||||
derived: <U>(f: (v: T) => U) => Readable<U>
|
||||
}
|
||||
@ -123,7 +123,7 @@ export class Key<T extends R> implements Readable<T> {
|
||||
this.store = base.derived<T>(m => m.get(key) as T)
|
||||
}
|
||||
|
||||
get = () => this.base.get().get(this.key)
|
||||
get = () => this.base.get().get(this.key) as T
|
||||
|
||||
subscribe = (f: Subscriber) => this.store.subscribe(f)
|
||||
|
||||
@ -151,7 +151,7 @@ export class Key<T extends R> implements Readable<T> {
|
||||
|
||||
set = (v: T) => this.update(() => v)
|
||||
|
||||
merge = (d: T) => this.update(v => ({...v, ...d}))
|
||||
merge = (d: Partial<T>) => this.update(v => ({...v, ...d}))
|
||||
|
||||
remove = () =>
|
||||
this.base.update(m => {
|
||||
@ -199,6 +199,7 @@ export const writable = <T>(v: T) => new Writable(v)
|
||||
export const derived = <T>(stores: Derivable, getValue: (values: any) => T) =>
|
||||
new Derived(stores, getValue) as Readable<T>
|
||||
|
||||
export const key = <T extends R>(base: Writable<M<T>>, pk: string, key: string) => new Key<T>(base, pk, key)
|
||||
export const key = <T extends R>(base: Writable<M<T>>, pk: string, key: string) =>
|
||||
new Key<T>(base, pk, key)
|
||||
|
||||
export const collection = <T extends R>(pk: string) => new Collection<T>(pk)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
|
||||
export let src
|
||||
export let src: string
|
||||
export let size = 4
|
||||
</script>
|
||||
|
||||
|
@ -53,7 +53,7 @@ export const modal = {
|
||||
getCurrent() {
|
||||
return last(get(modal.stack))
|
||||
},
|
||||
sync($stack, opts = {}) {
|
||||
sync($stack: any[], opts = {}) {
|
||||
const hash = $stack.length > 0 ? `#m=${$stack.length}` : ""
|
||||
|
||||
if (hash !== window.location.hash) {
|
||||
@ -62,16 +62,16 @@ export const modal = {
|
||||
|
||||
return $stack
|
||||
},
|
||||
remove(id) {
|
||||
remove(id: string) {
|
||||
modal.stack.update($stack => modal.sync(reject(whereEq({id}), $stack)))
|
||||
},
|
||||
push(data) {
|
||||
push(data: {type: string, [k: string]: any}) {
|
||||
modal.stack.update($stack => modal.sync($stack.concat(data)))
|
||||
},
|
||||
pop() {
|
||||
modal.stack.update($stack => modal.sync($stack.slice(0, -1)))
|
||||
},
|
||||
replace(data) {
|
||||
replace(data: {type: string, [k: string]: any}) {
|
||||
modal.stack.update($stack => $stack.slice(0, -1).concat(data))
|
||||
},
|
||||
clear() {
|
||||
@ -84,7 +84,7 @@ export const modal = {
|
||||
},
|
||||
}
|
||||
|
||||
location.subscribe($location => {
|
||||
location.subscribe(($location: any) => {
|
||||
const match = $location.hash.match(/\bm=(\d+)/)
|
||||
const i = match ? parseInt(match[1]) : 0
|
||||
|
||||
@ -93,12 +93,12 @@ location.subscribe($location => {
|
||||
|
||||
// Themes
|
||||
|
||||
const THEME = fromPairs(import.meta.env.VITE_THEME.split(",").map(x => x.split(":")))
|
||||
const THEME = fromPairs(import.meta.env.VITE_THEME.split(",").map((x: string) => x.split(":"))) as Record<string, string>
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
|
||||
export const theme = synced("ui/theme", prefersDark ? "dark" : "light")
|
||||
|
||||
export const getThemeColors = $theme => {
|
||||
export const getThemeColors = ($theme: string) => {
|
||||
for (const x of range(1, 10)) {
|
||||
const lum = $theme === "dark" ? (5 - x) * 25 : (x - 5) * 25
|
||||
|
||||
@ -108,9 +108,9 @@ export const getThemeColors = $theme => {
|
||||
return THEME
|
||||
}
|
||||
|
||||
export const getThemeColor = ($theme, k) => prop(k, getThemeColors($theme))
|
||||
export const getThemeColor = ($theme: string, k: string) => prop(k, getThemeColors($theme))
|
||||
|
||||
export const getThemeVariables = $theme =>
|
||||
export const getThemeVariables = ($theme: string) =>
|
||||
Object.entries(getThemeColors($theme))
|
||||
.map(([k, v]) => `--${k}: ${v};`)
|
||||
.join("\n")
|
||||
|
3
src/types.d.ts
vendored
3
src/types.d.ts
vendored
@ -1 +1,2 @@
|
||||
declare module 'fuse.js/dist/fuse.min.js'
|
||||
declare module "fuse.js/dist/fuse.min.js"
|
||||
declare module "paravel"
|
||||
|
@ -2,10 +2,10 @@ const DIVISORS = {
|
||||
m: BigInt(1e3),
|
||||
u: BigInt(1e6),
|
||||
n: BigInt(1e9),
|
||||
p: BigInt(1e12)
|
||||
p: BigInt(1e12),
|
||||
}
|
||||
|
||||
const MAX_MILLISATS = BigInt('2100000000000000000')
|
||||
const MAX_MILLISATS = BigInt("2100000000000000000")
|
||||
|
||||
const MILLISATS_PER_BTC = BigInt(1e11)
|
||||
|
||||
@ -15,25 +15,24 @@ function hrpToMillisat(hrpString: string) {
|
||||
divisor = hrpString.slice(-1)
|
||||
value = hrpString.slice(0, -1)
|
||||
} else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) {
|
||||
throw new Error('Not a valid multiplier for the amount')
|
||||
throw new Error("Not a valid multiplier for the amount")
|
||||
} else {
|
||||
value = hrpString
|
||||
}
|
||||
|
||||
if (!value.match(/^\d+$/))
|
||||
throw new Error('Not a valid human readable amount')
|
||||
if (!value.match(/^\d+$/)) throw new Error("Not a valid human readable amount")
|
||||
|
||||
const valueBN = BigInt(value)
|
||||
|
||||
const millisatoshisBN = divisor
|
||||
? (valueBN * MILLISATS_PER_BTC) / DIVISORS[divisor]
|
||||
? (valueBN * MILLISATS_PER_BTC) / (DIVISORS as any)[divisor]
|
||||
: valueBN * MILLISATS_PER_BTC
|
||||
|
||||
if (
|
||||
(divisor === 'p' && !(valueBN % BigInt(10) === BigInt(0))) ||
|
||||
(divisor === "p" && !(valueBN % BigInt(10) === BigInt(0))) ||
|
||||
millisatoshisBN > MAX_MILLISATS
|
||||
) {
|
||||
throw new Error('Amount is outside of valid range')
|
||||
throw new Error("Amount is outside of valid range")
|
||||
}
|
||||
|
||||
return millisatoshisBN
|
||||
|
@ -125,17 +125,17 @@ export const stringToHue = (value: string) => {
|
||||
return hash % 360
|
||||
}
|
||||
|
||||
export const hsl = (hue: string, {saturation = 100, lightness = 50, opacity = 1} = {}) =>
|
||||
export const hsl = (hue: number, {saturation = 100, lightness = 50, opacity = 1} = {}) =>
|
||||
`hsl(${hue}, ${saturation}%, ${lightness}%, ${opacity})`
|
||||
|
||||
export const tryJson = (f: <T>() => T) =>
|
||||
export const tryJson = <T>(f: () => T) =>
|
||||
tryFunc(f, (e: Error) => {
|
||||
if (!e.toString().includes("JSON")) {
|
||||
warn(e)
|
||||
}
|
||||
})
|
||||
|
||||
export const tryFetch = (f: <T>() => T) =>
|
||||
export const tryFetch = <T>(f: () => T) =>
|
||||
tryFunc(f, (e: Error) => {
|
||||
if (!e.toString().includes("fetch")) {
|
||||
warn(e)
|
||||
@ -219,7 +219,7 @@ export const webSocketURLToPlainOrBase64 = (url: string): string => {
|
||||
return url
|
||||
}
|
||||
|
||||
export const pushToKey = (xs: any[], k: number, v: any) => {
|
||||
xs[k] = xs[k] || []
|
||||
xs[k].push(v)
|
||||
export const pushToKey = <T>(m: Record<string, T[]>, k: string, v: T) => {
|
||||
m[k] = m[k] || []
|
||||
m[k].push(v)
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export class Tags {
|
||||
any(f: (t: any) => boolean) {
|
||||
return this.filter(f).exists()
|
||||
}
|
||||
type(type: string) {
|
||||
type(type: string | string[]) {
|
||||
const types = ensurePlural(type)
|
||||
|
||||
return new Tags(this.tags.filter(t => types.includes(t[0])))
|
||||
|
@ -2,6 +2,7 @@ import {last, pluck, identity} from "ramda"
|
||||
import {nip19} from "nostr-tools"
|
||||
import {first, switcherFn} from "hurdak"
|
||||
import {fromNostrURI} from "src/util/nostr"
|
||||
import type {Event} from "src/engine/types"
|
||||
|
||||
export const NEWLINE = "newline"
|
||||
export const ELLIPSIS = "ellipsis"
|
||||
@ -15,11 +16,11 @@ export const NOSTR_NPUB = "nostr:npub"
|
||||
export const NOSTR_NPROFILE = "nostr:nprofile"
|
||||
export const NOSTR_NADDR = "nostr:naddr"
|
||||
|
||||
export const urlIsMedia = url =>
|
||||
!url.match(/\.(apk|docx|xlsx|csv|dmg)/) && last(url.split("://")).includes("/")
|
||||
export const urlIsMedia = (url: string) =>
|
||||
!url.match(/\.(apk|docx|xlsx|csv|dmg)/) && last(url.split("://"))?.includes("/")
|
||||
|
||||
export const parseContent = ({content, tags = []}) => {
|
||||
const result = []
|
||||
export const parseContent = ({content, tags = []}: {content: string; tags: string[][]}) => {
|
||||
const result: any[] = []
|
||||
let text = content.trim()
|
||||
let buffer = ""
|
||||
|
||||
@ -42,7 +43,7 @@ export const parseContent = ({content, tags = []}) => {
|
||||
const [tag, value, url] = tags[i]
|
||||
const relays = [url].filter(identity)
|
||||
|
||||
let type, data, entity
|
||||
let type, data: any, entity
|
||||
if (tag === "p") {
|
||||
type = "nprofile"
|
||||
data = {pubkey: value, relays}
|
||||
@ -162,13 +163,22 @@ export const parseContent = ({content, tags = []}) => {
|
||||
return result
|
||||
}
|
||||
|
||||
export const truncateContent = (content, {showEntire, maxLength, showMedia = false}) => {
|
||||
type TruncateContentOpts = {
|
||||
showEntire: boolean
|
||||
maxLength: number
|
||||
showMedia: boolean
|
||||
}
|
||||
|
||||
export const truncateContent = (
|
||||
content: any[],
|
||||
{showEntire, maxLength, showMedia = false}: TruncateContentOpts
|
||||
) => {
|
||||
if (showEntire) {
|
||||
return content
|
||||
}
|
||||
|
||||
let length = 0
|
||||
const result = []
|
||||
const result: any[] = []
|
||||
const truncateAt = maxLength * 0.6
|
||||
const mediaLength = maxLength / 3
|
||||
const entityLength = 30
|
||||
@ -199,7 +209,7 @@ export const truncateContent = (content, {showEntire, maxLength, showMedia = fal
|
||||
return result
|
||||
}
|
||||
|
||||
export const getLinks = parts =>
|
||||
export const getLinks = (parts: any[]) =>
|
||||
pluck(
|
||||
"value",
|
||||
parts.filter(x => x.type === LINK && x.isMedia)
|
||||
|
@ -6,6 +6,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"strictPropertyInitialization": false,
|
||||
"strictNullChecks": false
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user