Last active
September 4, 2026 15:06
-
-
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...
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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