├── .eslintignore ├── .jshintrc ├── reddit-bot ├── .jshintrc ├── .eslintrc.json ├── package.json ├── comment-bot.js ├── blacklist.json └── service-bot.js ├── lib ├── shim.js ├── fetch_shim.js ├── aes1.patch ├── libs.txt └── cryptojs_aes.js ├── resources ├── logo.png ├── logo_256.png ├── logo_40.png ├── logo_48.png ├── logo_64.png ├── logo_96.png ├── disabled_40.png ├── disabled_48.png ├── disabled_64.png ├── disabled_96.png ├── imu_opera_banner.png ├── imu_opera_banner.xcf ├── imu_opera_banner_transparent.png └── imu.svg ├── tools ├── watch_tsc.sh ├── get_old_userscript.sh ├── rules_template.js ├── update_signed_xpi.sh ├── update_site_userscript.sh ├── fetch_libs.sh ├── bigimage_template.js ├── update_sitesnum.js ├── gen_minified.js ├── util.js ├── build_libs.sh ├── gen_po.js ├── update_strings.js ├── patch_libs.js ├── update_from_po.js ├── gen_rules_js.js └── remcomments.js ├── .gitignore ├── extension ├── updates.xml ├── options.html ├── welcome.html ├── popup.html ├── popup.js └── welcome.js ├── .github_old └── workflows │ └── build.yml ├── package.json ├── manifest.json ├── src └── module.d.ts ├── userscript.meta.js ├── tsconfig.json ├── docs └── pt │ ├── README.pt-BR.md │ └── CONTRIBUTING.pt-BR.md ├── README.md ├── LICENSE.txt └── CONTRIBUTING.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/userscript.user.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /reddit-bot/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /lib/shim.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined') 2 | module.exports = lib_export; 3 | -------------------------------------------------------------------------------- /resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo.png -------------------------------------------------------------------------------- /resources/logo_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo_256.png -------------------------------------------------------------------------------- /resources/logo_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo_40.png -------------------------------------------------------------------------------- /resources/logo_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo_48.png -------------------------------------------------------------------------------- /resources/logo_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo_64.png -------------------------------------------------------------------------------- /resources/logo_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/logo_96.png -------------------------------------------------------------------------------- /resources/disabled_40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/disabled_40.png -------------------------------------------------------------------------------- /resources/disabled_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/disabled_48.png -------------------------------------------------------------------------------- /resources/disabled_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/disabled_64.png -------------------------------------------------------------------------------- /resources/disabled_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/disabled_96.png -------------------------------------------------------------------------------- /resources/imu_opera_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/imu_opera_banner.png -------------------------------------------------------------------------------- /resources/imu_opera_banner.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/imu_opera_banner.xcf -------------------------------------------------------------------------------- /resources/imu_opera_banner_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsniyg/maxurl/HEAD/resources/imu_opera_banner_transparent.png -------------------------------------------------------------------------------- /reddit-bot/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6 4 | }, 5 | "env": { 6 | "node": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tools/watch_tsc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd "$(dirname "$(readlink -f "$0")")/.." 4 | 5 | # tsc --watch segfaults after a few updates 6 | while true; do 7 | npx tsc --watch 8 | sleep 1 # eases ^C 9 | done 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | .env*.json 3 | node_modules 4 | *~ 5 | site/ 6 | #extension.xpi 7 | extension_source.zip 8 | *.pem 9 | #*.crx 10 | maxurl.zip 11 | *.pub 12 | *.sig 13 | build/tsout.js 14 | standalone_config.json 15 | -------------------------------------------------------------------------------- /tools/get_old_userscript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd "`dirname "$0"`/.." 4 | 5 | LASTTAG="`git tag | sort -V | tail -n1`" 6 | curl 'https://raw.githubusercontent.com/qsniyg/maxurl/'"$LASTTAG"'/userscript_smaller.user.js' -o olduserscript 7 | -------------------------------------------------------------------------------- /tools/rules_template.js: -------------------------------------------------------------------------------- 1 | var __IMU_GETBIGIMAGE__ = function(shared_variables) { 2 | // imu:shared_variables 3 | 4 | return { 5 | bigimage: function(src, options) { 6 | // imu:bigimage 7 | }, 8 | nonce: __IMU_NONCE__ // imu:nonce = __IMU_NONCE__ 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /extension/updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /reddit-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maximagebot", 3 | "version": "0.0.1", 4 | "main": "bot.js", 5 | "dependencies": { 6 | "dotenv": "^5.0.1", 7 | "iconv-lite": "^0.5.0", 8 | "js-yaml": "^3.13.1", 9 | "mongodb": "^3.7.4", 10 | "node-cache": "^4.2.0", 11 | "probe-image-size": "git+https://github.com/qsniyg/probe-image-size.git", 12 | "request": "^2.88.0", 13 | "snoostorm": "0.0.5", 14 | "snoowrap": "^1.21.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tools/update_signed_xpi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "`dirname "$0"`/.." 4 | 5 | which jq >/dev/null 2>&1 6 | if [ $? -ne 0 ]; then 7 | echo jq not installed 8 | exit 1 9 | fi 10 | 11 | #URL=`curl 'https://addons.mozilla.org/api/v5/addons/addon/image-max-url/versions/' | jq -r '.results[0].files[0].url'` 12 | URL=`curl 'https://addons.mozilla.org/api/v5/addons/addon/image-max-url/versions/' | jq -r '.results[0].file.url'` 13 | wget $URL -O build/ImageMaxURL_signed.xpi 14 | -------------------------------------------------------------------------------- /.github_old/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | paths: 6 | - src/userscript.ts 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - run: | 13 | npm install 14 | npm run build 15 | - uses: stefanzweifel/git-auto-commit-action@v5 16 | with: 17 | commit_message: Update userscript 18 | file_pattern: userscript.user.js 19 | -------------------------------------------------------------------------------- /tools/update_site_userscript.sh: -------------------------------------------------------------------------------- 1 | cd "`dirname "$0"`"/../ 2 | 3 | if [ ! -d site ]; then 4 | echo "Site directory doesn't exist" 5 | exit 1 6 | fi 7 | 8 | cd site 9 | 10 | if [ ! -z "`git diff --staged`" ]; then 11 | echo "Site has staged changes, aborting." 12 | exit 1 13 | fi 14 | 15 | cp ../userscript_smaller.user.js . 16 | 17 | git add userscript_smaller.user.js 18 | git diff --staged 19 | echo "Continue? (CTRL+C to exit)" 20 | read 21 | git commit -m "Update userscript" 22 | git push origin gh-pages 23 | -------------------------------------------------------------------------------- /tools/fetch_libs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "`dirname "$0"`" 4 | cd ../lib 5 | 6 | if [ ! -d orig ]; then 7 | mkdir orig 8 | fi 9 | cd orig 10 | 11 | cat ../libs.txt | sed -e '/^ *$/d' -e '/^ *#/d' | while read line; do 12 | URL=`echo "$line" | sed 's/^\([^ ]*\).*$/\1/g'` 13 | 14 | NAME= 15 | echo "$line" | grep ' = ' >/dev/null 2>&1 16 | if [ $? -eq 0 ]; then 17 | NAME=`echo "$line" | sed 's/.* = *\([^ ]*\) *$/\1/g'` 18 | fi 19 | 20 | if [ -z "$NAME" ]; then 21 | NAME=`basename "$URL"` 22 | fi 23 | 24 | wget "$URL" -O "$NAME" 25 | done 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-max-url", 3 | "version": "2025.12.1", 4 | "author": "qsniyg", 5 | "license": "Apache-2.0", 6 | "private": true, 7 | "devDependencies": { 8 | "concurrently": "^7.0.0", 9 | "crx3": "^1.1.3", 10 | "typescript": "~5.3" 11 | }, 12 | "scripts": { 13 | "watch-tsc": "./tools/watch_tsc.sh", 14 | "build-tsc": "(npx tsc || exit 0)", 15 | "watch-js": "node ./tools/remcomments.js", 16 | "build-js": "node ./tools/remcomments.js userscript.user.js nowatch", 17 | "watch": "npx concurrently -k \"npm:watch-tsc\" \"npm:watch-js\"", 18 | "build": "npm run build-tsc && npm run build-js", 19 | "build-libs": "./tools/build_libs.sh", 20 | "package": "./tools/package_extension.sh", 21 | "package-release": "./tools/package_extension.sh release" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /extension/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | Loading... 15 |
16 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/fetch_shim.js: -------------------------------------------------------------------------------- 1 | // somewhat inspired by https://github.com/developit/unfetch/blob/master/src/index.mjs 2 | var fetch = function(url, options) { 3 | if (!options) options = {}; 4 | 5 | return new Promise(function(resolve, reject) { 6 | //console.log("fetching", url, options); 7 | var xhr = new XMLHttpRequest(); 8 | 9 | var get_response = function() { 10 | // Response is needed for wasm 11 | // this shim is not trying to support fetch for browsers that don't support it, but rather to wrap XHR 12 | return new Response(xhr.response, { 13 | status: xhr.status, 14 | statusText: xhr.statusText, 15 | // todo: headers 16 | }); 17 | }; 18 | 19 | xhr.open(options.method || "GET", url, true); 20 | 21 | xhr.responseType = "blob"; 22 | 23 | xhr.onload = function() { 24 | // todo: get headers 25 | resolve(get_response()); 26 | }; 27 | xhr.onerror = reject; 28 | 29 | xhr.withCredentials = options.credentials === "include"; 30 | 31 | for (var header in options.headers) { 32 | xhr.setRequestHeader(header, options.headers[header]); 33 | } 34 | 35 | xhr.send(options.body || null); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/aes1.patch: -------------------------------------------------------------------------------- 1 | --- aes.orig.js 2020-05-28 22:09:23.016828763 -0700 2 | +++ aes.orig1.js 2020-05-28 22:07:48.952696598 -0700 3 | @@ -757,22 +757,24 @@ 4 | var padCount = 0; 5 | var padByte = -1; 6 | var blockSize = 16; 7 | - for (var i = data.length - 1; i >= data.length-1 - blockSize; i--) { 8 | - if (data[i] <= blockSize) { 9 | - if (padByte == -1) 10 | - padByte = data[i]; 11 | - if (data[i] != padByte) { 12 | - padCount = 0; 13 | + if (data.length > 16) { 14 | + for (var i = data.length - 1; i >= data.length-1 - blockSize; i--) { 15 | + if (data[i] <= blockSize) { 16 | + if (padByte == -1) 17 | + padByte = data[i]; 18 | + if (data[i] != padByte) { 19 | + padCount = 0; 20 | + break; 21 | + } 22 | + padCount++; 23 | + } else 24 | break; 25 | - } 26 | - padCount++; 27 | - } else 28 | - break; 29 | - if (padCount == padByte) 30 | - break; 31 | + if (padCount == padByte) 32 | + break; 33 | + } 34 | + if (padCount > 0) 35 | + data.splice(data.length - padCount, padCount); 36 | } 37 | - if (padCount > 0) 38 | - data.splice(data.length - padCount, padCount); 39 | } 40 | /* 41 | * END MODE OF OPERATION SECTION 42 | -------------------------------------------------------------------------------- /lib/libs.txt: -------------------------------------------------------------------------------- 1 | https://raw.githubusercontent.com/Stuk/jszip/v3.6.0/dist/jszip.js 2 | https://raw.githubusercontent.com/escolarea-labs/slowaes/f53404fb0aba47fcd336ae32623033bffa1dab41/js/aes.js = slowaes.js 3 | https://raw.githubusercontent.com/brix/crypto-js/release-3.1.2/build/rollups/aes.js = cryptojs_aes.js 4 | https://raw.githubusercontent.com/peterolson/BigInteger.js/v1.6.51/BigInteger.min.js 5 | https://raw.githubusercontent.com/NeilFraser/JS-Interpreter/824d8331cc4b72c09ca3e0f1b3a6ed7914cf16c6/acorn_interpreter.js 6 | 7 | # Built versions for mux.js aren't available on github 8 | https://unpkg.com/mux.js@5.7.0/dist/mux.js 9 | 10 | # Built versions for shaka aren't available on github 11 | https://ajax.googleapis.com/ajax/libs/shaka-player/4.12.5/shaka-player.compiled.debug.js 12 | 13 | # Libraries below are not included in the firefox extension 14 | https://unpkg.com/@ffmpeg/ffmpeg@0.12.15/dist/umd/ffmpeg.js = ffmpeg.min.js 15 | https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js 16 | #https://unpkg.com/@ffmpeg/core@0.8.5/dist/ffmpeg-core.worker.js 17 | https://unpkg.com/@ffmpeg/ffmpeg@0.12.15/dist/umd/814.ffmpeg.js = ffmpeg-core.worker.js 18 | https://unpkg.com/mpd-parser@0.15.0/dist/mpd-parser.js 19 | https://unpkg.com/m3u8-parser@4.5.0/dist/m3u8-parser.js 20 | -------------------------------------------------------------------------------- /extension/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image Max URL 5 | 6 | 7 | 8 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |

Image Max URL

23 |

Thank you for installing Image Max URL!

24 |

Please review the following options, which balance privacy with functionality.
For more information, please review our Privacy Policy.

