H::H -spam Fork

    @@ -8,13 +8,15 @@ // nitpicking via // http://let.hatelabo.jp/austinburk/let/hLHU6PPgg9Ya + // Proxy/Reflect ver + // http://let.hatelabo.jp/a-kuma3/let/hJmc7ZTR7vBO // TODO // user.js on Gist // non i18n similar services/software by id:tukihatu c.f. // Hatena Haiku a la mode (3rd party web services) - // http://hh-alamode.fanweb.jp/ + // http://hh-alamode.fanweb.jp // http://h.hatena.ne.jp/target?word=%E3%81%AF%E3%81%A6%E3%81%AA%E3%83%8F%E3%82%A4%E3%82%AF%E3%82%A2%E3%83%A9%E3%83%A2%E3%83%BC%E3%83%89 // Hatena Haiku Soldier (Chrome/ium Extensions) // https://chrome.google.com/webstore/detail/hhgejafgjjjfaocaopajkhgmdgaeimin
    @@ -22,79 +24,79 @@ // e.g. // http://h.hatena.ne.jp - (() => { + (async () => { 'use strict'; if (window.self !== window.top) return; // @noframes in Vanilla - // TODO - // - async use w/ await - // - Promise.all/.race for timeout + // timeout + // https://gist.github.com/noromanba/7e76cd75d15e27b102007298a8156d8f + // TBD Promise based functionize + const controller = new AbortController(); + const signal = controller.signal; + const TIMEOUT = 1000 * 10; + signal.onabort = () => { + console.error(`timeout: ${TIMEOUT} msec elapsed`); + }; + const timer = setTimeout(() => controller.abort(), TIMEOUT); + + // Access-Control-Allow-Origin: http://h.hatena.ne.jp + const FILTER_URL = 'https://haikuantispam.lightni.ng/api/recent_scores.json'; + // TBD no-referrer and/or use CORS proxy c.f. + // http://let.hatelabo.jp/noromanba/let/hJmc7rWIvsJj + // for avoid web-beacon/bug and fingerprinting like risks + const headers = new Headers(); + //headers.set('', ''); + // TBD // - refresh filter when infinite scrolling - let blacklist; - const syncBlockingFetch = () => { - const xhr = new XMLHttpRequest(); - /* XXX asynchronous only - xhr.timeout = 3000; - xhr.responseType = 'json'; - */ + // - IIFE async and try-catch-finally model + const blacklist = await fetch(FILTER_URL, { + headers, + //mode: 'cors', + signal, + }) + .then(res => { + if (!res.ok) { + return Promise.reject(new Error(res.status, res.statusText)); + } + return res.json(); + }) + .then(json => { + const SPAM_THRESHOLD = 5; + return new Map(Object.entries(json) + .filter(([, score]) => score >= SPAM_THRESHOLD)); + }) + .catch(err => { + console.error(err.message, err); + }) + .finally(() => { + clearTimeout(timer); + }); + console.debug(blacklist); - // TODO unreachable? - const cancel = (evt) => { - throw Error(evt.type, xhr.status, xhr.readyState, evt); - }; - - xhr.addEventListener('load', evt => { - if (xhr.status !== 200) { - cancel(evt); - } - - const THRESHOLD = 5; - // TBD omit JSON.parse - blacklist = new Map(Object.entries(JSON.parse(xhr.response)) - .filter(([, score]) => score >= THRESHOLD) - ); - }); - [ - 'abort', - 'timeout', - 'error', - ].forEach(type => { - xhr.addEventListener(type, evt => { - cancel(evt); - }); - }); - - // Access-Control-Allow-Origin: http://h.hatena.ne.jp - const FILTER_URL = 'https://haikuantispam.lightni.ng/api/recent_scores.json'; - // XXX synchronize - xhr.open('GET', FILTER_URL, false); - xhr.send(); - }; - syncBlockingFetch(); - const wipeout = (ctx) => { if (!ctx.querySelectorAll) return; ctx.querySelectorAll([ '.entry.tl-entry' ]).forEach(entry => { - const poster = entry.querySelector('.username a[href][title]'); + // permalink syntax; + // <a href="http://h.hatena.ne.jp/HATENA_ID/" title="id:HATENA_ID">SCREEN_NAME</a> + const poster = entry.querySelector('.username a[href][title^="id:"]'); if (!poster) return; const id = poster.title.slice('id:'.length); if (blacklist.has(id)) { /*/ - //entry.style.backgroundColor = 'red'; - entry.innerHTML = ` - <p><a href="https://haikuantispam.lightni.ng/id/${id}" - title="spam-detail" - target="_blank"> - &lt;censored&gt; - </a></p>`; + entry.style.backgroundColor = 'red'; /*/ + // TBD suppress reflow + console.debug(`id:${id} score: ${blacklist.get(id)} + https://haikuantispam.lightni.ng/id/${id} + `); entry.style.display = 'none'; + entry.innerHTML = ''; //*/ } });
  • /*
     * @title H::H lightni.ng filter
     * @description Hatena Haiku spam filtering w/ lightni.ng
     * @include *://h.hatena.ne.jp/*
     * @license  The MIT License https://opensource.org/licenses/MIT
     * @javascript_url
     */
    
    // nitpicking via
    // http://let.hatelabo.jp/austinburk/let/hLHU6PPgg9Ya
    // Proxy/Reflect ver
    // http://let.hatelabo.jp/a-kuma3/let/hJmc7ZTR7vBO
    
    // TODO
    // user.js on Gist
    
    // non i18n similar services/software by id:tukihatu c.f.
    // Hatena Haiku a la mode (3rd party web services)
    //  http://hh-alamode.fanweb.jp
    //   http://h.hatena.ne.jp/target?word=%E3%81%AF%E3%81%A6%E3%81%AA%E3%83%8F%E3%82%A4%E3%82%AF%E3%82%A2%E3%83%A9%E3%83%A2%E3%83%BC%E3%83%89
    // Hatena Haiku Soldier (Chrome/ium Extensions)
    //  https://chrome.google.com/webstore/detail/hhgejafgjjjfaocaopajkhgmdgaeimin
    //   http://h.hatena.ne.jp/target?word=%E3%81%AF%E3%81%A6%E3%81%AA%E3%83%8F%E3%82%A4%E3%82%AF%E3%82%BD%E3%83%AB%E3%82%B8%E3%83%A3%E3%83%BC
    
    // e.g.
    // http://h.hatena.ne.jp
    (async () => {
        'use strict';
    
        if (window.self !== window.top) return; // @noframes in Vanilla
    
        // timeout
        // https://gist.github.com/noromanba/7e76cd75d15e27b102007298a8156d8f
        // TBD Promise based functionize
        const controller = new AbortController();
        const signal = controller.signal;
        const TIMEOUT = 1000 * 10;
        signal.onabort = () => {
            console.error(`timeout: ${TIMEOUT} msec elapsed`);
        };
        const timer = setTimeout(() => controller.abort(), TIMEOUT);
    
        // Access-Control-Allow-Origin: http://h.hatena.ne.jp
        const FILTER_URL = 'https://haikuantispam.lightni.ng/api/recent_scores.json';
        // TBD no-referrer and/or use CORS proxy c.f.
        //     http://let.hatelabo.jp/noromanba/let/hJmc7rWIvsJj
        //     for avoid web-beacon/bug and fingerprinting like risks
        const headers = new Headers();
        //headers.set('', '');
    
        // TBD
        // - refresh filter when infinite scrolling
        // - IIFE async and try-catch-finally model
        const blacklist = await fetch(FILTER_URL, {
            headers,
            //mode: 'cors',
            signal,
        })
        .then(res => {
            if (!res.ok) {
                return Promise.reject(new Error(res.status, res.statusText));
            }
            return res.json();
        })
        .then(json => {
            const SPAM_THRESHOLD = 5;
            return new Map(Object.entries(json)
                .filter(([, score]) => score >= SPAM_THRESHOLD));
        })
        .catch(err => {
            console.error(err.message, err);
        })
        .finally(() => {
            clearTimeout(timer);
        });
        console.debug(blacklist);
    
        const wipeout = (ctx) => {
            if (!ctx.querySelectorAll) return;
    
            ctx.querySelectorAll([
                '.entry.tl-entry'
            ]).forEach(entry => {
                // permalink syntax;
                // <a href="http://h.hatena.ne.jp/HATENA_ID/" title="id:HATENA_ID">SCREEN_NAME</a>
                const poster = entry.querySelector('.username a[href][title^="id:"]');
                if (!poster) return;
    
                const id = poster.title.slice('id:'.length);
                if (blacklist.has(id)) {
                    /*/
                    entry.style.backgroundColor = 'red';
                    /*/
                    // TBD suppress reflow
                    console.debug(`id:${id} score: ${blacklist.get(id)}
                    https://haikuantispam.lightni.ng/id/${id}
                    `);
                    entry.style.display = 'none';
                    entry.innerHTML = '';
                    //*/
                }
            });
        };
        const timeline = document.body.querySelector('.entries');
        if (!timeline) return;
    
        wipeout(timeline);
    
        new MutationObserver(records => {
            records.forEach(record => {
                wipeout(record.target);
            });
        }).observe(timeline, { childList: true, subtree: true, });
    })();
    
    
    
  • Permalink
    このページへの個別リンクです。
    RAW
    書かれたコードへの直接のリンクです。
    Packed
    文字列が圧縮された書かれたコードへのリンクです。
    Userscript
    Greasemonkey 等で利用する場合の .user.js へのリンクです。
    Loader
    @require やソースコードが長い場合に多段ロードする Loader コミのコードへのリンクです。
    Metadata
    コード中にコメントで @xxx と書かれたメタデータの JSON です。