Last active
September 5, 2025 00:30
-
-
Save ltlapy/483f41fdb691c8bc6e3ca1451ae0ff21 to your computer and use it in GitHub Desktop.
노트 청소기
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
| // @ 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