flotilla/src/app/commands.ts

311 lines
8.2 KiB
TypeScript
Raw Normal View History

2024-10-08 18:39:16 +00:00
import {get} from "svelte/store"
2024-10-09 23:15:09 +00:00
import {ctx, uniq, sleep, chunk, equals, choice} from "@welshman/lib"
2024-10-08 18:39:16 +00:00
import {
DELETE,
PROFILE,
INBOX_RELAYS,
RELAYS,
FOLLOWS,
REACTION,
2024-10-16 17:14:33 +00:00
AUTH_JOIN,
2024-10-08 18:39:16 +00:00
isSignedEvent,
createEvent,
displayProfile,
normalizeRelayUrl,
2024-10-08 22:30:18 +00:00
makeList,
addToListPublicly,
removeFromListByPredicate,
getListTags,
getRelayTags,
2024-10-08 18:39:16 +00:00
} from "@welshman/util"
import type {TrustedEvent, EventTemplate} from "@welshman/util"
2024-09-11 18:09:25 +00:00
import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {PublishStatus, AuthStatus, ConnectionStatus} from "@welshman/net"
2024-10-08 18:39:16 +00:00
import {Nip59, stamp} from "@welshman/signer"
2024-08-16 22:17:30 +00:00
import {
2024-08-30 23:25:41 +00:00
pubkey,
2024-10-08 18:39:16 +00:00
signer,
2024-08-30 23:25:41 +00:00
repository,
publishThunk,
2024-10-17 18:27:17 +00:00
mergeThunks,
2024-08-16 22:17:30 +00:00
loadProfile,
2024-10-04 00:04:00 +00:00
loadInboxRelaySelections,
2024-08-30 23:25:41 +00:00
profilesByPubkey,
relaySelectionsByPubkey,
getWriteRelayUrls,
2024-08-16 22:17:30 +00:00
loadFollows,
loadMutes,
2024-09-24 23:30:32 +00:00
getFollows,
2024-09-25 22:49:17 +00:00
tagEvent,
tagReactionTo,
2024-10-03 19:21:03 +00:00
getRelayUrls,
2024-10-08 22:30:18 +00:00
userRelaySelections,
2024-10-08 18:39:16 +00:00
userInboxRelaySelections,
2024-10-08 22:30:18 +00:00
nip44EncryptToSelf,
loadRelay,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
2024-10-09 17:12:34 +00:00
import {tagRoom, userMembership, MEMBERSHIPS, INDEXER_RELAYS, loadMembership} from "@app/state"
2024-08-16 19:42:00 +00:00
2024-08-23 20:33:36 +00:00
// Utils
export const getPubkeyHints = (pubkey: string) => {
2024-08-30 23:25:41 +00:00
const selections = relaySelectionsByPubkey.get().get(pubkey)
2024-08-23 20:33:36 +00:00
const relays = selections ? getWriteRelayUrls(selections) : []
const hints = relays.length ? relays : INDEXER_RELAYS
return hints
}
export const getPubkeyPetname = (pubkey: string) => {
2024-08-30 23:25:41 +00:00
const profile = profilesByPubkey.get().get(pubkey)
2024-08-23 20:33:36 +00:00
const display = displayProfile(profile)
return display
}
2024-08-26 21:43:43 +00:00
export const makeMention = (pubkey: string, hints?: string[]) => [
"p",
pubkey,
choice(hints || getPubkeyHints(pubkey)),
getPubkeyPetname(pubkey),
]
2024-08-23 20:33:36 +00:00
2024-08-26 21:43:43 +00:00
export const makeIMeta = (url: string, data: Record<string, string>) => [
"imeta",
`url ${url}`,
...Object.entries(data).map(([k, v]) => [k, v].join(" ")),
]
2024-08-23 20:33:36 +00:00
// Loaders
2024-09-11 18:09:25 +00:00
export const loadUserData = (
pubkey: string,
request: Partial<SubscribeRequestWithHandlers> = {},
) => {
const promise = Promise.all([
2024-10-04 00:04:00 +00:00
loadInboxRelaySelections(pubkey, request),
2024-10-09 17:12:34 +00:00
loadMembership(pubkey, request),
2024-09-11 18:09:25 +00:00
loadProfile(pubkey, request),
loadFollows(pubkey, request),
loadMutes(pubkey, request),
])
// Load followed profiles slowly in the background without clogging other stuff up
promise.then(async () => {
2024-09-24 23:30:32 +00:00
for (const pubkeys of chunk(50, getFollows(pubkey))) {
2024-09-11 18:09:25 +00:00
await sleep(300)
for (const pubkey of pubkeys) {
loadProfile(pubkey)
2024-09-24 23:30:32 +00:00
loadFollows(pubkey)
loadMutes(pubkey)
2024-09-11 18:09:25 +00:00
}
}
})
return promise
}
2024-08-14 23:25:36 +00:00
2024-10-03 19:21:03 +00:00
// Synchronization
export const broadcastUserData = async (relays: string[]) => {
const authors = [pubkey.get()!]
const kinds = [RELAYS, INBOX_RELAYS, FOLLOWS, PROFILE]
const events = repository.query([{kinds, authors}])
for (const event of events) {
if (isSignedEvent(event)) {
2024-10-17 18:56:13 +00:00
await publishThunk({event, relays}).result
2024-10-03 19:21:03 +00:00
}
}
}
// List updates
2024-08-23 20:33:36 +00:00
2024-10-08 22:30:18 +00:00
export const addSpaceMembership = async (url: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
2024-08-14 23:25:36 +00:00
2024-10-17 19:08:19 +00:00
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
2024-08-14 23:25:36 +00:00
}
2024-10-08 22:30:18 +00:00
export const removeSpaceMembership = async (url: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const pred = (t: string[]) => equals(["r", url], t) || t[2] !== url
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
2024-08-23 21:42:50 +00:00
2024-10-17 19:08:19 +00:00
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
2024-10-08 22:30:18 +00:00
}
2024-08-23 21:42:50 +00:00
2024-10-08 22:30:18 +00:00
export const addRoomMembership = async (url: string, room: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf)
2024-09-25 22:49:17 +00:00
2024-10-17 19:08:19 +00:00
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
2024-10-08 22:30:18 +00:00
}
2024-10-03 18:49:52 +00:00
2024-10-08 22:30:18 +00:00
export const removeRoomMembership = async (url: string, room: string) => {
const list = get(userMembership) || makeList({kind: MEMBERSHIPS})
const pred = (t: string[]) => equals(tagRoom(room, url), t)
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
2024-10-03 18:49:52 +00:00
2024-10-17 19:08:19 +00:00
return publishThunk({event, relays: ctx.app.router.WriteRelays().getUrls()}).result
2024-10-08 22:30:18 +00:00
}
2024-10-03 18:49:52 +00:00
2024-10-08 22:30:18 +00:00
export const setRelayPolicy = (url: string, read: boolean, write: boolean) => {
const list = get(userRelaySelections) || makeList({kind: RELAYS})
2024-10-03 18:49:52 +00:00
2024-10-09 23:15:09 +00:00
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
2024-10-03 19:21:03 +00:00
2024-10-08 22:30:18 +00:00
if (read && write) {
tags.push(["r", url])
} else if (read) {
tags.push(["r", url, "read"])
} else if (write) {
tags.push(["r", url, "write"])
}
2024-10-03 19:21:03 +00:00
2024-10-08 22:30:18 +00:00
return publishThunk({
event: createEvent(list.kind, {tags}),
relays: ctx.app.router.WriteRelays().getUrls(),
2024-10-17 19:08:19 +00:00
}).result
2024-10-08 22:30:18 +00:00
}
2024-10-03 19:21:03 +00:00
export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
2024-10-08 22:30:18 +00:00
const list = get(userInboxRelaySelections) || makeList({kind: INBOX_RELAYS})
2024-10-03 19:21:03 +00:00
// Only update inbox policies if they already exist or we're adding them
2024-10-08 22:30:18 +00:00
if (enabled || getRelayUrls(list).includes(url)) {
2024-10-09 23:15:09 +00:00
const tags = getRelayTags(getListTags(list)).filter(t => normalizeRelayUrl(t[1]) !== url)
2024-10-03 19:21:03 +00:00
2024-10-08 22:30:18 +00:00
if (enabled) {
tags.push(["relay", url])
}
2024-10-03 19:21:03 +00:00
2024-10-08 22:30:18 +00:00
return publishThunk({
event: createEvent(list.kind, {tags}),
relays: ctx.app.router.WriteRelays().getUrls(),
2024-10-17 19:08:19 +00:00
}).result
2024-10-03 19:21:03 +00:00
}
}
// Relay access
export const checkRelayAccess = async (url: string, claim = "") => {
2024-10-15 19:02:11 +00:00
const connection = ctx.net.pool.get(url)
2024-10-14 22:29:30 +00:00
await connection.auth.attemptIfRequested()
await connection.auth.waitIfPending()
2024-10-16 22:57:18 +00:00
const thunk = publishThunk({
2024-10-16 17:14:33 +00:00
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
relays: [url],
})
2024-10-16 22:57:18 +00:00
const result = await thunk.result
if (result[url].status !== PublishStatus.Success) {
2024-10-17 19:49:15 +00:00
const message =
connection.auth.message?.replace(/^.*: /, "") ||
result[url].message?.replace(/^.*: /, "") ||
"join request rejected"
return `Failed to join relay: ${message}`
}
}
export const checkRelayProfile = async (url: string) => {
const relay = await loadRelay(url)
if (!relay?.profile) {
return "Sorry, we weren't able to find that relay."
}
}
export const checkRelayConnection = async (url: string) => {
const connection = ctx.net.pool.get(url)
2024-10-14 22:29:30 +00:00
const okStatuses = [ConnectionStatus.Ok, ConnectionStatus.Slow]
await connection.ensureConnected()
2024-10-11 00:49:42 +00:00
if (!okStatuses.includes(connection.meta.getStatus())) {
2024-10-14 22:29:30 +00:00
return `Failed to connect`
2024-10-03 19:21:03 +00:00
}
}
2024-10-03 19:21:03 +00:00
export const checkRelayAuth = async (url: string) => {
const connection = ctx.net.pool.get(url)
2024-10-14 22:29:30 +00:00
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
2024-10-14 22:29:30 +00:00
await connection.auth.attemptIfRequested()
await connection.auth.waitIfPending()
2024-10-14 22:29:30 +00:00
if (!okStatuses.includes(connection.auth.status)) {
return `Failed to authenticate: "${connection.auth.message}"`
}
2024-10-03 19:21:03 +00:00
}
export const attemptRelayAccess = async (url: string, claim = "") =>
2024-10-11 00:15:20 +00:00
(await checkRelayProfile(url)) ||
(await checkRelayConnection(url)) ||
(await checkRelayAccess(url, claim)) ||
(await checkRelayAuth(url))
2024-09-25 22:49:17 +00:00
// Actions
2024-10-08 18:39:16 +00:00
export const sendWrapped = async ({
template,
pubkeys,
2024-10-17 18:27:17 +00:00
delay,
2024-10-08 18:39:16 +00:00
}: {
template: EventTemplate
pubkeys: string[]
2024-10-17 18:27:17 +00:00
delay?: number
2024-09-25 22:49:17 +00:00
}) => {
2024-10-08 18:39:16 +00:00
const nip59 = Nip59.fromSigner(signer.get()!)
2024-10-17 18:27:17 +00:00
return mergeThunks(
await Promise.all(
uniq(pubkeys).map(async recipient =>
publishThunk({
event: await nip59.wrap(recipient, stamp(template)),
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
delay,
})
),
)
2024-10-08 18:39:16 +00:00
)
2024-09-25 22:49:17 +00:00
}
2024-10-08 18:39:16 +00:00
export const makeReaction = ({
event,
content,
tags = [],
}: {
event: TrustedEvent
content: string
tags?: string[][]
}) =>
createEvent(REACTION, {
content,
tags: [...tags, ...tagReactionTo(event)],
2024-09-25 22:49:17 +00:00
})
2024-10-08 18:39:16 +00:00
export const publishReaction = ({
relays,
event,
content,
tags = [],
}: {
relays: string[]
event: TrustedEvent
content: string
tags?: string[][]
}) => publishThunk({event: makeReaction({event, content, tags}), relays})
2024-10-08 18:39:16 +00:00
export const makeDelete = ({event}: {event: TrustedEvent}) =>
createEvent(DELETE, {tags: [["k", String(event.kind)], ...tagEvent(event)]})
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
publishThunk({event: makeDelete({event}), relays})