// ==UserScript== // @name Unclosed Request Review Script // @namespace http://github.com/Tiny-Giant // @version 2.1.0 // @description Adds buttons to the chat buttons controls; clicking on the button takes you to the recent unclosed close vote request, or delete request query, then it scans the results and displays them along with additional information. // @author @TinyGiant @rene @mogsdad @Makyen // @include /^https?://chat\.stackoverflow\.com/rooms/(?:41570|90230|126195|68414|111347|126814|123602|167908|167826)(?:\b.*$|$)/ // @include /^https?://chat\.stackoverflow\.com/search.*[?&]room=(?:41570|90230|126195|68414|111347|126814|123602|167908|167826)(?:\b.*$|$)/ // @include /^https?://chat\.stackoverflow\.com/transcript/(?:41570|90230|126195|68414|111347|126814|123602|167908|167826)(?:\b.*$|$)/ // @include /^https?://chat\.stackoverflow\.com/transcript/.*$/ // @include /^https?://chat\.stackoverflow\.com/users/.*$/ // @require https://github.com/SO-Close-Vote-Reviewers/UserScripts/raw/master/gm4-polyfill.js // @downloadURL https://github.com/SO-Close-Vote-Reviewers/UserScripts/raw/master/UnclosedRequestReview.user.js // @updateURL https://github.com/SO-Close-Vote-Reviewers/UserScripts/raw/master/UnclosedRequestReview.user.js // @grant GM_openInTab // @grant GM.openInTab // ==/UserScript== /* jshint -W097 */ /* jshint -W107 */ /* jshint esnext:true */ /* globals CHAT */ (function() { 'use strict'; if (window !== window.top) { //If this is running in an iframe, then we do nothing. return; } if (window.location.pathname.indexOf('/transcript/message') > -1) { //This is a transcript without an indicator in the URL that it is a room for which we should be active. if (document.title.indexOf('SO Close Vote Reviewers') === -1 && document.title.indexOf('SOCVR Request Graveyard') === -1 && document.title.indexOf('SOCVR /dev/null') === -1 && document.title.indexOf('SOCVR Testing Facility') === -1 && document.title.indexOf('SOBotics') === -1 ) { //The script should not be active on this page. return; } } const NUMBER_UI_GROUPS = 8; const LSPREFIX = 'unclosedRequestReview-'; const MAX_DAYS_TO_REMEMBER_VISITED_LINKS = 7; const MAX_BACKOFF_TIMER_SECONDS = 120; const MESSAGE_THROTTLE_PROCESSING_ACTIVE = -9999; const MESSAGE_PROCESSING_DELAY_FOR_MESSAGE_VALID = 1000; const MESSAGE_PROCESSING_DELAYED_ATTEMPTS = 5; const MESSAGE_PROCESSING_ASSUMED_MAXIMUM_PROCESSING_SECONDS = 10; const DEFAULT_MINIMUM_UPDATE_DELAY = 5; // (seconds) const DEFAULT_AUTO_UPDATE_RATE = 5; // (minutes) const MESSAGE_PROCESSING_REQUEST_TYPES = ['questions', 'answers', 'posts']; const UI_CONFIG_DEL_PAGES = 'uiConfigDel'; const UI_CONFIG_CV_PAGES = 'uiConfigCv'; const UI_CONFIG_REOPEN_PAGES = 'uiConfigReopen'; const months3charLowerCase = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; const weekdays3charLowerCase = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; /* The following code for detecting browsers is from my answer at: * http://stackoverflow.com/a/41820692/3773011 * which is based on code from: * http://stackoverflow.com/a/9851769/3773011 */ //Opera 8.0+ (tested on Opera 42.0) const isOpera = (!!window.opr && !!window.opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; //Firefox 1.0+ (tested on Firefox 45 - 53) const isFirefox = typeof InstallTrigger !== 'undefined'; //Internet Explorer 6-11 // Untested on IE (of course). Here because it shows some logic for isEdge. const isIE = /*@cc_on!@*/false || !!document.documentMode; //Edge 20+ (tested on Edge 38.14393.0.0) const isEdge = !isIE && !!window.StyleMedia; //The other browsers are trying to be more like Chrome, so picking // capabilities which are in Chrome, but not in others, is a moving // target. Just default to Chrome if none of the others is detected. const isChrome = !isOpera && !isFirefox && !isIE && !isEdge; // Blink engine detection (tested on Chrome 55.0.2883.87 and Opera 42.0) const isBlink = (isChrome || isOpera) && !!window.CSS; // eslint-disable-line no-unused-vars //Various objects to hold functions and current state. const funcs = { visited: {}, config: {}, backoff: {}, ui: {}, mmo: {}, mp: {}, orSearch: {}, }; //Current state information const config = { ui: {}, nonUi: {}, backoff: {}, }; //Global backoff timer, which is synced between tabs. const backoffTimer = { timer: 0, isPrimary: false, timeActivated: 0, milliseconds: 0, }; //State for message processing const messageProcessing = { throttle: 0, throttleTimeActivated: 0, isRequested: false, interval: 0, mostRecentRequestInfoTime: 0, }; //State information for adding OR functionality to searches. const orSearch = { framesToProces: 0, maxPages: 0, }; //Update RegExp from list here: https://github.com/AWegnerGitHub/SE_Zephyr_VoteRequest_bot const pleaseRegExText = '(?:pl(?:ease|s|z)|p.?[sz]|.?l[sz]|pl.?|.pl[sz]|p.l[sz]|pl.[sz]|pl[sz].)'; const requestTagRegExStandAlonePermittedTags = '(?:spam|off?en[cs]ive|abb?u[cs]ive|(?:re)?-?flag(?:-?(?:naa|spam|off?en[cs]ive|rude|abb?u[cs]ive))|(?:(?:naa|spam|off?en[cs]ive|rude|abb?u[cs]ive)-?(?:re)?-?flag))'; //spam is an actual SO tag, so we're going to need to deal with that. const requestTagRequirePleaseRegExText = '(?:cv|(?:un-?)?(?:del(?:v)?|dv|delete)|rov?|re-?open|app?rove?|reject|rv|review|(?:re)?-?flag|nuke?|spam|off?en[cs]ive|naa|abbu[cs]ive)'; const requestTagRequirePleaseOrStandAloneRegExText = '(?:' + requestTagRequirePleaseRegExText + '|' + requestTagRegExStandAlonePermittedTags + ')'; const requestTagRequirePleasePleaseFirstRegExText = '(?:' + pleaseRegExText + '[-.]?' + requestTagRequirePleaseOrStandAloneRegExText + ')'; const requestTagRequirePleasePleaseLastRegExText = '(?:' + requestTagRequirePleaseOrStandAloneRegExText + '[-.]?' + pleaseRegExText + ')'; const requestTagRegExText = '\\b(?:' + requestTagRegExStandAlonePermittedTags + '|' + requestTagRequirePleasePleaseFirstRegExText + '|' + requestTagRequirePleasePleaseLastRegExText + ')\\b'; //Current, now older, result: https://regex101.com/r/dPtRnS/3 /*Need to update with (?:re\W?)? for flags \b(?:(?:spam|off?ensive|abb?usive|flag(?:-?(?:naa|spam|off?ensive|rude|abb?usive))|(?:(?:naa|spam|off?ensive|rude|abb?usive)-?flag))|(?:(?:pl(?:ease|s|z)|p.?[sz]|.?l[sz]|pl.?|.pl[sz]|p.l[sz]|pl.[sz]|pl[sz].)-(?:(?:cv|(?:un)?(?:del(?:v)?|dv|delete)|rov?|reopen|app?rove?|reject|rv|review|flag|nuke?|spam|off?ensive|naa|abbusive)|(?:spam|off?ensive|abb?usive|flag(?:-?(?:naa|spam|off?ensive|rude|abb?usive))|(?:(?:naa|spam|off?ensive|rude|abb?usive)-?flag))))|(?:(?:(?:cv|(?:un)?(?:del(?:v)?|dv|delete)|rov?|reopen|app?rove?|reject|rv|review|flag|nuke?|spam|off?ensive|naa|abbusive)|(?:spam|off?ensive|abb?usive|flag(?:-?(?:naa|spam|off?ensive|rude|abb?usive))|(?:(?:naa|spam|off?ensive|rude|abb?usive)-?flag)))-(?:pl(?:ease|s|z)|p.?[sz]|.?l[sz]|pl.?|.pl[sz]|p.l[sz]|pl.[sz]|pl[sz].)))\b */ //Used to look in text to see if there are any messages which contain the action tag as text. //Only a limited set of action types are recognized in text format. const getActionTagInTextRegEx = /(?:\[(?:tag\W?)?(?:cv|(?:un-?)?del(?:ete|v)?|re-?open)-[^\]]*\])/; //Detect the type of request based on tag text content. const tagsInTextContentRegExes = { delete: /\b(?:delv?|dv|delete)(?:pls)?\b/i, undelete: /\b(?:un?-?delv?|un?-?dv|un?-?delete)(?:pls)?\b/i, close: /\b(?:cv)(?:pls)?\b/i, reopen: /\b(?:re-?open)(?:pls)?\b/i, spam: /\bspam\b/i, offensive: /\b(?:off?en[cs]ive|rude|abb?u[cs]ive)\b/i, flag: /\b(?:re)?-?flag-?(?:pl(?:ease|s|z)|p.?[sz]|.?l[sz]|pl.?|.pl[sz]|p.l[sz]|pl.[sz]|pl[sz].)?\b/i, reject: /\b(?:reject|review)(?:pls)?\b/i, //20k+ tags tag20k: /^(?:20k\+?(?:-only)?)$/i, tagN0k: /^(?:\d0k\+?(?:-only)?)$/i, request: new RegExp(requestTagRegExText, 'i'), }; //The extra escapes in RegExp are due to bugs in the syntax highlighter in an editor. They are only there because it helps make the syntax highlighting not be messed up. const getQuestionIdFromURLRegEx = /(?:^|[\s"])(?:(?:https?:)?(?:(?:\/\/)?(?:www\.|\/\/)?stackoverflow\.com\/))(?:q[^\/]*|posts)\/+(\d+)/g; // eslint-disable-line no-useless-escape //https://regex101.com/r/QzH8Jf/2 const getSOQuestionIdFfromURLButNotIfAnswerRegEx = /(?:^|[\s"(])(?:(?:https?:)?(?:(?:\/\/)?(?:www\.|\/\/)?stackoverflow\.com\/))(?:q[^\/]*)\/+(\d+)(?:(?:\/[^#\s]*)#?)?(?:$|[\s")])/g; // eslint-disable-line no-useless-escape //XXX Temp continue to use above variable name until other uses resolved. const getSOQuestionIdFfromURLNotPostsNotAnswerRegEx = getSOQuestionIdFfromURLButNotIfAnswerRegEx; //https://regex101.com/r/w2wQoC/1/ //https://regex101.com/r/SMVJv6/3/ const getSOAnswerIdFfromURLRegExes = [ /(?:^|[\s"(])(?:(?:https?:)?(?:(?:\/\/)?(?:www\.|\/\/)?stackoverflow\.com\/))(?:a[^\/]*)\/+(\d+)(?:\s*|\/[^/#]*\/?\d*\s*)(?:$|[\s")])/g, // eslint-disable-line no-useless-escape /(?:^|[\s"'(])(?:(?:https?:)?(?:(?:\/\/)?(?:www\.|\/\/)?stackoverflow\.com\/))(?:q[^\/]*|posts)[^\s#]*#(\d+)(?:$|[\s"')])/g, // eslint-disable-line no-useless-escape ]; const getSOPostIdFfromURLButNotIfAnswerRegEx = /(?:^|[\s"(])(?:(?:https?:)?(?:(?:\/\/)?(?:www\.|\/\/)?stackoverflow\.com\/))(?:posts)\/+(\d+)(?:\s*|\/[^\/#]*\/?\d*\s*)(?:\s|$|[\s")])/g; // eslint-disable-line no-useless-escape const getSOQuestionOrAnswerIdFfromURLRegExes = [getSOQuestionIdFfromURLNotPostsNotAnswerRegEx].concat(getSOAnswerIdFfromURLRegExes); //Some constants which it helps to have some functions in order to determine const isChat = window.location.pathname.indexOf('/rooms/') === 0; const isSearch = window.location.pathname === '/search'; const isTranscript = window.location.pathname.indexOf('/transcript') === 0; const isUserPage = window.location.pathname.indexOf('/users') === 0; var uiConfigStorage; //Functions needed on both the chat page and the search page //Utility functions funcs.executeInPage = function(functionToRunInPage, leaveInPage, id) { // + any additional JSON-ifiable arguments for functionToRunInPage //Execute a function in the page context. // Any additional arguments passed to this function are passed into the page to the // functionToRunInPage. // Such arguments must be Object, Array, functions, RegExp, // Date, and/or other primitives (Boolean, null, undefined, // Number, String, but not Symbol). Circular references are // not supported. Prototypes are not copied. // Using () => doesn't set arguments, so can't use it to define this function. // This has to be done without jQuery, as jQuery creates the script // within this context, not the page context, which results in // permission denied to run the function. function convertToText(args) { //This uses the fact that the arguments are converted to text which is // interpreted within a