Oddbean new post about | logout
 ```
import { NSecSigner, NRelay1, NSchema as n } from '@nostrify/nostrify';
import { BlossomUploader } from '@nostrify/nostrify/uploaders';
import * as nip19 from 'nostr-tools/nip19';

// Helper function to convert a hex string to Uint8Array
function hexToUint8Array(hex: string): Uint8Array {
  if (hex.length % 2 !== 0) {
    throw new Error("Hex string must have an even length");
  }
  const array = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    array[i / 2] = parseInt(hex.substr(i, 2), 16);
  }
  return array;
}

// Function to read uploaded files from JSON
async function readUploadedFiles(uploadedFilesPath: string): Promise<Set<string>> {
  try {
    const data = await Deno.readTextFile(uploadedFilesPath);
    return new Set(JSON.parse(data));
  } catch {
    return new Set();
  }
}

// Function to write uploaded files to JSON
async function writeUploadedFiles(uploadedFiles: Set<string>, uploadedFilesPath: string) {
  await Deno.writeTextFile(uploadedFilesPath, JSON.stringify(Array.from(uploadedFiles)));
}

// Function to append a URL to a text file
async function appendUrlToFile(fileUrl: string, urlFilePath: string) {
  try {
    await Deno.writeTextFile(urlFilePath, fileUrl + '\n', { append: true });
    console.log(`Appended URL to file: ${fileUrl}`);
  } catch (error) {
    console.error('Error appending URL to file:', error);
  }
}

// Function to sign, parse, and upload a media file
async function signAndUploadMedia(
  filePath: string,
  uploadedFiles: Set<string>,
  urlFilePath: string,
  signer: NSecSigner,
  relays: { relay: NRelay1; url: string }[]
) {
  try {
    // Check if the file has already been uploaded
    if (uploadedFiles.has(filePath)) {
      console.log(`File ${filePath} has already been uploaded. Skipping.`);
      return;
    }

    // Get the public key from the signer
    const pubkey = await signer.getPublicKey();

    // Initialize the uploader
    const uploader = new BlossomUploader({
      servers: [
        'https://cdn.satellite.earth',
        'https://nstore.nostrver.se',
        'https://blossom.puhcho.me',
        'https://blossom.primal.net',
        'https://cdn.nostrcheck.me'
      ],
      signer: signer,
    });

    // Read the file
    const fileBuffer = await Deno.readFile(filePath);
    const file = new File([fileBuffer], filePath.split('/').pop()!);

    // Upload the file and get the tags
    const tags = await uploader.upload(file);

    // Find the URL in the tags
    let fileUrl = 'Unknown URL';
    for (const tag of tags) {
      if (tag[0] === "url" && tag[1]) {
        fileUrl = tag[1];
        break;
      }
    }

    // Append the URL to the text file
    await appendUrlToFile(fileUrl, urlFilePath);

    // Create event data
    const eventData = {
      kind: 1,
      content: `${fileUrl}`,
      tags: tags,
      created_at: Math.floor(Date.now() / 1000),
    };

    // Sign the event to get id and sig
    const signedEvent = await signer.signEvent(eventData);
    const completeEventData = {
      ...eventData,
      id: signedEvent.id,
      pubkey: pubkey,
      sig: signedEvent.sig,
    };

    // Parse and validate the complete event data using NSchema
    const event = n.event().parse(completeEventData);
    console.log('Parsed and validated event:', event);

    // Send the event to each relay
    for (const { relay, url } of relays) {
      try {
        console.log(`Sending event to relay ${url}`);
        await relay.event(event);
        console.log(`Event sent successfully to ${url}`);
      } catch (error) {
        console.error(`Error sending event to relay ${url}:`, error);
      } finally {
        try {
          await relay.close();
          console.log(`Relay ${url} closed`);
        } catch (closeError) {
          console.error(`Error closing relay ${url}:`, closeError);
        }
      }
    }

    // Add the file to the uploaded files set and update the JSON file
    uploadedFiles.add(filePath);
    await writeUploadedFiles(uploadedFiles, './uploaded_files.json');

    console.log("Done!");
  } catch (error) {
    console.error('Error signing and uploading media:', error);
  }
}

// Function to select a random valid file from a folder
async function getRandomValidFileFromFolder(folderPath: string, uploadedFiles: Set<string>): Promise<string | null> {
  const validExtensions = ['jpg', 'mp4', 'webp', 'gif'];
  const files: string[] = [];

  for await (const dirEntry of Deno.readDir(folderPath)) {
    if (dirEntry.isFile) {
      const extension = dirEntry.name.split('.').pop()?.toLowerCase();
      if (extension && validExtensions.includes(extension)) {
        files.push(dirEntry.name);
      }
    }
  }

  // Filter out files that have already been uploaded
  const unuploadedFiles = files.filter(file => !uploadedFiles.has(`${folderPath}/${file}`));

  if (unuploadedFiles.length === 0) {
    console.log('All files have been uploaded. Selecting a random URL to publish.');
    return null;
  }

  const randomIndex = Math.floor(Math.random() * unuploadedFiles.length);
  return `${folderPath}/${unuploadedFiles[randomIndex]}`;
}

// Function to publish a Nostr event with a random URL
async function publishRandomUrlEvent(urlFilePath: string, signer: NSecSigner, relays: { relay: NRelay1; url: string }[]) {
  try {
    const urls = (await Deno.readTextFile(urlFilePath)).trim().split('\n');

        if (urls.length === 0) {
          console.error('No URLs found in the URL file.');
          return;
        }

        const randomUrl = urls[Math.floor(Math.random() * urls.length)];

        // Create event data
        const eventData = {
          kind: 1,
          content: `${randomUrl}`,
          tags: [],
          created_at: Math.floor(Date.now() / 1000),
        };

        // Sign the event to get id and sig
        const signedEvent = await signer.signEvent(eventData);
        const completeEventData = {
          ...eventData,
          id: signedEvent.id,
          pubkey: await signer.getPublicKey(),
          sig: signedEvent.sig,
        };

        // Parse and validate the complete event data using NSchema
        const event = n.event().parse(completeEventData);
        console.log('Parsed and validated event:', event);

        for (const { relay, url } of relays) {
          try {
                console.log(`Sending event to relay ${url}`);
                await relay.event(event);
                console.log(`Event sent successfully to ${url}`);
          } catch (error) {
                console.error(`Error sending event to relay ${url}:`, error);
          } finally {
                try {
                  await relay.close();
                  console.log(`Relay ${url} closed`);
                } catch (closeError) {
                  console.error(`Error closing relay ${url}:`, closeError);
                }
          }
        }

        console.log("Published random URL event successfully!");

   } catch (error) {

           console.error('Error publishing random URL event:', error);

   }

}

// Main function to execute the script
async function main() {

        const hexSecretKey = Deno.env.get('SECRET_KEY_HEX');
        if (!hexSecretKey) {

           console.error('Environment variable "SECRET_KEY_HEX" is not set.');
           Deno.exit(1);

           return;

   }

   const secretKey: Uint8Array = hexToUint8Array(hexSecretKey);

   // Initialize the signer with your secret key

   const signer = new NSecSigner(secretKey);

   // Define the relay URLs

   const relayUrls = [

           'wss://nostr.mom',
           'wss://nos.lol',
           'wss://relay.primal.net',
           'wss://e.nos.lol',
           'wss://relay.damus.io',
           'wss://nostr.lu.ke',
           'wss://nostr.oxtr.dev',
           'wss://relay.nostrcheck.me',
           'wss://nostr.data.haus',
           'wss://ditto.puhcho.me/relay',
           'wss://offchain.pub',
           'wss://strfry.iris.to'

   ];

   // Create an array of NRelay1 instances with their URLs

   const relays = relayUrls.map(url => ({ relay: new NRelay1(url), url }));

   // Path to the JSON file that stores uploaded files

   const uploadedFilesPath = './home/user/test_bloom/uploaded_files.json';

   // Path to the text file that stores uploaded URLs

   const urlFilePath = './home/user/test_bloom/uploaded_urls.txt';

   // Example usage

   const folderPath = Deno.env.get('MEDIA_FOLDER_PATH');

   if (folderPath) {

                   try {

                          const uploadedFiles = await readUploadedFiles(uploadedFilesPath);

                          const randomFilePath = await getRandomValidFileFromFolder(folderPath, uploadedFiles);

                          if (randomFilePath) {

                                        await signAndUploadMedia(randomFilePath, uploadedFiles, urlFilePath, signer, relays);

                          } else {

                                        await publishRandomUrlEvent(urlFilePath, signer, relays);

                          }

          } catch (error) {

                          console.error('Error during execution:', error);

          } finally {

                          Deno.exit();

          }

   } else {

                   console.error('Environment variable "MEDIA_FOLDER_PATH" is not set.');

                   Deno.exit(1);

   }

}

// Execute main function

main();

```

Better as it was uploading to just one blossom.