Skip to content

Instantly share code, notes, and snippets.

@chaosharmonic
Last active October 31, 2023 22:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chaosharmonic/8cd5dd0a05602ecc0233d5e4b8fbb6b2 to your computer and use it in GitHub Desktop.
Save chaosharmonic/8cd5dd0a05602ecc0233d5e4b8fbb6b2 to your computer and use it in GitHub Desktop.
Userscript for wiping content from your Reddit account
/*
Shred your reddit history, right in the browser!
To run: open a browser console, paste this code in,
and then run bulkDelete() or overwriteComments()
Replace () with (true) to do a dry run instead of
making any destructive changes.
NOTE:
I did this entirely in a console, and maybe
wouldn't recommend throwing it directly into a
userscript manager like Violentmonkey
That said, when you do this, it won't *run*
any of the functions, just *define* them.
You, the user, can then call them in
the console at will.
(I've done my best to document what the code is
doing as thoroughly as possible, but I also
encourage you to look up anything you don't
understand for yourself.)
WARNING THE FIRST: This only makes destructive changes.
If you need backups, do those first.
WARNING THE SECOND: for obvious reasons,
I *strongly* encourage starting with the dry run.
Wanna know about how I built this? Writeup at:
https://bhmt.dev/blog/scraping
*/
/*
utility functions
these are used by the primary delete/overwrite handlers
and are basically here as shorthand
*/
// delay execution by a specific time frame (measured in seconds)
// used to make sure there's adequate time to load additional contents,
// make changes to page contents, etc.
// if you have a slow connection and have trouble getting this all to work,
// look at the points that this is called and toy around with the time intervals
const sleep = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
// get HTML from an endpoint, parse it as a document...
const fetchHTML = (endpoint, options) => fetch(endpoint, options).then(res => res.text())
const parseHTML = page => new DOMParser().parseFromString(page, 'text/html')
// ...or, both in one operation
const getDoc = (e, o) => fetchHTML(e, o).then(res => parseHTML(res))
// query the page for elements matching a specific CSS selector,
// then pass them into an array. (If you're not familiar w Web dev,
// tl;dr is that this adds additional list functionality that isn't
// available to standard element selection)
const getDOMQueryResults = (selector) => [...document.querySelectorAll(selector)]
// check if an HTML element contains a text string (case insensitive)
const checkTextContents = (el, text) => el.innerText.toLowerCase().includes(text)
// scroll to end of page
// keep going until full history is loaded, or you hit a max number of calls
// this is here as a performance optimization. If you have *lots* of content,
// you might want to refresh and do multiple runs instead of trying it all
// in a single browser process, which could eventually crash
const loadMoreHistory = async () => {
const paginationInterval = 3 * 1000
// this limit is also a guess; tweak it to your own needs
const maxRuns = 100
let isFinishedLoading = false
let currentScrollPosition = 0
let counter = 0
while (!isFinishedLoading && counter !== maxRuns) {
window.scrollTo(0, document.body.scrollHeight)
await sleep(paginationInterval) // wait 3 seconds
isFinishedLoading = currentScrollPosition === window.scrollY
currentScrollPosition = window.scrollY
counter++
}
}
/* the core functionality */
// delete all posts or comments loaded in the current tab
// run this from a /posts or /comments section specifically
// it'll let you cover more of them in one run if you have a *lot*,
// but more importantly the initial menu button selector is different
// so this won't work if you're in on a general /u/username page
const bulkDelete = async (dryRun = false) => {
const waitInterval = 0.5 * 1000
// before doing anything else, load as much history as possible
await loadMoreHistory()
// find button to open menu for each entry
const menus = getDOMQueryResults('[aria-label="more options"]')
for (let menuButton of menus) {
// click various buttons
menuButton.click()
// (after finding the rest of them)
const deleteButton = getDOMQueryResults('button[role="menuitem"]')
.find(e => checkTextContents(e, 'delete'))
deleteButton.click()
const confirmButton = getDOMQueryResults('button')
.find(e => checkTextContents(e, 'delete'))
const cancelButton = getDOMQueryResults('button')
.find(e => checkTextContents(e, 'cancel'))
await sleep(waitInterval).then(() => {
const targetButton = dryRun ? cancelButton : confirmButton
targetButton.click()
})
}
}
// overwrite your comment history, preferably with a message spiting /u/spez
// run this from old reddit. I ran into various issues getting this to work
// on New Reddit, for reasons detailed in the writeup
const overwriteComments = async (dryRun = false) => {
// customize this to create your own 'fuck you' message
// it would still be cool of you to credit my handle/keep this link,
// but I don't ultimately *care* very much, so follow the WTFPL and just...
// Do What the Fuck You Want To
const gistLink = 'https://gist.github.com/chaosharmonic/8cd5dd0a05602ecc0233d5e4b8fbb6b2'
const modPurgeLink = 'https://www.theverge.com/23779477/reddit-protest-blackouts-crushed'
const monetizationLink = 'https://www.theverge.com/2023/4/18/23688463/reddit-developer-api-terms-change-monetization-ai'
const RIPLink = 'https://www.rollingstone.com/culture/culture-news/the-brilliant-life-and-tragic-death-of-aaron-swartz-177191/'
const fediverseLink = 'https://fediverse.party'
const fuckYouSpez = `
This comment has been scrubbed, courtesy of a userscript created by /u/chaosharmonic, a >10yr Redditor making an exodus in the wake of [Reddit's latest fuckening](${modPurgeLink}) (and rolling his own exit path, because even though Shreddit is back up, you'd still ultimately have to pay Reddit for its API usage).
Since this is brazen cash grab to force users onto the first-party client (ads and all), [monetize all of our discussions](${monetizationLink}), here's an unfriendly reminder to the Reddit admins that open information access is a cause one of your founders [actually fucking died over](${RIPLink}).
Pissed about the API shutdown, but don't have an easy way to wipe your interaction with the site because of the API shutdown? [Give this a shot!](${gistLink})
Fuck you, /u/spez.
P.S. See you on the [Fediverse](${fediverseLink})
`.trim().replaceAll(' ', '') // remove extraneous whitespace
// if you'd rather run the delete logic on old reddit too, comment everything above this line,
// the values in the comments, and comment everything above this line
// (note that I haven't tested this on posts)
const scrubComments = async () => {
getDOMQueryResults('a.edit-usertext').forEach(e => e.click())
// 'a[data-event-action="delete"]'
getDOMQueryResults('textarea').forEach(e => e.value = fuckYouSpez)
// this one you'd just comment out
const buttonType = dryRun ? 'cancel' : 'save' // 'no' : 'yes'
const interval = dryRun ? 2500 : 12500
// loop over each submit button, and stagger submissions
// by {interval} seconds each
// don't proceed until all target buttons have been clicked
// offset by 1 to avoid immediately submitting
// the first entry after a new page is fetched
await Promise.allSettled((
getDOMQueryResults(`button.${buttonType}`) // `a.${selection}`
.map(async (e, i) => {
await sleep(interval * i)
e.click()
})
))
}
await scrubComments()
const nextLink = document.querySelector('a[rel="nofollow next"]')
if (nextLink) {
// get next page
const { href } = nextLink
const nextDoc = await getDoc(nextLink)
// replace existing table contents with next page's table contents
const nextResults = nextDoc.querySelector('#siteTable')
document.querySelector('#siteTable').innerHTML = nextResults.innerHTML
// recursively call the whole function again until you're done
await overwriteComments(dryRun)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment