3

One of the sites I use has in excess of 10% of their questions protected. It's a small site but this seems like a huge amount, largely due to questions being on the HNQ regularly and attracting many low-quality answers.

If the site decided on these parameters (likely based on either question age or last activity, whether it'd been protected more than once, who had protected it... etc) would the API allow users with the required reputation (3500 on a beta site) to run such a script?

I'm trying to ascertain what's possible without knowing the parameters because I don't want to sell a tool that's not something that can be implemented.

See this related meta question about whether the site wants to develop and use this tool for context for this.

  • 2
    The 2.2 API doesn't have an endpoint that makes this possible, at least I can't find it. You can flag posts but protecting and unprotecting are different beasts. I'm not sure if the API used in the Stack Overflow app does offer the protect/unprotect option? – rene Dec 5 '17 at 18:23
2

Yes, it is possible to create such a script and here it is:

// ==UserScript==
// @name         Unprotect questions
// @namespace    https://meta.stackexchange.com/users/158100/rene
// @version      0.2
// @description  Unprotect questions in a batch
// @author       rene
// @match        https://*.stackexchange.com/tools/protected-questions
// @grant        none
// ==/UserScript==

(function(fkey) {
    'use strict';

    // this determines which question to select, available fields are: 'title','questionDate','displayname','protectedDate','answerCount','deletedCount'
    const filter = (i) => i.protectedDate < Date.parse('2017-11-01') && i.displayname === 'rene';


    // I need the question id, so regex it out of the url
    const parseUserOrQuestionId = /.*(?:\/(?:questions|users)\/(\d+))/g;

    // map each TD to a property of a question object
    function mapRow2Question(tr) {
        // this are the TD elements
        var qitems = tr.children;
        var q = {};
        // map a column to a property, with a few to spare at the end
        var key = ['title','questionDate','displayname','protectedDate','answerCount','deletedCount','qurl','dummy', 'uurl','questionid', 'dummy2', 'userid'];
        for (var j=0; j<qitems.length; j++) {
            var qitem = qitems[j];
            if ( j === 0 || j === 2) {
                // this td contains an anchor element
                // I keep the text and the href in separate properties
                qitem = qitem.children[0];
                // the href hold the id (question or user)
                var m = parseUserOrQuestionId.exec(qitem.href);
                if (m !== null) {
                  // map to the right column
                  q[key[j+9]] = m[1];
                }
                q[key[j+6]] = qitem.href;
                q[key[j]] = qitem.textContent;
            } else if (j === 1 || j === 3)  {
                // these are date columns
                q[key[j]] = Date.parse(qitem.textContent);
            } else {
                // these are integer columns
                q[key[j]] = Number.parseInt(qitem.textContent, 10);
            }
        }
        q.src = tr; // for feedback in the UI
        return q;
    }

    function search(filter) {
        var questions = document.getElementById('questions').querySelectorAll('tbody')[0].children;
        var qlist = [],
            selected =[];

        for(var i=0; i<questions.length; i++) {
            qlist.push(mapRow2Question(questions[i]));
        }

        // apply the filter, keep the new array around
        selected = qlist.filter(filter);
        // add checkboxes so you can deselect a question
        selected.forEach( (i) => {
            var qtd = i.src.children[0];
            var cb = document.createElement('input');
            i.selected = true;
            cb.type='checkbox';
            cb.checked ='checked';
            cb.addEventListener('click', function() { i.selected = !i.selected; });
            qtd.insertBefore(cb, qtd.children[0]);
        });
        return selected;
    }

    // clumsy way to add the buttons in the header
    function addButton(txt, handler) {
        var tabs = document.getElementById('tabs');
        var hdr = tabs.parentNode;
        var btn = document.createElement('button');
        btn.addEventListener('click', handler);
        btn.textContent = txt;
        btn.style.margin = "5px";
        hdr.insertBefore(btn, tabs);
        return btn;
    }

    // this will unprotect the question for real
    function unprotect(qid, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/question/unprotect');
        xhr.addEventListener('load', function() {
            if (xhr.status !== 200) {
                console.log('no success for ', qid);
                if (callback) callback();
            }
        });
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        console.log('about to do post /question/unprotect', "id=" + qid.toString() + "&fkey=" + fkey.toString());
        // uncomment the next line to actual call un-protect
        // xhr.send("id=" + qid.toString() + "&fkey=" + fkey.toString());
    }

    // add the buttons and wire the click events
    function init() {
        var timer,
            questionsToUnprotect = [],
            unprotectBtn,
            start;

        start = addButton('search', function() {
            questionsToUnprotect = search(filter);
            unprotectBtn.disabled = false;
            start.disabled = true;
        });
        unprotectBtn = addButton('unprotect', function() {
            unprotectBtn.disabled = true; // we don't want to fire multiple times
            // get all protected questions that are selected
            var work = questionsToUnprotect.filter( (i) => i.selected);
            // to prevent throttle, go over them every 5 seconds
            timer = setInterval(function() {
                var item = work.shift();
                if (item === null || item === undefined) {
                    clearInterval(item);
                    start.disabled = false;
                } else {
                    if (item.selected) {
                      unprotect(item.questionid, function() {
                          // error handling
                          item.src.children[0].style.backgroundColor = 'red';
                          clearInterval(timer); // make sure to stop
                      });
                      item.src.children[0].children[0].checked = false;
                    }
                }
            }, 5000);
        });
        unprotectBtn.disabled = true;
    }

    init();

})(StackExchange.options.user.fkey);

You simply visit /tools/protected-questions and then the script will add two buttons, search and unprotect. Click Search to select the question that match the filter (hardcoded in the script for now). The questions that are selected will have a checkmark in front of them. Deselect any questions you don't want to unprotect. Click Unprotect to start the processing of the questions that are still selected. To prevent issues it handles a question every 5 seconds.

I tested it on a few questions on MSE as I didn't want to annoy anyone.

Notice in the code there are these lines:

// uncomment the next line to actual call un-protect
// xhr.send("id=" + qid.toString() + "&fkey=" + fkey.toString());

They work as advertised. It allows you play with the script without it doing any damage (at least that I noted).

The filter criteria are at the moment not settable from the UI. You have to alter the script. The filter is near the top:

// this determines which question to select, available fields are: 'title','questionDate','displayname','protectedDate','answerCount','deletedCount'
const filter = (i) => i.protectedDate < Date.parse('2017-11-01') && i.displayname === 'rene';

You want to create a predicate after the => that returns true for those questions that you want to have unprotected. You'll have to change it anyway as I don't believe you want to only unprotect questions that I protected ...

You can find the source on github(direct install)

I tested the script on TamperMonkey with Chrome and like to keep it that way.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .