forked from coracle/flotilla
430a3a589d
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com> Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
239 lines
6.5 KiB
TypeScript
239 lines
6.5 KiB
TypeScript
import {
|
|
RELAY_INVITE,
|
|
RELAY_JOIN,
|
|
RELAY_LEAVE,
|
|
ManagementMethod,
|
|
getRelaysFromList,
|
|
getTagValue,
|
|
isShareableRelayUrl,
|
|
makeEvent,
|
|
normalizeRelayUrl,
|
|
} from "@welshman/util"
|
|
import type {List, RelayProfile} from "@welshman/util"
|
|
import {AuthStateEvent, AuthStatus, Pool, SocketEvent, SocketStatus, load} from "@welshman/net"
|
|
import {derived, readable} from "svelte/store"
|
|
import {
|
|
call,
|
|
filterVals,
|
|
fromPairs,
|
|
isDefined,
|
|
last,
|
|
on,
|
|
poll,
|
|
simpleCache,
|
|
uniq,
|
|
} from "@welshman/lib"
|
|
import {throttled} from "@welshman/store"
|
|
import {
|
|
getRelay,
|
|
loadRelay,
|
|
manageRelay,
|
|
publishThunk,
|
|
sign,
|
|
waitForThunkError,
|
|
} from "@welshman/app"
|
|
import {checkRelayHasLivekit} from "$lib/livekit"
|
|
import {stripPrefix} from "@lib/util"
|
|
import {relaysMostlyRestricted} from "@app/policies"
|
|
|
|
export const hasNip29 = (relay?: RelayProfile) =>
|
|
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("29"))
|
|
|
|
export const hasNip50 = (relay?: RelayProfile) =>
|
|
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("50"))
|
|
|
|
export const hasNip70 = (relay?: RelayProfile) =>
|
|
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("70"))
|
|
|
|
export const encodeRelay = (url: string) =>
|
|
encodeURIComponent(
|
|
normalizeRelayUrl(url)
|
|
.replace(/^wss:\/\//, "")
|
|
.replace(/\/$/, ""),
|
|
)
|
|
|
|
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))
|
|
|
|
export const deriveSocket = (url: string) => {
|
|
const socket = Pool.get().get(url)
|
|
|
|
return readable(socket, set => {
|
|
const subs = [
|
|
on(socket, SocketEvent.Error, () => set(socket)),
|
|
on(socket, SocketEvent.Status, () => set(socket)),
|
|
on(socket.auth, AuthStateEvent.Status, () => set(socket)),
|
|
]
|
|
|
|
return () => subs.forEach(call)
|
|
})
|
|
}
|
|
|
|
export const deriveSocketStatus = (url: string) =>
|
|
throttled(
|
|
800,
|
|
derived([deriveSocket(url), relaysMostlyRestricted], ([$socket, $relaysMostlyRestricted]) => {
|
|
if ($socket.status === SocketStatus.Opening) {
|
|
return {theme: "warning", title: "Connecting"}
|
|
}
|
|
|
|
if ($socket.status === SocketStatus.Closing) {
|
|
return {theme: "gray-500", title: "Not Connected"}
|
|
}
|
|
|
|
if ($socket.status === SocketStatus.Closed) {
|
|
return {theme: "gray-500", title: "Not Connected"}
|
|
}
|
|
|
|
if ($socket.status === SocketStatus.Error) {
|
|
return {theme: "error", title: "Failed to Connect"}
|
|
}
|
|
|
|
if ($socket.auth.status === AuthStatus.Requested) {
|
|
return {theme: "warning", title: "Authenticating"}
|
|
}
|
|
|
|
if ($socket.auth.status === AuthStatus.PendingSignature) {
|
|
return {theme: "warning", title: "Authenticating"}
|
|
}
|
|
|
|
if ($socket.auth.status === AuthStatus.DeniedSignature) {
|
|
return {theme: "error", title: "Failed to Authenticate"}
|
|
}
|
|
|
|
if ($socket.auth.status === AuthStatus.PendingResponse) {
|
|
return {theme: "warning", title: "Authenticating"}
|
|
}
|
|
|
|
if ($socket.auth.status === AuthStatus.Forbidden) {
|
|
return {theme: "error", title: "Access Denied"}
|
|
}
|
|
|
|
if ($relaysMostlyRestricted[url]) {
|
|
return {theme: "error", title: "Access Denied"}
|
|
}
|
|
|
|
return {theme: "success", title: "Connected"}
|
|
}),
|
|
)
|
|
|
|
export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
|
|
return readable<ManagementMethod[]>([], set => {
|
|
manageRelay(url, {
|
|
method: ManagementMethod.SupportedMethods,
|
|
params: [],
|
|
}).then(({result = []}) => set(result))
|
|
})
|
|
})
|
|
|
|
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
|
|
readable<boolean | undefined>(undefined, set => {
|
|
checkRelayHasLivekit(url).then(has => set(has))
|
|
}),
|
|
)
|
|
|
|
export const shouldIgnoreError = (error: string) => {
|
|
const isIgnored = error.startsWith("mute: ")
|
|
const isAborted = error.includes("Signing was aborted")
|
|
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
|
|
|
|
return isIgnored || isAborted || isStrictNip29Relay
|
|
}
|
|
|
|
export const discoverRelays = (lists: List[]) =>
|
|
Promise.all(
|
|
uniq(lists.flatMap($l => getRelaysFromList($l)))
|
|
.filter(isShareableRelayUrl)
|
|
.map(url => loadRelay(url)),
|
|
)
|
|
|
|
export const requestRelayClaim = async (url: string) => {
|
|
const filters = [{kinds: [RELAY_INVITE], limit: 1}]
|
|
const events = await load({filters, relays: [url]})
|
|
|
|
if (events.length > 0) {
|
|
return getTagValue("claim", events[0].tags)
|
|
}
|
|
}
|
|
|
|
export const requestRelayClaims = async (urls: string[]) =>
|
|
filterVals(
|
|
isDefined,
|
|
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
|
|
)
|
|
|
|
export const canEnforceNip70 = (url: string) => hasNip70(getRelay(url))
|
|
|
|
export const attemptRelayAccess = async (url: string, claim = "") => {
|
|
const socket = Pool.get().get(url)
|
|
|
|
socket.attemptToOpen()
|
|
|
|
await poll({
|
|
signal: AbortSignal.timeout(3000),
|
|
condition: () => socket.status === SocketStatus.Open,
|
|
})
|
|
|
|
if (socket.status !== SocketStatus.Open) {
|
|
return `Failed to connect`
|
|
}
|
|
|
|
await socket.auth.attemptAuth(sign)
|
|
|
|
if (![AuthStatus.None, AuthStatus.Ok].includes(socket.auth.status)) {
|
|
if (socket.auth.details) {
|
|
return `Failed to authenticate (${socket.auth.details})`
|
|
} else {
|
|
return `Failed to authenticate (${last(socket.auth.status.split(":"))})`
|
|
}
|
|
}
|
|
|
|
const error = await waitForThunkError(publishJoinRequest({url, claim}))
|
|
|
|
if (shouldIgnoreError(error)) return
|
|
|
|
if (claim) {
|
|
if (error.includes("invite code")) {
|
|
return "join request rejected"
|
|
}
|
|
} else if (error.includes("invite code")) {
|
|
return
|
|
}
|
|
|
|
return stripPrefix(error)
|
|
}
|
|
|
|
export const deriveRelayAuthError = (url: string) => {
|
|
Pool.get().get(url).auth.attemptAuth(sign)
|
|
|
|
return derived(
|
|
[relaysMostlyRestricted, deriveSocket(url)],
|
|
([$relaysMostlyRestricted, $socket]) => {
|
|
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
|
return stripPrefix($socket.auth.details)
|
|
}
|
|
|
|
if ($relaysMostlyRestricted[url]) {
|
|
return stripPrefix($relaysMostlyRestricted[url])
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
export type JoinRequestParams = {
|
|
url: string
|
|
claim: string
|
|
}
|
|
|
|
export const makeJoinRequest = (params: JoinRequestParams) =>
|
|
makeEvent(RELAY_JOIN, {tags: [["claim", params.claim]]})
|
|
|
|
export const publishJoinRequest = (params: JoinRequestParams) =>
|
|
publishThunk({event: makeJoinRequest(params), relays: [params.url]})
|
|
|
|
export type LeaveRequestParams = {
|
|
url: string
|
|
}
|
|
|
|
export const publishLeaveRequest = (params: LeaveRequestParams) =>
|
|
publishThunk({event: makeEvent(RELAY_LEAVE), relays: [params.url]})
|