|
(() => { |
|
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) |
|
}) |
|
})() |