Source for "4chan updater"

By Chris Done
Has 2 other scripts.


// ==UserScript==
// @name          4chan updater
// @namespace     http://img.4chan.org/
// @description   Script to update 4chan threads at user defined interval
// @include       http://*.4chan.org/*/res/*.html*
// ==/UserScript==
// TODO: Add preference panel so prefs don't have to be hand edited in the source
// TODO: Add out-of-band form submit via ajax so the user can stay on the page.

(function() {  // vim won't indent correctly without this -> )

    // PREFERENCES: Change these to suit your taste.
    const POLL_INTERVAL = 10; // how often to poll the server (in seconds)
    const START_ON_PAGE_LOAD = false; // start updating automatically
    const ALERT_WHEN_THREAD_DIES = false; // show alert dialog when thread dies
    const DISABLE_FORMS_WHEN_THREAD_DIES = true; // prevent submitting to dead thread
    const TITLE_WHEN_THREAD_DIES = '(THREAD DIED)'; // window/tab title
    const NOTICE_WHEN_THREAD_DIES = 'This thread no longer exists';
    const STARTED_CAPTION = 'Updater Running';
    const STOPPED_CAPTION = 'Updater Stopped';
    const THREAD_DIED_CAPTION = 'Thread Died';
    
    // Allow 4chan extension and other Greasemonkey scripts to run on new posts.
    // It takes more a bit more computing so don't use it unless you need it.
    const RUN_SCRIPTS_ON_NEW_POSTS = true; 

    const DEBUG = false; // turn this on to get log messages
    // END PREFERENCES

    var last_update = null;
    var monitor_div = null;
    var monitor_link = null;

    /* poll_timer pseudo-object to get all this shite in one place */
    var poll_timer = { 
        id : null,

        is_running: function() { return this.id },

        start: function() { 
            if (this.is_running()) {
                log_msg('timer already started { %s }, skipping', this.id);
           } else {
                log_msg('starting timer');
                this.id = window.setInterval(poll_server, 1000 * POLL_INTERVAL);
            }
        },

        stop: function() { 
            log_msg('stop timer');
            clearInterval(this.id); 
            this.id = null; 
        }
    }

    // FUNCTIONS
    function init_updater()
    {
        // bail if called on a framed document.
        if (window.frameElement &&
                window.parent.location.href.replace(/#.*/, '') == location.href) {
            return;  
        }

        last_update = new Date(document.lastModified);

        set_up_controls();

        START_ON_PAGE_LOAD && start_updater();
    }

    function set_up_controls()
    {
        GM_addStyle('#bupdater_monitor_div { color: #FFF; padding: 3px; border: 1px; position:fixed; bottom:2px; right:0px; }');
        GM_addStyle('#bupdater_thread_died { color:red; text-align:center }');
        GM_addStyle('div.running { background-color: #0A0; }');
        GM_addStyle('div.thread404d { background-color: #A00; }');
        GM_addStyle('div.stopped { background-color: #000; }');

        monitor_link = document.createElement('a');
        monitor_link.setAttribute('id', 'bupdater_monitor_link');
        monitor_link.addEventListener('click', toggle_updater, false);
        monitor_link.style.cursor = 'pointer';
        monitor_link.style.color = 'white';
        monitor_link.innerHTML = STOPPED_CAPTION;

        monitor_div = document.createElement('div');
        monitor_div.setAttribute('id', 'bupdater_monitor_div');
        monitor_div.setAttribute('class', 'stopped');
        monitor_div.appendChild(monitor_link);

        $X('/html/body').appendChild(monitor_div);
    }

    function toggle_updater() {
        poll_timer.is_running() ? stop_updater() : start_updater();
    }

    function start_updater()
    {
        monitor_link.innerHTML = STARTED_CAPTION;
        monitor_div.setAttribute('class', 'running');
        poll_timer.start();
    }

    function stop_updater()
    {
        monitor_link.innerHTML = STOPPED_CAPTION;
        monitor_div.setAttribute('class', 'stopped');
        poll_timer.stop();
    }

    function poll_server()
    {
        log_msg('polling server...');
        var request = new XMLHttpRequest();

        request.onreadystatechange = function () {
            if (request.readyState == 4) {
                switch (request.status) {
                    case 200: // OK
                        log_msg('updating page');
                        update_page(request); 
                        // NOTE: update_page is responsible
                        // for restarting timer when it's done
                        break;
                    case 304: // Not Modified
                        log_msg('page not modified');
                        poll_timer.start();
                        break;
                    case 404: // Not Found
                        log_msg('thread died');
                        do_thread_died();
                        break;
                    default:
                        log_msg('unhandled server response: ' + request.status);
                        poll_timer.start();
                        break;
                }
            }
        }

        try {
            poll_timer.stop(); // until request is handled

            req_type = RUN_SCRIPTS_ON_NEW_POSTS ? 'HEAD' : 'GET';

            request.open(req_type, window.location.href.replace(/#.*/, ''), true);
            request.setRequestHeader('If-Modified-Since', last_update.toUTCString());
            request.send(null);
        } catch (e) {
            log_msg('Error connecting to server: ' + e.toString());
        }
    }

    function handle_iframe_loaded(iframe_doc)
    {
        var container = null;
        var insert_func = null;
        var last_item = null;

        // dont try to run on 404'd pages
        if (!iframe_doc.getElementById("navtop")) {
            return;
        }

        last_update = new Date(iframe_doc.lastModified);

        // FIXME: All these xpaths need major optimization
        if ( (container = $X("//div[@class='4chan_ext_thread_replies']")) ) {
            add_post = function(item, cont) { cont.appendChild(item); }
        } else {
            container = $X("//form[@name='delform']");
            last_item = $X("//form[@name='delform']//table[last()-1]", document);

            add_post = function(item, cont) {
                cont.insertBefore(item, last_item.nextSibling);
            }
        }

        current_reply_count = 
            $X("count(//form[@name='delform']//td[@class='reply' or @class='replyhl'])", document); 

        new_replies = $x("//form[@name='delform']//table[position() > " +
                current_reply_count +
                "]//td[@class='reply' or @class='replyhl']/ancestor::table", 
                iframe_doc);

        for (var i = 0; i < new_replies.length; i++) {
            add_post(new_replies[i], container);
        }

        poll_timer.start();
    }

    function update_page(request)
    {
        if (request.responseText) {

            last_update = new Date(request.getResponseHeader('Last-Modified'));

            document.getElementsByName('delform')[0].innerHTML = 
                request.responseText.split(/<form .*?name=["]delform["].*?>/i)[1];

            // rerun 4chan's script to highlight replies
            unsafeWindow.init();
            poll_timer.start();
        } else {
            // HACK HACK: we can't reload the main page into an iframe due to 
            // a recursion guard mechanism in Fireflodge so we call
            // 'dat.4chan.org' and let it redirect back to img.4chan.org.
            // This both bypasses the guard and makes me cringe.
            url = window.location.href.replace(/#.*/, '').replace('img','dat');

            dom_from_hidden_iframe(url, handle_iframe_loaded); // NOTE: handle_iframe_loaded restarts timer when page rendering is done.
        }    
    }

    function do_thread_died() 
    {
        stop_updater();

        // display thread died message
        var delform = document.getElementsByName('delform')[0];
        thread_died_message = document.createElement('h2');
        thread_died_message.setAttribute('id', 'bupdater_thread_died');
        thread_died_message.innerHTML = NOTICE_WHEN_THREAD_DIES;
        delform.parentNode.insertBefore(thread_died_message, delform);

        monitor_link.innerHTML = THREAD_DIED_CAPTION;
        monitor_div.setAttribute('class', 'thread404d');

        // add thread died message to page title
        document.title = TITLE_WHEN_THREAD_DIES;

        // disable forms if the user requested
        DISABLE_FORMS_WHEN_THREAD_DIES && disable_forms();

        // do alert dialog if the user requested one
        ALERT_WHEN_THREAD_DIES && alert(NOTICE_WHEN_THREAD_DIES);
    }

    function disable_forms()
    {
        // change submit button title so the user knows why it's disabled
        $X("/html/body//form[@name='post']//input[@type='submit']").value = 
            TITLE_WHEN_THREAD_DIES;
        // disable all inputs but textarea so the user can copy their text still
        // if they want to save or use it somewhere else.
        inputs = $x("/html/body//form//input");
        for (i = 0; i < inputs.length; i++) {
            inputs[i].disabled='true';
        }
    }

    // list nodes matching this expression, optionally relative to the node `root'
    function $x( xpath, root )
    {
        var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
        var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = [];

        switch (got.resultType) {
            case got.STRING_TYPE:
                return got.stringValue;
            case got.NUMBER_TYPE:
                return got.numberValue;
            case got.BOOLEAN_TYPE:
                return got.booleanValue;
            default:
                while ((next = got.iterateNext()))
                    result.push( next );
                return result;
        }
    }

    function $X( xpath, root )
    {
        var got = $x( xpath, root );
        return got instanceof Array ? got[0] : got;
    }

    function log_msg(msg)
    {
        DEBUG && console.log(msg);
    }

    function dom_from_hidden_iframe(url, cb/*( xml )*/) {
        function loaded() {
            doc = iframe.contentDocument;
            iframe.removeEventListener('load', loaded, false);
            doc.removeEventListener('DOMContentLoaded', loaded, false);
            // avoid racing with GM's DOMContentLoaded callback
            setTimeout(function() { cb( doc ); }, 10);
        };

        var iframe = null;

        if ( !(iframe = document.getElementById('updater_iframe')) ) {
            iframe = document.createElement('iframe');
            iframe.style.height = iframe.style.width = '0';
            iframe.style.visibility = 'hidden';
            iframe.style.position = 'absolute';
            iframe.id = 'updater_iframe';
            document.body.appendChild(iframe);
        }

        iframe.addEventListener('load', loaded, false);
        log_msg('loading new content into iframe');
    
        try {
            iframe.src = url;
        } catch(e) {
            log_msg('Error in another script in the iframe');
        }
    }

    window.addEventListener("load", function() { init_updater(); }, false);

})();