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)
})