NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name DCInside Write Helper // @description 디시인사이드에 글을 작성할 때 자동으로 자짤, 말머리, 말꼬리 등을 추가합니다 // @version 1.0.0 // @author toriato // @copyright 2021, Sangha Lee // @license MIT // @icon https://nstatic.dcinside.com/dc/m/img/dcinside_icon.png // @require https://unpkg.com/js-sha1@0.6.0/build/sha1.min.js // @require https://openuserjs.org/src/libs/toriato/Promisified_GM_xmlhttpRequest.min.js // @require https://openuserjs.org/src/libs/toriato/XHR_Hooks.min.js // @match https://gall.dcinside.com/board/write/* // @match https://gall.dcinside.com/mgallery/board/write/* // @match https://gall.dcinside.com/mini/board/write/* // @run-at document-end // @grant GM_getValue // @grant GM_xmlhttpRequest // @updateURL https://openuserjs.org/meta/toriato/DCInside_Write_Helper.user.js // @supportURL https://github.com/toriato/userscripts/issues // @homepageURL https://github.com/toriato/userscripts // ==/UserScript== /** * 이미지 객체 * @typedef Image * @property {string} name 원래 파일 이름 * @property {string} hash 이미지 SHA1 해시 */ /** * 갤러리 설정 객체 * @typedef Option * @property {string[]} headers 글머리 배열 * @property {string[]} footers 글꼬리 배열 * @property {Image[]} images 이미지 배열 * @property {bool} useRandomFilename 무작위 파일 이름을 사용할지? * @property {bool} appendRandomBytes 파일 끝에 무작위 바이트를 추가할지? */ // 갤러리 별 옵션 불러오기 const params = (new URL(location.href)).searchParams const galleryId = params.get('id') /** @type {Option} */ const options = GM_getValue(`option_${galleryId}`, GM_getValue('option', {})) /** * 이미지 구조를 Blob 으로 변환합니다 * @param {Image} image * @returns {Blob|Error} */ function imageToBlob(image) { // 이미지 데이터 불러오기 const encoded = GM_getValue(`image_${image.hash}`) if (!encoded) { return new Error(`값이 존재하지 않습니다 (image_${image.hash})`) } // Mime 확인하기 let type = '' switch (image.name.split('.').pop()) { case 'jpg': case 'jpeg': type = 'image/jpeg' break case 'png': type = 'image/png' break case 'gif': type = 'image/gif' break // case 'webp': // type = 'image/webp' // break default: return new Error('허용하지 않는 파일입니다') } const bStr = atob(encoded) const bytes = new Uint8Array(bStr.length) let bLen = bStr.length while (bLen--) { bytes[bLen] = bStr.charCodeAt(bLen) } return new Blob([bytes], { type }) } /** * 자짤을 서버에 업로드한 뒤 편집기에 삽입합니다 * @returns {Promise<void>} */ async function attachImage() { const images = options.images if (images.length < 1) { return } // 이미지 디코딩하기 const image = images[Math.floor(Math.random() * images.length)] let blob = imageToBlob(image) if (blob instanceof Error) { throw blob } if (options.appendRandomBytes) { let shit = new Uint32Array(10) crypto.getRandomValues(shit) blob = new Blob([blob, shit], { type: blob.type }) } // 폼 데이터 만들기 const data = new FormData() data.append('r_key', document.getElementById('r_key').value) data.append('gall_id', galleryId) data.append('files[]', blob, options.useRandomFilename ? `${sha1(new Date)}.${image.name.split('.').pop()}` : image.name) // 이미지 업로드 const res = await fetch({ url: 'https://upimg.dcinside.com/upimg_file.php?id=' + galleryId, method: 'POST', responseType: 'json', data, }) if (res.responseText.includes('Web firewall security policies have been blocked')) { throw new Error('웹 방화벽에 의해 차단됐습니다') } // 편집기에서 이미지 삽입 객체 가져오기 const attacher = Editor.getSidebar().getAttacher('image', this) // 편집기에 이미지 추가하기 for (let f of res.response.files) { // https://github.com/kakao/DaumEditor/blob/e47ecbea89f98e0ca6e8b2d9eeff4c590007b4eb/daumeditor/js/trex/attacher/image.js const entry = { filename: f.name, filesize: f.size, imagealign: 'L', imageurl: f.url, originalurl: f.url, thumburl: f._s_url, file_temp_no: f.file_temp_no, mp4: f.mp4 } if (f.web__url) { entry.imageurl = f.web__url } else if (f.web2__url) { entry.imageurl = f.web2__url } // 파일 추가하기 attacher.attachHandler(entry) } } // 말머리와 말꼬리 추가를 위해 훅 추가하기 XMLHttpRequest.registerHook( (method, url) => method === 'POST' && url === '/board/forms/article_submit', function (data) { const params = new URLSearchParams(data) const contents = [params.get('memo')] if (options.headers && options.headers.length > 0) { const header = options.headers[Math.floor(Math.random() * options.headers.length)] contents.unshift(`<div id="dcappheader">${header}</div>`) } if (options.footers && options.footers.length > 0) { const footer = options.footers[Math.floor(Math.random() * options.footers.length)] contents.push(`<div id="dcappheader">${footer}</div>`) } params.set('memo', contents.join('')) return params.toString() } ) // 자짤 올리기 attachImage() .catch(e => { alert('자짤 업로드 중 오류가 발생했습니다:\n' + e.message) console.error(e) })