First_Name

2ch-autosolver

May 4th, 2026 (edited)
1,752
1
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 36.42 KB | Source Code | 1 0
  1. /* Автоматический разгадыватель капчи из 4ёх искажённых картинок на форуме 2ch.su. Установи браузерное расширение по типу Tampermonkey ( https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ru&pli=1 ) и скопируй скрипт туда. Для смартфона подойдёт Firefox, Kiwi Browser и др.
  2.  
  3. Код можно скормить нейронке и она подтвердит что он безопасен.
  4.  
  5. Если есть проблемы при установке или баги, пишите в Github или прямо в комментах к этому посту */
  6.  
  7. // ==UserScript==
  8. // @name         2ch-autosolver
  9. // @namespace    http://tampermonkey.net/
  10. // @version      20.06.2026
  11. // @description  См. выше
  12. // @include      https://2ch.su/*
  13. // @include      https://2ch.org/*
  14. // @include      https://2ch.life/*
  15. // @icon         https://www.google.com/s2/favicons?sz=64&domain=2ch.su
  16. // @require      https://code.jquery.com/jquery-3.1.0.min.js
  17. // @require      https://gist.githubusercontent.com/JackOutsideTheBox/ca3d4aec3e1464d26c2f3c19f88d03de/raw/6447d74eccbba9378ccdcc066f73017265c5f1d2/opencvwithsift.js
  18. // @grant        none
  19. // ==/UserScript==
  20.  
  21. /**
  22.  * Captcha Autosolver - JavaScript implementation using OpenCV.js
  23.  * Performs SIFT-based feature matching to identify candidate icons within captcha images
  24.  */
  25.  
  26. (async () => {
  27.     cv = (cv instanceof Promise) ? await cv : cv;
  28.  
  29.     class KpDesc {
  30.         constructor() {
  31.             this.keypoints;
  32.             this.descriptors;
  33.         }
  34.     }
  35.  
  36.     class CandidateInfo {
  37.         constructor(image) {
  38.             this.image = image; // cv.Mat
  39.             this.keypoints = new cv.KeyPointVector();
  40.             this.descriptors = cv.Mat.zeros(this.image.rows, this.image.cols, cv.CV_32F);
  41.         }
  42.     }
  43.  
  44.     class CandidateResult {
  45.         constructor(info, finalScore, combinedPoints, bestWindowPoints, approved) {
  46.             this.info = info; //CandidateInfo
  47.             this.finalScore = finalScore;
  48.             this.combinedPoints = combinedPoints; // array of { x, y } points
  49.             this.bestWindowPoints = bestWindowPoints; // array of { x, y } points
  50.             this.approved = approved;
  51.         }
  52.     }
  53.  
  54.     class PointSet {
  55.         constructor() {
  56.             this.map = new Map();
  57.         }
  58.  
  59.         add(point) {
  60.             this.map.set(`${point.x},${point.y}`, point);
  61.         }
  62.  
  63.         has(point) {
  64.             return this.map.has(`${point.x},${point.y}`);
  65.         }
  66.  
  67.         get size() {
  68.             return this.map.size;
  69.         }
  70.  
  71.         values() {
  72.             return this.map.values();
  73.         }
  74.  
  75.         [Symbol.iterator]() {
  76.             return this.map.values();
  77.         }
  78.     }
  79.  
  80.     class CaptchaAutosolver {
  81.         constructor() {
  82.             this.candidateInfos = [];
  83.             this.results = new Map(); //img -> CandidateResult
  84.             this.originalCaptcha = null; // cv.Mat
  85.             this.captchaVariants = [];
  86.  
  87.             // Algorithm parameters
  88.             // Array of SIFT detectors with progressively more aggressive settings
  89.             this.siftDetectors = [
  90.                 new cv.SIFT(0, 3, 0.04, 10, 1.2),     // default / conservative
  91.                 new cv.SIFT(0, 5, 0.03, 20, 1.2),     // slightly more keypoints
  92.                 new cv.SIFT(0, 10, 0.02, 30, 1.2),    // more aggressive
  93.                 new cv.SIFT(0, 15, 0.01, 40, 1.2)     // most aggressive
  94.             ];
  95.             this.captchaDetector = new cv.SIFT(0, 3, 0.04, 10, 1.6);
  96.  
  97.             this.MAX_SUBREGION_WIDTH = 60;
  98.             this.MAX_SUBREGION_HEIGHT = 60;
  99.         }
  100.  
  101.         /**
  102.        * Clean up captcha variants and keypoint/descriptor data
  103.        * @public
  104.        */
  105.         cleanup() {
  106.             this.results.clear();
  107.  
  108.             for (const variant of this.captchaVariants) {
  109.                 variant.delete();
  110.             }
  111.             for (const kpd of this.captchaKpDesc) {
  112.                 kpd.keypoints.delete();
  113.                 kpd.descriptors.delete();
  114.             }
  115.             this.captchaVariants = [];
  116.             this.captchaKpDesc = [];
  117.         }
  118.  
  119.         /**
  120.        * Convert HTMLImageElement to cv.Mat
  121.        * @private
  122.        * @param {HTMLImageElement} imgElement - HTML image element
  123.        * @returns {cv.Mat} The image as cv.Mat
  124.        */
  125.         _imgToMat(imgElement) {
  126.             const canvas = document.createElement('canvas');
  127.             canvas.width = imgElement.width;
  128.             canvas.height = imgElement.height;
  129.  
  130.             const ctx = canvas.getContext('2d');
  131.             ctx.drawImage(imgElement, 0, 0);
  132.  
  133.             // Create cv.Mat from canvas (returns RGBA)
  134.             const mat = cv.imread(canvas);
  135.  
  136.             // Convert RGBA to BGR (OpenCV standard)
  137.             /* const bgrMat = cv.Mat.zeros(mat.rows, mat.cols, cv.CV_8UC3);
  138.             cv.cvtColor(mat, bgrMat, cv.COLOR_RGBA2BGR);
  139.             //mat.delete();
  140.  
  141.             return bgrMat; */
  142.             this._opaqueToWhite(mat);
  143.             return mat;
  144.         }
  145.  
  146.         /**
  147.        * Load captcha image from an HTML img element with data:image URI
  148.        * @param {HTMLImageElement|string} imageSource - HTML img element or image element ID containing data:image
  149.        */
  150.         loadCaptcha(imageSource) {
  151.             let imgElement;
  152.  
  153.             if (typeof imageSource === 'string') {
  154.                 imgElement = document.getElementById(imageSource);
  155.                 if (!imgElement) {
  156.                     console.error(`Image element with ID '${imageSource}' not found`);
  157.                     return;
  158.                 }
  159.             } else {
  160.                 imgElement = imageSource;
  161.             }
  162.  
  163.             if (!(imgElement instanceof HTMLImageElement)) {
  164.                 console.error("Source must be an HTMLImageElement or valid element ID");
  165.                 return;
  166.             }
  167.  
  168.             // Convert img element to canvas, then to cv.Mat
  169.             const captchaImage = this._imgToMat(imgElement);
  170.  
  171.             if (captchaImage.empty()) {
  172.                 console.error("Failed to convert image element to cv.Mat");
  173.                 return;
  174.             }
  175.  
  176.             this._preprocessCaptcha(captchaImage);
  177.             captchaImage.delete();
  178.         }
  179.  
  180.         /**
  181.        * Preprocess captcha image and prepare variants for matching
  182.        * @private
  183.        * @param {cv.Mat} captchaImage - The captcha image to preprocess
  184.        */
  185.         _preprocessCaptcha(captchaImage) {
  186.             // Store original captcha for later use
  187.             this.originalCaptcha = captchaImage.clone();
  188.  
  189.             // Prepare captcha variants: Lab channels and their inversions
  190.  
  191.             // Convert to Lab color space
  192.             const reschemedCaptcha = new cv.Mat();
  193.             cv.cvtColor(captchaImage, reschemedCaptcha, cv.COLOR_RGB2Lab);
  194.  
  195.             // Split Lab channels
  196.             const channels = new cv.MatVector();
  197.             cv.split(reschemedCaptcha, channels);
  198.  
  199.             // Apply CLAHE to each channel
  200.             const clahe = new cv.CLAHE(5, new cv.Size(4, 4));
  201.  
  202.             for (let i = 0; i < 3; i++) {
  203.                 const channel = channels.get(i);
  204.                 const processed = new cv.Mat();
  205.                 clahe.apply(channel, processed);
  206.  
  207.                 // Add channel and its inversion
  208.                 const inverted = new cv.Mat();
  209.                 cv.bitwise_not(processed, inverted);
  210.  
  211.                 this.captchaVariants.push(processed);
  212.                 this.captchaVariants.push(inverted);
  213.             }
  214.  
  215.             // Add grayscale variants
  216.             const greyscaleCaptcha = new cv.Mat();
  217.             cv.cvtColor(captchaImage, greyscaleCaptcha, cv.COLOR_BGR2GRAY);
  218.  
  219.             const equalCaptcha = new cv.Mat();
  220.             cv.equalizeHist(greyscaleCaptcha, equalCaptcha);
  221.  
  222.             const claheCaptcha = new cv.Mat();
  223.             clahe.apply(greyscaleCaptcha, claheCaptcha);
  224.  
  225.             const invGreyscale = new cv.Mat();
  226.             cv.bitwise_not(greyscaleCaptcha, invGreyscale);
  227.  
  228.             const invEqualCaptcha = new cv.Mat();
  229.             cv.bitwise_not(equalCaptcha, invEqualCaptcha);
  230.  
  231.             const invClaheCaptcha = new cv.Mat();
  232.             cv.bitwise_not(claheCaptcha, invClaheCaptcha);
  233.  
  234.             this.captchaVariants.push(greyscaleCaptcha);
  235.             this.captchaVariants.push(invGreyscale);
  236.  
  237.             this.captchaVariants.push(equalCaptcha);
  238.             this.captchaVariants.push(invEqualCaptcha);
  239.  
  240.             this.captchaVariants.push(claheCaptcha);
  241.             this.captchaVariants.push(invClaheCaptcha);
  242.  
  243.             // Extract keypoints and descriptors for all captcha variants
  244.             const captchaKpDesc = [];
  245.             for (let i = 0; i < this.captchaVariants.length; i++) {
  246.                 const kp = new KpDesc();
  247.                 kp.keypoints = new cv.KeyPointVector();
  248.                 kp.descriptors = cv.Mat.zeros(this.captchaVariants[i].rows, this.captchaVariants[i].cols, cv.CV_32F);
  249.  
  250.                 this.captchaDetector.detectAndCompute(this.captchaVariants[i], new cv.Mat(), kp.keypoints, kp.descriptors);
  251.                 captchaKpDesc.push(kp);
  252.             }
  253.  
  254.             this.captchaKpDesc = captchaKpDesc;
  255.  
  256.             // Cleanup temporary mats
  257.             reschemedCaptcha.delete();
  258.             channels.delete();
  259.         }
  260.  
  261.         /**
  262.        * Perform feature matching between candidate and captcha variants
  263.        * Uses cached results if candidate has been matched before
  264.        * @param {cv.Mat} img - The candidate icon
  265.        * @param {Array} excludedWindows - Windows to exclude from matching
  266.        * @returns {CandidateResult} Result containing match information and best window points
  267.        */
  268.         matchCandidate(img, excludedWindows = []) {
  269.             const compensatePoorKpCount = (result) => {
  270.                 const keypointCount = result.info.keypoints.length;
  271.                 if (keypointCount <= -100) { //turn off
  272.                     result.finalScore = Math.round((100.0 / keypointCount) * result.finalScore);
  273.                 }
  274.             };
  275.  
  276.             // Check if we've already calculated matches for this candidate
  277.             if (this.results.has(img.src)) {
  278.                 const result = this.results.get(img.src);
  279.                 const newBestWindow = this._findBestWindow(result.combinedPoints, excludedWindows);
  280.                 result.bestWindowPoints = newBestWindow;
  281.                 result.finalScore = newBestWindow.length;
  282.  
  283.                 compensatePoorKpCount(result);
  284.                 return result;
  285.             }
  286.  
  287.             let candidateImage = this._imgToMat(img);
  288.  
  289.             // Resize 2x using INTER_CUBIC interpolation
  290.             let upscaledCandidateImage = new cv.Mat();
  291.             let size = new cv.Size(candidateImage.cols * 2, candidateImage.rows * 2);
  292.             cv.resize(candidateImage, upscaledCandidateImage, size, 0, 0, cv.INTER_CUBIC);
  293.  
  294.             const candidateInfo = new CandidateInfo(upscaledCandidateImage);
  295.  
  296.             // Try each SIFT detector with increasing aggressiveness
  297.             for (let i = 0; i < this.siftDetectors.length; i++) {
  298.  
  299.                 this.siftDetectors[i].detectAndCompute(candidateInfo.image, new cv.Mat(), candidateInfo.keypoints, candidateInfo.descriptors);
  300.  
  301.                 if (candidateInfo.keypoints.size() >= 100) {
  302.                     console.log(`Used SIFT instance ${i} for candidate ${img.src} with ${candidateInfo.keypoints.size()} keypoints.`);
  303.                     break;
  304.                 }
  305.             }
  306.  
  307.             if (candidateInfo.keypoints.size() < 100) {
  308.                 console.warn(`Even with maximum precision candidate ${img.src} only got ${candidateInfo.keypoints.size()} keypoints.`);
  309.             }
  310.  
  311.             this.candidateInfos.push(candidateInfo);
  312.             candidateImage.delete();
  313.  
  314.             const candidateKpts = candidateInfo.keypoints;
  315.             const candidateDpts = candidateInfo.descriptors;
  316.             const combinedPointsSet = new PointSet();
  317.  
  318.             for (let i = 0; i < this.captchaVariants.length; i++) {
  319.                 const captchaKpts = this.captchaKpDesc[i].keypoints;
  320.                 const captchaDpts = this.captchaKpDesc[i].descriptors;
  321.  
  322.                 if (!captchaDpts || captchaDpts.empty() || !candidateDpts || candidateDpts.empty()) {
  323.                     continue;
  324.                 }
  325.  
  326.                 // Perform KNN matching with k=2
  327.                 const loweMatches = [];
  328.                 const matcher = new cv.BFMatcher(cv.NORM_L2, false);
  329.                 const knnMatches = new cv.DMatchVectorVector();
  330.  
  331.                 matcher.knnMatch(candidateDpts, captchaDpts, knnMatches, 2);
  332.  
  333.                 // Apply Lowe's ratio test
  334.                 for (let j = 0; j < knnMatches.size(); j++) {
  335.                     const matches = knnMatches.get(j);
  336.                     if (matches.size() === 2 && matches.get(0).distance < 0.8 * matches.get(1).distance) {
  337.                         loweMatches.push(matches.get(0));
  338.                     }
  339.                 }
  340.  
  341.                 const uniquePoints = new PointSet();
  342.  
  343.                 if (loweMatches.length >= 4) {
  344.                     // Use RANSAC to estimate affine transformation
  345.                     const candidatePts = [];
  346.                     const captchaPts = [];
  347.  
  348.                     for (const m of loweMatches) {
  349.                         candidatePts.push(candidateKpts.get(m.queryIdx).pt);
  350.                         captchaPts.push(captchaKpts.get(m.trainIdx).pt);
  351.                     }
  352.  
  353.                     // Convert to cv.Mat format for estimateAffineTransform
  354.                     const srcMat = cv.matFromArray(candidatePts.length, 1, cv.CV_32FC2, candidatePts.flatMap(p => [p.x, p.y]));
  355.                     const dstMat = cv.matFromArray(captchaPts.length, 1, cv.CV_32FC2, captchaPts.flatMap(p => [p.x, p.y]));
  356.  
  357.                     const inliersMask = new cv.Mat();
  358.                     const affineMatrix = cv.estimateAffine2D(srcMat, dstMat, inliersMask, cv.RANSAC, 10.0);
  359.  
  360.                     // Collect inlier points
  361.                     for (let j = 0; j < loweMatches.length; j++) {
  362.                         if (inliersMask.ucharPtr(j)[0]) {
  363.                             const point = captchaKpts.get(loweMatches[j].trainIdx).pt;
  364.                             uniquePoints.add(point);
  365.                         }
  366.                     }
  367.  
  368.                     srcMat.delete();
  369.                     dstMat.delete();
  370.                     inliersMask.delete();
  371.                     affineMatrix?.delete();
  372.                 } else {
  373.                     // If fewer than 4 matches, collect all points
  374.                     for (const m of loweMatches) {
  375.                         const point = captchaKpts.get(m.trainIdx).pt;
  376.                         uniquePoints.add(point);
  377.                     }
  378.                 }
  379.  
  380.                 // Filter applicable subregion and add to combined points
  381.                 if (uniquePoints.size > 0) {
  382.                     const pointsArray = Array.from(uniquePoints);
  383.                     const [isApplicable, filteredPoints] = this._isApplicableSubregion(pointsArray);
  384.  
  385.                     for (const pt of filteredPoints) {
  386.                         combinedPointsSet.add(pt);
  387.                     }
  388.                 }
  389.  
  390.                 matcher.delete();
  391.                 knnMatches.delete();
  392.             }
  393.  
  394.             // Convert combined points to array
  395.             const combinedPoints = Array.from(combinedPointsSet);
  396.  
  397.             // Find best window without exclusions
  398.             const bestWindowPoints = this._findBestWindow(combinedPoints, excludedWindows);
  399.  
  400.             const result = new CandidateResult(
  401.                 candidateInfo,
  402.                 bestWindowPoints.length,
  403.                 combinedPoints,
  404.                 bestWindowPoints,
  405.                 false
  406.             );
  407.  
  408.             compensatePoorKpCount(result);
  409.  
  410.             this.results.set(img.src, result);
  411.  
  412.             return result;
  413.         }
  414.  
  415.         /**
  416.          * Check if a subregion is applicable (has at least 2 points in best window)
  417.          * @private
  418.          */
  419.         _isApplicableSubregion(regionPoints) {
  420.             if (regionPoints.length < 2) {
  421.                 return [false, []];
  422.             }
  423.  
  424.             const bestWindowPoints = this._findBestWindow(regionPoints);
  425.  
  426.             if (bestWindowPoints.length >= 2) {
  427.                 return [true, bestWindowPoints];
  428.             }
  429.  
  430.             return [false, []];
  431.         }
  432.  
  433.         /**
  434.          * Find best window for maximum point density
  435.          * @private
  436.          */
  437.         _findBestWindow(combinedPoints, excludedWindows = []) {
  438.             let filteredCombinedPoints = [];
  439.  
  440.             if (excludedWindows.length === 0) {
  441.                 filteredCombinedPoints = [...combinedPoints];
  442.             } else {
  443.                 // Filter out points inside excluded rectangles
  444.                 for (const pt of combinedPoints) {
  445.                     let excluded = false;
  446.                     for (const r of excludedWindows) {
  447.                         if (pt.x >= r.x && pt.x < r.x + r.width &&
  448.                             pt.y >= r.y && pt.y < r.y + r.height) {
  449.                             excluded = true;
  450.                             break;
  451.                         }
  452.                     }
  453.                     if (!excluded) {
  454.                         filteredCombinedPoints.push(pt);
  455.                     }
  456.                 }
  457.             }
  458.  
  459.             if (filteredCombinedPoints.length === 0) {
  460.                 return [];
  461.             }
  462.  
  463.             // Find bounding box of all points
  464.             const ptsMat = cv.matFromArray(filteredCombinedPoints.length, 1, cv.CV_32FC2, filteredCombinedPoints.flatMap(p => [p.x, p.y]));
  465.             const boundingBox = cv.boundingRect(ptsMat);
  466.             ptsMat.delete();
  467.  
  468.             // Sliding window search
  469.             let bestScore = 0;
  470.             let bestWindow = new cv.Rect(0, 0, this.MAX_SUBREGION_WIDTH, this.MAX_SUBREGION_HEIGHT);
  471.  
  472.             const stepX = Math.floor(this.MAX_SUBREGION_WIDTH / 3);
  473.             const stepY = Math.floor(this.MAX_SUBREGION_HEIGHT / 3);
  474.  
  475.             // Search within and beyond bounding box
  476.             for (let x = boundingBox.x - this.MAX_SUBREGION_WIDTH; x < boundingBox.x + boundingBox.width; x += stepX) {
  477.                 for (let y = boundingBox.y - this.MAX_SUBREGION_HEIGHT; y < boundingBox.y + boundingBox.height; y += stepY) {
  478.                     const currentWindow = new cv.Rect(x, y, this.MAX_SUBREGION_WIDTH, this.MAX_SUBREGION_HEIGHT);
  479.  
  480.                     // Count points within this window
  481.                     let pointsInWindow = 0;
  482.                     for (const pt of filteredCombinedPoints) {
  483.                         if (pt.x >= currentWindow.x && pt.x < currentWindow.x + currentWindow.width &&
  484.                             pt.y >= currentWindow.y && pt.y < currentWindow.y + currentWindow.height) {
  485.                             pointsInWindow++;
  486.                         }
  487.                     }
  488.  
  489.                     // Update best window if this covers more points
  490.                     if (pointsInWindow > bestScore) {
  491.                         bestScore = pointsInWindow;
  492.                         bestWindow = currentWindow;
  493.                     }
  494.                 }
  495.             }
  496.  
  497.             // Extract points covered by best window
  498.             const bestWindowPoints = [];
  499.             for (const pt of filteredCombinedPoints) {
  500.                 if (pt.x >= bestWindow.x && pt.x < bestWindow.x + bestWindow.width &&
  501.                     pt.y >= bestWindow.y && pt.y < bestWindow.y + bestWindow.height) {
  502.                     bestWindowPoints.push(pt);
  503.                 }
  504.             }
  505.  
  506.             return bestWindowPoints;
  507.         }
  508.  
  509.         /**
  510.          * Convert semi-transparent pixels to white
  511.          * @private
  512.          */
  513.         _opaqueToWhite(img) {
  514.             const data = img.data;
  515.             const rows = img.rows;
  516.             const cols = img.cols;
  517.             const channels = img.channels();
  518.  
  519.             if (channels === 4) {
  520.                 // RGBA format
  521.                 for (let i = 0; i < rows * cols * 4; i += 4) {
  522.                     const alpha = data[i + 3];
  523.                     if (alpha < 100) {
  524.                         data[i] = 255;     // Blue
  525.                         data[i + 1] = 255; // Green
  526.                         data[i + 2] = 255; // Red
  527.                         data[i + 3] = 255; // Alpha
  528.                     }
  529.                 }
  530.             }
  531.         }
  532.     }
  533.  
  534.     $('._captcha').prepend(`
  535. <div class="captcha__autosolver-wrapper">
  536.   <div class="captcha__autosolver">
  537.     <div class="options__box">
  538.       <input type="checkbox" js-target-autosolve id="autosolve">
  539.       <label for="autosolve"><span title="Заработает со следующей капчи">Автоотгадывание капчи</span></label>
  540.     </div>
  541.     <div class="options__box" style="visibility: hidden">
  542.       <input type="checkbox" js-target-autoresend id="autoresend">
  543.       <label for="autoresend"><span title="Отгадывать, пока не отправится">Автопереотправка при неудаче</span></label>
  544.     </div>
  545.     <div class="options__box" style="visibility: hidden">
  546.       <input type="checkbox" js-target-hidecaptcha id="hidecaptcha">
  547.       <label for="hidecaptcha"><span title="">Скрыть капчу</span></label>
  548.     </div>
  549.   </div>
  550. </div>`).first().prepend(`
  551.     <style id="autosolver-style">
  552.       .captcha__autosolver-wrapper {
  553.           margin-bottom: 20px;
  554.       }
  555.       .captcha__autosolver {
  556.           margin: 10px auto -10px auto;
  557.           width: fit-content;
  558.           scale: 1.2;
  559.           user-select: none;
  560.       }
  561.       .captcha__autosolver input:checked,
  562.       .captcha__autosolver input:checked ~ label {
  563.           font-weight: 700;
  564.           color: var(--theme_default_link);
  565.       }
  566.       .captcha__scanner {
  567.           width: 5px;
  568.           background-color: green;
  569.           opacity: 0.9;
  570.           position: absolute;
  571.           z-index: 2;
  572.           box-shadow: 0em 0em 1em 6px green;
  573.       }
  574.  
  575.       .captcha__hidden {
  576.           display: none !important;
  577.       }
  578.     </style>`);
  579.  
  580.     const captchaObserver = new MutationObserver(mutationsList => {
  581.         for (const mutation of mutationsList) {
  582.             if (mutation.type === 'childList') {
  583.                 mutation.addedNodes.forEach(node => setTimeout(() => processNode(node), 100)); //workaround for firefox
  584.             }
  585.         }
  586.     }
  587.     );
  588.  
  589.     const responseObserver = new MutationObserver(mutations => {
  590.         mutations.forEach(mutation => {
  591.             mutation.addedNodes.forEach(node => {
  592.                 if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('alert')) {
  593.                     const text = node.textContent.toLowerCase();
  594.  
  595.                     if (text.includes('успешно')) {
  596.                         successCount++;
  597.                         pendingResend = false;
  598.                     } else if (text.includes('невалидна')) {
  599.                         failCount++;
  600.                         if (autoresend) {
  601.                             pendingResend = true;
  602.                             console.log('Pending resend flagged due to failed operation.');
  603.                         }
  604.                     }
  605.                     showStats();
  606.                     saveState();
  607.                     // Persist updated config
  608.                     console.log(`Success: ${successCount}, total: ${successCount + failCount}`);
  609.                 }
  610.             }
  611.             );
  612.         }
  613.         );
  614.     }
  615.     );
  616.  
  617.     function turnSolverOn() {
  618.  
  619.         captchaObserver.observe(document.querySelector('._captcha-content'), {
  620.             childList: true,
  621.             subtree: true
  622.         });
  623.         responseObserver.observe(document.getElementById('js-posts'), {
  624.             childList: true,
  625.             subtree: true
  626.         });
  627.     }
  628.  
  629.     function turnSolverOff() {
  630.         captchaObserver.disconnect();
  631.         responseObserver.disconnect();
  632.         $('.captcha__scanner').remove();
  633.     }
  634.  
  635.     function loadState() {
  636.         const state = localStorage.getItem('autosolverOptions');
  637.         if (state) {
  638.             try {
  639.                 return JSON.parse(state);
  640.             } catch (e) {
  641.                 console.warn('Failed to parse saved captcha options', e);
  642.             }
  643.         }
  644.         return {
  645.             autosolve: false,
  646.             autoresend: false,
  647.             hidecaptcha: false,
  648.             successCount: 0,
  649.             failCount: 0
  650.         };
  651.     }
  652.  
  653.     let { autosolve, autoresend, hidecaptcha, successCount, failCount } = loadState();
  654.     successCount = parseInt(successCount);
  655.     failCount = parseInt(failCount);
  656.     totalCount = () => successCount + failCount;
  657.     percentage = () => Math.floor(successCount / totalCount() * 100);
  658.  
  659.     let pendingResend;
  660.     let prevCaptcha = '';
  661.     let solver;
  662.     let chosenCandidates = [];
  663.  
  664.     // Refinement: iteratively exclude regions and recompute
  665.     let excludedWindows = [];
  666.        
  667.     function statistics() {
  668.         return `Авто-отгадано капч: ${successCount}/${totalCount()} (${percentage()}%)`;
  669.     }
  670.  
  671.     function showStats() {
  672.         $('#captcha-stats').text(statistics());
  673.     }
  674.    
  675.     $('.header__opts').append(`<a id="captcha-stats-reset" class="header__menuitem" href="#" style="font-size: smaller; margin-left: auto;">[сброс]</a><div class="header__menuitem" id="captcha-stats">${statistics()}</div>`);
  676.  
  677.     function saveState() {
  678.         localStorage.setItem('autosolverOptions', JSON.stringify({
  679.             autosolve,
  680.             autoresend,
  681.             hidecaptcha,
  682.             successCount,
  683.             failCount
  684.         }));
  685.     }
  686.  
  687.     $('#captcha-stats-reset').on('click', function () {
  688.         successCount = failCount = 0;
  689.         showStats();
  690.         saveState();
  691.     });
  692.    
  693.     $('[id=autosolve]').on('change', function () {
  694.         autosolve = $(this).prop('checked');
  695.         const $additionals = $('[id=autoresend], [id=hidecaptcha]');
  696.         if (autosolve) {
  697.             $additionals.closest('.options__box').css('visibility', 'visible');
  698.             turnSolverOn();
  699.             processNode(document.querySelector('._captcha-content'), true);
  700.  
  701.         } else {
  702.             turnSolverOff();
  703.             $additionals.prop('checked', false).trigger('change'); //needs explicit call to react to checkbox state change
  704.             $additionals.closest('.options__box').css('visibility', 'hidden');
  705.         }
  706.         saveState();
  707.     });
  708.  
  709.     $('[id=autoresend]').on('change', function () {
  710.         autoresend = $(this).prop('checked');
  711.         saveState();
  712.     });
  713.  
  714.     $('[id=hidecaptcha]').on('change', function () {
  715.         hidecaptcha = $(this).prop('checked');
  716.         if (hidecaptcha) {
  717.             $('#autosolver-style').after('<style id="captcha-hider">._captcha-wrapper{display: none !important;}</style>');
  718.         } else {
  719.             $('[id=captcha-hider]').remove();
  720.         }
  721.         saveState();
  722.     });
  723.  
  724.     if (autosolve)
  725.         $('[id=autosolve]').click();
  726.     if (autoresend)
  727.         $('[id=autoresend]').click();
  728.     if (hidecaptcha)
  729.         $('[id=hidecaptcha]').click();
  730.  
  731.     // sync other instances
  732.     $('.captcha__autosolver input').click(event => {
  733.         const id = event.target.id;
  734.         const isChecked = $(event.target).prop('checked');
  735.         $(`input[id="${id}"]`).prop('checked', isChecked);
  736.     }
  737.     );
  738.  
  739.     window.addEventListener('storage', (event) => {
  740.       if (event.key === 'autosolverOptions') {
  741.         ({ autosolve, autoresend, hidecaptcha, successCount, failCount } = loadState());
  742.         showStats();
  743.          console.log('Solver options synced from another tab');
  744.       }
  745.     });
  746.  
  747.     function yieldToEventLoop() {
  748.         return new Promise(resolve => setTimeout(resolve, 0));
  749.     }
  750.  
  751.  
  752.     //_captcha-keyboard-selected-icon
  753.     async function processNode(node, force = false) {
  754.         // Ensure it's an element
  755.         if (node.nodeType === 1) {
  756.             const captcha = node.querySelector('._captcha-image');
  757.             const candidates = Array.from(node.querySelectorAll('._captcha-keyboard-button'));
  758.             const success = node.querySelector('.captcha__msg-success-text');
  759.             const error = node.querySelector('.captcha__msg-error');
  760.  
  761.             if (captcha && (captcha.src !== prevCaptcha || force)) {
  762.                 excludedWindows = [];
  763.  
  764.                 $('.captcha__scanner').remove();
  765.  
  766.                 console.log('New captcha image arrived:', captcha.src);
  767.                 chosenCandidates = Array.from(node.querySelectorAll('._captcha-keyboard-selected-icon')).map(chosen => chosen.src);
  768.  
  769.                 // Iterate over each outmost container
  770.                 if (!hidecaptcha && candidates.length > 0) {
  771.                     $('._captcha').each(async function () {
  772.                         const $outmost = $(this);
  773.  
  774.                         const $img = $outmost.find('._captcha-image');
  775.  
  776.                         // Calculate the topOffset between captcha and outmost container
  777.                         const captchaRect = $img[0].getBoundingClientRect();
  778.                         const containerRect = $outmost[0].getBoundingClientRect();
  779.                         const topOffset = Math.abs(captchaRect.top - containerRect.top);
  780.                         const height = $img[0].height;
  781.  
  782.                         const scanner = document.createElement('div');
  783.                         scanner.className = 'captcha__scanner';
  784.                         scanner.style.top = `${topOffset}px`;
  785.                         scanner.style.height = `${height}px`;
  786.  
  787.                         scanner.animate([{
  788.                             transform: `translateX(${captchaRect.left - containerRect.left}px)`,
  789.                             easing: "ease-in-out"
  790.                         }, {
  791.                             transform: `translateX(${captchaRect.right - containerRect.left}px)`,
  792.                             easing: "ease-in-out"
  793.                         }, {
  794.                             transform: `translateX(${captchaRect.left - containerRect.left}px)`,
  795.                             easing: "ease-in-out"
  796.                         }], {
  797.                             duration: 3000,
  798.                             iterations: Infinity,
  799.                         });
  800.                         $outmost.prepend(scanner);
  801.                         await yieldToEventLoop();
  802.  
  803.                     });
  804.                 }
  805.                 await yieldToEventLoop();
  806.  
  807.                 solver?.cleanup();
  808.                 solver = new CaptchaAutosolver();
  809.                 solver.loadCaptcha(captcha);
  810.                 prevCaptcha = captcha.src;
  811.             }
  812.  
  813.             await yieldToEventLoop();
  814.  
  815.             if (candidates.length > 0) {
  816.                 console.log('In process of solving captcha');
  817.  
  818.                 // Calculate match counts and pick the best candidate
  819.                 let indexOfBest = -1;
  820.                 let bestCandidate = null;
  821.                 let bestResult = null;
  822.                 let bestScore = -1;
  823.  
  824.                 for (let i = 0; i < candidates.length; i++) {
  825.                     const img = candidates[i].querySelector('img');
  826.                     const result = solver.matchCandidate(img, excludedWindows);
  827.                     await yieldToEventLoop();
  828.  
  829.                     if (result.finalScore > bestScore) {
  830.                         bestResult = result;
  831.                         bestScore = result.finalScore;
  832.                         bestCandidate = candidates[i];
  833.                         indexOfBest = i;
  834.                     }
  835.                 }
  836.  
  837.                 bestResult.approved = true;
  838.  
  839.                 // Create exclusion rectangle from best window points
  840.                 const ptsMat = cv.matFromArray(bestResult.bestWindowPoints.length, 1, cv.CV_32FC2, bestResult.bestWindowPoints.flatMap(p => [p.x, p.y]));
  841.                 const bb = cv.boundingRect(ptsMat);
  842.                 ptsMat.delete();
  843.                 excludedWindows.push(new cv.Rect(bb.x, bb.y, bb.width, bb.height));
  844.  
  845.                 let bestCandidateData = bestCandidate.querySelector('img').src;
  846.                 chosenCandidates.push(bestCandidateData);
  847.                 console.log(`Clicking candidate ${indexOfBest} = ${bestCandidateData} with highest match count ${bestScore}`);
  848.                 bestCandidate.click();
  849.             } else if (success && !force) {
  850.                 turnSolverOff();
  851.                 let chosenCandidatesIcons = `
  852. <div class="_captcha-container">
  853.     <div class="_captcha-wrapper">
  854.         <div class="_captcha-keyboard-selected-list">
  855.             ${chosenCandidates.map(src => `
  856.             <div class="_captcha-keyboard-selected-item" type="button">
  857.                 <img class="_captcha-keyboard-selected-icon" src="${src}">
  858.             </div>`).join('')}
  859.         </div>
  860.         <div class="_captcha-image-container">
  861.             <img js-target-image="" class="_captcha-image" src="${prevCaptcha}">
  862.         </div>
  863.     </div>
  864. </div>`;
  865.                 $('._captcha-content').prepend(chosenCandidatesIcons);
  866.  
  867.                 console.log('Solving finished');
  868.                 if (pendingResend) {
  869.                     const submitId = document.getElementById('qr-postform').checkVisibility() ? 'qr-submit' : 'submit';
  870.                     document.getElementById(submitId).click();
  871.                 }
  872.  
  873.                 turnSolverOn();
  874.  
  875.             } else if (error) {
  876.                 $('.captcha__scanner').remove();
  877.             }
  878.         }
  879.     }
  880.  
  881.     function normalizeThreadUrl(url) {
  882.  
  883.         url = url.trim();
  884.        
  885.         if (!url.startsWith(location.origin)) {
  886.             url = location.origin + url;
  887.         }
  888.  
  889.         return url.replace(/\/arch\/[^/]+\//, "/").replace(/#.*$/, "");
  890.     }
  891.  
  892.     function arhivachLink(url = null) {
  893.         return `https://arhivach.vc/ajax/?act=locate_thread&url=${normalizeThreadUrl(url ?? location.href)}`;
  894.     }
  895.  
  896.     if (location.href.includes('/arch/') &&
  897.         document.querySelector('.nf__nf')?.textContent.trim() === '404') {
  898.  
  899.         location.replace(arhivachLink());
  900.     }
  901.  
  902.     function watchAndAlertForDeletedThread() {
  903.         let deletionAlerted = false;
  904.  
  905.         const check = () => {
  906.             if (deletionAlerted) return;
  907.  
  908.             const favicon = document.querySelector(
  909.                 'link[rel*="icon"]'
  910.             )?.href;
  911.  
  912.             if (favicon?.includes('favicon_deleted.ico')) {
  913.                 deletionAlerted = true;
  914.                 $alert(`<b>Тред удалён, но можно <a href="${arhivachLink()}">поискать в Архиваче</a>.</b>`, 'wait');
  915.             }
  916.         };
  917.  
  918.         check();
  919.  
  920.         new MutationObserver(check).observe(document.head, {
  921.             subtree: true,
  922.             childList: true,
  923.             attributes: true,
  924.             attributeFilter: ['href']
  925.         });
  926.     }
  927.  
  928.     watchAndAlertForDeletedThread();
  929.  
  930.  
  931.     const favs = document.getElementById('favorites-table');
  932.     const favThreadSelector = '.bb__row';
  933.     function addArhivachLinkToFavoritedThread(row) {
  934.         const $row = $(row);
  935.  
  936.         const href = $row.find('a.fav__title').first().attr('href');
  937.  
  938.         $('<a>', {
  939.             href: arhivachLink(href),
  940.             title: 'Найти в Архиваче',
  941.             target: '_blank',
  942.             rel: 'noopener noreferrer'
  943.         })
  944.         .append(`
  945.         <svg xmlns="http://www.w3.org/2000/svg" class="icon fav__fa fav__fa_type_findimg">
  946.             <use xlink:href="#icon__findimg"></use>
  947.         </svg>`)
  948.         .insertAfter($row.find('svg').last());
  949.     }
  950.  
  951.     const favsObserver = new MutationObserver(mutations => {
  952.         mutations.forEach(mutation => {
  953.             mutation.addedNodes.forEach(function() {
  954.                 for (const node of mutation.addedNodes) {
  955.                     if (!(node instanceof Element)) continue;
  956.  
  957.                     if (node.matches(favThreadSelector)) {
  958.                         addArhivachLinkToFavoritedThread(node);
  959.                     } else {
  960.                         node.querySelectorAll?.(favThreadSelector).forEach(addArhivachLinkToFavoritedThread);
  961.                     }
  962.                 }
  963.             });
  964.         });
  965.     });
  966.  
  967.     favsObserver.observe(favs, {
  968.         childList: true,
  969.         subtree: true
  970.     });
  971.  
  972.     // Optional: process existing rows
  973.     $(favs).find(favThreadSelector).each(function () {
  974.         addArhivachLinkToFavoritedThread(this);
  975.     });
  976.  
  977. })()
  978.  
  979.  
  980.  
Advertisement
Comments
  • fomil48552
    178 days
    # text 0.05 KB | 0 0
    1. https://tgach.top/b/res/367384.html
    2.  
    3. тред
    4.  
    • First_Name
      175 days
      # text 0.27 KB | 0 0
      1. На всякий случай, я -автор скрипта - к этой борде отношения не имею, рекламой её не занимаюсь, просто нужно было безопасное место для обсуждения сабжа
Add Comment
Please, Sign In to add comment