hzrd149 / blossom-client-sdk

A simple client for managing blobs on blossom servers
https://hzrd149.github.io/blossom-client-sdk/
MIT License
6 stars 2 forks source link

🌸 blossom-client-sdk

A client for manage blobs on blossom servers

Documentation

Using the client

Using the static methods

import { BlossomClient } from "blossom-client-sdk/client";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

// create an upload auth event
const uploadAuth = await BlossomClient.getUploadAuth(file, server, "Upload bitcoin.pdf");

// encode it using base64
const encodedAuthHeader = BlossomClient.encodeAuthorizationHeader(auth);

// manually make the request
const res = await fetch(new URL("/upload", server), {
  method: "PUT",
  body: file,
  headers: { authorization: encodedAuthHeader },
});

// or use the static method
const res = await BlossomClient.uploadBlob(server, file, uploadAuth);

// check if successful
if (res.ok) {
  console.log("Blob uploaded!");
}

Using the class

The BlossomClient class can be used to talk to a single server

import { BlossomClient } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const client = new BlossomClient("https://cdn.example.com", signer);

const pubkey = "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5";
const blobs = await client.listBlobs(pubkey, undefined, true);
// passing true as the last argument will make it send an auth event with the list request

Using with NDK

The BlossomClient class and methods optionally take a signer method that is used to sign the upload auth events

If your using NDK in your app you can use this method

const signer = async (draft: EventTemplate) => {
  // add the pubkey to the draft event
  const event: UnsignedEvent = { ...draft, pubkey: user.pubkey };
  // get the signature
  const sig = await ndk.signer!.sign(event);

  // return the event + id + sig
  return { ...event, sig, id: getEventHash(event) };
};

Helper Methods

Getting the hash from a URL

The getHashFromURL method will return the last SHA256 hash it finds in a URL

import { getHashFromURL } from "blossom-client-sdk";

// blossom compatible URLs
console.log(
  getHashFromURL("https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"),
);
// -> b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553

// non-blossom URLs
console.log(
  getHashFromURL(
    "https://cdn.example.com/266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5/media/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf",
  ),
);
// -> b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553

// returns null when no hash is found
console.log(getHashFromURL("https://example.com/index.html"));
// -> null

Handling broken images

This package also exports a few helper methods for handling broken images

The handleImageFallbacks(image, getServers) method listen for an error event on an <img/> element and if the element has a data-pubkey attribute. it will call getServers to ask for a list of blossom servers for the pubkey

import { handleImageFallbacks, USER_BLOSSOM_SERVER_LIST_KIND, getServersFromServerListEvent } from "blossom-client-sdk";

const image = new Image();
image.src = "https://cdn.censorship.com/72cb99b689b4cfe1a9fb6937f779f3f9c65094bf0e6ac72a8f8261efa96653f5.png";

// set the pubkey from the kind 1 event this image was found it
image.dataset.pubkey = event.pubkey;

// this is called when
async function getServers(pubkey) {
  if (pubkey) {
    // use NDK to find the users blossom server list event (k:10063)
    const event = await ndk.fetchEvent({ kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });

    // if its found return a list of blossom servers
    if (event) return getServersFromServerListEvent(event);
  }
  return undefined;
}

// listen for "error" events
handleImageFallbacks(image, getServers);

document.body.appendChild(image);

Other Examples

List all blobs on a server

import { BlossomClient } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const pubkey = "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5";
const server = "https://cdn.example.com";

async function listBlobs() {
  try {
    return BlossomClient.listBlobs(server, pubkey);
  } catch (e) {
    if (e.status === 401) {
      const auth = await BlossomClient.getListAuth(signer, "List Blobs from " + server);
      return BlossomClient.listBlobs(server, pubkey, undefined, auth);
    }
  }
}

Upload a single blob

import { BlossomClient } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const client = new BlossomClient("https://cdn.example.com", signer);

const blobs = await client.listBlobs();

await client.uploadBlob(new File(["testing"], "test.txt"));

Upload a single blob to multiple servers

import { BlossomClient } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const servers = ["https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

const auth = await BlossomClient.getUploadAuth(file, signer, "Upload test.txt");

for (let server of servers) {
  await BlossomClient.uploadBlob(server, file, auth);
}

Uploading and mirroring to multiple servers

The multiServerUpload method can be used to upload a single blob to multiple servers

Example of uploading to each server one at time

import { multiServerUpload } from "blossom-server-sdk";

async function signer(event: any) {
  // @ts-expect-error
  return await window.nostr.signEvent(event);
}

const servers = ["https://cdn.server-a.com", "https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

// create async generator for upload
const upload = multiServerUpload(servers, file, signer);

let successful = 0;

while (true) {
  let result: Awaited<ReturnType<typeof upload.next>> | undefined = undefined;

  // attempt to upload
  try {
    result = await upload.next();
    successful++;
  } catch (error) {
    if (error instanceof Error) console.log(`Failed to upload ${error.message}`);
  }

  if (result) {
    // if upload was successful log progress
    if (!result.done) {
      console.log(`Uploaded to ${result.value.server} ${result.value.progress * 100}%`);
    } else if (result.done && result.value) {
      console.log(`Successfully uploaded ${result.value.sha256} to ${servers} servers`);

      // exit while loop (important)
      break;
    } else {
      throw Error("Failed to upload blob to any servers");
    }
  }
}

Upload and Mirror manually

import { BlossomClient } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const mainServer = "https://cdn.server-a.com";
const mirrorServers = ["https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

const auth = await BlossomClient.getUploadAuth(file, signer, "Upload test.txt");

// first upload blob to main server
const blob = await BlossomClient.uploadBlob(mainServer, file, auth);

// then tell mirror servers to download it
for (let server of mirrorServers) {
  // reuse the same auth for mirroring
  await BlossomClient.mirrorBlob(server, blob.url, auth);
}