From 84b475d11a66fbde3b2d0e8975cbb50f4f549f5f Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 18 Jun 2025 14:48:40 +0100 Subject: [PATCH] fix: AI slop --- src/routes/blossom.rs | 51 ++++++++++++++------ ui_src/src/components/mirror-suggestions.tsx | 45 ++++++++--------- ui_src/src/const.ts | 3 ++ ui_src/src/hooks/use-blossom-servers.ts | 32 ++++++------ ui_src/src/upload/blossom.ts | 2 +- ui_src/src/views/upload.tsx | 45 ++++++++--------- 6 files changed, 96 insertions(+), 82 deletions(-) diff --git a/src/routes/blossom.rs b/src/routes/blossom.rs index e653223..c2be6c3 100644 --- a/src/routes/blossom.rs +++ b/src/routes/blossom.rs @@ -5,7 +5,7 @@ use crate::routes::{delete_file, Nip94Event}; use crate::settings::Settings; use log::error; use nostr::prelude::hex; -use nostr::{Alphabet, SingleLetterTag, TagKind}; +use nostr::{Alphabet, JsonUtil, SingleLetterTag, TagKind}; use rocket::data::ByteUnit; use rocket::futures::StreamExt; use rocket::http::{Header, Status}; @@ -15,6 +15,7 @@ use rocket::{routes, Data, Request, Response, Route, State}; use serde::{Deserialize, Serialize}; use tokio::io::AsyncRead; use tokio_util::io::StreamReader; +use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(crate = "rocket::serde")] @@ -225,15 +226,25 @@ async fn mirror( settings: &State, req: Json, ) -> BlossomResponse { - if !check_method(&auth.event, "mirror") { + if !check_method(&auth.event, "upload") { return BlossomResponse::error("Invalid request method tag"); } if let Some(e) = check_whitelist(&auth, settings) { return e; } + let url = match Url::parse(&req.url) { + Ok(u) => u, + Err(e) => return BlossomResponse::error(format!("Invalid URL: {}", e)), + }; + + let hash = url + .path_segments() + .and_then(|mut c| c.next_back()) + .and_then(|s| s.split(".").next()); + // download file - let rsp = match reqwest::get(&req.url).await { + let rsp = match reqwest::get(url.clone()).await { Err(e) => { error!("Error downloading file: {}", e); return BlossomResponse::error("Failed to mirror file"); @@ -250,9 +261,10 @@ async fn mirror( let pubkey = auth.event.pubkey.to_bytes().to_vec(); process_stream( - StreamReader::new(rsp.bytes_stream().map(|result| { - result.map_err(std::io::Error::other) - })), + StreamReader::new( + rsp.bytes_stream() + .map(|result| result.map_err(std::io::Error::other)), + ), &mime_type, &None, &pubkey, @@ -261,6 +273,7 @@ async fn mirror( fs, db, settings, + hash.and_then(|h| hex::decode(h).ok()), ) .await } @@ -353,7 +366,7 @@ async fn process_upload( None } }); - + let size = size_tag.or(auth.x_content_length).unwrap_or(0); if size > 0 && size > settings.max_upload_bytes { return BlossomResponse::error("File too large"); @@ -367,15 +380,11 @@ async fn process_upload( // check quota (only if payments are configured) #[cfg(feature = "payments")] if let Some(payment_config) = &settings.payments { - let free_quota = payment_config.free_quota_bytes - .unwrap_or(104857600); // Default to 100MB + let free_quota = payment_config.free_quota_bytes.unwrap_or(104857600); // Default to 100MB let pubkey_vec = auth.event.pubkey.to_bytes().to_vec(); if size > 0 { - match db - .check_user_quota(&pubkey_vec, size, free_quota) - .await - { + match db.check_user_quota(&pubkey_vec, size, free_quota).await { Ok(false) => return BlossomResponse::error("Upload would exceed quota"), Err(_) => return BlossomResponse::error("Failed to check quota"), Ok(true) => {} // Quota check passed @@ -395,6 +404,7 @@ async fn process_upload( fs, db, settings, + None, ) .await } @@ -409,6 +419,7 @@ async fn process_stream<'p, S>( fs: &State, db: &State, settings: &State, + expect_hash: Option>, ) -> BlossomResponse where S: AsyncRead + Unpin + 'p, @@ -417,6 +428,15 @@ where Ok(FileSystemResult::NewFile(blob)) => { let mut ret: FileUpload = (&blob).into(); + // check expected hash (mirroring) + if let Some(h) = expect_hash { + if h != ret.id { + if let Err(e) = tokio::fs::remove_file(fs.get(&ret.id)).await { + log::warn!("Failed to cleanup file: {}", e); + } + return BlossomResponse::error("Mirror request failed, server responses with invalid file content (hash mismatch)"); + } + } // update file data before inserting ret.name = name.map(|s| s.to_string()); @@ -443,9 +463,8 @@ where #[cfg(feature = "payments")] if size == 0 { if let Some(payment_config) = &settings.payments { - let free_quota = payment_config.free_quota_bytes - .unwrap_or(104857600); // Default to 100MB - + let free_quota = payment_config.free_quota_bytes.unwrap_or(104857600); // Default to 100MB + match db.check_user_quota(pubkey, upload.size, free_quota).await { Ok(false) => { // Clean up the uploaded file if quota exceeded diff --git a/ui_src/src/components/mirror-suggestions.tsx b/ui_src/src/components/mirror-suggestions.tsx index 2e81f8c..441de1c 100644 --- a/ui_src/src/components/mirror-suggestions.tsx +++ b/ui_src/src/components/mirror-suggestions.tsx @@ -35,19 +35,20 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) { async function fetchSuggestions() { if (!pub || !login?.pubkey) return; - + if (loading) return; + try { setLoading(true); setError(undefined); - + const fileMap: Map = new Map(); - + // Fetch files from each server for (const serverUrl of servers) { try { const blossom = new Blossom(serverUrl, pub); const files = await blossom.list(login.pubkey); - + for (const file of files) { const suggestion = fileMap.get(file.sha256); if (suggestion) { @@ -68,7 +69,7 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) { // Continue with other servers instead of failing completely } } - + // Determine missing servers for each file for (const suggestion of fileMap.values()) { for (const serverUrl of servers) { @@ -77,12 +78,12 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) { } } } - + // Filter to only files that are missing from at least one server and available on at least one const filteredSuggestions = Array.from(fileMap.values()).filter( s => s.missing_from.length > 0 && s.available_on.length > 0 ); - + setSuggestions(filteredSuggestions); } catch (e) { if (e instanceof Error) { @@ -97,23 +98,23 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) { async function mirrorFile(suggestion: FileMirrorSuggestion, targetServer: string) { if (!pub) return; - + const mirrorKey = `${suggestion.sha256}-${targetServer}`; setMirroring(prev => new Set(prev.add(mirrorKey))); - + try { const blossom = new Blossom(targetServer, pub); await blossom.mirror(suggestion.url); - + // Update suggestions by removing this server from missing_from - setSuggestions(prev => - prev.map(s => - s.sha256 === suggestion.sha256 + setSuggestions(prev => + prev.map(s => + s.sha256 === suggestion.sha256 ? { - ...s, - available_on: [...s.available_on, targetServer], - missing_from: s.missing_from.filter(server => server !== targetServer) - } + ...s, + available_on: [...s.available_on, targetServer], + missing_from: s.missing_from.filter(server => server !== targetServer) + } : s ).filter(s => s.missing_from.length > 0) // Remove suggestions with no missing servers ); @@ -174,14 +175,14 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) {

The following files are missing from some of your servers and can be mirrored:

- +
{suggestions.map((suggestion) => (

- File: {suggestion.sha256.substring(0, 16)}... + File: {suggestion.sha256}

Size: {FormatBytes(suggestion.size)} @@ -189,7 +190,7 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) {

- +

Available on:

@@ -201,14 +202,14 @@ export default function MirrorSuggestions({ servers }: MirrorSuggestionsProps) { ))}
- +

Missing from:

{suggestion.missing_from.map((server) => { const mirrorKey = `${suggestion.sha256}-${server}`; const isMirroring = mirroring.has(mirrorKey); - + return (
diff --git a/ui_src/src/const.ts b/ui_src/src/const.ts index 72823ae..3aa345c 100644 --- a/ui_src/src/const.ts +++ b/ui_src/src/const.ts @@ -43,3 +43,6 @@ export function FormatBytes(b: number, f?: number) { if (b >= kiB) return (b / kiB).toFixed(f) + " KiB"; return b.toFixed(f) + " B"; } + +export const ServerUrl = + import.meta.env.VITE_API_URL || `${location.protocol}//${location.host}`; \ No newline at end of file diff --git a/ui_src/src/hooks/use-blossom-servers.ts b/ui_src/src/hooks/use-blossom-servers.ts index 9b7e97a..3526347 100644 --- a/ui_src/src/hooks/use-blossom-servers.ts +++ b/ui_src/src/hooks/use-blossom-servers.ts @@ -1,26 +1,22 @@ -import { useMemo } from "react"; import useLogin from "./login"; +import { useRequestBuilder } from "@snort/system-react"; +import { EventKind, RequestBuilder } from "@snort/system"; +import { appendDedupe, dedupe, removeUndefined, sanitizeRelayUrl } from "@snort/shared"; +import { ServerUrl } from "../const"; -const DefaultMediaServers = ["https://cdn.satellite.earth/", "https://cdn.self.hosted/"]; +const DefaultMediaServers = ["https://blossom.band/", "https://blossom.primal.net", ServerUrl]; export function useBlossomServers() { const login = useLogin(); - return useMemo(() => { - // For now, just return default servers - // TODO: Implement proper nostr event kind 10063 querying when system supports it - const servers = DefaultMediaServers; + const rb = new RequestBuilder("media-servers"); + if (login?.pubkey) { + rb.withFilter() + .kinds([10_063 as EventKind]) + .authors([login.pubkey]); + } + const req = useRequestBuilder(rb); - return { - servers, - addServer: async (serverUrl: string) => { - // TODO: Implement adding server to event kind 10063 - console.log("Adding server not implemented yet:", serverUrl); - }, - removeServer: async (serverUrl: string) => { - // TODO: Implement removing server from event kind 10063 - console.log("Removing server not implemented yet:", serverUrl); - }, - }; - }, [login?.pubkey]); + const servers = req === undefined ? undefined : dedupe(removeUndefined(req.flatMap((e) => e.tags.filter(t => t[0] === "server").map((t) => sanitizeRelayUrl(t[1]))))); + return appendDedupe(DefaultMediaServers, servers); } \ No newline at end of file diff --git a/ui_src/src/upload/blossom.ts b/ui_src/src/upload/blossom.ts index 9b130d0..e29b6c4 100644 --- a/ui_src/src/upload/blossom.ts +++ b/ui_src/src/upload/blossom.ts @@ -65,7 +65,7 @@ export class Blossom { const rsp = await this.#req( "mirror", "PUT", - "mirror", + "upload", JSON.stringify({ url }), undefined, { diff --git a/ui_src/src/views/upload.tsx b/ui_src/src/views/upload.tsx index b38eecc..c92cbb4 100644 --- a/ui_src/src/views/upload.tsx +++ b/ui_src/src/views/upload.tsx @@ -11,7 +11,7 @@ import useLogin from "../hooks/login"; import usePublisher from "../hooks/publisher"; import { Nip96, Nip96FileList } from "../upload/nip96"; import { AdminSelf, Route96 } from "../upload/admin"; -import { FormatBytes } from "../const"; +import { FormatBytes, ServerUrl } from "../const"; import { UploadProgress } from "../upload/progress"; export default function Upload() { @@ -25,14 +25,11 @@ export default function Upload() { const [isUploading, setIsUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(); - const { servers: blossomServers } = useBlossomServers(); + const blossomServers = useBlossomServers(); const login = useLogin(); const pub = usePublisher(); - const url = - import.meta.env.VITE_API_URL || `${location.protocol}//${location.host}`; - // Check if file should have compression enabled by default const shouldCompress = (file: File) => { return file.type.startsWith('video/') || file.type.startsWith('image/'); @@ -52,7 +49,7 @@ export default function Upload() { setUploadProgress(progress); }; - const uploader = new Blossom(url, pub); + const uploader = new Blossom(ServerUrl, pub); // Use compression by default for video and image files, unless explicitly disabled const useCompression = shouldCompress(file) && !noCompress; const result = useCompression @@ -75,11 +72,11 @@ export default function Upload() { async function handleFileSelection() { if (isUploading) return; - + try { const files = await openFiles(); if (!files || files.length === 0) return; - + // Start uploading each file immediately for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -99,7 +96,7 @@ export default function Upload() { if (!pub) return; try { setError(undefined); - const uploader = new Nip96(url, pub); + const uploader = new Nip96(ServerUrl, pub); await uploader.loadInfo(); const result = await uploader.listFiles(n, 50); setListedFiles(result); @@ -115,14 +112,14 @@ export default function Upload() { } } }, - [pub, url], + [pub], ); async function deleteFile(id: string) { if (!pub) return; try { setError(undefined); - const uploader = new Blossom(url, pub); + const uploader = new Blossom(ServerUrl, pub); await uploader.delete(id); } catch (e) { if (e instanceof Error) { @@ -143,10 +140,10 @@ export default function Upload() { useEffect(() => { if (pub && !self) { - const r96 = new Route96(url, pub); + const r96 = new Route96(ServerUrl, pub); r96.getSelf().then((v) => setSelf(v.data)); } - }, [pub, self, url]); + }, [pub, self]); if (!login) { return ( @@ -189,8 +186,8 @@ export default function Upload() { {/* Upload Progress */} {isUploading && uploadProgress && ( - )} @@ -240,13 +237,12 @@ export default function Upload() {
0.8 + className={`h-2.5 rounded-full transition-all duration-300 ${self.total_size / self.total_available_quota > 0.8 ? "bg-red-500" : self.total_size / self.total_available_quota > 0.6 ? "bg-yellow-500" : "bg-green-500" - }`} + }`} style={{ width: `${Math.min(100, (self.total_size / self.total_available_quota) * 100)}%`, }} @@ -261,13 +257,12 @@ export default function Upload() { % used 0.8 + className={`${self.total_size / self.total_available_quota > 0.8 ? "text-red-400" : self.total_size / self.total_available_quota > 0.6 ? "text-yellow-400" : "text-green-400" - }`} + }`} > {FormatBytes( Math.max( @@ -332,7 +327,7 @@ export default function Upload() { {showPaymentFlow && pub && (
{ console.log("Payment requested:", pr); }} @@ -342,9 +337,9 @@ export default function Upload() { )} {/* Mirror Suggestions */} - {blossomServers.length > 1 && ( - 1 && ( + )}