import * as path from 'https://deno.land/std@0.102.0/path/mod.ts'; 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 if ( MASTODON_INSTANCE_URL == null || MASTODON_INSTANCE_URL === "" || MASTODON_ACCESS_TOKEN == null || MASTODON_ACCESS_TOKEN === "" ) { console.error("Please set MASTODON_INSTANE_URL and MASTODON_ACCESS_TOKEN!"); Deno.exit(1); } // some helpers to work with the mastodon api const handleResponse = async (res: Response) => { if (res.ok) return { response: res, data: await res.json() }; throw new Error(await res.text()); }; const api = ( path: string, params: { body?: FormData | string; method?: string }, ) => fetch( `${MASTODON_INSTANCE_URL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`, { method: "POST", headers: { Authorization: `Bearer ${MASTODON_ACCESS_TOKEN}`, }, ...params, }, ).then(handleResponse); // first we need to find out which files we can post, and which ones we already // have posted const scriptDir = path.dirname(path.fromFileUrl(Deno.mainModule)) const imgDir = path.resolve(scriptDir, Deno.args[0] || 'posts') console.log("Picking file to post…"); const alreadyPostedFiles = new Set(); for await (const dirEntry of Deno.readDir(`${imgDir}/.posted`)) { if (dirEntry.isSymlink) { alreadyPostedFiles.add(dirEntry.name); } } const availableFiles = []; for await (const dirEntry of Deno.readDir(imgDir)) { 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(`${imgDir}/${fileToPost}`); console.log(`Will post file ${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])); console.log("Sending post request to", `${MASTODON_INSTANCE_URL}/api/v2/media`); const { data: mediaRequest } = await api("/api/v2/media", { body: mediaData, }) as unknown as { data: { id: string } }; console.log("mediaRequest", mediaRequest); // now we have to wait until the media file is processed const sleep = (milliseconds: number) => new Promise((resolve, reject) => { setTimeout(resolve, milliseconds); }); const startTime = Date.now(); while (true) { if ((Date.now() - startTime) / 1000 >= 60) { console.error("Timeout waiting for media to be processed."); Deno.exit(1); } const { response } = await api(`/api/v1/media/${mediaRequest.id}`, { method: "GET", }); if (response.status === 200) { console.log("Media finished processing!"); break; } else { console.log("Media still processing…"); await sleep(5000); } } // 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 { data: statusRequest } = await api("/api/v1/statuses", { body: statusData, }); 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`; // deno should provide a finer-grained permission prompt here // see https://github.com/denoland/deno/issues/9607 await Deno.symlink(`../${fileToPost}`, `${imgDir}/.posted/${fileToPost}`); console.log("Done!");