Skip to content

Instantly share code, notes, and snippets.

@ltlapy
Last active September 5, 2025 00:30
Show Gist options
  • Save ltlapy/483f41fdb691c8bc6e3ca1451ae0ff21 to your computer and use it in GitHub Desktop.
Save ltlapy/483f41fdb691c8bc6e3ca1451ae0ff21 to your computer and use it in GitHub Desktop.
노트 청소기
// @ 0.19.0
// 노트 청소기: 일정 시간에 오래된/새로운 노트부터 하나씩 삭제합니다
// 같은 계정에서 노트 청소기가 동작 중일때에는 절대로 다른 창/기기에서 동시에 실행하지 마세요
// (새로고침하기 전까지는 진행되지 않은 것처럼 보이더라도 청소기가 계속해서 동작하는 상태입니다)
// v 0.5.5 노트를 가져오는 과정에서 문제가 발생했을 때의 처리가 올바르지 않은 문제 해결
// v 0.5.4 REMOVE_FROM_NEW 관련 안내 오작동 해결
// v 0.5.3 REMOVE_FROM_NEW 관련 안내 추가
// v 0.5.2 구문 오류 해결
// v 0.5.1 구문 오류 해결
// v 0.4 노트 조회 시 발생하는 Rate limit를 무시하지 못하는 문제 해결. 알림 기능 추가
// v 0.3 REMOVE_PINNED가 반대로 작동하는 문제 해결.
// RENOTE_THRESHOLD/REACTION_THRESHOLD 판정 변경.
// 오류를 무시하고 계속하는 기능 추가
// v 0.2 노트 목록 새로고침할 때, 작업을 완료할 때에 시각을 같이 보이기
// v 0.1 초기 릴리즈
// ---- 사용자 설정 ----
let INTERVAL = 2 // 삭제할 주기 (초). API 요청 횟수 제한 오류가 발생할 경우 이 숫자를 키워 주세요.
let NOTIFY = true // 작업 시작/종료 시 알림을 보낼 지 여부. 다른 기기에서 스크립트를 실행할 때에 유용합니다.
let IGNORE_ERROR = true // 오류가 발생한 노트를 무시하고 계속 진행할 지 여부.
// false로 설정하면, 오류 발생 시 작업을 중단합니다.
// 청소기를 켜고 방치할 경우 활성화하는 것을 추천합니다.
let REMOVE_REPLY = false // 답글을 삭제할 지 여부
// false로 설정되어 있어도, 자신의 글에 단 답글은 삭제될 수 있습니다.
let REMOVE_PINNED = false // 고정한 노트를 삭제할 지 여부
let REMOVE_FILE_FROM_DRIVE = false // 첨부된 파일을 드라이브에서 삭제할 지 여부
// 경고! 드라이브 기능을 정말 한 번도 쓰지 않았을 경우에만 사용하십시오.
// 파일을 여러 노트에 첨부하였거나, 이모지로 사용되고 있는 파일이
// 삭제될 수 있으며, 이 경우 다른 노트에 첨부된 파일, 또는 노트 자체가
// 삭제되거나, 이모지가 표시되지 않게 될 수 있습니다.
let REMOVE_FROM_NEW = false // 최신 노트부터 삭제할 지 여부.
// 삭제할 노트가 많이 남았는데도 노트가 없다고 표시될 경우,
// 이 기능을 활성화해 보세요.
let RENOTE_THRESHOLD = 999999 // 리노트 수가 이 이상일 때에 삭제하지 않음. 0 이하일 때에 모든 노트 삭제
// 릴레이 관련 버그로 인해 리노트 수보다 실제보다 많이 집계되는 경우가 있으며,
// 이로 인해 삭제되지 않는 노트가 발생할 수 있습니다.
let REACTION_THRESHOLD = 999999 // 리액션 수가 이 이상일 때에 삭제하지 않음. 0 이하일 때에 모든 노트 삭제.
var checkpointId = '' // 이 노트 ID를 기준으로 작업 시작 (해당 노트 미포함)
// 비어 있으면 가장 오래된/새로운 노트부터 삭제합니다.
// REMOVE_FROM_NEW 가 false일 경우, 이 노트 이후부터 오래된 순으로 삭제.
// true일 경우, 이 노트 이전부터 새로운 순으로 삭제.
// --- 사용자 설정 끝 ---
var pinnedNoteIds = []
var deletedNotes = 0
var failedNotes = 0
// 오류인지 아닌지 검사하고, 오류인 경우 콘솔에 메시지를 출력하고 true를 반환
@IsError(v) {
if (Core:type(v) == 'error') {
let errInfo = v.info
if (errInfo.Info == 'RATE_LIMIT_EXCEEDED') {
<: ` - 오류 발생 시각: {Date:to_iso_str()}`
<: ' - API 요청 횟수 제한에 도달했습니다.'
if (IGNORE_ERROR == true) {
<: ' - 1분 동안 쉬었다가 계속 진행합니다...'
Core:sleep(1000*60)
}
} else if (errInfo.Info == 'NO_SUCH_NOTE') {
// 존재하지 않는 노트, 혹은 노트 목록에 있었으나 CASCADE 방식의 삭제로 인해 없어진 노트
return false
} else {
<: ` - 오류 발생 시각: {Date:to_iso_str()}`
<: ' - 알 수 없는 오류가 발생하였습니다.'
each (let item, Obj:kvs(errInfo)) {
<: ` - {item[0]} : {item[1]}`
}
}
return true
} else {
return false
}
}
// 알림을 생성. NOTIFY가 false인 경우 무시됨
@Notify(body) {
if (NOTIFY == true) {
Mk:api('notifications/create', {
body: body
header: '노트 청소기'
})
}
}
/// main
<: `노트 청소기`
<: '유저 정보를 가져오고 있습니다...'
let user = Mk:api('users/show', { userId: USER_ID })
if (IsError(user)) {
Core:abort('유저 정보를 가져오는 데에 실패했습니다.')
}
pinnedNoteIds = user.pinnedNoteIds
<: `반갑습니다, {user.username} 님.`
<: '명령 수행 전 사용자 확인을 요청하고 있습니다...'
let warning_msg = [
'노트 삭제기를 실행하고 계십니다.'
`@{user.username} 님의 노트를 {if REMOVE_FROM_NEW '새' else '오래된'} 노트부터 삭제합니다.`
'삭제기를 실행하기 전, 노트를 삭제할 계정과 조건이 올바른지 다시 한 번 확인하시기 바랍니다.'
'확인을 누르시면 즉시 노트 삭제가 시작됩니다.'
].join(Str:lf)
if (Mk:confirm('노트 삭제기', warning_msg) == false) {
Core:abort('노트 삭제기를 중단합니다.')
}
Notify('노트 청소를 시작합니다.')
<: `시작한 시각: {Date:to_iso_str()}`
let endFlag = false
if (checkpointId == '') {
if (REMOVE_FROM_NEW) {
checkpointId = 'zzzzzzzzzzzzzzzzzzzzz'
} else {
checkpointId = '0'
}
}
loop {
var res = {}
if (REMOVE_FROM_NEW) {
<: `새 노트를 가져옵니다... ({Date:to_iso_str()})`
res = Mk:api('users/notes', {
userId: USER_ID,
withChannelNotes: true,
// withFiles: if REMOVE_REPLY true else false,
withReplies: REMOVE_REPLY,
withRenotes: true,
limit: 30,
untilId: checkpointId
})
} else {
<: `오래된 노트를 가져옵니다... ({Date:to_iso_str()})`
res = Mk:api('users/notes', {
userId: USER_ID,
withChannelNotes: true,
// withFiles: if REMOVE_REPLY true else false,
withReplies: REMOVE_REPLY,
withRenotes: true,
limit: 30,
sinceId: checkpointId
})
}
if (IsError(res)) {
if (IGNORE_ERROR == true) {
// checkpoint를 갱신하지 않고 다시 시도
continue
} else {
endFlag = true
break
}
}
if (res.len == 0) {
<: '삭제할 노트가 없습니다!'
if (deletedNotes == 0) {
<: '만약 삭제될 노트가 있는데도 삭제되지 않는다면... "사용자 설정"의 REMOVE_FROM_NEW를 true로 고친 후 다시 시도해 보세요.'
} else {
<: `{deletedNotes}개 노트 이상을 삭제하였으며, 마지막 체크포인트는 {checkpointId} 입니다.`
if (REMOVE_FROM_NEW) {
<: '(REMOVE_FROM_NEW 가 켜져 있습니다. 체크포인트 사용 시 유의하세요)'
}
}
<: `완료한 시각: {Date:to_iso_str()}`
Notify(`노트 청소가 완료되었으며, {deletedNotes}개 이상의 노트가 삭제되었습니다.{Str:lf}자세한 내용은 로그를 확인하세요.`)
break
}
each(let note, res) {
if (note.renoteCount >= RENOTE_THRESHOLD && RENOTE_THRESHOLD > 0) {
checkpointId = note.id
continue
} else if (note.reactionCount >= REACTION_THRESHOLD && REACTION_THRESHOLD > 0) {
checkpointId = note.id
continue
} else if (pinnedNoteIds.incl(note.id) && REMOVE_PINNED == false) {
checkpointId = note.id
continue
}
<: `{note.id} ({note.createdAt}))`
Mk:api('notes/delete', {
noteId: note.id
})
deletedNotes += 1
if (IsError(res)) {
if (IGNORE_ERROR == true) {
<: ' - 오류로 인해 삭제하지 못했습니다. 건너뜁니다.'
} else {
endFlag = true
}
} else if (REMOVE_FILE_FROM_DRIVE == true) {
each(let fileId, note.fileIds) {
Mk:api('drive/files/delete', { fileId: fileId })
}
}
Core:sleep(1000*INTERVAL)
}
if (endFlag) {
Notify(`오류가 발생하여 작업이 중단되었습니다.{Str:lf}자세한 내용은 로그를 확인하세요.`)
break
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment