flotilla/src/app/commands.ts

506 lines
13 KiB
TypeScript
Raw Normal View History

import * as nip19 from "nostr-tools/nip19"
2024-10-08 18:39:16 +00:00
import {get} from "svelte/store"
2025-02-25 21:36:32 +00:00
import {ctx, randomId, uniq, equals} from "@welshman/lib"
2024-10-08 18:39:16 +00:00
import {
DELETE,
2025-01-15 01:23:27 +00:00
REPORT,
2024-10-08 18:39:16 +00:00
PROFILE,
INBOX_RELAYS,
RELAYS,
FOLLOWS,
REACTION,
2024-10-16 17:14:33 +00:00
AUTH_JOIN,
2024-12-10 17:44:04 +00:00
GROUP_JOIN,
GROUP_LEAVE,
GROUP_CREATE,
GROUP_EDIT_META,
2024-12-10 21:07:17 +00:00
GROUPS,
COMMENT,
2024-10-08 18:39:16 +00:00
isSignedEvent,
createEvent,
displayProfile,
normalizeRelayUrl,
2024-10-08 22:30:18 +00:00
makeList,
addToListPublicly,
removeFromListByPredicate,
getTag,
2024-10-08 22:30:18 +00:00
getListTags,
getRelayTags,
getRelayTagValues,
toNostrURI,
2025-02-25 21:36:32 +00:00
unionFilters,
2024-10-08 18:39:16 +00:00
} from "@welshman/util"
2025-02-25 21:36:32 +00:00
import type {TrustedEvent, Filter, EventContent, EventTemplate} from "@welshman/util"
2024-12-11 00:38:22 +00:00
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
2024-10-29 21:40:37 +00:00
import {Nip59, makeSecret, stamp, Nip46Broker} 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-11-12 22:32:36 +00:00
publishThunks,
2024-08-30 23:25:41 +00:00
profilesByPubkey,
relaySelectionsByPubkey,
getWriteRelayUrls,
2024-09-25 22:49:17 +00:00
tagEvent,
2025-01-17 23:01:05 +00:00
tagEventForReaction,
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-10-29 21:40:37 +00:00
addSession,
2024-12-04 18:10:41 +00:00
clearStorage,
dropSession,
2025-01-17 23:01:05 +00:00
tagEventForComment,
tagEventForQuote,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
2024-12-12 22:49:30 +00:00
import type {Thunk} from "@welshman/app"
2024-10-17 23:03:50 +00:00
import {
2024-12-10 17:44:04 +00:00
tagRoom,
PROTECTED,
2024-10-17 23:03:50 +00:00
userMembership,
INDEXER_RELAYS,
2024-11-19 17:25:02 +00:00
NIP46_PERMS,
2025-02-25 21:36:32 +00:00
ALERT,
NOTIFIER_PUBKEY,
NOTIFIER_RELAY,
2024-12-17 01:07:52 +00:00
userRoomsByUrl,
2024-10-17 23:03:50 +00:00
} from "@app/state"
2025-02-14 19:12:19 +00:00
import {loadUserData} from "@app/requests"
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
}
export const getThunkError = async (thunk: Thunk) => {
const result = await thunk.result
const [{status, message}] = Object.values(result) as any
if (status !== PublishStatus.Success) {
return message
}
}
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
if (parent) {
const nevent = nip19.neventEncode({
id: parent.id,
kind: parent.kind,
author: parent.pubkey,
relays: ctx.app.router.Event(parent).limit(3).getUrls(),
})
tags = [...tags, tagEventForQuote(parent)]
content = toNostrURI(nevent) + "\n\n" + content
}
return {content, tags}
}
2024-10-29 21:40:37 +00:00
// Log in
2024-12-03 00:21:54 +00:00
export const loginWithNip46 = async ({
relays,
signerPubkey,
clientSecret = makeSecret(),
connectSecret = "",
}: {
relays: string[]
signerPubkey: string
clientSecret?: string
connectSecret?: string
}) => {
const broker = Nip46Broker.get({relays, clientSecret, signerPubkey})
2025-01-02 18:04:28 +00:00
const result = await broker.connect(connectSecret, NIP46_PERMS)
2024-12-03 00:21:54 +00:00
// TODO: remove ack result
if (!["ack", connectSecret].includes(result)) return false
2024-10-29 21:40:37 +00:00
const pubkey = await broker.getPublicKey()
if (!pubkey) return false
2024-12-04 00:18:36 +00:00
await loadUserData(pubkey)
2024-12-03 00:21:54 +00:00
const handler = {relays, pubkey: signerPubkey}
addSession({method: "nip46", pubkey, secret: clientSecret, handler})
2024-10-29 21:40:37 +00:00
return true
}
2024-12-04 18:10:41 +00:00
// Log out
export const logout = async () => {
const $pubkey = pubkey.get()
if ($pubkey) {
dropSession($pubkey)
}
await clearStorage()
2024-12-04 23:11:32 +00:00
2024-12-04 18:10:41 +00:00
localStorage.clear()
}
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
}
}
}
2024-12-10 17:44:04 +00:00
// NIP 29 stuff
export const nip29 = {
createRoom: (url: string, room: string) => {
const event = createEvent(GROUP_CREATE, {tags: [tagRoom(room, url)]})
return publishThunk({event, relays: [url]})
},
editMeta: (url: string, room: string, meta: Record<string, string>) => {
const event = createEvent(GROUP_EDIT_META, {
tags: [tagRoom(room, url), ...Object.entries(meta)],
})
return publishThunk({event, relays: [url]})
},
2024-12-10 17:44:04 +00:00
joinRoom: (url: string, room: string) => {
const event = createEvent(GROUP_JOIN, {tags: [tagRoom(room, url)]})
return publishThunk({event, relays: [url]})
},
leaveRoom: (url: string, room: string) => {
const event = createEvent(GROUP_LEAVE, {tags: [tagRoom(room, url)]})
return publishThunk({event, relays: [url]})
},
}
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) => {
2024-12-10 21:07:17 +00:00
const list = get(userMembership) || makeList({kind: GROUPS})
2024-10-08 22:30:18 +00:00
const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf)
2024-11-12 21:32:54 +00:00
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
2024-08-14 23:25:36 +00:00
2024-11-15 22:03:54 +00:00
return publishThunk({event, relays})
2024-08-14 23:25:36 +00:00
}
2024-10-08 22:30:18 +00:00
export const removeSpaceMembership = async (url: string) => {
2024-12-10 21:07:17 +00:00
const list = get(userMembership) || makeList({kind: GROUPS})
2024-10-21 22:19:55 +00:00
const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url
2024-10-08 22:30:18 +00:00
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
2024-11-15 22:03:54 +00:00
const relays = uniq([
url,
...ctx.app.router.FromUser().getUrls(),
...getRelayTagValues(event.tags),
])
2024-08-23 21:42:50 +00:00
2024-11-15 22:03:54 +00:00
return publishThunk({event, relays})
2024-10-08 22:30:18 +00:00
}
2024-08-23 21:42:50 +00:00
export const addRoomMembership = async (url: string, room: string, name: string) => {
2024-12-10 21:07:17 +00:00
const list = get(userMembership) || makeList({kind: GROUPS})
const newTags = [
["r", url],
["group", room, url, name],
]
const event = await addToListPublicly(list, ...newTags).reconcile(nip44EncryptToSelf)
2024-11-12 21:32:54 +00:00
const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)])
2024-09-25 22:49:17 +00:00
2024-11-15 22:03:54 +00:00
return publishThunk({event, relays})
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) => {
2024-12-10 21:07:17 +00:00
const list = get(userMembership) || makeList({kind: GROUPS})
const pred = (t: string[]) => equals(["group", room, url], t.slice(0, 3))
2024-10-08 22:30:18 +00:00
const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf)
2024-11-15 22:03:54 +00:00
const relays = uniq([
url,
...ctx.app.router.FromUser().getUrls(),
...getRelayTagValues(event.tags),
])
2024-10-03 18:49:52 +00:00
2024-11-15 22:03:54 +00:00
return publishThunk({event, relays})
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-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}),
2024-11-15 22:03:54 +00:00
relays: [
url,
...INDEXER_RELAYS,
...ctx.app.router.FromUser().getUrls(),
2024-12-17 01:07:52 +00:00
...userRoomsByUrl.get().keys(),
2024-11-15 22:03:54 +00:00
],
})
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}),
2024-11-15 22:03:54 +00:00
relays: [
...INDEXER_RELAYS,
...ctx.app.router.FromUser().getUrls(),
2024-12-17 01:07:52 +00:00
...userRoomsByUrl.get().keys(),
2024-11-15 22:03:54 +00:00
],
})
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
2024-11-13 01:44:29 +00:00
await connection.auth.attempt(5000)
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
2024-12-30 23:36:54 +00:00
if (result[url].status === PublishStatus.Failure) {
2024-10-17 19:49:15 +00:00
const message =
connection.auth.message?.replace(/^.*: /, "") ||
result[url].message?.replace(/^.*: /, "") ||
"join request rejected"
2024-12-10 17:44:04 +00:00
// If it's a strict NIP 29 relay don't worry about requesting access
// TODO: remove this if relay29 ever gets less strict
if (message !== "missing group (`h`) tag") {
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-11-07 23:10:28 +00:00
await connection.socket.open()
await connection.socket.wait(3000)
2024-11-07 23:10:28 +00:00
if (connection.socket.status !== SocketStatus.Open) {
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, timeout = 3000) => {
const connection = ctx.net.pool.get(url)
2024-10-14 22:29:30 +00:00
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
await connection.auth.attempt(timeout)
2024-11-14 00:14:05 +00:00
// 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 (!okStatuses.includes(connection.auth.status) && connection.auth.message) {
return `Failed to authenticate (${connection.auth.message})`
}
2024-10-03 19:21:03 +00:00
}
export const attemptRelayAccess = async (url: string, claim = "") => {
2024-11-19 04:31:17 +00:00
const checks = [
() => checkRelayConnection(url),
() => checkRelayAccess(url, claim),
() => checkRelayAuth(url),
]
for (const check of checks) {
2024-11-19 04:31:17 +00:00
const error = await check()
if (error) {
return error
}
}
}
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-11-12 22:32:36 +00:00
return publishThunks(
2024-10-17 18:27:17 +00:00
await Promise.all(
2024-11-12 22:32:36 +00:00
uniq(pubkeys).map(async recipient => ({
event: await nip59.wrap(recipient, stamp(template)),
relays: ctx.app.router.PubkeyInbox(recipient).getUrls(),
delay,
})),
2024-10-17 23:03:50 +00:00
),
2024-10-08 18:39:16 +00:00
)
2024-09-25 22:49:17 +00:00
}
export const makeDelete = ({event}: {event: TrustedEvent}) => {
const tags = [["k", String(event.kind)], ...tagEvent(event)]
const groupTag = getTag("h", event.tags)
if (groupTag) {
tags.push(PROTECTED)
tags.push(groupTag)
}
return createEvent(DELETE, {tags})
}
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
publishThunk({event: makeDelete({event}), relays})
2025-01-15 01:23:27 +00:00
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 createEvent(REPORT, {content, tags})
}
export const publishReport = ({
relays,
event,
reason,
content,
}: ReportParams & {relays: string[]}) =>
publishThunk({event: makeReport({event, reason, content}), relays})
2024-10-23 21:20:24 +00:00
export type ReactionParams = {
2024-10-08 18:39:16 +00:00
event: TrustedEvent
content: string
2024-10-23 21:20:24 +00:00
}
2024-09-25 22:49:17 +00:00
export const makeReaction = ({event, content}: ReactionParams) => {
2025-01-17 23:01:05 +00:00
const tags = tagEventForReaction(event)
const groupTag = getTag("h", event.tags)
if (groupTag) {
tags.push(PROTECTED)
tags.push(groupTag)
}
return createEvent(REACTION, {content, tags})
}
2024-10-23 21:20:24 +00:00
export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
publishThunk({event: makeReaction(params), relays})
2025-01-17 23:01:05 +00:00
export type CommentParams = {
2024-10-08 18:39:16 +00:00
event: TrustedEvent
content: string
tags?: string[][]
2024-10-23 21:20:24 +00:00
}
2025-01-17 23:01:05 +00:00
export const makeComment = ({event, content, tags = []}: CommentParams) =>
createEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event)]})
2024-10-23 21:20:24 +00:00
2025-01-17 23:01:05 +00:00
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
2024-10-24 20:05:25 +00:00
publishThunk({event: makeComment(params), relays})
2025-02-25 21:36:32 +00:00
export type AlertParams = {
cron: string
email: string
relay: string
filters: Filter[]
2025-03-20 19:55:12 +00:00
bunker: string
secret: string
2025-02-25 21:36:32 +00:00
}
2025-03-20 19:55:12 +00:00
export const makeAlert = async ({cron, email, relay, filters, bunker, secret}: AlertParams) => {
const tags = [
["cron", cron],
["email", email],
["relay", relay],
["channel", "email"],
[
"handler",
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
"wss://relay.nostr.band/",
"web",
],
]
for (const filter of unionFilters(filters)) {
tags.push(["filter", JSON.stringify(filter)])
}
if (bunker) {
tags.push(["nip46", secret, bunker])
}
2025-03-20 16:38:57 +00:00
return createEvent(ALERT, {
2025-03-20 19:55:12 +00:00
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
2025-02-25 21:36:32 +00:00
tags: [
["d", randomId()],
["p", NOTIFIER_PUBKEY],
],
})
2025-03-20 16:38:57 +00:00
}
2025-02-25 21:36:32 +00:00
export const publishAlert = async (params: AlertParams) =>
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})