Guest User

Untitled

a guest
Oct 26th, 2026
36
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2. // @name         zasrywacz 3.1 beta
  3. // @match        https://karachan.org/*
  4. // @grant        GM_xmlhttpRequest
  5. // @connect      www.hovno.sk
  6. // @run-at       document-end
  7. // ==/UserScript==
  8.  
  9.  
  10. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms) );
  11.  
  12. const normURL = url => url.replace(/\/$/, ''); // karachan.org/b/ --> karachan.org/b
  13.  
  14. function parseTargets(text) {
  15.     const STRIP = /[^a-zA-Z0-9:/?&=#._~%-]/g;
  16.     const URL_START = /^https?:\/\/./;
  17.     const WS = /\s+/;
  18.  
  19.     const out = [];
  20.     for (const token of text.split(WS)) {
  21.         const t = token.replace(STRIP, '');
  22.         if (URL_START.test(t)) out.push(normURL(t));
  23.     }
  24.     return out;
  25. }
  26.  
  27.  
  28. function waitForDocument() {
  29.     return new Promise(resolve => {
  30.         if (document.readyState === 'complete')
  31.             resolve();
  32.         else
  33.             window.addEventListener('load', resolve, { once: true });
  34.         console.log("document loaded")
  35.     });
  36. }
  37.  
  38.  
  39. function applyHTML() {
  40.     const menu = document.createElement('div');
  41.  
  42.     menu.innerHTML = `
  43.         <div class="menu-window" contenteditable="false">
  44.             <div class="menu-section">
  45.                 <textarea id="list-of-targets" name="message" rows="6" placeholder="lista linków"></textarea>
  46.                 <div class="menu-row">
  47.                     <input type="button" id="add-button" value="dodaj link">
  48.                     <input type="button" id="save-button" value="zapisz">
  49.                     <input type="button" id="clear-button" value="wyczyść">
  50.                 </div>
  51.                 <div class="menu-row">
  52.                     <label><input type="checkbox" id="sage-checkbox"> saguj</label>
  53.                     <label><input type="checkbox" id="test-checkbox"> test</label>
  54.                 </div>
  55.                 <input type="button" id="start-button" value="start">
  56.                 <div id="count-text"></div>
  57.             </div>
  58.         </div>
  59.     `;
  60.  
  61.     document.body.appendChild(menu);
  62. }
  63.  
  64.  
  65. function applyCSS() {
  66.     const style = document.createElement('style');
  67.     style.textContent = `
  68.     .menu-window {
  69.         position: fixed;
  70.         top: 30%;
  71.         right: 1%;
  72.         z-index: 9999;
  73.         width: 18rem;
  74.         max-width: 90vw;
  75.         box-sizing: border-box;
  76.         font-size: .9rem;
  77.     }
  78.     .menu-section {
  79.         display: flex;
  80.         flex-direction: column;
  81.         gap: 8px;
  82.         padding: 10px;
  83.         border: 1px solid #888;
  84.         border-radius: 4px;
  85.         background: Canvas;
  86.         color: CanvasText;
  87.     }
  88.     .menu-section textarea {
  89.         width: 100%;
  90.         box-sizing: border-box;
  91.         resize: vertical;
  92.         font-size: .8rem;
  93.     }
  94.     .menu-section input[type="button"] {
  95.         font: inherit;
  96.         padding: 4px 8px;
  97.         border-radius: 4px;
  98.         cursor: pointer;
  99.     }
  100.     .menu-row {
  101.         display: flex;
  102.         gap: 8px;
  103.     }
  104.     .menu-row > * {
  105.         flex: 1;
  106.     }
  107.     .menu-section label {
  108.         display: flex;
  109.         align-items: center;
  110.         gap: 4px;
  111.         cursor: pointer;
  112.     }
  113.     #count-text,
  114.     #status-text {
  115.         min-height: 1.2em;
  116.         font-size: .85em;
  117.         opacity: .8;
  118.     }
  119.         `;
  120.     document.head.appendChild(style);
  121. }
  122.  
  123.  
  124. function getRandomPictureUrl() {
  125.     const page = Math.floor(Math.random() * 930) + 1;
  126.  
  127.     console.log("finding a picture")
  128.  
  129.     return new Promise(resolve => {
  130.         GM_xmlhttpRequest({
  131.             method: 'GET',
  132.             url: `https://www.hovno.sk/galeria?strana=${page}`,
  133.             onload: function (response) {
  134.  
  135.                 const doc = new DOMParser().parseFromString(
  136.                     response.responseText,
  137.                     'text/html'
  138.                 );
  139.  
  140.                 const images = doc.querySelectorAll('a.list-group-item.shit img');
  141.                 const randomImage = images[Math.floor(Math.random() * images.length)];
  142.                 const picture = randomImage.closest('a.list-group-item.shit');
  143.                 const title = picture.querySelector('span:not(.badge)').textContent.trim();
  144.                 const fullUrl = randomImage.src.replace('/img/thumb/', '/img/full/');
  145.  
  146.                 console.log("picture found");
  147.                 console.log(fullUrl);
  148.  
  149.                 resolve({
  150.                     url: fullUrl,
  151.                     title: title
  152.                 });
  153.             }
  154.         });
  155.     });
  156. }
  157.  
  158.  
  159. function downloadAndSetFile(url, title) {
  160.     const timestamp = Date.now().toString(); // for duplicate evasion
  161.  
  162.     console.log('setting the picture: "' + title + '"')
  163.  
  164.     return new Promise((resolve, reject) => {
  165.         GM_xmlhttpRequest({
  166.             method: 'GET',
  167.             url: url,
  168.             responseType: 'blob',
  169.             onload: function (response) {
  170.                 if (response.status < 200 || response.status >= 300) {
  171.                     reject(new Error(`download failed, http code: ${response.status}`));
  172.                     return;
  173.                 }
  174.  
  175.                 const file = new File(
  176.                     [response.response, new Blob([timestamp])], // appending the bytes
  177.                     title + '.jpg',
  178.                     { type: response.response.type }
  179.                 );
  180.  
  181.                 const input = document.querySelector(
  182.                     'input.upload[name="upfile"]'
  183.                 );
  184.  
  185.                 if (!input) {
  186.                     reject(new Error('file input not found'));
  187.                     return;
  188.                 }
  189.  
  190.                 const dataTransfer = new DataTransfer();
  191.                 dataTransfer.items.add(file);
  192.  
  193.                 input.files = dataTransfer.files;
  194.                 input.dispatchEvent(new Event('change', { bubbles: true }));
  195.  
  196.                 // pliczek załączony?????
  197.                 if (input.files.length > 0 && input.files[0].name === file.name) {
  198.                     console.log("file attached")
  199.                     resolve();
  200.                 }
  201.                 else reject(new Error('file was not attached'));
  202.  
  203.             },
  204.  
  205.             onerror: function () {
  206.                 reject(new Error('file download failed'));
  207.             }
  208.         });
  209.     });
  210. }
  211.  
  212.  
  213. async function cooldown() {
  214.     const duration = config.boarddata.time_between_posts;
  215.     let elapsed = (Date.now() - Number(localStorage.getItem('lastPostCreatedTime') || 0)) / 1000;
  216.     let remaining = Math.max(0, duration - elapsed) + 1; // +1 just to be safe
  217.  
  218.     if (remaining <= 1) return; // no need to be safe if there is no cooldown
  219.  
  220.     while (remaining <= 0) {
  221.         elapsed = (Date.now() - Number(localStorage.getItem('lastPostCreatedTime') || 0)) / 1000;
  222.         remaining = Math.max(0, duration - elapsed);
  223.         console.log("still sending?")
  224.         await sleep(500)
  225.     }
  226.  
  227.     if (remaining > 0) {
  228.         console.log(`cooldown remaining: ${remaining}s`);
  229.         await sleep(remaining * 1000);
  230.     }
  231.  
  232.     console.log("cooldown finished")
  233. }
  234.  
  235.  
  236. async function catalog() {
  237.     // this utter mess is a sad necessity
  238.     const whereami = new URL(location.href);
  239.     const board = `${whereami.origin}/${whereami.pathname.split('/')[1]}`;
  240.     const base = normURL(board) + '/';
  241.     const url = new URL('catalog.html', base).href;
  242.     let targets = JSON.parse(localStorage.getItem('targets'));
  243.  
  244.     if (location.href !== url) {
  245.         localStorage.setItem('cataloging', 'true');
  246.         localStorage.setItem('departed', location.href);
  247.         console.log('navigating to catalog:', url);
  248.         location.href = url;
  249.         return;
  250.     }
  251.  
  252.     console.log("updating the catalog")
  253.  
  254.     await waitForDocument();
  255.     await sleep(1000);
  256.  
  257.     const anchors = document.querySelectorAll('.thread a[href^="./res/"]');
  258.     const length = anchors.length;
  259.     const links = new Array(length);
  260.     for (let i = 0; i < length; i++) {
  261.         links[i] = base + anchors[i].getAttribute('href').slice(2);
  262.     }
  263.  
  264.     localStorage.setItem('catalog', JSON.stringify(links));
  265.     localStorage.setItem('cataloging', 'false');
  266.  
  267.     // remove 404 threads
  268.     targets = targets.filter(target => links.includes(target));
  269.     localStorage.setItem('targets', JSON.stringify(targets));
  270.     console.log(JSON.stringify(links))
  271.  
  272.     // let's get back to where we departed from
  273.     location.href = localStorage.getItem('departed')
  274. }
  275.  
  276.  
  277.  
  278. async function zasrywacz(isSage, isTest, board) {
  279.  
  280.     async function submit(isSage, isTest, creating) {
  281.  
  282.         async function send(creating) {
  283.             const button = document.querySelector('td.ladda-td button.ladda-button');
  284.             const lastPostTime = Number(localStorage.getItem('lastPostCreatedTime'))
  285.             let retries = 0;
  286.  
  287.             if (!button) {
  288.                 log ('failed to locate the submit button');
  289.                 throw new Error('failed to locate the submit button')
  290.                 return;
  291.             }
  292.  
  293.             button.click();
  294.  
  295.             // If creating a thread, reloading will restart the loop independently.
  296.             if (creating) {
  297.                 localStorage.setItem('save', 'true');
  298.  
  299.                 while (retries < 50) {
  300.                     log(`waiting for a reload (${retries + 1}/50)`);
  301.                     await sleep(2000);
  302.                     button.click();
  303.                     retries++;
  304.                 }
  305.             }
  306.  
  307.             // When replying, handle it on our own.
  308.             while (retries < 50 && lastPostTime === Number(localStorage.getItem('lastPostCreatedTime'))) {
  309.                 log(`still sending? (${retries + 1}/50)`);
  310.                 await sleep(2000);
  311.                 button.click();
  312.                 retries++;
  313.             }
  314.             return;
  315.         }
  316.  
  317.  
  318.         if (isSage) {
  319.             const email = document.querySelector('input[name="email"]');
  320.             if (email) {
  321.                 email.value = 'sage';
  322.                 log('sage on');
  323.             }
  324.             else {
  325.                 log('failed to locate email textarea');
  326.                 throw new Error('failed to locate email textarea')
  327.                 location.href = board // restarting if zasrywacz ends with an error
  328.                 return;
  329.             }
  330.         }
  331.  
  332.  
  333.         if (isTest) {
  334.             log('(test mode, not sent)');
  335.             return;
  336.         }
  337.  
  338.  
  339.         const { url, title } = await getRandomPictureUrl();
  340.         await downloadAndSetFile(url, title);
  341.         await cooldown();
  342.         await send(creating);
  343.     }
  344.  
  345.     const log = (...args) => console.log('zasrywacz(): ', ...args);
  346.  
  347.     const bumps = Number(localStorage.getItem('bumps'));
  348.     const ratio = Number(localStorage.getItem('ratio'));
  349.     let targets = JSON.parse(localStorage.getItem('targets') || '[]');
  350.     const creating = ratio < bumps;
  351.     const dest = creating ? localStorage.getItem('board') : targets[0];
  352.  
  353.     if (!dest) {
  354.         log(creating ? 'no board set' : 'no targets');
  355.         return;
  356.     }
  357.  
  358.     const target = new URL(dest, location.href).href;
  359.  
  360.     await waitForDocument();
  361.  
  362.     if (normURL(location.href) !== normURL(target)) {
  363.         log(`${normURL(location.href)} !== ${normURL(target)}`)
  364.         log('renavigating to target:', target);
  365.  
  366.         location.href = target;
  367.         return;
  368.     }
  369.     else log("no need to navigate")
  370.  
  371.  
  372.     if (creating) {
  373.         log('creating a thread');
  374.         localStorage.setItem('bumps', 1);
  375.         localStorage.setItem('creating', 'true') // with the reload in mind, we gotta make this info persistent
  376.     }
  377.     else {
  378.         log(`bumping ${bumps} out of ${ratio}`);
  379.         localStorage.setItem('bumps', bumps + 1);
  380.         localStorage.setItem('creating', 'false') // just in case, more robust
  381.     }
  382.  
  383.  
  384.     try {
  385.         await submit(isSage, isTest, creating);
  386.     }
  387.     catch (error) {
  388.         const index = targets.indexOf(target);
  389.         log('could not prepare post:', error);
  390.         log('this link is gonna be removed from the list')
  391.         log(target)
  392.  
  393.         if (index !== -1) targets.splice(index, 1);
  394.  
  395.         localStorage.setItem('targets', JSON.stringify(targets));
  396.  
  397.         await sleep(2000)
  398.         location.href = board // restarting
  399.         return;
  400.     }
  401.  
  402.  
  403.  
  404.     // this part of the script will only be executed when bumping a thread.
  405.     // rotate the list of targets and save
  406.     targets.push(targets.shift());
  407.     localStorage.setItem('targets', JSON.stringify(targets));
  408.  
  409.     // define the next target
  410.     const next = new URL(targets[0], location.href).href;
  411.     log('navigating to next target:', next);
  412.     location.href = next;
  413.     return;
  414. }
  415.  
  416.  
  417.  
  418. async function init() {
  419.  
  420.     function setTargets(input) {
  421.         const text = Array.isArray(input) ? input.join(' ') : String(input ?? '');
  422.         const boardKey = board ? normURL(board) : null;
  423.  
  424.         const cleaned = [...new Set(
  425.             parseTargets(text).map(target => target.replace(/(\.html).*$/i, '$1'))
  426.         )].filter(target => normURL(target) !== boardKey);
  427.  
  428.         localStorage.setItem('targets', JSON.stringify(cleaned));
  429.         targetList.value = cleaned.join(' ');
  430.         countText.textContent = `liczba linków: ${cleaned.length}`;
  431.  
  432.         console.log(cleaned);
  433.         return cleaned;
  434.     }
  435.  
  436.     function addTarget(url) {
  437.         return setTargets(`${targetList.value} ${url}`);
  438.     }
  439.  
  440.  
  441.     const menu        = document.querySelector('.menu-window');
  442.     const targetList  = menu.querySelector('#list-of-targets');
  443.     const countText   = menu.querySelector('#count-text');
  444.     const sageBox     = menu.querySelector('#sage-checkbox');
  445.     const testBox     = menu.querySelector('#test-checkbox');
  446.     const addButton   = menu.querySelector('#add-button');
  447.     const saveButton  = menu.querySelector('#save-button');
  448.     const clearButton = menu.querySelector('#clear-button');
  449.     const startButton = menu.querySelector('#start-button');
  450.  
  451.     const board = localStorage.getItem('board');
  452.     let isRunning = localStorage.getItem('isRunning') === 'true';
  453.  
  454.  
  455.     setTargets(JSON.parse(localStorage.getItem('targets') || '[]'));
  456.     sageBox.checked = localStorage.getItem('isSage') === 'true';
  457.     testBox.checked = localStorage.getItem('isTest') === 'true';
  458.     startButton.value = isRunning ? 'stop' : 'start';
  459.  
  460.     addButton.addEventListener('click', () => addTarget(location.href));
  461.     saveButton.addEventListener('click', () => setTargets(targetList.value));
  462.     clearButton.addEventListener('click', () => setTargets([]));
  463.  
  464.     sageBox.addEventListener('change', () => localStorage.setItem('isSage', sageBox.checked));
  465.     testBox.addEventListener('change', () => localStorage.setItem('isTest', testBox.checked));
  466.  
  467.     startButton.addEventListener('click', () => {
  468.         isRunning = !isRunning;
  469.         localStorage.setItem('isRunning', isRunning);
  470.  
  471.         if (isRunning) {
  472.             console.log('started by the user');
  473.             startButton.value = 'stop';
  474.             initializeData();
  475.             zasrywacz(sageBox.checked, testBox.checked, board);
  476.         }
  477.         else {
  478.             console.log('interrupted by the user');
  479.             startButton.value = 'start';
  480.             localStorage.setItem('cataloging', 'false');
  481.             localStorage.setItem('save', 'false');
  482.             location.href = board;
  483.         }
  484.     });
  485.  
  486.  
  487.     //if (localStorage.getItem('cataloging') === 'true') await catalog();
  488.  
  489.     if (localStorage.getItem('save') === 'true') {
  490.         console.log(`saving this link: ${location.href}`);
  491.         addTarget(location.href);
  492.         localStorage.setItem('save', 'false');
  493.     }
  494.  
  495.     if (isRunning) {
  496.         zasrywacz(sageBox.checked, testBox.checked, board);
  497.     }
  498.     else {
  499.         initializeData();
  500.     }
  501. }
  502.  
  503.  
  504.  
  505. function initializeData() {
  506.     const cooldownBump = config.boarddata.time_between_posts;
  507.     const cooldownThread = config.boarddata.time_between_threads;
  508.     const ratio = Math.max(0, Math.ceil(cooldownThread / cooldownBump) - 1);
  509.  
  510.     const whereami = new URL(location.href);
  511.     const board = `${whereami.origin}/${whereami.pathname.split('/')[1]}`;
  512.  
  513.     //const catalog = JSON.parse(localStorage.getItem('catalog') || '[]')
  514.     const targets = JSON.parse(localStorage.getItem('targets') || '[]')
  515.  
  516.     const running = localStorage.getItem('isRunning') === 'true';
  517.  
  518.     if (running) {
  519.         // pre-run sanity check
  520.         if (targets.some(target => !target.startsWith(`${board}/`))) {
  521.             console.log('navigate to your chosen board');
  522.             localStorage.setItem('isRunning', 'false');
  523.             location.href = board;
  524.             return;
  525.         }
  526.         else {
  527.             console.log('all targets are valid')
  528.         }
  529.     }
  530.  
  531.  
  532.     console.log("board: "+ board);
  533.     console.log("cooldown for threads: " + cooldownThread + "s, bumps: " + cooldownBump + "s");
  534.     console.log("required ratio: " + ratio + " bumps for every new thread");
  535.     //console.log(catalog)
  536.  
  537.     // initiate states
  538.     localStorage.setItem('ratio', ratio);
  539.     localStorage.setItem('bumps', ratio + 1);
  540.     localStorage.setItem('board', board)
  541.     localStorage.setItem('save', 'false');
  542.     //localStorage.setItem('creating','false')
  543.     //localStorage.setItem('sending', 'false');
  544.     //localStorage.setItem('isRunning', 'false');
  545.     //localStorage.setItem('departed', null);
  546.     //localStorage.setItem('targets', null);
  547.     //localStorage.setItem('cataloging', false);
  548. }
  549.  
  550.  
  551.  
  552.  
  553.  
  554.  
  555.  
  556. (function main() {
  557.     applyHTML();
  558.     applyCSS();
  559.     init();
  560. })();
  561.  
Advertisement
Add Comment
Please, Sign In to add comment