diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/README.md b/README.md index 37b521b..2f439c6 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ Run the script: deno run --allow-read --allow-net main.ts ``` +Optionally add `--env --allow-env` to use a `.env` file. + First it will prompt for the path of your archive. E.g. `/Users/you/Downloads/instagram-you-2024-01-01-abcdef` diff --git a/deno.lock b/deno.lock index 2c03a70..3bc1a55 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,9 @@ "specifiers": { "jsr:@nostrify/nostrify@^0.30.1": "jsr:@nostrify/nostrify@0.30.1", "jsr:@nostrify/types@^0.30.0": "jsr:@nostrify/types@0.30.0", + "jsr:@std/assert@^0.224.0": "jsr:@std/assert@0.224.0", + "jsr:@std/crypto@^0.224.0": "jsr:@std/crypto@0.224.0", + "jsr:@std/encoding@^0.224.0": "jsr:@std/encoding@0.224.3", "jsr:@std/encoding@^0.224.1": "jsr:@std/encoding@0.224.3", "npm:@noble/hashes@^1.4.0": "npm:@noble/hashes@1.4.0", "npm:@scure/bip32@^1.4.0": "npm:@scure/bip32@1.4.0", @@ -19,6 +22,7 @@ "integrity": "fcc923707e87a9fbecc82dbb18756d1d3d134cd0763f4b1254c4bce709e811eb", "dependencies": [ "jsr:@nostrify/types@^0.30.0", + "jsr:@std/crypto@^0.224.0", "jsr:@std/encoding@^0.224.1", "npm:@scure/bip32@^1.4.0", "npm:@scure/bip39@^1.3.0", @@ -31,6 +35,16 @@ "@nostrify/types@0.30.0": { "integrity": "1f38fa849cff930bd709edbf94ef9ac02f46afb8b851f86c8736517b354616da" }, + "@std/assert@0.224.0": { + "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" + }, + "@std/crypto@0.224.0": { + "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", + "dependencies": [ + "jsr:@std/assert@^0.224.0", + "jsr:@std/encoding@^0.224.0" + ] + }, "@std/encoding@0.224.3": { "integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" } diff --git a/main.ts b/main.ts index 6718c63..67d4e3a 100644 --- a/main.ts +++ b/main.ts @@ -6,17 +6,24 @@ import { NRelay1 } from '@nostrify/nostrify'; +import { BlossomUploader } from '@nostrify/nostrify/uploaders'; + import * as nip19 from 'nostr-tools/nip19' 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 = prompt("Enter Instagram backup (absolute) path (unzip first): "); +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, duplicates are skipped the next run."); const n = Number(prompt("How many do you want to upload?", posts.length)); @@ -26,7 +33,7 @@ if (n == 0) { Deno.exit(0); } assert(n > 0); assert(n <= posts.length); -const nsec_str = prompt("\nPlease enter your nsec:"); +const nsec_str = Deno.env.get("NSEC") || prompt("\nPlease enter your nsec:"); // Sanity check format NSchema.bech32('nsec').parse(nsec_str); @@ -42,7 +49,7 @@ console.log("\nYour npub: ", npub); console.log("In hex format: ", pubkey_hex); // TODO: get relays from profile and/or allow multiple -const relay_str = prompt("Pick a relay:"); +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):"); @@ -53,3 +60,94 @@ for await (const msg of relay.req([{ kinds: [1], limit: 1, authors: [pubkey_hex] } await relay.close() + +// 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) { + 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"; + } + + 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.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); + } + const tags = await uploader.upload(blob); + + if (extension == ".mp4") { + console.log(tags); + } + + // add URL to message (plus extension) + message += tags[0][1] + extension + + // TODO: get city? + + } + + // Generate Nostr event + const event = await signer.signEvent({ kind: 1, content: message, tags: [], created_at: created_at}); + console.log(event); + events.push(event); + + completed++; + if (completed == n) break; +} + +alert("About to post to Nostr."); + +for (const event in events) { + await relay.event(event); +} + +console.log("Done!");