BUD-03 implementation (WIP)

This commit is contained in:
2025-04-19 23:20:08 +02:00
parent ab80abc209
commit 94a92af606
2 changed files with 179 additions and 6 deletions

76
.github/prompts/blossom-bud03.prompt.md vendored Normal file
View File

@@ -0,0 +1,76 @@
# BUD-03
## User Server List
`draft` `optional`
Defines a replaceable event using `kind:10063` to advertise the blossom servers a user uses to host their blobs.
The event MUST include at least one `server` tag containing the full server URL including the `http://` or `https://`.
The order of these tags is important and should be arranged with the users most "reliable" or "trusted" servers being first.
The `.content` field is not used.
```json
{
"id": "e4bee088334cb5d38cff1616e964369c37b6081be997962ab289d6c671975d71",
"pubkey": "781208004e09102d7da3b7345e64fd193cd1bc3fce8fdae6008d77f9cabcd036",
"content": "",
"kind": 10063,
"created_at": 1708774162,
"tags": [
["server", "https://cdn.self.hosted"],
["server", "https://cdn.satellite.earth"]
],
"sig": "cc5efa74f59e80622c77cacf4dd62076bcb7581b45e9acff471e7963a1f4d8b3406adab5ee1ac9673487480e57d20e523428e60ffcc7e7a904ac882cfccfc653"
}
```
## Client Upload Implementation
When uploading blobs clients MUST attempt to upload the blob to at least the first `server` listed in the users server list.
Optionally clients MAY upload the blob to all the servers or mirror the blob to the other servers if they support [BUD-04](./04.md)
This ensures that the blob is available in multiple locations in the case one of the servers goes offline.
## Client Retrieval Implementation
When extracting the SHA256 hash from the URL clients MUST use the last occurrence of a 64 char hex string. This allows clients to extract hashes from blossom URLs and SOME non-blossom URLs.
In all the following examples, the hash `b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553` should be selected
- Blossom URLs
- `https://blossom.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- `https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553`
- Non Blossom URLs
- `https://cdn.example.com/user/ec4425ff5e9446080d2f70440188e3ca5d6da8713db7bdeef73d0ed54d9093f0/media/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- `https://cdn.example.com/media/user-name/documents/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
- `http://download.example.com/downloads/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553`
- `http://media.example.com/documents/b1/67/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf`
In the context of nostr events, clients SHOULD use the author's server list when looking for blobs that are no longer available at the original URL.
Take the following event as an example
```json
{
"id": "834185269f4ab72539193105060dbb1c8b2efd702d14481cea345c47beefe6eb",
"pubkey": "ec4425ff5e9446080d2f70440188e3ca5d6da8713db7bdeef73d0ed54d9093f0",
"content": "I've developed a new open source P2P e-cash system called Bitcoin. check it out\nhttps://cdn.broken-domain.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf",
"kind": 1,
"created_at": 1297484820,
"tags": [],
"sig": "bd4bb200bdd5f7ffe5dbc3e539052e27b05d6f9f528e255b1bc4261cc16b8f2ad85c89eef990c5f2eee756ef71b4c571ecf6a88ad12f7338e321dd60c6a903b5"
}
```
Once the client discovers that the URL `https://cdn.broken-domain.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf` is no longer available. It can perform the following steps to find the blob:
1. Get the SHA256 has from the URL
2. Look for the authors server list `kind:10063`
3. If found, Attempt to retrieve the blob from each `server` listed started with the first
4. If not found, the client MAY fallback to using a well-known popular blossom server to retrieve the blob
This ensures clients can quickly find missing blobs using the users list of trusted servers.

View File

