Skip to content

Instantly share code, notes, and snippets.

@boxmein
Last active December 15, 2015 15:18
Show Gist options
  • Save boxmein/5280207 to your computer and use it in GitHub Desktop.
Save boxmein/5280207 to your computer and use it in GitHub Desktop.
A small IRC bot which has a weird command syntax, no helps or command names, and it's quick. :D
/*jslint node: true */
/*
A really small and lightweight IRC bot
Made from pieces of NodeIRCBot (http://github.com/boxmein/NodeIRCBot/)
Features:
1. Commands are numbered, but have a prefix character
2. Output logging levels
3. Sandboxed Javascript handling (1)
*/
// A startup message with the current working directory for require() reference
console.log("-- working directory: " + process.cwd());
// Libraries required:
// net - Networking, the connection to the IRC serer
// sandbox - Sandboxed Javascript interpreting
// http - HTTP requests
var net = require("net"), http = require('http'), Sandbox = require("sandbox"), sand = new Sandbox();
var relays = [], relaycount = 0;
/*
Configuration variables
rawlogging (false): will also display every raw IRC command sent to it
sendlogging (true): will display every raw IRC command it sends
realname (magic): this name is sent as the IRC Real Name
nickname (boxmini): this is the bot's nickname
channels : the channels the bot joins. Separated by commas and no spaces!
*/
var config = {
rawlogging: false,
sendlogging: true,
nickname: "boxmini",
realname: "magikku",
channels: "##boxmein,##powder-bots,##jacob1"
};
/*
Commands array
Contains the commands addressed by index
input: ircdata (object) - contains lots of useful data, is created in connection.onData
return: the string to respond with, if any. Return false if nothing is to be told.
*/
var commands = [
// Pig latin [0]
function (ircdata) {
var endstring = ircdata.args.map(function (each) {
// Move the first character to the end
var chars = each.split('');
// - => spaces!
chars.map(function(i) {if (i === '-') return ' '; });
chars.push(chars.shift());
// Append "ay"
chars = chars.join('');
chars += "ay";
// Return value
return chars;
}).join(" ");
return endstring;
},
// Sandboxed Javascript [1]
function (ircdata) {
console.log("~~ doing Javascript: " + ircdata.saymsg);
sand.run("(function() {" + ircdata.saymsg + "})();", function (out) {
// Output longer than 200 chars will be truncated
if (out.result.length < 200) {
console.log(out.result);
respond(ircdata, out.result);
}
else {
console.log(out.result.slice(0,200)+"...");
respond(ircdata, out.result.slice(0, 200) + "...");
}
});
},
// It'll come in handy. [2]
function (ircdata) {
irc.action(ircdata.channel, "slaps Ristovski");
return false;
},
// Relaying (add) [3]
function (ircdata) {
// relaydir should look something like
// #channel1->#channel2
var relaydir = ircdata.args[0] || false;
if (!relaydir)
return false;
else {
var channels = relaydir.split("->");
channels.map(function(e) { return sanitizeChannel(e); });
relays[relaycount++] = {
from: channels[0],
to: channels[1]
};
return ''+relaycount-1;
}
},
// Relaying (remove) [4]
function (ircdata) {
var index;
try {
index = parseInt(ircdata.args[0], 10);
}
catch (err) {return err;}
if (relays[index])
delete relays[index];
return false;
},
// Reverse filter [5]
function (ircdata) {
var endstr = [];
ircdata.saymsg
.split('')
.map(function(e) {
endstr.unshift(e);
return e;
});
return endstr.join('');
},
// Echo command [6]
function (ircdata) {
respond(ircdata, ircdata.saymsg, true);
},
// Slap someone else [7]
function (ircdata) {
var slappee = ircdata.saymsg || false;
if (slappee)
irc.action(ircdata.channel, "slaps " + slappee);
}
// Increment every character by one [8] UNSAFE
/* function (ircdata) {
return ircdata.saymsg.split('').map(function (each) {
return String.fromCharCode(each.charCodeAt() + 1);
}).join('');
}*/
// Test the down status of a site [8] UNSAFE
/*function (ircdata) {
if (!ircdata.args[0]) return 0;
// Get the domain
var domain = ircdata.args.shift()
.replace(/^http:\/\//i,"")
.split("/") || false;
console.log("received domain: " + domain.join('/'));
if (!domain[0])
return "No domain specified ;_;";
return http.request({
host: domain[0],
path: (domain[1] || "/")
},
function(response) {
console.log("request fulfilled: status=" + response.statusCode);
if (response.statusCode == 200)
respond(ircdata,"Seems up (200)");
else
respond(ircdata, "{0}: something happened ):)".format(response.statusCode));
}).end();
}*/
];
/*
Connection object
Contains everything one needs to get connected, including handleCommands to do initial command
parsing and ircdata generation
*/
var connection = {
client: new net.Socket(),
PORT: 6667,
HOST: "irc.freenode.net",
onConnected: function() {
console.log("** Connection successful");
setTimeout(function() {
irc.raw("NICK " + config.nickname);
irc.raw("USER " + config.nickname + " 8 * :" + config.realname);
irc.raw("JOIN " + config.channels);
console.log("--- --- Finished startup --- ---");
}, 7000);
},
// Main command handler
// Input: object
/* {
hostmask: "sendernick!ident@irc-dns-blah.derpamerp.isp.co.nz",
channel: "#powder-bots",
sender: "sendernick",
message: "\<id> derp merp herp",
args: ["\<id>", "derp", "merp", "herp"],
// Added in this function
command: 5,
saymsg: "derp merp herp"
}
*/
handleCommands: function(ircdata) {
ircdata.command = ircdata.args.shift().substring(1).trim().replace(/[^0-9]+?/gi, "");
ircdata.saymsg = ircdata.args.join(" ");
try {
ircdata.command = parseInt(ircdata.command, 10); // parseInt implies integers
if (ircdata.command < commands.length && ircdata.command >= 0) {
var response = commands[ircdata.command](ircdata) || false;
if (response)
respond(ircdata, response);
}
}
catch (err) {
respond(ircdata, err);
}
},
onData: function(data) {
// :boxmein!boxmein@unaffiliated/boxmein PRIVMSG ##powder-bots :this is a test message
if(config.rawlogging)
console.log("<< "+data);
if (data.indexOf("PRIVMSG ") != -1) {
var arr = data.split(' ');
var result = arr.splice(0,3);
result.push(arr.join(' '));
// result = [":hostmask", "command", "channel", ":mess age"]
var ircdata = {
hostmask : result[0].substring(1),
channel : result[2],
sender : result[0].substring(1, result[0].indexOf("!")),
message : result[3].substring(1).trim()
// command: ['0']
// args: ['arg1', 'arg2', 'arg3']
// saymsg: "arg1 arg2 arg3"
// message: "\0 arg1 arg2 arg3"
// sender: "boxmein"
// hostmask: "boxmein!boxmein@unaffiliated/boxmein"
// channel: "##powder-bots"
};
var cmd;
// 3. Command handling
ircdata.args = ircdata.message.split(" ");
if (ircdata.message.indexOf("\\") === 0) { // Ristovski wanted to be ignored :(
//if (ircdata.message.indexOf("\\") == 0 && ircdata.hostmask.indexOf("Ristovski") === -1) {
console.log("~~ command get: " + ircdata.message);
connection.handleCommands(ircdata);
}
// 4. Command handling II: Durararararararara
else if (ircdata.message.indexOf("dura") === 0) {
cmd = ircdata.args.shift();
// args = ["argument1", "argument2"]
// cmd = "durararara" (dura + ra + ra + ra)
cmd = cmd.replace(/^dura/i, "\\");
// cmd = "\rarararara"
var duracount = 0;
try {
// Count the rest of the ra's
duracount = cmd.match(/ra/gi).length;
} catch(err) {}
cmd = cmd.replace(/ra/i, "");
// cmd = "\"; duracount = X;
console.log("cmd ended up: " + cmd);
cmd += duracount;
// cmd = "\X";
ircdata.args.unshift(cmd);
// args = ["\X", "argument1", "argument2", ...]
connection.handleCommands(ircdata);
}
// 4.1. Command handling III: ?!????!?!?!
else if (ircdata.message.indexOf("?") === 0) {
cmd = ircdata.args.shift().substring(1);
cmd = cmd.replace(/\?/g, "0");
cmd = cmd.replace(/!/g, "1");
var cmdcount;
try {
if (cmd === "")
cmd = "0";
cmdcount = parseInt(cmd, 2);
}
catch (err) { return; }
console.log("(?!): cmdcount ended up: " + cmdcount + "; cmd = " + cmd);
ircdata.args.unshift('\\'+cmdcount);
connection.handleCommands(ircdata);
}
/* // 4.2. Command handling IV: Mooooo
else if (ircdata.message.indexOf("?") === 0) {
cmd = ircdata.args.shift().substring(1);
cmd = cmd.replace(/\.\.\./g, "0");
cmd = cmd.replace(/moo/g, "1");
var cmdcount;
try {
cmdcount = parseInt(cmd, 2);
}
catch (err) { return; }
console.log("(moo:) cmdcount ended up: " + cmdcount + "; cmd = " + cmd);
ircdata.args.unshift('\\'+cmdcount);
connection.handleCommands(ircdata);
} // */
// 5. Relaying (channel bridging really)
for (var i in relays) {
if (!(relays[i].hasOwnProperty("from") && relays[i].hasOwnProperty("to")))
continue;
if (relays[i].from == ircdata.channel) {
irc.privmsg(relays[i].to, "<{0}/{1}> {2}".format(ircdata.channel, ircdata.sender, ircdata.message));
}
}
// 6. Special words
var responded = false;
ircdata.args.map(function(word) {
if (!responded) {
switch (word) {
case 'implement':
responded = true;
respond(ircdata, "You keep using that word. I do not think it means what you think it means.");
break;
case 'boxmini':
responded = true;
respond(ircdata, "Huh?");
break;
default: break;
}
}
});
}
else if(data.indexOf("PING") != -1) {
irc.raw("PONG " + (data.split(" ")[1] || ":00000000"), true);
}
},
onConnectionClose: function() {
console.log("** Disconnected. ");
process.exit(0);
},
onErr: function(evt) {
console.log(":( Error connecting: " + evt);
}
};
// A simple yet safe respond function
var respond = function(ircdata, message, nonick) {
console.log("-> {0}{1}: {2}{3}".format(irc.col.c(irc.col.navy), ircdata.sender, irc.col.c(irc.col.black, irc.col.black), message));
irc.privmsg(ircdata.channel, (nonick ? "" : ircdata.sender + ": ") + message);
};
// Overly attached sanitization.
var sanitizeChannel = function(query) {
return (''+query).trim().replace(/[^\w#-_]+?/gi, "");
};
// A bit better sanitization
var sanitize = function(query) {
return (''+query).trim().replace(/[^\w-_ !"#¤%&\/()]/gi, "");
}
// Here's the entry point for the bot. Out of all places, I know.
connection.client.setEncoding("ascii");
connection.client.setNoDelay();
connection.client.connect(connection.PORT, connection.HOST, connection.onConnected);
connection.client.on("data", connection.onData);
connection.client.on("close", connection.onConnectionClose);
connection.client.on("error", connection.onErr);
/*
Free to use IRC protocol wrapper functions
Feel free to contract that entire var since it contains nothing super interesting.
*/
var irc = {
// Sends a CTCP request to the channel or user. Used for ACTION and various CTCP requests.
// @param channel The user or channel to send a request to
// @param data The CTCP data to send. Includes the sub-command.
// Looks like "VERSION" or like "ACTION eats a vegetable"
ctcp: function(channel, data) {
irc.raw("PRIVMSG {0} :\x01{1}\x01".format(channel, data));
},
// Sets a channel mode. Will also work with bans/stuff since mode can contain a name.
// @param channel The channel that will receive the given mode.
// @param mode The mode being set. May either be "+o" or even "+b boxmein!*@*"
mode: function (channel, mode) {
irc.raw("MODE {0} {1}".format(channel, mode));
},
// Sends a NAMES request to the given channel to find out all about the users joined there.
// @param channel The channel to send NAMES to.
names: function (channel) {
irc.raw("NAMES " + channel);
},
// Sends a CTCP ACTION, the equivalent of /me in IRC clients.
// @param channel The channel to send the ACTION to. May also be an username.
// @param action The action to send.
action: function(channel, action) {
irc.ctcp(channel, "ACTION {0}".format(action));
},
// Sends a private message (also a channel message!)
// @param channel The given channel to send to. May also be an username.
// @param message The message that will be sent.
// @param hide Whether or not to hide the sent message. Gets passed on to irc.raw. May be left
// undefined.
privmsg: function(channel, message, hide) {
irc.raw("PRIVMSG {0} :{1}".format(channel, message), hide);
},
// Sends a NOTICE to the channel or user
// @param channel The channel or user to send the given message to.
// @param message The message being sent.
notice: function(channel, message) {
irc.raw("NOTICE {0} :{1}".format(channel, message));
},
// Joins a channel.
// @param channel The channel to join to
join: function(channel) {
irc.raw("JOIN {0}".format(channel));
},
// Removes self from the given channel. No leave messages.
// @param channel The channel to leave from
part: function(channel) {
irc.raw("PART {0}".format(channel));
},
// Quits the IRC. Sends a quit message and waits for the server for 5 seconds then quits
// the program.
quit: function() {
irc.raw("QUIT :Sayonara");
setTimeout(function() {
process.exit(0);
}, 5000);
},
// Sends raw data to the IRC server.
// @param data The data that is to be written, also known as raw IRC. Use wrappers instead :D
// @param hide Whether or not to hide the sending notification into the console.
raw: function(data, hide) {
connection.client.write(data + "\n", "ascii", function() {
if(!hide && config.sendlogging)
console.log(">> " + data);
});
},
// IRC Colours.
col: {
cchar: '\x03', // Control character => 0x03 or ^C.
beep: '\x07', // Bell character
clear: '\x0F', // Clear Formatting
bold: '\x02', // Bold character
italic: '\x15', // Italic formatting
uline: '\x1E', // Underlines
white: 0, // Note: may be interpreted differently
black: 1,
navy: 2,
green: 3,
red: 4,
brown: 5,
purple: 6,
orange: 7,
yellow: 8,
lime: 9,
teal: 10,
cyan: 11,
lblue: 12,
pink: 13,
grey: 14,
lgrey: 15,
c: function(col, bgcol) {
return this.cchar + col + (bgcol? "," + bgcol : '');
}
}
};
/*
Utility functions courtesy of dearly beloved Stack Overflow
<3
*/
// woo stackoverflow
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, "");
};
if (!String.prototype.format) {
String.prototype.format = function() {
var args = arguments;
return this.replace(/{(\d+)}/g, function(match, number) {
return typeof args[number] != 'undefined' ?
args[number] :
match;
});
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment