// ==UserScript==
// @name kone base64 자동복호화
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description base64코드 자동복호화
// @author SYJ
// @match https://arca.live/*
// @match https://kone.gg/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @license MIT
// ==/UserScript==
// 자주 바뀜. 취약한 셀렉터
const SHADOW_ROOT_SELECTOR = "body main div.prose-container";
window.addEventListener('load', ()=>setTimeout(main, 1000));
async function main(){
observeUrlChange(renderUI);
const isAutoMode = await GM_getValue('toggleVal', true);
if (isAutoMode) {
observeUrlChange(autoApply);
}
else {
setTimeout(manuallyApply, 1000);
}
}
function doDecode(text) {
///'use strict';
let result = text;
//result = dec(/aHR0c[0-9A-Za-z+]{20,}[=]{0,2}/, result); //aHR0c:1회인코딩된것.
//result = dec(/YUhSMGN[0-9A-Za-z]{20,}[=]{0,2}/, result, true); //YUhSMGN:2회인코딩된것.
//result = dec(/WVVoU01HTklUVFp[0-9A-Za-z+]{20,}[=]{0,2}/, result, true); // 3회인코딩된것.
//result = dec(/V1ZWb1UwMUhUa2[0-9A-Za-z]{20,}[=]{0,2}/, result, true); // 4회인코딩된것.
result = dec(/[0-9A-Za-z]{6,}[=]{0,2}/, result); //문자열 5회 + '=' 1~2회
//result = dec(/[0-9A-Za-z]{50,}[=]{0,2}/, result); //문자열 200회 + '=' 0~2회
return result;
}
function dec(reg, text) {
let result = text;
const originals = Array.from(reg.exec(result) ?? []);
for (const original of originals){
const decoded = decodeNtime(original);
result = result.replace(original, decoded);
}
return result;
}
const MAX_DECODE_COUNT = 10+1;
function decodeNtime(str) {
let decoded = str;
let old = str;
for (let i=0; i<MAX_DECODE_COUNT; i++){
old = decoded;
decoded = decodeOneTime(decoded);
console.log(decoded, old)
if (decoded === old) return decoded;
}
function decodeOneTime(str) {
try{ return base64DecodeUnicode(str);}
catch{ return str;}
}
function base64DecodeUnicode(str) {
// 1) atob으로 디코딩 → 바이너리(한 글자당 1바이트) 문자열
// 2) 각 문자 코드를 16진수 %xx 형태로 변환
// 3) decodeURIComponent로 UTF-8 해석
const percentEncodedStr = Array
.from(atob(str))
.map(char => '%' + char.charCodeAt(0).toString(16).padStart(2, '0'))
.join('');
return decodeURIComponent(percentEncodedStr);
}
}
function manuallyApply() {
document.body.addEventListener('dblclick', function(e) {
console.log('더블클릭 감지! 🎉',e.target,event.composedPath()[0]);
const el = e.composedPath()[0];
const nodes = Array.from(el.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)
console.log(nodes)
for (const node of nodes){
const original = node.textContent;
const decodedLink = doDecode(original);
// console.log(node, original, decodedLink);
if (original === decodedLink) continue;
linkifyTextNode(node, decodedLink);
}
})
}
function autoApply() {
const contents = Array.from(document.body.querySelectorAll(`main ${textTagNames}`));
const mainContents = Array.from(document.querySelector(SHADOW_ROOT_SELECTOR)?.shadowRoot?.querySelectorAll(textTagNames) ?? []);
contents.push(...mainContents);
for (const tag of contents) {
const nodes = Array.from(tag.childNodes).filter(node=>node.nodeType ===Node.TEXT_NODE)
for (const node of nodes){
const original = node.textContent;
const decodedLink = doDecode(original);
if (original === decodedLink) continue;
console.log('[DECODE] ',original, decodedLink);
linkifyTextNode(node, decodedLink);
}
}
}
const textTagNames = 'p, span, div, a, li,' + // 일반 컨테이너
'h1, h2, h3, h4, h5, h6,' + // 제목 요소
'em, strong, u, b, i, small, mark, ' + // 인라인 포맷팅 요소
'label, button, option, textarea' // 폼/인터페이스 요소
function linkifyTextNode(Node, text) { // 텍스트노드 중 url을 찾아 a태그로 변환. (액션 포함)
// URL 매칭 (https:// 로 시작해서 공백 전까지)
const urlRegex = /(https?:\/\/[^\s]+)/;
const match = urlRegex.exec(text);
if (!match) { // URL 없으면 텍스트 덮어씌우고 종료
Node.textContent = text;
return;
}
const url = match[0];
const start = match.index;
const urlLen = url.length;
// "텍스트1 URL 텍스트2" 꼴의 텍스트노드를 세 개로 분리
// 1) URL 앞부분과 뒤를 분리
const textNode = document.createTextNode(text);
const afterUrlStart = textNode.splitText(start);
const afterUrlEnd = afterUrlStart.splitText(urlLen);
const beforeUrlStart = textNode;
// 3) <a> 요소 생성 후 URL 텍스트 노드 대신 교체
const a = makeATag(url)
Node.parentNode.replaceChild(a, Node);
a.before(beforeUrlStart);
a.after(afterUrlEnd);
function makeATag(link){
const aTag = document.createElement('a');
aTag.href = link;
aTag.textContent = link;
aTag.target = '_blank';
aTag.rel = 'noreferrer';
return aTag;
}
}
// UI
async function renderUI() {
// 1) 값 로드
let val = await GM_getValue('toggleVal', false);
let menuId;
// 2) 배지 생성
/* const badge = document.createElement('div');
Object.assign(badge.style, {
position: 'fixed',
top: '10px',
right: '10px',
padding: '4px 8px',
background: 'rgba(0,0,0,0.7)',
color: '#fff',
fontSize: '14px',
borderRadius: '4px',
zIndex: '9999',
});
document.body.append(badge);
*/
// 3) 렌더 함수
function render() {
// 메뉴 해제 후 다시 등록
if (menuId) GM_unregisterMenuCommand(menuId);
menuId = GM_registerMenuCommand(
`자동모드 토글 (현재: ${val?'ON':'OFF'})`,
toggleValue
);
// 배지 업데이트
//badge.textContent = `현재 값: ${val}`;
}
// 4) 토글 함수 (즉시 UI 업데이트 포함)
async function toggleValue() {
const newVal = !val;
await GM_setValue('toggleVal', newVal);
val = newVal; // 변수 갱신
render(); // 메뉴·배지 즉시 갱신
}
// 초기 렌더
render();
}
const observeUrlChange = (func) => {
func();
let oldHref = document.location.href;
const body = document.querySelector('body');
const observer = new MutationObserver(mutations => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
setTimeout(func, 1000);
}
});
observer.observe(body, { childList: true, subtree: true });
};