2018-11-16 19:12:22 +00:00
// ==UserScript==
// @name 'Saviour' of Lost Souls
// @namespace https://github.com/Glorfindel83/
2019-05-02 18:05:16 +00:00
// @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.
2018-11-16 19:12:22 +00:00
// @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
2019-10-09 10:18:06 +00:00
// @version 1.1
2018-11-16 19:12:22 +00:00
// @match *://meta.stackexchange.com/questions/*
2019-01-06 17:23:15 +00:00
// @match *://meta.stackoverflow.com/questions/*
2019-04-01 13:52:59 +00:00
// @match *://softwarerecs.stackexchange.com/questions/*
2019-04-11 07:05:23 +00:00
// @match *://softwarerecs.stackexchange.com/review/first-posts*
2019-04-10 21:00:18 +00:00
// @match *://hardwarerecs.stackexchange.com/questions/*
2019-04-11 07:05:23 +00:00
// @match *://hardwarerecs.stackexchange.com/review/first-posts*
2018-12-27 08:20:02 +00:00
// @exclude *://meta.stackexchange.com/questions/ask
2019-01-06 17:23:15 +00:00
// @exclude *://meta.stackoverflow.com/questions/ask
2019-04-01 13:52:59 +00:00
// @exclude *://softwarerecs.stackexchange.com/questions/ask
2019-04-10 21:00:18 +00:00
// @exclude *://hardwarerecs.stackexchange.com/questions/ask
2018-11-16 19:12:22 +00:00
// @grant none
2019-04-10 21:00:18 +00:00
// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
2018-11-16 19:12:22 +00:00
// ==/UserScript==
2019-04-10 21:00:18 +00:00
/* global $, waitForKeyElements */
2018-11-16 19:12:22 +00:00
2019-01-15 04:23:05 +00:00
( function ( $ ) {
2018-11-16 19:12:22 +00:00
"use strict" ;
2019-04-10 21:00:18 +00:00
// Find question (works when on Q&A page)
2019-01-06 17:23:15 +00:00
let question = $ ( '#question' ) ;
2019-04-10 21:00:18 +00:00
if ( question . length == 0 )
return ;
main ( question ) ;
} ) ( window . jQuery ) ;
2019-01-15 04:23:05 +00:00
2019-04-10 21:00:18 +00:00
// Wait for question (works when in review queue)
waitForKeyElements ( 'div.review-content div.question' , function ( jNode ) {
main ( jNode ) ;
} ) ;
function main ( question ) {
2019-01-06 17:23:15 +00:00
// Check if author is likely to be a lost soul
2019-04-10 21:00:18 +00:00
let owner = question . find ( 'div.post-signature.owner' ) ;
2019-01-06 17:27:20 +00:00
if ( owner . length == 0 )
2019-01-06 17:23:15 +00:00
// happens with Community Wiki posts
2018-11-16 19:12:22 +00:00
return ;
2019-01-06 17:23:15 +00:00
let reputation = owner . find ( 'span.reputation-score' ) [ 0 ] . innerText ;
2019-01-15 04:23:05 +00:00
if ( reputation === "1" ) {
// Do nothing: 1 rep qualifies for a lost soul
2019-01-06 17:23:15 +00:00
} else {
2019-01-15 18:23:27 +00:00
// Child meta sites require some reputation to post a question, so we need other rules:
2019-01-06 17:23:15 +00:00
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 ;
2018-11-16 19:12:22 +00:00
}
2019-10-09 06:49:33 +00:00
// Which site?
let isMetaSE = location . host == 'meta.stackexchange.com' ;
let isBeta = location . host == 'hardwarerecs.stackexchange.com' ;
2018-11-16 19:12:22 +00:00
2019-10-09 06:49:33 +00:00
// My reputation
2018-11-16 19:12:22 +00:00
let myReputation = parseInt ( $ ( 'a.my-profile div.-rep' ) [ 0 ] . innerText . replace ( /,/g , '' ) ) ;
2019-10-09 06:49:33 +00:00
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 ) ;
2018-11-25 12:08:31 +00:00
let isModerator = $ ( "a.js-mod-inbox-button" ) . length > 0 ;
2019-10-09 06:49:33 +00:00
// 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 ;
2019-01-15 04:23:05 +00:00
2018-11-16 19:12:22 +00:00
// Add post menu button
let menu = question . find ( 'div.post-menu' ) ;
menu . append ( $ ( '<span class="lsep">|</span>' ) ) ;
let button = $ ( '<a href="#" title="down-/close-/delete vote and post a welcoming comment">lost soul</a>' ) ;
menu . append ( button ) ;
button . click ( function ( ) {
2019-10-09 06:49:33 +00:00
// Generate HTML for dialog
var html = `
< aside class = "s-modal bg-transparent pe-none js-stacks-managed-popup js-fades-with-aria-hidden" id = "modal-base" tabindex = "-1" role = "dialog" aria - labelledby = "mod-modal-title" aria - describedby = "mod-modal-description" aria - hidden = "false" >
< form class = "s-modal--dialog js-modal-dialog js-keyboard-navigable-modal pe-auto" role = "document" data - controller = "se-draggable se-mod-menu" data - se - mod - menu - model - type = "post" data - se - mod - menu - model - id = "371841" method = "get" action = "#" data - action = "se-mod-menu#submit" data - keyboard - actions = "#mod-screen-select-menu, input[name=action], .js-info-link" >
< h1 class = "s-modal--header fs-headline1 fw-bold mr48 c-move js-first-tabbable" id = "modal-title" tabindex = "0" data - target = "se-draggable.handle" >
'Saviour' of Lost Souls
< / h 1 > ` ;
html += `
< div class = "grid--cell" >
< span class = "js-short-label" > Please confirm you want to < / s p a n >
< ul > ` ;
// List actions that will be taken
if ( shouldComment ) {
html += `
< li > leave a welcoming comment < / l i > ` ;
}
if ( shouldDownvote ) {
html += `
< li > downvote the question < / l i > ` ;
}
if ( shouldFlag ) {
html += `
< li > flag the question as off - topic < / l i > ` ;
}
if ( shouldVoteToClose ) {
html += `
< li > vote to close the question as off - topic < / l i > ` ;
}
if ( shouldVoteToDelete ) {
html += `
< li > vote to delete the question < / l i > ` ;
}
html += `
< / u l >
< / d i v >
< br / > ` ;
if ( hasCommentPrivilege && hasNonOwnerComment ) {
// Add option to post a comment anyway
html += `
< div class = "grid--cell" >
< span > Another user already posted a comment . < / s p a n >
< fieldset class = "mt4 ml4 mb16" >
< div class = "grid gs8 gsx ff-row-nowrap" >
< div class = "grid--cell" >
< input class = "s-checkbox" type = "checkbox" name = "postCommentAnyway" id = "sols-postCommentAnyway" >
< / d i v >
< label class = "grid--cell s-label fw-normal" for = "sols-postCommentAnyway" >
Post a welcoming comment anyway
< / l a b e l >
< / d i v >
< / f i e l d s e t >
< / d i v > ` ;
}
html += `
< div class = "grid gs8 gsx s-modal--footer" >
< button class = "grid--cell s-btn s-btn__primary" type = "submit" onclick = "saviourOfLostSouls.submitDialog();" > Confirm < / b u t t o n >
< button class = "grid--cell s-btn js-modal-close" type = "button" onclick = "saviourOfLostSouls.closeDialog();" > Cancel < / b u t t o n >
< / d i v >
< button class = "s-modal--close s-btn s-btn__muted js-modal-close js-last-tabbable" type = "button" aria - label = "Close" onclick = "saviourOfLostSouls.closeDialog();" >
< svg aria - hidden = "true" class = "svg-icon m0 iconClearSm" width = "14" height = "14" viewBox = "0 0 14 14" > < path d = "M12 3.41L10.59 2 7 5.59 3.41 2 2 3.41 5.59 7 2 10.59 3.41 12 7 8.41 10.59 12 12 10.59 8.41 7 12 3.41z" > < / p a t h > < / s v g >
< / b u t t o n >
< / f o r m >
< / a s i d e > ` ;
$ ( document . body ) . append ( $ ( html ) ) ;
} ) ;
// Define functions which can be called by the dialog
window . saviourOfLostSouls = { } ;
saviourOfLostSouls . closeDialog = function ( ) {
$ ( "#modal-base" ) . remove ( ) ;
} ;
saviourOfLostSouls . submitDialog = function ( ) {
2018-11-16 19:12:22 +00:00
// Prepare votes/comments
let postID = parseInt ( question . attr ( 'data-questionid' ) ) ;
let fkey = window . localStorage [ "se:fkey" ] . split ( "," ) [ 0 ] ;
2019-01-15 04:23:05 +00:00
2019-10-09 06:49:33 +00:00
if ( shouldComment || $ ( "#sols-postCommentAnyway" ) . prop ( "checked" ) ) {
2018-11-16 19:12:22 +00:00
// Post comment
let author = owner . find ( 'div.user-details a' ) [ 0 ] . innerText ;
2019-10-09 06:49:33 +00:00
2019-04-01 13:52:59 +00:00
let comment = window . location . host === "softwarerecs.stackexchange.com"
? ( "Hi " + author + ", welcome to [softwarerecs.se]! " +
2019-05-02 18:05:16 +00:00
"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)." )
2019-04-10 21:00:18 +00:00
: window . location . host === "hardwarerecs.stackexchange.com"
? ( "Hi " + author + ", welcome to [hardwarerecs.se]! " +
2019-05-02 18:05:16 +00:00
"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)." )
2019-04-01 13:52:59 +00:00
: ( "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." ) ;
2018-11-16 19:12:22 +00:00
$ . 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 ) ;
}
} ) ;
}
2019-01-15 11:11:08 +00:00
2019-10-09 06:49:33 +00:00
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 ( ) ;
}
2019-01-15 11:11:08 +00:00
}
2019-10-09 06:49:33 +00:00
} ) ;
}
2018-11-16 19:12:22 +00:00
2019-10-09 06:49:33 +00:00
if ( shouldDownvote ) {
// Downvote (not when the post is already on -3 or lower, to be slightly more welcoming)
2018-11-16 19:12:22 +00:00
$ . post ( {
url : "https://" + document . location . host + "/posts/" + postID + "/vote/3" , // 3 = downvote
data : "fkey=" + fkey ,
success : function ( ) {
2019-10-09 06:49:33 +00:00
// NICETOHAVE: set downvote button color
2018-11-16 19:12:22 +00:00
console . log ( "Downvote cast." ) ;
} ,
error : function ( jqXHR , textStatus , errorThrown ) {
window . alert ( "An error occurred, please try again later." ) ;
console . log ( "Error: " + textStatus + " " + errorThrown ) ;
}
} ) ;
}
2019-01-15 04:23:05 +00:00
2019-10-09 10:18:06 +00:00
if ( shouldFlag || shouldVoteToClose ) {
2019-10-09 06:49:33 +00:00
// Flag/vote to close (which one doesn't matter for the API call)
2018-11-16 19:12:22 +00:00
$ . post ( {
url : "https://" + document . location . host + "/flags/questions/" + postID + "/close/add" ,
2019-04-10 21:00:18 +00:00
data : "fkey=" + fkey + "&closeReasonId=OffTopic&closeAsOffTopicReasonId=" + ( window . location . host === "softwarerecs.stackexchange.com" ||
window . location . host === "hardwarerecs.stackexchange.com" ? "1" : "8" ) ,
2018-11-16 19:12:22 +00:00
success : function ( ) {
2019-10-09 06:49:33 +00:00
// NICETOHAVE: update close vote count
2018-11-16 19:12:22 +00:00
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 ) ;
}
} ) ;
2019-10-09 10:18:06 +00:00
}
if ( shouldVoteToDelete ) {
2018-11-16 19:12:22 +00:00
// Delete vote
2019-10-09 06:49:33 +00:00
// NICETOHAVE: maybe also if myReputation >= 10000 and question age >= 48 hours
2018-11-16 19:12:22 +00:00
$ . post ( {
url : "https://" + document . location . host + "/posts/" + postID + "/vote/10" , // 10 = delete
data : "fkey=" + fkey ,
success : function ( ) {
2019-10-09 06:49:33 +00:00
// NICETOHAVE: update delete vote count
2018-11-16 19:12:22 +00:00
console . log ( "Delete vote cast." ) ;
} ,
error : function ( jqXHR , textStatus , errorThrown ) {
window . alert ( "An error occurred, please try again later." ) ;
console . log ( "Error: " + textStatus + " " + errorThrown ) ;
}
} ) ;
}
2019-01-15 04:23:05 +00:00
2019-10-09 06:49:33 +00:00
// Reload page; this is less elegant than waiting for all POST calls, but it works.
window . setTimeout ( ( ) => window . location . reload ( false ) , 1000 ) ;
} ;
2019-04-10 21:00:18 +00:00
}