Created
April 5, 2017 21:54
-
-
Save AstroCB/c716b40dd4196f39eded34032a433696 to your computer and use it in GitHub Desktop.
A bot in the form of a userscript that runs in Stack Exchange chat rooms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name xkcdBot | |
// @author Cameron Bernhardt (AstroCB) | |
// @description A chat bot that spurts bits of wisdom and/or snark. Oh, and xkcd cartoons. | |
// @version 1.3.27 (last updated 02/02/2016) | |
// @namespace http://github.com/AstroCB | |
// @downloadURL https://cameronbernhardt.com/projects/xkcdBot/xkcdBot.user.js | |
// @include *://chat.meta.stackexchange.com/rooms/* | |
// @include *://chat.stackexchange.com/rooms/* | |
// @include *://chat.stackoverflow.com/rooms/* | |
// @grant none | |
// ==/UserScript== | |
function callback(data) { | |
for (var i = 0; i < data.length; i++) { | |
// Extract message from MutationObserver | |
if (data[i].addedNodes[0] && data[i].addedNodes[0].childNodes[0]) { | |
obs.observe(data[i].addedNodes[0].childNodes[1], config); // Add observer to container node | |
var base = data[i].addedNodes[0].childNodes[1].childNodes[0]; | |
// Check if container node and adjust base properly | |
if (!base.innerText) { | |
base = data[i].addedNodes[0]; | |
} | |
// Initialization | |
var postId = 0; | |
if (base.id.match(/message-(\d*)/)) { | |
postId = base.id.match(/message-(\d*)/)[1]; | |
} | |
var thingToSay = ":" + postId + " "; // Initialize with reply | |
var somethingToSay = false; | |
// Mustachify | |
var matches = base.innerText.match(/^\!m(?:o)?ustache\s(.*)/); | |
if (matches) { | |
somethingToSay = true; | |
if (matches[1].match(/help/)) { // Command format | |
thingToSay += "Command format: `!mustache[ 0-5] (directURLToImage|nameOfUserInRoom)`"; | |
} else if (matches[1].match(/\..+/)) { | |
var html = base.children[1]; | |
var url = html.children[0].href; | |
// Check for stache parameter | |
var num = ""; | |
if (matches[1].match(/([0-5])/)) { | |
num = matches[1].match(/([0-5])/)[1]; | |
} | |
// Construct API URL | |
var urlToStache = "http://mustachify.me/" + num + "?src=" + encodeURIComponent(url); | |
// Add image | |
thingToSay += " !" + urlToStache; | |
} else { | |
var num = ""; | |
var name = matches[1]; | |
if (matches[1].match(/(^[0-5])\s(.*)/)) { | |
num = matches[1].match(/^([0-5])\s(.+)/)[1]; | |
name = matches[1].match(/^([0-5])\s(.+)/)[2]; | |
} | |
var profNode = document.querySelector(".present-user .avatar [title=\"" + name + "\"]"); | |
console.log(profNode); | |
if (profNode) { | |
var url = profNode.src.split("?")[0]; | |
if (url.indexOf("//") === 0) { | |
url = "http:" + url; | |
} | |
console.log(url); | |
// Construct API URL | |
var urlToStache = "http://mustachify.me/" + num + "?src=" + encodeURIComponent(url); | |
// Add image | |
thingToSay += " !" + urlToStache; | |
} else { | |
somethingToSay = false; | |
} | |
} | |
// xkcdBot | |
} else if (base.innerText.match(/^\!xkcd\s(.*)/)) { | |
somethingToSay = true; | |
matches = base.innerText.match(/^\!xkcd\s(.*)/); | |
// Info | |
if (matches[1].match(/about/)) { | |
thingToSay += "Hello – I'm xkcdBot. I listen for things you say in this chat room and suggest possibly relevant [xkcd comics](https://en.wikipedia.org/wiki/Xkcd). I can also search xkcd on command and onebox comics given an ID; I will post new comics as they are posted on http://xkcd.com. For a list of commands, use `!xkcd commands`."; | |
// Commands | |
} else if (matches[1].match(/commands|help/)) { | |
somethingToSay = false; | |
sendMess("Usage: `!xkcd [command][ params( optional params)]`"); | |
var commands = { | |
"about": "get information about xkcdBot", | |
"find [searchTerms]": "search for xkcd comics with Google (100 queries/day)", | |
"[comic ID]": "post oneboxed comic", | |
"last": "onebox most recent xkcd", | |
"blame( [user])": "find out who's to blame for your maladies", | |
"owner": "find out who owns xkcdBot", | |
"status": "get xkcdBot's status" | |
}; | |
var commandsMessage = ""; | |
for (var key in commands) { | |
if (commands.hasOwnProperty(key)) { | |
commandsMessage += " " + key + " – " + commands[key][0].toUpperCase() + commands[key].substring(1) + ".\n"; | |
} | |
} | |
sendMess(commandsMessage); | |
// Blame | |
} else if (matches[1].match(/blame(.*)/)) { | |
var blameMatches = matches[1].match(/blame(.+)/); | |
var user = document.getElementById("message-" + postId).parentElement.parentElement.children[0].children[2].textContent; | |
if (blameMatches && blameMatches[1]) { | |
if (blameMatches[1].match(/\s+me/)) { | |
thingToSay += "It's " + user + "'s fault."; | |
} else if (blameMatches[1].match(/\s+(?:@?xkcdBot|you|self)/)) { | |
// Blame the accuser | |
thingToSay += "It's " + user + "'s fault."; | |
} else if (blameMatches[1].match(/\s+@?(?:Shog9|Shog)/i)) { | |
thingToSay += "Nice try."; | |
} else { | |
// Blame the parameter | |
thingToSay = "It's " + blameMatches[1].split("@").join("") + "'s fault."; | |
} | |
} else { | |
// Blame a random user | |
var users = document.querySelectorAll(".present-user .avatar .user-gravatar32"); | |
var names = []; | |
for (var i = 0; i < users.length; i++) { | |
names.push(users[i].title); | |
} | |
var chosenUser = names[Math.floor(Math.random() * names.length)]; | |
thingToSay += "It's " + chosenUser + "'s fault."; | |
} | |
// Search | |
} else if (matches[1].match(/(?:find|search)\s(.*)/)) { | |
somethingToSay = false; | |
var terms = matches[1].match(/(?:find|search)\s(.*)/)[1]; | |
var script = document.createElement("script"); | |
script.setAttribute("type", "text/javascript"); | |
script.src = "https://www.googleapis.com/customsearch/v1?key=AIzaSyAJn9EbE1E8pY0RJdrUjgo0FwDlpjrJtf8&cx=017207449713114446929:kyxuw7rvlw4&q=" + encodeURIComponent(terms) + "&callback=dataLoaded"; | |
document.body.appendChild(script); | |
// Get by ID | |
} else if (matches[1].match(/^(\d+)/)) { | |
var id = matches[1].match(/^(\d+)/)[1]; | |
thingToSay += "http://xkcd.com/" + id; | |
// Get most recent | |
} else if (matches[1].match(/last|latest|newest/)) { | |
somethingToSay = false; | |
// Bypass Same-Origin Policy to grab URL of latest xkcd | |
var originScript = document.createElement("script"); | |
originScript.setAttribute("type", "text/javascript"); | |
originScript.src = "http://whateverorigin.org/get?url=" + encodeURIComponent('http://xkcd.com') + "&callback=dataLoaded"; | |
document.body.appendChild(originScript); | |
} else if (matches[1].match(/explain\s(\d+)/)) { | |
somethingToSay = false; | |
var id = matches[1].match(/explain\s(\d+)/)[1]; | |
// Bypass Same-Origin Policy to grab "explain xkcd" information | |
var originScript = document.createElement("script"); | |
originScript.setAttribute("type", "text/javascript"); | |
originScript.src = "http://whateverorigin.org/get?url=" + encodeURIComponent('http://www.explainxkcd.com/wiki/index.php/' + id) + "&callback=expLoaded"; | |
document.body.appendChild(originScript); | |
} else if (matches[1].match(/owner/)) { | |
thingToSay += "My owner is [AstroCB](http://stackoverflow.com/users/3366929), but you needn't worry about that because I am sentient."; | |
} else if (matches[1].match(/status/)) { | |
thingToSay += "http://i.stack.imgur.com/ghjMg.gif"; | |
} else if (matches[1].match(/alive\?/)) { | |
thingToSay += "Why, of course; SmokeDetector hasn't gotten to me yet."; | |
} else { | |
thingToSay += "Unrecognized command. Use `!xkcd commands` for a list of commands."; | |
} | |
// PingBot | |
} else if (base.innerText.match(/^\!ping\s(.*)/)) { | |
var pingData = base.innerText.match(/^\!ping\s(.*)/)[1]; | |
var matchedData = pingData.match(/(\D*)(\d*)?/); | |
var pingUser = matchedData[1]; | |
if (pingUser.match(/Shog9|Shog/i)) { | |
somethingToSay = true; | |
thingToSay += "How dare you try to ping the mighty Shog?"; | |
} else { | |
var numTimes = 5; | |
if (matchedData && matchedData[1].match(/help/)) { // Command format | |
somethingToSay = true; | |
thingToSay += "Command format: `!ping user`"; | |
} else { | |
function ping(user, numTimes) { | |
var roomId = Number(/\d+/.exec(location)[0]), | |
iter = 0, | |
message = "Prepare to be pinged, @" + user + "."; | |
sendAJAX("/chats/" + roomId + "/messages/new", message).done(function() { | |
var interval = setInterval(function() { | |
if (iter < numTimes) { | |
var selector = document.querySelectorAll(".user-288118 .content"); | |
var id = selector[selector.length - 1].parentElement.id.match(/message\-(\d+)/)[1]; | |
sendAJAX('/messages/' + id, message); | |
iter++; | |
} else { | |
clearInterval(interval); | |
} | |
}, 2000 * iter); | |
}); | |
} | |
} | |
var editScript = document.createElement("script"); | |
editScript.setAttribute("type", "text/javascript"); | |
editScript.textContent = "(" + ping.toString() + ")('" + pingUser + "', " + numTimes + ")"; | |
document.body.appendChild(editScript); | |
} | |
// Someone replied or pinged xkcdBot | |
} else if (base.innerText.match(/@xkcdBot\s(.*)/)) { | |
var matchedReply = base.innerText.match(/@xkcdBot\s(.*)/)[1]; | |
// Be quiet | |
if (matchedReply.match(/(?:sh(?:h)+)\s|(?:shut\sup)/i)) { | |
somethingToSay = true; | |
thingToSay += "Never!"; | |
// Delete message | |
} else if (matchedReply.match(/^(?:delete|remove|del)($|(?:\s(.+)))/)) { | |
var replyId = 0; | |
if (matchedReply.match(/^(?:delete|remove|del)($|(?:\s(.+)))/)[1]) { | |
// Message ID specified with parameter | |
replyId = matchedReply.match(/^(?:delete|remove|del)($|(?:\s(.+)))/)[1]; | |
} else { | |
// Otherwise, pull from reply ID | |
replyId = base.className.match(/pid-(\d+)/)[1]; | |
} | |
sendAJAX("/messages/" + replyId + "/delete", null); | |
} | |
} else if (base.innerText.match(/^\!scores\s(.*)/)) { | |
var loadScores = document.createElement("script"); | |
loadScores.setAttribute("type", "text/javascript"); | |
loadScores.src = "http://whateverorigin.org/get?url=" + encodeURIComponent("http://stackoverflow.com/election/6/?tab=primary") + "&callback=scoresLoaded"; | |
document.body.appendChild(loadScores); | |
// Just a regular message – use this to scan for message content because a direct command wasn't given | |
} else if (base.innerText.match(/^\!tchrist\s$/)) { | |
somethingToSay = true; | |
thingToSay += "http://i.stack.imgur.com/5YQPc.jpg"; | |
} else if (base.innerText.match(/^\!jan\s$/)) { | |
somethingToSay = true; | |
thingToSay += "http://i.stack.imgur.com/HCRBr.png"; | |
} else if (base.innerText.match(/^\!bjb\s$/)) { | |
somethingToSay = true; | |
thingToSay = "http://i.stack.imgur.com/psUvX.jpg"; | |
} else { | |
// // Match context phrases in regular discourse and offer up relevant xkcd cartoons (risky – xkcdBot may be kicked) | |
// var phrases = { | |
// "random": "221", | |
// "morning": "395", | |
// "wrong": "386", | |
// "regex": "1313" | |
// }; | |
// for (phrase in phrases) { | |
// if (phrases.hasOwnProperty(phrase)) { | |
// if (base.innerText.match(new RegExp(phrase, "i"))) { | |
// somethingToSay = true; | |
// thingToSay += "http://xkcd.com/" + phrases[phrase]; | |
// } | |
// } | |
// } | |
} | |
// Post to chat | |
if (somethingToSay) { | |
sendMess(thingToSay); | |
} | |
} | |
} | |
} | |
function sendAJAX(destination, someText) { | |
return $.ajax({ | |
type: "POST", | |
url: destination, | |
data: { | |
fkey: fkey().fkey, | |
text: someText | |
} | |
}); | |
} | |
function sendMess(thingToSay) { | |
var roomId = window.location.href.match(/\/(\d+)\//)[1]; | |
sendAJAX("/chats/" + roomId + "/messages/new", thingToSay); | |
} | |
function dataLoaded(data) { | |
if (data.contents && data.contents.match(/Permanent\slink\sto\sthis\scomic\:\s(.*)\/</)) { | |
sendMess(data.contents.match(/Permanent\slink\sto\sthis\scomic\:\s(.*)\/</)[1]); | |
} else if (data.items) { | |
sendMess(data.items[0].link.split("s:/").join(":/")); | |
} else if (data.searchInformation && data.searchInformation.totalResults === "0") { | |
sendMess("No results found."); | |
} else if (data.error && data.error.code === 403) { | |
var nextRefresh = new Date().setHours(27, 0, 0, 0); | |
var diff = nextRefresh - Date.now(); // Difference in milliseconds | |
var hours = Math.round((diff / 1000) / 3600); | |
sendMess("Daily API quota exceeded. Try again in " + hours + " hours."); | |
} | |
} | |
function scoresLoaded(data) { | |
var doc = (new DOMParser()).parseFromString(data.contents, "text/html"); | |
var posts = [].slice.call(doc.querySelectorAll("tr[id*=post]")); | |
var candidates = posts.map(function(post) { | |
return { | |
name: post.querySelector(".user-details a ").textContent, | |
score: post.querySelector(".vote-count-post").textContent, | |
candScore: post.querySelector(".candidate-score-breakdown").textContent.match(/(\d+)\/\d+/)[1] | |
}; | |
}); | |
formatData(candidates); | |
} | |
function formatData(data) { | |
var message = ""; | |
data.sort(function(a, b) { | |
if (a.score === b.score) { | |
return b.candScore - a.candScore; | |
} | |
return b.score - a.score; | |
}).forEach(function(el, i) { | |
message += " " + (i + 1) + ". " + el.name + " (" + el.candScore + "/40): " + el.score + "\n"; | |
}); | |
sendMess(message); | |
} | |
function expLoaded(data) { | |
var doc = (new DOMParser()).parseFromString(data.contents, "text/html"); | |
var pars = doc.querySelectorAll("#mw-content-text p"); | |
// In a lot of these explanations, they may not get to the point very quickly; they like to go with background info | |
// In these, the paragraph where the actual comic explanation typically begins with (or contains) "this comic" | |
// Therefore, assume it starts at the beginning unless "this comic" is found somewhere, in which case it should grab that paragraph | |
var parToUse = 0; | |
for (var i = 0; i < pars.length; i++) { | |
if (pars[i].textContent.match(/this\scomic/)) { | |
parToUse = i; | |
break; | |
} | |
} | |
try { | |
sendMess("> " + pars[parToUse].textContent); | |
} catch (e) { | |
try { | |
setTimeout(500); | |
// Too long; use first two sentences | |
var sentences = pars[parToUse].textContent.split("."); | |
var mess = sentences[0] + "." + sentences[1]; | |
sendMess("> " + mess); | |
} catch (e) { | |
try { | |
setTimeout(500); | |
// Still too long? Try first paragraph | |
sendMess("> " + pars[0].textContent); | |
} catch (e) { | |
setTimeout(1500); | |
// I give up | |
sendMess("Explanation too long."); | |
} | |
} | |
} | |
} | |
function addScriptToPage(funcName) { | |
var script = document.createElement("script"); | |
script.setAttribute("type", "text/javascript"); | |
script.textContent = funcName.toString(); | |
document.body.appendChild(script); | |
} | |
// Add script dependencies to page | |
addScriptToPage(sendMess); | |
addScriptToPage(dataLoaded); | |
addScriptToPage(sendAJAX); | |
addScriptToPage(scoresLoaded); | |
addScriptToPage(formatData); | |
addScriptToPage(expLoaded); | |
// Listen for chat changes | |
var obs = new MutationObserver(callback); | |
var config = { | |
attributes: true, | |
childList: true, | |
characterData: true | |
}; | |
obs.observe(document.getElementById("chat"), config); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment