// ==UserScript==
// @name Flag Dialog Smokey Controls
// @namespace https://github.com/Charcoal-SE/
// @description Adds Smokey status of a post and feedback options to flag dialogs.
// @author ArtOfCode
// @contributor angussidney
// @contributor rene
// @contributor J F
// @contributor Glorfindel
// @attribution Brock Adams (https://github.com/BrockA)
// @version 1.19.1
// @updateURL https://raw.githubusercontent.com/Charcoal-SE/Userscripts/master/fdsc/fdsc.meta.js
// @downloadURL https://raw.githubusercontent.com/Charcoal-SE/Userscripts/master/fdsc/fdsc.user.js
// @supportURL https://github.com/Charcoal-SE/Userscripts/issues
// @match *://*.stackexchange.com/*
// @match *://*.stackoverflow.com/*
// @match *://*.superuser.com/*
// @match *://*.serverfault.com/*
// @match *://*.askubuntu.com/*
// @match *://*.stackapps.com/*
// @match *://*.mathoverflow.net/*
// @exclude *://chat.stackexchange.com/*
// @exclude *://chat.meta.stackexchange.com/*
// @exclude *://chat.stackoverflow.com/*
// @exclude *://blog.stackoverflow.com/*
// @exclude *://*.area51.stackexchange.com/*
// @exclude *://stackexchange.com/*
// @exclude *://api.stackexchange.com/*
// @exclude *://data.stackexchange.com/*
// @exclude *://winterbash*.stackexchange.com/*
// @require https://cdn.rawgit.com/ofirdagan/cross-domain-local-storage/d779a81a6383475a1bf88595a98b10a8bd5bb4ae/dist/scripts/xdLocalStorage.min.js
// @require https://charcoal-se.org/userscripts/vendor/debug.min.js
// @grant none
// ==/UserScript==
/* global fdsc, $, xdLocalStorage, confirm, unsafeWindow */
/* eslint-disable max-nested-callbacks */
(function () {
"use strict";
const createDebug = typeof unsafeWindow === "undefined" ? window.debug : unsafeWindow.debug || window.debug;
const debug = createDebug("fdsc");
var userscript = function ($) {
window.fdsc = {};
fdsc.metasmokeKey = "070f26ebb71c5e6cfca7893fe1139460cf23f30d686566f5707a4acfd50c";
fdsc.ready = false;
/*!
* Given a DOM element containing the post in question, will construct the URL to that post in the form
* required by metasmoke. For questions and answers, respectively:
*
* //stackoverflow.com/questions/12345
* //stackoverflow.com/a/12345
*
*/
fdsc.constructUrl = function (postContainer) {
var base = "//" + location.host + "/";
if ($(postContainer).hasClass("answer")) {
return base + "a/" + $(postContainer).data("answerid");
} else if ($(postContainer).hasClass("question")) {
return base + "questions/" + $(postContainer).data("questionid");
}
return "";
};
/*!
* Given a blurb and a callback method, will prompt the user for input using an SE native prompt and the
* text of the blurb. The callback will be invoked once the input is submitted, and the first parameter
* will contain the submitted data.
*/
fdsc.input = function (blurb, callback) {
function loaded() {
$("#fdsc-popup-submit").on("click", function () {
StackExchange.helpers.closePopups("#fdsc-popup-prompt");
callback($("#fdsc-popup-input").val());
$("#fdsc-popup-submit").off("click");
});
}
$("body").loadPopup({
lightbox: false,
target: $("body"),
html: "
",
loaded: loaded
});
};
fdsc.confirm = function (blurb, callback) {
function loaded() {
$("#fdsc-popup-ok").on("click", function () {
StackExchange.helpers.closePopups("#fdsc-popup-confirm");
callback(true);
$("#fdsc-popup-ok").off("click");
});
$("#fdsc-popup-cnl").on("click", function () {
StackExchange.helpers.closePopups("#fdsc-popup-confirm");
callback(false);
$("#fdsc-popup-cnl").off("click");
});
}
$("body").loadPopup({
lightbox: false,
target: $("body"),
html: "",
loaded: loaded
});
};
/*!
* The token that allows us to perform write operations using the metasmoke API. Obtained via MicrOAuth.
* `localStorage` call is left in for backwards compatibility. It's overwritten later.
*/
fdsc.msWriteToken = localStorage.getItem("fdsc_msWriteToken");
/*!
* Obtains a write token and stores it both in `fdsc.msWriteToken` and `xdLocalStorage['fdsc_msWriteToken']`.
* _May_ cause problems with popup blockers, because the window opening isn't triggered by a click... we'll
* have to see how much of a problem that is.
*/
fdsc.getWriteToken = function (afterFlag, callback) {
debug("entering getWriteToken");
window.open("https://metasmoke.erwaysoftware.com/oauth/request?key=" + fdsc.metasmokeKey, "_blank");
function getInput() {
fdsc.input("Once you've authenticated FDSC with metasmoke, you'll be given a code; enter it here.", function (code) {
debug("input callback: " + code);
$.ajax({
url: "https://metasmoke.erwaysoftware.com/oauth/token?key=" + fdsc.metasmokeKey + "&code=" + code,
method: "GET"
}).done(function (data) {
fdsc.msWriteToken = data.token;
xdLocalStorage.setItem("fdsc_msWriteToken", data.token, function () {
callback();
});
}).error(function (jqXHR) {
if (jqXHR.status === 404) {
StackExchange.helpers.showErrorMessage($(".topbar"), "metasmoke could not find a write token - did you authorize the app?", {
position: "toast",
transient: true,
transientTimeout: 10000
});
} else {
StackExchange.helpers.showErrorMessage($(".topbar"), "An unknown error occurred during OAuth with metasmoke.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
debug("getting write token failed", jqXHR);
}
});
});
}
if (afterFlag) {
$(document).on("DOMNodeRemoved", function (ev) {
if ($(ev.target).attr("id") === "popup-flag-post") {
getInput();
$(document).off("DOMNodeRemoved");
}
});
} else {
getInput();
}
};
/*!
* Given a Smokey-recognized feedback type, sends that feedback to metasmoke via the API. Requires a valid
* API key and write token; if you don't have these before this is called, get hold of them. A write token
* can be obtained using `fdsc.getWriteToken()`.
*/
fdsc.sendFeedback = function (feedbackType, postId) {
debug("entering sendFeedback");
debug("write token:", fdsc.msWriteToken);
var token;
if (typeof (fdsc.msWriteToken) === "object") {
token = fdsc.msWriteToken.value;
} else {
token = fdsc.msWriteToken;
}
$.ajax({
type: "POST",
url: "https://metasmoke.erwaysoftware.com/api/v2.0/feedbacks/post/" + postId + "/create",
data: {
type: feedbackType,
key: fdsc.metasmokeKey,
token: token
}
}).done(function (data) {
StackExchange.helpers.showSuccessMessage($(".topbar"), "Fed back " + feedbackType + " to metasmoke.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
debug("feedback sent:", data);
$(window.event.target).attr("data-fdsc-ms-id", null);
fdsc.postFound = null;
}).error(function (jqXHR) {
if (jqXHR.status === 401) {
StackExchange.helpers.showErrorMessage($(".topbar"), "Can't send feedback to metasmoke - not authenticated.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
console.error("fdsc.sendFeedback was called without having a valid write token");
fdsc.confirm("Write token invalid. Attempt re-authentication?", function (result) {
if (result) {
fdsc.getWriteToken(false, function () {
fdsc.sendFeedback(feedbackType, postId);
});
}
});
} else {
StackExchange.helpers.showErrorMessage($(".topbar"), "An error occurred sending post feedback to metasmoke.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
debug("feedback failed:", jqXHR);
}
$(window.event.target).attr("data-fdsc-ms-id", null);
fdsc.postFound = null;
});
};
/*!
* Given the URL to a post not yet reported by Smokey, reports that post via the metasmoke API. Requires a valid
* API key and write token; if you don't have these before this is called, get hold of them. A write token
* can be obtained using `fdsc.getWriteToken()`.
*/
fdsc.reportPost = function (postUrl) {
if (StackExchange.options.user.isModerator) {
return;
}
debug("entering reportPost");
debug("write token:", fdsc.msWriteToken);
var token;
if (typeof (fdsc.msWriteToken) === "object") {
token = fdsc.msWriteToken.value;
} else {
token = fdsc.msWriteToken;
}
$.ajax({
type: "POST",
url: "https://metasmoke.erwaysoftware.com/api/v2.0/posts/report",
data: {
post_link: postUrl, // eslint-disable-line camelcase
key: fdsc.metasmokeKey,
token: token
}
}).done(function (data) {
StackExchange.helpers.showSuccessMessage($(".topbar"), "Reported post to metasmoke.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
debug("post reported", data);
}).error(function (jqXHR) {
if (jqXHR.status === 401) {
StackExchange.helpers.showErrorMessage($(".topbar"), "Can't report post to metasmoke - not authenticated.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
console.error("fdsc.reportPost was called without having a valid write token");
fdsc.confirm("Write token invalid. Attempt re-authentication?", function (result) {
if (result) {
fdsc.getWriteToken(false, function () {
fdsc.reportPost(postUrl);
});
}
});
} else {
StackExchange.helpers.showErrorMessage($(".topbar"), "An error occurred while reporting the post to metasmoke.", {
position: "toast",
transient: true,
transientTimeout: 10000
});
debug("report failed:", jqXHR);
}
});
};
/*!
* Well this is a mess.
*/
xdLocalStorage.init({
iframeUrl: "https://metasmoke.erwaysoftware.com/xdom_storage.html",
initCallback: function () {
xdLocalStorage.getItem("fdsc_msWriteToken", function (data) {
fdsc.msWriteToken = data.value;
debug("write token", data.value);
});
$(".js-flag-post-link").on("click", function (clickEvent) {
$(document).on("DOMNodeInserted", function (nodeEvent) {
var container;
if ($(nodeEvent.target).hasClass("popup") && $(nodeEvent.target).attr("id") === "popup-flag-post") {
container = $(clickEvent.target).parents(".question, .answer").first();
fdsc.ajaxPromise = $.ajax({
type: "GET",
url: "https://metasmoke.erwaysoftware.com/api/v2.0/posts/urls",
data: {
urls: fdsc.constructUrl(container),
filter: "HFHNHJFMGNKNFFFIGGOJLNNOFGNMILLJ",
key: fdsc.metasmokeKey
}
}).done(function (data_) {
data_ = data_.items;
function registerFeedbackButton(buttonSelector, feedback, logMessage) {
$(buttonSelector).on("click", function (ev) {
debug("feedback clicked:", logMessage);
ev.preventDefault();
if (!fdsc.msWriteToken || fdsc.msWriteToken === "null") {
fdsc.getWriteToken(true, function () {
fdsc.sendFeedback(feedback, $(nodeEvent.target).attr("data-fdsc-ms-id"));
});
} else {
fdsc.sendFeedback(feedback, $(nodeEvent.target).attr("data-fdsc-ms-id"));
}
StackExchange.helpers.closePopups("#popup-flag-post");
$(buttonSelector).off("click");
});
}
if (data_.length > 0 && data_[0].id) {
$(nodeEvent.target).attr("data-fdsc-ms-id", data_[0].id);
fdsc.postFound = true;
var isAutoflagged = data_[0].autoflagged.flagged === true;
var isFlagged = $(".popup .already-flagged").length > 0;
if (isAutoflagged) {
fdsc.autoflagged = "autoflagged";
} else {
fdsc.autoflagged = "not autoflagged";
}
// Retrieve feedback
$.get("https://metasmoke.erwaysoftware.com/api/v2.0/feedbacks/post/" + data_[0].id +
"?key=" + fdsc.metasmokeKey, function (data) {
console.log(data);
// Determine # of feedbacks for each type
var tps = 0;
var fps = 0;
var naa = 0;
for (var i = 0; i < data.items.length; i++) {
switch (data.items[0].feedback_type.charAt(0)) {
case "t":
tps++;
break;
case "f":
fps++;
break;
case "n":
naa++;
break;
default:
break;
}
}
var fpButtonStyle = "style='color:rgba(255,0,0,0.5);' onMouseOver='this.style.color=\"rgba(255,0,0,1)\"' onMouseOut='this.style.color=\"rgba(255,0,0,0.5)\"'";
var tpButtonStyle = "style='color:rgba(0,100,0,0.5);' onMouseOver='this.style.color=\"rgba(0,100,0,1)\"' onMouseOut='this.style.color=\"rgba(0,100,0,0.5)\"'";
var isAnswer = $(".popup-actions").parents(".answer").length !== 0;
// Build status
var status = "Smokey report: ";
status += "" + tps + " tp, ";
status += "" + fps + " fp, ";
// Don't add naa if the dialog opened for a question
if (isAnswer) {
status += "" + naa + " naa, ";
}
status += fdsc.autoflagged + "";
var writeTokenButton = false;
if (!fdsc.msWriteToken || fdsc.msWriteToken === "null") {
status += " -
get write token ";
writeTokenButton = true;
}
if (isFlagged || isAutoflagged) {
status += " - tpu-";
}
if (tps === 0) {
status += " - false positive?";
} else {
// If someone else has already marked as tp, you should mark it as fp in chat where you can discuss with others.
// Hence, do not display the false positive button
status += "";
}
$(".popup-actions").prepend(status);
// On click of the false positive button
registerFeedbackButton("#feedback-fp", "fp-", "Reporting as false positive");
// On click of the confirm autoflag
registerFeedbackButton("#autoflag-tp", "tpu-", "Reporting as true positive");
if (writeTokenButton) {
$("#get-write-token").on("click", function (ev) {
ev.preventDefault();
fdsc.getWriteToken(false, function () {
debug("click event", clickEvent);
$(".popup-close a").click();
$(clickEvent.currentTarget).click();
}); // Add a "Get write token" link.
});
}
});
} else {
fdsc.postFound = false;
}
}).error(function (jqXHR) {
StackExchange.helpers.showMessage($(".topbar"), "An error occurred fetching post from metasmoke - has the post been reported by Smokey?", {
position: "toast",
transient: true,
transientTimeout: 10000,
type: "warning"
});
console.error(jqXHR.status, jqXHR.responseText);
});
// We should remove the DOMNodeInserted handler when we're done with it to avoid multiple fires of
// the same handler caused by re-adding it each time you click the flag link.
$(document).off("DOMNodeInserted");
}
$(".js-popup-submit").on("click", function () {
var selected = $("input[name=top-form]").filter(":checked");
var feedbackType;
if (selected.val() === "PostSpam" || selected.val() === "PostOffensive") {
feedbackType = "tpu-";
} else if (selected.val() === "AnswerNotAnAnswer") {
feedbackType = "naa-";
}
fdsc.ajaxPromise.then(function () {
if (feedbackType && $(nodeEvent.target).attr("data-fdsc-ms-id")) {
// because it looks like xdls returns null as a string for some reason
if (!fdsc.msWriteToken || fdsc.msWriteToken === "null") {
fdsc.getWriteToken(true, function () {
fdsc.sendFeedback(feedbackType, $(nodeEvent.target).attr("data-fdsc-ms-id"));
});
} else {
fdsc.sendFeedback(feedbackType, $(nodeEvent.target).attr("data-fdsc-ms-id"));
}
} else if (feedbackType === "tpu-" && fdsc.postFound === false) {
if (!fdsc.msWriteToken || fdsc.msWriteToken === "null") {
fdsc.getWriteToken(true, function () {
fdsc.reportPost(fdsc.constructUrl(fdsc.container)); // container variable defined on line 299
});
} else {
fdsc.reportPost(fdsc.constructUrl(fdsc.container));
}
}
});
// Likewise, remove this handler when it's finished to avoid multiple fires.
$(".js-popup-submit").off("click");
});
});
});
$(".popup-close").on("click", function () {
fdsc.postFound = null;
});
$(document).trigger("fdsc:ready");
fdsc.ready = true;
}
});
};
/*!
* This is here because since we're injecting the userscript into the page, we also need to inject
* any libraries we need.
*/
var sourceEl = document.createElement("script");
sourceEl.type = "application/javascript";
sourceEl.id = "fdsc-script";
sourceEl.onload = function () {
var el = document.createElement("script");
el.type = "application/javascript";
el.text = "(" + userscript + ")(jQuery);";
document.body.appendChild(el);
};
sourceEl.src = "https://cdn.rawgit.com/ofirdagan/cross-domain-local-storage/d779a81a6383475a1bf88595a98b10a8bd5bb4ae/dist/scripts/xdLocalStorage.min.js";
document.body.appendChild(sourceEl);
})();