Files
flotilla/src/app/uploads.ts
T
hodlbod 9df8cee501 Migrate to new @welshman/domain + instance-based @welshman/app API
Adopts the rewritten welshman API: the removed @welshman/util helpers
(Profile/List/Room/Handler/Encryptable) are now Reader/Builder classes in
@welshman/domain, and @welshman/app dropped its global singletons for an App
instance + app.use(Plugin) registry.

- src/app/welshman.ts is now the app bootstrap + session-state module (one shared
  App instance, multi-account sessions/login, app-wide reactive views) rather than
  a compat shim re-exporting the old globals.
- Rewrote ~100 callers to use app.use(Plugin) directly (thunks, profiles, relays,
  rooms, zaps, tags, wot, feeds, sync); thunk helpers are now thunk methods.
- Added @welshman/domain dependency.
- Resolved residual gaps (storage hydration via plugin.onItem/wrapManager/Plaintext,
  relay-list mutators, search-relay list, outbox #d filter).

Best-effort: no toolchain/linking available, so this is not build- or
type-checked. Remaining judgment calls are flagged with TODO(welshman-migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsMjvv7krpZeHK1Njeneru
2026-06-20 14:55:06 +00:00

143 lines
3.8 KiB
TypeScript

import {Nip01Signer} from "@welshman/signer"
import type {UploadTask} from "@welshman/editor"
import {
canUploadBlob,
encryptFile,
getTagValues,
makeBlossomAuthEvent,
uploadBlob,
} from "@welshman/util"
import {Relays} from "@welshman/app"
import {app, signer, userBlossomServerList} from "@app/welshman"
import {first, normalizeUrl, parseJson, sha256, simpleCache} from "@welshman/lib"
import {get} from "svelte/store"
import {compressFile} from "@lib/html"
import {DEFAULT_BLOSSOM_SERVERS} from "@app/env"
export const normalizeBlossomUrl = (url: string) => normalizeUrl(url.replace(/^ws/, "http"))
export const fetchHasBlossomSupport = async (url: string) => {
const relay = app.use(Relays).get(url)
if (relay?.supported_nips?.map(String).includes("BUD-02")) {
return true
}
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", get(userBlossomServerList)?.tags() ?? [])
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 task
try {
task = parseJson(text)
} catch (e) {
return {error: text}
}
if (!task?.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
let url = task.url
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()}
}
}