// ==UserScript== // @name Stack Exchange Flag Tracker // @namespace https://so.floern.com/ // @version 1.2.1 // @description Tracks flagged posts on Stack Exchange. // @author Floern // @contributor double-beep // @include /^https?:\/\/(?:[^/.]+\.)*(?:stackexchange\.com|stackoverflow\.com|serverfault\.com|superuser\.com|askubuntu\.com|stackapps\.com|mathoverflow\.net)\/(?:q(?:uestions)?)/ // @exclude *://chat.stackoverflow.com/* // @exclude *://chat.stackexchange.com/* // @exclude *://chat.*.stackexchange.com/* // @exclude *://api.*.stackexchange.com/* // @exclude *://data.stackexchange.com/* // @connect so.floern.com // @grant GM_xmlhttpRequest // @run-at document-end // @updateURL https://github.com/SOBotics/Userscripts/raw/master/GenericBot/flagtracker.user.js // @downloadURL https://github.com/SOBotics/Userscripts/raw/master/GenericBot/flagtracker.user.js // ==/UserScript== /* globals StackExchange */ (function() { if (!StackExchange.options.user.isRegistered) return; // user is not logged in const key = 'Cm45BSrt51FR3ju'; const myProfileElement = document.querySelector('.my-profile .gravatar-wrapper-24'); const flaggername = myProfileElement ? myProfileElement.title : null; const sitename = window.location.hostname; const flagTrackerButtonHtml = '
' + ' ' + '
'; function computeContentHash(postContent) { if (!postContent) return 0; var hash = 0; for (var i = 0; i < postContent.length; ++i) { hash = ((hash << 5) - hash) + postContent.charCodeAt(i); hash = hash & hash; } return hash; } function addXHRListener(callback) { let open = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function() { this.addEventListener('load', callback.bind(null, this), false); open.apply(this, arguments); }; } function sendTrackRequest(postId, contentHash, flagTrackerButtonElement) { if (!flaggername || !postId || !contentHash) return; // one of these doesn't exist for whatever reason; return GM_xmlhttpRequest({ method: 'POST', url: 'https://so.floern.com/api/trackpost.php', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: 'key=' + key + '&postId=' + postId + '&site=' + sitename + '&contentHash=' + contentHash + '&flagger=' + encodeURIComponent(flaggername), onload: function (response) { if (response.status !== 200) { StackExchange.helpers.showToast('Flag Tracker Error: Status ' + response.status + '. See console for details', { type: 'danger' }); console.error(response.responseText); return; } flagTrackerButtonElement.classList.add('flag-tracked'); flagTrackerButtonElement.innerHTML = 'Track ✓'; }, onerror: function (response) { StackExchange.helpers.showToast('Flag Tracker Error. See console for details.', { type: 'danger' }); console.error(response.responseText); } }); } function trackFlag(element) { const postId = element.querySelector('.js-share-link').href.split('/')[4]; const postContent = element.closest('.post-layout--right').querySelector('.s-prose').innerHTML.trim(); const contentHash = computeContentHash(postContent); sendTrackRequest(postId, contentHash, element.querySelector('.flag-tracker-link')); } function handlePosts() { [...document.querySelectorAll('.post-layout .js-post-menu')].forEach(element => { if (element.innerText.match('track')) return; // element already exists element.children[0].insertAdjacentHTML('beforeend', flagTrackerButtonHtml); element.children[0].querySelector('.flag-tracker-link').addEventListener('click', () => trackFlag(element)); }); } addXHRListener(xhr => { if (/ajax-load-realtime/.test(xhr.responseURL)) handlePosts(); }); addXHRListener(function(xhr) { let matches = /flags\/posts\/(\d+)\/add\//.exec(xhr.responseURL); if (matches !== null && xhr.status === 200) { const postId = matches[1]; const postIsQuestion = document.querySelector('.question').getAttribute('data-questionid') == postId; const element = postIsQuestion ? document.querySelector('.question .js-post-menu') : document.querySelector(`#answer-${postId} .js-post-menu`); trackFlag(element); } }); handlePosts(); })();