This script is a userscript designed for giving a notification when the bot SmokeDetector posts a message in the Charcoal HQ or any other room were he is active.
The script has 3 modes of operating, which can be selected at the bottom of the screen
chat only (default)
It only gives a notification when a spam related message is posted in chat, the definition of this is when it matches the pattern
/spam|\/\/git.io\/vgx7b|offensive|abusive/i
and contains a URL to a Stack Exchange site (line 328).The notifications are made using a sound notification (triggered by calling
play()
on a html 5 audio element and a request using the desktop notification api.on
In addition to the above code, it also opens a websocket connection to the Stack Exchange realtime feed and listens for new posts coming from this location, it uses multiple spam patterns defined at the top of the code for this detection (and checks them at line 186), including the url patterns from the Smoke Detector bot itself.
When a post is detected, it is reported in the same way, and added to a list that is placed in the place of the rooms description, for a quick overview of the reported spam. This list can be quickly cleared using the "clear" button, by opening the post in question, or clicking the background to dismiss it directly.
off
The userscript is turned off
This userscript also needs some CSS rules. These rules are injected at the start of the page, and they are used to style the place where the notifications appear in the "on" mode of the script.
This script is created as a port of the fork of the orginal code for Spam Tracker, because the orginal code was designed to be a Google Chrome extension, but it was removed by the orginal author for unknown reasons.
// ==UserScript==
// @name Spamtracker
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Alerts you when new smokedetector messages show up
// @author NormalHuman
// @author Ferrybig
// @match *://chat.meta.stackexchange.com/*
// @match *://chat.stackexchange.com/*
// @match *://chat.stackoverflow.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
var sumRules = [/^\S*$/i, /\bcolon.*clean/i, /cleans/i, /\b(phone|support).*number\b/i, /\bwow\sgold\b/i, /\bessays?\b/i, /\bbaba\s?ji\b/i,
/\+91[\s\-\(]/i, /professional.*writ/i, /kickstarter/i, /natural.*ingredient/i, /\baffiliate\b/i, /\baging\b/i, /\bfifa\b/i, /\bbajotz\b/i,
/\bbagprada\b/i, /\bbabyliss/i, /\bblack magic\b/i, /vashikaran/i, /advantage.*price/i, /natural(ly)?\b/i, /pure\sbody/i, /fuck/i, /\bshit/i, /bitch/i, /\bsuck/i,
/brain.*(boost|power)/i, /facts?\sabout/i, /\b100%\b/i, /live\sstream/i, /make\smoney/i, /sale/i, /\bhack/i, /cheat/i, /\bwow\sgold\b/i, /runescape/i,
/\bfifa.*coin/i, /\bcheap/i, /\bskin/i, /\bweight\b/i, /\bacne\b/i, /\bage\b/i, /\bbody.*build/i, /\bsupplements?\b/i, /\bhealth/i, /\bpenis\b/i,
/\bnutrition/i, /\bfat\b/i, /\bwrinkl/i, /\bdiet/i, /muscle/i, /\bbrain\b/i, /\bbaba\b/i, /clash ?of ?clans/i, /\bmale\b/i, /testo/i,
/\blover?\b/i, /\bloans?/i, /serum/i, /overcome/i, /workout/i, /fitness/i, /\bAlpha\b/, /\bultra\b/i, /\bPro\b/, /beauty/i ];
var titleRules = sumRules.concat([/(\d)\1{2}/, /care\b/i, /\bwatch\b/i, /\bsell/i, /\bcleans/i, /\bloss\b/i, /\blose\b/i, /\bhelpline\b/i, /\bbuy\b/i, /\blose\b/i,
/\b(phone|support).*number\b/i, /\bimprove/i, /\bonline\b/i, /\byou\scan\b/i, /\bfree\b/i, /\bwholesale\b/i, /\bmarriage\b/i, /\blove\b/i,
/\bpurchas/i, /\bfull\shd\b/i, /\bcraigslist\b/i, /\bbenefits?\b/i, /beneficial/i, /advice/i, /perfect/i ]);
var urlRules = []; // Will be loaded from the SmokeDetector github
$.ajax("https://cdn.rawgit.com/Charcoal-SE/SmokeDetector/master/blacklisted_websites.txt",{
cache: true,
success: function(e)
{
urlRules = e
.filter(function(i){return i;})
.map(function(i){try{return new RegExp(i, "i");}catch(e){}})
.filter(function(i){return !!i;});
},
dataType: "text",
dataFilter: function(i){return i.split("\n");}
});
var style= "#priorityList + #roomtitle + #roomdesc, #priorityList + #roomtitle + #roomdesc + #room-tags {display: none;}";
style += ".q-block {padding: 5px 0px;font-size: 11px;line-height: 14px;}";
style += ".q-title {color: #333;font-weight: bold;margin: 2px;display: block;}";
style += "a.q-title:visited {color: #888 !important;}";
style += "#priorityList {background-color: #ffffff;}";
style += "#clearchat, #clearside {margin-left: 2px;}";
var prioritySites = ['academia', 'android', 'beer', 'boardgames', 'chess', 'coffee', 'computergraphics', 'cooking', 'datascience',
'drupal', 'ebooks', 'engineering', 'expatriates', 'freelancing', 'hsm', 'law', 'mechanics', 'money', 'mythology', 'opensource', 'patents',
'poker', 'productivity', 'quant', 'ru', 'startups', 'travel', 'webapps', 'webmasters', 'writers'];
var timeSensitiveSites = ['drupal', 'superuser', 'askubuntu', 'meta'];
var ignoredSites = ['biology', 'fitness', 'health', 'ja', 'pt', 'es', 'islam'];
var insertRef, ws, clearchat, clearside, priorityList, savingData, wsVolume=0;
var box = document.getElementById('input');
var chat = document.getElementById('chat');
var room = window.location.href.match(/chat[^/]*\/rooms\/\d+/)[0];
var keepGoing = true;
var currentStatus = 'chat only';
var notifications = {};
window.setInterval(checkForSpam, 500);
if (box && chat && room) {
insertRef = document.getElementById('footer-legal');
var separator = document.createTextNode(' | ');
insertRef.insertBefore(separator, insertRef.firstChild);
var onoff = newElem('a', 'on-off', '', 'spamtracker: '+currentStatus);
onoff.title = 'toggle spam tracking';
onoff.onclick = toggleTracking;
onoff.style.cursor = 'pointer';
insertRef.insertBefore(onoff, insertRef.firstChild);
var metabeep = new Audio('//cdn-chat.sstatic.net/chat/meta2.mp3');
var apiKey = '1gtS)lKgyVceC11VlgjyQw((';
var stored = {maxQ: {}, maxU: {}};
var inserted = [], time = 0;
clearchat = newElem('a', 'clearchat', 'button', 'clear chat');
clearchat.title = 'remove all chat messages';
clearchat.onclick = clearChat;
insertRef = document.querySelector('#chat-buttons');
insertRef.appendChild(clearchat, insertRef);
addStyleString(style);
//chrome.storage.sync.get(stored, function(items) {
// var room = window.location.href.match(/chat[^/]*\/rooms\/\d+/)[0];
// stored = items;
//});
}
function checkForSpam() {
if (currentStatus == 'off') {
return;
}
var messageList = document.getElementsByClassName('message');
var message = messageList[messageList.length-1];
if (message && !message.classList.contains('checkedForSpam')) {
message.classList.add('checkedForSpam');
if (message.children[1] && !message.parentNode.parentNode.classList.contains('mine') && !message.querySelector('.onebox')) {
processChatMessage(message);
}
}
}
function switchOn() {
var prot = (window.location.protocol === 'https:' ? 'wss' : 'ws');
ws = new WebSocket(prot+'://qa.sockets.stackexchange.com/');
ws.onmessage = function(e) {
wsVolume = wsVolume + e.data.length;
processQuestion(JSON.parse(JSON.parse(e.data).data));
};
ws.onopen = function() { ws.send('155-questions-active'); };
ws.onclose = function() {if (keepGoing) {window.setTimeout(switchOn, 10000);} };
clearside = newElem('a', 'clearside', 'button', 'clear');
clearside.title = 'dismiss all reports';
clearside.onclick = clearSide;
insertRef = document.querySelector('.fl');
insertRef.appendChild(clearside);
insertRef = document.getElementById('roomtitle');
priorityList = newElem('div','priorityList','question-list','');
insertRef.parentNode.insertBefore(priorityList, insertRef);
currentStatus = 'on';
savingData = window.setInterval(function() {/*chrome.storage.sync.set(stored);*/}, 120000);
keepGoing = true;
console.log('Max post Id');
console.log(stored.maxQ);
console.log('Max user Id');
console.log(stored.maxU);
}
function pauseST() {
keepGoing = false;
ws.close();
clearside.remove();
priorityList.remove();
currentStatus = 'chat only';
//chrome.storage.sync.set(stored);
window.clearInterval(savingData);
}
function switchOff() {
currentStatus = 'off';
}
function processQuestion(q) {
var i, data, url, site, shortSite, qId, uId, title, user, summary, qblock, insert, consider, hh, report, reg;
var msgId, msgTitle;
time = Math.max(time, q.lastActivityDate);
title = q.titleEncodedFancy;
site = q.siteBaseHostAddress;
shortSite = site.split('.')[0];
qId = q.id;
url = '//'+site+'/q/'+qId;
user = q.ownerDisplayName;
summary = q.bodySummary;
uId = (q.ownerUrl ? parseInt(q.ownerUrl.split('/')[4],10) : 0);
if (!stored.maxQ[site]) {
stored.maxQ[site] = 1;
}
if (!stored.maxU[site]) {
stored.maxU[site] = 1;
}
insert = false;
consider = (uId > stored.maxU[site]-3 && uId < 1.01*stored.maxU[site]) ;
consider = consider && (qId > stored.maxQ[site] && qId < 1.01*stored.maxQ[site]);
consider = consider && (ignoredSites.indexOf(shortSite) == -1);
report = 'http://'+site+'/q/'+qId+' ';
if (consider) {
if (summary.length < 100) {
insert = true;
report = report + 'short summary: ' + summary + '\n';
}
hh = new Date().getUTCHours();
if (timeSensitiveSites.indexOf(shortSite)!=-1 && hh>=4 && hh<= 12) {
insert = true;
report = report + 'peak spam time\n';
}
reg = bad(title, titleRules);
if (reg) {
insert = true;
report = report + 'title matched ' + reg + '\n';
}
reg = bad(summary, sumRules);
if (reg) {
insert = true;
report = report + 'summary matched ' + reg + '\n';
}
reg = bad(summary, urlRules);
if (reg) {
insert = true;
report = report + 'smokedetector pattern matched ' + reg + '\n';
}
if (insert) {
reportIt(report, site, qId, 'Q', title, url, q.ownerUrl, user, summary);
}
}
if (!insert && prioritySites.indexOf(shortSite)!=-1) {
window.setTimeout(fetchBody, 60000, shortSite);
}
if (qId>stored.maxQ[site]) {
stored.maxQ[site] = qId;
}
if (uId>stored.maxU[site]) {
stored.maxU[site] = uId;
}
}
function reportIt(report, site, qId, type, title, url, ownerURL, ownerName, summary) {
var qblock, elem, msgId, shortSite = site.split('.')[0], ueTitle, ueSummary;
if (inserted.indexOf(shortSite+qId) == -1) {
report = report + 'At ' + new Date().toString().split(" ")[4] + '\nWebsockets volume: '+wsVolume+'\n';
console.log(report);
inserted.push(shortSite+qId);
msgId = room+'-'+site+'-'+qId+'-'+Date.now();
elem = newElem('span', '', '', '');
elem.innerHTML = title;
ueTitle = elem.textContent;
elem.innerHTML = summary;
ueSummary = elem.textContent;
notifyMe(msgId, shortSite + ' ' + type + ': ' + ueTitle, ueSummary);
qblock = newElem('div',msgId,'q-block','');
qblock.innerHTML = '<a class="q-title" target="_blank" href="'+url+'">' + shortSite + ' ' + type+': '+title + '</a><a target="_blank" href="'+ownerURL+'">' + ownerName + '</a>';
qblock.onclick = removeBlock;
elem.innerHTML = ': '+summary;
qblock.appendChild(elem);
priorityList.insertBefore(qblock, priorityList.firstElementChild);
}
}
function fetchBody(shortSite) {
var request = '//api.stackexchange.com/2.2/posts?pagesize=1&order=desc&sort=creation&site='+(shortSite=='ru'?'ru.stackoverflow':shortSite)+'&filter=!5RBFam4sA56hQ2Q5G3*uvo3fl&key='+apiKey;
getStuff(request, 'json', function(e) {
var q=e.currentTarget.response.items[0], url, site, body, elem, report, qId, insert, reg;
url = q.share_link;
site = url.split('/')[2];
qId = q.post_id;
elem = document.createElement('span');
elem.innerHTML = q.body;
body = elem.textContent;
report = url+' ';
insert = false;
if (q.owner && q.owner.reputation == 1) {
if (q.post_type == 'answer') {
insert = true;
report = report+' answer by a rep 1 user\n';
}
if (body.length < 100) {
insert = true;
report = report + 'short post: ' + body + '\n';
}
if (q.post_type == 'question') {
reg = bad(body, sumRules);
}
if (reg) {
insert = true;
report = report + 'body matched ' + reg + '\n';
}
}
if (insert) {
reportIt(report, site, qId, (q.post_type=='question' ? 'Q' : 'A'), q.title, url, q.owner.link, q.owner.display_name, body.slice(0,150));
report = 'Quota remaining: '+e.currentTarget.response.quota_remaining;
console.log(report);
}
});
}
function notifyMe(id,title,message) {
// var msg = {};
// msg.id = id;
// msg.title = title;
// msg.message = message;
// notifications[id] = msg;
var notification = new Notification(title, { body: message, icon: "//i.imgur.com/kS4QNIv.png" });
notification.onshow = function() {
setTimeout(notification.close, 15000);
};
notification.onclick = function() {
window.focus();
};
notifications[id] = notification;
}
function dismissNotification(id) {
notifications[id].close();
}
function removeBlock(e) {
killBlock(e.target);
}
function killBlock(elem) {
var list = elem.parentNode;
if (list.classList.contains('question-list')) {
dismissNotification(elem.id);
list.removeChild(elem);
}
else {
killBlock(list);
}
}
function processChatMessage(message) {
var smoke = /spam|vgx7b|offensive|abusive/i;
var content = message.children[1].innerHTML;
var i, msg = {}, parts, ch, path, hash, site = '', qId = '', sq;
if (smoke.test(content) && /\/\/[a-z]*.stackexchange.com|stackoverflow.com|superuser.com|serverfault.com|askubuntu.com|stackapps.com|mathoverflow.net/i.test(content)) {
ch = message.children[1].children;
for (i=ch.length-1; i>=0; i--) {
if (ch[i].tagName == 'A') {
hash = ch[i].href.split('#');
path = ch[i].href.split('/');
if (path[3] == 'questions' && hash.length>1) {
site = path[2];
qId = hash[1];
}
else if (/^[qa]/.test(path[3])) {
site = path[2];
qId = path[4];
}
}
}
if (site && qId) {
metabeep.play();
sq = site.split('.')[0] + qId;
if (inserted.indexOf(sq) != -1) {
return;
}
else {
inserted.push();
}
}
msg.id = room+'-'+site+'-'+qId+'-'+Date.now();
parts = message.children[1].textContent.split(': ');
if (parts.length > 1) {
msg.title = parts[0];
msg.message = parts[1];
}
else {
msg.title = 'Flag Request';
msg.message = message.children[1].textContent;
return; // Not used for now...
}
msg.type = 'chat';
notifyMe(msg.id, msg.title, msg.message);
}
}
function clearChat() {
var chat = document.getElementById('chat');
while (chat.firstElementChild) {
chat.removeChild(chat.firstElementChild);
}
var starred = document.querySelector('#starred-posts ul');
while (starred.firstElementChild) {
starred.removeChild(starred.firstElementChild);
}
}
function clearSide() {
while (priorityList.firstElementChild) {
priorityList.removeChild(priorityList.firstElementChild);
}
}
function toggleTracking() {
switch (currentStatus) {
case "off":
switchOn();
break;
case "on":
pauseST();
break;
case "chat only":
switchOff();
}
onoff.textContent = 'spamtracker: '+currentStatus;
}
function bad(text, rules) {
for (var i=0; i<rules.length; i++) {
if (rules[i].test(text)) {
return rules[i];
}
}
return false;
}
function newElem(eType,eId,eClass,eText) {
var e = document.createElement(eType);
if (eId.length>0) {e.id = eId;}
if (eClass.length>0) {e.classList.add(eClass);}
if (eText.length>0) {e.textContent = eText;}
return e;
}
function getStuff(theUrl, type, listener) {
var req = new XMLHttpRequest();
req.responseType = type;
req.onload = listener;
req.open("GET", theUrl, true);
req.send();
}
function htmlDecode(input){
var e = document.createElement('div');
e.innerHTML = input;
return e.textContent;
}
function addStyleString(str) {
var node = document.createElement('style');
node.innerHTML = str;
document.body.appendChild(node);
}
})();