Skip to content

Instantly share code, notes, and snippets.

@toriato
Last active March 31, 2024 21:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toriato/183e05071873ab95bc2ad9f63e1c0f63 to your computer and use it in GitHub Desktop.
Save toriato/183e05071873ab95bc2ad9f63e1c0f63 to your computer and use it in GitHub Desktop.
dcrmrf.js 디시 클리너

dcrmrf.js

image

디시인사이드의 봇 탐지를 최소한으로 하기 위해 빠른 삭제를 지원하지 않습니다.
삭제할 때 대략 1초 정도 필요하니 100,000개 기준으로 약 28시간 정도 걸리는데
갤로그 페이지를 한 번 열고 다음 작업으로 넘어가기 때문에 느린 게 정상입니다.

AntiCaptcha 서비스를 통한 자동 캡챠 풀이를 지원합니다.
캡챠 풀이 1건에 0.002불 (약 3원) 정도로 저렴하니 귀찮다면 해당 서비스도 사용해보세요.
당연히 수동으로도 삭제할 수 있습니다.

NFT 등 게시글 내에서 삭제할 수 있는 개체는 자동으로 넘겨집니다.
작업이 모두 종료되고 남은 게시글은 직접 제거해주세요.

쉬운 사용 방법

  1. 브라우저에 즐겨찾기(북마크)를 새로 만듭니다.
  2. dcrmrf.bookmark.js 코드를 모두 복사한 뒤 주소 입력 란에 붙여넣습니다.
  3. 갤로그 페이지에 접속해 위에서 만든 즐겨찾기 항목을 클릭합니다.

FAQ

  • 2Captcha 웨 안씀?
    가격 비싸고 둘 다 코드짜기 귀찮아서
