const MASTODON_INSTANCE_URL = Deno.env.get("MASTODON_INSTANCE_URL"); // e.g. https://botsin.space const MASTODON_ACCESS_TOKEN = Deno.env.get("MASTODON_ACCESS_TOKEN"); // you can get this at $INSTANCE_URL/settings/applications // first we need to find out which files we can post, and which ones we already // have posted console.log("Picking file to post…"); const alreadyPostedFiles = new Set(); for await (const dirEntry of Deno.readDir("./posts/.posted")) { if (dirEntry.isSymlink) { alreadyPostedFiles.add(dirEntry.name); } } const availableFiles = []; for await (const dirEntry of Deno.readDir("./posts/")) { if (dirEntry.name === ".gitignore") continue; if (dirEntry.isFile && !alreadyPostedFiles.has(dirEntry.name)) { availableFiles.push(dirEntry.name); } } console.log(`Found ${availableFiles.length} files which are not yet posted`); if (availableFiles.length === 0) { console.log("Nothing left to post at the moment. Bye!"); Deno.exit(); } const fileToPost = availableFiles[Math.floor(Math.random() * availableFiles.length)]; const bytes = await Deno.readFile(`./posts/${fileToPost}`); // we need two requests // 1. upload media // 2. create status with media id // see https://docs.joinmastodon.org/methods/media/#v2 const mediaData = new FormData(); mediaData.append("file", new Blob([bytes])); type MediaResponse = { "id": string; "type": string; "url": string; "preview_url": string; "remote_url": null; "text_url": string; "meta": { "focus": { "x": number; "y": number; }; "original": { "width": number; "height": number; "size": string; "aspect": number; }; "small": { "width": number; "height": number; "size": string; "aspect": number; }; }; "description": string; "blurhash": string; }; console.log("Sending post request to", `${MASTODON_INSTANCE_URL}/api/v2/media`); const mediaRequest = await fetch(`${MASTODON_INSTANCE_URL}/api/v2/media`, { method: "POST", headers: { Authorization: `Bearer ${MASTODON_ACCESS_TOKEN}`, }, body: mediaData, }).then((res) => res.json()) as unknown as MediaResponse; console.log("mediaRequest", mediaRequest); // see https://docs.joinmastodon.org/methods/statuses/#create const statusData = new FormData(); statusData.append("media_ids[]", mediaRequest.id); console.log( "Sending post request to", `${MASTODON_INSTANCE_URL}/api/v1/statuses`, ); const statusRequest = await fetch(`${MASTODON_INSTANCE_URL}/api/v1/statuses`, { method: "POST", headers: { Authorization: `Bearer ${MASTODON_ACCESS_TOKEN}`, }, body: statusData, }).then((res) => res.json()); console.log("statusRequest", statusRequest); console.log("Linking posted file so we can skip it in the future…"); // FIXME: This requires unrestricted `--allow-read` and `--allow-write` at the; // deno should provide a finer-grained permission prompt here await Deno.symlink(`./posts/${fileToPost}`, `./posts/.posted/${fileToPost}`); console.log("Done!");