@@ -22,6 +22,7 @@ import { signEvent } from "@/utils/utils"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { toast } from "./ui/use-toast"
async function calculateBlurhash(file: File): Promise<string> {
return new Promise((resolve, reject) => {
@@ -58,6 +59,77 @@ const UploadComponent: React.FC = () => {
const [shouldFetch, setShouldFetch] = useState(false)
const [serverChoice, setServerChoice] = useState("blossom.band")
const [enableNip89, setEnableNip89] = useState(false)
// New state for user Blossom servers
const [userBlossomServers, setUserBlossomServers] = useState<string[]>([])
const [loadingBlossomServers, setLoadingBlossomServers] = useState(false)
// Get the user's pubkey
const userPubkey = typeof window !== "undefined" ? window.localStorage.getItem("pubkey") || "" : ""
// Fetch user's Blossom servers from kind:10063 events
const { events: blossomServerEvents } = useNostrEvents({
filter: {
authors: userPubkey ? [userPubkey] : [],
kinds: [10063],
limit: 1,
},
enabled: !!userPubkey,
})
// Function to load user's Blossom servers
const loadUserBlossomServers = useCallback(() => {
setLoadingBlossomServers(true)
if (blossomServerEvents.length > 0) {
// Extract server URLs from server tags
const servers = blossomServerEvents[0].tags
.filter(tag => tag[0] === 'server')
.map(tag => {
// Extract hostname from URL
try {
const url = new URL(tag[1])
return url.hostname
} catch (e) {
console.error("Invalid server URL:", tag[1])
return null
}
})
.filter(Boolean) as string[]
setUserBlossomServers(servers)
// Set the first server as the selected server if available
if (servers.length > 0) {
setServerChoice(servers[0])
toast({
title: "Blossom Servers Loaded",
description: `Loaded ${servers.length} Blossom server(s) from your kind:10063 event.`,
})
} else {
toast({
title: "No Blossom Servers Found",
description: "Your kind:10063 event doesn't contain any valid server tags.",
variant: "destructive"
})
}
} else {
toast({
title: "No Blossom Servers Found",
description: "You don't have a kind:10063 event configured with Blossom servers.",
variant: "destructive"
})
}
setLoadingBlossomServers(false)
}, [blossomServerEvents, setServerChoice])
// Effect to detect when Blossom server events are loaded
useEffect(() => {
if (blossomServerEvents.length > 0) {
console.log("Found kind:10063 Blossom server event:", blossomServerEvents[0])
}
}, [blossomServerEvents])
const { events, isLoading: isNoteLoading } = useNostrEvents({
filter: shouldFetch
@@ -300,19 +372,44 @@ const UploadComponent: React.FC = () => {
<div className="grid w-full max-w-sm items-center gap-1.5">
<Input id="file" name="file" type="file" accept="image/*" onChange={handleFileChange} />
</div>
<div className="grid grid-cols-2 w-full max-w-sm items-center gap-1.5">
{/* <select value={serverChoice} onChange={handleServerChange} className="w-full">
<option value="nostr.download">nostr.download</option>
<option value="blossom.primal.net">blossom.primal.net</option>
</select> */}
Upload to
<div className="grid w-full max-w-sm items-center gap-1.5">
<div className="flex items-center justify-between space-x-2">
<Button
type="button"
variant="outline"
onClick={loadUserBlossomServers}
disabled={loadingBlossomServers || !userPubkey}
className="text-xs"
>
{loadingBlossomServers ? (
<>Loading Servers <ReloadIcon className="ml-2 h-3 w-3 animate-spin" /></>
) : (
"Load My Blossom Servers"
)}
</Button>
<Label>Upload to:</Label>
</div>
<Select onValueChange={handleServerChange} value={serverChoice}>
<SelectTrigger className="w-full">
<SelectValue placeholder={serverChoice} />
</SelectTrigger>
<SelectContent>
{/* Default servers */}
<SelectItem value="blossom.band">blossom.band</SelectItem>
<SelectItem value="media.lumina.rocks">media.lumina.rocks</SelectItem>
{/* User's Blossom servers */}
{userBlossomServers.length > 0 && (
<>
{userBlossomServers.map((server) => (
<SelectItem key={server} value={server}>
{server} (Your Server)
</SelectItem>
))}
</>
)}
</SelectContent>
</Select>
</div>