javascript:(()=>{class t extends Error{}class e extends Error{}class a{static async getUsername(){const t=await fetch("https://gallog.dcinside.com/"),e=(await t.text()).match(/https:\/\/gallog\.dcinside\.com\/([^"]+)/);if(!e)throw Error("%EC%82%AC%EC%9A%A9%EC%9E%90 %EC%95%84%EC%9D%B4%EB%94%94%EB%A5%BC %EC%B0%BE%EC%9D%84 %EC%88%98 %EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4, %EB%A1%9C%EA%B7%B8%EC%9D%B8 %EC%83%81%ED%83%9C%EB%A5%BC %ED%99%95%EC%9D%B8%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94");return e[1]}static async getLogs(t,e,a=1){const s=await fetch(`https://gallog.dcinside.com/${t}/${e}?p=${a}`),r=await s.text(),n=(new DOMParser).parseFromString(r,"text/html");return{ids:[...n.querySelectorAll("[data-no]")].map((t=>parseInt(t.getAttribute("data-no"),10))),count:parseInt(n.querySelector(".gallog_cont .num")?.textContent?.replace(/[^\d]/,""),10)}}static async removeLog(t,a,s=null){const r=new FormData;r.set("no",a),s&&r.set("g-recaptcha-response",s);const n=await fetch(`https://gallog.dcinside.com/${t}/ajax/log_list_ajax/delete`,{method:"POST",headers:{"X-Requested-With":"XMLHttpRequest"},body:r}),o=await n.json();switch(o?.result){case"success":return;case"captcha":throw new e}throw new Error(o?.msg??`%EC%95%8C %EC%88%98 %EC%97%86%EB%8A%94 %EC%98%A4%EB%A5%98%EA%B0%80 %EB%B0%9C%EC%83%9D%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4: ${JSON.stringify(o)}`)}}if(!location.href.startsWith("https://gallog.dcinside.com"))return void alert("%EA%B0%A4%EB%A1%9C%EA%B7%B8 %EB%82%B4%EC%97%90%EC%84%9C %EC%8B%A4%ED%96%89%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94!");let s;for(alert("dcrmrf.js by %EC%95%A0%EC%98%B9%EC%9D%B4%EB%8F%84%EB%91%91\nhttps://gist.github.com/toriato/183e05071873ab95bc2ad9f63e1c0f63");"string"!=typeof s;)switch(s=prompt(["%EC%82%AD%EC%A0%9C%ED%95%A0 %EB%8C%80%EC%83%81%EC%9D%84 %EC%9E%85%EB%A0%A5%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94:","- posting = %EA%B2%8C%EC%8B%9C%EA%B8%80","- comment = %EB%8C%93%EA%B8%80"].join("\n")),s){case"posting":case"comment":break;default:s=null}const r=new class{endpoint="https://api.anti-captcha.com";constructor(t){this.clientKey=t}createSimpleSolver(t,e,a){return()=>new Promise(((s,r)=>{this.createTask(t,e,a).then((async({taskId:t})=>{let e;for(;!e;){await new Promise((t=>setTimeout(t,3e3)));const a=await this.getTaskResult(t);"ready"===a?.status&&(e=a?.solution?.gRecaptchaResponse),console.debug("AntiCaptcha",t,a)}s(e)})).catch(r)}))}async request(t,e={}){"clientKey"in e||(e.clientKey=this.clientKey);const a=await fetch(`${this.endpoint}${t}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),s=await a.json();if("errorId"in s&&s.errorId>0)throw Error(`[AntiCaptcha] ${s.errorId}: ${s?.errorDescription}`);return s}async createTask(t,e,a){return await this.request("/createTask",{task:{type:t,websiteURL:e,websiteKey:a}})}async getTaskResult(t){return await this.request("/getTaskResult",{taskId:t})}async getBalance(){return await this.request("/getBalance")}}(prompt(["AntiCaptcha API %ED%82%A4%EB%A5%BC %EC%9E%85%EB%A0%A5%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94!","https://anti-captcha.com","","%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C %EA%B5%AC%EA%B8%80 %EB%A6%AC%EC%BA%A1%EC%B1%A0 %ED%92%80%EC%9D%B4%EB%A5%BC %EC%9A%94%EA%B5%AC%ED%95%A0 %EB%95%8C %EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C %ED%92%80%EA%B8%B0 %EC%9C%84%ED%95%98%EC%97%AC","AntiCaptcha %EC%9D%98 %EC%9C%A0%EB%A3%8C %EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%80 %ED%95%84%EC%9A%94%ED%95%A9%EB%8B%88%EB%8B%A4","","%EB%B9%84%EC%9B%8C%EB%91%90%EB%A9%B4 %EC%BA%A1%EC%B1%A0 %EB%B0%9C%EC%83%9D%EC%8B%9C %EC%98%A4%EB%A5%98%EB%A5%BC %EB%B0%98%ED%99%98%ED%95%98%EA%B3%A0 %EC%9E%91%EC%97%85%EC%9D%84 %EC%A4%91%EB%8B%A8%ED%95%A9%EB%8B%88%EB%8B%A4"].join("\n"),localStorage.getItem("dcrmrf.antiCaptchaKey")??""));localStorage.setItem("dcrmrf.antiCaptchaKey",r.clientKey),a.getUsername().then((async n=>{let o,c=["%EC%A7%80%EA%B8%88%EB%B6%80%ED%84%B0 %EA%B0%A4%EB%A1%9C%EA%B7%B8 %ED%81%B4%EB%A6%AC%EB%84%88%EB%A5%BC %EC%8B%A4%ED%96%89%ED%95%A9%EB%8B%88%EB%8B%A4!","%EC%A7%84%ED%96%89 %EA%B3%BC%EC%A0%95%EC%9D%80 %EA%B0%9C%EB%B0%9C%EC%9E%90 %EB%8F%84%EA%B5%AC(F12) %EC%86%8D %EC%BD%98%EC%86%94%EC%97%90%EC%84%9C %ED%99%95%EC%9D%B8%ED%95%A0 %EC%88%98 %EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4","%EC%98%A4%EB%A5%98%EA%B0%80 %EC%97%86%EC%9C%BC%EB%A9%B4 %EB%94%B0%EB%A1%9C %EC%95%8C%EB%A6%BC %EB%A9%94%EC%84%B8%EC%A7%80%EA%B0%80 %ED%91%9C%EC%8B%9C%EB%90%98%EC%A7%80 %EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4",`- %EC%8B%9D%EB%B3%84 %EC%BD%94%EB%93%9C: ${n}`,`- %EC%82%AD%EC%A0%9C %EB%8C%80%EC%83%81: ${s}`];if(r.clientKey){const t=await r.getBalance();c.push(`- %EC%BA%A1%EC%B1%A0 %ED%81%AC%EB%A0%88%EB%94%A7: ${t.balance} USD`),o=r.createSimpleSolver("RecaptchaV2TaskProxyless","https://gallog.dcinside.com/","6LcJyr4UAAAAAOy9Q_e9sDWPSHJ_aXus4UnYLfgL")}alert(c.join("\n"));const i=performance.now(),l=[];let h=0,g=1,p=0,d=0,u=!1;for(;++d;)try{const{ids:t,count:e}=await a.getLogs(n,s,g);if(e<1||e===l.length)return p;for(h of t){if(!(h in l))break;h=0}if(!h){console.log(h,"%EB%8B%A4%EC%9D%8C %ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C %EC%9D%B4%EB%8F%99%ED%95%A9%EB%8B%88%EB%8B%A4",{prevPage:g++,nextPage:g});continue}let r;u&&(r=await o(),u=!1),await a.removeLog(n,h,r);const c=(performance.now()-i)/++p,m=Math.round((e-1)*c/1e3/60);console.log(h,`%EC%82%AD%EC%A0%9C%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4, %EB%82%A8%EC%9D%80 %EC%8B%9C%EA%B0%84: %EC%95%BD ${m}%EB%B6%84`),h=0,d=0}catch(a){if(a instanceof t||d>=5)throw a;if(a instanceof e){if(console.log(h,"%EC%BA%A1%EC%B1%A0 %ED%92%80%EC%9D%B4%EA%B0%80 %ED%95%84%EC%9A%94%ED%95%A9%EB%8B%88%EB%8B%A4"),o){u=!0;continue}throw new Error("%EC%BA%A1%EC%B1%A0%EB%A5%BC %EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C %ED%92%80 %EC%88%98 %EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4, %EC%88%98%EB%8F%99%EC%9C%BC%EB%A1%9C %ED%92%80%EC%96%B4%EC%A3%BC%EC%84%B8%EC%9A%94")}a instanceof Error&&a.message.includes("NFT")?(l.push(h),console.log(h,"%EC%9E%90%EB%8F%99%EC%9C%BC%EB%A1%9C %EC%82%AD%EC%A0%9C%ED%95%A0 %EC%88%98 %EC%97%86%EC%8A%B5%EB%8B%88%EB%8B%A4",{reason:a.message})):console.error(h,{error:a,retries:d})}})).then((t=>{alert(`%EC%9E%91%EC%97%85%EC%9D%84 %EB%A7%88%EC%B3%A4%EC%8A%B5%EB%8B%88%EB%8B%A4, %EC%B4%9D ${t}%EA%B0%9C%EB%A5%BC %EC%82%AD%EC%A0%9C%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4`)})).catch((t=>{console.error(t),alert(t)}))})();
(() => {
class AlertError extends Error { }
class CaptchaError extends Error { }
class Utils {
/**
* 현재 로그인된 사용자의 식별 코드를 갤로그로부터 가져옵니다
* @returns {Promise<string>} 현재 로그인된 사용자의 식별 코드
*/
static async getUsername() {
const res = await fetch('https://gallog.dcinside.com/')
const body = await res.text()
const matches = body.match(/https:\/\/gallog\.dcinside\.com\/([^"]+)/)
if (!matches) {
throw Error('사용자 아이디를 찾을 수 없습니다, 로그인 상태를 확인해주세요')
}
return matches[1]
}
/**
* 갤로그 항목을 가져옵니다
* @param {string} username
* @param {string} mode
* @param {number} page
* @returns {Promise<{ ids: number[], count: number }>}
*/
static async getLogs(username, mode, page = 1) {
const res = await fetch(`https://gallog.dcinside.com/${username}/${mode}?p=${page}`)
const body = await res.text()
const doc = new DOMParser().parseFromString(body, 'text/html')
return {
ids: [...doc.querySelectorAll('[data-no]')]
.map(v => parseInt(v.getAttribute('data-no'), 10)),
count: parseInt(
doc.querySelector('.gallog_cont .num')
?.textContent
?.replace(/[^\d]/, ''),
10
)
}
}
/**
* 갤로그 항목을 삭제합니다
* @param {string} username
* @param {number} id
* @param {string} captchaResponse
*/
static async removeLog(username, id, captchaResponse = null) {
const body = new FormData()
body.set('no', id)
if (captchaResponse) {
body.set('g-recaptcha-response', captchaResponse)
}
const res = await fetch(`https://gallog.dcinside.com/${username}/ajax/log_list_ajax/delete`, {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body
})
// 응답 처리하기, 정상 처리시 오류 없이 반환함
const payload = await res.json()
switch (payload?.result) {
case 'success':
return
case 'captcha':
throw new CaptchaError()
}
throw new Error(
payload?.msg ??
`알 수 없는 오류가 발생했습니다: ${JSON.stringify(payload)}`
)
}
}
class AntiCaptcha {
endpoint = 'https://api.anti-captcha.com'
constructor(clientKey) {
this.clientKey = clientKey
}
createSimpleSolver(type, websiteURL, websiteKey) {
return () => new Promise((resolve, reject) => {
this.createTask(type, websiteURL, websiteKey)
.then(
async ({ taskId }) => {
let captchaResponse
while (!captchaResponse) {
await new Promise(r => setTimeout(r, 3000))
const result = await this.getTaskResult(taskId)
if (result?.status === 'ready') {
captchaResponse = result?.solution?.gRecaptchaResponse
}
console.debug('AntiCaptcha', taskId, result)
}
resolve(captchaResponse)
}
)
.catch(reject)
})
}
async request(path, body = {}) {
if (!('clientKey' in body)) {
body.clientKey = this.clientKey
}
const res = await fetch(`${this.endpoint}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
const result = await res.json()
if ('errorId' in result && result.errorId > 0) {
throw Error(`[AntiCaptcha] ${result.errorId}: ${result?.errorDescription}`)
}
return result
}
async createTask(type, websiteURL, websiteKey) {
return await this.request('/createTask', {
task: { type, websiteURL, websiteKey }
})
}
async getTaskResult(taskId) {
return await this.request('/getTaskResult', { taskId })
}
async getBalance() {
return await this.request('/getBalance')
}
}
if (!location.href.startsWith('https://gallog.dcinside.com')) {
alert('갤로그 내에서 실행해주세요!')
return
}
alert(
'dcrmrf.js by 애옹이도둑\n' +
'https://gist.github.com/toriato/183e05071873ab95bc2ad9f63e1c0f63'
)
let mode
while (typeof (mode) !== 'string') {
mode = prompt([
'삭제할 대상을 입력해주세요:',
'- posting = 게시글',
'- comment = 댓글'
].join('\n'))
switch (mode) {
case 'posting':
case 'comment':
break
default:
mode = null
}
}
const antiCaptcha = new AntiCaptcha(
prompt(
[
'AntiCaptcha API 키를 입력해주세요!',
'https://anti-captcha.com',
'',
'서버에서 구글 리캡챠 풀이를 요구할 때 자동으로 풀기 위하여',
'AntiCaptcha 의 유료 서비스가 필요합니다',
'',
'비워두면 캡챠 발생시 오류를 반환하고 작업을 중단합니다'
].join('\n'),
localStorage.getItem('dcrmrf.antiCaptchaKey') ?? ''
)
)
localStorage.setItem('dcrmrf.antiCaptchaKey', antiCaptcha.clientKey)
Utils.getUsername()
.then(async username => {
let captchaSolver
let lines = [
'지금부터 갤로그 클리너를 실행합니다!',
'진행 과정은 개발자 도구(F12) 속 콘솔에서 확인할 수 있습니다',
'오류가 없으면 따로 알림 메세지가 표시되지 않습니다',
`- 식별 코드: ${username}`,
`- 삭제 대상: ${mode}`
]
if (antiCaptcha.clientKey) {
const result = await antiCaptcha.getBalance()
lines.push(`- 캡챠 크레딧: ${result.balance} USD`)
captchaSolver = antiCaptcha.createSimpleSolver(
'RecaptchaV2TaskProxyless',
'https://gallog.dcinside.com/',
// TODO: 갤로그 페이지에서 직접 가져오기
// TODO: https://gallog.dcinside.com/_js/log_list.js?v=210708
'6LcJyr4UAAAAAOy9Q_e9sDWPSHJ_aXus4UnYLfgL'
)
}
alert(lines.join('\n'))
const startTime = performance.now()
const ignoreIds = []
let id = 0
let page = 1
let iters = 0
let retries = 0
let captcha = false
while (++retries) {
try {
const { ids, count } = await Utils.getLogs(username, mode, page)
// 더 이상 처리할 개체가 없다면 반환하기
if (count < 1 || count === ignoreIds.length) {
return iters
}
// 자동으로 삭제할 수 없는 개체는 건너뛰기
for (id of ids) {
if (!(id in ignoreIds)) break
id = 0
}
// 자동으로 삭제할 수 없는 개체가 전부라면 다음 페이지로 넘어가기
if (!id) {
console.log(id, '다음 페이지로 이동합니다', {
prevPage: page++,
nextPage: page
})
continue
}
// 캡챠 풀이가 필요한 상태라면
let captchaResponse
if (captcha) {
captchaResponse = await captchaSolver()
captcha = false
}
await Utils.removeLog(username, id, captchaResponse)
const avgIterMs = (performance.now() - startTime) / ++iters
const estIterMinute = Math.round((count - 1) * avgIterMs / 1000 / 60)
console.log(id, `삭제했습니다, 남은 시간: 약 ${estIterMinute}분`)
id = 0
retries = 0
} catch (error) {
// 수동 작업이 필요한 작업이거나
// 재시도 가능 횟수를 초과했다면 작업 중단하기
if (error instanceof AlertError || retries === 5) {
throw error
}
// 캡챠 풀이가 필요한 상태
else if (error instanceof CaptchaError) {
console.log(id, '캡챠 풀이가 필요합니다')
if (captchaSolver) {
captcha = true
continue
}
throw new Error('캡챠를 자동으로 풀 수 없습니다, 수동으로 풀어주세요')
}
// 수동 작업이 무조건 필요한 개체라면 뛰어넘기
else if (error instanceof Error && (
error.message.includes('NFT')
)) {
ignoreIds.push(id)
console.log(id, '자동으로 삭제할 수 없습니다', {
reason: error.message
})
} else {
console.error(id, { error, retries })
}
}
}
})
.then(iters => {
alert(`작업을 마쳤습니다, 총 ${iters}개를 삭제했습니다`)
})
.catch(err => {
console.error(err)
alert(err)
})
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment