forked from coracle/flotilla
752 lines
19 KiB
TypeScript
752 lines
19 KiB
TypeScript
import {nwc} from "@getalby/sdk"
|
|
import * as nip19 from "nostr-tools/nip19"
|
|
import {get, derived} from "svelte/store"
|
|
import {
|
|
first,
|
|
sha256,
|
|
append,
|
|
remove,
|
|
poll,
|
|
uniq,
|
|
equals,
|
|
parseJson,
|
|
last,
|
|
simpleCache,
|
|
normalizeUrl,
|
|
nthNe,
|
|
} from "@welshman/lib"
|
|
import {Nip01Signer} from "@welshman/signer"
|
|
import type {UploadTask} from "@welshman/editor"
|
|
import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util"
|
|
import {
|
|
DELETE,
|
|
REPORT,
|
|
PROFILE,
|
|
MESSAGING_RELAYS,
|
|
RELAYS,
|
|
FOLLOWS,
|
|
REACTION,
|
|
RELAY_JOIN,
|
|
RELAY_LEAVE,
|
|
ROOMS,
|
|
COMMENT,
|
|
APP_DATA,
|
|
isSignedEvent,
|
|
makeEvent,
|
|
normalizeRelayUrl,
|
|
makeList,
|
|
addToListPublicly,
|
|
removeFromListByPredicate,
|
|
updateList,
|
|
getTag,
|
|
getListTags,
|
|
getRelayTagValues,
|
|
toNostrURI,
|
|
RelayMode,
|
|
getTagValues,
|
|
uploadBlob,
|
|
canUploadBlob,
|
|
encryptFile,
|
|
makeBlossomAuthEvent,
|
|
isPublishedProfile,
|
|
editProfile,
|
|
createProfile,
|
|
uniqTags,
|
|
ManagementMethod,
|
|
} from "@welshman/util"
|
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
|
import {Router} from "@welshman/router"
|
|
import {
|
|
pubkey,
|
|
sign,
|
|
signer,
|
|
session,
|
|
repository,
|
|
publishThunk,
|
|
tagEvent,
|
|
tagPubkey,
|
|
tagEventForReaction,
|
|
nip44EncryptToSelf,
|
|
tagEventForComment,
|
|
tagEventForQuote,
|
|
waitForThunkError,
|
|
getPubkeyRelays,
|
|
userBlossomServerList,
|
|
getThunkError,
|
|
addRoomMember,
|
|
manageRelay,
|
|
} from "@welshman/app"
|
|
import {compressFile} from "@lib/html"
|
|
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
|
|
import {
|
|
SETTINGS,
|
|
PROTECTED,
|
|
INDEXER_RELAYS,
|
|
DEFAULT_BLOSSOM_SERVERS,
|
|
userSpaceUrls,
|
|
userSettingsValues,
|
|
getSetting,
|
|
getSettings,
|
|
userGroupList,
|
|
shouldIgnoreError,
|
|
stripPrefix,
|
|
relaysMostlyRestricted,
|
|
deriveSocket,
|
|
deriveSpaceMembers,
|
|
} from "@app/core/state"
|
|
|
|
// Utils
|
|
|
|
export const getPubkeyHints = (pubkey: string) => {
|
|
const relays = getPubkeyRelays(pubkey, RelayMode.Write)
|
|
const hints = relays.length ? relays : INDEXER_RELAYS
|
|
|
|
return hints
|
|
}
|
|
|
|
export const prependParent = (
|
|
parent: TrustedEvent | undefined,
|
|
{content, tags}: EventContent,
|
|
url?: string,
|
|
) => {
|
|
if (parent) {
|
|
const relays = url ? [url] : Router.get().Event(parent).limit(3).getUrls()
|
|
const nevent = nip19.neventEncode({...parent, relays})
|
|
|
|
tags = [...tags, tagEventForQuote(parent, url), tagPubkey(parent.pubkey)]
|
|
content = toNostrURI(nevent) + "\n\n" + content
|
|
}
|
|
|
|
return {content, tags}
|
|
}
|
|
|
|
// Synchronization
|
|
|
|
export const broadcastUserData = async (relays: string[]) => {
|
|
const authors = [pubkey.get()!]
|
|
const kinds = [RELAYS, MESSAGING_RELAYS, FOLLOWS, PROFILE]
|
|
const events = repository.query([{kinds, authors}])
|
|
|
|
for (const event of events) {
|
|
if (isSignedEvent(event)) {
|
|
await publishThunk({event, relays}).complete
|
|
}
|
|
}
|
|
}
|
|
|
|
// List updates
|
|
|
|
export const addSpaceMembership = async (url: string) => {
|
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
|
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
|
|
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
export const removeSpaceMembership = async (url: string) => {
|
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
|
const pred = (t: string[]) => normalizeRelayUrl(t[t[0] === "r" ? 1 : 2]) === url
|
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
|
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
export const setSpaceMembershipOrder = async (urls: string[]) => {
|
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
|
const orderedUrls = uniq(urls.map(normalizeRelayUrl))
|
|
const relayTags = list.publicTags.filter(t => t[0] === "r")
|
|
const otherPublicTags = list.publicTags.filter(t => t[0] !== "r")
|
|
const relayTagByUrl = new Map(relayTags.map(t => [normalizeRelayUrl(t[1]), t]))
|
|
const orderedRelayTags = orderedUrls.map(url => relayTagByUrl.get(url) || ["r", url])
|
|
const publicTags = [...orderedRelayTags, ...otherPublicTags]
|
|
const event = await updateList(list, {publicTags}).reconcile(nip44EncryptToSelf)
|
|
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
export const addRoomMembership = async (url: string, h: string) => {
|
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
|
const newTags = [
|
|
["r", url],
|
|
["group", h, url],
|
|
]
|
|
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
|
|
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
export const removeRoomMembership = async (url: string, h: string) => {
|
|
const list = get(userGroupList) || makeList({kind: ROOMS})
|
|
const pred = (t: string[]) => equals(["group", h, url], t.slice(0, 3))
|
|
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
|
|
const relays = uniq([url, ...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
// Relay access
|
|
|
|
export const canEnforceNip70 = async (url: string) => {
|
|
const socket = Pool.get().get(url)
|
|
|
|
await socket.auth.attemptAuth(sign)
|
|
|
|
return socket.auth.status !== AuthStatus.None
|
|
}
|
|
|
|
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)
|
|
|
|
// Only raise an error if it's not a timeout.
|
|
// If it is, odds are the problem is with our signer, not the relay
|
|
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 && error.includes("invite code size")) return
|
|
if (error.includes("invite code")) return "join request rejected"
|
|
|
|
return stripPrefix(error)
|
|
}
|
|
|
|
export const deriveRelayAuthError = (url: string, claim = "") => {
|
|
// Kick off the auth process
|
|
Pool.get().get(url).auth.attemptAuth(sign)
|
|
|
|
// Attempt to join the relay
|
|
const thunk = publishJoinRequest({url, claim})
|
|
|
|
return derived(
|
|
[thunk, relaysMostlyRestricted, deriveSocket(url)],
|
|
([$thunk, $relaysMostlyRestricted, $socket]) => {
|
|
if ($socket.auth.status === AuthStatus.Forbidden && $socket.auth.details) {
|
|
return stripPrefix($socket.auth.details)
|
|
}
|
|
|
|
if ($relaysMostlyRestricted[url]) {
|
|
return stripPrefix($relaysMostlyRestricted[url])
|
|
}
|
|
|
|
const error = getThunkError($thunk)
|
|
|
|
if (error) {
|
|
const isEmptyInvite = !claim && error.includes("invite code")
|
|
|
|
if (!shouldIgnoreError(error) && !isEmptyInvite) {
|
|
return stripPrefix(error) || "join request rejected"
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
// Deletions
|
|
|
|
export type DeleteParams = {
|
|
protect: boolean
|
|
event: TrustedEvent
|
|
tags?: string[][]
|
|
}
|
|
|
|
export const makeDelete = ({protect, event, tags = []}: DeleteParams) => {
|
|
const thisTags = [["k", String(event.kind)], ...tagEvent(event), ...tags]
|
|
const groupTag = getTag("h", event.tags)
|
|
|
|
if (groupTag) {
|
|
thisTags.push(groupTag)
|
|
}
|
|
|
|
if (protect) {
|
|
thisTags.push(PROTECTED)
|
|
}
|
|
|
|
return makeEvent(DELETE, {tags: thisTags})
|
|
}
|
|
|
|
export const publishDelete = ({relays, ...params}: DeleteParams & {relays: string[]}) =>
|
|
publishThunk({event: makeDelete(params), relays})
|
|
|
|
// Reports
|
|
|
|
export type ReportParams = {
|
|
event: TrustedEvent
|
|
content: string
|
|
reason: string
|
|
}
|
|
|
|
export const makeReport = ({event, reason, content}: ReportParams) => {
|
|
const tags = [
|
|
["p", event.pubkey],
|
|
["e", event.id, reason],
|
|
]
|
|
|
|
return makeEvent(REPORT, {content, tags})
|
|
}
|
|
|
|
export const publishReport = ({
|
|
relays,
|
|
event,
|
|
reason,
|
|
content,
|
|
}: ReportParams & {relays: string[]}) =>
|
|
publishThunk({event: makeReport({event, reason, content}), relays})
|
|
|
|
// Reactions
|
|
|
|
export type ReactionParams = {
|
|
protect: boolean
|
|
event: TrustedEvent
|
|
content: string
|
|
url?: string
|
|
tags?: string[][]
|
|
}
|
|
|
|
export const makeReaction = ({
|
|
url,
|
|
protect,
|
|
content,
|
|
event,
|
|
tags: paramTags = [],
|
|
}: ReactionParams) => {
|
|
const tags = [...paramTags, ...tagEventForReaction(event, url)]
|
|
const groupTag = getTag("h", event.tags)
|
|
|
|
if (groupTag) {
|
|
tags.push(groupTag)
|
|
}
|
|
|
|
if (protect) {
|
|
tags.push(PROTECTED)
|
|
}
|
|
|
|
return makeEvent(REACTION, {content, tags})
|
|
}
|
|
|
|
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) => {
|
|
publishThunk({event: makeReaction({url: relays[0], ...params}), relays})
|
|
}
|
|
|
|
// Comments
|
|
|
|
export type CommentParams = {
|
|
event: TrustedEvent
|
|
content: string
|
|
tags?: string[][]
|
|
url?: string
|
|
}
|
|
|
|
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
|
|
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
|
|
|
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
|
publishThunk({event: makeComment({url: relays[0], ...params}), relays})
|
|
|
|
// Settings
|
|
|
|
export const makeSettings = async (params: Partial<SettingsValues>) => {
|
|
const json = JSON.stringify({...get(userSettingsValues), ...params})
|
|
const content = await signer.get().nip44.encrypt(pubkey.get()!, json)
|
|
const tags = [["d", SETTINGS]]
|
|
|
|
return makeEvent(APP_DATA, {content, tags})
|
|
}
|
|
|
|
export const publishSettings = async (params: Partial<SettingsValues>) =>
|
|
publishThunk({event: await makeSettings(params), relays: Router.get().FromUser().getUrls()})
|
|
|
|
export const addTrustedRelay = async (url: string) =>
|
|
publishSettings({trusted_relays: append(url, getSetting<string[]>("trusted_relays"))})
|
|
|
|
export const removeTrustedRelay = async (url: string) =>
|
|
publishSettings({trusted_relays: remove(url, getSetting<string[]>("trusted_relays"))})
|
|
|
|
// Space and room notification settings
|
|
|
|
export const setSpaceNotifications = async (url: string, notify: boolean) => {
|
|
const {alerts} = getSettings()
|
|
const existing = alerts.find((s: SpaceNotificationSettings) => s.url === url)
|
|
|
|
let updated: typeof alerts
|
|
|
|
if (existing) {
|
|
// Clear exceptions when changing the space notification setting
|
|
updated = alerts.map((s: SpaceNotificationSettings) =>
|
|
s.url === url ? {...s, notify, exceptions: []} : s,
|
|
)
|
|
} else {
|
|
updated = [...alerts, {url, notify, exceptions: []}]
|
|
}
|
|
|
|
return publishSettings({alerts: updated})
|
|
}
|
|
|
|
export const toggleRoomNotifications = async (url: string, h: string) => {
|
|
const {alerts} = getSettings()
|
|
const existing = alerts.find((s: SpaceNotificationSettings) => s.url === url)
|
|
|
|
let updated: typeof alerts
|
|
|
|
if (!existing) {
|
|
// No space settings yet, create one with this room as an exception (default is notify: true)
|
|
updated = [...alerts, {url, notify: true, exceptions: [h]}]
|
|
} else {
|
|
// Toggle exception status
|
|
const hasException = existing.exceptions.includes(h)
|
|
const exceptions = hasException
|
|
? remove(h, existing.exceptions)
|
|
: append(h, existing.exceptions)
|
|
|
|
updated = alerts.map((s: SpaceNotificationSettings) => (s.url === url ? {...s, exceptions} : s))
|
|
}
|
|
|
|
return publishSettings({alerts: updated})
|
|
}
|
|
|
|
// Join request
|
|
|
|
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]})
|
|
|
|
// Leave request
|
|
|
|
export type LeaveRequestParams = {
|
|
url: string
|
|
}
|
|
|
|
export const publishLeaveRequest = (params: LeaveRequestParams) =>
|
|
publishThunk({event: makeEvent(RELAY_LEAVE), relays: [params.url]})
|
|
|
|
// Lightning
|
|
|
|
export const getWebLn = () => (window as any).webln
|
|
|
|
export const getNwcClient = () => {
|
|
const $session = session.get()
|
|
|
|
if (!$session?.wallet || $session.wallet.type !== "nwc") {
|
|
throw new Error("No NWC wallet is connected")
|
|
}
|
|
|
|
const {info} = $session.wallet
|
|
|
|
if (info.nostrWalletConnectUrl) {
|
|
return new nwc.NWCClient({nostrWalletConnectUrl: info.nostrWalletConnectUrl})
|
|
}
|
|
|
|
return new nwc.NWCClient(info)
|
|
}
|
|
|
|
export const payInvoice = async (invoice: string, msats?: number) => {
|
|
const $session = session.get()
|
|
|
|
if (!$session?.wallet) {
|
|
throw new Error("No wallet is connected")
|
|
}
|
|
|
|
if ($session.wallet.type === "nwc") {
|
|
const params: {invoice: string; amount?: number} = {invoice}
|
|
if (msats) params.amount = msats
|
|
return getNwcClient().payInvoice(params)
|
|
} else if ($session.wallet.type === "webln") {
|
|
if (msats) throw new Error("Unable to pay zero invoices with webln")
|
|
return getWebLn()
|
|
.enable()
|
|
.then(() => getWebLn().sendPayment(invoice))
|
|
}
|
|
}
|
|
|
|
export type CreateInvoiceParams = {
|
|
sats: number
|
|
description?: string
|
|
}
|
|
|
|
export const createInvoice = async ({
|
|
sats,
|
|
description = "Receive via lightning",
|
|
}: CreateInvoiceParams) => {
|
|
const $session = session.get()
|
|
|
|
if (!$session?.wallet) {
|
|
throw new Error("No wallet is connected")
|
|
}
|
|
|
|
const satAmount = Math.floor(sats)
|
|
|
|
if (!Number.isFinite(satAmount) || satAmount <= 0) {
|
|
throw new Error("Invalid satoshi amount")
|
|
}
|
|
|
|
if ($session.wallet.type === "nwc") {
|
|
const createdInvoice = await getNwcClient().makeInvoice({
|
|
amount: satAmount * 1000,
|
|
description,
|
|
})
|
|
|
|
if (!createdInvoice.invoice) {
|
|
throw new Error("NWC wallet failed to return an invoice")
|
|
}
|
|
|
|
return createdInvoice.invoice
|
|
}
|
|
|
|
if ($session.wallet.type === "webln") {
|
|
const webLn = getWebLn()
|
|
|
|
if (!webLn) {
|
|
throw new Error("WebLN not available")
|
|
}
|
|
|
|
await webLn.enable()
|
|
|
|
const response = await webLn.makeInvoice({
|
|
amount: satAmount,
|
|
defaultMemo: description,
|
|
})
|
|
|
|
const paymentRequest =
|
|
typeof response === "string" ? response : response?.paymentRequest || response?.pr || ""
|
|
|
|
if (!paymentRequest) {
|
|
throw new Error("Invalid payment request returned from WebLN")
|
|
}
|
|
|
|
return paymentRequest
|
|
}
|
|
|
|
throw new Error("Unsupported wallet type")
|
|
}
|
|
|
|
// File upload
|
|
|
|
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
|
|
|
|
export const fetchHasBlossomSupport = async (url: string) => {
|
|
const server = normalizeBlossomUrl(url)
|
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
|
const headers: Record<string, string> = {
|
|
"X-Content-Type": "text/plain",
|
|
"X-Content-Length": "1",
|
|
"X-SHA-256": "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac",
|
|
}
|
|
|
|
try {
|
|
const authEvent = await $signer.sign(makeBlossomAuthEvent({action: "upload", server}))
|
|
const res = await canUploadBlob(server, {authEvent, headers})
|
|
|
|
return res.status === 200
|
|
} catch (e) {
|
|
if (!String(e).match(/Failed to fetch|NetworkError/)) {
|
|
console.error(e)
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
export const hasBlossomSupport = simpleCache(([url]: [string]) => fetchHasBlossomSupport(url))
|
|
|
|
export type GetBlossomServerOptions = {
|
|
url?: string
|
|
}
|
|
|
|
export const getBlossomServer = async (options: GetBlossomServerOptions = {}) => {
|
|
if (options.url) {
|
|
if (await hasBlossomSupport(options.url)) {
|
|
return normalizeBlossomUrl(options.url)
|
|
}
|
|
}
|
|
|
|
const userUrls = getTagValues("server", getListTags(get(userBlossomServerList)))
|
|
|
|
for (const url of userUrls) {
|
|
return normalizeBlossomUrl(url)
|
|
}
|
|
|
|
return first(DEFAULT_BLOSSOM_SERVERS)!
|
|
}
|
|
|
|
export type UploadFileOptions = {
|
|
url?: string
|
|
encrypt?: boolean
|
|
maxWidth?: number
|
|
maxHeight?: number
|
|
}
|
|
|
|
export type UploadFileResult = {
|
|
error?: string
|
|
result?: UploadTask
|
|
}
|
|
|
|
export const uploadFile = async (file: File, options: UploadFileOptions = {}) => {
|
|
try {
|
|
const {name, type} = file
|
|
|
|
if (!type.match("image/(webp|gif|svg)")) {
|
|
file = await compressFile(file, options)
|
|
}
|
|
|
|
const tags: string[][] = []
|
|
|
|
if (options.encrypt) {
|
|
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
|
|
|
|
tags.push(
|
|
["decryption-key", key],
|
|
["decryption-nonce", nonce],
|
|
["encryption-algorithm", algorithm],
|
|
)
|
|
|
|
file = new File([new Uint8Array(ciphertext)], name, {
|
|
type: "application/octet-stream",
|
|
})
|
|
}
|
|
|
|
const ext = "." + type.split("/")[1]
|
|
const server = await getBlossomServer(options)
|
|
const hashes = [await sha256(await file.arrayBuffer())]
|
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
|
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
|
|
const authEvent = await $signer.sign(authTemplate)
|
|
const res = await uploadBlob(server, file, {authEvent})
|
|
const text = await res.text()
|
|
|
|
let {uploaded, url, ...task} = parseJson(text) || {}
|
|
|
|
if (!uploaded) {
|
|
return {error: text || `Failed to upload file (HTTP ${res.status})`}
|
|
}
|
|
|
|
// Always append correct file extension if we encrypted the file, or if it's missing
|
|
if (options.encrypt) {
|
|
url = url.replace(/\.\w+$/, "") + ext
|
|
} else if (new URL(url).pathname.split(".").length === 1) {
|
|
url += ext
|
|
}
|
|
|
|
const result = {...task, tags, url}
|
|
|
|
return {result}
|
|
} catch (e: any) {
|
|
console.error("Error caught when uploading file:", e)
|
|
|
|
return {error: e.toString()}
|
|
}
|
|
}
|
|
|
|
// Update Profile
|
|
|
|
export const initProfile = (profile: Profile) => {
|
|
const template = createProfile(profile)
|
|
|
|
// Start out protected by default
|
|
template.tags.push(PROTECTED)
|
|
|
|
const event = makeEvent(PROFILE, template)
|
|
|
|
// Don't publish anywhere yet, wait until they join a space
|
|
return publishThunk({event, relays: []})
|
|
}
|
|
|
|
export const updateProfile = ({
|
|
profile,
|
|
shouldBroadcast = !getTag(PROTECTED, profile.event?.tags || []),
|
|
}: {
|
|
profile: Profile
|
|
shouldBroadcast?: boolean
|
|
}) => {
|
|
const router = Router.get()
|
|
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
|
const scenarios = [router.FromRelays(get(userSpaceUrls))]
|
|
|
|
if (shouldBroadcast) {
|
|
scenarios.push(router.FromUser(), router.Index())
|
|
template.tags = template.tags.filter(nthNe(0, "-"))
|
|
} else {
|
|
template.tags = uniqTags([...template.tags, PROTECTED])
|
|
}
|
|
|
|
const event = makeEvent(template.kind, template)
|
|
const relays = router.merge(scenarios).getUrls()
|
|
|
|
return publishThunk({event, relays})
|
|
}
|
|
|
|
// Admin actions
|
|
|
|
export const addSpaceMembers = async (
|
|
url: string,
|
|
pubkeys: string[],
|
|
): Promise<string | undefined> => {
|
|
const spaceMembers = get(deriveSpaceMembers(url))
|
|
const results = await Promise.all(
|
|
pubkeys
|
|
.filter(pubkey => !spaceMembers.includes(pubkey))
|
|
.map(pubkey =>
|
|
manageRelay(url, {
|
|
method: ManagementMethod.AllowPubkey,
|
|
params: [pubkey],
|
|
}),
|
|
),
|
|
)
|
|
|
|
for (const {error} of results) {
|
|
if (error) {
|
|
return error
|
|
}
|
|
}
|
|
}
|
|
|
|
export const addRoomMembers = async (
|
|
url: string,
|
|
room: PublishedRoomMeta,
|
|
pubkeys: string[],
|
|
): Promise<string | undefined> => {
|
|
const error = await addSpaceMembers(url, pubkeys)
|
|
|
|
if (error) {
|
|
return error
|
|
}
|
|
|
|
const errors = await Promise.all(
|
|
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, room, pubkey))),
|
|
)
|
|
|
|
for (const error of errors) {
|
|
if (error) {
|
|
return error
|
|
}
|
|
}
|
|
}
|