// ==UserScript== // @name 'Saviour' of Lost Souls // @namespace https://github.com/Glorfindel83/ // @description Adds a shortcut to down-/close-/delete vote and post a welcoming comment to Lost Souls on Meta Stack Exchange and some other sites. // @author Glorfindel // @updateURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/saviour-of-lost-souls/saviour-of-lost-souls.user.js // @downloadURL https://raw.githubusercontent.com/Glorfindel83/SE-Userscripts/master/saviour-of-lost-souls/saviour-of-lost-souls.user.js // @version 1.1 // @match *://meta.stackexchange.com/questions/* // @match *://meta.stackoverflow.com/questions/* // @match *://softwarerecs.stackexchange.com/questions/* // @match *://softwarerecs.stackexchange.com/review/first-posts* // @match *://hardwarerecs.stackexchange.com/questions/* // @match *://hardwarerecs.stackexchange.com/review/first-posts* // @exclude *://meta.stackexchange.com/questions/ask // @exclude *://meta.stackoverflow.com/questions/ask // @exclude *://softwarerecs.stackexchange.com/questions/ask // @exclude *://hardwarerecs.stackexchange.com/questions/ask // @grant none // @require https://gist.github.com/raw/2625891/waitForKeyElements.js // ==/UserScript== /* global $, waitForKeyElements */ (function ($) { "use strict"; // Find question (works when on Q&A page) let question = $('#question'); if (question.length == 0) return; main(question); })(window.jQuery); // Wait for question (works when in review queue) waitForKeyElements('div.review-content div.question', function(jNode) { main(jNode); }); function main(question) { // Check if author is likely to be a lost soul let owner = question.find('div.post-signature.owner'); if (owner.length == 0) // happens with Community Wiki posts return; let reputation = owner.find('span.reputation-score')[0].innerText; if (reputation === "1") { // Do nothing: 1 rep qualifies for a lost soul } else { // Child meta sites require some reputation to post a question, so we need other rules: let isNewContributor = owner.find('span.js-new-contributor-label').length > 0; let hasLowReputation = reputation <= 101; // association bonus let negativeQuestionScore = parseInt(question.find('div.js-vote-count').text()) < 0; let numberOfReasons = (isNewContributor ? 1 : 0) + (hasLowReputation ? 1 : 0) + (negativeQuestionScore ? 1 : 0); if (numberOfReasons < 2) return; } // Which site? let isMetaSE = location.host == 'meta.stackexchange.com'; let isBeta = location.host == 'hardwarerecs.stackexchange.com'; // My reputation let myReputation = parseInt($('a.my-profile div.-rep')[0].innerText.replace(/,/g, '')); let hasCommentPrivilege = myReputation >= (isMetaSE ? 5 : 50); let hasFlagPrivilege = myReputation >= 15; let hasUpvotePrivilege = myReputation >= 15; let hasDownvotePrivilege = myReputation >= (isMetaSE ? 100 : 125); let hasCloseVotePrivilege = myReputation >= (isBeta ? 500 : 3000); let hasDeleteVotePrivilege = myReputation >= (isBeta ? 4000 : 20000); let isModerator = $("a.js-mod-inbox-button").length > 0; // Can the script do anything? if (!hasCommentPrivilege && !hasFlagPrivilege) return; // Score; downvoted or not? let downvoted = question.find('a.vote-down-on').length > 0; let score = parseInt(question.find('div.js-vote-count')[0].innerText.replace(/,/g, '')); // Closed? let status = $('div.question-status h2 b'); let statusText = status.length > 0 ? status[0].innerText : ''; let closed = statusText == 'marked' || statusText == 'put on hold' || statusText == 'closed'; // Is there any comment not by the author? let comments = question.find('ul.comments-list'); var hasNonOwnerComment = false; comments.find('a.comment-user').each(function() { if (!$(this).hasClass('owner')) { hasNonOwnerComment = true; } }); // Determine which actions to take // Comment let shouldComment = hasCommentPrivilege && !hasNonOwnerComment; // Downvote (not when the post is already on -3 or lower, to be slightly more welcoming) let shouldDownvote = hasDownvotePrivilege && !downvoted && score > -3; // Flag/vote to close let shouldFlag = hasFlagPrivilege && !hasCloseVotePrivilege && !closed; let shouldVoteToClose = hasCloseVotePrivilege && !closed; // Vote to delete let shouldVoteToDelete = (hasDeleteVotePrivilege && closed && score <= -3) || isModerator; // Add post menu button let menu = question.find('div.post-menu'); menu.append($('|')); let button = $('lost soul'); menu.append(button); button.click(function() { // Generate HTML for dialog var html = ` `; $(document.body).append($(html)); }); // Define functions which can be called by the dialog window.saviourOfLostSouls = {}; saviourOfLostSouls.closeDialog = function() { $("#modal-base").remove(); }; saviourOfLostSouls.submitDialog = function() { // Prepare votes/comments let postID = parseInt(question.attr('data-questionid')); let fkey = window.localStorage["se:fkey"].split(",")[0]; if (shouldComment || $("#sols-postCommentAnyway").prop("checked")) { // Post comment let author = owner.find('div.user-details a')[0].innerText; let comment = window.location.host === "softwarerecs.stackexchange.com" ? ("Hi " + author + ", welcome to [softwarerecs.se]! " + "This question does not appear to be about software recommendations, within [the scope defined on meta](https://softwarerecs.meta.stackexchange.com/questions/tagged/scope) and in the [help center](/help/on-topic). " + "If you think you can [edit] it to become on-topic, please have a look at the [question quality guidelines](https://softwarerecs.meta.stackexchange.com/q/336/23377).") : window.location.host === "hardwarerecs.stackexchange.com" ? ("Hi " + author + ", welcome to [hardwarerecs.se]! " + "This question does not appear to be about hardware recommendations, within [the scope defined on meta](https://hardwarerecs.meta.stackexchange.com/questions/tagged/scope) and in the [help center](/help/on-topic)." + "If you think you can [edit] it to become on-topic, please have a look at the [question quality guidelines](https://hardwarerecs.meta.stackexchange.com/q/205/4495).") : ("Hi " + author + ", welcome to Meta! " + "I'm not sure which search brought you here but the problem you describe will not be answered on this specific site. " + "To get an answer from users that have the expertise about the topic of your question you'll have to find and then re-post on the [proper site](https://stackexchange.com/sites). " + "Check [How do I ask a good question](/help/how-to-ask) and [What is on topic](/help/on-topic) on the *target* site to make sure your post is in good shape. " + "Your question is definitely off-topic on [Meta](/help/whats-meta) and is better deleted here."); $.post({ url: "https://" + document.location.host + "/posts/" + postID + "/comments", data: "fkey=" + fkey + "&comment=" + encodeURI(comment), success: function () { console.log("Comment posted."); }, error: function (jqXHR, textStatus, errorThrown) { window.alert("An error occurred, please try again later."); console.log("Error: " + textStatus + " " + errorThrown); } }); } if (hasUpvotePrivilege) { // Upvote all comments containing "welcome to" comments.find("li").each(function() { if ($(this).find("span.comment-copy")[0].innerText.toLowerCase().indexOf("welcome to") >= 0) { // Click the "up" triangle let upButtons = $(this).find("a.comment-up"); if (upButtons.length > 0) { upButtons[0].click(); } } }); } if (shouldDownvote) { // Downvote (not when the post is already on -3 or lower, to be slightly more welcoming) $.post({ url: "https://" + document.location.host + "/posts/" + postID + "/vote/3", // 3 = downvote data: "fkey=" + fkey, success: function () { // NICETOHAVE: set downvote button color console.log("Downvote cast."); }, error: function (jqXHR, textStatus, errorThrown) { window.alert("An error occurred, please try again later."); console.log("Error: " + textStatus + " " + errorThrown); } }); } if (shouldFlag || shouldVoteToClose) { // Flag/vote to close (which one doesn't matter for the API call) $.post({ url: "https://" + document.location.host + "/flags/questions/" + postID + "/close/add", data: "fkey=" + fkey + "&closeReasonId=OffTopic&closeAsOffTopicReasonId=" + (window.location.host === "softwarerecs.stackexchange.com" || window.location.host === "hardwarerecs.stackexchange.com" ? "1" : "8"), success: function () { // NICETOHAVE: update close vote count console.log("Close flag/vote cast."); }, error: function (jqXHR, textStatus, errorThrown) { window.alert("An error occurred, please try again later."); console.log("Error: " + textStatus + " " + errorThrown); } }); } if (shouldVoteToDelete) { // Delete vote // NICETOHAVE: maybe also if myReputation >= 10000 and question age >= 48 hours $.post({ url: "https://" + document.location.host + "/posts/" + postID + "/vote/10", // 10 = delete data: "fkey=" + fkey, success: function () { // NICETOHAVE: update delete vote count console.log("Delete vote cast."); }, error: function (jqXHR, textStatus, errorThrown) { window.alert("An error occurred, please try again later."); console.log("Error: " + textStatus + " " + errorThrown); } }); } // Reload page; this is less elegant than waiting for all POST calls, but it works. window.setTimeout(() => window.location.reload(false), 1000); }; }