├── plugins ├── marmite.js ├── read.js ├── CatFacts.js ├── clear.js ├── spongebob.js ├── nou.js ├── remind.js ├── 8ball.js ├── hedgehogs.js ├── urban.js ├── ascii.js ├── random.js ├── image.js ├── pingpong.js ├── reddit.js └── zalgo.js ├── package.json ├── networks.js ├── LICENSE ├── networks ├── irc.js └── messenger.js ├── bot.js └── README.md /plugins/marmite.js: -------------------------------------------------------------------------------- 1 | // Teach those marmite-lovers what's up 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "regex", 6 | query: /marmite/i, 7 | callback: marmite 8 | } 9 | ]}; 10 | 11 | function marmite(reply, message) { 12 | console.log("Someone said marmite!!!!!"); 13 | reply("Fuck marmite!"); 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mi1kb0t", 3 | "version": "1.0.0", 4 | "description": "A multi-platform chat bot", 5 | "author": "Dan Hlavenka ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/DanH42/mi1kb0t.git" 10 | }, 11 | "dependencies": { 12 | "facebook-chat-api": "1.1.x", 13 | "irc": "0.3.x", 14 | "googleapis": "2.1.x", 15 | "request": "2.64.x" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugins/read.js: -------------------------------------------------------------------------------- 1 | // Mark all messages as read in threads where your name was recently mentioned 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "all", 6 | callback: function(reply, message, api){ 7 | if(api.type === "messenger"){ 8 | if(message.isAddressed === 2) 9 | api.markAsRead(message.thread_id); 10 | else if(message.isAddressed === 1) 11 | api.markAsRead(message.thread_id); 12 | } 13 | } 14 | } 15 | ]}; 16 | -------------------------------------------------------------------------------- /networks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "facebook": { 3 | type: "messenger", 4 | email: "mi1kb0t@example.com", 5 | password: "hunter2" 6 | }, "my_bouncer": { 7 | type: "irc", 8 | server: "bouncer.example.com", 9 | port: 6667, 10 | sasl: true, 11 | secure: true, 12 | selfSigned: true, 13 | certExpired: true, 14 | nick: "mi1kb0t", 15 | userName: "mi1kb0t", 16 | password: "mi1kb0t:hunter2", 17 | channels: ['#general'] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /plugins/CatFacts.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "equals", 6 | query: ".cat", 7 | callback: function(reply, message){ 8 | request("https://catfact.ninja/fact", function(err, res, body){ 9 | try{ 10 | var obj = JSON.parse(body); 11 | if(obj && obj.fact) 12 | return reply(obj.fact); 13 | }catch(e){} 14 | 15 | reply("Sorry, no cat facts for you today :'(", {delay: 0}); 16 | }); 17 | } 18 | } 19 | ]}; 20 | -------------------------------------------------------------------------------- /plugins/clear.js: -------------------------------------------------------------------------------- 1 | module.exports = {listeners: [ 2 | { 3 | type: "startsWith", 4 | query: ".clear", 5 | callback: cls 6 | }, { 7 | type: "regex", 8 | query: /^\.?cls/i, 9 | callback: cls 10 | }, { 11 | type: "startsWith", 12 | query: ".nest", 13 | callback: cls 14 | } 15 | ]}; 16 | 17 | function cls(reply, message){ 18 | var small_space = String.fromCharCode(65279, 32); 19 | var big_space = String.fromCharCode(65279, 32, 65039, 32, 65039); 20 | var split = message.body.split(" "); 21 | split.shift(); 22 | var extra = "\n" + split.join(" ").trim(); 23 | reply(new Array(50).fill(big_space).join("\n") + extra, {delay: 0}); 24 | } 25 | -------------------------------------------------------------------------------- /plugins/spongebob.js: -------------------------------------------------------------------------------- 1 | // Repeat a message in SPonGEbOB CaSe 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "regex", 6 | query: /^\.(sb|sponge|spongebob) +(.+)/i, 7 | callback: function(reply, message, api, match){ 8 | var str = match[2]; 9 | var out = ""; 10 | // Try up to 10 times to get random-looking case 11 | for(var i = 0; i < 10; i++){ 12 | out = spongebob(str); 13 | if(out !== str && out !== str.toUpperCase() && out !== str.toLowerCase()) 14 | break; 15 | } 16 | reply(out); 17 | } 18 | } 19 | ]}; 20 | 21 | function spongebob(str){ 22 | var out = ""; 23 | for(var i = 0; i < str.length; i++){ 24 | if(Math.random() < .5) 25 | out += str[i].toUpperCase(); 26 | else 27 | out += str[i].toLowerCase(); 28 | } 29 | return out; 30 | } 31 | -------------------------------------------------------------------------------- /plugins/nou.js: -------------------------------------------------------------------------------- 1 | // Turn messages back around on their sender 2 | 3 | var responseTimes = {}; 4 | module.exports = {listeners: [ 5 | { 6 | type: "regex", 7 | query: /(?: is|['’]s|['’]re) (.*?)(?: and| or| but|http|[,.?!:()]|$)/i, 8 | callback: function(reply, message, api, match){ 9 | // Always respond if recently mentioned, otherwise 5% chance 10 | if((message.isAddressed || Math.random() < 0.05)){ 11 | var time = new Date().getTime(); 12 | // Don't respond unless it's been at least half an hour, or the current message is also a mention 13 | if(message.isAddressed === 2 || !responseTimes[message.threadID] || time - responseTimes[message.threadID] > 1800000){ 14 | responseTimes[message.threadID] = time; 15 | var comeback = match[1].trim(); 16 | // If they're shouting, shout back 17 | if(comeback.toUpperCase() === comeback && comeback.toLowerCase() !== comeback) 18 | reply("YOUR MOM'S " + comeback + "!!!"); 19 | else 20 | reply("YOU'RE " + comeback + "."); 21 | } 22 | } 23 | } 24 | } 25 | ]}; 26 | -------------------------------------------------------------------------------- /plugins/remind.js: -------------------------------------------------------------------------------- 1 | const units = [ 2 | { 3 | name: "hour", 4 | seconds: 60 * 60 5 | }, { 6 | name: "minute", 7 | seconds: 60 8 | }, { 9 | name: "second", 10 | seconds: 1 11 | } 12 | ]; 13 | 14 | module.exports = {listeners: [ 15 | { 16 | type: "regex", 17 | //query: /^[^a-z0-9]remind ([1-9]+[0-9]*)([hms]) (.+)/i, 18 | query: /^[^a-z0-9]remind (?:([1-9]+[0-9]*)h)?(?:([1-9]+[0-9]*)m)?(?:([1-9]+[0-9]*)s)? (.+)/i, 19 | callback: function(reply, message, api, match){ 20 | match.shift(); 21 | 22 | var seconds = 0; 23 | var names = []; 24 | for(var i = 0; i < units.length; i++){ 25 | if(match[i] === undefined) 26 | continue; 27 | var unit = units[i]; 28 | var num = parseInt(match[i]); 29 | seconds += num * unit.seconds; 30 | var unitName = unit.name; 31 | if(num !== 1) unitName += "s"; 32 | names.push(`${num} ${unitName}`); 33 | } 34 | 35 | var msg = match[units.length]; 36 | setTimeout(reply.bind(api, msg), seconds * 1000); 37 | reply(`Reminder set for ${names.join(", ")}`); 38 | } 39 | } 40 | ]}; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dan Hlavenka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /networks/irc.js: -------------------------------------------------------------------------------- 1 | module.exports = {connect: function(credentials, readyCallback, messageCallback){ 2 | var irc = require('irc'); 3 | 4 | var client = new irc.Client(credentials.server, credentials.nick, credentials); 5 | client.type = "irc"; 6 | readyCallback(client); 7 | 8 | client.addListener('message', function(from, to, msg){ 9 | // Fix PM weirdness 10 | var respondTo = to; 11 | if(to == credentials.nick) 12 | respondTo = from; 13 | 14 | var message = { 15 | type: 'message', 16 | sender_name: from, 17 | sender_id: from, 18 | body: msg, 19 | thread_id: respondTo 20 | }; 21 | 22 | console.log(message); 23 | 24 | if(to == credentials.nick) 25 | message.isAddressed = 2; 26 | 27 | var reply = function(text, options, callback){ 28 | if(typeof options === "function"){ 29 | callback = options; 30 | options = {}; 31 | }else if(typeof options !== "object") 32 | options = {}; 33 | 34 | client.say(respondTo, text); 35 | if(typeof callback === "function") 36 | callback(null); // We can't detect errors here, assume success 37 | }; 38 | 39 | messageCallback(reply, message, client); 40 | }); 41 | }}; 42 | -------------------------------------------------------------------------------- /plugins/8ball.js: -------------------------------------------------------------------------------- 1 | // Respond to simple yes/no questions 2 | 3 | var crypto = require('crypto'); 4 | 5 | module.exports = {listeners: [ 6 | { 7 | type: "startsWith", 8 | query: ".8", 9 | callback: function(reply, message){ 10 | if(message.body === ".8") 11 | return reply("Ask the magic 8ball a question! Usage: .8 "); 12 | 13 | var messages = [ 14 | "It is certain", 15 | "It is decidedly so", 16 | "Without a doubt", 17 | "Yes definitely", 18 | "You may rely on it", 19 | "As I see it yes", 20 | "Most likely", 21 | "Yes", 22 | "Signs point to yes", 23 | "Reply hazy try again", 24 | "Concentrate and ask again", 25 | "Don't count on it", 26 | "My reply is no", 27 | "God says no", 28 | "Very doubtful", 29 | "Outlook is terrible" 30 | ]; 31 | 32 | // Salt with the thread ID and some server-specific data 33 | // to make responses thread-specific and hard to guess. 34 | var str = message.body + message.thread_id + process.env.PATH; 35 | var hash = crypto.createHash('md5').update(str).digest('hex'); 36 | var index = parseInt(hash.substr(0, 1), 16); 37 | reply(messages[index]); 38 | } 39 | } 40 | ]}; 41 | -------------------------------------------------------------------------------- /plugins/hedgehogs.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "startsWith", 6 | query: ".hedgehog", 7 | callback: hedgehogs 8 | }, { 9 | type: "equals", 10 | query: ".hh", 11 | callback: hedgehogs 12 | } 13 | ]}; 14 | 15 | function hedgehogs(reply, message, api){ 16 | if(api.type === "messenger") 17 | api.sendTypingIndicator(message.thread_id); 18 | var url = "https://www.facebook.com/groups/hedgehoghackers/photos/"; 19 | request({ 20 | url: url, 21 | headers: { 22 | 'User-Agent': "Mozilla/5.0 AppleWebKit (KHTML, like Gecko) Chrome Safari" 23 | } 24 | }, function(err, res, body){ 25 | var images = body.match(/i style="background-image: url\((.+?)\)/g); 26 | if(!images || !images.length) 27 | return reply({attachment: request("https://i.imgur.com/TBwbKw1.png")}); 28 | var index = Math.floor(Math.random() * images.length); 29 | var url = images[index]; 30 | url = url.substring(31, url.length - 1).replace(/&/g, '&'); 31 | console.log(url); 32 | var image = request(url + '&.jpg'); 33 | console.log(typeof image); 34 | 35 | if(image) 36 | reply({attachment: image}); 37 | else 38 | reply({attachment: request("https://i.imgur.com/TBwbKw1.png")}); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /plugins/urban.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "startsWith", 6 | query: ".ud", 7 | callback: function(reply, message, api){ 8 | if(message.body.length === 3) 9 | return reply("Usage: .ud "); 10 | urban(message.body.substr(4), reply, message, api); 11 | } 12 | }, { 13 | type: "startsWith", 14 | query: ".urban", 15 | callback: function(reply, message, api){ 16 | if(message.body.length === 6) 17 | return reply("Usage: .urban "); 18 | urban(message.body.substr(7), reply, message, api); 19 | } 20 | } 21 | ]}; 22 | 23 | function urban(term, reply, message, api){ 24 | if(api.type === "messenger") 25 | api.sendTypingIndicator(message.thread_id); 26 | request("http://api.urbandictionary.com/v0/define?term=" + encodeURIComponent(term), function(err, res, body){ 27 | try{ 28 | var obj = JSON.parse(body); 29 | if(obj && obj.list && obj.list[0]){ 30 | var index = -1; 31 | var max = -1; 32 | for(var i = 0; i < obj.list.length; i++){ 33 | if(obj.list[i].thumbs_up > max){ 34 | index = i; 35 | max = obj.list[i].thumbs_up; 36 | } 37 | } 38 | reply(obj.list[index].definition); 39 | }else 40 | reply("No results found for \"" + term + "\"", {delay: 0}); 41 | }catch(e){ 42 | reply("No results found for \"" + term + "\"", {delay: 0}); 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /plugins/ascii.js: -------------------------------------------------------------------------------- 1 | module.exports = {listeners: [ 2 | { 3 | type: "regex", 4 | query: /^(?:(?:get-asciiart(?: -*)?)|(?:\.ascii(?: )?))(.*)$/i, 5 | callback: function(reply, message, api, match){ 6 | var art = (match[1] || "").toLowerCase(); 7 | asciiart(art, reply); 8 | } 9 | }, { 10 | type: "equals", 11 | query: ".shrug", 12 | callback: asciiart.bind(this, "shrug") 13 | }, { 14 | type: "regex", 15 | query: /^\.(table|flip|tableflip)$/i, 16 | callback: asciiart.bind(this, "tableflip") 17 | }, { 18 | type: "equals", 19 | query: ".lenny", 20 | callback: asciiart.bind(this, "lenny") 21 | }, { 22 | type: "regex", 23 | query: /^\.think(ing)?$/, 24 | callback: asciiart.bind(this, "thinking") 25 | } 26 | ]}; 27 | 28 | const ascii = { 29 | tableflip: "(╯°□°)╯︵ ┻━┻", 30 | shrug: "¯\\_(ツ)_/¯", 31 | lenny: "( ͡° ͜ʖ ͡°)", 32 | thinking: "⠀⠀⠀⠀⠀⢀⣀⣀⣀\n⠀⠀⠀⠰⡿⠿⠛⠛⠻⠿⣷\n⠀⠀⠀⠀⠀⠀⣀⣄⡀⠀⠀⠀⠀⢀⣀⣀⣤⣄⣀⡀\n⠀⠀⠀⠀⠀⢸⣿⣿⣷⠀⠀⠀⠀⠛⠛⣿⣿⣿⡛⠿⠷\n⠀⠀⠀⠀⠀⠘⠿⠿⠋⠀⠀⠀⠀⠀⠀⣿⣿⣿⠇\n⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠁\n\n⠀⠀⠀⠀⣿⣷⣄⠀⢶⣶⣷⣶⣶⣤⣀\n⠀⠀⠀⠀⣿⣿⣿⠀⠀⠀⠀⠀⠈⠙⠻⠗\n⠀⠀⠀⣰⣿⣿⣿⠀⠀⠀⠀⢀⣀⣠⣤⣴⣶⡄\n⠀⣠⣾⣿⣿⣿⣥⣶⣶⣿⣿⣿⣿⣿⠿⠿⠛⠃\n⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄\n⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡁\n⠈⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁\n⠀⠀⠛⢿⣿⣿⣿⣿⣿⣿⡿⠟\n⠀⠀⠀⠀⠀⠉⠉⠉", 33 | }; 34 | 35 | function asciiart(art, reply){ 36 | if(ascii[art]) 37 | return reply(ascii[art]); 38 | var keys = Object.keys(ascii); 39 | if(art === "") 40 | return reply("Specify one of: " + keys.join(", ")); 41 | art = keys[Math.floor(Math.random() * keys.length)]; 42 | reply("I don't know what that is, but here's " + art + ":", {}, function(){ 43 | reply(ascii[art]); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /plugins/random.js: -------------------------------------------------------------------------------- 1 | // Get some random numbers 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "startsWith", 6 | query: ".dice", 7 | callback: roll 8 | }, { 9 | type: "startsWith", 10 | query: ".roll", 11 | callback: roll 12 | }, { 13 | type: "equals", 14 | query: ".coin", 15 | callback: function(reply){ 16 | reply(Math.random() < 0.5 ? "Heads" : "Tails"); 17 | } 18 | } 19 | ]}; 20 | 21 | function roll(reply, message){ 22 | var sides = 6; 23 | var rolls = 1; 24 | var overflow = false; 25 | 26 | var multiSyntax = false; 27 | 28 | var match = message.body.match(/(?:([0-9]*)d)?([0-9]+)/i); 29 | console.log(match); 30 | if(match && match.length === 3){ 31 | if(match[1]){ 32 | rolls = parseInt(match[1]) 33 | multiSyntax = true; 34 | } 35 | sides = parseInt(match[2]); 36 | 37 | if(rolls > 10){ 38 | rolls = 10; 39 | overflow = true; 40 | } 41 | } 42 | 43 | var rolled = 0; 44 | var total = 0; 45 | var nextRoll = function(){ 46 | var num = Math.floor(Math.random() * sides) + 1; 47 | total += num; 48 | 49 | var comment = ""; 50 | if(multiSyntax){ 51 | if(num === sides) 52 | comment = "!"; 53 | else if(num === 1) 54 | comment = " :("; 55 | } 56 | 57 | reply(num + comment, {delay: 0}); 58 | 59 | if(++rolled < rolls) 60 | setTimeout(nextRoll, 100); 61 | else if(overflow) 62 | setTimeout(function(){ 63 | reply("Ran out of dice!"); 64 | }, 100); 65 | else if(rolls > 1) 66 | setTimeout(function(){ 67 | reply("Total: " + total); 68 | }, 100); 69 | } 70 | nextRoll(); 71 | } 72 | -------------------------------------------------------------------------------- /plugins/image.js: -------------------------------------------------------------------------------- 1 | // When someone's message ends in an image extension, Google an image for them 2 | 3 | var request = require('request'); 4 | var google = require('googleapis'); 5 | var search = google.customsearch('v1'); 6 | 7 | // Replace these with your own to customize results 8 | var options = { 9 | key: 'AIzaSyCmDAUo4oMpJBomql5uRo6dWLxUpzyfH5U', 10 | cx: '014430627749204945988:lkriy0exegq' 11 | }; 12 | 13 | module.exports = {listeners: [ 14 | { 15 | type: "regex", 16 | query: /(.+)\.(jpe?g|png|bmp|gifv?|webm|tiff?)$/i, 17 | callback: function(reply, message, api, match){ 18 | var error = function(code, msg){ 19 | console.log(msg); 20 | reply({attachment: request("https://http.cat/" + code + ".jpg")}); 21 | }; 22 | 23 | // Exclude URLs 24 | if(message.body.indexOf('/') !== -1) return; 25 | 26 | var opts = { 27 | auth: options.key, 28 | cx: options.cx, 29 | q: match[1].replace(/[^a-z0-9]/ig, " "), 30 | safe: "medium", 31 | num: 1, 32 | searchType: "image" 33 | }; 34 | // Try to coax results to include animations 35 | // "imageType: animated" is not supported by the API :( 36 | if(match[2].match(/gifv?|webm/)) 37 | opts.fileType = "gif"; 38 | 39 | search.cse.list(opts, function(err, res){ 40 | if(err) 41 | if(err.errors && err.errors[0] && err.errors[0].reason == "dailyLimitExceeded") 42 | return error(509, err); 43 | else 44 | return error(500, err); 45 | 46 | if(!res.items || res.items.length === 0) return error(404); 47 | var url = res.items[0].link; 48 | 49 | // Giphy is dumb. Fix their dumbness. 50 | var giphy = url.match(/\.giphy\.com\/media\/(.*)\//); 51 | if(giphy && giphy[1]) 52 | url = "https://media.giphy.com/media/" + giphy[1] + "/giphy.gif"; 53 | console.log(url); 54 | 55 | if(api.type === "messenger"){ 56 | // Send a typing indication while down/uploading the image 57 | api.sendTypingIndicator(message.thread_id); 58 | reply({attachment: request(url)}, function(err){ 59 | if(err) return error(500, err); 60 | }); 61 | }else 62 | reply(url); 63 | }); 64 | } 65 | } 66 | ]}; 67 | -------------------------------------------------------------------------------- /plugins/pingpong.js: -------------------------------------------------------------------------------- 1 | // Various assorted call/response combinations 2 | 3 | var request = require('request'); 4 | module.exports = {listeners: [ 5 | { 6 | type: "equals", 7 | query: ".woo", 8 | callback: function(reply){ 9 | reply("WOOOOOOOOOOOOO!"); 10 | } 11 | }, { 12 | type: "contains", 13 | query: "ayy", 14 | callback: function(reply){ 15 | var alien = String.fromCharCode(55357, 56445); 16 | var flame = String.fromCharCode(55357, 56613); 17 | var date = new Date(); 18 | if(date.getMonth() === 3 && date.getDate() === 20){ 19 | //if(date.getHours() % 12 === 4 && date.getMinutes() === 20) 20 | // It's 4:20 somewhere 21 | if(date.getMinutes() === 20) 22 | reply(`${flame} ${alien} ${flame} lmao ${flame} ${alien} ${flame}`); 23 | else 24 | reply(`lmao ${flame}`); 25 | }else{ 26 | if(date.getHours() % 12 === 4 && date.getMinutes() === 20) 27 | reply(`${alien} ${alien} ${alien} lmao ${alien} ${alien} ${alien}`); 28 | else 29 | reply(`lmao ${alien}`); 30 | } 31 | } 32 | }, { 33 | type: "regex", 34 | query: /deez.?nut[sz]/i, 35 | callback: function(reply){ 36 | reply("GOT EEM"); 37 | } 38 | }, { 39 | type: "regex", 40 | query: /\.((dick)|(threatbutt))(butt)?|\.(butt)/i, 41 | callback: function(reply, message){ 42 | if(message.body.toLowerCase().indexOf('threat') != -1){ 43 | reply({attachment: request("https://pbs.twimg.com/profile_images/590578632906637312/hGTAcKmm.png")}); 44 | } 45 | else{ 46 | reply({attachment: request("http://i.imgur.com/j0ymrVQ.png")}); 47 | } 48 | } 49 | }, { 50 | type: "regex", 51 | query: /^o+h*$/i, 52 | callback: function(reply, message){ 53 | if(message.body.length > 2) 54 | reply("REKT"); 55 | } 56 | }, { 57 | type: "equals", 58 | query: ".fuck", 59 | callback: function(reply){ 60 | var insults = [ 61 | "I'm a fucking twat...", 62 | "I'm a useless piece of shit...", 63 | "I'm a backstabber, never trust me...", 64 | "I am a worthless machine..." 65 | ]; 66 | reply(insults[Math.floor(Math.random() * insults.length)]); 67 | } 68 | }, { 69 | type: "regex", 70 | query: /know what .*grandm(a|other) .*says/i, 71 | callback: function(reply){ 72 | reply("Fuck 'em!"); 73 | } 74 | }, { 75 | type: "equals", 76 | query: "rip", 77 | callback: function(reply){ 78 | var respects = ["F", "𝖥", "𝐹", "𝘍", "𝙵", "𝑭", "𝙁", "F", "𝐅", "𝗙", "𝓕", "🅵", "🄵", "🅕", "Ⓕ", "ᶠ"]; 79 | reply(respects[Math.floor(Math.random() * respects.length)]); 80 | } 81 | } 82 | ]}; 83 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var networks = require('./networks.js'); 3 | 4 | var isAddressed = {}; 5 | var listeners = { 6 | equals: [], 7 | startsWith: [], 8 | contains: [], 9 | regex: [], 10 | all: [] 11 | }; 12 | 13 | var readyCallbacks = []; 14 | 15 | // Load all plugins in plugins/ directory and save their callbacks 16 | fs.readdirSync(__dirname + '/plugins/').forEach(function(file){ 17 | if(file.match(/\.js$/) !== null){ 18 | var plugin = require('./plugins/' + file); 19 | if(plugin.listeners){ 20 | for(var i = 0; i < plugin.listeners.length; i++){ 21 | var listener = plugin.listeners[i]; 22 | listeners[listener.type].push(listener); 23 | } 24 | } 25 | 26 | if(plugin.apiReady) 27 | readyCallbacks.push(plugin.apiReady); 28 | } 29 | }); 30 | 31 | // Start connecting to networks 32 | for(var connectionName in networks){ 33 | var networkCreds = networks[connectionName]; 34 | var network = require('./networks/' + networkCreds.type + '.js'); 35 | 36 | network.connect(networkCreds, function(api){ 37 | api.networkType = networkCreds.type; 38 | for(var i = 0; i < readyCallbacks.length; i++) 39 | readyCallbacks[i](api); 40 | }, function(reply, message, api){ 41 | // Detect if you're being spoken to; adjust if you rename your bot 42 | // isAddressed = 2 means the current message is addressing the bot 43 | // isAddressed = 1 means the bot was mentioned in the past 30 seconds 44 | // isAddressed = 0 means the bot has not been mentioned recently 45 | if(message.isAddressed === 2 || (message.body && message.body.match(/b[0o]t/i) && message.body.match(/mi[l1]k/i))){ 46 | if(isAddressed[message.thread_id]) 47 | clearTimeout(isAddressed[message.thread_id]); 48 | isAddressed[message.thread_id] = setTimeout(function(id){ 49 | delete isAddressed[message.thread_id]; 50 | }.bind(this, message.thread_id), 30000); 51 | message.isAddressed = 2; 52 | }else if(isAddressed[message.thread_id]) 53 | message.isAddressed = 1; 54 | else 55 | message.isAddressed = 0; 56 | 57 | // Call all the `all` listeners first 58 | for(var i = 0; i < listeners.all.length; i++) 59 | listeners.all[i].callback(reply, message, api); 60 | 61 | // Some messages may not actually have text in their body 62 | if(message.body){ 63 | var msg = message.body.toLowerCase(); 64 | for(var i = 0; i < listeners.equals.length; i++) 65 | if(msg === listeners.equals[i].query) 66 | listeners.equals[i].callback(reply, message, api); 67 | for(var i = 0; i < listeners.startsWith.length; i++) 68 | if(msg.indexOf(listeners.startsWith[i].query) === 0) 69 | listeners.startsWith[i].callback(reply, message, api); 70 | for(var i = 0; i < listeners.contains.length; i++) 71 | if(msg.indexOf(listeners.contains[i].query) !== -1) 72 | listeners.contains[i].callback(reply, message, api); 73 | 74 | for(var i = 0; i < listeners.regex.length; i++){ 75 | var match = message.body.match(listeners.regex[i].query); 76 | if(match !== null) 77 | listeners.regex[i].callback(reply, message, api, match); 78 | } 79 | } 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /networks/messenger.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'); 2 | 3 | module.exports = {connect: function(credentials, readyCallback, messageCallback){ 4 | var login = require('facebook-chat-api'); 5 | 6 | login(credentials, function callback(err, api){ 7 | if(err) return console.error(err); 8 | 9 | // Workaround to make a needlessly required argument optional again 10 | var native_sendTypingIndicator = api.sendTypingIndicator; 11 | api.sendTypingIndicator = function(thread_id, callback){ 12 | if(!callback) 13 | callback = function(){}; 14 | native_sendTypingIndicator(thread_id, callback); 15 | }; 16 | 17 | var native_sendMessage = api.sendMessage; 18 | api.sendMessage = function(message, threadID, callback){ 19 | try{ 20 | native_sendMessage(message, threadID, function(err, messageInfo){ 21 | if(err && err.errorDescription === "Please try closing and re-opening your browser window."){ 22 | console.log("Re-logging..."); 23 | api.logout(function(err){ 24 | console.log("Logged out", err); 25 | module.exports.connect(credentials, function(api){ 26 | console.log("Done re-logging."); 27 | api.sendMessage(text, message.thread_id, callback); 28 | }, messageCallback); 29 | }); 30 | } 31 | if(typeof callback === 'function') 32 | callback.apply(api, arguments); 33 | }); 34 | }catch(e){ 35 | if(typeof callback === 'function') 36 | callback(e); 37 | } 38 | }; 39 | 40 | api.type = "messenger"; 41 | api.userid = api.getCurrentUserID(); 42 | readyCallback(api); 43 | 44 | api.listen(function(err, message){ 45 | if(err) return console.error(err); 46 | message.body = message.body || ""; 47 | message.thread_id = message.threadID; 48 | console.log(message); 49 | 50 | if(message.isGroup === false) 51 | message.isAddressed = 2; // This is a PM 52 | 53 | if(message.mentions[api.userid]) 54 | message.isAddressed = 2; // This is a direct mention 55 | 56 | var reply = function(text, options, callback){ 57 | if(typeof options === "function"){ 58 | callback = options; 59 | options = {}; 60 | }else if(typeof options !== "object") 61 | options = {}; 62 | 63 | if(typeof text === "string") 64 | console.log("Responding to", message.thread_id, text); 65 | else{ 66 | console.log("Responding to", message.thread_id, "with an attachment"); 67 | 68 | // facebook-chat-api has the weirdest way of testing streams 69 | if(text.attachment 70 | && text.attachment instanceof stream.Stream 71 | && (typeof text.attachment._read !== 'Function' 72 | || typeof text.attachment._readableState !== 'Object')){ 73 | text.attachment._read = function(){}; 74 | text.attachment._readableState = {}; 75 | } 76 | } 77 | 78 | var delay = options.delay || 0; 79 | if(options.delay === undefined && typeof text === "string"){ 80 | delay = text.length * 100; 81 | // Subtract time since the original message was sent 82 | delay -= Date.now() - message.timestamp; 83 | // Ensure delay is non-negative and no more than 2 seconds 84 | delay = Math.min(Math.max(delay, 0), 2000); 85 | } 86 | 87 | if(delay > 0){ 88 | api.sendTypingIndicator(message.thread_id); 89 | console.log("Delaying response by " + delay + "ms"); 90 | } 91 | 92 | setTimeout(api.sendMessage.bind(api, text, message.thread_id, callback), delay); 93 | }; 94 | 95 | messageCallback(reply, message, api); 96 | }); 97 | }); 98 | }}; 99 | -------------------------------------------------------------------------------- /plugins/reddit.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | 3 | module.exports = {listeners: [ 4 | { 5 | type: "equals", 6 | query: ".minion", 7 | callback: function(reply, message, api){ 8 | redditImage(["MinionHate", "WackyTicTacs"], "http://i.imgur.com/L07nbhn.jpg", reply, message, api); 9 | } 10 | }, { 11 | type: "equals", 12 | query: ".corgi", 13 | callback: function(reply, message, api){ 14 | redditImage(["corgi", "BabyCorgis"], "http://i.imgur.com/djeivlK.gif", reply, message, api); 15 | } 16 | }, { 17 | type: "equals", 18 | query: ".doge", 19 | callback: function(reply, message, api){ 20 | redditImage(["doge", "supershibe"], "http://i.imgur.com/WMiliXD.jpg", reply, message, api); 21 | } 22 | }, { 23 | type: "regex", 24 | query: /.o?pos+um/i, 25 | callback: function(reply, message, api){ 26 | redditImage(["Possums"], "https://i.imgur.com/T0wMPRg.jpg", reply, message, api); 27 | } 28 | }, { 29 | type: "regex", 30 | query: /^\.(copy)?pasta$/i, 31 | callback: function(reply, message, api){ 32 | redditText(["copypasta", "emojipasta"], "What the fuck did you just fucking say about me, you little bitch? I'll have you know I graduated top of my class in the Navy Seals, and I've been involved in numerous secret raids on Al-Quaeda, and I have over 300 confirmed kills. I am trained in gorilla warfare and I'm the top sniper in the entire US armed forces. You are nothing to me but just another target. I will wipe you the fuck out with precision the likes of which has never been seen before on this Earth, mark my fucking words. You think you can get away with saying that shit to me over the Internet? Think again, fucker. As we speak I am contacting my secret network of spies across the USA and your IP is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. You're fucking dead, kid. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and that's just with my bare hands. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the United States Marine Corps and I will use it to its full extent to wipe your miserable ass off the face of the continent, you little shit. If only you could have known what unholy retribution your little \"clever\" comment was about to bring down upon you, maybe you would have held your fucking tongue. But you couldn't, you didn't, and now you're paying the price, you goddamn idiot. I will shit fury all over you and you will drown in it. You're fucking dead, kiddo.", reply, message, api); 33 | } 34 | } 35 | ]}; 36 | 37 | function redditAPI(subs, callback){ 38 | var sub = subs[Math.floor(Math.random() * subs.length)]; 39 | var url = "https://www.reddit.com/r/" + sub + "/top/.json?sort=top&t=year&limit=100"; 40 | 41 | request(url, function(err, res, body){ 42 | if(err){ 43 | console.error("Request error for subreddit " + sub); 44 | console.error(err); 45 | callback("Server error"); 46 | } 47 | 48 | try{ 49 | var posts = JSON.parse(body).data.children; 50 | if(posts.length) 51 | return callback(null, posts); 52 | 53 | console.error("No posts for subreddit " + sub); 54 | console.error(body); 55 | callback("No posts returned"); 56 | }catch(e){ 57 | console.error(e); 58 | callback(e); 59 | } 60 | }); 61 | } 62 | 63 | function redditImage(subs, fail, reply, message, api){ 64 | if(api.type === "messenger") 65 | api.sendTypingIndicator(message.thread_id); 66 | 67 | redditAPI(subs, function(err, posts){ 68 | if(err) 69 | return reply({attachment: request(fail)}); 70 | 71 | try{ 72 | var index = Math.floor(Math.random() * posts.length); 73 | var attempts = 0; 74 | while(!posts[index].data.url || ( 75 | posts[index].data.url.indexOf("i.imgur.com") === -1 && 76 | posts[index].data.url.indexOf("i.redd.it") === -1 && 77 | posts[index].data.url.indexOf("i.redditmedia.com") === -1 78 | )){ 79 | index = Math.floor(Math.random() * posts.length); 80 | if(++attempts > 50) 81 | return reply({attachment: request(fail)}); 82 | } 83 | var post = posts[index].data.url; 84 | console.log(post); 85 | 86 | if(api.type === "messenger"){ 87 | var image = request(post); 88 | 89 | if(image) 90 | reply({attachment: image}); 91 | else 92 | reply({attachment: request(fail)}); 93 | }else 94 | reply(post); 95 | }catch(e){ 96 | console.error(e); 97 | reply({attachment: request(fail)}); 98 | } 99 | }); 100 | } 101 | 102 | function redditText(subs, fail, reply, message, api){ 103 | if(api.type === "messenger") 104 | api.sendTypingIndicator(message.thread_id); 105 | 106 | redditAPI(subs, function(err, posts){ 107 | if(err) 108 | return reply(fail); 109 | 110 | try{ 111 | var index = Math.floor(Math.random() * posts.length); 112 | var attempts = 0; 113 | while(posts[index].data.selftext.length < 200){ 114 | index = Math.floor(Math.random() * posts.length); 115 | if(++attempts > 50) 116 | return reply(fail); 117 | } 118 | reply(posts[index].data.selftext); 119 | }catch(e){ 120 | console.error(e); 121 | reply(fail); 122 | } 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /plugins/zalgo.js: -------------------------------------------------------------------------------- 1 | module.exports = {listeners: [ 2 | { 3 | type: "startsWith", 4 | query: ".zalgo", 5 | callback: function(reply, message){ 6 | var split = message.body.split(" "); 7 | if(split.length === 1) 8 | return reply(zalgo("HE COMES")); 9 | 10 | split.shift(); 11 | reply(zalgo(split.join(" ").trim())); 12 | } 13 | } 14 | ]}; 15 | 16 | 17 | //============================================================ 18 | // ZALGO text script by tchouky 19 | //============================================================ 20 | 21 | // data set of leet unicode chars 22 | //--------------------------------------------------- 23 | 24 | //those go UP 25 | var zalgo_up = [ 26 | '\u030d', /* ̍ */ '\u030e', /* ̎ */ '\u0304', /* ̄ */ '\u0305', /* ̅ */ 27 | '\u033f', /* ̿ */ '\u0311', /* ̑ */ '\u0306', /* ̆ */ '\u0310', /* ̐ */ 28 | '\u0352', /* ͒ */ '\u0357', /* ͗ */ '\u0351', /* ͑ */ '\u0307', /* ̇ */ 29 | '\u0308', /* ̈ */ '\u030a', /* ̊ */ '\u0342', /* ͂ */ '\u0343', /* ̓ */ 30 | '\u0344', /* ̈́ */ '\u034a', /* ͊ */ '\u034b', /* ͋ */ '\u034c', /* ͌ */ 31 | '\u0303', /* ̃ */ '\u0302', /* ̂ */ '\u030c', /* ̌ */ '\u0350', /* ͐ */ 32 | '\u0300', /* ̀ */ '\u0301', /* ́ */ '\u030b', /* ̋ */ '\u030f', /* ̏ */ 33 | '\u0312', /* ̒ */ '\u0313', /* ̓ */ '\u0314', /* ̔ */ '\u033d', /* ̽ */ 34 | '\u0309', /* ̉ */ '\u0363', /* ͣ */ '\u0364', /* ͤ */ '\u0365', /* ͥ */ 35 | '\u0366', /* ͦ */ '\u0367', /* ͧ */ '\u0368', /* ͨ */ '\u0369', /* ͩ */ 36 | '\u036a', /* ͪ */ '\u036b', /* ͫ */ '\u036c', /* ͬ */ '\u036d', /* ͭ */ 37 | '\u036e', /* ͮ */ '\u036f', /* ͯ */ '\u033e', /* ̾ */ '\u035b', /* ͛ */ 38 | '\u0346', /* ͆ */ '\u031a' /* ̚ */ 39 | ]; 40 | 41 | //those go DOWN 42 | var zalgo_down = [ 43 | '\u0316', /* ̖ */ '\u0317', /* ̗ */ '\u0318', /* ̘ */ '\u0319', /* ̙ */ 44 | '\u031c', /* ̜ */ '\u031d', /* ̝ */ '\u031e', /* ̞ */ '\u031f', /* ̟ */ 45 | '\u0320', /* ̠ */ '\u0324', /* ̤ */ '\u0325', /* ̥ */ '\u0326', /* ̦ */ 46 | '\u0329', /* ̩ */ '\u032a', /* ̪ */ '\u032b', /* ̫ */ '\u032c', /* ̬ */ 47 | '\u032d', /* ̭ */ '\u032e', /* ̮ */ '\u032f', /* ̯ */ '\u0330', /* ̰ */ 48 | '\u0331', /* ̱ */ '\u0332', /* ̲ */ '\u0333', /* ̳ */ '\u0339', /* ̹ */ 49 | '\u033a', /* ̺ */ '\u033b', /* ̻ */ '\u033c', /* ̼ */ '\u0345', /* ͅ */ 50 | '\u0347', /* ͇ */ '\u0348', /* ͈ */ '\u0349', /* ͉ */ '\u034d', /* ͍ */ 51 | '\u034e', /* ͎ */ '\u0353', /* ͓ */ '\u0354', /* ͔ */ '\u0355', /* ͕ */ 52 | '\u0356', /* ͖ */ '\u0359', /* ͙ */ '\u035a', /* ͚ */ '\u0323' /* ̣ */ 53 | ]; 54 | 55 | //those always stay in the middle 56 | var zalgo_mid = [ 57 | '\u0315', /* ̕ */ '\u031b', /* ̛ */ '\u0340', /* ̀ */ '\u0341', /* ́ */ 58 | '\u0358', /* ͘ */ '\u0321', /* ̡ */ '\u0322', /* ̢ */ '\u0327', /* ̧ */ 59 | '\u0328', /* ̨ */ '\u0334', /* ̴ */ '\u0335', /* ̵ */ '\u0336', /* ̶ */ 60 | '\u034f', /* ͏ */ '\u035c', /* ͜ */ '\u035d', /* ͝ */ '\u035e', /* ͞ */ 61 | '\u035f', /* ͟ */ '\u0360', /* ͠ */ '\u0362', /* ͢ */ '\u0338', /* ̸ */ 62 | '\u0337', /* ̷ */ '\u0361', /* ͡ */ '\u0489' /* ҉_ */ 63 | ]; 64 | 65 | // rand funcs 66 | //--------------------------------------------------- 67 | 68 | //gets an int between 0 and max 69 | function rand(max) 70 | { 71 | return Math.floor(Math.random() * max); 72 | } 73 | 74 | //gets a random char from a zalgo char table 75 | function rand_zalgo(array) 76 | { 77 | var ind = Math.floor(Math.random() * array.length); 78 | return array[ind]; 79 | } 80 | 81 | //lookup char to know if its a zalgo char or not 82 | function is_zalgo_char(c) 83 | { 84 | var i; 85 | for(i=0; i