Remove engine entirely

This commit is contained in:
Jonathan Staab 2023-09-12 14:24:37 -07:00
parent 8ba9546fd6
commit 8b9e069f1d
38 changed files with 178 additions and 908 deletions

View File

@ -4,16 +4,15 @@
import type {ComponentType, SvelteComponentTyped} from "svelte"
import {nip19} from "nostr-tools"
import type {Relay} from "src/engine"
import {onMount} from "svelte"
import {Router, links} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history"
import {isNil, find, last} from "ramda"
import {seconds, Fetch, shuffle} from "hurdak"
import {tryFetch, hexToBech32, bech32ToHex, now} from "src/util/misc"
import {storage, relays, getSetting, dufflepud} from "src/engine2"
import type {Relay} from "src/engine2"
import {storage, session, stateKey, relays, getSetting, dufflepud} from "src/engine2"
import * as engine from "src/engine2"
import {Keys} from "src/app/engine"
import {loadAppData} from "src/app/state"
import {theme, getThemeVariables, appName, modal} from "src/partials/state"
import {logUsage} from "src/app/state"
@ -106,10 +105,8 @@
}
})
const {pubkey} = Keys
storage.ready.then(() => {
if ($pubkey) {
if ($session) {
loadAppData()
}
@ -147,7 +144,7 @@
<TypedRouter url={pathname}>
<div use:links>
{#key $pubkey || "anonymous"}
{#key $stateKey}
<Routes />
<ForegroundButtons />
<SideNav />

View File

@ -2,7 +2,7 @@
import {nip19} from "nostr-tools"
import {fade} from "src/util/transition"
import {modal, location} from "src/partials/state"
import {Keys} from "src/app/engine"
import {canSign} from "src/engine2"
import ForegroundButton from "src/partials/ForegroundButton.svelte"
import ForegroundButtons from "src/partials/ForegroundButtons.svelte"
@ -12,8 +12,6 @@
/conversations|channels|chat|relays|keys|settings|logout$/
)
const {canSign} = Keys
const scrollToTop = () => document.body.scrollIntoView({behavior: "smooth"})
const createNote = () => {

View File

@ -7,12 +7,11 @@
hasNewNip04Messages,
hasNewNip24Messages,
hasNewNotfications,
canUseGiftWrap,
canSign,
} from "src/engine2"
import {Keys} from "src/app/engine"
import {menuIsOpen} from "src/app/state"
const {canSign, canUseGiftWrap} = Keys
const toggleTheme = () => theme.update(t => (t === "dark" ? "light" : "dark"))
const install = () => {

View File

@ -1,46 +0,0 @@
import {identity} from "ramda"
import {Engine} from "src/engine"
const IMGPROXY_URL = import.meta.env.VITE_IMGPROXY_URL
const DUFFLEPUD_URL = import.meta.env.VITE_DUFFLEPUD_URL
const MULTIPLEXTR_URL = import.meta.env.VITE_MULTIPLEXTR_URL
const FORCE_RELAYS = (import.meta.env.VITE_FORCE_RELAYS || "").split(",").filter(identity)
const COUNT_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://rbr.bio"]
const SEARCH_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://relay.nostr.band"]
const DEFAULT_RELAYS =
FORCE_RELAYS.length > 0
? FORCE_RELAYS
: [
"wss://purplepag.es",
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://relayable.org",
"wss://nostr.wine",
]
const DEFAULT_FOLLOWS = (import.meta.env.VITE_DEFAULT_FOLLOWS || "").split(",").filter(identity)
const ENABLE_ZAPS = JSON.parse(import.meta.env.VITE_ENABLE_ZAPS)
const engine = new Engine({
DEFAULT_FOLLOWS,
IMGPROXY_URL,
DUFFLEPUD_URL,
MULTIPLEXTR_URL,
FORCE_RELAYS,
COUNT_RELAYS,
SEARCH_RELAYS,
DEFAULT_RELAYS,
ENABLE_ZAPS,
})
export default engine
export const Env = engine.Env
export const Events = engine.Events
export const Keys = engine.Keys

View File

@ -1,5 +1,4 @@
<script lang="ts">
import type {DynamicFilter} from "src/engine/types"
import {onMount, onDestroy} from "svelte"
import {readable} from "svelte/store"
import {FeedLoader} from "src/engine2"
@ -13,8 +12,8 @@
import FeedControls from "src/app/shared/FeedControls.svelte"
import RelayFeed from "src/app/shared/RelayFeed.svelte"
import Note from "src/app/shared/Note.svelte"
import {getSetting, searchableRelays, mergeHints, getPubkeyHints} from "src/engine2"
import {Keys} from "src/app/engine"
import type {DynamicFilter} from "src/engine2"
import {session, getSetting, searchableRelays, mergeHints, getPubkeyHints} from "src/engine2"
import {compileFilter} from "src/app/state"
export let relays = []
@ -57,7 +56,7 @@
}
const limit = getSetting("relay_limit")
const authors = (compileFilter(filter).authors || []).concat(Keys.pubkey.get())
const authors = (compileFilter(filter).authors || []).concat($session.pubkey)
const hints = authors.map(pubkey => getPubkeyHints(limit, pubkey, "write"))
return mergeHints(limit, hints)

View File

@ -4,14 +4,24 @@
import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {mute, unmute, follow, unfollow, isMuted, isFollowing} from "src/engine2"
import {Env, Keys} from "src/app/engine"
import {addToList} from "src/app/state"
import {
env,
loginWithPublicKey,
session,
mute,
unmute,
canSign,
canUseGiftWrap,
follow,
unfollow,
isMuted,
isFollowing,
} from "src/engine2"
import {addToList, boot} from "src/app/state"
export let pubkey
const {canSign, canUseGiftWrap} = Keys
const isSelf = Keys.pubkey.get() === pubkey
const isSelf = $session?.pubkey === pubkey
const npub = nip19.npubEncode(pubkey)
const following = isFollowing(pubkey)
const muted = isMuted(pubkey)
@ -49,7 +59,7 @@
actions.push({onClick: loginAsUser, label: "Login as", icon: "right-to-bracket"})
}
if (Env.FORCE_RELAYS.length === 0) {
if ($env.FORCE_RELAYS.length === 0) {
actions.push({onClick: openProfileInfo, label: "Details", icon: "info"})
}
@ -64,7 +74,8 @@
const loginAsUser = () => {
modal.clear()
Keys.login("pubkey", pubkey)
loginWithPublicKey(pubkey)
boot()
}
const openProfileInfo = () => modal.push({type: "person/info", pubkey})

View File

@ -6,8 +6,7 @@
import {numberFmt} from "src/util/misc"
import {modal} from "src/partials/state"
import type {Event} from "src/engine2"
import {people, count, Subscription, getPubkeyHints} from "src/engine2"
import {Keys} from "src/app/engine"
import {session, people, count, Subscription, getPubkeyHints} from "src/engine2"
export let pubkey
@ -36,7 +35,7 @@
sub = new Subscription({
timeout: 30_000,
ephemeral: true,
relays: getPubkeyHints(3, Keys.pubkey.get(), "read"),
relays: getPubkeyHints(3, $session?.pubkey, "read"),
filters: [{kinds: [3], "#p": [pubkey]}],
})

View File

@ -2,8 +2,7 @@
import {last, prop} from "ramda"
import {modal} from "src/partials/state"
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {relays, relayPolicyUrls, addRelay, removeRelay, hasRelay} from "src/engine2"
import {Keys} from "src/app/engine"
import {canSign, relays, relayPolicyUrls, addRelay, removeRelay, hasRelay} from "src/engine2"
import {addToList} from "src/app/state"
export let relay
@ -30,7 +29,7 @@
})
}
if (Keys.canSign.get()) {
if ($canSign) {
actions.push({
onClick: () => addToList("r", relay.url),
label: "Add to list",

View File

@ -8,8 +8,7 @@
import Anchor from "src/partials/Anchor.svelte"
import RelayStatus from "src/app/shared/RelayStatus.svelte"
import RelayCardActions from "src/app/shared/RelayCardActions.svelte"
import {getSetting, displayRelay, setRelayPolicy} from "src/engine2"
import {Keys} from "src/app/engine"
import {canSign, getSetting, displayRelay, setRelayPolicy} from "src/engine2"
export let relay
export let rating = null
@ -50,7 +49,7 @@
{#if relay.description}
<p>{relay.description}</p>
{/if}
{#if showControls && Keys.canSign.get()}
{#if showControls && $canSign}
<div class="-mx-6 my-1 h-px bg-gray-7" />
<div class="flex justify-between gap-2">
<span>Publish to this relay?</span>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import OverflowMenu from "src/partials/OverflowMenu.svelte"
import {addToList} from "src/app/state"
import {Keys} from "src/app/engine"
import {canSign} from "src/engine2"
export let topic
@ -10,7 +10,7 @@
$: {
actions = []
if (Keys.canSign.get()) {
if ($canSign) {
actions.push({
onClick: () => addToList("t", topic),
label: "Add to list",

View File

@ -1,5 +1,4 @@
import type {Filter} from "nostr-tools"
import type {DynamicFilter} from "src/engine/types"
import Bugsnag from "@bugsnag/js"
import {nip19} from "nostr-tools"
import {navigate} from "svelte-routing"
@ -10,9 +9,11 @@ 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/engine2"
import type {DynamicFilter, Event} from "src/engine2"
import {
env,
pool,
session,
loadPubkeys,
channels,
follows,
@ -21,8 +22,8 @@ import {
getUserRelayUrls,
getSetting,
dufflepud,
events,
} from "src/engine2"
import {Events, Env, Keys} from "src/app/engine"
// Routing
@ -67,17 +68,17 @@ setTimeout(() => {
})
})
const session = Math.random().toString().slice(2)
const sessionId = Math.random().toString().slice(2)
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()
const pubkey = session.get()?.pubkey
const ident = pubkey ? hash(pubkey) : "unknown"
if (getSetting("report_analytics")) {
try {
await fetch(dufflepud(`usage/${ident}/${session}/${name}`), {method: "post"})
await fetch(dufflepud(`usage/${ident}/${sessionId}/${name}`), {method: "post"})
} catch (e) {
if (!e.toString().includes("Failed to fetch")) {
warn(e)
@ -115,11 +116,10 @@ let listener
let timeout
export const listenForNotifications = async () => {
const pubkey = Keys.pubkey.get()
const {pubkey} = session.get()
const channelIds = pluck("id", channels.get().filter(path(["nip28", "joined"])))
const eventIds: string[] = doPipe(Events.cache.get(), [
const eventIds: string[] = doPipe(events.get(), [
filter((e: Event) => noteKinds.includes(e.kind)),
sortBy((e: Event) => -e.created_at),
slice(0, 256),
@ -150,7 +150,7 @@ export const listenForNotifications = async () => {
}
export const loadAppData = async () => {
const pubkey = Keys.pubkey.get()
const {pubkey} = session.get()
// Make sure the user and their follows are loaded
await loadPubkeys(pubkey, {force: true, kinds: userKinds})
@ -162,10 +162,8 @@ export const loadAppData = async () => {
listenForNotifications()
}
export const login = async (method: string, key: string | {pubkey: string; token: string}) => {
Keys.login(method, key)
if (Env.FORCE_RELAYS.length > 0) {
export const boot = async () => {
if (env.get().FORCE_RELAYS.length > 0) {
modal.replace({
type: "message",
message: "Logging you in...",
@ -175,7 +173,7 @@ export const login = async (method: string, key: string | {pubkey: string; token
await Promise.all([
sleep(1500),
loadPubkeys(Keys.pubkey.get(), {force: true, kinds: userKinds}),
loadPubkeys([session.get().pubkey], {force: true, kinds: userKinds}),
])
navigate("/notes")
@ -223,7 +221,7 @@ export const toastProgress = progress => {
// Feeds
export const getAuthorsWithDefaults = (pubkeys: string[]) =>
shuffle(pubkeys.length > 0 ? pubkeys : (Env.DEFAULT_FOLLOWS as string[])).slice(0, 1024)
shuffle(pubkeys.length > 0 ? pubkeys : (env.get().DEFAULT_FOLLOWS as string[])).slice(0, 1024)
export const compileFilter = (filter: DynamicFilter): Filter => {
if (filter.authors === "global") {

View File

@ -9,17 +9,16 @@
import PersonAbout from "src/app/shared/PersonAbout.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import {
session,
channels,
displayPubkey,
createNip24Message,
nip24MarkChannelRead,
loadNip59Messages,
} from "src/engine2"
import {Keys} from "src/app/engine"
export let entity
const userPubkey = Keys.pubkey.get()
const channel = channels.key(entity)
const pubkeys = entity.split(",")
@ -72,15 +71,15 @@
slot="message"
let:message
class={cx("flex overflow-hidden text-ellipsis", {
"ml-12 justify-end": message.pubkey === userPubkey,
"mr-12": message.pubkey !== userPubkey,
"ml-12 justify-end": message.pubkey === $session.pubkey,
"mr-12": message.pubkey !== $session.pubkey,
})}>
<div
class={cx("inline-block flex max-w-xl flex-col rounded-2xl px-4 py-2", {
"rounded-br-none bg-gray-1 text-gray-8": message.pubkey === userPubkey,
"rounded-bl-none bg-gray-7": message.pubkey !== userPubkey,
"rounded-br-none bg-gray-1 text-gray-8": message.pubkey === $session.pubkey,
"rounded-bl-none bg-gray-7": message.pubkey !== $session.pubkey,
})}>
{#if message.showProfile && message.pubkey !== userPubkey}
{#if message.showProfile && message.pubkey !== $session.pubkey}
<Anchor class="mb-1" on:click={() => showPerson(message.pubkey)}>
<strong>{displayPubkey(message.pubkey)}</strong>
</Anchor>
@ -92,8 +91,8 @@
</div>
<small
class="mt-1"
class:text-gray-7={message.pubkey === userPubkey}
class:text-gray-1={message.pubkey !== userPubkey}>
class:text-gray-7={message.pubkey === $session.pubkey}
class:text-gray-1={message.pubkey !== $session.pubkey}>
{formatTimestamp(message.created_at)}
</small>
</div>

View File

@ -10,6 +10,8 @@
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import {
canSign,
session,
channels,
imgproxy,
publishNip28Message,
@ -18,7 +20,6 @@
loadNip28Messages,
publishNip28ChannelChecked,
} from "src/engine2"
import {Keys} from "src/app/engine"
export let entity
@ -68,7 +69,7 @@
<div class="flex h-12 flex-col pt-px">
<div class="flex w-full items-center justify-between">
<div class="flex gap-2">
{#if $channel?.nip28?.owner === Keys.pubkey.get()}
{#if $channel?.nip28?.owner === $session.pubkey}
<button class="cursor-pointer text-sm" on:click={edit}>
<i class="fa-solid fa-edit" /> Edit
</button>
@ -78,7 +79,7 @@
<i class="fa fa-right-from-bracket" />
<span>Leave</span>
</Anchor>
{:else if Keys.canSign.get()}
{:else if $canSign}
<Anchor theme="button" killEvent class="flex items-center gap-2" on:click={join}>
<i class="fa fa-right-to-bracket" />
<span>Join</span>

View File

@ -11,8 +11,7 @@
import Content from "src/partials/Content.svelte"
import NoteById from "src/app/shared/NoteById.svelte"
import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte"
import {labels, getUserRelayUrls, follows, Subscription} from "src/engine2"
import {Keys} from "src/app/engine"
import {session, labels, getUserRelayUrls, follows, Subscription} from "src/engine2"
type LabelGroup = {
label: string
@ -21,8 +20,6 @@
authors: string[]
}
const {pubkey} = Keys
const labelGroups = labels.derived($labels => {
const $labelGroups = {}
@ -69,7 +66,7 @@
{
kinds: [1985],
"#L": ["#t", "ugc"],
authors: $follows.concat($pubkey),
authors: $follows.concat($session.pubkey),
},
],
})

View File

@ -1,15 +1,14 @@
<script lang="ts">
import cx from "classnames"
import {filter, propEq} from "ramda"
import type {DynamicFilter} from "src/engine/types"
import {Tags, noteKinds} from "src/util/nostr"
import {modal, theme} from "src/partials/state"
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Popover from "src/partials/Popover.svelte"
import Feed from "src/app/shared/Feed.svelte"
import {stateKey, follows, lists} from "src/engine2"
import {Keys} from "src/app/engine"
import type {DynamicFilter} from "src/engine2"
import {session, canSign, stateKey, follows, lists} from "src/engine2"
const userLists = lists.derived(filter(propEq("pubkey", stateKey.get())))
@ -51,7 +50,7 @@
</script>
<Content>
{#if !Keys.pubkey.get()}
{#if !$session}
<Content size="lg" class="text-center">
<p class="text-xl">Don't have an account?</p>
<p>
@ -62,7 +61,7 @@
{#key key}
<Feed filter={feedFilter} {relays}>
<div slot="controls">
{#if Keys.canSign.get()}
{#if $canSign}
{#if $userLists.length > 0}
<Popover placement="bottom" opts={{hideOnClick: true}} theme="transparent">
<i slot="trigger" class="fa fa-ellipsis-v cursor-pointer p-2" />

View File

@ -5,19 +5,18 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {login} from "src/app/state"
import {withExtension, loginWithExtension} from "src/engine2"
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const autoLogIn = async () => {
const {nostr} = window as any
if (nostr) {
login("extension", await nostr.getPublicKey())
} else {
modal.push({type: "login/privkey"})
}
}
const autoLogIn = () =>
withExtension(async ext => {
if (ext) {
loginWithExtension(await ext.getPublicKey())
} else {
modal.push({type: "login/privkey"})
}
})
const signUp = () => {
modal.push({type: "onboarding", stage: "intro"})

View File

@ -5,8 +5,8 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {Keys} from "src/app/engine"
import {login} from "src/app/state"
import {isKeyValid, loginWithNsecBunker} from "src/engine2"
import {boot} from "src/app/state"
let input = ""
@ -14,8 +14,9 @@
const [npub, token] = input.split("#")
const pubkey = npub.startsWith("npub") ? toHex(npub) : npub
if (Keys.isKeyValid(pubkey)) {
login("bunker", {pubkey, token})
if (isKeyValid(pubkey)) {
loginWithNsecBunker(pubkey, token)
boot()
} else {
toast.show("error", "Sorry, but that's an invalid public key.")
}

View File

@ -12,12 +12,9 @@
import Anchor from "src/partials/Anchor.svelte"
import Modal from "src/partials/Modal.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import {relays, pool, loadPubkeys, getUserRelayUrls} from "src/engine2"
import {Env, Keys} from "src/app/engine"
import {env, session, relays, pool, loadPubkeys, getUserRelayUrls} from "src/engine2"
import {loadAppData} from "src/app/state"
const pubkey = Keys.pubkey.get()
let modal = null
let customRelayUrl = null
let searching = true
@ -29,7 +26,7 @@
uniqBy(
prop("url"),
// Make sure our hardcoded urls are first, since they're more likely to find a match
Env.DEFAULT_RELAYS.map(objOf("url")).concat(shuffle($relays))
$env.DEFAULT_RELAYS.map(objOf("url")).concat(shuffle($relays))
)
)
@ -57,7 +54,7 @@
// Wait a bit before removing the relay to smooth out the ui
Promise.all([
sleep(1500),
loadPubkeys([pubkey], {
loadPubkeys([$session.pubkey], {
force: true,
relays: [relay.url],
kinds: userKinds,
@ -122,7 +119,7 @@
}}>here</Anchor
>.
</p>
{#if Env.FORCE_RELAYS.length > 0}
{#if $env.FORCE_RELAYS.length > 0}
<Spinner />
{:else if Object.values(currentRelays).length > 0}
<p>Currently searching:</p>

View File

@ -6,8 +6,8 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {Keys} from "src/app/engine"
import {login} from "src/app/state"
import {isKeyValid, loginWithPrivateKey} from "src/engine2"
import {boot} from "src/app/state"
let nsec = ""
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
@ -15,8 +15,9 @@
const logIn = () => {
const privkey = nsec.startsWith("nsec") ? toHex(nsec) : nsec
if (Keys.isKeyValid(privkey)) {
login("privkey", privkey)
if (isKeyValid(privkey)) {
loginWithPrivateKey(privkey)
boot()
} else {
toast.show("error", "Sorry, but that's an invalid private key.")
}

View File

@ -4,17 +4,18 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {Keys} from "src/app/engine"
import {toast} from "src/partials/state"
import {login} from "src/app/state"
import {isKeyValid, loginWithPublicKey} from "src/engine2"
import {boot} from "src/app/state"
let npub = ""
const logIn = () => {
const pubkey = npub.startsWith("npub") ? toHex(npub) : npub
if (Keys.isKeyValid(pubkey)) {
login("pubkey", pubkey)
if (isKeyValid(pubkey)) {
loginWithPublicKey(pubkey)
boot()
} else {
toast.show("error", "Sorry, but that's an invalid public key.")
}

View File

@ -7,6 +7,7 @@
import Anchor from "src/partials/Anchor.svelte"
import NoteContent from "src/app/shared/NoteContent.svelte"
import {
session,
channels,
derivePerson,
displayPerson,
@ -14,7 +15,6 @@
nip04MarkChannelRead,
loadNip04Messages,
} from "src/engine2"
import {Keys} from "src/app/engine"
import {routes} from "src/app/state"
import PersonCircle from "src/app/shared/PersonCircle.svelte"
import PersonAbout from "src/app/shared/PersonAbout.svelte"
@ -68,13 +68,13 @@
slot="message"
let:message
class={cx("flex overflow-hidden text-ellipsis", {
"ml-12 justify-end": message.pubkey === Keys.pubkey.get(),
"mr-12": message.pubkey !== Keys.pubkey.get(),
"ml-12 justify-end": message.pubkey === $session.pubkey,
"mr-12": message.pubkey !== $session.pubkey,
})}>
<div
class={cx("inline-block max-w-xl rounded-2xl px-4 py-2", {
"rounded-br-none bg-gray-1 text-gray-8": message.pubkey === Keys.pubkey.get(),
"rounded-bl-none bg-gray-7": message.pubkey !== Keys.pubkey.get(),
"rounded-br-none bg-gray-1 text-gray-8": message.pubkey === $session.pubkey,
"rounded-bl-none bg-gray-7": message.pubkey !== $session.pubkey,
})}>
<div class="break-words">
{#if typeof message.content === "string"}
@ -83,8 +83,8 @@
</div>
<small
class="mt-1"
class:text-gray-7={message.pubkey === Keys.pubkey.get()}
class:text-gray-1={message.pubkey !== Keys.pubkey.get()}>
class:text-gray-7={message.pubkey === $session.pubkey}
class:text-gray-1={message.pubkey !== $session.pubkey}>
{formatTimestamp(message.created_at)}
</small>
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {pluck} from 'ramda'
import type {Event} from 'src/engine/types'
import {pluck} from "ramda"
import type {Event} from "src/engine2"
import Content from "src/partials/Content.svelte"
import NotificationSection from "src/app/views/NotificationSection.svelte"

View File

@ -11,15 +11,17 @@
import OnboardingFollows from "src/app/views/OnboardingFollows.svelte"
import OnboardingNote from "src/app/views/OnboardingNote.svelte"
import {
env,
user,
mention,
session,
loadPubkeys,
publishNote,
publishPetnames,
publishProfile,
publishRelays,
loginWithPrivateKey,
} from "src/engine2"
import {Env, Keys} from "src/app/engine"
import {listenForNotifications} from "src/app/state"
import {modal} from "src/partials/state"
@ -29,14 +31,14 @@
const profile = {}
let petnames = closure(() => {
if (Keys.keyState.get().length > 0) {
if ($session) {
return []
}
const {petnames} = user.get()
if (petnames.length === 0) {
for (const pubkey of Env.DEFAULT_FOLLOWS) {
for (const pubkey of $env.DEFAULT_FOLLOWS) {
petnames.push(mention(pubkey))
}
}
@ -48,7 +50,7 @@
const {relays} = user.get()
if (relays.length === 0) {
for (const url of Env.DEFAULT_RELAYS) {
for (const url of $env.DEFAULT_RELAYS) {
relays.push({url, read: true, write: true})
}
}
@ -57,7 +59,7 @@
})
const signup = async noteContent => {
Keys.login("privkey", privkey)
loginWithPrivateKey(privkey)
// Wait for the published event to go through
await publishRelays(relays)
@ -82,7 +84,7 @@
onMount(() => {
// Prime our database with some defaults
loadPubkeys(Env.DEFAULT_FOLLOWS)
loadPubkeys($env.DEFAULT_FOLLOWS)
})
</script>

View File

@ -6,12 +6,12 @@
import Anchor from "src/partials/Anchor.svelte"
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import {Env} from "src/app/engine"
import {env} from "src/engine2"
export let privkey
const nsec = nip19.nsecEncode(privkey)
const nextStage = Env.FORCE_RELAYS.length > 0 ? "follows" : "relays"
const nextStage = $env.FORCE_RELAYS.length > 0 ? "follows" : "relays"
const copyKey = () => {
copyToClipboard(nsec)

View File

@ -1,5 +1,4 @@
<script lang="ts">
import type {Relay} from "src/engine"
import {pluck, reject, propEq} from "ramda"
import {fuzzy} from "src/util/misc"
import {modal} from "src/partials/state"
@ -8,6 +7,7 @@
import Heading from "src/partials/Heading.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte"
import type {Relay} from "src/engine2"
import {relays as knownRelays} from "src/engine2"
export let relays: Relay[]

View File

@ -7,18 +7,17 @@
import Content from "src/partials/Content.svelte"
import Toggle from "src/partials/Toggle.svelte"
import Heading from "src/partials/Heading.svelte"
import {Keys} from "src/app/engine"
import {session} from "src/engine2"
import {toast} from "src/partials/state"
const {current} = Keys
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const keypairUrl = "https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/"
let asHex = false
$: pubkeyDisplay = asHex ? $current?.pubkey : nip19.npubEncode($current.pubkey)
$: pubkeyDisplay = asHex ? $session?.pubkey : nip19.npubEncode($session.pubkey)
$: privkeyDisplay =
asHex || !$current?.privkey ? $current.privkey : nip19.nsecEncode($current.privkey)
asHex || !$session?.privkey ? $session.privkey : nip19.nsecEncode($session.privkey)
const copyKey = (type, value) => {
copyToClipboard(value)
@ -62,7 +61,7 @@
on nostr.
</p>
</div>
{#if $current?.privkey}
{#if $session?.privkey}
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input disabled type="password" value={privkeyDisplay}>
@ -80,14 +79,14 @@
</p>
</div>
{/if}
{#if $current?.bunkerKey}
{#if $session?.bunkerKey}
<div class="flex flex-col gap-1">
<strong>Bunker Key</strong>
<Input disabled type="password" value={$current.bunkerKey}>
<Input disabled type="password" value={$session.bunkerKey}>
<button
slot="after"
class="fa-solid fa-copy cursor-pointer"
on:click={() => copyKey("bunker", $current.bunkerKey)} />
on:click={() => copyKey("bunker", $session.bunkerKey)} />
</Input>
<p class="text-sm text-gray-1">
Your bunker key is used to authorize Coracle with your nsec bunker to sign events on

View File

@ -1,17 +0,0 @@
import type {Env} from "./types"
import {Events} from "./components/Events"
import {Keys} from "./components/Keys"
export class Engine {
Env: Env
Events = new Events()
Keys = new Keys()
constructor(Env: Env) {
this.Env = Env
for (const component of Object.values(this)) {
component.initialize?.(this)
}
}
}

View File

@ -1,30 +0,0 @@
import type {Event} from "src/engine/types"
import {pushToKey} from "src/util/misc"
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 {
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.Keys.pubkey.get()) {
this.cache.key(event.id).set(event)
}
for (const handler of this.handlers[ANY_KIND] || []) {
await handler(event)
}
for (const handler of this.handlers[event.kind.toString()] || []) {
await handler(event)
}
})
}
}

View File

@ -1,157 +0,0 @@
import {propEq, equals, prop, defaultTo, find, reject} from "ramda"
import type {Event} from "nostr-tools"
import {nip19, getPublicKey, getSignature, generatePrivateKey} from "nostr-tools"
import NDK, {NDKEvent, NDKNip46Signer, NDKPrivateKeySigner} from "@nostr-dev-kit/ndk"
import {switcherFn} from "hurdak"
import {writable} from "src/engine/util/store"
import type {Session} from "src/engine/types"
import type {Engine} from "src/engine/Engine"
export class Keys {
pubkey = writable<string | null>(null)
keyState = writable<Session[]>([])
stateKey = this.pubkey.derived(defaultTo("anonymous"))
current = this.pubkey.derived(k => this.getSession(k))
privkey = this.current.derived(prop("privkey"))
method = this.current.derived(prop("method"))
canSign = this.method.derived($method => ["bunker", "privkey", "extension"].includes($method))
canUseGiftWrap = this.method.derived(equals("privkey"))
getSession = (k: string) => find(propEq("pubkey", k), this.keyState.get())
setSession = (v: Session) =>
this.keyState.update((s: Session[]) => reject(propEq("pubkey", v.pubkey), s).concat(v))
removeSession = (k: string) =>
this.keyState.update((s: Session[]) => reject(propEq("pubkey", k), s))
withExtension = (() => {
let extensionLock = Promise.resolve()
const getExtension = () => (window as {nostr?: any}).nostr
return (f: (ext: any) => void) => {
extensionLock = extensionLock.catch(e => console.error(e)).then(() => f(getExtension()))
return extensionLock
}
})()
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 {
nip19.npubEncode(key)
} catch (e) {
return false
}
return true
}
getNDK = (() => {
const ndkInstances = new Map()
const prepareNDK = async (token?: string) => {
const {pubkey, bunkerKey} = this.current.get() as Session
const localSigner = new NDKPrivateKeySigner(bunkerKey)
const ndk = new NDK({
explicitRelayUrls: [
"wss://relay.f7z.io",
"wss://relay.damus.io",
"wss://relay.nsecbunker.com",
],
})
ndk.signer = Object.assign(new NDKNip46Signer(ndk, pubkey, localSigner), {token})
await ndk.connect(5000)
await ndk.signer.blockUntilReady()
ndkInstances.set(pubkey, ndk)
}
return async (token?: string) => {
const {pubkey} = this.current.get() as Session
if (!ndkInstances.has(pubkey)) {
await prepareNDK(token)
}
return ndkInstances.get(pubkey)
}
})()
login = (method: string, key: string | {pubkey: string; token: string}) => {
let pubkey = null
let privkey = null
let bunkerKey = null
if (method === "privkey") {
privkey = key as string
pubkey = getPublicKey(privkey)
} else if (["pubkey", "extension"].includes(method)) {
pubkey = key as string
} else if (method === "bunker") {
pubkey = (key as {pubkey: string}).pubkey
bunkerKey = generatePrivateKey()
this.getNDK((key as {token: string}).token)
}
this.setSession({method, pubkey, privkey, bunkerKey})
this.pubkey.set(pubkey)
}
sign = async (event: Event, sk?: string) => {
if (sk) {
return Object.assign(event, {
sig: getSignature(event, sk),
})
}
const {method, privkey} = this.current.get()
console.assert(event.id)
console.assert(event.pubkey)
console.assert(event.created_at)
return switcherFn(method, {
bunker: async () => {
const ndk = await this.getNDK()
const ndkEvent = new NDKEvent(ndk, event)
await ndkEvent.sign(ndk.signer)
return ndkEvent.rawEvent()
},
privkey: () => {
return Object.assign(event, {
sig: getSignature(event, privkey),
})
},
extension: () => this.withExtension(ext => ext.signEvent(event)),
})
}
clear = () => {
const $pubkey = this.pubkey.get()
this.pubkey.set(null)
if ($pubkey) {
this.removeSession($pubkey)
}
}
initialize(engine: Engine) {
// pass
}
}

View File

@ -1,5 +0,0 @@
export * from "./types"
export {Engine} from "./Engine"
export {Events} from "./components/Events"
export {Keys} from "./components/Keys"

View File

@ -1,4 +0,0 @@
{
"extends": "../../tsconfig.json",
"strict": true
}

View File

@ -1,183 +0,0 @@
import type {Event as NostrToolsEvent} from "nostr-tools"
export type Event = Omit<NostrToolsEvent, "kind"> & {
kind: number
seen_on: string[]
wrap?: Event
}
export type ZapEvent = Event & {
invoiceAmount: number
request: Event
}
export type DisplayEvent = Event & {
zaps: Event[]
replies: DisplayEvent[]
reactions: Event[]
matchesFilter?: boolean
}
export type Filter = {
ids?: string[]
kinds?: number[]
authors?: string[]
since?: number
until?: number
limit?: number
search?: string
[key: `#${string}`]: string[]
}
export type DynamicFilter = Omit<Filter, "authors"> & {
authors?: string[] | "follows" | "network" | "global"
}
export type Zapper = {
pubkey: string
lnurl: string
callback: string
minSendable: number
maxSendable: number
nostrPubkey: string
created_at: number
updated_at: number
}
export type Handle = {
profile: Record<string, any>
pubkey: string
address: string
created_at: number
updated_at: number
}
export type RelayInfo = {
contact?: string
description?: string
last_checked?: number
supported_nips?: number[]
limitation?: {
payment_required?: boolean
auth_required?: boolean
}
}
export type Relay = {
url: string
count?: number
first_seen?: number
info?: RelayInfo
}
export type RelayPolicyEntry = {
url: string
read: boolean
write: boolean
}
export type RelayPolicy = {
pubkey: string
created_at: number
updated_at: number
relays: RelayPolicyEntry[]
}
export type GraphEntry = {
pubkey: string
updated_at: number
petnames_updated_at?: number
petnames?: string[][]
mutes_updated_at?: number
mutes?: string[][]
}
export type Profile = {
pubkey: string
created_at?: number
updated_at?: number
name?: string
nip05?: string
lud16?: string
about?: string
banner?: string
picture?: string
display_name?: string
}
export type Channel = {
id: string
name?: string
about?: string
picture?: string
pubkey: string
updated_at: number
last_sent?: number
last_received?: number
last_checked?: number
joined?: boolean
hints: string[]
}
export type Contact = {
id: string
pubkey: string
updated_at: number
last_sent?: number
last_received?: number
last_checked?: number
hints: string[]
}
export type Message = {
id: string
contact?: string
channel?: string
pubkey: string
created_at: number
content: string
tags: string[][]
}
export type Nip24Channel = {
id: string
updated_at: number
last_sent?: number
last_received?: number
last_checked?: number
hints: string[]
}
export type Topic = {
name: string
count?: number
}
export type List = {
name: string
naddr: string
pubkey: string
tags: string[][]
updated_at: number
created_at: number
deleted_at?: number
}
export type Env = {
IMGPROXY_URL: string
DUFFLEPUD_URL: string
MULTIPLEXTR_URL: string
FORCE_RELAYS: string[]
COUNT_RELAYS: string[]
SEARCH_RELAYS: string[]
DEFAULT_RELAYS: string[]
ENABLE_ZAPS: boolean
[key: string]: any
}
export type Session = {
method: string
pubkey: string
privkey: string | null
bunkerKey: string | null
}

View File

@ -1,90 +0,0 @@
import {verifySignature, matchFilters} from "nostr-tools"
import EventEmitter from "events"
import {defer, tryFunc} from "hurdak"
import type {Executor} from "paravel"
import type {Event, Filter} from "src/engine/types"
import {warn} from "src/util/logger"
type SubscriptionOpts = {
executor: typeof Executor
relays: string[]
filters: Filter[]
timeout?: number
}
export class Subscription extends EventEmitter {
opened = Date.now()
closed: number = null
result = defer()
events = []
seen = new Map()
eose = new Set()
sub: {unsubscribe: () => void} = null
id = Math.random().toString().slice(12, 16)
constructor(readonly opts: SubscriptionOpts) {
super()
if (opts.timeout) {
setTimeout(this.close, opts.timeout)
}
this.sub = opts.executor.subscribe(opts.filters, {
onEvent: this.onEvent,
onEose: this.onEose,
})
}
onEvent = (url: string, event: Event) => {
const {filters} = this.opts
const seen_on = this.seen.get(event.id)
if (seen_on) {
if (!seen_on.includes(url)) {
seen_on.push(url)
}
return
}
event.seen_on = [url]
event.content = event.content || ""
this.seen.set(event.id, event.seen_on)
if (!tryFunc(() => verifySignature(event))) {
warn("Signature verification failed", {event})
return
}
if (!matchFilters(filters, event)) {
warn("Event failed to match filter", {filters, event})
return
}
this.emit("event", event)
}
onEose = (url: string) => {
const {timeout, relays} = this.opts
this.emit("eose", url)
this.eose.add(url)
if (timeout && this.eose.size === relays.length) {
this.close()
}
}
close = () => {
if (!this.closed) {
this.closed = Date.now()
this.result.resolve(this.events)
this.sub.unsubscribe()
this.opts.executor.target.cleanup()
this.emit("close", this.events)
this.removeAllListeners()
}
}
}

View File

@ -1,37 +0,0 @@
export class Worker<T> {
buffer: T[]
handlers: Array<(x: T) => void>
timeout: NodeJS.Timeout | undefined
constructor() {
this.buffer = []
this.handlers = []
}
#doWork = async () => {
for (const message of this.buffer.splice(0, 50)) {
for (const handler of this.handlers) {
await handler(message)
}
}
this.timeout = undefined
this.#enqueueWork()
}
#enqueueWork = () => {
if (!this.timeout && this.buffer.length > 0) {
this.timeout = setTimeout(this.#doWork, 50)
}
}
push = (message: T) => {
this.buffer.push(message)
this.#enqueueWork()
}
listen = (handler: (x: T) => void) => {
this.handlers.push(handler)
}
}

View File

@ -1,210 +0,0 @@
import {is, reject, filter, map, findIndex, equals} from "ramda"
import {ensurePlural} from "hurdak"
type Invalidator<T> = (value?: T) => void
type Derivable = Readable<any> | Readable<any>[]
type Subscriber<T> = (value: T) => void
type Unsubscriber = () => void
type R = Record<string, any>
type M<T> = Map<string, T>
export interface Readable<T> {
get: () => T
subscribe(this: void, run: Subscriber<T>, invalidate?: Invalidator<T>): Unsubscriber
derived: <U>(f: (v: T) => U) => Readable<U>
}
export class Writable<T> implements Readable<T> {
private value: T
private subs: Subscriber<T>[] = []
constructor(defaultValue: T) {
this.value = defaultValue
}
notify() {
for (const sub of this.subs) {
sub(this.value)
}
}
get() {
return this.value
}
set(newValue: T) {
this.value = newValue
this.notify()
}
update(f: (v: T) => T) {
this.set(f(this.value))
}
subscribe(f: Subscriber<T>) {
this.subs.push(f)
f(this.value)
return () => {
const idx = findIndex(equals(f), this.subs)
this.subs.splice(idx, 1)
}
}
derived<U>(f: (v: T) => U): Derived<U> {
return new Derived<U>(this, f)
}
}
export class Derived<T> implements Readable<T> {
private callerSubs: Subscriber<T>[] = []
private mySubs: Unsubscriber[] = []
private value: T = null
private stores: Derivable
private getValue: (values: any) => T
constructor(stores: Derivable, getValue: (values: any) => T) {
if (!getValue) {
throw new Error(`Invalid derivation function`)
}
this.stores = stores
this.getValue = getValue
}
notify() {
this.callerSubs.forEach(f => f(this.get()))
}
getInput() {
if (is(Array, this.stores)) {
return ensurePlural(this.stores).map(s => s.get())
} else {
return this.stores.get()
}
}
get = (): T => this.getValue(this.getInput())
subscribe(f: Subscriber<T>) {
if (this.callerSubs.length === 0) {
for (const s of ensurePlural(this.stores)) {
this.mySubs.push(s.subscribe(() => this.notify()))
}
}
this.callerSubs.push(f)
f(this.get())
return () => {
const idx = findIndex(equals(f), this.callerSubs)
this.callerSubs.splice(idx, 1)
if (this.callerSubs.length === 0) {
for (const unsub of this.mySubs.splice(0)) {
unsub()
}
}
}
}
derived<U>(f: (v: T) => U): Readable<U> {
return new Derived(this, f) as Readable<U>
}
}
export class Key<T extends R> implements Readable<T> {
readonly pk: string
readonly key: string
private base: Writable<M<T>>
private store: Readable<T>
constructor(base: Writable<M<T>>, pk: string, key: string) {
if (!is(Map, base.get())) {
throw new Error("`key` can only be used on map collections")
}
this.pk = pk
this.key = key
this.base = base
this.store = base.derived<T>(m => m.get(key) as T)
}
get = () => this.base.get().get(this.key) as T
subscribe = (f: Subscriber<T>) => this.store.subscribe(f)
derived = <U>(f: (v: T) => U) => this.store.derived<U>(f)
exists = () => this.base.get().has(this.key)
update = (f: (v: T) => T) =>
this.base.update((m: M<T>) => {
if (!this.key) {
throw new Error(`Cannot set key: "${this.key}"`)
}
// Make sure the pk always get set on the record
m.set(this.key, f({...m.get(this.key), [this.pk]: this.key}))
return m
})
set = (v: T) => this.update(() => v)
merge = (d: Partial<T>) => this.update(v => ({...v, ...d}))
remove = () =>
this.base.update(m => {
m.delete(this.key)
return m
})
}
export class Collection<T extends R> implements Readable<T[]> {
readonly pk: string
readonly mapStore: Writable<M<T>>
readonly listStore: Readable<T[]>
constructor(pk: string) {
this.pk = pk
this.mapStore = writable(new Map())
this.listStore = this.mapStore.derived<T[]>((m: M<T>) => Array.from(m.values()))
}
get = () => this.listStore.get()
getMap = () => this.mapStore.get()
subscribe = (f: Subscriber<T[]>) => this.listStore.subscribe(f)
derived = <U>(f: (v: T[]) => U) => this.listStore.derived<U>(f)
key = (k: string) => new Key(this.mapStore, this.pk, k)
set = (xs: T[]) => this.mapStore.set(new Map(xs.map(x => [x[this.pk], x])))
update = (f: (v: T[]) => T[]) =>
this.mapStore.update(m => new Map(f(Array.from(m.values())).map(x => [x[this.pk], x])))
reject = (f: (v: T) => boolean) => this.update(reject(f))
filter = (f: (v: T) => boolean) => this.update(filter(f))
map = (f: (v: T) => T) => this.update(map(f))
}
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 collection = <T extends R>(pk: string) => new Collection<T>(pk)

View File

@ -1,4 +1,5 @@
import {whereEq} from "ramda"
import {nip19} from "nostr-tools"
import {derived} from "src/engine2/util/store"
import {session, people} from "src/engine2/state"
import {prepareNdk, ndkInstances} from "./ndk"
@ -7,6 +8,18 @@ import {Nip04} from "./nip04"
import {Nip44} from "./nip44"
import {Nip59} from "./nip59"
export const 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 {
nip19.npubEncode(key)
} catch (e) {
return false
}
return true
}
export const stateKey = session.derived($s => $s?.pubkey || "anonymous")
export const user = derived([session, people.mapStore], ([$s, $p]) => $p.get($s?.pubkey))

View File

@ -1,6 +1,8 @@
import "src/app.css"
import {identity} from "ramda"
import Bugsnag from "@bugsnag/js"
import {env} from "src/engine2"
import App from "src/app/App.svelte"
import {installPrompt} from "src/partials/state"
@ -19,6 +21,45 @@ window.addEventListener("beforeinstallprompt", e => {
installPrompt.set(e)
})
const IMGPROXY_URL = import.meta.env.VITE_IMGPROXY_URL
const DUFFLEPUD_URL = import.meta.env.VITE_DUFFLEPUD_URL
const MULTIPLEXTR_URL = import.meta.env.VITE_MULTIPLEXTR_URL
const FORCE_RELAYS = (import.meta.env.VITE_FORCE_RELAYS || "").split(",").filter(identity)
const COUNT_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://rbr.bio"]
const SEARCH_RELAYS = FORCE_RELAYS.length > 0 ? FORCE_RELAYS : ["wss://relay.nostr.band"]
const DEFAULT_RELAYS =
FORCE_RELAYS.length > 0
? FORCE_RELAYS
: [
"wss://purplepag.es",
"wss://relay.damus.io",
"wss://relay.nostr.band",
"wss://relayable.org",
"wss://nostr.wine",
]
const DEFAULT_FOLLOWS = (import.meta.env.VITE_DEFAULT_FOLLOWS || "").split(",").filter(identity)
const ENABLE_ZAPS = JSON.parse(import.meta.env.VITE_ENABLE_ZAPS)
env.set({
DEFAULT_FOLLOWS,
IMGPROXY_URL,
DUFFLEPUD_URL,
MULTIPLEXTR_URL,
FORCE_RELAYS,
COUNT_RELAYS,
SEARCH_RELAYS,
DEFAULT_RELAYS,
ENABLE_ZAPS,
})
export default new App({
target: document.getElementById("app"),
})

View File

@ -1,7 +1,7 @@
import {nip19} from "nostr-tools"
import {is, fromPairs, mergeLeft, last, identity, prop, flatten, uniq} from "ramda"
import {ensurePlural, between, mapVals, tryFunc, avg, first} from "hurdak"
import type {Filter, Event, DisplayEvent} from "src/engine/types"
import type {Filter, Event, DisplayEvent} from "src/engine2/model"
import {tryJson} from "src/util/misc"
export const noteKinds = [1, 30023, 1063, 9802, 1808]