25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /extension/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 17 | 22 | 27 |
28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Image Max URL", 4 | "author": "qsniyg", 5 | "version": "2025.12.1", 6 | 7 | "description": "Finds larger or original versions of images", 8 | 9 | "background": { 10 | "scripts": [ 11 | "extension/background.js", 12 | "userscript.user.js" 13 | ], 14 | "persistent": true 15 | }, 16 | 17 | "browser_specific_settings": { 18 | "gecko": { 19 | "id": "maxurl@qsniyg" 20 | } 21 | }, 22 | 23 | "content_scripts": [ 24 | { 25 | "all_frames": true, 26 | "matches": [""], 27 | "js": ["userscript.user.js"] 28 | } 29 | ], 30 | 31 | "icons": { 32 | "48": "resources/logo_48.png", 33 | "96": "resources/logo_96.png" 34 | }, 35 | 36 | "browser_action": { 37 | "browser_style": false, 38 | "default_popup": "extension/popup.html", 39 | "default_title": "Image Max URL", 40 | "default_icon": { 41 | "40": "resources/logo_40.png", 42 | "48": "resources/logo_48.png", 43 | "96": "resources/logo_96.png" 44 | } 45 | }, 46 | 47 | "options_ui": { 48 | "page": "extension/options.html", 49 | "open_in_tab": true 50 | }, 51 | 52 | "options_page": "extension/options.html", 53 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtWO5xT8unDmhWBJpTF1KN2loSBtp7RHE19cVT46zxBcvscy2QLlZAjBQ/0m5paqVXO4ln2KzgH5unyNNZowbT7P9+DukwymjeXoSnaE9+ooKBmxz5Wr6j+x43hWxPAf8PcgnIgY99DgPXV7ZlPzHOGIe9dRarrGNRsbQyoI+Bj4gpu5yIgvg0jYHKAUCpAAwIA9Vhg92+vD7rCUEMwEo+DQp7rOA4RkFQjp83xMdqZwOYzX8+0FDy2TpuGKlkW+N4DvqbcJIi8U/CZhdgSM/KcRKaEc6cGI7Zv6GcrXxPsLqYWj/4e7IpHydLshvtQxcfQ7BV2IWIx/41NtuAIBzNQIDAQAB", 54 | "update_url": "https://raw.githubusercontent.com/qsniyg/maxurl/master/extension/updates.xml", 55 | 56 | "permissions": [ 57 | "storage", 58 | "cookies", 59 | "webRequest", 60 | "webRequestBlocking", 61 | "contextMenus", 62 | 63 | "" 64 | ], 65 | 66 | "optional_permissions": [ 67 | "history", 68 | "notifications", 69 | "downloads", 70 | "clipboardWrite" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /tools/bigimage_template.js: -------------------------------------------------------------------------------- 1 | var bigimage = function(src, options) { 2 | if (options.null_if_no_change) 3 | return null; 4 | 5 | return src; 6 | }; 7 | 8 | var _get_bigimage = function() { 9 | // imu:shared_variables 10 | 11 | if (typeof __IMU_GETBIGIMAGE__ === "undefined") { 12 | require_rules_failed = { 13 | type: "undefined", 14 | data: __IMU_GETBIGIMAGE__, 15 | func: __IMU_GETBIGIMAGE__, 16 | message: "Rules library not included" 17 | }; 18 | } else { 19 | try { 20 | var bigimage_obj = __IMU_GETBIGIMAGE__(shared_variables); 21 | 22 | if (!bigimage_obj || !bigimage_obj.bigimage) { 23 | require_rules_failed = { 24 | type: "returned_falsey", 25 | data: bigimage_obj, 26 | message: "Unable to get bigimage function" 27 | }; 28 | } else if (bigimage_obj.nonce !== __IMU_NONCE__) { 29 | // This could happen if for some reason the userscript manager updates the userscript, 30 | // but not the required libraries. 31 | require_rules_failed = { 32 | type: "bad_nonce", 33 | data: bigimage_obj.nonce, 34 | message: "Bad nonce, expected: " + __IMU_NONCE__ 35 | }; 36 | } else { 37 | bigimage = bigimage_obj.bigimage; 38 | } 39 | 40 | if (require_rules_failed) { 41 | require_rules_failed.func = __IMU_GETBIGIMAGE__; 42 | } 43 | } catch (e) { 44 | require_rules_failed = { 45 | type: "js_error", 46 | data: e, 47 | message: "JS error fetching bigimage function", 48 | func: __IMU_GETBIGIMAGE__ 49 | }; 50 | } 51 | 52 | // in case the userscript is loaded in the window context 53 | //delete __IMU_GETBIGIMAGE__; // not allowed in strict mode 54 | __IMU_GETBIGIMAGE__ = void 0; 55 | } 56 | 57 | if (require_rules_failed) { 58 | console_error(require_rules_failed); 59 | } 60 | }; 61 | _get_bigimage(); 62 | -------------------------------------------------------------------------------- /tools/update_sitesnum.js: -------------------------------------------------------------------------------- 1 | // git clone the gh-pages branch into site 2 | var about = require("../site/about.js"); 3 | var util = require("./util.js"); 4 | 5 | const fs = require("fs"); 6 | const process = require("process"); 7 | process.chdir(__dirname + "/.."); 8 | 9 | var userscript_smaller = util.read_userscript("userscript_smaller.user.js"); 10 | about.get_userscript_stats(userscript_smaller); 11 | 12 | var userscript = util.read_userscript(util.ts_userscript_filename); 13 | 14 | var sites = about.get_sites(); 15 | var total_sites = sites.length; 16 | var fuzzy_sites = (((total_sites / 1000)|0) * 1000) | 0; 17 | var fuzzy_sites_str = (fuzzy_sites + "").replace(/^([0-9]{2})([0-9]{3})$/, "$1,$2"); 18 | var fuzzy_sites_fstr = fuzzy_sites_str.replace(/,/, " "); // 1 234 19 | 20 | var update_sitesnum_line = function(line) { 21 | return line 22 | .replace(/([^0-9])(?:[0-9]{4,5}|[0-9]{1,2},[0-9]{3})([^0-9])/, "$1" + fuzzy_sites_str + "$2") 23 | .replace(/([^0-9])[0-9]{1,2} [0-9]{3,4}([^0-9])/, "$1" + fuzzy_sites_fstr + "$2"); 24 | }; 25 | 26 | var update_pofile = function(pofile) { 27 | var str = fs.readFileSync(pofile).toString(); 28 | var splitted = str.split(/\n/); 29 | var found = false; 30 | for (var i = 0; i < splitted.length; i++) { 31 | var line = splitted[i]; 32 | if (line.indexOf("msgid \"$description$\"") >= 0) { 33 | found = true; 34 | continue; 35 | } 36 | 37 | if (!found) continue; 38 | 39 | if (line.indexOf("msgid ") >= 0) { 40 | console.log(line) 41 | console.warn("Unable to replace sitesnum for", pofile); 42 | return; 43 | } 44 | 45 | var newline = update_sitesnum_line(line); 46 | if (newline !== line) { 47 | splitted[i] = newline; 48 | break; 49 | } 50 | } 51 | 52 | fs.writeFileSync(pofile, splitted.join("\n")); 53 | }; 54 | 55 | var lines = userscript.split("\n"); 56 | var changed = false; 57 | for (var i = 0; i < lines.length; i++) { 58 | var line = lines[i]; 59 | 60 | if (!/^\/\/ @description(?:[:][-a-zA-Z]+)?\s+/.test(line)) continue; 61 | 62 | var oldline = lines[i]; 63 | lines[i] = update_sitesnum_line(lines[i]); 64 | 65 | if (lines[i] !== oldline) changed = true; 66 | } 67 | 68 | if (changed) { 69 | util.write_userscript_lines(lines, util.ts_userscript_filename); 70 | 71 | var pofiles = fs.readdirSync("po"); 72 | for (const pofile of pofiles) { 73 | if (!/\.po$/.test(pofile)) continue; 74 | 75 | update_pofile("po/" + pofile); 76 | } 77 | 78 | console.log("Changed to: ", fuzzy_sites_str); 79 | } else { 80 | console.log("Unchanged: ", fuzzy_sites_str); 81 | } 82 | -------------------------------------------------------------------------------- /src/module.d.ts: -------------------------------------------------------------------------------- 1 | declare class ClipboardItem { 2 | constructor(...args) 3 | } 4 | 5 | declare var unsafeWindow : Window; 6 | 7 | interface Clipboard { 8 | write: Function 9 | } 10 | 11 | declare function GM_getValue(key:string, default_value?:any):any; 12 | declare function GM_setValue(key:string, value:any):void; 13 | declare function GM_setClipboard(data:string, options?:any):void; 14 | declare function GM_addValueChangeListener(key:string, cb:Function):void; 15 | declare function GM_notification(details:Object, ondone:Function):void; 16 | declare function GM_xmlhttpRequest(data:Object):void; 17 | declare var GM_info: (Object|Function); 18 | declare var GM_fetch: Function; 19 | declare var GM_registerMenuCommand: Function; 20 | declare var GM_unregisterMenuCommand: Function; 21 | declare var GM_openInTab: Function; 22 | declare var GM_download: Function; 23 | 24 | declare var GM: { 25 | getValue: (key:string, default_value?:any)=>Promise; 26 | setValue: (key:string, value:any)=>Promise; 27 | setClipboard: (data:string)=>void, 28 | notification: (details:Object, ondone:Function)=>void, 29 | xmlHttpRequest: (data:Object)=>void, 30 | openInTab: Function 31 | } 32 | 33 | declare function require(filename:string):any; 34 | declare var Buffer: { 35 | from: (...args)=>any; 36 | concat: (...args)=>any; 37 | }; 38 | declare var module: { exports: any }; 39 | declare var process: { 40 | argv: Array 41 | }; 42 | 43 | declare var imu_variable:any; 44 | 45 | // for custom_xhr_wrap, which is stringified 46 | declare var imu_xhr: { 47 | custom_xhr: any 48 | } 49 | 50 | declare var chrome: { 51 | permissions: { 52 | request: Function 53 | }, 54 | runtime: { 55 | getURL: (url:string)=>any; 56 | getManifest: ()=>any; 57 | onMessage: { 58 | addListener: Function 59 | } 60 | } 61 | } 62 | 63 | declare var userscript_extension_message_handler:Function; 64 | declare var imu_userscript_message_sender:Function; 65 | 66 | // node 67 | declare var __dirname:string; 68 | 69 | declare function BigInt(n:number):any; 70 | 71 | declare class Map { 72 | set: (key:K, value:V)=>void 73 | get: (key:K)=>V 74 | has: (key:K)=>boolean 75 | delete: (key:K)=>void 76 | keys: ()=>any 77 | size: number 78 | } 79 | 80 | interface Object { 81 | assign: (...args)=>any 82 | } 83 | 84 | declare class SharedArrayBuffer { 85 | constructor(n:number) 86 | } 87 | 88 | interface HeaderDict { 89 | [key: string]: string; 90 | } 91 | 92 | type SingleHeader = { 93 | name: string; 94 | value: string; 95 | }; 96 | 97 | type HeaderList = Array; 98 | 99 | /*declare class Promise { 100 | static all: (promises:any)=>Promise 101 | then: (callback:any)=>Promise 102 | catch: (callback:any)=>Promise 103 | }*/ 104 | -------------------------------------------------------------------------------- /tools/gen_minified.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | var fs = require('fs'); 3 | 4 | var get_uglifyjs_version = function(cb) { 5 | var prc = spawn("uglifyjs", ["--version"]); 6 | var data = ""; 7 | prc.stdout.on('data', function(stdout_data) { 8 | data += stdout_data.toString(); 9 | }); 10 | 11 | prc.on('close', function(code) { 12 | var splitted = data.split(/\s+/); 13 | if (splitted.length > 1 && splitted[1].match(/^[0-9]+[0-9.]+$/)) { 14 | return cb(splitted[1]); 15 | } else { 16 | return cb(null); 17 | } 18 | }); 19 | }; 20 | 21 | var read_userscript_header = function(path) { 22 | var userscript = fs.readFileSync(path).toString(); 23 | var lines = userscript.split("\n"); 24 | 25 | var header = null; 26 | for (const line of lines) { 27 | var is_end_header = false; 28 | if (line.indexOf(" ==UserScript==") >= 0) { 29 | header = line; 30 | } else if (header) { 31 | if (line.indexOf(" ==/UserScript==") >= 0) { 32 | is_end_header = true; 33 | 34 | // present in userscript.user.js now, no need to keep this 35 | /*header += "\n" + "//"; 36 | header += "\n" + "// This script is quickly approaching OpenUserJS's 1MB limit, so the update URL is set to github in order to future-proof updates"; 37 | header += "\n" + "// @updateURL https://raw.githubusercontent.com/qsniyg/maxurl/master/userscript.meta.js"; 38 | header += "\n" + "// @downloadURL https://raw.githubusercontent.com/qsniyg/maxurl/master/userscript_smaller.user.js";*/ 39 | } 40 | 41 | header += "\n" + line; 42 | } 43 | 44 | if (is_end_header) { 45 | header += "\n\n"; 46 | break; 47 | } 48 | } 49 | 50 | return header; 51 | }; 52 | 53 | var in_filename = "build/userscript_extr.user.js"; 54 | var out_filename = "build/userscript_extr_min.user.js"; 55 | var reserved = ["$__imu_get_bigimage"]; 56 | 57 | console.log("Minifying..."); 58 | var prc = spawn("uglifyjs", ['-m', '-c', '-o', out_filename, '--', in_filename], {stdio: "inherit"}); 59 | prc.on('close', function(code) { 60 | console.log("Finished minifying"); 61 | if (code !== 0) { 62 | console.log("Wrong status code:", code); 63 | return; 64 | } 65 | 66 | var userscript_header = read_userscript_header(process.argv[2] || "userscript.user.js"); 67 | fs.writeFileSync("userscript.meta.js", userscript_header); 68 | 69 | get_uglifyjs_version(function(version) { 70 | if (!version) { 71 | console.log("Warning: Unable to find UglifyJS version!"); 72 | version = "v???"; 73 | } else { 74 | version = "v" + version; 75 | } 76 | 77 | var extr_header = read_userscript_header(in_filename); 78 | 79 | // OUJS doesn't allow GM_cookie 80 | extr_header = extr_header.replace(/(\n)\s*\/\/\s*@grant\s+GM.cookie\s*?[\r\n]+/ig, "$1"); 81 | 82 | extr_header += "// Due to OpenUserJS's 1MB limit, the source code had to be minified.\n"; 83 | extr_header += "// The minification was done by gen_minified.js (in the maxurl repository below) using `uglifyjs -m -c` (" + version + ").\n"; 84 | extr_header += "// This unfortunately renders the code pretty much unreadable, but it was the only way to fit it within the 1MB limit.\n"; 85 | extr_header += "// You can view the original source code here: https://github.com/qsniyg/maxurl/blob/master/userscript.user.js\n"; 86 | extr_header += "// Please let me know if you have any questions or concerns regarding the script.\n"; 87 | extr_header += "\n"; 88 | 89 | var min_userscript = fs.readFileSync(out_filename).toString(); 90 | 91 | fs.writeFileSync(out_filename, extr_header + min_userscript); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tools/util.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | var userscript_filename = module.exports.userscript_filename = __dirname + "/../userscript.user.js"; 4 | var ts_userscript_filename = module.exports.ts_userscript_filename = __dirname + "/../src/userscript.ts"; 5 | 6 | // https://stackoverflow.com/a/31652607/13255485 7 | var json_escape_unicode = function(stringified) { 8 | return stringified.replace(/[\u007F-\uFFFF]/g, function(chr) { 9 | return "\\u" + ("0000" + chr.charCodeAt(0).toString(16).toUpperCase()).substr(-4) 10 | }); 11 | }; 12 | module.exports.json_escape_unicode = json_escape_unicode; 13 | 14 | var strings_regex = module.exports.strings_regex = /(\n\tvar strings = )({[\s\S]+?})(;\n)/; 15 | 16 | var stringify_strings = function(strings) { 17 | var stringified = JSON.stringify(strings, null, "\t").replace(/\n/g, "\n\t"); 18 | stringified = json_escape_unicode(stringified); 19 | 20 | return stringified; 21 | }; 22 | module.exports.stringify_strings = stringify_strings; 23 | 24 | module.exports.update_userscript_strings = function(strings, userscript) { 25 | var stringified = stringify_strings(strings); 26 | 27 | if (!userscript) 28 | userscript = fs.readFileSync(ts_userscript_filename).toString(); 29 | 30 | userscript = userscript.replace(strings_regex, "$1" + stringified + "$3"); 31 | 32 | fs.writeFileSync(ts_userscript_filename, userscript); 33 | }; 34 | 35 | var read_userscript = module.exports.read_userscript = function(filename) { 36 | if (!filename) filename = userscript_filename; 37 | var userscript = fs.readFileSync(filename).toString(); 38 | 39 | return userscript; 40 | }; 41 | 42 | module.exports.get_userscript_lines = function(filename) { 43 | return read_userscript(filename).split("\n"); 44 | }; 45 | 46 | module.exports.write_userscript_lines = function(lines, filename) { 47 | if (!filename) filename = userscript_filename; 48 | 49 | fs.writeFileSync(filename, lines.join("\n")); 50 | }; 51 | 52 | var sort_by_array = module.exports.sort_by_array = function(array, key) { 53 | array.sort(function(a, b) { 54 | var a_index = key.indexOf(a); 55 | var b_index = key.indexOf(b); 56 | 57 | if (a_index < 0) { 58 | if (b_index >= 0) 59 | return 1; 60 | else 61 | return a.localeCompare(b); 62 | } else { 63 | if (b_index < 0) 64 | return -1; 65 | else 66 | return a_index - b_index; 67 | } 68 | }); 69 | 70 | return array; 71 | }; 72 | 73 | module.exports.sort_keys_by_array = function(object, key) { 74 | var keys = Object.keys(object); 75 | sort_by_array(keys, key); 76 | 77 | var newobj = {}; 78 | for (const key of keys) { 79 | newobj[key] = object[key]; 80 | } 81 | 82 | return newobj; 83 | }; 84 | 85 | module.exports.read_as_lines = function(file) { 86 | var read = fs.readFileSync(file).toString(); 87 | return read.split("\n"); 88 | }; 89 | 90 | module.exports.get_line_indentation = function(line) { 91 | return line.replace(/^(\s+).*$/, "$1"); 92 | }; 93 | 94 | module.exports.indent = function(lines, indentation) { 95 | var base_indent_regex = null; 96 | 97 | for (var i = 0; i < lines.length; i++) { 98 | var line = lines[i]; 99 | 100 | if (!base_indent_regex && line.length > 0) { 101 | var our_indentation = line.replace(/^(\t*).*$/, "$1"); 102 | if (our_indentation !== line && our_indentation.length > 0) { 103 | base_indent_regex = new RegExp("^\t{" + our_indentation.length + "}") 104 | } else { 105 | base_indent_regex = /^/; 106 | } 107 | } 108 | 109 | if (base_indent_regex) { 110 | line = line.replace(base_indent_regex, ""); 111 | } 112 | 113 | if (line.length > 0) 114 | lines[i] = indentation + line; 115 | } 116 | 117 | return lines; 118 | }; 119 | 120 | var lowerfirst_upperrest = function(splitted) { 121 | splitted[0] = splitted[0].toLowerCase(); 122 | for (var i = 1; i < splitted.length; i++) { 123 | splitted[i] = splitted[i].toUpperCase(); 124 | } 125 | }; 126 | 127 | module.exports.to_langcode = function(langcode) { 128 | langcode = langcode.replace(/_/, "-"); 129 | var splitted = langcode.split("-"); 130 | lowerfirst_upperrest(splitted); 131 | return splitted.join("-"); 132 | }; 133 | 134 | module.exports.to_pocode = function(langcode) { 135 | langcode = langcode.replace(/-/, "_"); 136 | var splitted = langcode.split("_"); 137 | lowerfirst_upperrest(splitted); 138 | return splitted.join("_"); 139 | } 140 | -------------------------------------------------------------------------------- /reddit-bot/comment-bot.js: -------------------------------------------------------------------------------- 1 | const NodeCache = require("node-cache"); 2 | var fs = require("fs"); 3 | 4 | var blacklist_json = JSON.parse(fs.readFileSync("./blacklist.json")); 5 | var env_json = {}; 6 | 7 | require('dotenv').config({ path: "./.env-common" }); 8 | require('dotenv').config({ path: "./.env-comment" }); 9 | env_json.user_agent = process.env.USERAGENT; 10 | env_json.client_id = process.env.CLIENT_ID; 11 | env_json.client_secret = process.env.CLIENT_SECRET; 12 | env_json.refresh_token = process.env.REFRESH_TOKEN; 13 | env_json.access_token = process.env.ACCESS_TOKEN; 14 | env_json.imgur_ua = process.env.IMGUR_UA; 15 | env_json.imgur_cookie = process.env.IMGUR_COOKIE; 16 | //env_json.username = process.env.REDDIT_USER; 17 | //env_json.password = process.env.REDDIT_PASS; 18 | 19 | //console.dir(env_json); 20 | 21 | const Snoowrap = require('snoowrap'); 22 | const Snoostorm = require('snoostorm'); 23 | 24 | const r = new Snoowrap(env_json); 25 | 26 | r.config({ requestDelay: 1001 }); 27 | const client = new Snoostorm(r); 28 | 29 | var dourl = require("./dourl.js"); 30 | 31 | const links = new NodeCache({ stdTTL: 600, checkperiod: 100 }); 32 | 33 | //console.dir(blacklist_json.disallowed); 34 | if (true) { 35 | var submissionStream = client.SubmissionStream({ 36 | "subreddit": "all", 37 | "results": 100, 38 | // using a polltime of 1010 results in ratelimits 39 | "pollTime": 2000 40 | }); 41 | 42 | setInterval(() => { 43 | r.getInbox({ "filter": "messages" }).then((inbox) => { 44 | inbox.forEach((message_data) => { 45 | if (message_data.subject.indexOf("delete:") !== 0 || 46 | message_data.subject.length >= 50 || 47 | !message_data["new"]) { 48 | return; 49 | } 50 | 51 | var comment = message_data.subject.replace(/.*:[ +]*([A-Za-z0-9_]+).*/, "$1"); 52 | if (comment === message_data.subject) 53 | return; 54 | console.log(comment); 55 | 56 | r.getComment(comment).fetch().then((comment_data) => { 57 | if (!comment_data) { 58 | console.log("Unable to fetch comment data for " + comment); 59 | message_data.deleteFromInbox(); 60 | return; 61 | } 62 | 63 | if (!comment_data.author || 64 | comment_data.author.name === "[deleted]") { 65 | console.log("Removing message for " + comment); 66 | message_data.deleteFromInbox(); 67 | //return; 68 | } 69 | 70 | if (comment_data.author.name.toLowerCase() !== "maximagebot") 71 | return; 72 | 73 | // only delete top-level comments, if the parent is a comment, don't delete it 74 | // parent should be t3_ (link) 75 | if (/^t1_/.test(comment_data.parent_id)) { 76 | return; 77 | } 78 | 79 | //console.log(comment_data); 80 | if (comment_data.body.indexOf(comment) < 0) { 81 | console.log("Comment id", comment, "isn't in MaxImageBot's message"); 82 | message_data.deleteFromInbox(); 83 | return; 84 | } 85 | 86 | r.getComment(comment_data.parent_id).fetch().then((post_data) => { 87 | if (!post_data.author || 88 | !message_data.author || 89 | post_data.author.name !== "[deleted]" && 90 | post_data.author.name.toLowerCase() !== message_data.author.name.toLowerCase()) { 91 | return; 92 | } 93 | 94 | console.log("Deleting " + comment); 95 | comment_data.delete(); 96 | message_data.deleteFromInbox(); 97 | }); 98 | }); 99 | }); 100 | }); 101 | }, 10 * 1000); 102 | 103 | submissionStream.on("submission", function (post) { 104 | if (post.domain.startsWith("self.")) { 105 | return; 106 | } 107 | 108 | var options = { 109 | imgur_ua: env_json.imgur_ua, 110 | imgur_cookie: env_json.imgur_cookie, 111 | exclude_mod: true 112 | }; 113 | 114 | if (post.subreddit.display_name) { 115 | if (blacklist_json.disallowed.indexOf(post.subreddit.display_name.toLowerCase()) >= 0 || 116 | blacklist_json.users.indexOf(post.author.name.toLowerCase()) >= 0) { 117 | //console.log(post.subreddit); 118 | return; 119 | } 120 | 121 | if (blacklist_json.shocking.indexOf(post.subreddit.display_name.toLowerCase()) >= 0) { 122 | options.shocking = true; 123 | } 124 | 125 | if (blacklist_json.np.indexOf(post.subreddit.display_name.toLowerCase()) >= 0) { 126 | options.np = true; 127 | } 128 | } 129 | 130 | if (links.get(post.permalink) === true) { 131 | //console.log("Already processed " + post.permalink + ", skipping"); 132 | return; 133 | } 134 | 135 | links.set(post.permalink, true); 136 | 137 | var url = post.url; 138 | try { 139 | dourl(url, post, options); 140 | } catch (e) { 141 | console.error(e); 142 | } 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /reddit-bot/blacklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments-only": [ 3 | "talesfromyourserver" 4 | ], 5 | "posts-only": [ 6 | "bmw" 7 | ], 8 | "disallowed": [ 9 | "anime", 10 | "asianamerican", 11 | "askhistorians", 12 | "askscience", 13 | "askreddit", 14 | "aww", 15 | "chicagosuburbs", 16 | "cosplay", 17 | "cumberbitches", 18 | "d3gf", 19 | "deer", 20 | "depression", 21 | "depthhub", 22 | "drinkingdollars", 23 | "forwardsfromgrandma", 24 | "geckos", 25 | "giraffes", 26 | "grindsmygears", 27 | "indianfetish", 28 | "me_irl", 29 | "misc", 30 | "movies", 31 | "mixedbreeds", 32 | "news", 33 | "newtotf2", 34 | "omaha", 35 | "petstacking", 36 | "pics", 37 | "pigs", 38 | "politicaldiscussion", 39 | "politics", 40 | "programmingcirclejerk", 41 | "raerthdev", 42 | "rants", 43 | "runningcirclejerk", 44 | "salvia", 45 | "science", 46 | "seiko", 47 | "shoplifting", 48 | "sketches", 49 | "sociopath", 50 | "suicidewatch", 51 | "talesfromtechsupport", 52 | "torrent", 53 | "torrents", 54 | "trackers", 55 | "tr4shbros", 56 | "unitedkingdom", 57 | "crucibleplaybook", 58 | 59 | "opieandanthony", 60 | "opienanthony", 61 | "opieandanthonyxyz", 62 | "opieradio", 63 | "acpocketcamp", 64 | "sadcringe", 65 | "atlantahawks", 66 | "the_donald", 67 | "belgistan", 68 | "hittablefaces", 69 | "bigboobproblems", 70 | "badwomensanatomy", 71 | "thedickshow", 72 | "dataisbeautiful", 73 | "circlejerk", 74 | "picturegame", 75 | "imgoingtohellforthis", 76 | "switzerland", 77 | "enoughinternet", 78 | "plebeianar", 79 | "disneyvacation", 80 | "crimemugshots", 81 | "pedologic", 82 | "neovaginadisasters", 83 | "botchedsurgeries", 84 | 85 | "u_314159826", 86 | 87 | "androidthemes", 88 | "iphonexwallpapers", 89 | "sensualfingers", 90 | 91 | "nsfwwhatever", 92 | "backtoschooldeals", 93 | "cogscommunity", 94 | "asktheouija", 95 | "deppenapostroph", 96 | "nsfw", 97 | "celebs" 98 | ], 99 | "users": [ 100 | "gogoluke", 101 | "fancyred", 102 | "quantum-quetzal", 103 | "imadudeduh", 104 | "aliliquori", 105 | "zorgrak", 106 | "atticus_ross", 107 | "bewfloor", 108 | "digiplay", 109 | "funtrees25", 110 | "funtrees225", 111 | "analcurve", 112 | "amadula1", 113 | 114 | "myrandall", 115 | "just-a-traveler", 116 | "ghostofhumid", 117 | "dazzlingdig", 118 | "kingknotts", 119 | "gator426428", 120 | 121 | "useful-chicken", 122 | "gladyesterday0", 123 | "expresspreparation4", 124 | "true_result", 125 | "old_wafer", 126 | "planebrilliant", 127 | "horrornegotiation3", 128 | "nice_condition", 129 | "anygarbage5", 130 | "rich_try", 131 | "motherminimum", 132 | "illustrioussearch5", 133 | "main-ground" 134 | ], 135 | "shocking": [ 136 | "nsfw_wtf" 137 | ], 138 | "np": [ 139 | "pakistan", 140 | "shitrconservativesays", 141 | "warriors", 142 | "aznidentity", 143 | "liverpoolfc", 144 | "protectandserve", 145 | "cricket", 146 | "oaklandathletics", 147 | "india", 148 | "russia", 149 | "maddenmobileforums", 150 | "calamariraceteam", 151 | "ccj2", 152 | "turkey", 153 | "pcmasterrace", 154 | "dankleft", 155 | "sonataarctica", 156 | "antifascistsofreddit", 157 | "jacksepticeye", 158 | "overwatchcirclejerk", 159 | "vegancirclejerk", 160 | "1200isjerky", 161 | "lal_salaam", 162 | "clits", 163 | "gunsarecool", 164 | "quityourbullshit", 165 | "empiredidnothingwrong", 166 | "catholicism", 167 | "kotakuinaction", 168 | "nyyankees", 169 | "truestl", 170 | "visualization", 171 | "underboob", 172 | "abcdesis", 173 | "mcfc", 174 | "nature", 175 | "accounting", 176 | "librandu", 177 | "indianews", 178 | "enough_sanders_spam", 179 | "frat", 180 | "joebiden", 181 | "chodi", 182 | "fnafcringe", 183 | "trump", 184 | "cryptocurrency", 185 | "republicans", 186 | "thickloads", 187 | "donaldtrump", 188 | "unitedstatesofindia", 189 | "darkbeauties", 190 | "barelylegalteens", 191 | "festivalsluts", 192 | "elizabethwarren" 193 | ], 194 | "permission": [ 195 | "benfrick", 196 | "bsa", 197 | "futurology", 198 | "graphic_design", 199 | "historicalwhatif", 200 | "lolgrindr", 201 | "malifaux", 202 | "nfl", 203 | "toonami", 204 | "trumpet", 205 | "ps2ceres", 206 | "duelingcorner" 207 | ] 208 | } 209 | -------------------------------------------------------------------------------- /extension/popup.js: -------------------------------------------------------------------------------- 1 | document.body.oncontextmenu = function(e) { 2 | if (false) { 3 | return true; 4 | } else { 5 | e.preventDefault(); 6 | return false; 7 | } 8 | }; 9 | 10 | // Firefox needs a specified height 11 | function updateheight() { 12 | setTimeout(function() { 13 | var html = document.getElementsByTagName("html")[0]; 14 | html.style.height = document.body.scrollHeight + "px"; 15 | }, 1); 16 | } 17 | 18 | function addactionbtn(info) { 19 | var buttons_el = document.getElementById("buttons"); 20 | 21 | var button_container_el = document.createElement("li"); 22 | button_container_el.classList.add("action"); 23 | button_container_el.id = info.id + "_container"; 24 | 25 | var button_el = document.createElement("button"); 26 | button_el.classList.add("action"); 27 | button_el.id = info.id; 28 | button_el.innerText = info.name; 29 | button_el.onclick = function() { 30 | chrome.runtime.sendMessage({ 31 | type: "popupaction", 32 | data: { 33 | action: info.action 34 | } 35 | }); 36 | }; 37 | button_container_el.appendChild(button_el); 38 | 39 | return new Promise(function(resolve) { 40 | if (!info.toggle_setting) { 41 | buttons_el.appendChild(button_container_el); 42 | resolve(); 43 | } else { 44 | get_option(info.toggle_setting, function(value) { 45 | if (value) { 46 | buttons_el.appendChild(button_container_el); 47 | } 48 | 49 | resolve(); 50 | }, info.toggle_default || false) 51 | } 52 | resolve(); 53 | }) 54 | } 55 | 56 | function get_option(name, cb, _default) { 57 | chrome.runtime.sendMessage({ 58 | type: "getvalue", 59 | data: [name] 60 | }, function(response) { 61 | response = response.data; 62 | 63 | var value = _default; 64 | 65 | if (Object.keys(response).length > 0 && response[name] !== undefined) { 66 | value = JSON.parse(response[name]); 67 | } 68 | 69 | cb(value); 70 | }); 71 | } 72 | 73 | function set_option(name, value) { 74 | var kv = {}; 75 | kv[name] = JSON.stringify(value); 76 | 77 | chrome.runtime.sendMessage({ 78 | type: "setvalue", 79 | data: kv 80 | }); 81 | } 82 | 83 | function get_menucommands(cb) { 84 | chrome.runtime.sendMessage({ 85 | type: "get_menucommands", 86 | }, function(response) { 87 | cb(response.data); 88 | }); 89 | } 90 | 91 | function update_logo(value) { 92 | if (value) { 93 | document.getElementById("enabled-state").classList.remove("disabled"); 94 | document.getElementById("enabled-state").innerText = "Enabled"; 95 | 96 | document.getElementById("enabled-logo").style = ""; 97 | document.getElementById("disabled-logo").style = "display:none"; 98 | } else { 99 | document.getElementById("enabled-state").classList.add("disabled"); 100 | document.getElementById("enabled-state").innerText = "Disabled"; 101 | 102 | document.getElementById("enabled-logo").style = "display:none"; 103 | document.getElementById("disabled-logo").style = ""; 104 | } 105 | } 106 | 107 | function update_highlightimages(value) { 108 | var container = document.getElementById("highlightimages_container"); 109 | 110 | if (value) { 111 | container.style.display = "block"; 112 | } else { 113 | container.style.display = "none"; 114 | } 115 | 116 | updateheight(); 117 | } 118 | 119 | var prefers_dark_mode = function() { 120 | try { 121 | return window.matchMedia("(prefers-color-scheme: dark)").matches; 122 | } catch (e) { 123 | return false; 124 | } 125 | }; 126 | 127 | function update_dark_mode(value) { 128 | if (value === undefined) 129 | value = prefers_dark_mode(); 130 | 131 | if (value) { 132 | document.documentElement.classList.add("dark"); 133 | } else { 134 | document.documentElement.classList.remove("dark"); 135 | } 136 | } 137 | 138 | function toggle_enabled() { 139 | get_option("imu_enabled", function(value) { 140 | set_option("imu_enabled", !value); 141 | update_logo(!value); 142 | }, true); 143 | } 144 | 145 | document.getElementById("logo").onclick = toggle_enabled; 146 | //document.getElementById("enabled-state").onclick = toggle_enabled; 147 | 148 | get_option("dark_mode", function(value) { 149 | update_dark_mode(value); 150 | }, undefined); 151 | 152 | get_option("imu_enabled", function(value) { 153 | update_logo(value); 154 | }, true); 155 | 156 | get_menucommands(function(menuitems) { 157 | var promises = []; 158 | 159 | for (let item of menuitems) { 160 | promises.push(addactionbtn({ 161 | id: item.id, 162 | action: item.id, 163 | name: item.name 164 | })); 165 | } 166 | 167 | Promise.all(promises).then(updateheight); 168 | }); 169 | -------------------------------------------------------------------------------- /extension/welcome.js: -------------------------------------------------------------------------------- 1 | function get_option(name, cb, _default) { 2 | chrome.runtime.sendMessage({ 3 | type: "getvalue", 4 | data: [name] 5 | }, function (response) { 6 | response = response.data; 7 | 8 | var value = _default; 9 | 10 | if (Object.keys(response).length > 0 && response[name] !== undefined) { 11 | value = JSON.parse(response[name]); 12 | } 13 | 14 | cb(value); 15 | }); 16 | } 17 | 18 | function set_option(name, value) { 19 | var kv = {}; 20 | kv[name] = JSON.stringify(value); 21 | 22 | chrome.runtime.sendMessage({ 23 | type: "setvalue", 24 | data: kv 25 | }); 26 | } 27 | 28 | document.getElementById("closebtn").onclick = () => { window.close(); } 29 | 30 | let options = [ 31 | { 32 | "name": "Enable redirection", 33 | "description": "Automatically redirect media opened in their own tab to their larger/original versions.\nThe URL is queried directly from your browser to the respective website (nothing is sent to us).\nQueried information:", 34 | "setting": "redirect", 35 | "list": [ 36 | "Larger media URL (to ensure it exists)", 37 | "No data is sent to us." 38 | ] 39 | }, 40 | { 41 | "name": "Rules using API calls", 42 | "description": "Supports ~1000 extra websites, which require API calls to find the media.\nThe API calls are sent directly from your browser to the respective website (nothing is sent to us).\nPossibly transmitted information (depending on the rule):", 43 | "setting": "allow_apicalls", 44 | "list": [ 45 | "Current page URL or part of it (usually just the media ID)", 46 | "Website-specific user ID and/or session token (if required)", 47 | "No data is sent to us." 48 | ] 49 | }, 50 | { 51 | "name": "Enable website request button", 52 | "description": "Enables a button: \"Request support for website\".\nClicking it will send us the current page URL so that we can take a look at the website to support it.\nTransmitted information (when clicked):", 53 | "setting": "website_request_enable_button", 54 | "list": [ 55 | "Current page URL", 56 | "Anonymized fingerprint (to prevent abuse)", 57 | "Sent to us." 58 | ] 59 | } 60 | ]; 61 | 62 | let optionsdiv = document.getElementById("options_add"); 63 | for (let option of options) { 64 | let framediv = document.createElement("DIV"); 65 | framediv.classList.add("subcat"); 66 | framediv.classList.add("frame"); 67 | 68 | let optiondiv = document.createElement("DIV"); 69 | optiondiv.classList.add("option"); 70 | optiondiv.id = "option_" + option.setting; 71 | framediv.appendChild(optiondiv); 72 | 73 | let tableel = document.createElement("TABLE"); 74 | tableel.classList.add("option-table"); 75 | optiondiv.appendChild(tableel); 76 | 77 | let trel = document.createElement("TR"); 78 | tableel.appendChild(trel); 79 | 80 | let nametd = document.createElement("TD"); 81 | nametd.classList.add("name_td"); 82 | nametd.classList.add("name_td_va_middle"); 83 | trel.appendChild(nametd); 84 | 85 | let strongel = document.createElement("STRONG"); 86 | strongel.innerText = option.name; 87 | nametd.appendChild(strongel); 88 | 89 | let valuetd = document.createElement("TD"); 90 | valuetd.classList.add("value_td"); 91 | trel.appendChild(valuetd); 92 | 93 | let opts = { 94 | "true": "Yes", 95 | "false": "No" 96 | }; 97 | 98 | for (let opt in opts) { 99 | let optname = opts[opt]; 100 | 101 | let checkbox = document.createElement("INPUT"); 102 | checkbox.type = "checkbox"; 103 | checkbox.value = opt; 104 | checkbox.name = option.setting; 105 | checkbox.id = option.setting + "_" + opt; 106 | valuetd.appendChild(checkbox); 107 | 108 | let label = document.createElement("LABEL"); 109 | label.for = option.setting + "_" + opt; 110 | label.innerText = optname; 111 | valuetd.appendChild(label); 112 | 113 | label.onclick = () => { 114 | checkbox.checked = true; 115 | 116 | let our_opt = opt; 117 | for (let opt in opts) { 118 | if (opt === our_opt) 119 | continue; 120 | 121 | let el = document.getElementById(option.setting + "_" + opt); 122 | if (!el) 123 | continue; 124 | 125 | el.checked = false; 126 | } 127 | 128 | let value = opt === "true" ? true : false; 129 | 130 | set_option(option.setting, value); 131 | }; 132 | 133 | get_option(option.setting, function(value) { 134 | let valuestr = value + ""; 135 | 136 | if (valuestr === opt && opt === "true") 137 | checkbox.checked = true; 138 | }) 139 | } 140 | 141 | for (let str of option.description.split("\n")) { 142 | let descel = document.createElement("P"); 143 | descel.classList.add("description"); 144 | descel.innerText = str; 145 | optiondiv.appendChild(descel); 146 | } 147 | 148 | if (option.list) { 149 | let examples = document.createElement("UL"); 150 | examples.classList.add("examples"); 151 | 152 | for (let item of option.list) { 153 | let li = document.createElement("LI"); 154 | li.innerText = item; 155 | examples.appendChild(li); 156 | } 157 | 158 | optiondiv.appendChild(examples); 159 | } 160 | 161 | optionsdiv.appendChild(framediv); 162 | } 163 | -------------------------------------------------------------------------------- /tools/build_libs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "`dirname "$0"`" 4 | cd ../lib 5 | 6 | if [ "$1" = "fetch" ]; then 7 | ../tools/fetch_libs.sh 8 | fi 9 | 10 | CLEANUP_FILES= 11 | 12 | strip_whitespace() { 13 | sed -i -e 's/[ \t]*$//g' -e 's/^ *$//g' "$1" 14 | } 15 | 16 | to_uricomponent() { 17 | cat "$@" | node -e 'var fs = require("fs"); var data = fs.readFileSync(0, "utf8"); process.stdout.write(encodeURIComponent(data));' 18 | } 19 | 20 | node ../tools/patch_libs.js slowaes orig > testcookie_slowaes.js 21 | node ../tools/patch_libs.js cryptojs_aes orig > cryptojs_aes.js 22 | node ../tools/patch_libs.js shaka orig > shaka.debug.js 23 | node ../tools/patch_libs.js jszip orig > jszip.js 24 | node ../tools/patch_libs.js BigInteger orig > BigInteger.js 25 | node ../tools/patch_libs.js acorn_interpreter orig > acorn_interpreter.js 26 | 27 | # The following libraries are not present in the Firefox version 28 | 29 | if [ -f orig/ffmpeg.min.js ]; then 30 | cp orig/ffmpeg.min.js ffmpeg.min.orig.js 31 | cp orig/ffmpeg-core.js ffmpeg-core.orig.js 32 | cp orig/ffmpeg-core.worker.js ffmpeg-core.worker.js 33 | cp ffmpeg-core.orig.js ffmpeg-core.js 34 | # window.* and self->_fakeGlobal are for preventing leaking 35 | # remove sourcemapping to avoid warnings under devtools 36 | sed -i \ 37 | -e 's/window.FFMPEG_CORE_WORKER_SCRIPT/FFMPEG_CORE_WORKER_SCRIPT/g' \ 38 | -e 's/window\.createFFmpegCore/createFFmpegCore/g' \ 39 | -e 's/(self,(()=>/(_fakeGlobal,(()=>/' \ 40 | -e '/\/\/# sourceMappingURL=/d' ffmpeg.min.orig.js 41 | # since ffmpeg-core is being prepended, this is necessary in order to have requests work properly 42 | # note that the unpkg url is used instead of integrating it in the repo. this is for cache reasons, as all other scripts using ffmpeg.js will use the same url 43 | sed -i 's/{return [a-z]*\.locateFile[?][a-z]*\.locateFile(a,[^}]*}var/{return "https:\/\/unpkg.com\/@ffmpeg\/core@0.12.9\/dist\/" + a}var/' ffmpeg-core.js 44 | # prevents blob urls from being used, fixes loading under chrome 45 | # node is used instead of sed due to the size of ffmpeg-core.orig.js 46 | node < ffmpeg.js 72 | echo "var exports = void 0;" >> ffmpeg.js 73 | echo "var module = void 0;" >> ffmpeg.js 74 | echo "var define = void 0;" >> ffmpeg.js 75 | cat fetch_shim.js >> ffmpeg.js 76 | #cat ffmpeg-core.js >> ffmpeg.js 77 | echo "" >> ffmpeg.js 78 | cat ffmpeg.min.orig.js >> ffmpeg.js 79 | echo "" >> ffmpeg.js 80 | echo "var lib_export = _fakeGlobal.FFmpegWASM;" >> ffmpeg.js 81 | cat shim.js >> ffmpeg.js 82 | dos2unix ffmpeg.js 83 | strip_whitespace ffmpeg.js 84 | 85 | CLEANUP_FILES="$CLEANUP_FILES ffmpeg.min.orig.js ffmpeg-core.orig.js ffmpeg-core.js ffmpeg-core.worker.js" 86 | fi 87 | 88 | if [ -f orig/mpd-parser.js ]; then 89 | cp orig/mpd-parser.js mpd-parser.js 90 | # isNaN prevents failing under firefox addon 91 | # location.href is to avoid resolving to the local href (breaks v.redd.it dash streams) 92 | sed -i \ 93 | -e 's/}(this, (function (exports/}(_fakeGlobal, (function (exports/' \ 94 | -e 's/window\.isNaN/isNaN/g' \ 95 | -e 's/window__[^ ]*\.location\.href/""/g' mpd-parser.js 96 | cp orig/m3u8-parser.js m3u8-parser.js 97 | sed -i 's/}(this, function (exports/}(_fakeGlobal, function (exports/' m3u8-parser.js 98 | echo "var _fakeGlobal={window: window};" > stream_parser.js 99 | echo "var exports = void 0;" >> stream_parser.js 100 | echo "var module = void 0;" >> stream_parser.js 101 | echo "var define = void 0;" >> stream_parser.js 102 | cat mpd-parser.js m3u8-parser.js >> stream_parser.js 103 | echo "" >> stream_parser.js 104 | echo "var lib_export = { dash: _fakeGlobal.mpdParser, hls: _fakeGlobal.m3u8Parser };" >> stream_parser.js 105 | cat shim.js >> stream_parser.js 106 | dos2unix stream_parser.js 107 | strip_whitespace stream_parser.js 108 | 109 | CLEANUP_FILES="$CLEANUP_FILES mpd-parser.js m3u8-parser.js" 110 | fi 111 | 112 | CLEANUP=1 113 | if [ $CLEANUP -eq 1 ] && [ ! -z "$CLEANUP_FILES" ]; then 114 | rm $CLEANUP_FILES 115 | fi 116 | -------------------------------------------------------------------------------- /userscript.meta.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Image Max URL 3 | // @name:en Image Max URL 4 | // @name:ar Image Max URL 5 | // @name:cs Image Max URL 6 | // @name:da Image Max URL 7 | // @name:de Image Max URL 8 | // @name:el Image Max URL 9 | // @name:eo Image Max URL 10 | // @name:es Image Max URL 11 | // @name:fi Image Max URL 12 | // @name:fr Image Max URL 13 | // @name:fr-CA Image Max URL 14 | // @name:he Image Max URL 15 | // @name:hi Image Max URL 16 | // @name:hu Image Max URL 17 | // @name:id Image Max URL 18 | // @name:it Image Max URL 19 | // @name:ja Image Max URL 20 | // @name:ko Image Max URL 21 | // @name:nb Image Max URL 22 | // @name:nl Image Max URL 23 | // @name:pl Image Max URL 24 | // @name:pt-BR Image Max URL 25 | // @name:ru Image Max URL 26 | // @name:bg Image Max URL 27 | // @name:uk Image Max URL 28 | // @name:th Image Max URL 29 | // @name:tr Image Max URL 30 | // @name:vi Image Max URL 31 | // @name:zh-CN Image Max URL 32 | // @name:zh-TW Image Max URL 33 | // @name:zh-HK Image Max URL 34 | // @description Finds larger or original versions of images and videos for 10,000+ websites, including a powerful media popup and download feature 35 | // @description:en Finds larger or original versions of images and videos for 10,000+ websites, including a powerful media popup and download feature 36 | // @description:ar البحث عن نسخ أكبر أو أصلية من الصور لأكثر من 10,000 موقع ويب 37 | // @description:cs Vyhledá větší nebo původní verze obrázků a videí pro více než 10,000 webů 38 | // @description:da Finder større eller originale versioner af billeder og videoer til mere end 10,000 websteder 39 | // @description:de Sucht nach größeren oder originalen Versionen von Bildern und Videos für mehr als 10,000 Websites 40 | // @description:el Βρίσκει μεγαλύτερες ή πρωτότυπες εκδόσεις εικόνων και βίντεο για περισσότερους από 10,000 ιστότοπους 41 | // @description:eo Trovas pli grandajn aŭ originalajn versiojn de bildoj kaj filmetoj por pli ol 10,000 retejoj 42 | // @description:es Encuentra imágenes más grandes y originales para más de 10,000 sitios 43 | // @description:fi Etsii suurempia tai alkuperäisiä versioita kuvista ja videoista yli 10,000 verkkosivustolle 44 | // @description:fr Trouve des versions plus grandes ou originales d'images et de vidéos pour plus de 10 000 sites web, y compris une puissante fonction de popup média 45 | // @description:fr-CA Trouve des versions plus grandes ou originales d'images et de vidéos pour plus de 10 000 sites web, y compris une puissante fonction de popup média 46 | // @description:he מוצא גרסאות גדולות יותר או מקוריות של תמונות וסרטונים עבור יותר מ-10,000 אתרים 47 | // @description:hi 10,000 से अधिक वेबसाइटों के लिए फ़ोटो और वीडियो के बड़े या मूल संस्करण ढूँढता है 48 | // @description:hu Több mint 10,000 webhely képének és videóinak nagyobb vagy eredeti változatát találja 49 | // @description:id Menemukan versi gambar dan video yang lebih besar atau orisinal untuk lebih dari 10,000 situs web 50 | // @description:it Trova versioni più grandi o originali di immagini e video per oltre 10,000 siti web 51 | // @description:ja 10,000以上のウェブサイトで高画質や原本画像を見つけ出します 52 | // @description:ko 10,000개 이상의 사이트에 대해 고화질이나 원본 이미지를 찾아드립니다 53 | // @description:nb Finner større eller originale versjoner av bilder og videoer for mer enn 10,000 nettsteder 54 | // @description:nl Vindt grotere of originele versies van foto's en video's voor meer dan 10,000 websites 55 | // @description:pl Wyszukuje większe lub oryginalne wersje obrazów i filmów dla ponad 10,000 stron internetowych 56 | // @description:pt-BR Encontra versões maiores ou originais de imagens e vídeos para mais de 10,000 sites 57 | // @description:ru Находит увеличенные или оригинальные версии изображений и видео для 10,000+ сайтов. Имеет мощную функцию всплывающего окна и скачивание медиафайлов. 58 | // @description:bg Намира увеличени или оригинални версии на изображения за повече от 10,000 уеб сайтове 59 | // @description:uk Знаходить збільшені або оригінальні версії зображень для більш ніж 10,000 веб-сайтів 60 | // @description:th หาที่ใหญ่กว่าหรือเวอร์ชั่นดั้งเดิมของภาพทั้งหมดและวีดีโอสำหรับมากกว่า 10,000 งเว็บไซต์ 61 | // @description:tr 10,000'den fazla web sitesi için resim ve videoların daha büyük veya orijinal sürümlerini bulur 62 | // @description:vi Tìm phiên bản lớn hơn hoặc phiên bản gốc của hình ảnh và video cho hơn 10,000 trang web 63 | // @description:zh-CN 在近万个网站上查找尺寸更大或原版的图像/视频,提供媒体文件小弹窗和下载功能 64 | // @description:zh-TW 為10,000多個網站查找更大或原始圖像 65 | // @description:zh-HK 為10,000多個網站查找更大或原始圖像 66 | // @namespace http://tampermonkey.net/ 67 | // @version 2025.12.1 68 | // @author qsniyg 69 | // @homepageURL https://qsniyg.github.io/maxurl/options.html 70 | // @supportURL https://github.com/qsniyg/maxurl/issues 71 | // @icon https://raw.githubusercontent.com/qsniyg/maxurl/b5c5488ec05e6e2398d4e0d6e32f1bbad115f6d2/resources/logo_256.png 72 | // @include * 73 | // @match *://*/* 74 | // @grant GM.xmlHttpRequest 75 | // @grant GM_xmlhttpRequest 76 | // @grant GM.setValue 77 | // @grant GM_setValue 78 | // @grant GM.getValue 79 | // @grant GM_getValue 80 | // @grant GM_registerMenuCommand 81 | // @grant GM_unregisterMenuCommand 82 | // @grant GM_addValueChangeListener 83 | // @grant GM_download 84 | // @grant GM_openInTab 85 | // @grant GM.openInTab 86 | // @grant GM_notification 87 | // @grant GM.notification 88 | // @grant GM_setClipboard 89 | // @grant GM.setClipboard 90 | // @grant GM_cookie 91 | // @connect * 92 | // api.github.com is used for checking for updates (can be disabled through the "Check Updates" setting) 93 | // @connect api.github.com 94 | // @run-at document-start 95 | // @license Apache-2.0 96 | // non-greasyfork/oujs versions need updateURL and downloadURL to auto-update for certain userscript managers 97 | // @updateURL https://raw.githubusercontent.com/qsniyg/maxurl/master/userscript.meta.js 98 | // @downloadURL https://raw.githubusercontent.com/qsniyg/maxurl/master/userscript_smaller.user.js 99 | // imu:require_rules (this is replaced by the build system for userscript versions that require external rules) 100 | // ==/UserScript== 101 | 102 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ES5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ 8 | "module": "none", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | "lib": ["es5", "es2015", "DOM"], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | "outFile": "./build/tsout.js", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": false, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": false, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ 44 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 45 | 46 | /* Module Resolution Options */ 47 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 48 | "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tools/gen_po.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const maximage = require("../userscript.user.js"); 3 | const util = require("./util.js"); 4 | 5 | var split_value = function(lines, cmd, value) { 6 | var splitted = value.split("\n"); 7 | 8 | for (var i = 0; i < splitted.length; i++) { 9 | var header = ""; 10 | var line = splitted[i]; 11 | 12 | if (i === 0) { 13 | header = cmd + " "; 14 | 15 | if (splitted.length > 1) { 16 | lines.push(header + '""'); 17 | header = ""; 18 | } 19 | } 20 | 21 | if ((i + 1) < splitted.length) 22 | line += "\n"; 23 | 24 | if (line.length === 0) 25 | continue; 26 | 27 | lines.push(header + JSON.stringify(line)); 28 | } 29 | }; 30 | 31 | var start = function(userscript) { 32 | var pofiles = {}; 33 | 34 | var supported_language_names = { 35 | "af": "Afrikaans", 36 | "ar": "Arabic", 37 | "be": "Belarusian", 38 | "bg": "Bulgarian", 39 | "ca": "Catalan", 40 | "cs": "Czech", 41 | "cy": "Welsh", 42 | "da": "Danish", 43 | "de": "German", 44 | "dv": "Divehi", 45 | "el": "Greek", 46 | "en": "English", 47 | "eo": "Esperanto", 48 | "es": "Spanish", 49 | "et": "Estonian", 50 | "eu": "Basque", 51 | "fa": "Farsi", 52 | "fi": "Finnish", 53 | "fo": "Faroese", 54 | "fr": "French", 55 | "gl": "Galician", 56 | "gu": "Gujarati", 57 | "he": "Hebrew", 58 | "hi": "Hindi", 59 | "hr": "Croatian", 60 | "hu": "Hungarian", 61 | "hy": "Armenian", 62 | "id": "Indonesian", 63 | "is": "Icelandic", 64 | "it": "Italian", 65 | "ja": "Japanese", 66 | "ka": "Georgian", 67 | "kk": "Kazakh", 68 | "kn": "Kannada", 69 | "ko": "Korean", 70 | "ky": "Kyrgyz", 71 | "lt": "Lithuanian", 72 | "lv": "Latvian", 73 | "mi": "Maori", 74 | "mn": "Mongolian", 75 | "mr": "Marathi", 76 | "ms": "Malay", 77 | "mt": "Maltese", 78 | "nb": "Norwegian", 79 | "nl": "Dutch", 80 | "pa": "Punjabi", 81 | "pl": "Polish", 82 | "ps": "Pashto", 83 | "pt": "Portuguese", 84 | "pt_BR": "Portuguese (Brazil)", 85 | "qu": "Quechua", 86 | "ro": "Romanian", 87 | "ru": "Russian", 88 | "sa": "Sanskrit", 89 | "sk": "Slovak", 90 | "sl": "Slovenian", 91 | "sq": "Albanian", 92 | "sv": "Swedish", 93 | "sw": "Swahili", 94 | "ta": "Tamil", 95 | "te": "Telugu", 96 | "th": "Thai", 97 | "tl": "Tagalog", 98 | "tn": "Tswana", 99 | "tr": "Turkish", 100 | "tt": "Tatar", 101 | "ts": "Tsonga", 102 | "uk": "Ukrainian", 103 | "ur": "Urdu", 104 | "vi": "Vietnamese", 105 | "xh": "Xhosa", 106 | "zh_CN": "Chinese (Simplified)", 107 | "zh_HK": "Chinese (Hong Kong)", 108 | "zh_TW": "Chinese (Taiwan)", 109 | "zu": "Zulu" 110 | }; 111 | 112 | var supported_languages = userscript.match(/\n\tvar supported_languages = (\[(?:\n\t{2}"[-a-zA-Z]+",?)*\n\t\]);\n/); 113 | if (!supported_languages) { 114 | console.error("Unable to find supported languages match in userscript"); 115 | return; 116 | } 117 | var supported_languages_json = JSON.parse(supported_languages[1]); 118 | 119 | var strings = userscript.match(/\n\tvar strings = ({[\s\S]+?});\n/); 120 | if (!strings) { 121 | console.error("Unable to find strings match in userscript"); 122 | return; 123 | } 124 | 125 | var strings_json = JSON.parse(strings[1]); 126 | 127 | // add languages that only have descriptions 128 | if (strings_json["$description$"]) { 129 | var description = strings_json["$description$"]; 130 | for (const lang in description) { 131 | if (supported_languages_json.indexOf(lang) < 0) { 132 | supported_languages_json.push(lang); 133 | } 134 | } 135 | } 136 | 137 | const language_options = maximage.internal.settings_meta.language.options; 138 | for (var supported_language of supported_languages_json) { 139 | var old_supported_language = supported_language; 140 | if (supported_language === "en") { 141 | supported_language = "imu"; 142 | } else { 143 | supported_language = util.to_pocode(supported_language); 144 | } 145 | 146 | pofiles[supported_language] = []; 147 | 148 | if (supported_language !== "imu") { 149 | var language_name = supported_language_names[supported_language] || supported_language; 150 | pofiles[supported_language].push("# " + language_name + " translations for Image Max URL"); 151 | pofiles[supported_language].push("#"); 152 | } 153 | 154 | pofiles[supported_language].push("msgid \"\""); 155 | pofiles[supported_language].push("msgstr \"\""); 156 | pofiles[supported_language].push("\"Project-Id-Version: Image Max URL\\n\""); 157 | pofiles[supported_language].push("\"MIME-Version: 1.0\\n\""); 158 | pofiles[supported_language].push("\"Content-Type: text/plain; charset=UTF-8\\n\""); 159 | pofiles[supported_language].push("\"Content-Transfer-Encoding: 8bit\\n\""); 160 | 161 | if (supported_language !== "imu") { 162 | pofiles[supported_language].push("\"Language: " + supported_language + "\\n\""); 163 | } 164 | 165 | pofiles[supported_language].push(""); 166 | } 167 | 168 | for (const string in strings_json) { 169 | var string_data = strings_json[string]; 170 | 171 | var comments = {}; 172 | for (const pofile in pofiles) comments[pofile] = []; 173 | 174 | if (string_data._info) { 175 | if (string_data._info.instances) { 176 | var comment = "#. "; 177 | 178 | var instances_text = []; 179 | for (const instance of string_data._info.instances) { 180 | instances_text.push(instance.setting + "." + instance.field); 181 | } 182 | 183 | comment += instances_text.join(", "); 184 | 185 | for (const pofile in pofiles) { 186 | comments[pofile].push(comment); 187 | } 188 | } 189 | 190 | if (string_data._info.comments) { 191 | var _comments = string_data._info.comments; 192 | 193 | for (const pofile in pofiles) { 194 | var comment = null; 195 | var langcode = util.to_langcode(pofile); 196 | if (langcode in _comments) comment = _comments[langcode]; 197 | else if ("en" in _comments) comment = _comments.en; 198 | 199 | if (comment) { 200 | comments[pofile].push("# " + comment.replace(/\r?\n/g, "\n# ")); 201 | } 202 | } 203 | } 204 | } 205 | 206 | if ("en" in string_data && string_data.en !== string) { 207 | for (const pofile in pofiles) { 208 | comments[pofile].push("#. English: " + string_data.en); 209 | } 210 | } 211 | 212 | for (const pofile in pofiles) { 213 | for (const comment of comments[pofile]) { 214 | pofiles[pofile].push(comment); 215 | } 216 | 217 | split_value(pofiles[pofile], "msgid", string); 218 | 219 | var langcode = util.to_langcode(pofile); 220 | 221 | if (pofile !== "imu" && langcode in string_data) { 222 | split_value(pofiles[pofile], "msgstr", string_data[langcode]); 223 | } else { 224 | pofiles[pofile].push("msgstr \"\""); 225 | } 226 | 227 | pofiles[pofile].push(""); 228 | } 229 | } 230 | 231 | for (const pofile in pofiles) { 232 | var ext = "po"; 233 | if (pofile === "imu") 234 | ext = "pot"; 235 | 236 | var filename = "po/" + pofile + "." + ext; 237 | fs.writeFileSync(filename, pofiles[pofile].join("\n")); 238 | } 239 | }; 240 | 241 | var userscript = fs.readFileSync(process.argv[2] || util.ts_userscript_filename).toString(); 242 | start(userscript); 243 | -------------------------------------------------------------------------------- /tools/update_strings.js: -------------------------------------------------------------------------------- 1 | var util = require("./util.js"); 2 | var maximage = require("../userscript.user.js"); 3 | 4 | var get_descriptions = function(strings) { 5 | var descriptions = {}; 6 | 7 | var userscript = util.read_userscript(util.ts_userscript_filename); 8 | var match_regex = /\/\/ @description:([-a-zA-Z]+) +(.*)/; 9 | var matches = userscript.match(new RegExp(match_regex, "g")); 10 | for (const match_str of matches) { 11 | var match = match_str.match(match_regex); 12 | descriptions[match[1]] = match[2]; 13 | } 14 | 15 | for (const lang in descriptions) { 16 | var value = descriptions[lang]; 17 | strings["$description$"][lang] = value; 18 | } 19 | }; 20 | 21 | var process_strings = function(internal) { 22 | /*for (var string in strings) { 23 | if (!("en" in strings[string])) { 24 | strings[string]["en"] = string; 25 | } 26 | }*/ 27 | 28 | var settings_meta = internal.settings_meta; 29 | var settings = internal.settings; 30 | var strings = internal.strings; 31 | 32 | for (var setting in settings_meta) { 33 | var meta = settings_meta[setting]; 34 | 35 | // don't add dead settings to translate 36 | if (!(setting in settings)) { 37 | continue; 38 | } 39 | 40 | var add_info_field = function(setting, fieldname, value) { 41 | if (!value) 42 | return; 43 | 44 | if (!(value in strings)) { 45 | strings[value] = {}; 46 | } else if (true) { 47 | // reordering 48 | var oldvalue = strings[value]; 49 | delete strings[value]; 50 | strings[value] = oldvalue; 51 | } 52 | 53 | if (!("_info" in strings[value])) { 54 | strings[value]._info = {}; 55 | } 56 | 57 | if (!("instances" in strings[value]._info)) { 58 | strings[value]._info.instances = []; 59 | } 60 | 61 | var instance = { 62 | setting: setting, 63 | field: fieldname 64 | }; 65 | 66 | var instancejson = JSON.stringify(instance); 67 | 68 | var instances = strings[value]._info.instances; 69 | var found = false; 70 | for (var i = 0; i < instances.length; i++) { 71 | if (JSON.stringify(instances[i]) === instancejson) { 72 | found = true; 73 | break; 74 | } 75 | } 76 | 77 | if (!found) 78 | instances.push(instance); 79 | }; 80 | 81 | add_info_field(setting, "name", meta.name); 82 | add_info_field(setting, "description", meta.description); 83 | add_info_field(setting, "description_userscript", meta.description_userscript); 84 | add_info_field(setting, "number_unit", meta.number_unit); 85 | 86 | if (meta.warning) { 87 | for (var key in meta.warning) { 88 | add_info_field(setting, "warning." + key, meta.warning[key]); 89 | } 90 | } 91 | 92 | if (meta.options) { 93 | var process_options = function(options) { 94 | for (var key in options) { 95 | if (/^_group/.test(key)) { 96 | process_options(options[key]); 97 | } else if (key[0] !== "_") { 98 | add_info_field(setting, "options." + key + ".name", options[key].name); 99 | add_info_field(setting, "options." + key + ".description", options[key].description); 100 | } 101 | }; 102 | } 103 | 104 | process_options(meta.options); 105 | } 106 | 107 | if (meta.documentation) { 108 | add_info_field(setting, "documentation.title", meta.documentation.title); 109 | add_info_field(setting, "documentation.value", meta.documentation.value); 110 | } 111 | 112 | if (meta.example_websites) { 113 | for (var i = 0; i < meta.example_websites.length; i++) { 114 | add_info_field(setting, "example_websites[" + i + "]", meta.example_websites[i]); 115 | } 116 | } 117 | } 118 | 119 | for (var stringname in strings) { 120 | var stringvalue = strings[stringname]; 121 | 122 | var warn = function(message) { 123 | console.warn(JSON.stringify(stringname), message); 124 | }; 125 | 126 | if (!("_info" in stringvalue)) { 127 | continue; 128 | } 129 | 130 | var info = stringvalue._info; 131 | if (!("instances" in info)) 132 | continue; 133 | 134 | var instances = info.instances; 135 | var validinstances = []; 136 | for (var i = 0; i < instances.length; i++) { 137 | var instance = instances[i]; 138 | if (!instance.setting || !instance.field) 139 | continue; 140 | 141 | if (!(instance.setting in settings)) { 142 | warn("Setting not found in settings: " + instance.setting); 143 | continue; 144 | } 145 | 146 | if (!(instance.setting in settings_meta)) { 147 | warn("Setting not found in settings_meta: " + instance.setting); 148 | continue; 149 | } 150 | 151 | var meta = settings_meta[instance.setting]; 152 | 153 | var value; 154 | 155 | try { 156 | if (/^options\./.test(instance.field)) { 157 | var option_match = instance.field.match(/^options\.(.*?)\.(name|description)$/); 158 | if (!option_match) { 159 | warn("Unable to find match for: " + instance.field); 160 | continue; 161 | } 162 | 163 | var option_name = option_match[1]; 164 | 165 | var check_option = function(options) { 166 | for (var key in options) { 167 | if (/^_group/.test(key)) { 168 | var value = check_option(options[key]); 169 | if (value) 170 | return value; 171 | } else { 172 | if (key === option_name) 173 | return options[key]; 174 | } 175 | } 176 | 177 | return false; 178 | }; 179 | 180 | var option_value = check_option(meta.options); 181 | if (!option_value) { 182 | // hacky but it works 183 | throw "test"; 184 | } 185 | 186 | if (!(option_match[2] in option_value)) 187 | throw "test"; 188 | 189 | value = option_value[option_match[2]]; 190 | } else { 191 | value = eval("meta." + instance.field); 192 | } 193 | } catch (e) { 194 | warn("Field: " + instance.field + " not found for: " + instance.setting); 195 | continue; 196 | } 197 | 198 | if (value === undefined) { 199 | warn("Field: " + instance.field + " not found for: " + instance.setting); 200 | continue; 201 | } 202 | 203 | if (value !== stringname) { 204 | warn("Different value: " + JSON.stringify(value)); 205 | continue; 206 | } 207 | 208 | validinstances.push(instance); 209 | } 210 | 211 | info.instances = validinstances; 212 | 213 | if (validinstances.length === 0) { 214 | warn("No valid instances found"); 215 | delete strings[stringname]; 216 | } 217 | } 218 | 219 | var string_order = ["_info", "en"]; 220 | for (var string in strings) { 221 | var value = strings[string]; 222 | 223 | strings[string] = {}; 224 | var keys = Object.keys(value).sort(function(a, b) { 225 | var a_index = string_order.indexOf(a); 226 | var b_index = string_order.indexOf(b); 227 | 228 | if (a_index < 0) { 229 | if (b_index >= 0) 230 | return 1; 231 | else 232 | return a.localeCompare(b); 233 | } else { 234 | if (b_index < 0) 235 | return -1; 236 | else 237 | return a_index - b_index; 238 | } 239 | }); 240 | 241 | // lists strings that don't have instances (non-setting strings) 242 | if (false && keys[0] !== "_info") { 243 | console.error("'_info' should be first", string, keys); 244 | } 245 | 246 | for (var i = 0; i < keys.length; i++) { 247 | strings[string][keys[i]] = value[keys[i]]; 248 | } 249 | } 250 | 251 | return strings; 252 | }; 253 | 254 | var start = function() { 255 | var strings = process_strings(maximage.internal); 256 | get_descriptions(strings); 257 | util.update_userscript_strings(strings); 258 | }; 259 | 260 | start(); 261 | -------------------------------------------------------------------------------- /tools/patch_libs.js: -------------------------------------------------------------------------------- 1 | var patch_lib = null; 2 | 3 | // wrap in anonymous function because it can be included in background.js 4 | (function() { 5 | function strip_trailing_whitespace(text) { 6 | return text.replace(/[ \t]*$/mg, ""); 7 | } 8 | 9 | function dos_to_unix(text) { 10 | return text.replace(/\r*\n/g, "\n"); 11 | } 12 | 13 | function libexport_shim(text, varname, add_newline) { 14 | var base = [text]; 15 | 16 | if (add_newline) 17 | base.push(""); 18 | 19 | base.push( 20 | "var lib_export = " + varname + ";", 21 | "if (typeof module !== 'undefined')", 22 | "\tmodule.exports = lib_export;", 23 | "" 24 | ) 25 | 26 | return base.join("\n"); 27 | } 28 | 29 | var lib_patches = {}; 30 | 31 | // slowaes needs to be patched to match testcookie's version of slowaes 32 | // patch is adapted from https://raw.githubusercontent.com/kyprizel/testcookie-nginx-module/eb9f7d65f50f054a0e7525cf6ad225ca076d1173/util/aes.patch 33 | function patch_slowaes(text) { 34 | var patched = text 35 | .replace(/(var blockSize = 16;\s*)(for \(var i = data\.length - 1;)/, "$1if (data.length > 16) {\r\n\t\t$2") 36 | .replace(/(data\.splice\(data\.length - padCount, padCount\);\r\n)/, "$1\t\t}\r\n"); 37 | 38 | // dos_to_unix because web archive does this and therefore integrity checks for the userscript can fail 39 | patched = dos_to_unix(patched); 40 | // this section is to ensure byte-for-byte correctness with the old build_libs.sh version, it's otherwise useless 41 | var matchregex = /for \(var i = data\.length - 1;[\s\S]+data\.splice\(data\.length - padCount, padCount\);/ 42 | var match = patched.match(matchregex); 43 | var indented = match[0].replace(/^/mg, "\t"); 44 | patched = patched.replace(matchregex, indented); 45 | patched = strip_trailing_whitespace(patched); 46 | 47 | return libexport_shim(patched, "slowAES"); 48 | } 49 | lib_patches["slowaes"] = { 50 | patch: patch_slowaes, 51 | files: "slowaes.js" 52 | }; 53 | 54 | function patch_cryptojs_aes(text) { 55 | var patched = libexport_shim(text, "CryptoJS"); 56 | 57 | return dos_to_unix(strip_trailing_whitespace(patched)); 58 | } 59 | lib_patches["cryptojs_aes"] = { 60 | patch: patch_cryptojs_aes, 61 | files: "cryptojs_aes.js" 62 | }; 63 | 64 | function patch_muxjs(text) { 65 | // don't store in window 66 | return text 67 | .replace(/^/, "var muxjs=null;\n") 68 | .replace(/\(function\(f\){if\(typeof exports/, "(function(f){muxjs = f();return;if(typeof exports"); 69 | } 70 | lib_patches["muxjs"] = { 71 | patch: patch_muxjs, 72 | files: "mux.js", 73 | cached: true 74 | }; 75 | 76 | function patch_shaka(data) { 77 | var text = data["shaka-player.compiled.debug.js"]; 78 | 79 | text = [ 80 | data["muxjs"], 81 | 82 | // move exportTo outside the anonymous function scope 83 | "var _fakeGlobal={};var exportTo={};\n", 84 | text.replace(/var exportTo={};/g, "") 85 | ].join(""); 86 | 87 | text = text 88 | // XHR is to allow overriding, the others fix the content script failing under FF 89 | .replace(/window\.(XMLHttpRequest|decodeURIComponent|parseInt|muxjs)/g, "$1") 90 | .replace(/innerGlobal\.shaka/g, "_fakeGlobal.shaka") 91 | .replace(/goog\.global\.XMLHttpRequest/g, "XMLHttpRequest") // more XHR 92 | .replace(/(HttpFetchPlugin\.isSupported=function..{)/g, "$1return false;") // disable fetch to force XHR 93 | .replace(/\r*\n\/\/# sourceMappingURL=.*/, "") // remove sourcemapping to avoid warnings under devtools 94 | ; 95 | 96 | text = libexport_shim(text, "exportTo.shaka"); 97 | return strip_trailing_whitespace(dos_to_unix(text)); 98 | } 99 | lib_patches["shaka"] = { 100 | patch: patch_shaka, 101 | files: "shaka-player.compiled.debug.js", 102 | requires: "muxjs" 103 | }; 104 | 105 | function patch_jszip(text) { 106 | // don't store in window 107 | // remove setImmediate delay, which causes zipping to be extremely slow 108 | text = text 109 | .replace(/^/, "var _fakeWindow={};\nvar define = void 0;\nvar module = void 0;\n") 110 | .replace(/\("undefined"!=typeof window.window:"undefined"!=typeof global.global:"undefined"!=typeof self.self:this\)/g, "(_fakeWindow)") 111 | .replace(/\("undefined"!=typeof window.window:void 0!==...:"undefined"!=typeof self\?self:this\)/g, "(_fakeWindow)") 112 | .replace(/if\(typeof window!=="undefined"\){g=window}/, 'if(typeof _fakeWindow!=="undefined"){g=_fakeWindow}') 113 | .replace(/(\.delay=function\(.,.,.\){)[a-zA-Z]+\(function\(\){(.*?)}\)(},)/, "$1$2$3") 114 | .replace(/typeof global !== "undefined" . global/, 'typeof _fakeWindow !== "undefined" ? _fakeWindow'); 115 | 116 | return libexport_shim(text, "_fakeWindow.JSZip"); 117 | } 118 | lib_patches["jszip"] = { 119 | patch: patch_jszip, 120 | files: "jszip.js" 121 | }; 122 | 123 | function patch_biginteger(text) { 124 | return libexport_shim(text, "bigInt"); 125 | } 126 | lib_patches["BigInteger"] = { 127 | patch: patch_biginteger, 128 | files: "BigInteger.min.js" 129 | }; 130 | 131 | function patch_jsinterpreter(text) { 132 | text = text 133 | // bugfixes from: https://github.com/NeilFraser/JS-Polyfills/pull/2 134 | .replace('"if (arguments.length < 1) {"', '"if (arguments.length < 2) {"') 135 | .replace('"len -= deleteCount;","var arl = arguments.length - 2;"', '"len -= deleteCount;","if (arguments.length > 2){var arl = arguments.length - 2;"') 136 | .replace('arguments[i];","}","o.length = len;"', 'arguments[i];","}}","o.length = len;"') 137 | .replace(/^/, "var globalThis={};\n"); 138 | return libexport_shim(text, "globalThis.Interpreter"); 139 | } 140 | lib_patches["acorn_interpreter"] = { 141 | patch: patch_jsinterpreter, 142 | files: "acorn_interpreter.js" 143 | }; 144 | 145 | var unwrap_object = function(obj) { 146 | var keys = Object.keys(obj); 147 | if (keys.length !== 1) 148 | return obj; 149 | return obj[keys[0]]; 150 | } 151 | 152 | var cache = {}; 153 | async function do_patch(libname, getfile) { 154 | if (!(libname in lib_patches)) { 155 | console.error("Invalid library", libname); 156 | return null; 157 | } 158 | 159 | if (libname in cache) 160 | return cache[libname]; 161 | 162 | var patchinfo = lib_patches[libname]; 163 | var data = {}; 164 | 165 | var fetched_file = await getfile(patchinfo.files); 166 | if (!fetched_file) { 167 | console.error("Unable to load file", patchinfo.files); 168 | return null; 169 | } else { 170 | data[patchinfo.files] = fetched_file; 171 | } 172 | 173 | if (patchinfo.requires) { 174 | var fetched_require = await do_patch(patchinfo.requires, getfile); 175 | if (!fetched_require) { 176 | console.error("Unable to load dependency", patchinfo.requires); 177 | return null; 178 | } 179 | 180 | data[patchinfo.requires] = fetched_require; 181 | } 182 | 183 | var patched = patchinfo.patch(unwrap_object(data)); 184 | if (patchinfo.cached) 185 | cache[libname] = patched; 186 | 187 | return patched; 188 | } 189 | patch_lib = do_patch; 190 | 191 | // https://stackoverflow.com/a/42587206 192 | //var isCLI = typeof navigator === "undefined" && !module.parent; 193 | var isCLI = !module.parent; 194 | if (isCLI) { 195 | var fs = require("fs"); 196 | var readfile = function(file) { 197 | return fs.readFileSync(file).toString(); 198 | }; 199 | 200 | var process_cli = async function(argv) { 201 | var patch = argv[2]; 202 | if (!patch) { 203 | console.error("Need patch type"); 204 | return; 205 | } 206 | 207 | if (!(patch in lib_patches)) { 208 | console.error("Invalid patch", patch); 209 | return; 210 | } 211 | 212 | var dirname = argv[3]; 213 | if (!dirname) { 214 | console.error("Need orig library dirname"); 215 | return; 216 | } 217 | 218 | process.stdout.write(await do_patch(patch, function(libname) { 219 | return readfile(dirname + "/" + libname); 220 | })); 221 | }; 222 | 223 | process_cli(process.argv); 224 | } 225 | })(); 226 | -------------------------------------------------------------------------------- /resources/imu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 32 | 36 | 37 | 40 | 44 | 48 | 49 | 52 | 56 | 57 | 66 | 75 | 76 | 98 | 103 | 108 | 109 | 111 | 112 | 114 | image/svg+xml 115 | 117 | 118 | 119 | 120 | 121 | 130 | 136 | 139 | 145 | 154 | 155 | 166 | + 177 | 178 | 179 | -------------------------------------------------------------------------------- /tools/update_from_po.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const util = require("./util.js"); 3 | const maximage = require("../userscript.user.js"); 4 | 5 | const process = require("process"); 6 | process.chdir(__dirname + "/.."); 7 | 8 | var parse = function(lines) { 9 | var strings = {}; 10 | 11 | var current_ref = null; 12 | var current_command = null; 13 | var current_text = null; 14 | var current_comment = null; 15 | 16 | var apply_current = function() { 17 | if (current_command === "msgid") { 18 | current_ref = current_text; 19 | } else if (current_command === "msgstr") { 20 | if (current_ref === null) { 21 | console.error("msgstr without msgid?", current_command, current_text); 22 | } else if (current_ref !== "") { 23 | strings[current_ref] = current_text; 24 | 25 | if (current_comment) { 26 | strings[current_ref + "#comment"] = current_comment; 27 | } 28 | } 29 | 30 | current_comment = null; 31 | } 32 | 33 | current_command = null; 34 | current_text = null; 35 | }; 36 | 37 | for (var i = 0; i < lines.length; i++) { 38 | var line = lines[i]; 39 | 40 | var match = line.match(/^#\s+(.*)$/); 41 | if (match) { 42 | var comment = match[1]; 43 | if (current_comment) { 44 | current_comment += "\n"; 45 | } else { 46 | current_comment = ""; 47 | } 48 | 49 | current_comment += comment; 50 | continue; 51 | } 52 | 53 | if (!line.length && current_command === "msgstr") { 54 | apply_current(); 55 | continue; 56 | } 57 | 58 | if (line[0] === "#") 59 | continue; 60 | 61 | var match = line.match(/^(msg(?:id|str)) (".*")\s*$/); 62 | if (match) { 63 | apply_current(); 64 | current_command = match[1]; 65 | current_text = JSON.parse(match[2]); 66 | } else { 67 | match = line.match(/^(".*")\s*$/); 68 | if (match) { 69 | current_text += JSON.parse(match[1]); 70 | } else { 71 | console.error("Unknown line:", line); 72 | } 73 | } 74 | } 75 | 76 | apply_current(); 77 | 78 | return strings; 79 | }; 80 | 81 | var read_po = function(filename) { 82 | var pofile = fs.readFileSync(filename).toString().replace(/\r/g, ""); 83 | return parse(pofile.split("\n")); 84 | }; 85 | 86 | var update_userscript_strings = function(newstrings) { 87 | var filename = util.ts_userscript_filename; 88 | var userscript = fs.readFileSync(filename).toString().replace(/\r/g, ""); 89 | var strings_regex = /(\n\tvar strings = )({[\s\S]+?})(;\n)/; 90 | 91 | var match = userscript.match(strings_regex); 92 | if (!match) { 93 | console.error("Unable to find strings match in userscript"); 94 | return; 95 | } 96 | 97 | var strings_json = JSON.parse(match[2]); 98 | for (var string in newstrings) { 99 | if (!(string in strings_json)) { 100 | console.warn("msgid not in userscript?", string); 101 | continue; 102 | } 103 | 104 | var newstring = newstrings[string] 105 | for (const lang in newstring) { 106 | if (!newstring[lang]) { 107 | if (lang in strings_json[string]) { 108 | console.warn("Deleting", lang, "for", string); 109 | delete strings_json[string][lang]; 110 | } 111 | } else { 112 | strings_json[string][lang] = newstring[lang]; 113 | } 114 | } 115 | } 116 | 117 | var stringified = JSON.stringify(strings_json, null, "\t").replace(/\n/g, "\n\t"); 118 | stringified = util.json_escape_unicode(stringified); 119 | 120 | userscript = userscript.replace(strings_regex, "$1" + stringified + "$3"); 121 | 122 | fs.writeFileSync(filename, userscript); 123 | }; 124 | 125 | var update_userscript_supported_languages = function(supported_languages) { 126 | var filename = util.ts_userscript_filename; 127 | var userscript = fs.readFileSync(filename).toString().replace(/\r/g, ""); 128 | var strings_regex = /(\n\tvar supported_languages = )(\[[\s\S]+?\])(;\n)/; 129 | 130 | var match = userscript.match(strings_regex); 131 | if (!match) { 132 | console.error("Unable to find supported languages match in userscript"); 133 | return; 134 | } 135 | 136 | util.sort_by_array(supported_languages, ["en"]); 137 | 138 | var stringified = JSON.stringify(supported_languages, null, "\t").replace(/\n/g, "\n\t"); 139 | stringified = util.json_escape_unicode(stringified); 140 | 141 | userscript = userscript.replace(strings_regex, "$1" + stringified + "$3"); 142 | 143 | fs.writeFileSync(filename, userscript); 144 | }; 145 | 146 | var update_userscript_language_options = function(languages) { 147 | var filename = util.ts_userscript_filename; 148 | var userscript = fs.readFileSync(filename).toString().replace(/\r/g, ""); 149 | var strings_regex = /(\n\t\tlanguage: {\n[^}]+?\n\t\t\toptions: )(\{\n\t{4}_type: "combo",(?:\n\t{4}(?:"[^"]+"|[_a-z]+): \{\n\t{5}name: "[^"]+"(?:,\n\t{5}name_gettext: false)?\n\t{4}\},?)*\n\t{3}\})(,\n)/; 150 | 151 | var match = userscript.match(strings_regex); 152 | if (!match) { 153 | console.error("Unable to find language options match in userscript"); 154 | return; 155 | } 156 | 157 | languages = util.sort_keys_by_array(languages, ["_type", "en"]); 158 | 159 | for (var key in languages) { 160 | if (key === "_type") 161 | continue; 162 | languages[key] = {name: languages[key], name_gettext: false}; 163 | } 164 | 165 | var stringified = JSON.stringify(languages, null, "\t").replace(/\n/g, "\n\t\t\t"); 166 | stringified = util.json_escape_unicode(stringified); 167 | stringified = stringified.replace(/(\t)"([_a-z]+)":/g, "$1$2:"); 168 | 169 | userscript = userscript.replace(strings_regex, "$1" + stringified + "$3"); 170 | 171 | fs.writeFileSync(filename, userscript); 172 | }; 173 | 174 | var update_userscript_description = function(languages) { 175 | var filename = util.ts_userscript_filename; 176 | var userscript = fs.readFileSync(filename).toString().replace(/\r/g, ""); 177 | 178 | for (var key in languages) { 179 | var regex = new RegExp("(// @description:" + key + " +).*"); 180 | userscript = userscript.replace(regex, "$1" + languages[key]); 181 | } 182 | 183 | fs.writeFileSync(filename, userscript); 184 | }; 185 | 186 | var get_all_strings = function() { 187 | var strings = {}; 188 | 189 | var supported_languages = ["en"]; 190 | var language_options = {"_type": "combo", "en": "English"}; 191 | var descriptions = {}; 192 | 193 | fs.readdir("./po", function (err, files) { 194 | files.forEach(function(file) { 195 | var match = file.match(/^([-_a-zA-Z]+)\.pot?$/) 196 | if (!match) 197 | return; 198 | 199 | var langcode = match[1]; 200 | 201 | // for comments 202 | var fake_lang = false; 203 | if (langcode === "imu") { 204 | langcode = "en"; 205 | fake_lang = true; 206 | } 207 | 208 | var real_langcode = util.to_langcode(langcode); // en_US -> en-US 209 | 210 | var langstrings = read_po("./po/" + file); 211 | var valid_strings = 0; 212 | 213 | for (var string in langstrings) { 214 | if (string.indexOf("#comment") >= 0) { 215 | var real_string = string.replace(/#comment.*/, ""); 216 | if (!(real_string in strings)) strings[real_string] = {}; 217 | if (!("_info" in strings[real_string])) strings[real_string]._info = {}; 218 | if (!("comments" in strings[real_string]._info)) strings[real_string]._info.comments = {}; 219 | 220 | strings[real_string]._info.comments[real_langcode] = langstrings[string]; 221 | continue; 222 | } 223 | 224 | if (fake_lang || !langstrings[string]) 225 | continue; 226 | 227 | valid_strings++; 228 | 229 | if (string === "$language_native$") { 230 | language_options[real_langcode] = langstrings[string]; 231 | valid_strings--; 232 | } 233 | 234 | if (string === "$description$") { 235 | descriptions[real_langcode] = langstrings[string]; 236 | valid_strings--; 237 | } 238 | 239 | if (!(string in strings)) 240 | strings[string] = {}; 241 | 242 | strings[string][real_langcode] = langstrings[string]; 243 | } 244 | 245 | if (!fake_lang && valid_strings > 0) 246 | supported_languages.push(real_langcode); 247 | }); 248 | 249 | for (const string_name in strings) { 250 | var string = strings[string_name]; 251 | if (!("_info" in string)) continue; 252 | if (!("comments" in string._info)) continue; 253 | 254 | var comments = string._info.comments; 255 | if (!comments.en) continue; 256 | 257 | for (const lang in comments) { 258 | if (lang === "en") continue; 259 | if (comments[lang] === comments.en) delete comments[lang]; 260 | } 261 | } 262 | 263 | update_userscript_strings(strings); 264 | update_userscript_supported_languages(supported_languages); 265 | update_userscript_language_options(language_options); 266 | update_userscript_description(descriptions); 267 | }); 268 | }; 269 | 270 | get_all_strings(); 271 | -------------------------------------------------------------------------------- /reddit-bot/service-bot.js: -------------------------------------------------------------------------------- 1 | const NodeCache = require("node-cache"); 2 | var fs = require("fs"); 3 | var yaml = require("js-yaml"); 4 | 5 | var blacklist_json = JSON.parse(fs.readFileSync("./blacklist.json")); 6 | var env_json = {}; 7 | 8 | require('dotenv').config({ path: "./.env-common" }); 9 | require('dotenv').config({ path: "./.env-service" }); 10 | env_json.user_agent = process.env.USERAGENT; 11 | env_json.client_id = process.env.CLIENT_ID; 12 | env_json.client_secret = process.env.CLIENT_SECRET; 13 | env_json.refresh_token = process.env.REFRESH_TOKEN; 14 | env_json.access_token = process.env.ACCESS_TOKEN; 15 | env_json.imgur_ua = process.env.IMGUR_UA; 16 | env_json.imgur_cookie = process.env.IMGUR_COOKIE; 17 | //env_json.username = process.env.REDDIT_USER; 18 | //env_json.password = process.env.REDDIT_PASS; 19 | 20 | //console.dir(env_json); 21 | 22 | const Snoowrap = require('snoowrap'); 23 | const Snoostorm = require('snoostorm'); 24 | 25 | const r = new Snoowrap(env_json); 26 | 27 | r.config({ requestDelay: 1001 }); 28 | const client = new Snoostorm(r); 29 | 30 | var dourl = require("./dourl.js"); 31 | 32 | const links = new NodeCache({ stdTTL: 0, checkperiod: 100 }); 33 | 34 | function is_number(n) { 35 | if (typeof n === "number") 36 | return true; 37 | 38 | if (typeof n === "string" && /^(?:[0-9]+|[0-9]+\.[0-9]+)$/.test(n)) { 39 | if (n.length > 30) 40 | return false; 41 | 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | function strip_whitespace(x) { 49 | return x.replace(/^\s*|\s*$/g, ""); 50 | } 51 | 52 | function parse_wiki_doc(doc, options) { 53 | if (!doc || typeof doc !== "object") { 54 | //console.log("Invalid YAML document"); 55 | return; 56 | } 57 | 58 | var options_map = { 59 | "removable": true, 60 | "lock_comment": true, 61 | "sticky_comment": true, 62 | "distinguish_comment": true, 63 | "lock_post": true, 64 | "remove_post": true, 65 | "report_post": "text", 66 | //"set_post_flair": "array", 67 | "explain_original": true, 68 | "original_page": true, 69 | //"blacklisted_words": "blacklist", 70 | //"blacklisted_users": "blacklist", 71 | "only_original": true, 72 | "allow_nsfw": true, 73 | "comment_header": "text", 74 | "add_about_link": true, 75 | "add_imu_links": true 76 | //"min_ratio": true, 77 | //"min_pixels": "thresh_px" 78 | }; 79 | 80 | for (var option in options_map) { 81 | if (option in doc) { 82 | var value; 83 | 84 | if (options_map[option] === true) { 85 | value = !!doc[option]; 86 | } else if (options_map[option] === "text") { 87 | value = doc[option]; 88 | if (typeof value !== "string" || value.length < 1) 89 | continue; 90 | } 91 | 92 | options[option] = value; 93 | } 94 | } 95 | 96 | if ("blacklisted_words" in doc && Array.isArray(doc.blacklisted_words)) { 97 | if (!("blacklist" in options)) { 98 | options.blacklist = []; 99 | } 100 | 101 | doc.blacklisted_words.forEach(function(word) { 102 | if (typeof word === "string") { 103 | options.blacklist.push(strip_whitespace(word.toLowerCase())); 104 | } 105 | }); 106 | } 107 | 108 | if ("blacklisted_users" in doc && Array.isArray(doc.blacklisted_users)) { 109 | if (!("user_blacklist" in options)) { 110 | options.user_blacklist = []; 111 | } 112 | 113 | doc.blacklisted_users.forEach(function(user) { 114 | if (typeof user === "string") { 115 | options.user_blacklist.push(strip_whitespace(user.toLowerCase())); 116 | } 117 | }); 118 | } 119 | 120 | if ("min_ratio" in doc && is_number(doc.min_ratio)) { 121 | var value = parseFloat(doc.min_ratio); 122 | if (isNaN(value) || value < 1) 123 | value = 1; 124 | 125 | options.min_ratio = value; 126 | } 127 | 128 | if ("min_pixels" in doc && is_number(doc.min_pixels)) { 129 | var value = parseInt(doc.min_pixels); 130 | if (isNaN(value) || value < 0) 131 | value = 0; 132 | 133 | options.thresh_px = value; 134 | } 135 | 136 | /*if ("report_post" in doc && typeof doc.report_post === "string" && doc.report_post.length > 0) { 137 | options.report_post = doc.report_post; 138 | }*/ 139 | 140 | if ("set_post_flair" in doc && Array.isArray(doc.set_post_flair) && (doc.set_post_flair.length === 1 || doc.set_post_flair.length === 2)) { 141 | options.set_post_flair = doc.set_post_flair; 142 | } 143 | } 144 | 145 | var default_options = {}; 146 | 147 | function get_options_for_wikitext(wikitext) { 148 | if (wikitext.length > 10*1024) { 149 | console.log("Wiki text is too big!"); 150 | return null; 151 | } 152 | 153 | var options = {}; 154 | 155 | try { 156 | yaml.safeLoadAll(wikitext, function(doc) { 157 | parse_wiki_doc(doc, options); 158 | }, {json: true}); 159 | } catch (e) { 160 | console.error(e); 161 | } 162 | 163 | return options; 164 | } 165 | 166 | var wikioptions = new NodeCache({ stdTTL: 20, checkperiod: 60 }); 167 | 168 | function get_wikitext_for_subreddit(subreddit, cb) { 169 | r.getSubreddit(subreddit).getWikiPage("maximagebot").fetch().then( 170 | wikipage => { 171 | cb(wikipage.content_md); 172 | }, 173 | error => { 174 | //console.error(error); 175 | //console.error("Unable to fetch maximagebot wiki page for ", subreddit); 176 | cb(null); 177 | } 178 | ); 179 | } 180 | 181 | function get_wikioptions_for_subreddit(subreddit, cb) { 182 | subreddit = subreddit.toLowerCase(); 183 | 184 | var options = wikioptions.get(subreddit); 185 | if (options) { 186 | cb(options); 187 | } else { 188 | get_wikitext_for_subreddit(subreddit, function(text) { 189 | if (!text) { 190 | return cb({}); 191 | } 192 | 193 | options = get_options_for_wikitext(text); 194 | if (options) { 195 | wikioptions.set(subreddit, options); 196 | } 197 | 198 | cb(options); 199 | }); 200 | } 201 | } 202 | 203 | //var lastchecked = Date.now(); 204 | 205 | var started = Date.now(); 206 | 207 | var whitelisted_subreddits = [ 208 | "maximagetest" 209 | ]; 210 | //console.dir(blacklist_json.disallowed); 211 | if (true) { 212 | var submissionStream = client.SubmissionStream({ 213 | "subreddit": "mod", 214 | "results": 100, 215 | // using a polltime of 1010 results in ratelimits 216 | "pollTime": 10*1000 217 | }); 218 | 219 | setInterval(() => { 220 | r.getInbox({ "filter": "messages" }).then((inbox) => { 221 | inbox.forEach((message_data) => { 222 | if (!message_data["new"]) 223 | return; 224 | 225 | var subreddit = message_data.subreddit_name_prefixed; 226 | 227 | if (/^invitation to moderate \/?(?:[ru]|user)\/\S+$/.test(message_data.subject)) { 228 | console.log("Invited to moderate " + subreddit); 229 | 230 | try { 231 | message_data.markAsRead(); 232 | r.getSubreddit(subreddit).acceptModeratorInvite(); 233 | } catch (e) { 234 | console.error(e); 235 | } 236 | } 237 | }); 238 | }); 239 | }, 10 * 1000); 240 | 241 | submissionStream.on("submission", function (post) { 242 | if (!post || !post.subreddit || !post.subreddit.display_name) { 243 | console.error("Post is invalid", post); 244 | return; 245 | } 246 | 247 | // for testing purposes 248 | var post_subreddit = post.subreddit.display_name.toLowerCase(); 249 | if (false) { 250 | if (whitelisted_subreddits.indexOf(post_subreddit) < 0) 251 | return; 252 | } 253 | 254 | if (post.domain.startsWith("self.")) { 255 | return; 256 | } 257 | 258 | var current_time = Date.now(); 259 | var created_millis = post.created_utc * 1000; 260 | /*if ((current_time - created_millis) > 60*1000) { 261 | console.log("Post is too old", post.permalink, current_time, created_millis, current_time - created_millis); 262 | return; 263 | }*/ 264 | if (created_millis < started) { 265 | console.log("Post is too old", post.permalink, started, created_millis, started - created_millis); 266 | return; 267 | } 268 | 269 | var options = { 270 | imgur_ua: env_json.imgur_ua, 271 | imgur_cookie: env_json.imgur_cookie, 272 | exclude_mod: false, 273 | blacklist: [] 274 | }; 275 | 276 | // service bot, so this section is useless 277 | if (false && post.subreddit.display_name) { 278 | if (blacklist_json.disallowed.indexOf(post.subreddit.display_name.toLowerCase()) >= 0 || 279 | blacklist_json.users.indexOf(post.author.name.toLowerCase()) >= 0) { 280 | console.log(post.subreddit); 281 | return; 282 | } 283 | 284 | if (blacklist_json.shocking.indexOf(post.subreddit.display_name.toLowerCase()) >= 0) { 285 | options.shocking = true; 286 | } 287 | 288 | if (blacklist_json.np.indexOf(post.subreddit.display_name.toLowerCase()) >= 0) { 289 | options.np = true; 290 | } 291 | } 292 | 293 | if (links.get(post.id) === true) { 294 | //console.log("Already processed " + post.permalink + ", skipping"); 295 | return; 296 | } 297 | 298 | links.set(post.id, true); 299 | 300 | var url = post.url; 301 | 302 | get_wikioptions_for_subreddit(post_subreddit, function(new_options) { 303 | for (var option in new_options) { 304 | options[option] = new_options[option]; 305 | } 306 | 307 | console.log(options); 308 | 309 | try { 310 | dourl(url, post, options); 311 | } catch (e) { 312 | console.error(e); 313 | } 314 | }); 315 | }); 316 | } 317 | -------------------------------------------------------------------------------- /docs/pt/README.pt-BR.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [English](../../README.md) | **Português (Brasil)** 4 | 5 |
6 | 7 | --- 8 | 9 |

10 | Image Max URL 11 |

12 | 13 | **Image Max URL** é um programa que busca versões maiores ou originais de imagens e vídeos, substituindo padrões de URL. 14 | 15 | Suporta mais de 9000 sites (veja a lista completa em [sites-suportados.txt](sites-suportados.txt)), além de vários mecanismos genéricos como WordPress e MediaWiki. 16 | 17 | Disponível como: 18 | 19 | - **Userscript**: (maioria dos navegadores) 20 | - Estável: [userscript_smaller.user.js](https://github.com/qsniyg/maxurl/blob/master/userscript_smaller.user.js?raw=true) ou [OpenUserJS](https://openuserjs.org/scripts/qsniyg/Image_Max_URL) 21 | - Desenvolvimento: [userscript.user.js](https://github.com/qsniyg/maxurl/blob/master/userscript.user.js?raw=true) (recomendado) 22 | - Serve como base para todas as opções listadas abaixo. Também funciona como um módulo node (usado pelo bot do Reddit) e pode ser incorporado em um site. 23 | 24 | - **Extensão de navegador**: [Firefox](https://addons.mozilla.org/firefox/addon/image-max-url/) (outros navegadores compatíveis com WebExtensions podem carregar a extensão via repositório git) 25 | - Extensões possuem mais privilégios que userscripts, oferecendo funcionalidades extras. 26 | - Código fonte: [manifest.json](https://github.com/qsniyg/maxurl/blob/master/manifest.json) e pasta [extension](https://github.com/qsniyg/maxurl/tree/master/extension) 27 | 28 | - **Website**: [https://qsniyg.github.io/maxurl/](https://qsniyg.github.io/maxurl/) 29 | - Devido a restrições de segurança, algumas URLs (que exigem solicitações de origem cruzada) não são suportadas. 30 | - Código fonte: branch [gh-pages](https://github.com/qsniyg/maxurl/tree/gh-pages) 31 | 32 | - **Bot do Reddit**: [/u/MaxImageBot](https://www.reddit.com/user/MaxImageBot/) 33 | - Código fonte: [comment-bot.js](https://github.com/qsniyg/maxurl/blob/master/reddit-bot/comment-bot.js) e [dourl.js](https://github.com/qsniyg/maxurl/blob/master/reddit-bot/dourl.js) 34 | 35 | **Comunidade**: 36 | - [Servidor Discord](https://discord.gg/fH9Pf54) 37 | - [Matrix](https://matrix.to/#/#image-max-url:tedomum.net?via=tedomum.net) 38 | - [Subreddit](http://reddit.com/r/MaxImage) 39 | 40 | ## Carregando a extensão manualmente 41 | 42 | A extensão não está disponível em outras lojas (como Chrome e Microsoft Edge), mas pode ser carregada manualmente: 43 | 44 | - **Repositório**: 45 | - Baixe o repositório (recomendado clonar via git para facilitar atualizações) 46 | - **Chromium**: 47 | - Vá para `chrome://extensions` ou `edge://extensions`, ative o "Modo de desenvolvedor", clique em "Carregar sem pacote" e navegue até a pasta clonada do maxurl. 48 | - **Firefox**: 49 | - Vá para `about:debugging -> Este Firefox`, selecione "Carregar complemento temporário..." e navegue até `manifest.json` na pasta clonada do maxurl. 50 | - A extensão será excluída ao fechar o Firefox. 51 | 52 | - **CRX (navegadores baseados em Chromium)**: 53 | - Baixe o arquivo CRX em [ImageMaxURL_crx3.crx](https://github.com/qsniyg/maxurl/blob/master/build/ImageMaxURL_crx3.crx) 54 | - Vá para `chrome://extensions`, ative o "Modo do desenvolvedor", arraste e solte o arquivo CRX na página. 55 | 56 | - **XPI (navegadores baseados em Firefox)**: 57 | - Baixe o arquivo XPI em [ImageMaxURL_signed.xpi](https://github.com/qsniyg/maxurl/blob/master/build/ImageMaxURL_signed.xpi) 58 | - Vá para `about:addons`, clique no ícone de engrenagem, selecione "Instalar complemento de arquivo..." e navegue até o arquivo XPI baixado. 59 | 60 | ## Contribuindo 61 | 62 | Contribuições são bem-vindas! Para relatórios de bugs, solicitações de recursos ou novos sites, abra uma issue no repositório. 63 | 64 | Se não tem uma conta Github, use os links da comunidade ou entre em contato diretamente [aqui](https://qsniyg.github.io/). 65 | 66 | Para contribuir com código ou traduções, consulte [CONTRIBUTING.md](CONTRIBUTING.pt-BR.md). 67 | 68 | ## Integrando o IMU no seu programa 69 | 70 | O `userscript.user.js` também funciona como um módulo Node. 71 | 72 | ```javascript 73 | var maximage = require('./userscript.user.js'); 74 | 75 | maximage(smallimage, { 76 | // Se definido como false, retornará apenas a URL sem propriedades adicionais. 77 | // Recomendado manter como true para obter informações detalhadas. 78 | // Utilizado como um hack para verificar se o IMU já suporta uma regra. 79 | fill_object: true, 80 | 81 | // Número máximo de iterações para buscar a imagem maior. 82 | // Recomendado pelo menos 5. 83 | iterations: 200, 84 | 85 | // Define se deve armazenar e utilizar um cache interno para URLs. 86 | // Use "read" para usar o cache sem armazenar novos resultados. 87 | use_cache: true, 88 | 89 | // Tempo de validade (em segundos) para entradas no cache de URL. 90 | urlcache_time: 60 * 60, 91 | 92 | // Lista de problemas (ex: marcas d'água, imagens corrompidas) a serem excluídos. 93 | // Por padrão, todos os problemas são excluídos. 94 | // Defina como [] para não excluir nenhum problema. 95 | //exclude_problems: [], 96 | 97 | // Define se deve excluir vídeos dos resultados. 98 | exclude_videos: false, 99 | 100 | // Incluirá um histórico de objetos encontrados durante as iterações. 101 | // Desativar manterá apenas os objetos da última iteração bem-sucedida. 102 | include_pastobjs: true, 103 | 104 | // Tentará encontrar a página original da imagem, mesmo que exija solicitações extras. 105 | force_page: false, 106 | 107 | // Permite o uso de regras que utilizam sites de terceiros para encontrar imagens maiores. 108 | allow_thirdparty: false, 109 | 110 | // Implementa uma lista negra ou lista branca de URLs. 111 | // Se não especificado, aceita todas as URLs. 112 | filter: function(url) { 113 | return true; 114 | }, 115 | 116 | // Função auxiliar para realizar solicitações HTTP, usada para sites como Flickr. 117 | // Espera-se que a API seja semelhante à API GM_xmlHTTPRequest. 118 | // Uma implementação usando o módulo de solicitação do Node pode ser encontrada em reddit-bot/dourl.js 119 | do_request: function(options) { 120 | // options = { 121 | // url: "", 122 | // method: "GET", 123 | // data: "", // para method: "POST" 124 | // overrideMimeType: "", // usado para decodificar conjuntos de caracteres alternativos 125 | // headers: {}, // Se um cabeçalho for null ou "", não inclua esse cabeçalho 126 | // onload: function(resp) { 127 | // // resp é esperado como um objeto semelhante a XMLHttpRequest, implementando estes campos: 128 | // // finalUrl 129 | // // readyState 130 | // // responseText 131 | // // status 132 | // } 133 | // } 134 | }, 135 | 136 | // Função de callback para processar os resultados. 137 | cb: function(result) { 138 | if (!result) return; 139 | 140 | if (result.length === 1 && result[0].url === smallimage) { 141 | // Nenhuma imagem maior foi encontrada. 142 | return; 143 | } 144 | 145 | for (var i = 0; i < result.length; i++) { 146 | // Processa o objeto resultante. 147 | } 148 | } 149 | }); 150 | ``` 151 | 152 | O resultado é uma lista de objetos com propriedades úteis: 153 | 154 | ```javascript 155 | [{ 156 | // URL da imagem 157 | url: null, 158 | 159 | // Se a URL é de um vídeo 160 | video: false, 161 | 162 | // Se a URL deve funcionar sempre 163 | // Não dependa deste valor se não for necessário 164 | always_ok: false, 165 | 166 | // Se a URL provavelmente funcionará 167 | likely_broken: false, 168 | 169 | // Se o servidor suporta requisições HEAD 170 | can_head: true, 171 | 172 | // Lista de erros HEAD que podem ser ignorados 173 | head_ok_errors: [], 174 | 175 | // Se o servidor pode retornar um cabeçalho Content-Type incorreto na requisição HEAD 176 | head_wrong_contenttype: false, 177 | 178 | // Se o servidor pode retornar um cabeçalho Content-Length incorreto na requisição HEAD 179 | head_wrong_contentlength: false, 180 | 181 | // Se a URL está aguardando processamento 182 | // Este valor será sempre falso se um callback estiver sendo usado 183 | waiting: false, 184 | 185 | // Se a URL redireciona para outra URL 186 | redirects: false, 187 | 188 | // Se a URL é temporária ou funciona apenas no IP atual (como um link de download gerado) 189 | is_private: false, 190 | 191 | // Se a URL é da imagem original armazenada nos servidores do site 192 | is_original: false, 193 | 194 | // Se true, não deve inserir esta URL novamente no IMU 195 | norecurse: false, 196 | 197 | // Se a URL deve ser usada ou não 198 | // Se true, trate como um erro 404 199 | // Se "mask", esta imagem é uma máscara sobreposta 200 | bad: false, 201 | 202 | // Lista de condições para considerar a imagem ruim 203 | // Exemplo: 204 | // [{ 205 | // headers: {"Content-Length": "1000"}, 206 | // status: 301 207 | // }] 208 | // Use maximage.check_bad_if(bad_if, resp) para verificar se a resposta corresponde a uma condição ruim 209 | // (resp é esperado como um objeto XHR) 210 | bad_if: [], 211 | 212 | // Se a URL é uma URL "falsa" usada internamente (ou seja, se true, não use esta URL) 213 | fake: false, 214 | 215 | // Cabeçalhos necessários para visualizar a URL retornada 216 | // Se um cabeçalho for null, não inclua esse cabeçalho 217 | headers: {}, 218 | 219 | // Propriedades adicionais que podem ser úteis 220 | extra: { 221 | // Página original onde a imagem estava hospedada 222 | page: null, 223 | 224 | // Título ou legenda anexada à imagem 225 | caption: null 226 | }, 227 | 228 | // Nome de arquivo descritivo para a imagem 229 | filename: "", 230 | 231 | // Lista de problemas com a imagem. Use exclude_problems para excluir imagens com problemas específicos 232 | problems: { 233 | // Se true, a imagem é provavelmente maior que a inserida, mas contém uma marca d'água (quando a inserida não tem) 234 | watermark: false, 235 | 236 | // Se true, a imagem é provavelmente menor que a inserida, mas não tem marca d'água 237 | smaller: false, 238 | 239 | // Se true, a imagem pode ser totalmente diferente da inserida 240 | possibly_different: false, 241 | 242 | // Se true, a imagem pode estar corrompida (como GIFs no Tumblr) 243 | possibly_broken: false 244 | } 245 | }] 246 | ``` 247 | -------------------------------------------------------------------------------- /tools/gen_rules_js.js: -------------------------------------------------------------------------------- 1 | var process = require("process"); 2 | var util = require("./util.js"); 3 | var fs = require("fs"); 4 | const child_process = require("child_process"); 5 | process.chdir(__dirname + "/.."); 6 | 7 | var variables_list = [ 8 | "_nir_debug_", 9 | "nir_debug", 10 | "Math_floor", 11 | "Math_round", 12 | "Math_random", 13 | "Math_max", 14 | "Math_min", 15 | "Math_abs", 16 | "Math_pow", 17 | "get_random_text", 18 | "console_log", 19 | "console_error", 20 | "console_warn", 21 | "console_trace", 22 | "JSON_stringify", 23 | "JSON_parse", 24 | "base64_decode", 25 | "base64_decode_urlsafe", 26 | "base64_encode", 27 | "is_array", 28 | "array_indexof", 29 | "string_indexof", 30 | //"native_blob", 31 | //"native_URL", 32 | "string_fromcharcode", 33 | "string_charat", 34 | "array_extend", 35 | "array_foreach", 36 | "array_map", 37 | "array_or_null", 38 | "array_upush", 39 | "string_replaceall", 40 | "match_all", 41 | "obj_foreach", 42 | "obj_extend", 43 | "shallowcopy", 44 | "deepcopy", 45 | "_", 46 | "settings", // shouldn't be used, but just in case 47 | "new_map", 48 | "map_set", 49 | "map_get", 50 | "map_has", 51 | "map_remove", 52 | "map_foreach", 53 | "map_size", 54 | "new_set", 55 | "set_add", 56 | "set_has", 57 | "set_remove", 58 | "real_api_cache", 59 | "real_api_query", 60 | "real_website_query", 61 | "is_invalid_url", 62 | "mod", 63 | "norm_url", 64 | "urljoin", 65 | "fillobj", 66 | "fillobj_urls", 67 | "add_full_extensions", 68 | "add_full_extensions2", 69 | "add_extensions", 70 | "add_extensions_jpeg", 71 | "add_extensions_with_jpeg", 72 | "add_extensions_gif", 73 | "add_extensions_upper", 74 | "add_extensions_upper_jpeg", 75 | "add_extensions_from_webp", 76 | "add_http", 77 | "force_https", 78 | "decodeuri_ifneeded", 79 | "encodeuri_ifneeded", 80 | "replace_sizes", 81 | "zpadnum", 82 | "hex_to_ascii", 83 | "hex_to_numberarray", 84 | "numberarray_to_hex", 85 | "reverse_str", 86 | "decode_entities", 87 | "encode_entities", 88 | "encode_regex", 89 | "get_queries", 90 | "stringify_queries", 91 | "remove_queries", 92 | "keep_queries", 93 | "add_queries", 94 | "fuzzify_text", 95 | "fuzzy_date_compare", 96 | "parse_headers", 97 | "headers_list_to_dict", 98 | "headers_dict_to_list", 99 | "get_resp_finalurl", 100 | "get_ext_from_contenttype", 101 | "get_library", 102 | "normalize_whitespace", 103 | "strip_whitespace", 104 | "get_image_size", 105 | "sort_by_key", 106 | "sort_by_array", 107 | "parse_tag_def", 108 | "get_meta", 109 | "fixup_js_obj", 110 | "fixup_js_obj_proper", 111 | "common_functions", 112 | "get_domain_from_url", 113 | "get_domain_nosub", 114 | "looks_like_valid_link", 115 | "IMUCache", 116 | "url_basename", 117 | "parse_int", 118 | "get_localstorage" 119 | ]; 120 | 121 | var get_random_text = function(length) { 122 | var text = ""; 123 | 124 | while (text.length < length) { 125 | var newtext = Math.floor(Math.random() * 10e8).toString(26); 126 | text += newtext; 127 | } 128 | 129 | text = text.substr(0, length); 130 | return text; 131 | }; 132 | 133 | var nonce = get_random_text(16); 134 | 135 | function get_bigimage(splitted) { 136 | var bigimage_start = -1; 137 | var bigimage_end = -1; 138 | 139 | for (var i = 0; i < splitted.length; i++) { 140 | if (bigimage_start < 0) { 141 | if (/^\tfunction bigimage\s*\(src, options\)\s*{$/.test(splitted[i])) { 142 | bigimage_start = i; 143 | } 144 | } else { 145 | if (splitted[i] === "\t}" && splitted[i + 1] === "\t// -- end bigimage --") { 146 | bigimage_end = i; 147 | break; 148 | } 149 | } 150 | } 151 | 152 | if (bigimage_start < 0 || bigimage_end < 0) { 153 | console.error("Unable to find bigimage start/end", bigimage_start, bigimage_end); 154 | return null; 155 | } 156 | 157 | return [bigimage_start, bigimage_end]; 158 | } 159 | 160 | var add_lines = function(in_arr, out_arr, fn) { 161 | for (var i = 0; i < in_arr.length; i++) { 162 | var line = fn(in_arr[i]); 163 | 164 | out_arr.push(line + ((i + 1) < in_arr.length ? "," : "")); 165 | } 166 | } 167 | 168 | function get_host_shim() { 169 | var lines = []; 170 | lines.push("var shared_variables = {"); 171 | 172 | add_lines(variables_list, lines, function(variable) { 173 | return "\t'" + variable + "': " + variable; 174 | }); 175 | 176 | lines.push("};"); 177 | 178 | return lines; 179 | } 180 | 181 | function get_rules_shim() { 182 | var lines = []; 183 | 184 | for (var i = 0 ; i < variables_list.length; i++) { 185 | var variable = variables_list[i]; 186 | 187 | lines.push("var " + variable + " = shared_variables['" + variable + "'];"); 188 | } 189 | 190 | return lines; 191 | } 192 | 193 | function get_line_indentation(line) { 194 | return line.replace(/^(\s+).*$/, "$1"); 195 | } 196 | 197 | function indent(lines, indentation) { 198 | var base_indent_regex = null; 199 | 200 | for (var i = 0; i < lines.length; i++) { 201 | var line = lines[i]; 202 | 203 | if (!base_indent_regex && line.length > 0) { 204 | var our_indentation = line.replace(/^(\t*).*$/, "$1"); 205 | if (our_indentation !== line && our_indentation.length > 0) { 206 | base_indent_regex = new RegExp("^\t{" + our_indentation.length + "}") 207 | } else { 208 | base_indent_regex = /^/; 209 | } 210 | } 211 | 212 | if (base_indent_regex) { 213 | line = line.replace(base_indent_regex, ""); 214 | } 215 | 216 | if (line.length > 0) 217 | lines[i] = indentation + line; 218 | } 219 | 220 | return lines; 221 | }; 222 | 223 | function replace_vars(line) { 224 | return line 225 | .replace(/__IMU_NONCE__/g, JSON.stringify(nonce)) 226 | .replace(/__IMU_GETBIGIMAGE__/g, "$__imu_get_bigimage"); 227 | } 228 | 229 | function gen_rules_js(lines, userscript_lines, startend) { 230 | var out_lines = []; 231 | 232 | for (var i = 0; i < lines.length; i++) { 233 | if (/^\s*\/\/ imu:/.test(lines[i])) { 234 | var indentation = get_line_indentation(lines[i]); 235 | if (lines[i].indexOf("imu:shared_variables") >= 0) { 236 | var rules_shim = get_rules_shim(); 237 | indent(rules_shim, indentation); 238 | [].push.apply(out_lines, rules_shim); 239 | } else if (lines[i].indexOf("imu:bigimage") >= 0) { 240 | var bigimage_lines = indent(userscript_lines.slice(startend[0] + 1, startend[1]), indentation); 241 | [].push.apply(out_lines, bigimage_lines); 242 | } 243 | } else { 244 | out_lines.push(replace_vars(lines[i])); 245 | } 246 | } 247 | 248 | return out_lines; 249 | } 250 | 251 | function gen_userscript_replace(lines) { 252 | var out_lines = []; 253 | 254 | for (var i = 0; i < lines.length; i++) { 255 | if (/^\s*\/\/ imu:/.test(lines[i])) { 256 | var indentation = get_line_indentation(lines[i]); 257 | if (lines[i].indexOf("imu:shared_variables") >= 0) { 258 | var host_shim = get_host_shim(); 259 | indent(host_shim, indentation); 260 | [].push.apply(out_lines, host_shim); 261 | } 262 | } else { 263 | out_lines.push(replace_vars(lines[i])); 264 | } 265 | } 266 | 267 | return out_lines; 268 | } 269 | 270 | function gen_userscript(lines, userscript_lines, startend) { 271 | var replace = gen_userscript_replace(lines); 272 | var indentation = get_line_indentation(userscript_lines[startend[0]]); 273 | indent(replace, indentation); 274 | 275 | replace.unshift((startend[1] + 2) - startend[0]); 276 | replace.unshift(startend[0]); 277 | 278 | [].splice.apply(userscript_lines, replace); 279 | 280 | return userscript_lines; 281 | } 282 | 283 | function rem_nonce(text) { 284 | return text.replace(/nonce: "[0-9a-z]+" \/\/ imu:nonce = .*/, ""); 285 | } 286 | 287 | function get_commit_for(file) { 288 | var proc_result = child_process.spawnSync("git", ["log", "-1", file]); 289 | 290 | var stdout = proc_result.stdout.toString(); 291 | 292 | var commit = stdout.match(/^commit ([0-9a-f]{20,})/); 293 | if (!commit) { 294 | return null; 295 | } 296 | 297 | return commit[1]; 298 | } 299 | 300 | function modified_from_git(file) { 301 | var proc_result = child_process.spawnSync("git", ["diff-index", "HEAD", file]); 302 | 303 | return proc_result.stdout.length > 0; 304 | } 305 | 306 | function get_mb(text) { 307 | return parseInt((text.length / 1024 / 1024) * 10) / 10; 308 | } 309 | 310 | function start(userscript_filename) { 311 | var userscript_lines = util.read_as_lines(userscript_filename); 312 | var rules_lines = util.read_as_lines("tools/rules_template.js"); 313 | var bigimage_lines = util.read_as_lines("tools/bigimage_template.js"); 314 | 315 | var startend = get_bigimage(userscript_lines); 316 | if (!startend) 317 | return; 318 | 319 | var rules_js_lines = gen_rules_js(rules_lines, userscript_lines, startend); 320 | var rules_js = rules_js_lines.join("\n"); 321 | 322 | var changed = true; 323 | try { 324 | var new_rules_js = fs.readFileSync("build/rules.js").toString(); 325 | if (rem_nonce(new_rules_js) === rem_nonce(rules_js)) { 326 | console.log("Unchanged rules.js"); 327 | rules_js = new_rules_js; 328 | changed = false; 329 | } 330 | } catch (e) { 331 | console.error(e); 332 | } 333 | 334 | var modified = true; 335 | var commit = null; 336 | 337 | if (changed) { 338 | fs.writeFileSync("build/rules.js", replace_vars(rules_js)); 339 | } else { 340 | modified = modified_from_git("build/rules.js"); 341 | if (!modified) { 342 | commit = get_commit_for("build/rules.js"); 343 | if (!commit) { 344 | console.error("Unable to get commit for build/rules.js"); 345 | } 346 | } 347 | } 348 | 349 | nonce = JSON.parse(rules_js.match(/\/\/ imu:nonce = ("[0-9a-z]+")/)[1]); 350 | 351 | userscript_lines = gen_userscript(bigimage_lines, userscript_lines, startend); 352 | var userscript_js = userscript_lines.join("\n"); 353 | 354 | var userscript_require = userscript_js; 355 | if (commit) { 356 | var require_statement = [ 357 | "//", 358 | "// Greasyfork and OpenUserJS have 2MB and 1MB limits for userscripts (respectively).", 359 | "// Because of this, the rules (~" + get_mb(rules_js) + "MB) have been split into a separate file, linked below.", 360 | "// Note that jsdelivr.net might not always be reliable, but (AFAIK) this is the only reasonable option from what greasyfork allows.", 361 | "// I'd recommend using the Github version of the script if you encounter any issues (linked in the 'Project links' section below).", 362 | "//", 363 | "// @require https://cdn.jsdelivr.net/gh/qsniyg/maxurl@" + commit + "/build/rules.js" 364 | ].join("\n"); 365 | userscript_require = userscript_require 366 | .replace(/\n\/\/\s*@(?:name|description):en.*/g, "") // greasyfork no longer allows this 367 | .replace(/^\/\/\s*imu:require_rules.*/m, require_statement) 368 | .replace(/\n\/\/\/ All comments within bigimage.*\n\/\/\/ You can view.*/, ""); 369 | } 370 | 371 | // extr = external rules 372 | fs.writeFileSync("build/userscript_extr.user.js", userscript_require); 373 | 374 | fs.writeFileSync("build/userscript_extr_cat.user.js", rules_js + "\n\n" + userscript_js); 375 | } 376 | 377 | start("userscript_smaller.user.js"); 378 | -------------------------------------------------------------------------------- /tools/remcomments.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const util = require("./util.js"); 3 | 4 | var about = null; 5 | try { 6 | // git clone the gh-pages branch into site 7 | var about = require("../site/about.js"); 8 | } catch (e) { 9 | console.warn("about.js not found, not generating sites.txt"); 10 | } 11 | 12 | const process = require("process"); 13 | process.chdir(__dirname + "/.."); 14 | 15 | const child_process = require("child_process"); 16 | 17 | var do_watch = true; 18 | var do_rulesjs = true; 19 | 20 | for (var i = 3; i < process.argv.length; i++) { 21 | var arg = process.argv[i]; 22 | 23 | if (arg === "nowatch") do_watch = false; 24 | else if (arg === "norules") do_rulesjs = false; 25 | } 26 | 27 | var get_multidomain = function(name, userscript) { 28 | var multidomain_text = "common_functions(?:\\.|\\[\")" + name + "(?:\"\\])?\\s*=\\s*function\\s*\\(.*?\\)\\s*{\\s*(?://.*\\n\\s*)*return\\s+([\\s\\S]*?);\\s*};"; 29 | var multidomain_regex = new RegExp(multidomain_text); 30 | var match = userscript.match(multidomain_regex); 31 | if (!match) return null; 32 | 33 | return match[1]; 34 | }; 35 | 36 | var replace_multidomain = function(call, prevchar, line, userscript) { 37 | // comment 38 | if (prevchar === "/") return null; 39 | 40 | var multidomain_name = call.replace(/^common_functions(?:\.|\[")(.*?)(?:"\])?\s*\(.*/, "$1"); 41 | var is_host = /\(\s*host_domain/.test(call); 42 | 43 | var multidomain_text = get_multidomain(multidomain_name, userscript); 44 | if (!multidomain_text) return null; 45 | 46 | if (is_host) { 47 | multidomain_text = multidomain_text.replace(/domain([_\s])/g, "host_domain$1"); 48 | } 49 | 50 | multidomain_text = multidomain_text.replace(/^(.*?\n(\s+))/, "$2$1"); 51 | 52 | var indentation = util.get_line_indentation(line); 53 | if (prevchar === "(") indentation += "\t"; 54 | 55 | multidomain_text = util.indent(multidomain_text.split("\n"), indentation).join("\n").replace(/^\s*/, ""); 56 | 57 | return "(" + multidomain_text + ")"; 58 | }; 59 | 60 | var spaces_to_tabs = function(splitted) { 61 | for (var i = 0; i < splitted.length; i++) { 62 | let match = splitted[i].match(/^( +)(.*)$/); 63 | if (!match) 64 | continue; 65 | 66 | let ws = match[1].replace(/ {4}/g, "\t"); 67 | splitted[i] = ws + (match[2]||""); 68 | } 69 | 70 | return splitted; 71 | }; 72 | 73 | var normalize_tsstyle = function(script) { 74 | return script 75 | .replace(/function \((.*?)\)(\s*{)/g, "function($1)$2") 76 | .replace(/}\n\s*((?:catch|else)\s)/g, "} $1"); 77 | } 78 | 79 | var move_awaiter_generator = function(script) { 80 | let ag_regex = /\n(var __awaiter = [\s\S]*?\n};\nvar __generator = [\s\S]*?\n};)\r*\n/; 81 | let ag_match = script.match(ag_regex); 82 | if (!ag_match) { 83 | console.warn("Unable to find __awaiter and __generator"); 84 | return script; 85 | } 86 | let ag_script = ag_match[1]; 87 | script = script 88 | .replace(ag_regex, "\n") 89 | .replace(/(\n\(function\(\) {\n(?:\s*\/\/.*\r*\n)*)/, "$1" + ag_script + "\n"); 90 | return script; 91 | } 92 | 93 | var fill_current_version = function(script) { 94 | let versionmatch = script.match(/\/\/\s+@version\s+([0-9.]+)\s+\/\//); 95 | if (!versionmatch) { 96 | console.warn("Unable to find @version match"); 97 | return script; 98 | } 99 | 100 | let v = versionmatch[1]; 101 | 102 | let cvregex = /(var current_version = )null(;)/ 103 | if (!cvregex.test(script)) { 104 | console.warn("Unable to find current_version match"); 105 | return script; 106 | } 107 | 108 | script = script.replace(cvregex, "$1\"" + v + "\"$2"); 109 | return script; 110 | } 111 | 112 | function build_userscript_user_js(tsout) { 113 | var userscript = fs.readFileSync(tsout).toString(); 114 | userscript = normalize_tsstyle(userscript); 115 | userscript = move_awaiter_generator(userscript); 116 | userscript = fill_current_version(userscript); 117 | var lines = spaces_to_tabs(userscript.split("\n")); 118 | 119 | var newlines = []; 120 | for (const line of lines) { 121 | if (/^\/\/\/ 2 | Image Max URL 3 |

4 | 5 | --- 6 | 7 |

8 | English | Português (Brasil) 9 |

10 | 11 | --- 12 | 13 | Image Max URL is a program that will try to find larger/original versions of images and videos, usually by replacing URL patterns. 14 | 15 | It currently contains support for \>10,000 hardcoded websites (full list in [sites.txt](https://github.com/qsniyg/maxurl/blob/master/sites.txt)), 16 | but it also supports a number of generic engines (such as Wordpress and MediaWiki), which means it can work for many other websites as well. 17 | 18 | It is currently released as: 19 | 20 | - Userscript: (most browsers) 21 | - Stable: [userscript_smaller.user.js](https://github.com/qsniyg/maxurl/blob/master/userscript_smaller.user.js?raw=true) or [OpenUserJS](https://openuserjs.org/scripts/qsniyg/Image_Max_URL) 22 | - Development: [userscript.user.js](https://github.com/qsniyg/maxurl/blob/master/userscript.user.js?raw=true) (recommended) 23 | - It serves as the base for everything listed below. It also serves as a node module (used by the reddit bot), and can be embedded in a website. 24 | - Browser extension: [Firefox](https://addons.mozilla.org/firefox/addon/image-max-url/) 25 | - Other browsers supporting WebExtensions can sideload the extension through this git repository. 26 | - Since addons have more privileges than userscripts, it has a bit of extra functionality over the userscript. 27 | - Source code is in [manifest.json](https://github.com/qsniyg/maxurl/blob/master/manifest.json) and the [extension](https://github.com/qsniyg/maxurl/tree/master/extension) folder. 28 | - [Website](https://qsniyg.github.io/maxurl/) 29 | - Due to browser security constraints, some URLs (requiring cross-origin requests) can't be supported by the website. 30 | - Source code is in the [gh-pages](https://github.com/qsniyg/maxurl/tree/gh-pages) branch. 31 | - Reddit bot ([/u/MaxImageBot](https://www.reddit.com/user/MaxImageBot/)) 32 | - Source code is in [reddit-bot/comment-bot.js](https://github.com/qsniyg/maxurl/blob/master/reddit-bot/comment-bot.js) and [reddit-bot/dourl.js](https://github.com/qsniyg/maxurl/blob/master/reddit-bot/dourl.js) 33 | 34 | Community: 35 | 36 | - [Discord Server](https://discord.gg/fH9Pf54) 37 | - [Matrix](https://matrix.to/#/#image-max-url:tedomum.net?via=tedomum.net) (`#image-max-url:tedomum.net`) 38 | - [Subreddit](http://reddit.com/r/MaxImage) 39 | 40 | # Sideloading the extension 41 | 42 | The extension is currently unavailable to other browsers\' addon stores (such as Chrome and Microsoft Edge), 43 | but you can sideload this repository if you wish to use the extension version instead of the userscript. 44 | 45 | - Repository: 46 | - Download the repository however you wish (I\'d recommend cloning it through git as it allows easier updating) 47 | - Chromium: 48 | - Go to , make sure \"Developer mode\" is enabled, click \"Load unpacked \[extension\]\", and navigate to the maxurl repository 49 | - Firefox: 50 | - Go to \>This Firefox, select \"Load temporary Add-on\...\", and navigate to \"manifest.json\" within the maxurl repository 51 | - Note that the addon will be deleted once Firefox is closed. There\'s unfortunately nothing I can do about this. 52 | - CRX (Chromium-based browsers): 53 | - Download the CRX build from 54 | - Go to , make sure \"Developer mode\" is enabled, then drag&drop the downloaded CRX file onto the page. 55 | - XPI (Firefox-based browsers): 56 | - Download the XPI build from 57 | - Go to , click on the gear icon, then select \"Install Add-on from From File\...\", and navigate to the downloaded XPI file. 58 | 59 | ## Tor Browser/Mullvad Browser 60 | 61 | In order to use the extension on Tor Browser, ensure that the "Run in Private Windows" option is enabled in the page. 62 | 63 | Due to various anti-fingerprinting measures, many of the default shortcuts (such as any that include Alt) will not work. You'll need to rebind these. Some users prefer rebinding Shift+Alt+I to Ctrl+\`. 64 | 65 | # Contributing 66 | 67 | Any contribution is greatly appreciated! If you have any bug reports, feature requests, or new websites you want supported, please file an issue here. 68 | 69 | If you don't have a Github account, feel free to either use one of the community links above or [contact me directly](https://qsniyg.github.io/). 70 | 71 | If you wish to contribute to the repository itself (code contributions, translations, etc.), please check [CONTRIBUTING.md](https://github.com/qsniyg/maxurl/blob/master/CONTRIBUTING.md) 72 | for more information. 73 | 74 | # Integrating IMU in your program 75 | 76 | As mentioned above, userscript.user.js also functions as a node module. 77 | 78 | ```js 79 | var maximage = require('./userscript.user.js'); 80 | 81 | maximage(smallimage, { 82 | // If set to false, it will return only the URL if there aren't any special properties 83 | // Recommended to keep true. 84 | // 85 | // The only reason this option exists is as a small hack for a helper userscript used to find new rules, 86 | // to check if IMU already supports a rule. 87 | fill_object: true, 88 | 89 | // Maximum amount of times it should be run. 90 | // Recommended to be at least 5. 91 | iterations: 200, 92 | 93 | // Whether or not to store to, and use an internal cache for URLs. 94 | // Set this to "read" if you want to use the cache without storing results to it. 95 | use_cache: true, 96 | 97 | // Timeout (in seconds) for cache entries in the URL cache 98 | urlcache_time: 60*60, 99 | 100 | // List of "problems" (such as watermarks or possibly broken image) to exclude. 101 | // 102 | // By default, all problems are excluded. 103 | // You can access the excluded problems through maximage.default_options.exclude_problems 104 | // By setting it to [], no problems will be excluded. 105 | //exclude_problems: [], 106 | 107 | // Whether or not to exclude videos 108 | exclude_videos: false, 109 | 110 | // This will include a "history" of objects found through iterations. 111 | // Disabling this will only keep the objects found through the last successful iteration. 112 | include_pastobjs: true, 113 | 114 | // This will try to find the original page for an image, even if it requires extra requests. 115 | force_page: false, 116 | 117 | // This allows rules that use 3rd-party websites to find larger images 118 | allow_thirdparty: false, 119 | 120 | // This is useful for implementing a blacklist or whitelist. 121 | // If unspecified, it accepts all URLs. 122 | filter: function(url) { 123 | return true; 124 | }, 125 | 126 | // Helper function to perform HTTP requests, used for sites like Flickr 127 | // The API is expected to be like GM_xmlHTTPRequest's API. 128 | // An implementation using node's request module can be found in reddit-bot/dourl.js 129 | do_request: function(options) { 130 | // options = { 131 | // url: "", 132 | // method: "GET", 133 | // data: "", // for method: "POST" 134 | // overrideMimeType: "", // used to decode alternate charsets 135 | // headers: {}, // If a header is null or "", don't include that header 136 | // onload: function(resp) { 137 | // // resp is expected to be XMLHttpRequest-like object, implementing these fields: 138 | // // finalUrl 139 | // // readyState 140 | // // responseText 141 | // // status 142 | // } 143 | // } 144 | }, 145 | 146 | // Callback 147 | cb: function(result) { 148 | if (!result) 149 | return; 150 | 151 | if (result.length === 1 && result[0].url === smallimage) { 152 | // No larger image was found 153 | return; 154 | } 155 | 156 | for (var i = 0; i < result.length; i++) { 157 | // Do something with the object 158 | } 159 | } 160 | }); 161 | ``` 162 | 163 | The result is a list of objects that contain properties that may be useful in using the returned image(s): 164 | 165 | ```js 166 | [{ 167 | // The URL of the image 168 | url: null, 169 | 170 | // Whether or not this URL is a video 171 | video: false, 172 | 173 | // Whether it's expected that it will always work or not. 174 | // Don't rely on this value if you don't have to 175 | always_ok: false, 176 | 177 | // Whether or not the URL is likely to work. 178 | likely_broken: false, 179 | 180 | // Whether or not the server supports a HEAD request. 181 | can_head: true, 182 | 183 | // HEAD errors that can be ignored 184 | head_ok_errors: [], 185 | 186 | // Whether or not the server might return the wrong Content-Type header in the HEAD request 187 | head_wrong_contenttype: false, 188 | 189 | // Whether or not the server might return the wrong Content-Length header in the HEAD request 190 | head_wrong_contentlength: false, 191 | 192 | // This is used in the return value of the exported function. 193 | // If you're using a callback (as shown in the code example above), 194 | // this value will always be false 195 | waiting: false, 196 | 197 | // Whether or not the returned URL is expected to redirect to another URL 198 | redirects: false, 199 | 200 | // Whether or not the URL is temporary/only works on the current IP (such as a generated download link) 201 | is_private: false, 202 | 203 | // Whether or not the URL is expected to be the original image stored on the website's servers. 204 | is_original: false, 205 | 206 | // If this is true, you shouldn't input this URL again into IMU. 207 | norecurse: false, 208 | 209 | // Whether or not this URL should be used. 210 | // If true, treat this like a 404 211 | // If "mask", this image is an overlayed mask 212 | bad: false, 213 | 214 | // Same as above, but contains a list of objects, e.g.: 215 | // [{ 216 | // headers: {"Content-Length": "1000"}, 217 | // status: 301 218 | // }] 219 | // If one of the objects matches the response, it's a bad image. 220 | // You can use maximage.check_bad_if(bad_if, resp) to check. 221 | // (resp is expected to be an XHR-like object) 222 | bad_if: [], 223 | 224 | // Whether or not this URL is a "fake" URL that was used internally (i.e. if true, don't use this) 225 | fake: false, 226 | 227 | // Headers required to view the returned URL 228 | // If a header is null, don't include that header. 229 | headers: {}, 230 | 231 | // Additional properties that could be useful 232 | extra: { 233 | // The original page where this image was hosted 234 | page: null, 235 | 236 | // The title/caption attached to the image 237 | caption: null 238 | }, 239 | 240 | // If set, this is a more descriptive filename for the image 241 | filename: "", 242 | 243 | // A list of problems with this image. Use exclude_problems to exclude images with specific problems 244 | problems: { 245 | // If true, the image is likely larger than the one inputted, but it also has a watermark (when the inputted one doesn't) 246 | watermark: false, 247 | 248 | // If true, the image is likely smaller than the one inputted, but it has no watermark 249 | smaller: false, 250 | 251 | // If true, the image might be entirely different from the one inputted 252 | possibly_different: false, 253 | 254 | // If true, the image might be broken (such as GIFs on Tumblr) 255 | possibly_broken: false 256 | } 257 | }] 258 | ``` 259 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018-2024 qsniyg 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/cryptojs_aes.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, 8 | r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 9 | 32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, 10 | 2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, 11 | q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< 15 | l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); 16 | (function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, 17 | _doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), 18 | f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, 19 | m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, 20 | E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ 21 | 4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); 22 | (function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, 28 | this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, 29 | 1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, 30 | decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, 31 | b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); 32 | (function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, 33 | 16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> 34 | 8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= 35 | d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); 36 | 37 | var lib_export = CryptoJS; 38 | if (typeof module !== 'undefined') 39 | module.exports = lib_export; 40 | -------------------------------------------------------------------------------- /docs/pt/CONTRIBUTING.pt-BR.md: -------------------------------------------------------------------------------- 1 | ### Traduções 2 | 3 | As traduções são feitas através de arquivos padrão `.po` (gettext), localizados no [subdiretório `po`](https://github.com/qsniyg/maxurl/tree/master/po). Você pode traduzir manualmente usando um editor de texto ou utilizar ferramentas de tradução `.po`, como [Poedit](https://poedit.net/) ou [POEditor](https://poeditor.com/) (online). 4 | 5 | Para testar uma tradução modificada, execute: `node tools/update_from_po.js`. Isso atualizará `src/userscript.ts` com as traduções do subdiretório `po`. 6 | 7 | **Nota:** Ao enviar uma pull request para uma tradução, não inclua o arquivo `userscript.ts` modificado para evitar conflitos de mesclagem. 8 | 9 | Para adicionar suporte a um novo idioma, crie um novo arquivo `.po` para o código do idioma a partir de [po/imu.pot](https://github.com/qsniyg/maxurl/blob/master/po/imu.pot) e certifique-se de traduzir `$language_native$` (a palavra nativa para seu idioma, como "Français" para francês ou "한국어" para coreano). 10 | 11 | ### Contribuições de Sites/Regras 12 | 13 | Se você encontrar problemas com regras existentes ou quiser sugerir novos sites, **abra uma issue**. Pull requests são aceitas (especialmente se a regra for complexa), mas devido ao armazenamento atual em um único arquivo (`src/userscript.ts`), pode haver conflitos de mesclagem. 14 | 15 | Se decidir fazer uma pull request, siga estas diretrizes gerais. Não se preocupe em acertar perfeitamente; posso corrigir eventuais erros. 16 | 17 | - **Verifique se a regra já existe:** 18 | 19 | - Há a chance de a regra existir, mas sem suporte para o site específico. Pesquise no script usando regex para ver se uma regra semelhante já foi criada. 20 | 21 | - **Novas regras específicas de sites:** 22 | 23 | - Adicione antes da linha `// -- general rules --`. 24 | - Regras gerais são adicionadas no final da seção de regras gerais. 25 | - Algumas regras precisam estar acima de outras por vários motivos (ex.: regras específicas de host). 26 | 27 | - **Use `domain`, `domain_nosub` ou `domain_nowww` com comparação `===` para a verificação `if`:** 28 | 29 | - Para testes regex, use `/regex/.test(...)` após a comparação inicial `===`. 30 | Por exemplo, se você quiser corresponder a `img[0-9]+\.example\.com`, pode usar `if (domain_nosub === "example.com" && /^img[0-9]+\./.test(domain))`. 31 | Isso ajuda a garantir que o desempenho não será muito ruim. 32 | - `domain_nowww` corresponde a example.com e www.example.com. Use `domain_nowww` a menos que ambos os domínios sejam diferentes. 33 | - Para buckets da Amazon, use `amazon_container === "bucket"`. 34 | Note que ambas as formas de URL são geralmente (sempre?) válidas, então certifique-se de que a regra considera ambas. 35 | Por exemplo, tenha cuidado ao fazer algo como: `://[^/]+\/+images\/+`, pois não funcionará para a segunda forma. 36 | 37 | - **Use funções wrapper do script:** 38 | 39 | - Ex.: `array_indexof` ou `string_indexof` em vez de `foo.indexOf()`, `base64_decode` em vez de `atob`, `JSON_parse` em vez de `JSON.parse`. 40 | Isso ocorre porque alguns sites (ou bloqueadores de anúncios) substituem essas funções com implementações quebradas. 41 | O IMU usará sua própria implementação dessas funções se a versão do navegador falhar em alguns testes de sanidade. 42 | - Procure por `var JSON_parse` no userscript para encontrar uma lista delas. 43 | 44 | - **Funções auxiliares úteis:** (você pode encontrar uma lista delas em `variables_list` em [tools/gen_rules_js.js](https://github.com/qsniyg/maxurl/blob/master/tools/gen_rules_js.js)). Elas estão atualmente sem documentação, mas aqui está uma lista das mais comumente usadas: 45 | 46 | - `get_queries`: Retorna consultas como um objeto: 47 | - `get_queries("https://example.com/?a=5&b=10")` -> `{a: 5, b: 10}` 48 | - `remove_queries`: Remove consultas especificadas: 49 | - `remove_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"])` -> `"https://example.com/?a=5"` 50 | - `remove_queries("https://example.com/?a=5&b=10&c=20", "b")` -> `"https://example.com/?a=5&c=20"` 51 | 52 | - `keep_queries`: Remove todas as consultas, exceto as especificadas: 53 | - `keep_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"])` -> `"https://example.com/?b=10&c=20"` 54 | - `keep_queries("https://example.com/?a=5&b=10&c=20", "b")` -> `"https://example.com/?b=10"` 55 | - `keep_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"], {overwrite: {"c": 1, "d": 2}})` -> `"https://example.com/?b=10&c=1&d=2"` 56 | - `keep_queries("https://example.com/?a=5&b=10", ["b", "c"], {required: ["c"]})` -> `"https://example.com/?a=5&b=10"` 57 | - `add_queries`: Adiciona ou sobrescreve consultas: 58 | - `add_queries("https://example.com/?a=5&b=10", {b: 20, c: 30})` -> `"https://example.com/?a=5&b=20&c=30"` 59 | 60 | - `decodeuri_ifneeded`: Executa `decodeURIComponent` se uma URL parecer codificada: 61 | - `decodeuri_ifneeded("https%3A%2F%2Fexample.com%2F")` -> `"https://example.com/"` 62 | - `decodeuri_ifneeded("https%253A%252F%252Fexample.com%252F")` -> `"https://example.com/"` (suporta decodificação mais de uma vez) 63 | - `decodeuri_ifneeded("https://example.com/?a=5%20")` -> `"https://example.com/?a=5%20"` (inalterado porque `https://` não está codificado) 64 | - Use esta função se quiser retornar uma URL de uma consulta (por exemplo, `https://example.com/thumb.php?img=https%3A%2F%2Fexample.com%2Ftest.png`) 65 | 66 | - **Adicione casos de teste:** 67 | 68 | - Formato geral: 69 | 70 | ```ts 71 | // https://img1.example.com/thumbs/image.jpg -- imagem menor 72 | // https://img1.example.com/medium/image.jpg -- imagem maior 73 | // https://img1.example.com/images/image.jpg -- maior imagem 74 | ``` 75 | 76 | - O "formato" é bastante solto, então não se preocupe muito em acertá-lo. 77 | - **Por favor, não adicione nenhum caso de teste NSFW.** 78 | 79 | - **Estilo Regex:** 80 | 81 | - Identificadores de pasta (`/`) devem ser referidos como `/+` (a menos que o servidor web diferencie entre uma ou mais barras). 82 | - Strings de consulta ou hash podem incluir um `/`. Adicione `(?:[?#].*)?$` no final. 83 | - Mantenha a regra precisa: 84 | 85 | ```ts 86 | // https://www.example.com/images/image_500.jpg 87 | // https://www.example.com/images/image.jpg 88 | return src.replace(/(\/images\/+[^/?#]+)_[0-9]+(\.[^/.]+(?:[?#].*)?)$/, "$1$2"); // bom 89 | return src.replace(/_[0-9]+(\.[^/.]+(?:[?#].*)?)$/, "$1$2"); // ruim 90 | ``` 91 | 92 | - Embora não seja uma regra estrita, eu não uso `\d` ou `\w` porque acho que especificar exatamente quais caracteres são permitidos torna mais fácil de entender e modificar. Sua escolha :) 93 | 94 | - **Regras mais antigas podem não seguir essas diretrizes:** Atualize-as conforme necessário. 95 | 96 | - Você provavelmente verá que muitas das regras não seguem muitas das diretrizes acima. Regras mais recentes tendem a seguir melhor as diretrizes, mas regras mais antigas não foram atualizadas e são muitas vezes ou muito específicas ou muito genéricas como resultado. Tento atualizá-las conforme as vejo, mas como existem literalmente milhares de regras, e cada atualização muitas vezes quebra algo (o que significa que pelo menos algumas edições são necessárias para atualizar uma única regra), não consegui atualizar a maioria do script ainda. As maravilhas do software escrito organicamente! 97 | 98 | ### Para construir o userscript: 99 | 100 | - Build único: `npm run build` 101 | - Build e assistir por mudanças: `npm run watch` 102 | 103 | Pessoalmente, instalo `build/userscript_extr_cat.js` como um userscript no Violentmonkey, com a configuração "Track local file..." habilitada. Isso permite atualizações automáticas em cerca de 5 segundos após salvar. Usar o arquivo compilado em vez de `userscript.user.js` tem vantagens: 104 | 105 | - Devido ao tamanho do userscript, seu editor pode demorar para salvar o script inteiro, o que pode levar a uma condição de corrida onde o Violentmonkey atualizará uma versão incompleta do userscript. Embora ainda seja possível ao usar uma versão compilada, é significativamente menos provável. 106 | - Como a versão compilada é a que é publicada no Greasyfork/OUJS, caso haja algum problema com ela (como se uma variável compartilhada estiver faltando), isso permite que os problemas sejam detectados muito mais rapidamente. 107 | 108 | ### Chamadas de API/Regras de links de página 109 | 110 | Existem algumas considerações para implementar regras que usam chamadas de API: 111 | 112 | - Verifique `options.do_request` e `options.cb`: 113 | - Algumas partes do script chamam `bigimage` sem `do_request`, o que pode causar falhas nas chamadas de API. 114 | - `website_query` trata isso automaticamente. 115 | 116 | - Retorne `{waiting: true}` no final da função se o resultado for retornado em um callback (`options.cb`). 117 | 118 | - Caso contrário, resultará em comportamento inconsistente (como múltiplos popups). 119 | - Pense nisso como retornar uma Promise. 120 | 121 | - Use `api_cache` para reduzir chamadas de API duplicadas: 122 | 123 | - Prefira `api_cache.fetch` sobre `api_cache.has/get` + `api_cache.set`. 124 | - Você (provavelmente) não precisará interagir com `api_cache` se usar `api_query` ou `website_query` (mais sobre isso adiante) 125 | - Para a duração do cache, minha regra geral (embora um tanto arbitrária) é uma hora (`60*60`) para dados que foram gerados (ou que se espera que mudem dentro de um dia ou mais), e 6 horas (`6*60*60`) para dados permanentes, a menos que sejam enormes (por exemplo, páginas HTML, scripts ou imagens). 126 | 127 | - Use `api_query` em vez de `api_cache.fetch` + `options.do_request`. 128 | 129 | - Isso permite um código muito mais simples com menos indentação. Note que, embora `options.do_request` seja chamado implicitamente, você ainda deve verificá-lo. 130 | 131 | - Use regras de links de página (`website_query`) em vez de `api_query` ou `options.do_request`. 132 | 133 | - Regras de links de página são relativamente novas no script, mas permitem um código (geralmente) mais simples, acesso à mídia principal incorporada em uma página apenas a partir do link, e para deduplicação de código sem depender de `common_functions`. 134 | 135 | A ideia por trás das regras de links de página é suportar uma URL pública, geralmente (sempre?) uma página HTML, e então retornar a mídia principal (ou um álbum). 136 | 137 | ### Exemplo de regra de link de página: 138 | 139 | Para documentar, darei um exemplo com uma rede social imaginária: 140 | 141 | ```ts 142 | if (domain_nowww === "mysocialnetwork.com") { 143 | // Exemplo de URL: https://mysocialnetwork.com/post/123 144 | 145 | // Observe que `newsrc` é definido no início de `bigimage`, então não há necessidade de `var newsrc = ...`. 146 | newsrc = website_query({ 147 | // Expressão regular para URLs suportados. 148 | // O grupo de captura é o ID, usado internamente como chave de cache (mysocialnetwork.com:ID) 149 | // e externamente para consultar a página ou API. 150 | // Pode ser um array para suportar múltiplos padrões. 151 | website_regex: /^[a-z]+:\/\/[^/]+\/+post\/+([0-9]+)(?:[?#].*)?$/, 152 | 153 | // ${id} é substituído pelo primeiro grupo de captura. Também pode usar ${1}, ${2}, etc. para grupos adicionais. 154 | // Consulta a página e executa `process`. 155 | query_for_id: "https://mysocialnetwork.com/post/${id}", 156 | 157 | // Argumentos similares a `api_query`, com "match" adicionado, que é a correspondência da regex. 158 | process: function(done, resp, cache_key, match) { 159 | var img_match = resp.responseText.match(//); 160 | if (!img_match) { 161 | // Erro para facilitar a depuração se falhar 162 | console_error(cache_key, "Não foi possível encontrar correspondência de imagem para", resp); 163 | 164 | // Primeiro argumento é o resultado (nulo) e o segundo é o tempo de armazenamento (false significa não armazenar) 165 | return done(null, false); 166 | } 167 | 168 | var title = get_meta(resp.responseText, "og:description"); 169 | 170 | // Decodifica entidades HTML no interior das tags. 171 | // Adicionalmente, faz isso para garantir a correta exibição de fontes de imagens. 172 | var src = decode_entities(img_match[1]); 173 | 174 | return done({ 175 | url: src, 176 | extra: { 177 | caption: title 178 | } 179 | }, 6 * 60 * 60); // Armazena a imagem por 6 horas (em segundos) 180 | } 181 | }); 182 | 183 | // `newsrc` será undefined (se a URL não corresponder ou `options.do_request` não existir) ou {waiting: true} 184 | if (newsrc) return newsrc; 185 | } 186 | 187 | if (domain === "image.mysocialnetwork.com") { 188 | // Exemplo de URL: https://image.mysocialnetwork.com/postimage/123.jpg?width=500 189 | 190 | // Substitui a URL para remover parâmetros de consulta. 191 | newsrc = src.replace(/(\/postimage\/+[0-9]+\.[^/.?#]+)(?:[?#].*)?$/, "$1$2"); 192 | // Permite continuar para a próxima parte da regra se a URL não for substituída. 193 | // `bigimage` geralmente é executado mais de uma vez, resultando em um histórico, 194 | // o que permite cair de volta para esta URL (ou a anterior) se a próxima falhar. 195 | if (newsrc !== src) 196 | return newsrc; 197 | 198 | match = src.match(/\/postimage\/+([0-9]+)\./); 199 | if (match) { 200 | return { 201 | url: "https://mysocialnetwork.com/post/" + match[1], 202 | 203 | // Impede que redirecione ou consulte a página erroneamente no popup, 204 | // caso `bigimage` não seja executado novamente por qualquer motivo. 205 | is_pagelink: true 206 | }; 207 | } 208 | } 209 | ``` 210 | 211 | --- 212 | 213 | Agradecemos qualquer contribuição! 214 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Translations 4 | ============ 5 | 6 | Translations are done through standard .po (gettext) files, located in the [po subdirectory](https://github.com/qsniyg/maxurl/tree/master/po). 7 | You can either translate it manually using a text editor, or use one of the numerous .po translation tools, such as [poedit](https://poedit.net/) or [poeditor](https://poeditor.com/) (online). 8 | 9 | To test a modified translation, run: `node tools/update_from_po.js`. This will update src/userscript.ts with the translations from the po subdirectory. 10 | 11 | Note: when submitting a pull request for a translation, please do not include the modified userscript.ts, as it will increase the risk of merge conflicts. 12 | 13 | To add support for a new language, create a new .po file for the language code from [po/imu.pot](https://github.com/qsniyg/maxurl/blob/master/po/imu.pot), 14 | and make sure to translate `$language_native$` (the native word for your language, such as Français for French, or 한국어 for Korean). 15 | 16 | Website/rule contributions 17 | ========================== 18 | 19 | If you spot any issue with existing rules, or want to suggest a new websites, **the easiest way for the moment is if you file an issue**. 20 | 21 | Pull requests are also accepted (especially if the rule you want to submit is complex), but since everything is currently stored in one file (src/userscript.ts), 22 | it can lead to merge conflicts. 23 | 24 | ------------ 25 | 26 | If you decide to make a pull request, here are the general guidelines I follow when adding a new rule. Don't worry too much about getting it 27 | perfect though, I often get it wrong myself :) I can fix it up if you make a mistake. 28 | 29 | - Check if the rule already exists 30 | 31 | - There's a chance the rule might already exist, but without support for the specific website you want to add support for. 32 | Try doing a regex search of the script to see if a similar rule has already been created. 33 | 34 | - New website-specific rules are generally added before the `// -- general rules --` line (there's a large whitespace gap above it to make it clear). 35 | 36 | - General rules are added at the end of the general rules section (after the aforementioned line) 37 | - Sometimes some rules need to be above others for various reasons (e.g. host-specific rules). 38 | 39 | - Use `domain`, `domain_nosub` or `domain_nowww` with a `===` comparison if possible for the `if` check. 40 | 41 | - If a regex test is needed, use `/regex/.test(...)`, and always try to make sure that it's after the initial `===` comparison. 42 | For example, if you want to match `img[0-9]+\.example\.com`, you can use `if (domain_nosub === "example.com" && /^img[0-9]+\./.test(domain))`. 43 | This helps to ensure that performance won't be too terrible :) 44 | - `domain_nowww` matches both example.com and www.example.com. Unless both domains are different (or one is nonexistant), use `domain_nowww` 45 | when referring to either of these domains. 46 | - An exception for this is with amazon buckets (e.g. bucket.s3.amazonaws.com or s3.amazonaws.com/bucket/). Use `amazon_container === "bucket"` instead. 47 | Note that both URL forms are usually (always?) valid, so make sure the rule accounts for both. 48 | For example, be careful when doing something like: `://[^/]+\/+images\/+` as it won't work for the second form. 49 | 50 | - Use the script's wrapper functions over builtin functions: 51 | 52 | - For example, `array_indexof` or `string_indexof` instead of `foo.indexOf()`, `base64_decode` instead of `atob`, `JSON_parse` instead of `JSON.parse`, etc. 53 | This is because some websites (or adblock) override these functions with broken implementations. 54 | IMU will use its own implementation of these functions if the browser's version fails a few sanity checks. 55 | - Search for `var JSON_parse` in the userscript to find a list of them. 56 | 57 | - The script has a lot of helper functions that may be useful (you can find a list of them in `variables_list` in [tools/gen_rules_js.js](https://github.com/qsniyg/maxurl/blob/master/tools/gen_rules_js.js)). They're currently undocumented, but here is a list of commonly used ones: 58 | 59 | - `get_queries`: Returns the queries as an object: 60 | - `get_queries("https://example.com/?a=5&b=10")` -> `{a: 5, b: 10}` 61 | 62 | - `remove_queries`: Removes the specified queries: 63 | - `remove_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"])` -> `"https://example.com/?a=5"` 64 | - `remove_queries("https://example.com/?a=5&b=10&c=20", "b")` -> `"https://example.com/?a=5&c=20"` 65 | 66 | - `keep_queries`: Removes every query except for the specified queries: 67 | - `keep_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"])` -> `"https://example.com/?b=10&c=20"` 68 | - `keep_queries("https://example.com/?a=5&b=10&c=20", "b")` -> `"https://example.com/?b=10"` 69 | - `keep_queries("https://example.com/?a=5&b=10&c=20", ["b", "c"], {overwrite: {"c": 1, "d": 2}})` -> `"https://example.com/?b=10&c=1&d=2"` 70 | - `keep_queries("https://example.com/?a=5&b=10", ["b", "c"], {required: ["c"]})` -> `"https://example.com/?a=5&b=10"` 71 | 72 | - `add_queries`: Adds or overwrites queries: 73 | - `add_queries("https://example.com/?a=5&b=10", {b: 20, c: 30})` -> `"https://example.com/?a=5&b=20&c=30"` 74 | 75 | - `decodeuri_ifneeded`: Runs `decodeURIComponent` only if a url looks encoded: 76 | - `decodeuri_ifneeded("https%3A%2F%2Fexample.com%2F")` -> `"https://example.com/"` 77 | - `decodeuri_ifneeded("https%253A%252F%252Fexample.com%252F")` -> `"https://example.com/"` (it supports decoding more than one time) 78 | - `decodeuri_ifneeded("https://example.com/?a=5%20")` -> `"https://example.com/?a=5%20"` (unchanged because `https://` is not encoded) 79 | - Use this function if you want to return a URL from a query (e.g. `https://example.com/thumb.php?img=https%3A%2F%2Fexample.com%2Ftest.png`) 80 | 81 | - Add test cases 82 | 83 | - The general format is: 84 | 85 | ```ts 86 | // https://img1.example.com/thumbs/image.jpg -- smaller image (-- denotes a comment) 87 | // https://img1.example.com/medium/image.jpg -- a larger image of the one above available on the website that this rule also works for 88 | // https://img1.example.com/images/image.jpg -- largest image returned by this rule from any of the above (/medium/ or /thumbs/) 89 | ``` 90 | 91 | - The "format" is quite loose though, don't worry too much about getting it right. 92 | - **Please do not add any NSFW test cases.** 93 | 94 | - Regex style 95 | 96 | - Folder identifiers (`/`) should be referred to as `/+` (unless the web server distinguishes between one or more slashes) 97 | - Account for query strings or hash strings possibly including a /. The way I usually do it is to add `(?:[?#].*)?$` at the end 98 | - Try to keep the rule as tight as possible (within reason). For example: 99 | 100 | ```ts 101 | // https://www.example.com/images/image_500.jpg 102 | // https://www.example.com/images/image.jpg 103 | return src.replace(/(\/images\/+[^/?#]+)_[0-9]+(\.[^/.]+(?:[?#].*)?)$/, "$1$2"); // good 104 | return src.replace(/_[0-9]+(\.[^/.]+(?:[?#].*)?)$/, "$1$2"); // bad 105 | ``` 106 | 107 | - While not a strict rule, I don't use `\d` or `\w` as I find that specifying exactly which characters are allowed allows it to be easier 108 | to understand and modify. Your choice though :) 109 | 110 | - You'll probably see that a lot of the rules don't follow many of the guidelines above. More recent rules tend to follow the guidelines better, but older 111 | rules haven't been updated, and are often either too specific or too generic as a result. I try to update them as I see them, but since there are literally thousands 112 | of rules, and each update often breaks something (meaning at least a few edits are required to update a single rule), I haven't been able to update the 113 | majority of the script yet. The wonders of organically written software! 114 | 115 | As mentioned earlier, these are just guidelines, and you don't have to get it to be perfect to submit a rule :) 116 | 117 | To build the userscript: 118 | 119 | - Single build: `npm run build` 120 | - Build and watch for changes: `npm run watch` 121 | 122 | Personally I install build/userscript_extr_cat.js as a userscript under Violentmonkey, with the "Track local file..." setting enabled. This allows the userscript 123 | to be automatically updated within ~5 seconds after saving. Using the built file instead of userscript.user.js also has a few advantages: 124 | 125 | - Due to the size of the userscript, your editor might take a while to save the entire script, which can lead to a race condition where Violentmonkey will 126 | update an incomplete version of the userscript. While it's still possible when using a built version, it's significantly less likely. 127 | - Since the built version is the one that is published on Greasyfork/OUJS, in case there are any issues with it (such as if a shared variable is missing), 128 | this allows one to catch the issues much quicker. 129 | 130 | ### API calls/Pagelink rules 131 | 132 | There are a few considerations for implementing rules that use API calls: 133 | 134 | - Check for `options.do_request` and `options.cb` 135 | 136 | - There are a few parts of the script that call `bigimage` without `do_request`, which will cause API calls to crash if the check isn't present. 137 | - This isn't required if you use `website_query`, it will do this automatically for you. 138 | 139 | - Return `{waiting: true}` at the end of the function if the result will be returned in a callback (`options.cb`). 140 | 141 | - Otherwise it will result in inconsistent behavior (such as multiple popups). 142 | - Think of this like returning a Promise. 143 | 144 | - Use `api_cache` wherever possible in order to reduce duplicate API calls. 145 | 146 | - Unless there's a good reason not to, prefer `api_cache.fetch` over `api_cache.has/get` + `api_cache.set`. This allows for much simpler logic, as well as avoiding races. 147 | - You (likely) don't need to interact with `api_cache` if using `api_query` or `website_query` (more on that later) 148 | - For cache duration, my general (though admittedly rather arbitrary) rule is an hour (`60*60`) for data that has been generated (or is otherwise expected to change within a day or so), and 6 hours (`6*60*60`) for permanent data, unless it's huge (e.g. html pages, scripts, or images). 149 | 150 | - Use `api_query` over `api_cache.fetch` + `options.do_request` if possible. 151 | 152 | - This allows for much simpler code with less indentation. Note that even though `options.do_request` is called implicitly, you must still check for it. 153 | 154 | - Use pagelink rules (`website_query`) over `api_query` or direct `options.do_request` if possible. 155 | 156 | - Pagelink rules are relatively new to the script, but allow for (usually) simpler code, access to the main media embedded in a page from only the link, and for code deduplication without relying on `common_functions`. 157 | 158 | The idea behind pagelink rules is to support a public-facing URL, generally (always?) an HTML page, then return the main media (or an album). 159 | 160 | To document it, I'll give an example with an imaginary social network: 161 | 162 | ```ts 163 | if (domain_nowww === "mysocialnetwork.com") { 164 | // https://mysocialnetwork.com/post/123 165 | 166 | // Note that newsrc is defined at the beginning of bigimage, so no need to `var newsrc = ...`. 167 | newsrc = website_query({ 168 | // Regex(es) for the supported URLs. 169 | // The capture group is the ID, which will internally be used for the cache key (mysocialnetwork.com:ID), 170 | // as well as externally to query the page or API call. 171 | // This can also be an array (for multiple different patterns) if needed. 172 | website_regex: /^[a-z]+:\/\/[^/]+\/+post\/+([0-9]+)(?:[?#].*)?$/, 173 | 174 | // ${id} is replaced to the first capture. You can also use ${1}, ${2}, etc. for the first, second, ... capture group. 175 | // This will query the page, then run `process`. 176 | query_for_id: "https://mysocialnetwork.com/post/${id}", 177 | 178 | // Same arguments as for api_query, with "match" added, which is the regex match. 179 | process: function(done, resp, cache_key, match) { 180 | var img_match = resp.responseText.match(//); 181 | if (!img_match) { 182 | // An error to make it easier to debug if it fails 183 | console_error(cache_key, "Unable to find image match for", resp); 184 | 185 | // First argument is the result (null) and the second is how long to store it (false means not to store it) 186 | return done(null, false); 187 | } 188 | 189 | var title = get_meta(resp.responseText, "og:description"); 190 | 191 | // Remember that the inside of tags may also use html entities (such as "). 192 | // While unlikely for image sources, it also never hurts to add this. 193 | var src = decode_entities(img_match[1]); 194 | 195 | return done({ 196 | url: src, 197 | extra: { 198 | caption: title 199 | } 200 | }, 6*60*60); 201 | // Though it's arbitrary, 6*60*60 (6 hours measured in seconds) is used for images that will not expire. 202 | } 203 | }); 204 | 205 | // newsrc will either be undefined (if the URL doesn't match, or if options.do_request doesn't exist), or {waiting: true} 206 | if (newsrc) return newsrc; 207 | } 208 | 209 | if (domain === "image.mysocialnetwork.com") { 210 | // https://image.mysocialnetwork.com/postimage/123.jpg?width=500 211 | 212 | newsrc = src.replace(/(\/postimage\/+[0-9]+\.[^/.?#]+)(?:[?#].*)?$/, "$1$2"); 213 | // This allows us to fall through to the next portion of the rule if the URL hasn't been replaced. 214 | // Since bigimage is (generally) run more than once, this will result in a history, 215 | // which allows it to fall back to this URL (or the previous) if the next one fails. 216 | if (newsrc !== src) 217 | return newsrc; 218 | 219 | match = src.match(/\/postimage\/+([0-9]+)\./); 220 | if (match) { 221 | return { 222 | url: "https://mysocialnetwork.com/post/" + match[1], 223 | 224 | // In case bigimage isn't run again for whatever reason, this will prevent it from wrongfully 225 | // redirecting to/querying the page in the popup. 226 | is_pagelink: true 227 | }; 228 | } 229 | } 230 | ``` 231 | 232 | --- 233 | 234 | Thank you very much for whatever contribution you wish to give to this script, it's really appreciated! 235 | --------------------------------------------------------------------------------