mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Add support for AUTH
This commit is contained in:
parent
e326df9a22
commit
4152f96be0
@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.19
|
||||
|
||||
- [x] Add confirmation to zap dialog
|
||||
- [x] Avoid pruning profiles we know we'll use more often
|
||||
- [x] Re-write pool to remove dependency on nostr-tools.relay
|
||||
- [x] Add support for AUTH
|
||||
|
||||
## 0.2.18
|
||||
|
||||
- [x] Re-write data storage layer to conserve memory using a custom LRU cache
|
||||
|
@ -1,9 +1,10 @@
|
||||
# Current
|
||||
|
||||
- [ ] https://github.com/staab/coracle/issues/42
|
||||
- [ ] Show loading/success on zap invoice screen
|
||||
- [ ] Fix iOS/safari/firefox
|
||||
- [ ] Add AUTH
|
||||
- [ ] Multiplex, charge past a certain usage level based on bandwidth
|
||||
- [ ] Fix compose, topics
|
||||
- [ ] Fix onboarding workflow w/forced relays
|
||||
|
||||
# Others
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
import {find, is, identity, nthArg, pluck} from "ramda"
|
||||
import {log, warn} from "src/util/logger"
|
||||
import {timedelta, hexToBech32, bech32ToHex, shuffle, now, sleep} from "src/util/misc"
|
||||
import {displayPerson, isLike} from "src/util/nostr"
|
||||
import {displayPerson, Tags, isLike} from "src/util/nostr"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {onReady, relays, people} from "src/agent/tables"
|
||||
import keys from "src/agent/keys"
|
||||
@ -83,6 +83,14 @@
|
||||
|
||||
$: style.textContent = `:root { ${getThemeVariables($theme)}; background: var(--gray-8); }`
|
||||
|
||||
// When we get an AUTH challenge from our pool, attempt to authenticate
|
||||
pool.eventBus.on("AUTH", async (challenge, connection) => {
|
||||
const publishable = cmd.authenticate(challenge, url)
|
||||
const [event] = await publishable.publish([{url: connection.url}])
|
||||
|
||||
connection.checkAuth(event.id)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// Keep scroll position on body, but don't allow scrolling
|
||||
const unsubModal = modal.subscribe($modal => {
|
||||
|
@ -7,6 +7,14 @@ import pool from "src/agent/pool"
|
||||
import sync from "src/agent/sync"
|
||||
import keys from "src/agent/keys"
|
||||
|
||||
const authenticate = (challenge, relay) =>
|
||||
new PublishableEvent(22242, {
|
||||
tags: [
|
||||
["challenge", challenge],
|
||||
["relay", relay],
|
||||
],
|
||||
})
|
||||
|
||||
const updateUser = updates => new PublishableEvent(0, {content: JSON.stringify(updates)})
|
||||
|
||||
const setRelays = newRelays =>
|
||||
@ -140,6 +148,7 @@ class PublishableEvent {
|
||||
}
|
||||
|
||||
export default {
|
||||
authenticate,
|
||||
updateUser,
|
||||
setRelays,
|
||||
setPetnames,
|
||||
|
@ -1,11 +1,12 @@
|
||||
import type {Relay, Filter} from "nostr-tools"
|
||||
import type {Deferred} from "src/util/misc"
|
||||
import type {MyEvent} from "src/util/types"
|
||||
import {throttle} from 'throttle-debounce'
|
||||
import {verifySignature} from "nostr-tools"
|
||||
import {pluck, objOf, identity, is} from "ramda"
|
||||
import {ensurePlural, noop} from "hurdak/lib/hurdak"
|
||||
import {warn, log, error} from "src/util/logger"
|
||||
import {union, defer, tryJson, now, difference} from "src/util/misc"
|
||||
import {union, EventBus, defer, tryJson, difference} from "src/util/misc"
|
||||
import {isRelay, normalizeRelayUrl} from "src/util/nostr"
|
||||
|
||||
const forceRelays = (import.meta.env.VITE_FORCE_RELAYS || "")
|
||||
@ -15,14 +16,20 @@ const forceRelays = (import.meta.env.VITE_FORCE_RELAYS || "")
|
||||
|
||||
// Connection management
|
||||
|
||||
const eventBus = new EventBus()
|
||||
|
||||
const connections = {}
|
||||
|
||||
const CONNECTION_STATUS = {
|
||||
NEW: "new",
|
||||
ERROR: "error",
|
||||
PENDING: "pending",
|
||||
CLOSED: "closed",
|
||||
READY: "ready",
|
||||
AUTH: "auth",
|
||||
ERROR: {
|
||||
CONNECTION: "error/connection",
|
||||
AUTH: "error/auth",
|
||||
},
|
||||
}
|
||||
|
||||
class Connection {
|
||||
@ -30,27 +37,17 @@ class Connection {
|
||||
url: string
|
||||
promise?: Deferred<void>
|
||||
queue: string[]
|
||||
status: string
|
||||
closed?: number
|
||||
status: {code: string; message: string; occurredAt: number}
|
||||
timeout?: number
|
||||
listeners: Record<string, Record<string, (...args: any[]) => void>>
|
||||
stats: Record<string, number>
|
||||
bus: EventBus
|
||||
constructor(url) {
|
||||
this.ws = null
|
||||
this.url = url
|
||||
this.promise = null
|
||||
this.queue = []
|
||||
this.status = CONNECTION_STATUS.NEW
|
||||
this.closed = null
|
||||
this.timeout = null
|
||||
|
||||
this.listeners = {
|
||||
OK: {},
|
||||
ERROR: {},
|
||||
EVENT: {},
|
||||
EOSE: {},
|
||||
}
|
||||
|
||||
this.bus = new EventBus()
|
||||
this.stats = {
|
||||
timeouts: 0,
|
||||
subsCount: 0,
|
||||
@ -60,20 +57,24 @@ class Connection {
|
||||
activeSubsCount: 0,
|
||||
}
|
||||
|
||||
this.setStatus(CONNECTION_STATUS.NEW, "Waiting to connect")
|
||||
|
||||
connections[url] = this
|
||||
}
|
||||
setStatus(code, message, extra = {}) {
|
||||
this.status = {code, message, ...extra, occurredAt: Date.now()}
|
||||
}
|
||||
connect() {
|
||||
if (this.ws) {
|
||||
throw new Error("Attempted to connect when already connected")
|
||||
}
|
||||
|
||||
this.status = CONNECTION_STATUS.PENDING
|
||||
this.ws = new WebSocket(this.url)
|
||||
this.promise = defer()
|
||||
this.closed = null
|
||||
this.ws = new WebSocket(this.url)
|
||||
this.setStatus(CONNECTION_STATUS.PENDING, "Trying to connect")
|
||||
|
||||
this.ws.addEventListener("open", () => {
|
||||
this.status = CONNECTION_STATUS.READY
|
||||
this.setStatus(CONNECTION_STATUS.READY, "Connected")
|
||||
this.promise.resolve()
|
||||
})
|
||||
|
||||
@ -85,30 +86,48 @@ class Connection {
|
||||
}
|
||||
})
|
||||
|
||||
this.ws.addEventListener("error", () => {
|
||||
this.status = CONNECTION_STATUS.ERROR
|
||||
this.ws.addEventListener("error", e => {
|
||||
this.disconnect(CONNECTION_STATUS.ERROR.CONNECTION, "Failed to connect")
|
||||
this.promise.reject()
|
||||
this.closed = now()
|
||||
})
|
||||
|
||||
this.ws.addEventListener("close", () => {
|
||||
this.status = CONNECTION_STATUS.CLOSED
|
||||
this.disconnect()
|
||||
this.promise.reject()
|
||||
this.closed = now()
|
||||
})
|
||||
|
||||
// Propagate auth to global handler
|
||||
this.bus.on("AUTH", throttle(3000, challenge => {
|
||||
this.setStatus(CONNECTION_STATUS.AUTH, "Logging in")
|
||||
|
||||
eventBus.handle("AUTH", challenge, this)
|
||||
}))
|
||||
}
|
||||
disconnect() {
|
||||
disconnect(error = null) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.close()
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.setStatus(...error)
|
||||
} else {
|
||||
this.setStatus(CONNECTION_STATUS.CLOSED, "Closed")
|
||||
}
|
||||
|
||||
this.ws = null
|
||||
}
|
||||
async autoConnect() {
|
||||
if (this.status === CONNECTION_STATUS.NEW) {
|
||||
const {code, occurredAt} = this.status
|
||||
const {NEW, CLOSED} = CONNECTION_STATUS
|
||||
|
||||
|
||||
// If the connection has not been opened, or was closed, open 'er up
|
||||
if ([NEW, CLOSED].includes(code)) {
|
||||
this.connect()
|
||||
} else if (this.closed && now() - 10 > this.closed) {
|
||||
// If the connection was closed, try to re-open, but throttle it
|
||||
}
|
||||
|
||||
// If the connection failed, try to re-open after a while
|
||||
if (code.startsWith("error") && Date.now() - 30_000 > occurredAt) {
|
||||
this.disconnect()
|
||||
this.connect()
|
||||
}
|
||||
@ -117,22 +136,14 @@ class Connection {
|
||||
|
||||
return this
|
||||
}
|
||||
on(name, id, cb) {
|
||||
this.listeners[name][id] = cb
|
||||
}
|
||||
off(name, id) {
|
||||
delete this.listeners[name][id]
|
||||
}
|
||||
handleMessages() {
|
||||
for (const json of this.queue.splice(0, 10)) {
|
||||
const message = tryJson(() => JSON.parse(json))
|
||||
|
||||
if (message) {
|
||||
const [verb, ...payload] = message
|
||||
const [k, ...payload] = message
|
||||
|
||||
for (const listener of Object.values(this.listeners[verb] || {})) {
|
||||
listener(...payload)
|
||||
}
|
||||
this.bus.handle(k, ...payload)
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +153,10 @@ class Connection {
|
||||
this.ws.send(JSON.stringify(payload))
|
||||
}
|
||||
subscribe(filters, id, {onEvent, onEose}) {
|
||||
this.on("EVENT", id, (subid, e) => subid === id && onEvent(e))
|
||||
this.on("EOSE", id, subid => subid === id && onEose())
|
||||
const [eventChannel, eoseChannel] = [
|
||||
this.bus.on("EVENT", (subid, e) => subid === id && onEvent(e)),
|
||||
this.bus.on("EOSE", subid => subid === id && onEose()),
|
||||
]
|
||||
|
||||
this.send("REQ", id, ...filters)
|
||||
|
||||
@ -152,31 +165,46 @@ class Connection {
|
||||
unsub: () => {
|
||||
this.send("CLOSE", id, ...filters)
|
||||
|
||||
this.off("EVENT", id)
|
||||
this.off("EOSE", id)
|
||||
this.bus.off("EVENT", eventChannel)
|
||||
this.bus.off("EOSE", eoseChannel)
|
||||
},
|
||||
}
|
||||
}
|
||||
publish(event, {onOk, onError}) {
|
||||
const withCleanup = f => eid => {
|
||||
if (eid === event.id) {
|
||||
f()
|
||||
this.off("OK", event.id)
|
||||
this.off("ERROR", event.id)
|
||||
const withCleanup = cb => k => {
|
||||
if (k === event.id) {
|
||||
cb()
|
||||
this.bus.off("OK", okChannel)
|
||||
this.bus.off("ERROR", errorChannel)
|
||||
}
|
||||
}
|
||||
|
||||
this.on("OK", event.id, withCleanup(onOk))
|
||||
this.on("ERROR", event.id, withCleanup(onError))
|
||||
const [okChannel, errorChannel] = [
|
||||
this.bus.on("OK", withCleanup(onOk)),
|
||||
this.bus.on("ERROR", withCleanup(onError)),
|
||||
]
|
||||
|
||||
this.send("EVENT", event)
|
||||
}
|
||||
checkAuth(eid) {
|
||||
const channel = this.bus.on("OK", (id, ok, message) => {
|
||||
if (id === eid) {
|
||||
if (ok) {
|
||||
this.setStatus(CONNECTION_STATUS.READY, "Connected")
|
||||
} else {
|
||||
this.disconnect(CONNECTION_STATUS.ERROR.AUTH, message)
|
||||
}
|
||||
|
||||
this.bus.off("OK", channel)
|
||||
}
|
||||
})
|
||||
}
|
||||
hasRecentError() {
|
||||
return this.status === CONNECTION_STATUS.ERROR && now() - this.closed < 10
|
||||
return this.status.code.startsWith("error") && Date.now() - this.status.occurredAt < 30_000
|
||||
}
|
||||
getQuality() {
|
||||
if (this.status === CONNECTION_STATUS.ERROR) {
|
||||
return [0, "Failed to connect"]
|
||||
if (this.status.code.startsWith("error")) {
|
||||
return [0, this.status.message]
|
||||
}
|
||||
|
||||
const {timeouts, subsCount, eoseTimer, eoseCount} = this.stats
|
||||
@ -195,15 +223,13 @@ class Connection {
|
||||
return [eoseQuality, "Connected"]
|
||||
}
|
||||
|
||||
if ([CONNECTION_STATUS.NEW, CONNECTION_STATUS.PENDING].includes(this.status)) {
|
||||
return [0.5, "Trying to connect"]
|
||||
const {NEW, PENDING, AUTH, CLOSED, READY} = CONNECTION_STATUS
|
||||
|
||||
if ([NEW, PENDING, AUTH, CLOSED].includes(this.status.code)) {
|
||||
return [0.5, this.status.message]
|
||||
}
|
||||
|
||||
if (this.status === CONNECTION_STATUS.CLOSED) {
|
||||
return [0.5, "Disconnected"]
|
||||
}
|
||||
|
||||
if (this.status === CONNECTION_STATUS.READY) {
|
||||
if (this.status.code === READY) {
|
||||
return [1, "Connected"]
|
||||
}
|
||||
}
|
||||
@ -300,8 +326,10 @@ const publish = async ({relays, event, onProgress, timeout = 5000}) => {
|
||||
|
||||
relays.map(async relay => {
|
||||
const conn = await connect(relay.url)
|
||||
const {READY, AUTH} = CONNECTION_STATUS
|
||||
const canPublish = [READY, AUTH].includes(conn.status.code)
|
||||
|
||||
if (conn.status === CONNECTION_STATUS.READY) {
|
||||
if (canPublish) {
|
||||
conn.publish(event, {
|
||||
onOk: () => {
|
||||
succeeded.add(relay.url)
|
||||
@ -360,7 +388,7 @@ const subscribe = async ({relays, filter, onEvent, onEose, onError}: SubscribeOp
|
||||
const promises = relays.map(async relay => {
|
||||
const conn = await connect(relay.url)
|
||||
|
||||
if (conn.status !== "ready") {
|
||||
if (conn.status.code !== CONNECTION_STATUS.READY) {
|
||||
if (onError) {
|
||||
onError(relay.url)
|
||||
}
|
||||
@ -452,6 +480,7 @@ const describeFilter = ({kinds = [], ...filter}) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
eventBus,
|
||||
forceRelays,
|
||||
getConnections,
|
||||
getConnection,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type {Relay} from "src/util/types"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {slice, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from "ramda"
|
||||
import {slice, uniqBy, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from "ramda"
|
||||
import {findReplyId, findRootId} from "src/util/nostr"
|
||||
import {synced} from "src/util/misc"
|
||||
import {derived} from "svelte/store"
|
||||
@ -98,7 +98,7 @@ export default {
|
||||
relays,
|
||||
getRelays: () => profileCopy.relays,
|
||||
updateRelays(f) {
|
||||
const $relays = f(profileCopy.relays)
|
||||
const $relays = uniqBy(prop('url'), f(profileCopy.relays))
|
||||
|
||||
profile.update(assoc("relays", $relays))
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
const className = cx(
|
||||
$$props.class,
|
||||
"rounded shadow-inset py-2 px-4 pr-10 w-full placeholder:text-gray-4",
|
||||
"rounded shadow-inset py-2 px-4 pr-10 w-full placeholder:text-gray-5",
|
||||
"bg-input border border-solid border-gray-3 text-black",
|
||||
{"pl-10": $$slots.before, "pr-10": $$slots.after}
|
||||
)
|
||||
|
@ -2,6 +2,8 @@ import {bech32, utf8} from "@scure/base"
|
||||
import {debounce, throttle} from "throttle-debounce"
|
||||
import {
|
||||
gt,
|
||||
whereEq,
|
||||
reject,
|
||||
mergeDeepRight,
|
||||
aperture,
|
||||
path as getPath,
|
||||
@ -18,7 +20,7 @@ import {
|
||||
} from "ramda"
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
import {writable} from "svelte/store"
|
||||
import {isObject, round} from "hurdak/lib/hurdak"
|
||||
import {isObject, randomId, round} from "hurdak/lib/hurdak"
|
||||
import {warn} from "src/util/logger"
|
||||
|
||||
export const fuzzy = (data, opts = {}) => {
|
||||
@ -394,3 +396,28 @@ export const formatSats = sats => {
|
||||
if (sats < 100_000_000) return formatter.format(round(1, sats / 1_000_000)) + "MM"
|
||||
return formatter.format(round(2, sats / 100_000_000)) + "BTC"
|
||||
}
|
||||
|
||||
type EventBusListener = {
|
||||
id: string
|
||||
handler: (...args: any[]) => void
|
||||
}
|
||||
|
||||
export class EventBus {
|
||||
listeners: Record<string, Array<EventBusListener>> = {}
|
||||
on(name, handler) {
|
||||
const id = randomId()
|
||||
|
||||
this.listeners[name] = this.listeners[name] || ([] as Array<EventBusListener>)
|
||||
this.listeners[name].push({id, handler})
|
||||
|
||||
return id
|
||||
}
|
||||
off(name, id) {
|
||||
this.listeners[name] = reject(whereEq({id}), this.listeners[name])
|
||||
}
|
||||
handle(k, ...payload) {
|
||||
for (const {handler} of this.listeners[k] || []) {
|
||||
handler(...payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user