Skip to content

Instantly share code, notes, and snippets.

@postspectacular
Last active September 4, 2026 15:06
Show Gist options
  • Select an option

  • Save postspectacular/599fbf6ebe84ebaf7e2349a1ac8039e4 to your computer and use it in GitHub Desktop.

Select an option

Save postspectacular/599fbf6ebe84ebaf7e2349a1ac8039e4 to your computer and use it in GitHub Desktop.
Configurable batch deletion of Mastodon bookmarks (using Mastodon API). MIT Licensed. Edit the configuration at the top of the file and then run via `node delete-bookmark.js`. The script uses various delays to help with API throttling and generally be a good netizen...
import { writeFile } from "node:fs/promises";
// your account domain
const DOMAIN = "mastodon.social";
// your API access token
const ACCESS_TOKEN = "TODO";
// directory path for storing JSON backups of deleted bookmarks
const BACKUP_DIR = "backup/20260901";
// only remove older bookmarks
const THRESHOLD_DATE = "2026-09-01";
// set to null to start at newest
const MAX_ID = null;
// predefined HTTP headers
const HEADERS = { Authorization: `Bearer ${ACCESS_TOKEN}` };
// don't delete bookmarks from those accounts
// (all accounts must be lowercase)
const KEEP_ACCOUNTS = [
"toxi", // your local account name
"toxi@mastodon.thi.ng", // full account name
// others
// ...
];
// keep posts with these tags
const KEEP_TAGS = [
"art",
"opensource",
"silentsunday",
// ...
];
// https://docs.joinmastodon.org/methods/bookmarks/
// https://docs.joinmastodon.org/api/guidelines/#pagination
let pagination = MAX_ID ? `max_id=${MAX_ID}` : "";
const sleep = (x) =>
new Promise((resolve) => {
console.log("sleep", x);
setTimeout(resolve, x * 1000);
});
const processPost = async (post) => {
if (post.created_at >= THRESHOLD_DATE) return;
if (KEEP_ACCOUNTS.includes(post.account.acct.toLowerCase())) return;
if (post.tags.some((tag) => KEEP_TAGS.includes(tag.name.toLowerCase())))
return;
console.log(
"removing bookmark:",
post.created_at,
post.account.acct,
"\n",
post.url,
"\n",
post.content,
);
await backupPost(post);
for (let i = 1; i <= 5; i++) {
try {
const res = await fetch(
`https://${DOMAIN}/api/v1/statuses/${post.id}/unbookmark`,
{ method: "POST", headers: HEADERS },
);
if (res.ok) return true;
console.log(res);
} catch (e) {
console.warn(e.message);
}
await sleep(i * 5);
}
};
const backupPost = (post) =>
writeFile(
`${BACKUP_DIR}/${post.id}.json`,
JSON.stringify({
date: post.created_at,
user: post.account.acct,
url: post.url,
body: post.content,
tags: post.tags,
}),
);
let total = 0;
while (true) {
console.log("fetching bookmarks", pagination);
const resp = await fetch(
`https://${DOMAIN}/api/v1/bookmarks?limit=40&${pagination}`,
{
headers: HEADERS,
},
);
const posts = await resp.json();
console.log("loaded", posts.length, "bookmarks, ", posts[0].created_at);
let num = 0;
for (let post of posts) {
if (await processPost(post)) {
await sleep(1);
num++;
}
}
total += num;
console.log("removed bookmarks", num, "total", total);
const pageInfo = /max_id=\d+/.exec(resp.headers.get("link"));
if (!pageInfo) break;
pagination = pageInfo[0];
await sleep(num ? 10 : 2);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment