User:Former User aDB0haVymg/gadgets/trollShieldAlphaPilot.js

维基百科,自由的百科全书

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。

"use strict";
function trollJSContainerFunction () {
/**
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* global   TrollJSRegistrar, trollJSBlacklist, trollJSUsernameParser, 
            trollJSPostExtractor, trollJSKiller, trollJSPageEligibilityModule, trollJSBlockUI
            mw */

// script tunable constants
const trollJSAppIdentifier = "[trollJS v1]";

// UI interface message strings
const MSG_NO_TROLL = "Hooray! No troll in sight!";
const MSG_TROLL_REPORT_LINE = "$SLASHCOUNT post/s deleted for troll named $NAME";
const MSG_REPORT_HEADER = "Troll JS Gadget Report";
const MSG_PAGE_INELIGIBLE = trollJSAppIdentifier + ": skipping ineligible page.";
const MSG_DONE = trollJSAppIdentifier + ": done.";

// Troll JS Runtime
const trollJSApp = {

    // app's runtime variables
    registrar: null,

    // app's runtime functions

    /**
     * Detects if a link contains a blacklisted username
     * @param {Node} linkElmt   the link to analyse
     * @returns {Boolean}       true or false
     */
    _detectBlacklistedNameInLink: function(linkElmt) {

        const userNameInLink = trollJSUsernameParser.getUserNameFromLink(linkElmt);

        // if it's not a username link at all...
        if (!userNameInLink) {
            return false;
        }

        return trollJSBlacklist.isBlacklisted(userNameInLink);

    },

    /**
     * Assesses if a post's owner is blacklisted or not
     * @param {TrollJSPost} post    the post to detect
     * @returns {Boolean}           true or false
     */
    _detectBlacklistedOwnerInPost: function(post) {
        
        let ownerName = post.getOwner();

        return trollJSBlacklist.isBlacklisted(ownerName);

    },

    /**
     * Generates the text of the paragraph element in the report div
     * @returns {HTMLElement} the message text, in the form of a div
     */
    _generateReportContentDiv: function() {
        
        var newElmt = document.createElement("div");
        const slashCountEntries = Object.entries(this.registrar.slashCountByTroll);

        // if there is no troll found, then just leave silently
        if (slashCountEntries.length === 0) {
            return null;
        }

        // else, make a <ul> element
        var newUlElmt = document.createElement("ul");

        // give each troll a dedicated <li>
        for (const thisEntry of slashCountEntries) {

            // generate the text message string
            const trollName = thisEntry[0];
            const slashCount = thisEntry[1];
            const msgString = MSG_TROLL_REPORT_LINE
                .replace("$SLASHCOUNT", slashCount.toString()).replace("$NAME", trollName);

            // create <li>, and put the msgString inside
            const newLiElmt = document.createElement("li");
            newLiElmt.innerText = msgString;

            // add it to newUlElmt, at the end
            newUlElmt.insertAdjacentElement("beforeend", newLiElmt);

        }

        // add ul element to newElmt
        newElmt.insertAdjacentElement("beforeend", newUlElmt);

        // done
        return newElmt;

    },

    /**
     * Send report to user
     */
    reportToUser: function() {
        
        /** Sample Report:
         * <h3>Troll JS Gadget Report</h3>
         * <p>Troll.js has banished [\d]+ posts from [\d]+ trolls</p>
         */

        // Create elements
        const reportDiv = document.createElement("div");
        const reportHeader = document.createElement("h3");
        const reportContent = this._generateReportContentDiv();

        // leave silently if the last statement leaves us null.
        if (!reportContent) {
            return;
        }

        // fill in text
        reportHeader.textContent = MSG_REPORT_HEADER;

        // add to div and display
        reportDiv.appendChild(reportHeader);
        reportDiv.appendChild(reportContent);

        if (reportDiv) {
            mw.notify(reportDiv);
        }

    },

    /**
     * A function to start the whole operation
     */
    start: async function () {

        // verify that this page is indeed eligible first
        if (trollJSPageEligibilityModule.assessPageEligibility() === false) {
            console.log(MSG_PAGE_INELIGIBLE);
            return;
        }

        // generate a registrar
        this.registrar = new TrollJSRegistrar();

        // get all links, and filter by 
        let allLinks = Array.from(document.querySelector("#bodyContent").querySelectorAll("a"));
        const filteredLinks = allLinks.filter(this._detectBlacklistedNameInLink);

        // get posts on everything, add to the register
        for (const link of filteredLinks) {
            this.registrar.addPost(
                trollJSPostExtractor.getEntirePost(link)
            );
        }

        // filter the posts such that only the ones with a blacklisted owner appear!
        const allPosts = this.registrar.posts;
        const filteredPosts = allPosts.filter(this._detectBlacklistedOwnerInPost);

        // kill all of the posts with blacklisted owner!
        for (const post of filteredPosts) {
            
            // register it 
            this.registrar.markPostSlashed(post);

            // slash it
            trollJSKiller.killPost(post);

        }

        // all posts killed. report.
        this.reportToUser();
        console.log(MSG_DONE);

    },

};
/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * This module will add a quick link on users' userpages
 * Such that they can be easily blocked
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* global trollJSBlacklist, mw */
/* exported trollJSBlockUI */

// "global" message strings as tunable constants
const ERR_INVALID_SUITABILITY = "Invalid suitability string";
const MSG_BLOCK_THIS_USER = "Block this user";
const MSG_UNBLOCK_THIS_USER = "Unblock this user";
const MSG_BLOCK_CONFIRMATION = "Are you sure you want to block $1?";
const MSG_UNBLOCK_CONFIRMATION = "Are you sure you want to unblock $1?";
const MSG_BLOCKED = "$1 has been blocked.";
const MSG_UNBLOCKED = "$1 has been unblocked.";
const MSG_CANCELLED = "Cancelled by user.";

// runtime variables
var thisUsername;

const trollJSBlockUI = {



    /**
     * Checks whether current page is a user, and the block status of the user
     * @returns {string} one of "notuser", "blocked", or "notblocked"
     */
    checkPageSuitability: function() {

        // if there is a "contributions" link, we know it's a user-related page
        // if not, we can return "notuser" directly
        const contributionsLink = document.querySelector("#t-contributions a");
        if (!contributionsLink) {
            return "notuser";
        }

        thisUsername = this._extractUsernameFromLink(contributionsLink);
        if (trollJSBlacklist.isBlacklisted(thisUsername)) {
            return "blocked";
        } else {
            return "notblocked"
        }

    },

    /**
     * Extracts username from a "user contribution" link element
     * Exploratory test passed 2020-05-07
     * @param contributionsLink {HTMLElement} the link element
     * @returns {string} the username
     */
    _extractUsernameFromLink: function (contributionsLink) {
        const usernameExtractorPattern = /^.+\/Special:(.+)\/(.+)$/gi;
        const linkHref = decodeURI(contributionsLink.href);
        return usernameExtractorPattern.exec(linkHref)[2];
    },

    /**
     * Generates and insert into page the link
     * @param {string} suitability output of this.checkPageSuitability()
     */
    generateAndInsertLinkElmt: function(suitability) {

        const contributionsLink = document.querySelector("#t-contributions");

        // the new element contains a wrapper in the outside... (identical to contributionsLink, except the id)
        const newWrapperElmt = contributionsLink.cloneNode(false);
        newWrapperElmt.id = "t-trollJSToggleLink";

        // ...and an <a> element in the inside, which should be brand new
        var newLinkElmt;

        if (suitability === "blocked") {
            newLinkElmt = this.generateUnblockLinkElmt();
        } else if (suitability === "notblocked") {
            newLinkElmt = this.generateBlockLinkElmt();
        } else {
            throw new Error(ERR_INVALID_SUITABILITY);
        }

        newWrapperElmt.appendChild(newLinkElmt);
        contributionsLink.insertAdjacentElement("afterend", newWrapperElmt);

    },

    /**
     * A simple helper method to create the inner link.
     * @returns {HTMLAnchorElement} the link element
     * @private
     */
    _generateInnerLinkElmt: function() {
        return document.createElement("a");
    },

    /**
     * Gets the link element that says "block this user"
     * @returns {HTMLElement} the new element (should be an <a>)
     */
    generateBlockLinkElmt: function() {

        const newElmt = this._generateInnerLinkElmt();
        newElmt.innerText = MSG_BLOCK_THIS_USER;
        newElmt.addEventListener("click", this.blockLinkOnClick);

        return newElmt;

    },

    /**
     * Gets the link element that says "unblock this user"
     * @returns {HTMLElement} the new element (should be an <a>)
     */
    generateUnblockLinkElmt: function() {

        const newElmt = this._generateInnerLinkElmt();
        newElmt.innerText = MSG_UNBLOCK_THIS_USER;
        newElmt.addEventListener("click", this.unblockLinkOnClick);

        return newElmt;

    },

    /**
     * onclick handler for the "block this user" link
     * @param {Event} event default DOM event
     */
    // eslint-disable-next-line no-unused-vars
    blockLinkOnClick: async function(event) {

        // prompt the user for confirmation
        const promptText = MSG_BLOCK_CONFIRMATION.replace("$1", thisUsername);
        const userResponse = window.confirm(promptText);
        if (!userResponse) {
            mw.notify(MSG_CANCELLED);
            return;
        }

        // do block
        trollJSBlacklist.addToBlackList(thisUsername);

        // notify and done.
        mw.notify(MSG_BLOCKED.replace("$1", thisUsername));

    },

    /**
     * onclick handler for the "unblock this user" link
     * @param {Event} event default DOM event
     */
    // eslint-disable-next-line no-unused-vars
    unblockLinkOnClick: async function(event) {

        // prompt the user for confirmation
        const promptText = MSG_UNBLOCK_CONFIRMATION.replace("$1", thisUsername);
        const userResponse = window.confirm(promptText);
        if (!userResponse) {
            mw.notify(MSG_CANCELLED);
            return;
        }

        // do unblock
        trollJSBlacklist.removeFromBlackList(thisUsername);

        // notify and done
        mw.notify(MSG_UNBLOCKED.replace("$1", thisUsername));

    },

    /**
     * This meta function starts the whole operation of block selector
     */
    start: async function() {
        
        const pageSuitability = this.checkPageSuitability();

        // skip all non-user pages: return silently
        if (pageSuitability === "notuser") {
            return;
        }

        this.generateAndInsertLinkElmt(pageSuitability);

    }

}/**
 * Blacklist.js for Chinese Wikipedia
 * 
 * This module manages the persistent storage and processing of blacklists
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* global LocalStorage */
/* exported trollJSBlacklist */

const BLACKLIST_LOCAL_CACHE_AGE_LIMIT = 30 * 1000;  // 30 seconds

const trollJSBlacklist = {

    blackListCache: null, /* memory cache. this is only filled in runtime */
    objectKey: "blacklist",
    lastUpdatedTimestampKey: "updatetime",

    /**
     * Gets whatever's stored in MW API, and store into LocalStorage,
     * only if necessary.
     * @returns {Promise<void>} Resolves no matter what.
     * NOTE - will ignore errors
     * @private
     */
    _pullRemoteChanges: async function() {

        // check if the cache is too old
        const currentTime = Date.now();
        const localUpdateTime = this._getLocalUpdateTime();

        if (currentTime - localUpdateTime < BLACKLIST_LOCAL_CACHE_AGE_LIMIT) {
            // no need to update
            return;
        }

        // check whether remote end has updated
        const remoteUpdateTime = await RemoteStorage.getLastUpdateTime();
        if (remoteUpdateTime > localUpdateTime) {

            // need to refresh cache!
            var newBlacklistObject;
            try {
                newBlacklistObject = await RemoteStorage.getBlackList();
            } catch (error) {
                // if it's not already present, just create an empty one and push it up.
                if (error.toString().includes("The requested key is not present.")) {
                    newBlacklistObject = [];
                    RemoteStorage.pushBlackList(JSON.stringify(newBlacklistObject));
                } else {
                    throw error;
                }
            }

            LocalStorage.set(this.objectKey, newBlacklistObject);

        } else {
            // cache re-validated. Nothing to do.
        }

        // set new update time
        this._updateLocalUpdateTime();

    },

    /**
     * Gets the local cache updated time.
     * @returns {number} the last update time. If not present, returns 0.
     * @private
     */
    _getLocalUpdateTime: function() {

        const localStorageOutput = LocalStorage.get(this.lastUpdatedTimestampKey);

        if (!localStorageOutput) {
            return 0;
        } else {
            return localStorageOutput;
        }

    },

    /**
     * Sets the local cache update time to now.
     * @private
     */
    _updateLocalUpdateTime: function() {
        LocalStorage.set(this.lastUpdatedTimestampKey, Date.now());
    },

    /**
     * Loads blackListCache from persistent storage
     * If the persistent storage is empty, it will create a new array
     * If it's already loaded, then this does nothing
     * NOTE - remember to call this everytime this object is called!
     */
    _checkInitialisation: function() {

        // try loading the array from persistent storage
        if (!this.blackListCache) {

            this.blackListCache = LocalStorage.get(this.objectKey);

            // trigger a check for remote cache renewal
            this._pullRemoteChanges();

        }

        // still nothing? gotta create our own
        if (!this.blackListCache) {
            this.blackListCache = [];
        }

    },

    /**
     * Saves the current version of in memory cache to localstorage
     */
    commit: function() {

        this._checkInitialisation();
        LocalStorage.set(this.objectKey, this.blackListCache);

        // push to remote!
        RemoteStorage.pushBlackList(JSON.stringify(this.blackListCache));
        RemoteStorage.renewLastUpdateTime();

        this._updateLocalUpdateTime();

    },

    /**
     * Gets all black listed usernames
     * @returns {Iterable} a clone of the black list as an array
     */
    getBlackList: function() {

        this._checkInitialisation();
        return Array.from(this.blackListCache);

    },

    /**
     * determines whether a username is one of the blacklisted ones
     * @param {String} username the username to check
     * @returns {Boolean} true or false
     */
    isBlacklisted: function(username) {

        this._checkInitialisation();

        // if the cache is empty, then this definitely is not in the black list
        if (!this.blackListCache) {
            return false;
        }

        // cache is not empty
        if (this.blackListCache.includes(username)) {
            return true;
        } else {
            return false;
        }

    },

    /**
     * Adds a username to the black list, if it's not already there
     * @param {String} username username to add
     */
    addToBlackList: function(username) {
        
        this._checkInitialisation();

        if (this.isBlacklisted(username) === false) {
            this.blackListCache.push(username);
            this.commit();
        }

    },

    /**
     * Removes a username from the black list, 
     * if it isn't already in it
     * @param {String} username username to remove
     */
    removeFromBlackList: function(username) {
        
        this._checkInitialisation();

        if (this.isBlacklisted(username) === true) {

            // remove the username from the array
            let usernameIndex = this.blackListCache.indexOf(username);
            this.blackListCache.splice(usernameIndex, 1);

            this.commit();

        }

    }

};
/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * This module determines whether the tool will fire on a specific page or not
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* global mw */
/* exported trollJSPageEligibilityModule */

const trollJSPageEligibilityOptions = {

    // all pages on the whitelist will be processed, regardless of logic
    // format: regex literal
    whiteList: [

    ],

    // all pages on the blacklist will not be processed, regardless of logic
    // when in doubt, this one will prevail
    blackList: [
        /Wikipedia:首页/gi
    ]

}

const trollJSPageEligibilityModule = {

    commonWhiteListPrefix: "(Wikipedia(_talk)?|(User_)?talk)",

    /**
     * Gets the Wiki page title of the current page
     * @returns {String} wgPageName
     */
    getPageTitle: function() {
        return mw.config.get("wgPageName");
    },

    /**
     * Assess the current page's eligibility
     * If it returns true, App will execute
     * otherwise, App will skip this page
     * @returns {Boolean} true or false
     */
    assessPageEligibility: function() {
        
        const currentPageName = this.getPageTitle();

        // first check the black list
        for (let BLItem of trollJSPageEligibilityOptions.blackList) {
            if (currentPageName.match(BLItem)) {
                return false;
            }
        }

        // then check the white list
        for (let WLItem of trollJSPageEligibilityOptions.whiteList) {
            if (currentPageName.match(WLItem)) {
                return true;
            }
        }

        // real magic begins here

        /** the logic's quite simple for now, but it's very much open for expansion.  */
        const pageMatchRegexp = new RegExp(`${this.commonWhiteListPrefix}:(.+)`, "gi");

        if (currentPageName.match(pageMatchRegexp)) {
            return true;
        } else {
            return false;
        }

    }

}/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* exported trollJSKiller */
/* global  */

// this class specialises in processing specific DOM nodes
// to make it disappear
const trollJSKiller = {

    /**
     * Processes the given node, to help the user ignore it
     * @param {TrollJSPost} post the post to kill
     */
    killPost: function(post) {

        let nodes = post.nodes;
        nodes.forEach(this._killNode);

    },

    /**
     * Processes individual DOMNode
     * @param {Node} node the node to process. Must be a text leaf node
     */
    _killNode: function(node) {
        
        // if this not a text node, ignore it.
        if (node.nodeType !== document.TEXT_NODE) {
            return;
        }

        // get its parent element and remove this node
        const parentElmt = node.parentElement;
        parentElmt.removeChild(node);

        // done.

    }

}/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * This module handles storage of the user's blacklist preferences
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* global trollJSAppIdentifier */
/* exported trollJSPersistentObjectStorage */

// facade for window.localStorage, handles storing and saving of JavaScript objects
const LocalStorage = {

    KEY_PREFIX: "trollJS",
    STORAGE_OBJ: window.localStorage,

    /**
     * Gets an object from the storage
     * @param {String} key  the key
     * @returns {Object}    the object. null if there's none
     */
    get: function(key) {

        // key must be something
        if (!key) {
            throw new TypeError(trollJSAppIdentifier + ": cannot get storage value of falsy key");
        }

        // try to fetch the object from low level localstorage
        let objectString = this.STORAGE_OBJ.getItem(this.KEY_PREFIX + key);

        if (!objectString) {
            // nah, it's not set
            return null;
        } else {
            // good job. Parse the string into an object
            return JSON.parse(objectString);
        }

    },

    /**
     * Saves an object into the key
     * @param {String} key  must be a String
     * @param {*} value     can be any object
     */
    set: function(key, value) {

        // cannot process falsy values
        if (!key || !value) {
            throw new TypeError(trollJSAppIdentifier + 
                ": falsy key or value passed to persistent object storage's set()");
        }

        let valueString = JSON.stringify(value);
        let prefixedKey = this.KEY_PREFIX + key;
        this.STORAGE_OBJ.setItem(prefixedKey, valueString);

    },

    /**
     * Clears all items from the storage object
     */
    destroy: function() {

        let currentIndex = 0;
        let currentKey = this.STORAGE_OBJ.key(0);

        // iterate through all keys, and delete the ones that fits the prefix
        while (currentKey !== null) {
            
            // does the key match our prefix?
            if (currentKey.indexOf(this.KEY_PREFIX) === 0) {
                // yes!
                // need to clear its content
                this.STORAGE_OBJ.removeItem(currentKey);
            }

            // move on to the next one
            currentIndex += 1;
            currentKey = this.STORAGE_OBJ.key(currentIndex);

        }

    }

};/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* exported trollJSNavigator */

const trollJSNavigatorConfigs = {

    ROOT_CONTAINER_CLASS: "mw-parser-output"

}

// navigator
// specialises in navigating the tree of DOM, in our way
const trollJSNavigator = {

    /**
     * Fetches the previous node of the given node
     * By default it returns the node's previousSibling
     * If there is no previous sibling, then it returns the last leaf of 
     * the given node's parent's previous sibling
     * @param {Node} node   the node to get its previous for
     * @returns {Node}      the previous node. Null if there isn't one
     */
    getPrevious: function(node) {

        let cursor = node;

        // try getting the previous sibling
        let prevSibling = cursor.previousSibling;
        
        // if there isn't one, climb up the tree layers until there is one
        while (!prevSibling) {
            
            // climb up one layer with the cursor
            cursor = cursor.parentNode;

            // bail out if the parent is already mw-parser-output!
            if (cursor.classList.contains(trollJSNavigatorConfigs.ROOT_CONTAINER_CLASS)) {
                return null;
            }

            prevSibling = cursor.previousSibling;

        }

        // now we can commit the cursor to point to the prevSibling
        cursor = prevSibling;
        
        // climb down the ladder until we hit a leaf (the last leaf)
        while (cursor.lastChild) {
            cursor = cursor.lastChild;
        }

        return cursor;

    },

    /**
     * Fetches the next node of the given node
     * By default it returns the node's nextSibling
     * If there is no next sibling, then it returns the first child of 
     * the given node's parent's next
     * @param {Node} node   the node to get its next for
     * @returns {Node}      the result. Null if there isn't one.
     */
    getNext: function(node) {

        let cursor = node;

        // try to find a nextSibling
        let nextSibling = cursor.nextSibling;

        // if there's none, climb up a layer until there is one
        while (!nextSibling) {

            cursor = cursor.parentNode;

            // DANGER: bail out if the parent is already mw-parser-output!
            if (cursor.classList.contains(trollJSNavigatorConfigs.ROOT_CONTAINER_CLASS)) {
                return null;
            }

            nextSibling = cursor.nextSibling;

        }

        // got the next sibling
        // commit to cursor
        cursor = nextSibling;

        // now climb down the ladder until we hit a first leaf
        while (cursor.firstChild) {
            cursor = cursor.firstChild;
        }

        // done
        return cursor;

    }

};/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 * @dependency  post.js
 * 
 */
/* exported trollJSPostExtractor */
/* global TrollJSPost, trollJSNavigator */

// post Extractor
// specialises in capturing all nodes in a post
// factory method for TrollJSPost class
const trollJSPostExtractor = {

    // a series of functions to call on provisionally deployed nodes
    // if 1 of them return true on the node, then it is deemed invalid,
    // & will be discarded.
    invalidPostDetectors: [

        // NOTE - as a rule of thumb, all items here should be completed in O(1) time.

        // 1. posts with less than 3 nodes are definitely invalid
        (post) => {
            if (post.nodes.length <= 3) {
                return true;
            }
        },

        // 2. specifically detect for false positives in relation to
        //    the "vandal" template
        (post) => {
            
            // get the last node of the post
            const nNodes = post.nodes.length;
            const lastNode = post.nodes[nNodes - 1];

            // check its grandparent node. if it looks like what we expect from a vandal template,
            // return true.
            const grandparent = lastNode.parentElement.parentElement;
            
            if (
                grandparent.classList.contains("plainlinks") &&
                grandparent.id === post.getOwner() &&
                grandparent.textContent.length > 35 &&
                grandparent.textContent.length < 50
            ) {
                // jackpot!
                return true;
            }

            // no hit.
            return false;

        }

    ],


    /**
     * Factory method for Post class
     * Gets a person's entire post from one of the nodes
     * It seeks all previous siblings until it hits a time node, excludes it
     * and seeks all later siblings until it hits another time node, and includes it
     * @param {Node} node one node inside the zone
     * @returns {TrollJSPost} a new Post object
     */
    getEntirePost: function(node) {

        let newPost = new TrollJSPost();

        // if sourceNode has children, go down to its first child leaf first
        let sourceNode = node;
        while (sourceNode.firstChild) {
            sourceNode = sourceNode.firstChild;
        }

        // seek all nodes ahead, until we run into a barrier node
        // DO NOT add the barrier node itself
        let cursorAhead = sourceNode;
        while (TrollJSPost.detectBarrierNode(cursorAhead) === false) {

            // did not hit a barrier
            // add this one if it's not excluded, 
            // at the beginning because we're running to the front
            newPost.unshiftNode(cursorAhead);

            // and move on to the next one
            cursorAhead = trollJSNavigator.getPrevious(cursorAhead);

        }

        // seek all nodes behind, until we run into a barrier node
        // DO add the barrier node itself
        let cursorBehind = sourceNode;
        while (TrollJSPost.detectBarrierNode(cursorBehind) === false) {

            // first one OR did not hit a barrier
            // add this one if it's not excluded
            // at the end because we're running towards the end
            newPost.pushNode(cursorBehind);

            // move on to the next one
            cursorBehind = trollJSNavigator.getNext(cursorBehind);


        }

        // don't forget to add the barrier node itself
        newPost.pushNode(cursorBehind);
        newPost.timeNode = cursorBehind;

        // hold on... run it through the invalidPostDetector first
        for (const thisDetector of this.invalidPostDetectors) {
            if (thisDetector(newPost) === true) {
                // invalid post detected!
                return null;
            }
        }

        // got it.
        return newPost;

    }

};
/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 * @dependency  userNameExtractor.js (getOwner only)
 * 
 */
/* exported TrollJSPost */
/* global trollJSAppIdentifier, trollJSUsernameParser */

// tunable constants
const trollJSPostConfig = {
    
    excludedClasses: [

        /** CONFIG ITEM */
        // any class in this array will cause an element to become ignored by the Posts

        /^mw-headline$/,
        /^mw-editsection.+$/

    ],

    // a node's content OR title will be treated as a time
    // if it matches one of these regex patterns
    timeNodePatterns: [

        // regular signature pattern (zh.wiki)
        /(\d{4})年(\d\d?)月(\d\d?)日 \([一二三四五六日]\) (\d\d?):(\d\d?) (\(UTC\))/gi,

        // {{unsigned}} template pattern (zh.wiki)
        /—以上未簽名的留言(是)?由(.+)((對話.*))([於在](.+))?加入(的)?。/gi /** zh-hant */,
        /—以上未签名的留言(是)?由(.+)((对话.*))([于在](.+))?加入(的)?。/gi /** zh-hans */,

        // zh-yue wiki's signature pattern
        /(\d{4})年(\d\d?)月(\d\d?)[號号日] \([一二三四五六日]\) \d\d:\d\d \(UTC\)/gi,

        // enwiki signature time pattern
        /\d\d:\d\d, \d\d? (January|February|March|April|May|June|July|August|September|October|November|December) \d{4} \(UTC\)/gi

    ]

};

// post class
// represent one post by one person
class TrollJSPost {

    constructor() {
        this.nodes = [];
        this.timeNode = null;
        this._cachedOwner = null;
    }

    /**
     * Determines if this post is equal to another post
     * @param {TrollJSPost} anotherPost the other post
     * @returns {Boolean}               true or false
     */
    equals(anotherPost) {

        // node list can't be empty!
        if (this.nodes.length === 0) {
            throw new Error(trollJSAppIdentifier + 
                " equalsTo() is called on an empty post");
        }

        // if the owners aren't equal, they are different posts
        if (this._cachedOwner !== anotherPost._cachedOwner) {
            return false;
        }

        // if they have different numbers of nodes, they can't be equal
        if (this.nodes.length !== anotherPost.nodes.length) {
            return false;
        }

        // then,
        // two posts are equal iff every single node in this post
        // is present in the other post
        for (const currentNode of this.nodes) {
            
            if (anotherPost.nodes.includes(currentNode) === false) {
                return false;
            } 

        }

        // all tests passed. They are equal.
        return true;

    }

    /**
     * Detects if a node is excluded.
     * Excluded nodes are exempt from deletion, and won't be included in a post object at all.
     * Examples include mw-headline and mw-editsection* nodes
     * @param   {Node}      node    the node to check
     * @returns {Boolean}   true or false
     */
    static detectExcludedNode(node) {
        
        // falsy nodes are definitely excluded
        if (!node) {
            return true;
        }

        // determine node to check. 
        // if it's element, then nodeToCheck is node itself
        // otherwise, it's the node's first element parent
        let nodeToCheck;
        if (node.nodeType === document.ELEMENT_NODE) {
            nodeToCheck = node;
        } else {
            nodeToCheck = node.parentElement;
        }

        // iterate over all classes of the node, 
        // and check for a match with one of the excluded patterns
        for (const cssClass of nodeToCheck.classList) {
            for (const regexp of trollJSPostConfig.excludedClasses) {
                if (cssClass.match(regexp)) {
                    return true;
                }
            }
        }
        
        // all tests done and none matched
        return false;

    }

    /**
     * checks whether a given node is a signature time (one that's left with 2020年5月14日 (四) 13:16 (UTC))
     * @param {Node} node node to check
     * @returns true or false
     */
    static detectTimeNode(node) {

        let stringsToCheck = [];

        if (node.nodeType === document.TEXT_NODE) {

            stringsToCheck.push(node.textContent);
            
            // gets its parentnode's full text, if this node is the last child
            // this is to take care of unsigned templates & results of local time tool
            const parent = node.parentElement;
            if (parent.lastChild === node) {
                stringsToCheck.push(parent.textContent);
                stringsToCheck.push(parent.title);
            }

        } else { /** wrong type, definitely not what we're looking for */

            return false;

        }

        // iterate through timeNodePatterns and try to match stringToCheck one by one
        for (const targetRegex of trollJSPostConfig.timeNodePatterns) {

            // if one string in stringsToCheck matches, we're golden.
            for (const stringToCheck of stringsToCheck) {
                if (stringToCheck && stringToCheck.match(targetRegex)) {
                    return true;
                }
            }

        }

        // no match.
        return false;

    }


    /**
     * Check whether a node is a barrier, delinating different people's posts
     * A post should contain a barrier at its end, but not at its beginning.
     * Usually it's a time node, excluded node, or falsy (meaning we have run out of nodes)
     * @param   {Node} node Node to check
     * @returns {Boolean}   true or false
     */
    static detectBarrierNode(node) {

        if (!node) {
            return true;
        }

        return (this.detectExcludedNode(node) || this.detectTimeNode(node));

    }

    /**
     * Detects whether a particular node can be appended or not
     * @param {Node} node the node to detect
     * @returns {Boolean} true or false
     */
    assessNodeAppendability(node) {

        if (this.nodes.includes(node)) {
            return false;
        }

        return TrollJSPost.detectExcludedNode(node) !== true;



    }

    /**
     * Adds a node to the end of the list, if it doesn't already exist & is not excluded.
     * @param {Node} nodeToAdd  node to add
     */
    pushNode(nodeToAdd) {
        
        if (this.assessNodeAppendability(nodeToAdd) === true) {
            this.nodes.push(nodeToAdd);
        }

    }

    /**
     * Adds a node to the beginning of the list, if it doesn't already exist & is not excluded.
     * @param {Node} nodeToAdd node to add
     */
    unshiftNode(nodeToAdd) {
        
        if (this.assessNodeAppendability(nodeToAdd) === true) {
            this.nodes.unshift(nodeToAdd);
        }

    }

    /**
     * Gets the owner of the post
     * It's the last username that appears
     * @returns {String} the username
     */
    getOwner() {

        // we only need to find the owner once
        // if it's already been found before...
        if (this._cachedOwner) {
            return this._cachedOwner;
        }

        let result;

        // iterate through all posts from beginning to end
        // if we find a valid username, then mark the result as it
        for (let i = 0; i < this.nodes.length; ++i) {

            const thisNode = this.nodes[i];
            let thisUsername = trollJSUsernameParser.getUserNameFromNode(thisNode);

            if (thisUsername != null) {
                result = thisUsername;
            }

        }

        // now result should be the last username ever seen,
        // and that's our lucky user

        // hold on a moment... cache the result.
        this._cachedOwner = result;

        // all good
        return result;

    }

}
/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* exported TrollJSRegistrar */

// keeps track of which trolls and nodes have already been processed
class TrollJSRegistrar {

    constructor() {
        this.slashCountByTroll = {};
        this.posts = [];
    }

    /**
     * Increments a troll's slash count
     * @param {String} trollName 
     */
    _incrementTrollSlashCount(trollName) {
        
        // has this troll been recorded in our record yet?
        // if not, create its record
        if (!this.slashCountByTroll[trollName]) {
            this.slashCountByTroll[trollName] = 0;
        }

        this.slashCountByTroll[trollName] += 1;

    }

    /**
     * Mark a post slashed
     * by adding one to its owner in slashCountByTroll
     * @param {TrollJSPost} post 
     */
    markPostSlashed(post) {
        const postOwner = post.getOwner();
        this._incrementTrollSlashCount(postOwner);
    }

    /**
     * Adds a post, if it doesn't already exist
     * @param {TrollJSPost} newPost the post to add
     */
    addPost(newPost) {

        // don't do anything if newPost is falsy or empty!
        if (newPost == null) {
            return;
        }
        
        if (this.hasPost(newPost) === false) {
            this.posts.push(newPost);
        }

    }

    /**
     * Checks whether a post already exists or not
     * @param {TrollJSPost} post the post to check
     * @returns true or false
     */
    hasPost(post) {
        
        // must call every single post individually
        for (const existingPost of this.posts) {
            
            if (post.equals(existingPost)) {
                return true;
            }

        }

        // no hit.
        return false;

    }

    /**
     * Retrieves all posts in the registrar.
     * You can assume there'll be no duplicates
     * @returns a list of posts
     */
    getAllPosts() {
        return this.posts;
    }

}/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 *
 * This module handles interaction between the local storage cache and the remote MediaWiki-managed user options
 *
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 *
 */
/* global mw */
/* exported MWApiOptionsInterface */

const MW_API_BLACKLIST_KEY = "userjs-trollJSBlacklist";
const MW_API_TIMESTAMP_KEY = "userjs-trollJSLastUpdated";

const apiEndpoint = new mw.Api();

const RemoteStorage = {

    /**
     * This makes a request to the MW API for currently stored user options.
     * @param {string} key: the key to the value
     * @returns {Promise<string>} whatever that's stored.
     * @throws {Error} if the thing is not set
     * @private
     * Exploratory test completed 2020-05-11
     */
    _readOptions: async function(key) {
        
        const response = await apiEndpoint.post({
            "action": "query",
            "format": "json",
            "meta": "userinfo",
            "utf8": 1,
            "uiprop": "options"
        });

        const extractedValue = response.query.userinfo.options[key];
        if (!extractedValue) {
            throw Error("The requested key is not present.");
        }

        return extractedValue;

    },

    /**
     * This makes a request to the MW API to write a value.
     * @param key {string} key to the value
     * @param value {string} value to store
     * @private
     * Exploratory test passed 2020-05-11
     */
    _writeOptions: async function(key, value) {
        
        const queryParams = {
            "action": "options",
            "format": "json",
            "optionname": key,
            "optionvalue": value,
            "utf8": 1
        };

        const response = await apiEndpoint.postWithEditToken(queryParams);
        
        if (response.warnings) {
            throw Error("Failed to write to options");
        }

    },

    /**
     * Gets the current blacklist stored on MediaWiki
     * @returns {Promise<object>} whatever's stored.
     * @throws {Error} if blacklist is not stored
     */
    getBlackList: async function() {
        const rawResponse = await this._readOptions(MW_API_BLACKLIST_KEY);
        return JSON.parse(rawResponse);
    },

    /**
     * Gets the last update time stored in MediaWiki User Preferences
     * NOTE - this will parse the raw string into a number
     * @returns {Promise<Number>} whatever's stored
     * @throws {Error} if there's any error
     */
    getLastUpdateTime: async function() {
        const apiOutput = await this._readOptions(MW_API_TIMESTAMP_KEY);
        return parseInt(apiOutput);
    },

    /**
     * Pushes the blacklist into the API storage
     * @param {string} newBlacklistJSON the new JSON to store in the key
     * @throws {Error} if API returns an error
     * WARNING - this *does not* update the timestamp
     */
    pushBlackList: async function(newBlacklistJSON) {
        return await this._writeOptions(MW_API_BLACKLIST_KEY, newBlacklistJSON);
    },

    /**
     * Updates the last updated timestamp.
     * @param {Number} timestamp the new timestamp. Will be stringified.
     * @returns {Promise<void>}
     */
    pushLastUpdateTime: async function(timestamp) {
        return await this._writeOptions(MW_API_TIMESTAMP_KEY, JSON.stringify(timestamp));
    },

    /**
     * Same as the function above, but automatically fills in the current time from
     * Date.now()
     */
    renewLastUpdateTime: async function() {
        return await this.pushLastUpdateTime(Date.now());
    }

}/**
 * this file is part of...
 * troll.js for Chinese Wikipedia
 * 
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 * 
 */
/* exported trollJSUsernameParser */
/* global trollJSAppIdentifier */

const trollJSUserNameIDPrefix = "(User:|U:|User[ _]Talk:|UT:|Special:(用戶貢獻|用户贡献)\\/)";

// this object specialises in extracting username from nodes
const trollJSUsernameParser = {

    // for example: "/wiki/User:Classy_Melissa" => "Classy Melissa"
    //userNameExtractorPattern: /`(?<=^https.+\\/wiki\\/${trollJSUserNameIDPrefix}).+$`/gi,

    /**
     * Gets a username from a link node
     * @param {Node} link the link to analyse
     * @returns the user name. If there isn't one, it will return null.
     */
    getUserNameFromLink: function(link) {
        
        // first check that link is actually a link
        if (this._detectLinkNode(link) === false) {
            throw new TypeError(trollJSAppIdentifier + 
                " invalid node passed to userNameExtractor.getUserNameFromLink");
        }

        const extractorPattern = new RegExp(`${trollJSUserNameIDPrefix}(.+)[/]?$`, "gi");

        const linkDestination = decodeURI(link.href);

        // try to extract the user name with regex
        const userNameMatches = extractorPattern.exec(linkDestination);

        // did we get anything out?
        if (!userNameMatches) {
            return null;
        } else {
            // got something!
            return userNameMatches[3].replace(/_/gi, " ");
        }


    },

    /**
     * Detects if a node is a link or not
     * @param {Node} nodeToCheck the node to check
     * @returns {Boolean} true or false
     */
    _detectLinkNode: function(nodeToCheck) {
        
        // if it's not an element, then definitely not a link
        if (nodeToCheck.nodeType != document.ELEMENT_NODE) {
            return false;
        }

        // then it's just a matter of tagname
        if (nodeToCheck.tagName === 'A') {
            return true;
        } else {
            return false;
        }

    },

    /**
     * Parses the username from a node
     * If the node is not a link, this function will try to find a link in its parent elements
     * @param {Node} nodeToCheck the node to check
     * @returns {String} the username. If there isn't one, it will return null
     */
    getUserNameFromNode: function(nodeToCheck) {
        
        let linkNode = nodeToCheck;
        
        while (this._detectLinkNode(linkNode) === false) {

            // not a link yet
            // need to go for its parent element
            linkNode = linkNode.parentElement;

            // if we have run out of parent elements...
            if (!linkNode) {
                // definitely no username here.
                return null;
            }

        }

        // got a link node
        return this.getUserNameFromLink(linkNode);

    }

}
/**
 * main.js for Chinese Wikipedia
 *
 * This module simply launches all other necessary modules
 *
 * @author      User:Classy Melissa @ zh.wikipedia.org
 * @version     1
 * @licence     CC-0
 *
 */

trollJSBlockUI.start();
trollJSApp.start();}
trollJSContainerFunction();