imager

为轻小说文库++提供图床支持

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greasyfork.org/scripts/451075/1092146/imager.js

/* eslint-disable no-multi-spaces */
/* eslint-disable no-implicit-globals */
/* eslint-disable userscripts/no-invalid-headers */
/* eslint-disable userscripts/no-invalid-grant */

// ==UserScript==
// @name               imager
// @displayname        图床
// @namespace          Wenku8++
// @version            0.2
// @description        为轻小说文库++提供图床支持
// @author             PY-DNG
// @license            GPL-v3
// @regurl             https?://www\.wenku8\.net/.*
// @require            https://greasyfork.org/scripts/449412-basic-functions/code/Basic%20Functions.js?version=1085783
// @require            https://greasyfork.org/scripts/449583-configmanager/code/ConfigManager.js?version=1085836
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_listValues
// @grant              GM_deleteValue
// @grant              GM_xmlhttpRequest
// ==/UserScript==

(function __MAIN__() {
	const DATA_IMAGERS = {
		default: 'SDAIDEV',
		/* Imager Model
		_IMAGER_KEY_: {
			available: true,
			name: '_IMAGER_DISPLAY_NAME_',
			tip: '_IMAGER_DISPLAY_TIP_',
			upload: {
				request: {
					url: '_UPLOAD_URL_',
					data: {
						'_FORM_NAME_FOR_FILE_': '$file$'
					}
				},
				response: {
					checksuccess: (json)=>{return json._SUCCESS_KEY_ === '_SUCCESS_VALUE_';},
					geturl: (json)=>{return json._PATH_._SUCCESS_URL_KEY_;},
					getname: (json)=>{return json._PATH_ ? json._PATH_._FILENAME_ : null;},
					getsize: (json)=>{return json._PATH_._SIZE_},
					getpage: (json)=>{return json._PATH_ ? json._PATH_._PAGE_ : null;},
					gethash: (json)=>{return json._PATH_ ? json._PATH_._HASH_ : null;},
					getdelete: (json)=>{return json._PATH_ ? json._PATH_._DELETE_ : null;}
				}
			},
			isImager: true
		},
		*/
		PANDAIMG: {
			available: true,
			name: '熊猫图床',
			tip: '2022-01-16测试可用</br>单张图片最大5MB',
			upload: {
				request: {
					url: 'https://api.pandaimg.com/upload',
					data: {
						'file': '$file$',
						'classifications': '',
						'day': '0'
					},
					headers: {
						'usersOrigin': '5edd88d4dfe5d288518c0454d3ccdd2a'
					}
				},
				response: {
					checksuccess: (json)=>{return json.code === '200';},
					geturl: (json)=>{return json.data.url;},
					getname: (json)=>{return json.data.name;}
				}
			},
			isImager: true
		},
		SDAIDEV: {
			available: true,
			name: '流浪图床',
			tip: '2022-01-09测试可用</br>单张图片最大5MB',
			upload: {
				request: {
					url: 'https://p.sda1.dev/api/v1/upload_external_noform',
					urlargs: {
						'filename': '$filename$',
						'ts': '$time$',
						'rand': '$random$'
					}
				},
				response: {
					checksuccess: (json)=>{return json.success;},
					geturl: (json)=>{return json.data.url;},
					getdelete: (json)=>{return json.data ? json.data.delete_url : null;},
					getsize: (json)=>{return json.data ? json.data.size : null;}
				}
			},
			isImager: true
		},
		JITUDISK: {
			available: true,
			name: '极兔兔床',
			tip: '2022-02-02测试可用',
			upload: {
				request: {
					url: 'https://pic.jitudisk.com/api/upload',
					data: {
						'image': '$file$'
					}
				},
				response: {
					checksuccess: (json)=>{return json.code === 200;},
					geturl: (json)=>{return json.data.url;},
					getname: (json)=>{return json.data.name;}
				}
			},
			isImager: true
		},
		SMMS: {
			available: true,
			name: 'SM.MS',
			tip: '注意:此图床跨域访问较不稳定,且有用户反映其被国内部分服务商屏蔽,请谨慎使用此图床',
			warning: '注意:此图床跨域访问较不稳定,且有用户反映其被国内部分服务商屏蔽,请谨慎使用此图床</br>如出现上传错误/图片加载慢/无法加载图片等情况,请更换其他图床',
			upload: {
				request: {
					url: 'https://sm.ms/api/v2/upload?inajax=1',
					data: {
						'smfile': '$file$'
					}
				},
				response: {
					checksuccess: (json)=>{return json.success === true || /^https?:\/\//.test(json.images);},
					geturl: (json)=>{return json.data ? json.data.url : json.images;},
					getname: (json)=>{return json.data ? json.data.filename : null;},
					getpage: (json)=>{return json.data ? json.data.page : null;},
					gethash: (json)=>{return json.data ? json.data.hash : null;},
					getdelete: (json)=>{return json.data ? json.data.delete : null;}
				}
			},
			isImager: true
		},
		CATBOX: {
			available: true,
			name: 'CatBox',
			tip: '注意:此图床访问较不稳定,请谨慎使用此图床',
			warning: '注意:此图床访问较不稳定,请谨慎使用此图床</br>如出现上传错误/图片加载慢/无法加载图片等情况,请更换其他图床',
			upload: {
				request: {
					url: 'https://catbox.moe/user/api.php',
					responseType: 'text',
					data: {
						'fileToUpload': '$file$',
						'reqtype': 'fileupload'
					}
				},
				response: {
					checksuccess: (text)=>{return true;},
					geturl: (text)=>{return text;}
				}
			},
			isImager: true
		}
	};
	const CONST = {
		Text: {
			CurImage: '当前图片:',
			InputImage: '选择/粘贴/拖拽 上传图片',
			InvalidFile: '您选择的文件不是可识别的图片:(</br>请选择.jpg/.jpeg/.png格式的图片',
			UploadError: '上传错误!',
			NoNameFromSever: '空(服务器没有返回文件名)',
		},
		Config_Ruleset: {
			'version-key': 'config-version',
			'ignores': ["LOCAL-CDN"],
			'defaultValues': {
				imager: 'SDAIDEV'
			}
		}
	};

	const CM = new ConfigManager(CONST.Config_Ruleset);
	const CONFIG = CM.Config;
	const alertify = require('alertify');
	const settings = require('settings');
    const SettingPanel = require('SettingPanel');
	SettingPanel.registerElement('image', {
		createElement: function() {
			const SO = this;
			const data = SO.hasOwnProperty('data') ? SO.data : {};

			// <input type="file">
			const file = $CrE('input');
			file.type = 'file';
			file.addEventListener('change', fileGot);

			// Displayer div
			const div = $CrE('div');
			div.innerText = CONST.Text.CurImage + SO.url + '\n' + CONST.Text.InputImage;
			div.style.color = data.hasOwnProperty('textColor') ? data.textColor : 'grey';
			div.style.width = div.style.height = '100%';
			div.style.border = div.style.padding = div.style.margin = '0';
			data.hasOwnProperty('innerText') && (div.innerText = data.innerText);
			data.hasOwnProperty('innerHTML') && (div.innerHTML = data.innerHTML);
			div.addEventListener('click', file.click.bind(file));
			div.addEventListener('paste', fileGot);
			div.addEventListener('dragenter', destroyEvent);
			div.addEventListener('dragover', destroyEvent);
			div.addEventListener('drop', fileGot);
			return div;

			function fileGot(e) {
				const file = fileEvent(e);
				if (!file) {
					alertify.error(CONST.Text.InvalidFile);
					return false;
				}
				uploadImage({
					file: file,
					type: CONFIG.imager,
					onload: function(e) {
						copyProps(e, SO, Object.keys(e));
						div.innerText = CONST.Text.CurImage + SO.url + '\n' + CONST.Text.InputImage;
						div.dispatchEvent(new Event('change'));
					},
				});
			}
		},
		setValue: function(url) {
			this.url = url;
			this.element.innerText = CONST.Text.CurImage + url + '\n' + CONST.Text.InputImage;
		},
		getValue: function() {return this.url;},
	});

	function fileEvent(e) {
		destroyEvent(e);
		const input = e.dataTransfer || e.clipboardData || window.clipboardData || e.target;
		if (!input.files || input.files.length === 0) {return false;};

		for (const file of input.files) {
			const splited = file.name.split('.');
			const ext = splited[splited.length-1].toLowerCase();
			const extOkay = ['jpg', 'jpeg', 'png', 'webp'].includes(ext);
			const mimeOkay = ['image/bmp', 'image/gif', 'image/vnd.microsoft.icon', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/tiff', 'image/webp'].includes(file.type)
			if (extOkay || mimeOkay) {
				return file;
			}
		}

		return null;
	}

	// Upload image to KIENG images
	// details: {file: File, onload: Function({url, name, json}), onerror: Function, type: 'sm.ms/jd/sg/tt/...'}
	function uploadImage(details) {
		const file    = details.file;
		const onload  = details.onload  ? details.onload  : function() {};
		const onerror = details.onerror ? details.onerror : uploadError;
		const type    = details.imager    ? details.imager    : CONFIG.imager;
		if (!DATA_IMAGERS.hasOwnProperty(type) || !DATA_IMAGERS[type].available) {
			onerror();
			return false;
		}
		const imager = DATA_IMAGERS[type];
		const upload = imager.upload;
		const request = upload.request;
		const response = upload.response;

		// Construct request url
		let url = request.url;
		if (request.urlargs) {
			const args = request.urlargs;
			const makearg = (key, value) => ('{K}={V}'.replace('{K}', key).replace('{V}', value));
			const replacers = {
				'$filename$': () => (encodeURIComponent(file.name)),
				'$random$': () => (Math.random().toString()),
				'$time$': () => ((new Date()).getTime().toString())
			};
			for (let [key, value] of Object.entries(args)) {
				url += url.includes('?') ? '&' : '?';
				for (const [str, replacer] of Object.entries(replacers)) {
					while (value !== null && value.includes(str)) {
						const val = replacer(key);
						value = (val !== null) ? value.replace(str, val) : null;
					}
				}
				(value !== null) && (url += makearg(key, value));
			}
		}

		// Construst request body
		let data;
		if (request.data) {
			data = new FormData();
			const replacers = {
				'$file$': (key) => ((data.append(key, file), null)),
				'$random$': () => (Math.random().toString()),
				'$time$': () => ((new Date()).getTime().toString())
			};

			for (let [key, value] of Object.entries(request.data)) {
				for (const [str, replacer] of Object.entries(replacers)) {
					while (value !== null && value.includes(str)) {
						const val = replacer(key);
						value = (val !== null) ? value.replace(str, val) : null;
					}
				}
				(value !== null) && data.append(key, value);
			}
		} else {
			data = file;
		}

		// headers
		const headers = request.headers || {};

		GM_xmlhttpRequest({
			method: 'POST',
			url: url,
			timeout: 15 * 1000,
			data: data,
			headers: headers,
			responseType: request.responseType ? request.responseType : 'json',
			onerror: onerror,
			ontimeout: onerror,
			onabort: onerror,
			onload: (e) => {
				const json = e.response;
				const success = e.status === 200 && response.checksuccess(json);
				if (success) {
					const url = response.geturl(json);
					const name = response.getname ? (response.getname(json) ? response.getname(json) : CONST.Text.NoNameFromSever) : CONST.Text.NoNameFromSever
					onload({
						url: url,
						name: name,
						json: json
					});
				} else {
					onerror(json);
					return;
				}
			}
		});

		function uploadError(json) {
			alertify.error(CONST.Text.UploadError);
			DoLog(LogLevel.Error, [CONST.Text.UploadError, json]);
		}
	}
})();