您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
×
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/401726/825616/imageHostUploader.js
// ==UserScript== // @name imageHostUploader // @namespace https://greasyfork.org/cs/users/321857-anakunda // @version 1.94 // @author Anakunda // @description × // @require https://greasyfork.org/scripts/404642-js-xhr/code/js-xhr.js // @require https://greasyfork.org/scripts/404516-progressbars/code/progressBars.js // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // ==/UserScript== // https://funkyimg.com/ (4MB limit) // http://ge.tt/ (no HTTPS) // http://savephoto.ru/ (no HTTPS) 'use strict'; const ulTimeFactor = GM_getValue('upload_speed', 16); const rehostTimeout = 30000; const urlParser = /^\s*(https?:\/\/\S+)\s*$/i; const nonWordStripper = /[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\xFF]+/g; var testRemoteSizes = GM_getValue('test_remote_sizes'); if (testRemoteSizes === undefined) GM_setValue('test_remote_sizes', (testRemoteSizes = false)); // time consuming String.prototype.toASCII = function() { return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, ''); }; class PTPimg { constructor() { this.alias = 'PTPimg'; this.origin = 'https://ptpimg.me'; this.types = ['png', 'jpeg', 'gif', 'bmp']; this.whitelist = [ 'passthepopcorn.me', 'redacted.ch', 'orpheus.network', 'notwhat.cd', 'dicmusic.club', 'broadcasthe.net', ]; if (!(this.apiKey = GM_getValue('ptpimg_api_key'))) try { if (this.apiKey = JSON.parse(window.localStorage.ptpimg_it).api_key) GM_setValue('ptpimg_api_key', this.apiKey); } catch(e) { console.debug(e) } if (this.apiKey === undefined) GM_setValue('ptpimg_api_key', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); if (this.batchLimit && images.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return this.setSession().then(apiKey => new Promise((resolve, reject) => { const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; images.forEach((image, ndx) => { formData += 'Content-Disposition: form-data; name="file-upload[' + ndx + ']"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="api_key"\r\n\r\n'; formData += apiKey + '\r\n'; formData += '--' + boundary + '--\r\n'; GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload.php', responseType: 'json', headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (!response.response) return reject('void response'); if (response.response.length < images.length) { console.warn('PTPimg returning incomplete list of images (', response.response, ')'); return reject(`not all images uploaded (${response.response.length}/${images.length})`); } if (response.response.length > images.length) console.warn('PTPimg returns more links than expected (', response.response, images, ')'); resolve(response.response.map((item, ndx) => { if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(images[ndx].name)) item.ext = RegExp.$1; return this.origin + '/' + item.code + '.' + item.ext; })); }, onprogress: typeof progressHandler == 'function' ? progressHandler : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })); } rehost(urls) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return this.setSession().then(apiKey => Promise.all(urls.map(url => { if (!urlParser.test(url)) return Promise.reject('URL not valid (' + url + ')'); var hostname = new URL(url).hostname; if (hostname == 'img.discogs.com' || hostname.endsWith('omdb.org')) { return verifyImageUrl('https://reho.st/' + url) .catch(reason => imageHostHandlers.catbox.rehost([url]).then(imgUrls => imgUrls[0])) .catch(reason => imageHostHandlers.pixhost.rehost([url]).then(imgUrls => imgUrls[0].original)) .catch(reason => this.reupload(url)); } else if (!['png', 'jpg', 'jpeg', 'jfif', 'gif', 'bmp'].some(ext => url.toLowerCase().endsWith('.' + ext))) { return verifyImageUrl(url + '#.jpg') .catch(reason => imageHostHandlers.imgbb.rehost([url]).then(imgUrls => imgUrls[0].original)) .catch(reason => imageHostHandlers.jerking.rehost([url]).then(imgUrls => imgUrls[0].original)) .catch(reason => imageHostHandlers.pixhost.rehost([url]).then(imgUrls => imgUrls[0]).original) } return verifyImageUrl(url); })).then(imageUrls => { console.debug('PTPimg.rehost(...) input:', imageUrls); var formData = new URLSearchParams({ 'link-upload': imageUrls.join('\r\n'), 'api_key': apiKey, }); return globalFetch(this.origin + '/upload.php', { responseType: 'json', timeout: imageUrls.length * rehostTimeout, }, formData).then(response => { if (!response.response) return Promise.reject('void response'); if (response.response.length < imageUrls.length) { console.warn('PTPimg returning incomplete list of images (', response.response, ')'); return Promise.reject(`not all images rehosted to (${response.response.length}/${imageUrls.length})`) } if (response.response.length > imageUrls.length) console.warn('PTPimg returns more links than expected (', response.response, imageUrls, ')'); return response.response.map((item, ndx) => { if (!item.ext && /\.([a-z]+)(?=$|[\#\?])/i.test(imageUrls[ndx])) item.ext = RegExp.$1; return this.origin + '/' + item.code + '.' + item.ext; }); }); })); } reupload(imgUrl) { console.warn('PTPIMG rehoster fallback to local reupload'); return globalFetch(imgUrl, { responseType: 'blob' }).then(response => { var image = { name: imgUrl.replace(/^.*\//, ''), data: response.responseText, size: response.responseText.length, }; switch (imgUrl.replace(/^.*\./, '').toLowerCase()) { case 'jpg': case 'jpeg': case 'jfif': image.type = 'image/jpeg'; break; case 'png': image.type = 'image/png'; break; case 'gif': image.type = 'image/gif'; break; case 'bmp': image.type = 'image/bmp'; break; default: return Promise.reject('Unsupported extension'); } return this.upload([image]).then(imgUrls => imgUrls[0]); }); } setSession() { return this.apiKey ? Promise.resolve(this.apiKey) : globalFetch(this.origin).then(response => { var apiKey = response.document.getElementById('api_key'); if (apiKey == null) { let counter = GM_getValue('ptpimg_reminder_read', 0); if (counter < 3) { alert(` PTPimg API key could not be captured. Please login to ${this.origin}/ and redo the action. If you don\'t have PTPIMG account or don\'t want to use it, consider to remove PTPimg from upload_hosts and rehost_hosts local storage entries. `); GM_setValue('ptpimg_reminder_read', ++counter); } return Promise.reject('PTPimg API key not configured'); } if (!(this.apiKey = apiKey.value)) return Promise.reject('Assertion failed: empty PTPimg API key'); GM_setValue('ptpimg_api_key', this.apiKey); Promise.resolve(this.apiKey) .then(apiKey => { alert(`Your PTPimg API key [${apiKey}] was successfully configured`) }); return this.apiKey; }); } } class Chevereto { constructor(hostName, alias = undefined, types = undefined, sizeLimitAnonymous = undefined, sizeLimit = undefined, configPrefix = undefined, apiUrl = null, apiFieldName = undefined, apiResultKey = undefined) { if (typeof hostName != 'string' || !hostName) throw 'Chevereto adapter: missing host name'; this.hostName = hostName; this.alias = alias; if (Array.isArray(types)) this.types = types; this.sizeLimitAnonymous = sizeLimitAnonymous; this.sizeLimit = sizeLimit || sizeLimitAnonymous; if (alias) var al = alias.replace(nonWordStripper, ''); if (!configPrefix && al) configPrefix = al.toLowerCase(); if (!configPrefix && /^(?:www\.)?([\w\-]+)(?:\.[\w\-]+)+$/.test(hostName)) configPrefix = RegExp.$1.toLowerCase(); if (this.configPrefix = configPrefix) { this.uid = GM_getValue(this.configPrefix + '_uid'); if (this.uid === undefined && alias) GM_setValue(this.configPrefix + '_uid', ''); this.password = GM_getValue(this.configPrefix + '_password'); if (this.password === undefined && alias) GM_setValue(this.configPrefix + '_password', ''); this.apiKey = GM_getValue(this.configPrefix + '_api_key'); if (this.apiKey === undefined && alias && apiUrl) GM_setValue(this.configPrefix + '_api_key', ''); } else console.warn('Chevereto adapter: config prefix could not be evaluated, authorized operations not available'); this.apiUrl = apiUrl; this.apiFieldName = apiFieldName; this.apiResultKey = apiResultKey; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession(false).then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20 || !session.username && !!session.key && this.sizeLimitAnonymous >= 0 && image.size > this.sizeLimitAnonymous * 2**20) return Promise.reject(`image size exceeds upload limit (${image.size})`); const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = Object.assign({ action: 'upload', type: 'file', nsfw: 0, thumb_width: 200, //thumb_height: 200, format: 'json', }, session); Object.keys(params).forEach((field, index, arr) => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += params[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="' + (session.key && this.apiFieldName || 'source') + '"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n' + image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: session.key ? (this.apiUrl || 'https://' + this.hostName + '/api/1') + '/upload' : 'https://' + this.hostName + '/json', responseType: 'json', headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': 'https://' + this.hostName, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status >= 200 && response.status < 400) try { if (response.response.success) resolve(this.resultHandler(response.response[session.key && this.apiResultKey || 'image'])); else reject((response.response.error ? response.response.error.message : response.response.status_txt) + ' (' + response.response.status_code + ')'); } catch(e) { reject(e) } else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return this.setSession(false).then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams(Object.assign({ action: 'upload', type: 'url', nsfw: 0, thumb_width: 200, //thumb_height: 200, format: 'json', }, session)); formData.set(session.key && this.apiFieldName || 'source', imageUrl); return globalFetch(session.key ? (this.apiUrl || 'https://' + this.hostName + '/api/1') + '/upload' : 'https://' + this.hostName + '/json', { responseType: 'json', headers: { 'Referer': 'https://' + this.hostName }, timeout: urls.length * rehostTimeout, }, formData).then(response => { if (!response.response.success) return Promise.reject(`${this.hostName}: ${response.response.error.message} (${response.response.status_code})`); if (typeof progressHandler == 'function') progressHandler(true); return this.resultHandler(response.response[session.key && this.apiResultKey || 'image']); }); })))); } resultHandler(result) { try { return { original: result.image && result.image.url || result.url, thumb: result.thumb.url, share: result.url_viewer, }; } catch(e) { return result.url } } galleryResolver(url) { var albumId = /^\/(?:album|al)\/(\w+)\b/.test(url.pathname) && RegExp.$1; if (!albumId) return Promise.reject('Invlaid gallery URL'); return this.setSession(true, false).then(session => { var formData = new URLSearchParams(Object.assign({ action: 'get-album-contents', albumid: albumId, }, session)); return globalFetch('https://' + this.hostName + '/json', { responseType: 'json', headers: { 'Referer': url }, }, formData).then(response => { return response.response.status_txt == 'OK' && Array.isArray(response.response.contents) ? response.response.contents.map(image => image.url) : Promise.reject(`${this.hostName}: ${response.response.error.message} (${response.response.status_code})`); }); }).catch(reason => { console.warn(this.hostName, 'gallery couldn\'t be resolved via API:', reason, '(falling back to HTML parser)'); return new Promise((resolve, reject) => { var urls = [], domParser = new DOMParser; getPage(url); function getPage(url) { GM_xmlhttpRequest({ method: 'GET', url: url, headers: { Referer: url }, onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); var dom = domParser.parseFromString(response.responseText, 'text/html'); Array.prototype.push.apply(urls, Array.from(dom.querySelectorAll('div.list-item-image > a.image-container')).map(a => a.href)); var next = dom.querySelector('a[data-pagination="next"][href]'); if (next == null || !next.href) resolve(urls); else getPage(next.href); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); } }).then(urls => Promise.all(urls.map(imageUrlResolver))); }); } setSession(requireToken = true, requireLogin = false) { var session = { timestamp: Date.now() }; if (this.uid) session.login = this.uid; if (this.password) session.password = this.password; if (this.apiKey) { session.key = this.apiKey; if (!requireToken && !requireLogin) return Promise.resolve(session); } const index = 'https://' + this.hostName + '/'; return globalFetch(index).then(response => { if (/\b(?:auth_token)\s*=\s*"(\w+)"/m.test(response.responseText)) var authToken = RegExp.$1; if (!authToken) { authToken = response.document.querySelector('input[name="auth_token"][value]'); if (authToken != null) authToken = authToken.value; } if (authToken) session.auth_token = authToken; else { console.warn('Chevereto auth_token detection failure:', this.hostName, '\n\n', response.responseText); return Promise.reject('auth_token detection failure'); } if (getUser(response)) return session; if (!this.configPrefix || !this.uid || !this.password) return !requireLogin ? session : Promise.reject('not logged in'); var formData = new URLSearchParams({ 'login-subject': this.uid, 'password': this.password, 'auth_token': session.auth_token, }); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: index + 'login', headers: { 'Accept': '*/*', 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': formData.toString().length, 'Referer': index + 'login', }, data: formData.toString(), onload: response => { if (response.status < 200 || response.status > 400) defaultErrorHandler(response); resolve(response.status); }, onerror: response => { reject(defaultErrorHandler(response)); //resolve(response.status); }, ontimeout: response => { reject(defaultTimeoutHandler(response)); //resolve(response.status); }, }); }).then(status => globalFetch(index, { responseType: 'text' }).then(response => { if (!getUser(response)) return Promise.reject('unknown reason'); console.debug(this.hostName, 'login session:', session); return session; })).catch(reason => { console.warn('Chevereto login failed:', reason); return !requireLogin ? session : Promise.reject('login failed (' + reason + ')'); }); function getUser(response) { if (/\b(?:logged_user)\s*=\s*(\{.*\});/.test(response.responseText)) try { let logged_user = JSON.parse(RegExp.$1); session.username = logged_user.username; session.userid = logged_user.id; return Boolean(logged_user.username || logged_user.id); } catch(e) { console.warn(e) } return false; } }); } } class PixHost { constructor() { this.alias = 'PixHost'; this.origin = 'https://pixhost.to'; this.types = ['png', 'jpeg', 'gif']; this.sizeLimit = 10; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return Promise.all(images.map((image, index) => new Promise((resolve, reject) => { const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="img"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="content_type"\r\n\r\n'; formData += '0\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: 'https://api.pixhost.to/images', responseType: 'json', headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status >= 200 && response.status < 400) resolve(PixHost.resultHandler(response.response)); else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }))); } rehost(urls) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return verifyImageUrls(urls).then(imageUrls => { //console.debug('rehost2PixHost(...) input:', imageUrls.join('\n')); var formData = new URLSearchParams({ imgs: imageUrls.join('\r\n'), content_type: 0, tos: 'on', }); return globalFetch(this.origin + '/remote/', { responseType: 'text', timeout: imageUrls.length * rehostTimeout, }, formData).then(response => { if (!/\b(?:upload_results)\s*=\s*(\{.*\});$/m.test(response.responseText)) return Promise.reject('page parsing error'); var images = JSON.parse(RegExp.$1).images; if (images.length < imageUrls.length) return Promise.reject(`not all images rehosted (${images.length}/${imageUrls.length})`); return Promise.all(images.map(PixHost.resultHandler)); }); }); } static resultHandler(result) { try { return imageUrlResolver(result.show_url).then(imgUrl => ({ original: imgUrl, thumb: result.th_url, share: result.show_url, })); } catch(e) { return Promise.reject(e) } } } class Catbox { constructor() { this.alias = 'Catbox'; this.origin = 'https://catbox.moe'; this.sizeLimit = 200; if ((this.userHash = GM_getValue('catbox_userhash')) === undefined) GM_setValue('catbox_userhash', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().catch(reason => undefined).then(userHash => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { var now = Date.now(); const boundary = '--------WebKitFormBoundary-' + now.toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="reqtype"\r\n\r\n'; formData += 'fileupload\r\n'; formData += '--' + boundary + '\r\n'; if (userHash) { formData += 'Content-Disposition: form-data; name="userhash"\r\n\r\n'; formData += userHash + '\r\n'; formData += '--' + boundary + '\r\n'; } formData += 'Content-Disposition: form-data; name="fileToUpload"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/user/api.php', responseType: 'text', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status >= 200 && response.status < 400) resolve(response.responseText); else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return this.setSession().catch(reason => undefined).then(userHash => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams({ reqtype: 'urlupload', url: imageUrl, }); if (userHash) formData.set('userhash', userHash); return globalFetch(this.origin + '/user/api.php', { responseType: 'text', timeout: urls.length * rehostTimeout, }, formData).then(response => { if (typeof progressHandler == 'function') progressHandler(true); return response.responseText; }); })))); } setSession() { return this.userHash ? Promise.resolve(this.userHash) : globalFetch(this.origin).then(response => { var userHash = response.document.querySelector('input[name="userhash"][value]'); if (userHash == null) return Promise.reject('userhash not configured; please log-in to Catbox.moe to autodetect it'); if (!(this.userHash = userHash.value)) return Promise.reject('assertion failed: empty userhash value'); GM_setValue('catbox_userhash', this.userHash); return this.userHash; }); } } class ImgBox { constructor() { this.alias = 'ImgBox'; this.origin = 'https://imgbox.com'; this.types = ['jpeg', 'gif', 'png']; this.sizeLimit = 10; if ((this.uid = GM_getValue('imgbox_uid')) === undefined) GM_setValue('imgbox_uid', ''); if ((this.password = GM_getValue('imgbox_password')) === undefined) GM_setValue('imgbox_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; var now = Date.now(); const boundary = '--------WebKitFormBoundary-' + now.toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; Object.keys(session.params).forEach((field, index, arr) => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += session.params[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="files[]"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload/process', headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'X-CSRF-Token': session.csrf_token, }, data: formData, responseType: 'json', binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status >= 200 && response.status < 400) resolve({ original: response.response.files[0].original_url, thumb: response.response.files[0].thumbnail_url, share: response.response.files[0].url, }); else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } setSession() { return globalFetch(this.origin + '/').then(response => { var csrfToken = response.document.querySelector('meta[name="csrf-token"]'); if (csrfToken == null) return Promise.reject('ImgBox.com session token not found'); console.debug('ImgBox.com session token:', csrfToken.content); if (response.document.querySelector('div.btn-group > ul.dropdown-menu') != null) return csrfToken.content; if (!this.uid || !this.password) return csrfToken.content; var formData = new URLSearchParams({ "utf8": "✓", "authenticity_token": csrfToken.content, "user[login]": this.uid, "user[password]": this.password, }); GM_xmlhttpRequest({ method: 'POST', url: 'https://imgbox.com/login', headers: { 'Referer': 'https://imgbox.com/login', 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Content-Length': formData.toString().length, }, data: formData.toString() }); return new Promise((resolve, reject) => { setTimeout(() => { globalFetch('http://imgbox.com/').then(response => { if (response.document.querySelector('div.btn-group > ul.dropdown-menu') == null) console.warn('ImgBox.com login failed, continuing as anonymous', response); if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) { console.debug('ImgBox.com session token after login:', csrfToken.content); resolve(csrfToken.content); } else reject('ImgBox.com session token not found'); }) }, 1000); }); }).then(csrfToken => globalFetch(this.origin + '/ajax/token/generate', { method: 'POST', responseType: 'json', headers: { 'X-CSRF-Token': csrfToken }, }).then(response => ({ csrf_token: csrfToken, params: { token_id: response.response.token_id, token_secret: response.response.token_secret, content_type: 1, thumbnail_size: '150r', gallery_id: null, gallery_secret: null, comments_enabled: 0, }, }))); } } class Imgur { constructor() { this.alias = 'Imgur'; this.origin = 'https://imgur.com'; this.types = ['jpeg', 'png', 'gif', 'apng', 'tiff', 'bmp', 'icf', 'webp']; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(clientId => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="type"\r\n\r\n'; formData += 'file\r\n'; formData += '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="name"\r\n\r\n'; formData += image.name + '\r\n'; formData += '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="Filedata"; filename="' + image.name.toASCII() + '"\r\n'; //formData += 'Content-Disposition: form-data; name="image"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload', //url: 'https://api.imgur.com/3/image?client_id=' + clientId, responseType: 'json', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin + '/upload', //'Referer': this.origin + '/upload?beta', }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); resolve('https://i.imgur.com/' + response.response.data.hash + response.response.data.ext); //if (!response.response.success) return reject('status:' + response.response.status); //resolve(response.response.link); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams({ url: imageUrl }); return globalFetch(this.origin + '/upload', { responseType: 'json', headers: { Referer: this.origin + '/upload' }, timeout: urls.length * rehostTimeout, }, formData).then(result => { if (!result.response.success) return Promise.reject(result.response.status); if (typeof progressHandler == 'function') progressHandler(true); return 'https://i.imgur.com/' + result.response.data.hash + result.response.data.ext; }); })))); } setSession() { return Promise.resolve(''); } } class PostImage { constructor() { this.alias = 'PostImage'; this.origin = 'https://postimages.org'; this.sizeLimit = 24; if ((this.uid = GM_getValue('postimg_uid')) === undefined) GM_setValue('postimg_uid', ''); if ((this.password = GM_getValue('postimg_password')) === undefined) GM_setValue('postimg_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; Object.keys(session).forEach(field => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += session[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/json/rr', responseType: 'json', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.status != 'OK') return reject(response.response.status); resolve(response.response.url); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).then(PostImage.resultHandler)))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams(Object.assign({ url: imageUrl }, session)); return globalFetch(this.origin + '/json/rr', { responseType: 'json', timeout: urls.length * rehostTimeout, }, formData).then(response => { if (response.status < 200 || response.status >= 400) return Promise.reject(defaultErrorHandler(response)); if (response.response.status != 'OK') return Promise.reject(response.response.status); if (typeof progressHandler == 'function') progressHandler(true); return PostImage.resultHandler(response.response.url); }); })))); } static resultHandler(resultUrl) { return globalFetch(resultUrl).then(function(response) { var thumb = response.document.querySelector('div.thumb > a.img'); if (thumb == null || !/\b(?:url)\("(.+)"\)/.test(thumb.style.backgroundImage)) throw 'Page parsing error'; thumb = RegExp.$1; return { original: response.document.querySelector('meta[property="og:image"][content]').content, thumb: thumb, share: response.document.querySelector('meta[property="og:url"][content]').content, } }).catch(reason => imageUrlResolver(resultUrl)) } static galleryResolver(url) { return globalFetch(url, { responseType: 'text' }).then(function(response) { if (/\b(?:var\s+embed_value)=(\{[\S\s]+?\});/.test(response.responseText)) try { let embed_value = JSON.parse(RegExp.$1); return Object.keys(embed_value).map(key => 'https://i.postimg.cc/' .concat(embed_value[key][2], '/', embed_value[key][0], '.', embed_value[key][1])) } catch(e) { console.warn(e) } return Promise.reject('URL not resolvable'); }); } setSession() { return globalFetch(this.origin + '/').then(response => { var session = { session_upload: Date.now(), upload_session: randomString(32), optsize: 0, expire: 0, numfiles: 1, upload_referer: btoa(this.origin + '/'), }; if (/"token","(\w+)"/.test(response.responseText)) session.token = RegExp.$1; if (response.document.querySelector('nav.authorized') != null) return session; if (!this.uid || !this.password) return session; var formData = new URLSearchParams({ 'email': this.uid, 'password': this.password, }); return new Promise((resolve, reject) => GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/login', data: formData.toString(), headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Content-Length': formData.toString().length, 'Referer': this.origin + '/login', }, onload: response => { if (response.status >= 200 && response.status <= 400) resolve(session); else reject(defaultErrorHandler(response)); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, })); }); } } class ImageVenue { constructor() { this.alias = 'ImageVenue'; this.origin = 'https://www.imagevenue.com'; this.types = ['jpeg', 'png', 'gif']; if ((this.uid = GM_getValue('imagevenue_uid')) === undefined) GM_setValue('imagevenue_uid', ''); if ((this.password = GM_getValue('imagevenue_password')) === undefined) GM_setValue('imagevenue_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); if (this.batchLimit && images.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return this.setSession().then(session => new Promise((resolve, reject) => { var now = Date.now(); const boundary = '--------WebKitFormBoundary-' + now.toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; Object.keys(session).forEach((field, index, arr) => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += session[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); images.forEach((image, index, arr) => { formData += 'Content-Disposition: form-data; name="files[' + index + ']"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary; if (index + 1 >= arr.length) formData += '--'; formData += '\r\n'; }); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload', headers: { 'Accept': 'application/json', 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, }, data: formData, responseType: 'json', binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); resolve(response.response.success); }, onprogress: typeof progressHandler == 'function' ? progressHandler : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })).then(resultUrl => globalFetch(resultUrl)).then(response => { var thumbs = response.document.querySelectorAll('div.row > div > a > img'); return Promise.all(Array.from(thumbs).map(img => imageUrlResolver(img.parentNode.href).then(imgUrl => ({ original: imgUrl, thumb: img.src, share: img.parentNode.href, })))); }); } setSession() { return globalFetch(this.origin + '/').then(response => { var csrfToken = response.document.querySelector('meta[name="csrf-token"]'); if (csrfToken == null) return Promise.reject('ImageVenue.com session token not found'); console.debug('ImageVenue.com session token:', csrfToken.content); if (response.document.getElementById('navbarDropdown') != null) return csrfToken.content; if (!this.uid || !this.password) return csrfToken.content; var formData = new URLSearchParams({ '_token': csrfToken.content, 'email': this.uid, 'password': this.password, }); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/auth/login', headers: { 'Referer': this.origin + '/auth/login', 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 'Content-Length': formData.toString().length, }, data: formData.toString() }); return new Promise((resolve, reject) => { setTimeout(() => { globalFetch(this.origin + '/').then(response => { if (response.document.getElementById('navbarDropdown') == null) console.warn('ImageVenue.com login failed, continuing as anonymous', response); if ((csrfToken = response.document.querySelector('meta[name="csrf-token"]')) != null) { console.debug('ImageVenue.com session token after login:', csrfToken.content); resolve(csrfToken.content); } else reject('ImageVenue.com session token not found'); }); }, 1000); }); }).then(csrfToken => globalFetch(this.origin + '/upload/session', { responseType: 'json', headers: { 'X-CSRF-TOKEN': csrfToken }, }, new URLSearchParams({ thumbnail_size: 2, content_type: 'sfw', comments_enabled: false, })).then(response => ({ data: response.response.data, _token: csrfToken, }))); } } class FastPic { constructor() { this.alias = 'FastPic'; this.origin = 'https://fastpic.ru'; this.type = ['jpeg', 'png', 'gif']; this.sizeLimit = 25; this.batchLimit = 30; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); if (this.batchLimit && images.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return new Promise((resolve, reject) => { const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; images.forEach(image => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; formData += 'Content-Disposition: form-data; name="file[]"; filename="' + image.name.toASCII() + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="uploading"\r\n\r\n'; formData += '1\r\n'; formData += '--' + boundary + '--\r\n'; GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/uploadmulti', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (/^\s*(?:Refresh)\s*:\s*(\d+);url=(\S+)\s*$/im.test(response.responseHeaders)) resolve(RegExp.$2); else { console.warn('FastPic.ru invalid response header:', response.responseHeaders); reject('invalid response header'); } }, onprogress: typeof progressHandler == 'function' ? progressHandler : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).then(resultUrl => globalFetch(resultUrl).then(response => { var thumbs = Array.from(response.document.querySelectorAll('div.picinfo > div.dCenter > a > img')).map(img => img.src); return Promise.all(Array.from(response.document.querySelectorAll('ul.codes-list > li:first-of-type > input')) .map((input, index) => globalFetch(input.value).then(response => ({ original: response.document.querySelector('img.image').src, thumb: thumbs[index], share: response.finalUrl, })))); console.warn(`FastPic.ru: not all images uploaded (${directLinks.length}/${images.length})`, response.finalUrl); return Promise.reject(`not all images uploaded (${directLinks.length}/${images.length})`); })); } } class NWCD { constructor() { this.alias = 'NotWhatCd'; this.whitelist = ['notwhat.cd']; this.upload.acceptFiles = true; } upload(files) { if (!Array.isArray(files)) return Promise.reject('invalid argument'); if (files.length <= 0) return Promise.reject('nothing to upload'); return NWCD.loadJS().then(upload => Promise.all(files.map(upload)).then(results => results.map(result => result.url))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return NWCD.loadJS().then(upload => Promise.all(urls.map(url => verifyImageUrl(url).then(upload).then(result => { if (typeof progressHandler == 'function') progressHandler(true); return result.url; })))); } static loadJS() { if (document.domain != 'notwhat.cd') return Promise.reject('uploadToImagehost not available'); return typeof uploadToImagehost == 'function' ? Promise.resolve(uploadToImagehost) : new Promise((resolve, reject) => { var imageUpload = document.createElement('script'); imageUpload.type = 'text/javascript'; imageUpload.src = '/static/functions/image_upload.js'; imageUpload.onload = evt => { if (typeof uploadToImagehost == 'function') resolve(uploadToImagehost); else reject('uploadToImagehost() not loaded'); // assertion fail }; imageUpload.onerror = evt => { reject('Script load error: ' + evt.message ) }; document.head.append(imageUpload); }); } } class Abload { constructor() { this.alias = 'Abload'; this.origin = 'https://abload.de'; this.types = ['bmp', 'bmp2', 'bmp3', 'gif', 'jpeg', 'png']; this.sizeLimit = 10; this.batchLimit = 20; if ((this.uid = GM_getValue('abload_uid')) === undefined) GM_setValue('abload_uid', ''); if ((this.password = GM_getValue('abload_password')) === undefined) GM_setValue('abload_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var id = 'o_' + (index + 1).toString().padStart(2, '0') + randomString(28).toLowerCase(), params = { name: id, // + image.type.replace('image/', '.'), chunk: 0, chunks: 1, }, formData = '--' + boundary + '\r\n'; Object.keys(params).forEach(field => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += params[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); Object.keys(session).forEach(field => { formData += 'Content-Disposition: form-data; name="' + field + '"\r\n\r\n'; formData += session[field] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: 'https://' + session.server + '.abload.de/calls/newUpload.php', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin + '/', }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status >= 200 && response.status < 400) resolve({ id: id, name: image.name }); else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }))).then(uploadMapping => { var formData = new URLSearchParams(Object.assign({}, session, { resize: 'none', rules: 'on', gallery: '', upload_mapping: JSON.stringify(uploadMapping).replace(/"/g, '\\"'), })); return globalFetch('https://' + session.server + '.abload.de/flashUploadFinished.php?server=' + session.server, { headers: { Referer: this.origin + '/' }, }, formData).then(Abload.resolveRedirect); })); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject('batch limit exceeded (' + this.batchLimit + ')'); return this.setSession().then(session => verifyImageUrls(urls).then(imageUrls => { var formData = {}; imageUrls.forEach((imageUrl, index) => { formData['img' + index] = imageUrl }); formData = new URLSearchParams(Object.assign(formData, session, { resize: 'none', rules: 'on', gallery: '', upload_mapping: JSON.stringify([]), })); return globalFetch('https://' + session.server + '.abload.de/flashUploadFinished.php?server=' + session.server, { headers: { Referer: this.origin + '/' }, timeout: imageUrls.length * rehostTimeout, }, formData).then(Abload.resolveRedirect); })); } static resolveRedirect(response) { var form = response.document.querySelector('form#weiter'); if (form == null) return Promise.reject(response.responseText); var formData = new FormData(form); formData = new URLSearchParams(formData); return globalFetch(form.action, { headers: { Referer: response.finalUrl } }, formData).then(response => Array.from(response.document.querySelectorAll('table.image_links > tbody > tr > td > input[type="text"]')) .filter(input => urlParser.test(input.value) && input.parentNode.previousElementSibling.textContent.startsWith('Dire')) .map(input => ({ original: input.value.trim(), thumb: input.value.trim().replace('/img/', '/thumb/'), share: input.value.trim().replace('/img/', '/image.php?img='), }))); } setSession() { return globalFetch(this.origin).then(response => { var session = { userID: randomUser(32) }; if (!/^Server\s*:\s*Abload\s+(\w+)\b/im.test(response.responseHeaders)) return Promise.reject('Invalid response header'); session.server = RegExp.$1; if (/\b(?:user_logged_in)\s*=\s*true\b/.test(response.responseText)) return session; if (!this.uid || !this.password) return session; var formData = new URLSearchParams({ name: this.uid, password: this.password }); return globalFetch(this.origin + '/login.php', { method: 'HEAD', headers: { 'Referer': this.origin }, }, formData).catch(reason => { console.warn(reason) }).then(response => session); function randomUser(length) { const possible = "abcdefABCDEF0123456789"; var text = ""; for (var i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } }); } } class Radikal { constructor() { this.alias = 'Radikal'; this.origin = 'https://radikal.ru'; this.sizeLimit = 40; if ((this.uid = GM_getValue('radikal_uid')) === undefined) GM_setValue('radikal_uid', ''); if ((this.password = GM_getValue('radikal_password')) === undefined) GM_setValue('radikal_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = { OriginalFileName: image.name, MaxSize: 99999, PrevMaxSize: 500, IsPublic: false, NeedResize: false, Rotate: 0, RotateMetadataRelative: false, }; Object.keys(params).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += params[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="File"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/Img/SaveImg2', responseType: 'json', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin + '/', 'Cookie': 'USER_ID=' + session.Id || '', }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.IsError) return reject(`${response.response.ErrorSrvMsg} (${response.response.Errors._allerrors_.join(' / ')})`); resolve({ original: response.response.Url, thumb: response.response.PublicPrevUrl, share: response.response.PrevPageUrl, }); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams({ OriginalFileName: imageUrl, MaxSize: 99999, PrevMaxSize: 500, IsPublic: false, NeedResize: false, Rotate: 0, RotateMetadataRelative: false, Url: imageUrl, }); return globalFetch(this.origin + '/Img/SaveImg2', { responseType: 'json', headers: { 'Referer': this.origin }, cookie: 'USER_ID=' + session.Id || '', timeout: urls.length * rehostTimeout, }, formData).then(response => { if (response.status < 200 || response.status >= 400) return Promise.reject(defaultErrorHandler(response)); if (response.response.IsError) return Promise.reject(`${response.response.ErrorSrvMsg} (${response.response.Errors._allerrors_.join(' / ')})`); if (typeof progressHandler == 'function') progressHandler(true); return { original: response.response.Url, thumb: response.response.PublicPrevUrl, share: response.response.PrevPageUrl, }; }); })))); } setSession() { return globalFetch(this.origin, { responseType: 'text' }).then(response => { var session = getUserInfo(response); if (!session) return Promise.reject('Invalida page format'); if (!session.IsAnonym) return session; if (!this.uid || !this.password) return session; var formData = new URLSearchParams({ Login: this.uid, Password: this.password, IsRemember: false, ReturnUrl: '/', }); return globalFetch(this.origin + '/Auth/Login', { responseType: 'json', headers: { 'Referer': this.origin }, }, formData).then(response => response.response.IsError ? session : globalFetch(this.origin, { responseType: 'text' }).then(response => getUserInfo(response) || session), reason => { console.warn(reason) }); }); function getUserInfo(response) { if (/\b(?:var\s+serverVm)\s*=\s*(\{.*\});$/m.test(response.responseText)) try { return JSON.parse(RegExp.$1).CommonUserData; } catch(e) { console.warn(e) } return null; } } } class SVGshare { constructor() { this.alias = 'SVGshare'; this.origin = 'https://svgshare.com'; this.types = ['svg+xml']; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(submitUrl => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = { name: image.name, submit: 'Share', }; Object.keys(params).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += params[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: submitUrl, headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, fetch: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); var domParser = new DOMParser; domParser.parseFromString(response.responseText, 'text/html') .querySelectorAll('ul#shares > li > input[type="text"]') .forEach(input => { if (/^(?:https?:\/\/.+\.svg)$/.test(input.value)) resolve(input.value) }); reject('image URL could not be found'); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } setSession() { return globalFetch(this.origin).then(response => { var form = response.document.getElementById('filereader'); return form != null && form.action || Promise.reject('Invalid document format'); }); } } class GeekPic { constructor() { this.alias = 'GeekPic'; this.origin = 'https://geekpic.net'; //this.types = []; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/ajax.php?PHPSESSID=' + randomString(26).toLowerCase(), headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //fetch: true, responseType: 'json', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (!response.response.success) return reject(response.response.msg); resolve(this.origin + response.response.img); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).then(imageUrlResolver))); } } class LightShot { constructor() { this.alias = 'LightShot'; this.origin = 'https://prntscr.com'; //this.types = []; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(userInfo => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="image"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload.php', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //fetch: true, responseType: 'json', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.status == 'success') resolve(response.response.data); else reject(response.response.status); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).then(imageUrlResolver)))); } setSession() { var params = { id: 1, jsonrpc: '2.0', method: 'get_userinfo', params: {}, }; return globalFetch('https://api.prntscr.com/v1/', { responseType: 'json' }, JSON.stringify(params)).then(response => { if (response.response.result.success) return response.response.result; // TODO: login return response.response.result; }); } } class ImageBan { constructor() { this.alias = 'ImageBan'; this.origin = 'https://imageban.ru'; this.types = ['jpeg', 'png', 'gif', 'webp']; this.sizeLimit = 10; this.batchLimit = 100; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="Filedata"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/up', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, responseType: 'json', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.files[0].error) return reject(response.response.files[0].error); resolve({ original: response.response.files[0].link, thumb: response.response.files[0].thumbs, share: response.response.files[0].piclink, }); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject(`batch limit exceeded (${this.batchLimit})`); return this.setSession().then(session => verifyImageUrls(urls).then(imageUrls => { var formData = new URLSearchParams(Object.assign({ u_url: imageUrls.join('\n'), }, session)); return globalFetch(this.origin + '/urlup', { headers: { 'Referer': this.origin }, timeout: imageUrls.length * rehostTimeout, }, formData).then(response => Array.from(response.document.querySelectorAll('div.container > div[align="left"] ~ div.row')).map(row => ({ original: row.querySelector('div.input-group > input[id^="g"]').value, thumb: row.querySelector(':scope > a > img').src, share: row.querySelector('div.input-group > input[id^="a"]').value, }))); })); } setSession() { return Promise.resolve({}); } } class PicaBox { constructor() { this.alias = 'PicaBox'; this.origin = 'https://picabox.ru'; //this.types = ['jpeg', 'png', 'gif', 'webp']; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; Object.keys(session).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += session[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="ImagesForm[imageFiles][]"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/image/load', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin + '/image/load', }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), fetch: true, cookie: '_csrf=' + session._csrf, onload: response => { if (response.status >= 200 && response.status < 400) resolve(this.extractLinks(response)); else reject(defaultErrorHandler(response)); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); }).then(results => results[0])))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject(`batch limit exceeded (${this.batchLimit})`); return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams(session); formData.set('ImagesForm[imageFiles][]', ''); formData.set('ImagesForm[file_url]', imageUrl); return globalFetch(this.origin + '/image/load', { responseType: 'text', fetch: true, headers: { 'Referer': this.origin + '/image/load' }, timeout: urls.length * rehostTimeout, cookie: '_csrf=' + session._csrf, }, formData).then(PicaBox.extractLinks.bind(this)).then(results => { if (typeof progressHandler == 'function') progressHandler(true); return results[0]; }); })))); } extractLinks(response) { var domParser = new DOMParser; return Promise.all(Array.from(domParser.parseFromString(response.responseText, 'text/html').querySelectorAll('input[name="url"]')) .map(input => imageUrlResolver(input.value).then(imgUrl => ({ original: imgUrl, thumb: this.origin + '/img_small/' + input.value.replace(/^.*\//, ''), share: input.value, })))); } setSession() { return globalFetch(this.origin + '/image/load').then(response => { var formData = response.document.querySelector('form[name="form_image"]'); if (formData == null) return Promise.reject('Invalid document format'); formData = new FormData(formData); var session = { }, val, it = formData.entries(); while (!(val = it.next()).done) session[val.value[0]] = val.value[1]; ['ImagesForm[file_url]', 'ImagesForm[imageFiles][]', 'imagesform-text_color-source'] .forEach(key => { delete session[key] }); return session; }); } } class PimpAndHost { constructor() { this.alias = 'PimpAndHost'; this.origin = 'https://pimpandhost.com'; this.types = ['jpeg', 'png', 'gif']; this.sizeLimit = 5 * 1000 / 2**10; this.batchLimit = 100; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = { fileId: `${index.toString().padStart(3, '0')}_${session.albumId}`, albumId: session.albumId, }; Object.keys(params).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += params[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="files"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/image/upload-file', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin + '/album/' + session.albumId, 'X-CSRF-Token': session['csrf-token'], }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), //fetch: true, responseType: 'json', onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); resolve({ original: 'https:' + response.response.files[0].image[0], thumb: 'https:' + response.response.files[0].image[180], share: response.response.files[0].pageUrl, }); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject(`batch limit exceeded (${this.batchLimit})`); return this.setSession().then(session => Promise.all(urls.map((url, index) => (() => { return['png', 'jpg', 'jpeg', 'jfif', 'gif'].some(ext => url.toLowerCase().endsWith('.' + ext)) ? verifyImageUrl(url) : imageHostHandlers.imgbb.rehost([url]).then(imgUrls => imgUrls[0]) .catch(reason => imageHostHandlers.jerking.rehost([url]).then(imgUrls => imgUrls[0])) .catch(reason => imageHostHandlers.pixhost.rehost([url]).then(imgUrls => imgUrls[0])) })().then(imageUrl => { var formData = new URLSearchParams({ url: imageUrl, field: `${index.toString().padStart(3, '0')}_${session.albumId}`, albumId: session.albumId, }); return globalFetch(this.origin + '/image/upload-by-url', { responseType: 'json', headers: { 'Referer': this.origin + '/album/' + session.albumId, 'X-CSRF-Token': session['csrf-token'], }, timeout: urls.length * rehostTimeout, }, formData).then(response => { if (response.response.status != 'ok') return Promise.reject(response.response.message); if (response.response.file.error) return Promise.reject(response.response.file.error.title); if (typeof progressHandler == 'function') progressHandler(true); return { original: 'https:' + response.response.file.image[0], thumb: 'https:' + response.response.file.image[180], share: response.response.file.pageUrl, }; }); })))); } setSession() { return globalFetch(this.origin).then(response => { var meta = response.document.querySelector('meta[name="csrf-token"][content]'); if (meta == null) return Promise.reject('Invalid document structure'); var session = { 'csrf-token': meta.content }; return globalFetch(this.origin + '/album/create-by-uploading', { headers: { 'X-CSRF-Token': session['csrf-token'] }, responseType: 'json', }).then(response => { session.albumId = response.response.albumId; return session; }); }); } } class ScreenCast { constructor() { this.alias = 'ScreenCast'; this.origin = 'https://www.screencast.com'; //this.types = []; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { } rehost(urls, progressHandler = null) { } setSession() { } } class GoogleAPI { constructor(scope) { this.origin = 'https://www.googleapis.com'; this.clientId = '241768952066-r0pojdg0l8m4nqr31psf8rb01btt43c4.apps.googleusercontent.com'; this.apiKey = 'lk9MZc7eSYzi6tDQ-H6jeC-2'; this.scope = scope; } setSession() { return this.isTokenValid() ? Promise.resolve(this.token) : (this.auth ? Promise.resolve(this.auth) : (typeof gapi == 'object' ? Promise.resolve(gapi) : new Promise((resolve, reject) => { var gApi = document.createElement('script'); gApi.type = 'text/javascript'; gApi.src = 'https://apis.google.com/js/api.js'; gApi.onload = evt => { if (typeof gapi == 'object') resolve(gapi); else reject('Google API loading error') }; gApi.onerror = evt => { reject('Script load error: ' + evt.message ) }; document.head.append(gApi); })).then(gapi => new Promise((resolve, reject) => gapi.load('client:auth2', { callback: () => { gapi.client.init({ clientId: this.clientId, //apiKey: this.apiKey, scope: this.scope, discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], }).then(() => { resolve(this.auth = gapi.auth2.getAuthInstance()) }, error => reject(JSON.stringify(error))); }, onerror: () => { reject('Google API loading error') }, })))).then(auth => auth.isSignedIn.get() ? auth.currentUser.get() : auth.signIn().catch(e => Promise.reject(e.error))) .then(user => (this.token = gapi.client.getToken())); } isTokenValid() { if (!this.token || typeof this.token != 'object' || !this.token.token_type || !this.token.access_token) return false; var now = new Date(); return this.token.expires_at >= now.getTime() + now.getTimezoneOffset() * 60000 + 30000; } } class GoogleDrive extends GoogleAPI { constructor() { super('https://www.googleapis.com/auth/drive.file'); this.alias = 'GoogleDrive'; //this.types = []; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(token => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', metadata = { 'name' : image.name, 'mimeType' : image.type, }; formData += 'Content-Disposition: form-data; name="metadata"\r\n'; formData += 'Content-Type: application/json; charset=UTF-8\r\n\r\n'; formData += JSON.stringify(metadata) + '\r\n'; formData += '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload/drive/v3/files?uploadType=multipart&fields=id,webContentLink', headers: { 'Content-Type': 'multipart/related; boundary=' + boundary, 'Content-Length': formData.length, 'Authorization': token.token_type + ' ' + token.access_token, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), responseType: 'json', onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); gapi.client.drive.permissions.create({ fileId: response.response.id, resource: { role: 'reader', type: 'anyone' }, }).execute(result => { if (result.id == 'anyoneWithLink') resolve(response.response.webContentLink.replace(/&.*$/i, '')); else reject('failed to enable sharing for this file'); }, error => { reject(JSON.stringify(error)) }); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } } class GooglePhotos extends GoogleAPI { constructor() { super('https://www.googleapis.com/auth/photoslibrary.sharing'); this.alias = 'GooglePhotos'; //this.types = []; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(token => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); // TODO })))); } } class DropBox { constructor() { this.alias = 'DropBox'; this.origin = 'https://www.dropbox.com'; //this.types = []; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { } rehost(urls, progressHandler = null) { } setSession() { } } class OneDrive { constructor() { this.alias = 'OneDrive'; this.origin = 'https://onedrive.live.com'; //this.types = []; //this.sizeLimit = 10; //this.batchLimit = 100; } upload(images, progressHandler = null) { } rehost(urls, progressHandler = null) { } setSession() { } } class VgyMe { constructor() { this.alias = 'Vgy.me'; this.origin = 'https://vgy.me'; this.types = ['jpeg', 'png', 'gif']; this.sizeLimit = 20; if ((this.userKey = GM_getValue('vgyme_user_key')) === undefined) GM_setValue('vgyme_user_key', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); if (this.sizeLimit > 0 && images.some(image => image.size > this.sizeLimit * 2**20)) return Promise.reject('size limit exceeded by one or more images'); return this.setSession().then(userKey => new Promise((resolve, reject) => { const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; images.forEach((image, index) => { formData += 'Content-Disposition: form-data; name="file[' + index + ']"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="userkey"\r\n\r\n'; formData += userKey + '\r\n'; formData += '--' + boundary + '--\r\n'; GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //fetch: true, responseType: 'json', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (!response.response.error) { if (Array.isArray(response.response.upload_list)) return resolve(response.response.upload_list); if (response.response.image) return resolve([response.response.image]); reject('Invalid response'); } else reject('Error'); }, onprogress: typeof progressHandler == 'function' ? progressHandler : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })); } setSession() { return this.userKey ? Promise.resolve(this.userKey) : Promise.reject('user key not configured (https://vgy.me/account/details#userkeys)'); } } class ImgURL { constructor() { this.alias = 'ImgURL'; this.origin = 'https://www.png8.com'; //this.origin = 'https://imgurl.org'; this.types = ['jpeg', 'png', 'gif', 'bmp']; } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload/localhost', //url: this.origin + '/upload/ftp', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //fetch: true, responseType: 'json', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.code != 200) return reject('status: ' + response.response.code); resolve({ original: response.response.url, thumb: response.response.thumbnail_url, share: this.origin + '/img/' + response.response.imgid, }); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } setSession() { return Promise.resolve({}); } } class Slowpoke { constructor() { this.alias = 'Slowpoke'; this.origin = 'https://slow.pics'; //this.types = ['jpeg', 'png', 'gif', 'bmp']; if ((this.uid = GM_getValue('slowpoke_uid')) === undefined) GM_setValue('slowpoke_uid', ''); if ((this.password = GM_getValue('slowpoke_password')) === undefined) GM_setValue('slowpoke_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(csrfToken => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const now = Date.now(), boundary = '--------WebKitFormBoundary-' + now.toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = { collectionName: new Date(now).toISOString(), public: false, thumbnailSize: 180, }; Object.keys(params).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += params[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); images.forEach((image, index) => { formData += 'Content-Disposition: form-data; name="images[' + index + '].name"\r\n\r\n'; formData += image.name + '\r\n'; formData += '--' + boundary + '\r\n'; formData += 'Content-Disposition: form-data; name="images[' + index + '].file"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary; if (index >= images.length - 1) formData += '--'; formData += '\r\n'; }); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/api/collection', headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, 'X-XSRF-TOKEN': csrfToken, }, data: formData, binary: true, //fetch: true, responseType: 'text', //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); var shareUrl = this.origin + '/c/' + response.responseText; console.log('Slowpoke upload gallery link:', shareUrl); imageUrlResolver(shareUrl).then(result => resolve(Array.isArray(result) ? result : [result])); }, onprogress: typeof progressHandler == 'function' ? progressHandler : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })); } setSession() { return globalFetch(this.origin + '/login').then(response => { if (response.finalUrl.includes(this.origin)) return response; if (!this.uid || !this.password) return globalFetch(this.origin); var token = response.document.querySelector('input[name="_csrf"][value]'); if (token == null) return Promise.reject('invlid page structure'); return new Promise((resolve, reject) => { var formData = new URLSearchParams({ _csrf: token.value, username: this.uid, password: this.password, }); GM_xmlhttpRequest({ method: 'POST', url: response.finalUrl, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': formData.toString().length, }, data: formData.toString(), onload: response => { if (['my', 'exit'].some(p => response.finalUrl.endsWith('/' + p))) { console.log('Slowpoke successfull login:', response.finalUrl); resolve(globalFetch(this.origin + '/login')); } else if (response.finalUrl.endsWith('/login?credentials')) reject('invalid userid or password'); else { console.warn('Slowpoke unhandled redirect:', response); reject('unexpected redirect: ' + response.finalUrl); } }, onerror: response => { reject(defaultErrorHandler(response)) }, }); }).catch(reason => { console.warn('Slowpoke login failed:', reason); return globalFetch(this.origin); }); }).then(response => { var token = response.document.querySelector('input[name="_csrf"][value]'); return token != null ? token.value : Promise.reject('invlid page structure (' + response.finalUrl + ')'); }); } } class FunkyIMG { constructor() { this.alias = 'FunkyIMG'; this.origin = 'https://funkyimg.com'; this.sizeLimit = 4; this.types = ['jpeg', 'png', 'gif', 'bmp', 'tiff']; if ((this.uid = GM_getValue('funkyimg_uid')) === undefined) GM_setValue('funkyimg_uid', ''); if ((this.password = GM_getValue('funkyimg_password')) === undefined) GM_setValue('funkyimg_password', ''); } upload(images, progressHandler = null) { if (!Array.isArray(images)) return Promise.reject('invalid argument'); images = images.filter(isSupportedType.bind(this)); if (images.length <= 0) return Promise.reject('nothing to upload'); return this.setSession().then(session => Promise.all(images.map((image, index) => new Promise((resolve, reject) => { if (this.sizeLimit > 0 && image.size > this.sizeLimit * 2**20) throw 'size limit exceeded: ' + image.name; const boundary = '--------WebKitFormBoundary-' + Date.now().toString(16).toUpperCase(); var formData = '--' + boundary + '\r\n', params = { wmText: '', wmPos: 'TOPRIGHT', wmLayout: 2, wmFontSize: 14, wmTransparency: 50, addInfoType: 'res', labelText: '', _images: image.name, }; Object.keys(params).forEach(key => { formData += 'Content-Disposition: form-data; name="' + key + '"\r\n\r\n'; formData += params[key] + '\r\n'; formData += '--' + boundary + '\r\n'; }); formData += 'Content-Disposition: form-data; name="images"; filename="' + image.name + '"\r\n'; formData += 'Content-Type: ' + image.type + '\r\n\r\n'; formData += image.data + '\r\n'; formData += '--' + boundary + '--\r\n'; if (typeof progressHandler == 'function') progressHandler(formData.length - image.size); GM_xmlhttpRequest({ method: 'POST', url: this.origin + '/upload/?' + session, headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': formData.length, 'Referer': this.origin, }, data: formData, binary: true, //timeout: Math.max(Math.ceil(formData.length / ulTimeFactor), 10000), //fetch: true, responseType: 'json', onload: response => { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.success) resolve(this.resultHandler(response.response.jid)); else reject('failure'); }, onprogress: typeof progressHandler == 'function' ? progress => progressHandler(progress, index) : undefined, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); })))); } rehost(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('invalid argument'); if (urls.length <= 0) return Promise.reject('nothing to rehost'); if (this.batchLimit && urls.length > this.batchLimit) return Promise.reject(`batch limit exceeded (${this.batchLimit})`); return this.setSession().then(session => Promise.all(urls.map(url => verifyImageUrl(url).then(imageUrl => { var formData = new URLSearchParams({ wmText: '', wmPos: 'TOPRIGHT', wmLayout: 2, wmFontSize: 14, wmTransparency: 50, addInfoType: 'res', labelText: '', url: imageUrl, }); return globalFetch(this.origin + '/upload/?' + session, { responseType: 'json', headers: { 'Referer': this.origin }, timeout: urls.length * rehostTimeout, }, formData).then(response => response.response.success ? this.resultHandler(response.response.jid) : Promise.reject('failure')); })))); } resultHandler(jid) { return new Promise((resolve, reject) => { //var queries = 0; check.call(this); function check() { globalFetch(this.origin + '/upload/check/' + jid + '?_=' + Date.now(), { headers: { 'Referer': this.origin }, responseType: 'json', }).then(response => { //++queries; if (response.response.success) try { //console.debug('FunkyIMG queries to success:', queries, jid); var dom = domParser.parseFromString(response.response.bit, 'text/html'); resolve({ original: dom.querySelector('ul > li:nth-of-type(2) > input').value.trim(), thumb: dom.querySelector('ul > li:nth-of-type(2) > input').value.trim().replace('/i/', '/p/'), share: dom.querySelector('ul > li:nth-of-type(1) > input').value.trim(), }); } catch(e) { reject(e) } else setTimeout(check.bind(this), 250); }, reject); } }); } setSession() { return Promise.resolve('fileapi' + Date.now()); } } function singleImageGetter(results) { if (!Array.isArray(results)) throw 'Invalid result format'; if (results.length <= 0) return null; if (typeof results[0] == 'string' && urlParser.test(results[0])) return results[0]; if (typeof results[0] == 'object' && urlParser.test(results[0].original)) return results[0].original; throw 'Invalid result format'; } var imageHostHandlers = { 'abload' : new Abload, 'catbox': new Catbox, //'dropbox': new DropBox, 'fastpic': new FastPic, 'funkyimg': new FunkyIMG, 'geekpic': new GeekPic, 'gifyu': new Chevereto('gifyu.com', 'Gifyu', ['jpeg', 'png', 'gif', 'bmp', 'webp'], 50, 100), 'googledrive': new GoogleDrive, //'googlephotos': new GooglePhotos, 'imageban': new ImageBan, 'imagevenue': new ImageVenue, 'imgbb': new Chevereto('imgbb.com', 'ImgBB', ['jpeg', 'png', 'bmp', 'gif', 'webp', 'tiff', 'heic', 'heif'], 32, 32, undefined, 'https://api.imgbb.com/1', 'image', 'data'), 'imgbox': new ImgBox, 'imgur': new Imgur, 'imgurl': new ImgURL, 'jerking': new Chevereto('jerking.empornium.ph', 'Jerking', ['jpeg', 'png', 'bmp', 'gif', 'webp'], 5), 'lightshot': new LightShot, 'nwcd' : new NWCD, //'onedrive': new OneDrive, 'picabox': new PicaBox, 'pimpandhost': new PimpAndHost, 'pixhost': new PixHost, 'postimage': new PostImage, 'ptpimg': new PTPimg, 'radikal': new Radikal, //'screencast': new ScreenCast, 'slowpoke': new Slowpoke, 'svgshare': new SVGshare, 'vgyme': new VgyMe, 'z4a': new Chevereto('z4a.net', 'Z4A', ['jpeg', 'png', 'bmp', 'gif'], 50), }; var siteWhitelists = { 'notwhat.cd': ['nwcd'], }; var siteBlacklists = { 'passthepopcorn.me': ['imgbox', 'postimage', 'imgur', 'tinypic', 'imageshack', 'imagebam'], }; class ImageHostManager { constructor(messageHandler = null, UlHostList = undefined, rhHostList = undefined) { this.messageHandler = messageHandler; if (UlHostList) this.buildUploadChain(UlHostList); else this.ulHostChain = []; if (rhHostList) this.buildRehostChain(rhHostList); else this.rhHostChain = []; } processLists(alias) { alias = alias.replace(nonWordStripper, '').toLowerCase(); return (!Array.isArray(siteWhitelists[document.domain]) || siteWhitelists[document.domain].some(whiteAlias => alias == whiteAlias.toLowerCase())) && (!Array.isArray(siteBlacklists[document.domain]) || siteBlacklists[document.domain].every(blackAlias => alias != blackAlias.toLowerCase())); } buildUploadChain(list) { this.ulHostChain = (Array.isArray(list) ? list : typeof list == 'string' ? list.split(/\s*[\,\;\|\/]\s*/) : []) .filter(ImageHostManager.prototype.processLists.bind(this)).map(alias => imageHostHandlers[alias.toLowerCase()]) .filter(handler => typeof handler == 'object' && typeof handler.upload == 'function' && (!Array.isArray(handler.whitelist) || handler.whitelist.includes(document.domain)) && (!Array.isArray(handler.blacklist) || !handler.blacklist.includes(document.domain))); console.debug('Local upload hosts for ' + document.domain + ':', this.ulHostChain.map(handler => handler.alias).join(', ')); } buildRehostChain(list) { this.rhHostChain = (Array.isArray(list) ? list : typeof list == 'string' ? list.split(/\s*[\,\;\|\/]\s*/) : []) .filter(ImageHostManager.prototype.processLists.bind(this)).map(alias => imageHostHandlers[alias.toLowerCase()]) .filter(handler => typeof handler == 'object' && typeof handler.rehost == 'function' && (document.domain != 'redacted.ch' || handler.alias.toLowerCase() == 'ptpimg') && (!Array.isArray(handler.whitelist) || handler.whitelist.includes(document.domain)) && (!Array.isArray(handler.blacklist) || !handler.blacklist.includes(document.domain))); console.debug('Remote upload hosts for ' + document.domain + ':', this.rhHostChain.map(handler => handler.alias).join(', ')); } uploadImages(files, progressHandler = null) { if (!Array.isArray(this.ulHostChain) || this.ulHostChain.length <= 0) return Promise.reject('No hosts where to upload'); if (typeof files != 'object') return Promise.reject('Invalid argument'); if (!Array.isArray(files)) files = Array.from(files); //if (files.length > 1) files.push(files.shift()); files = files.filter(file => file instanceof File && file.size > 0 && (!file.type || file.type.startsWith('image/'))); if (files.length <= 0) return Promise.reject('Nothing to upload'); return Promise.all(files.map(file => new Promise(function(resolve, reject) { var reader = new FileReader(); reader.onload = function() { if (reader.result.length != file.size) console.warn(`FileReader: binary string read length mismatch (${reader.result.length} ≠ ${file.size})`); resolve({ name: file.name, type: file.type, size: reader.result.length, data: reader.result }); }; reader.onerror = reader.ontimeout = function() { reject(`FileReader error (${file.name})`) }; reader.readAsBinaryString(file); }))).then(images => { return uploadInternal.call(this); function uploadInternal(hostIndex = 0) { return hostIndex >= 0 && hostIndex < this.ulHostChain.length ? (() => { if (!files.every(isSupportedType.bind(this.ulHostChain[hostIndex]))) return Promise.reject('one or more files of unsupported format'); if (this.ulHostChain[hostIndex].sizeLimit > 0 && files.some(file => file.size > this.ulHostChain[hostIndex].sizeLimit * 2**20)) return Promise.reject(`one or more images exceed size limit (${this.ulHostChain[hostIndex].sizeLimit}MiB)`); // if (this.ulHostChain[hostIndex].batchLimit && files.length > this.ulHostChain[hostIndex].batchLimit) // return Promise.reject(`batch limit exceeded (${this.ulHostChain[hostIndex].batchLimit})`); if (typeof progressHandler == 'function') { progressHandler(hostIndex, null); var _progressHandler = (param = null, index = undefined) => progressHandler(hostIndex, param, index); } return this.ulHostChain[hostIndex].upload(this.ulHostChain[hostIndex].upload.acceptFiles ? files : images, _progressHandler); })().catch(reason => { console.warn('Upload to', this.ulHostChain[hostIndex].alias, 'failed:', reason); var msg = `Upload to ${this.ulHostChain[hostIndex].alias} failed (${reason})`; if (++hostIndex < this.ulHostChain.length) { if (typeof this.messageHandler == 'function') this.messageHandler(`${msg}, falling back to ${this.ulHostChain[hostIndex].alias}`); return uploadInternal.call(this, hostIndex); } if (typeof this.messageHandler == 'function') this.messageHandler(msg); return Promise.reject('Upload failed to all hosts'); }) : Promise.reject(`Host index out of bounds (${hostIndex})`); } }); } rehostImages(urls, progressHandler = null) { if (!Array.isArray(urls)) return Promise.reject('Invalid argument'); urls = urls.filter(url => urlParser.test(url)); if (urls.length <= 0) return Promise.reject('Nothing to rehost'); if (!Array.isArray(this.rhHostChain) || this.rhHostChain.length <= 0) return Promise.resolve(urls); if (testRemoteSizes) var start = Date.now(); return (testRemoteSizes ? Promise.all(urls.map(url => getRemoteFileSize(url).catch(reason => undefined))) : Promise.resolve('Size tests skipped')).then(lengths => { if (testRemoteSizes) console.debug('Size analysis time:', (Date.now() - start) / 1000, 's'); try { var h2 = urls.map(url => new URL(url).hostname) } catch(e) { console.error('Assertion failed: ' + e) } return rehostInternal.call(this); function rehostInternal(hostIndex = 0) { if (hostIndex < 0 || hostIndex >= this.rhHostChain.length) return Promise.reject(`Host index out of bounds (${hostIndex})`); var h1 = this.rhHostChain[hostIndex].hostName; if (!h1) try { h1 = new URL(this.rhHostChain[hostIndex].origin).hostname } catch(e) { h1 = null } if (h1 && Array.isArray(h2) && h2.every(h2 => h2.includes(h1) || h1.includes(h2))) return Promise.resolve(urls); // if (this.rhHostChain[hostIndex].batchLimit && urls.length > this.rhHostChain[hostIndex].batchLimit) // return Promise.reject('batch limit exceeded (' + this.rhHostChain[hostIndex].batchLimit + ')'); if (this.rhHostChain[hostIndex].sizeLimit > 0 && Array.isArray(lengths) && !lengths.every(length => !length || length <= this.rhHostChain[hostIndex].sizeLimit * 2**20)) return Promise.reject(`one or more images exceed size limit (${this.rhHostChain[hostIndex].sizeLimit}MiB)`); if (typeof progressHandler == 'function') { progressHandler(hostIndex, false); var _progressHandler = (param = true) => progressHandler(hostIndex, param); } return this.rhHostChain[hostIndex].rehost(urls, _progressHandler).catch(reason => { console.warn('Rehost to', this.rhHostChain[hostIndex].alias, 'failed:', reason); var msg = `Rehost to ${this.rhHostChain[hostIndex].alias} failed (${reason})`; if (++hostIndex < this.rhHostChain.length) { if (typeof this.messageHandler == 'function') this.messageHandler(`${msg}, falling back to ${this.rhHostChain[hostIndex].alias}`); return rehostInternal.call(this, hostIndex); } if (typeof this.messageHandler == 'function') this.messageHandler(msg); return Promise.reject('Rehost failed to all hosts'); }); } }); } } function urlResolver(url) { if (!urlParser.test(url)) return Promise.reject('Invalid URL:\n\n' + url); try { if (!(url instanceof URL)) url = new URL(url) } catch(e) { return Promise.reject(e) } switch (url.hostname) { case 'rutracker.org': if (url.pathname != '/forum/out.php') break; return globalFetch(url, { method: 'HEAD' }).then(response => urlResolver(response.finalUrl)); case 'www.anonymz.com': case 'anonymz.com': case 'anonym.to': case 'dereferer.me': var resolved = decodeURIComponent(url.search.slice(1)); return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver(); // case 'reho.st': // resolved = (url.pathname + url.search + url.hash).slice(1); // if (/\b(?:https?):\/\/(?:\w+\.)*(?:discogs\.com|omdb\.org)\//i.test(resolved)) break; // return urlParser.test(resolved) ? urlResolver(resolved) : genericResolver(); // URL shorteners case 'tinyurl.com': case 'bit.ly': case 'j.mp': case 't.co': case 'apple.co': case 'flic.kr': case 'rebrand.ly': case 'b.link': case 't2m.io': case 'zpr.io': case 'yourls.org': case 'ibn.im': return genericResolver(); } if (/\b(?:goo\.gl)$/i.test(url.hostname)) return genericResolver(); return Promise.resolve(url.href); function genericResolver() { return globalFetch(url).then(function(response) { var redirect = response.document.querySelector('meta[http-equiv="refresh"]'); if (redirect != null && (redirect = redirect.content.replace(/^.*?\b(?:URL)\s*=\s*/i, '')) != url.href || /^ *(?:Location) *: *(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$1) != url.href || /^ *(?:Refresh) *: *(\d+); *url=(\S+) *$/im.test(response.responseHeaders) && (redirect = RegExp.$2) != url.href || (redirect = response.finalUrl) != url.href) return urlResolver(redirect); return Promise.resolve(url.href); }); } } function verifyImageUrl(url) { return urlResolver(url).then(function(url) { return new Promise(function(resolve, reject) { var img = new Image(); img.onload = load => { resolve(url) }; img.onerror = function(error) { if (img.src.includes('?')) img.src = url.replace(/\?.*?(?=\#|$)/, ''); else reject('not valid image: ' + url); }; img.ontimeout = timeout => { reject('image load timed out:\n\n' + url) }; img.src = url; }); }); } function verifyImageUrls(urls) { return Array.isArray(urls) ? Promise.all(urls.map(verifyImageUrl)) : Promise.reject('argument not an array'); } function googlePhotosResolver(url) { return globalFetch(url).then(function(response) { var result; response.document.querySelectorAll('body > script[nonce]').forEach(function(script) { if (result) return; if (!/^(?:AF_initDataCallback)\(\{\s*key\s*:\s*'ds:(\d+)'.+\b(?:data:function)\(\)\s*\{\s*(?:return)\s*(\[[\S\s]+?\])\s*\}\}\);$/ .test(script.text)) return; var AF_initDataCallback = eval(RegExp.$2); if (AF_initDataCallback.length == 14 && Array.isArray(AF_initDataCallback[0])) try { if (urlParser.test(AF_initDataCallback[0][1][0])) result = AF_initDataCallback[0][1][0] + '=s0'; } catch(e) { console.warn(e, AF_initDataCallback) } else if (AF_initDataCallback.length == 6 && Array.isArray(AF_initDataCallback[1])) try { result = AF_initDataCallback[1].map(photo => photo[1][0] + '=s0'); } catch(e) { console.warn(e, AF_initDataCallback) } }); return result || Promise.reject('No image content for this URL'); }); } function pinterestResolver(url) { return globalFetch(url).then(function(response) { var initialState = response.document.querySelector('script#initial-state'); if (initialState != null) try { initialState = JSON.parse(initialState.text); let images = Object.keys(initialState.pins).map(pin => initialState.pins[pin].images.orig.url); if (images.length == 1) return images[0]; else if (images.length > 1) return images; let boards = Object.keys(initialState.boards.content); if (boards.length > 0) { return Promise.all(boards.map(function(board) { var params = new URLSearchParams({ source_url: response.finalUrl, data: JSON.stringify({ options: { board_id: initialState.boards.content[board].id, board_url: initialState.boards.content[board].url, } }), _: Date.now(), }); return globalFetch(url.origin + '/resource/BoardFeedResource/get/?' + params, { responseType: 'json', headers: { Referer: response.finalUrl }, }).then(function(response) { if (response.response.resource_response.status != 'success') { console.warn('Pinterest:', response.response.resource_response, response.finalUrl); return Promise.reject('Pinterest: ' + response.response.resource_response.status); } return response.response.resource_response.data.filter(it => it.type == 'pin').map(it => it.images.orig.url); }); })); } } catch(e) { console.warn(e, initialState) } return Promise.reject('No title image for this URL'); }); } function _500pxUrlHandler(path) { return globalFetch('https://api.500px.com/v1/' + path + '&image_size[]=4096', { responseType: 'json' }).then(function(response) { var results = Object.keys(response.response.photos).map(id => response.response.photos[id].image_url[0]); return results.length == 1 ? results[0] : results.length > 1 ? results : Promise.reject('No image content found on this UIRL'); }); } function pxhereCollectionResolver(url) { if (!/\/collection\/(\d+)\b/i.test(url.pathname)) return Promise.reject('Invalid URL'); var collectionId = parseInt(RegExp.$1); return new Promise(function(resolve, reject) { var domParser = new DOMParser(), photos = []; loadPage(); function loadPage(page = 1) { GM_xmlhttpRequest({ method: 'GET', url: `https://pxhere.com/en/collection/${collectionId}?page=${page}&format=json`, responseType: 'json', onload: function(response) { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response._msg != 'success') return reject(response.response._msg); if (!response.response.data) return resolve(photos); var dom = domParser.parseFromString(response.response.data, 'text/html'); Array.prototype.push.apply(photos, Array.from(dom.querySelectorAll('div.item > a')).map(a => a.pathname)); loadPage(page + 1); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); } }).then(urls => Promise.all(urls.map(url => imageUrlResolver('https://pxhere.com' + url)))); } function unsplashCollectionResolver(url) { if (!/\/collections\/(\d+)\b/i.test(url.pathname)) return Promise.reject('Invalid URL'); var collectionId = parseInt(RegExp.$1); return new Promise(function(resolve, reject) { var urls = []; loadPage(); function loadPage(page = 1) { GM_xmlhttpRequest({ method: 'GET', url: `https://unsplash.com/napi/collections/${collectionId}/photos?page=${page}&per_page=999`, responseType: 'json', onload: function(response) { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); if (response.response.length <= 0) return resolve(urls); Array.prototype.push.apply(urls, response.response.map(photo => photo.urls.raw.replace(/\?.*$/, ''))); loadPage(page + 1); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); } }); } function pexelsCollectionResolver(url) { if (!/\/collections\/([\w\-]+)\//i.test(url.pathname)) return Promise.reject('Invalid URL'); var collectionId = RegExp.$1; return new Promise(function(resolve, reject) { var domParser = new DOMParser, urls = []; loadPage(); function loadPage(page = 1) { GM_xmlhttpRequest({ method: 'GET', url: `https://www.pexels.com/collections/${collectionId}/?format=html&page=${page}`, onload: function(response) { if (response.status < 200 || response.status >= 400) return reject(defaultErrorHandler(response)); var dom = domParser.parseFromString(response.responseText, 'text/html'); var photos = dom.querySelectorAll('article.photo-item > a.js-photo-link'); if (photos.length <= 0) return resolve(urls); Array.prototype.push.apply(urls, Array.from(photos).map(a => a.pathname)); loadPage(page + 1); }, onerror: response => { reject(defaultErrorHandler(response)) }, ontimeout: response => { reject(defaultTimeoutHandler(response)) }, }); } }).then(urls => Promise.all(urls.map(url => imageUrlResolver('https://www.pexels.com' + url)))); } function getRemoteFileSize(url) { return new Promise(function(resolve, reject) { var imageSize, abort = GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onreadystatechange: function(response) { if (imageSize || response.readyState < XMLHttpRequest.HEADERS_RECEIVED || !/^Content-Length:\s*(\d+)\b/im.test(response.responseHeaders)) return; if (!(imageSize = parseInt(RegExp.$1))) return; resolve(imageSize); abort.abort(); }, onload: function(response) { // fail-safe if (imageSize) return; if (response.status >= 200 && response.status < 400) resolve(response.responseText.length /*response.response.byteLength*/); else reject(new Error('Image not accessible')); }, onerror: response => { reject('Image not accessible') }, ontimeout: response => { reject('Image not accessible') }, }); }); } function formattedSize(size) { return size < 1024**1 ? Math.round(size) + ' B' : size < 1024**2 ? (Math.round(size * 10 / 2**10) / 10) + ' KiB' : size < 1024**3 ? (Math.round(size * 100 / 2**20) / 100) + ' MiB' : size < 1024**4 ? (Math.round(size * 100 / 2**30) / 100) + ' GiB' : size < 1024**5 ? (Math.round(size * 100 / 2**40) / 100) + ' TiB' : (Math.round(size * 100 / 2**50) / 100) + ' PiB'; } function voidDragHandler0(evt) { return false } function inputDropHandler(evt) { return !evt.shiftKey ? inputDataHandler(evt, evt.dataTransfer) : true } function inputPasteHandler(evt) { return inputDataHandler(evt, evt.clipboardData) } function inputClear(evt) { evt.target.value = ''; coverPreview(evt.target, null); } function setInputHandlers(node) { node.ondragover = voidDragHandler0; node.ondblclick = inputClear; node.ondrop = inputDropHandler; node.onpaste = inputPasteHandler; node.placeholder = 'Paste/drop local or remote image'; } function setTextAreahandlers(node) { node.ondragover = voidDragHandler0; node.ondrop = textAreaDropHandler; node.onpaste = textAreaPasteHandler; }; function randomString(length) { const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; var text = ""; for (var i = 0; i < length; ++i) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; } function isSupportedType(image) { if (!this || typeof this != 'object' || !image || typeof image != 'object') return false; if (!Array.isArray(this.types) || this.types.length <= 0) return !image.type || image.type.startsWith('image/'); return this.types.some(function(mimeType) { if (!mimeType) return false; if (image.type) return image.type == 'image/' + mimeType; return image.name && testExt([mimeType]); function testExt(extensions) { return extensions.some(ext => image.name.toLowerCase().endsWith('.' + ext)) } }); }