import { assert } from "https://deno.land/std@0.224.0/assert/mod.ts"; import { NSchema, NSecSigner, NRelay1 } from '@nostrify/nostrify'; import { BlossomUploader } from '@nostrify/nostrify/uploaders'; import * as nip19 from 'nostr-tools/nip19' import ngeotags from 'nostr-geotags'; console.log("\nUse at your own risk!\n"); console.log("Considering trying it out with a throwaway nsec."); console.log("All data will be public. Anything you upload is hard to delete.\n"); const path = Deno.env.get("INSTAGRAM_BACKUP_PATH") || prompt("Enter Instagram backup (absolute) path (unzip first): "); const posts = JSON.parse(await Deno.readTextFile(path + "/content/posts_1.json")); console.log("Number of posts:", posts.length); // TODO: sanity check all posts for (const i in posts) { const post = posts[i] } console.log("You can try one picture at a time"); const offset = Number(prompt("Skip how many posts?", 0)); const n = Number(prompt("How many do you want to upload?", posts.length - offset)); if (n == 0) { Deno.exit(0); } assert(n > 0); assert(n <= posts.length - offset); const nsec_str = Deno.env.get("NSEC") || prompt("\nPlease enter your nsec:"); // Sanity check format NSchema.bech32('nsec').parse(nsec_str); // Actually parse nsec const { type, data } = nip19.decode(nsec_str); assert(type === 'nsec'); const signer = new NSecSigner(data); const pubkey_hex = await signer.getPublicKey(); const npub = nip19.npubEncode(pubkey_hex); console.log("\nYour npub: ", npub); console.log("In hex format: ", pubkey_hex); // TODO: get relays from profile and/or allow multiple const relay_str = Deno.env.get("RELAY") || prompt("Pick a relay:"); const relay = new NRelay1(relay_str); console.log("\nAs a sanity check, here's your last message (if any):"); for await (const msg of relay.req([{ kinds: [1], limit: 1, authors: [pubkey_hex] }])) { if (msg[0] === 'EVENT') console.log(msg[2].content); if (msg[0] === 'EOSE') break; } // TODO: suggest public (free or paid) Blossom servers, allow multiple const blossom_url = Deno.env.get("BLOSSOM") || prompt("Enter Blossom server URL:", "https://blossom.primal.net/"); const uploader = new BlossomUploader({ servers: [String(blossom_url)], signer: signer, }); alert("About to upload all images. Not posting to Nostr yet."); let completed = 0; let events = []; for (const i in posts) { if (i < offset) continue; const post = posts[i] console.log(post); // Not all posts have a creation timestamp. If it's absent we'll use // the first media timestamp. let created_at = post.creation_timestamp; // Message is the post title followed by each image on a new line. // If an image has a title, that's added too. let message = ""; // Some posts don't have a "title" field, others have an empty string. if (post.title != undefined && post.title != "") { message = post.title + "\n"; } // Populated from the first media item with coordinates let geotags; let event_tags: string[][] = []; let first = true; for (const j in post.media) { const media = post.media[j]; if (first) { first = false; } else { message += "\n"; } if (created_at == undefined) { created_at = media.creation_timestamp; } if (media.title != undefined && media.title != "") { message += media.title + "\n"; } const data = await Deno.readFile(path + "/" + media.uri); let blob; let extension; // Check file extension if (media.uri.slice(-5) == ".webp") { blob = new Blob([data], {type: 'image/webp'}); extension = ".webp" } else if (media.uri.slice(-4) == ".jpg") { blob = new Blob([data], {type: 'image/jpeg'}); extension = ".jpg" } else if (media.uri.slice(-4) == ".mp4") { blob = new Blob([data], {type: 'video/mp4'}); extension = ".mp4" } else if (!media.uri.includes(".")) { // Assume it's an mp4 movie blob = new Blob([data], {type: 'video/mp4'}); extension = ".mp4" } else { // Unexpected, abort console.error("Unexpected file type for: ", media.uri); console.log(post); Deno.exit(1); } // TODO: if upload fails, log something useful // * e.g. file size (might be over the upload limit) const tags = await uploader.upload(blob); // add URL to message (plus extension) message += tags[0][1] + extension if (geotags == undefined && media.media_metadata && media.media_metadata.photo_metadata) { const options = { geohash: true, // l tag per NIP-52, adds multiple tags at decreasing resolution gps: false, // Avoid multiple tags, insert them manually below. city: false, iso31662: false, iso31663: false }; const exif_data = media.media_metadata.photo_metadata.exif_data; if (exif_data) { for (const k in exif_data) { const exif_item = exif_data[k]; if (exif_item.latitude != undefined && exif_item.latitude != undefined) { // Add coordinates as a tag. // There is currently no NIP defining a wgs84 tag! event_tags.push(['wgs84', String(exif_item.latitude), String(exif_item.longitude)]) // Encode coordinates as Geohash in different resolutions: // https://en.wikipedia.org/wiki/Geohash event_tags = [...event_tags, ...ngeotags({ lat: exif_item.latitude, lon: exif_item.longitude }, options)]; } } } } } // Generate Nostr event let event = { kind: 1, content: message, tags: event_tags, created_at: created_at } const signed_event = await signer.signEvent(event); console.log(signed_event); events.push(signed_event); completed++; if (completed == n) break; } alert("About to post to Nostr."); for (const i in events) { const event = events[i] await relay.event(event); console.log(event.id); } await relay.close() console.log("Done!");