require "rubygems"
require "htmlentities"
require "open-uri"
require "timeout"
require "socket"
require "open4"
require "json"
require "yaml"
require "cgi"
def run(s, maxt = 10*60, errorIsOnStdout=false)
begin
pid, cmdin, cmdout, cmderr = Open4.popen4(s)
rescue Exception => e
$stderr << 'Execution of '+s+' has failed :' + e.to_s
raise "run failed"
end
begin
Timeout::timeout(maxt) do |t|
a = Process.waitpid2(pid)
if a[1].exitstatus != 0
$stderr << 'Error while executing ' + s
errorMessage = (errorIsOnStdout ? cmdout.read : cmderr.read)
raise errorMessage.gsub(/\n/, ' ')
end
end
rescue Timeout::Error => e
$stderr << 'Execution of '+s+' has timed out. Killing subprocess'
begin
Process.kill('KILL', pid)
cmdout.close; cmdin.close; cmderr.close
rescue Object => e
$stderr << 'Failed killing process : ' + e.to_s
end
raise "timeout"
end
out = cmdout.read
cmdout.close; cmdin.close; cmderr.close
out
end
class IRC
attr_reader :server, :port, :nick, :channel, :count
V8 = 'v8'
JSC = 'jsc'
RHINO = 'rhino'
ALL = '__ALL__'
MOZ = '__MOZ__'
SPIDERMONKEY = 'spidermonkey'
PASSWORD_FILE = ".password"
COMMANDS_FILE = "commands.yaml"
INTERPRETERS_FILE = "interpreters.yaml"
SEARCH_PREFIX = "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&hl=en&rsz=small&safe=active&q="
def initialize(server, port, nick, nickpassword, channels)
@server, @port, @nick, @nickpassword, @channels, @count = server, port, nick, nickpassword, channels, 0
@entitydecoder = HTMLEntities.new
@admin_password = nil
@interpreters = nil
@tmp_js_file = nil
@commands = nil
@timeout = nil
load_password
load_commands
load_interpreter_info
end
private
def load_password
password = File.read(PASSWORD_FILE)
if password == "password"
puts "Please Change Password"
puts "See the '.password' file."
exit 1
else
@admin_password = password
puts "ADMIN PASSWORD: #{password}"
puts
end
end
def load_interpreter_info
@interpreters = YAML.load_file(INTERPRETERS_FILE)
@tmp_js_file = @interpreters['tmpfile']
@timeout = @interpreters['timeout']
@interpreters[ALL] = ALL
end
def load_commands
@commands = YAML.load_file(COMMANDS_FILE)
end
def save_commands
File.open(COMMANDS_FILE, "w") { |f| f.puts @commands.to_yaml }
end
def add_command(term, value)
@commands[term] = value
save_commands
end
def remove_command(term)
@commands.delete(term)
save_commands
end
public
def silent_send(s)
@irc.send "#{s}\n", 0
end
def send(s, bool=false)
if bool
@count += 1
puts "[#{@count}] --> #{s}"
else
puts "--> #{s}"
end
@irc.send "#{s}\n", 0
end
def msg(s)
send "PRIVMSG #{@channels.first} :#{s}"
end
def connect()
@irc = TCPSocket.open(@server, @port)
send "USER bot JoePeckBot bogojoker.com :Joseph Pecoraro (BOT)"
send "NICK #{@nick}"
@channels.each { |channel| send "JOIN #{channel}" }
send "PRIVMSG NickServ :identify #{@nickpassword}" unless @nickpassword == "-"
end
def parsedmsg(personSending, channel, str, to='')
out = (channel==@nick) ? personSending : channel
msg = (to.nil?) ? str : "#{to}: #{str}"
send "PRIVMSG #{out} :#{msg}", true
end
def lucky(personSending, channel, str, to='')
begin
url = "#{SEARCH_PREFIX}#{CGI.escape(str)}"
json = JSON.parse( open(url).read )
results = json['responseData']['results']
if results.size.zero?
parsedmsg(personSending, channel, "No Results", to)
else
firstResult = results[0]
title = @entitydecoder.decode( firstResult['titleNoFormatting'] )
href = firstResult['unescapedUrl']
result = "#{title} - #{href}"
parsedmsg(personSending, channel, result, to)
end
rescue
parsedmsg(personSending, channel, "No Results (Error)", to)
end
end
def admin(personSending, password, cmd, term, value)
if password != @admin_password
send "PRIVMSG #{personSending} :Bad Password"
return
end
confirm = ""
if cmd =~ /add/i
if value.nil?
confirm = "Forgot Value!"
else
add_command(term, value)
confirm = "ADDED TERM #{term}"
end
elsif cmd =~ /remove/i
remove_command(term)
confirm = "REMOVED TERM #{term}"
end
send "PRIVMSG #{personSending} :#{confirm}"
end
def parse_cmd(cmd, terms)
result = @commands[cmd]
return nil if result.nil?
if terms.nil?
return result.gsub("$1", '')
else
return result.gsub("$1", CGI.escape(terms))
end
end
def jseval(engine, personSending, channel, code, to)
puts "<-- CODE: #{code}"
code = code.gsub(/\\/, '\\\\\\\\').gsub(/"/, '\\"')
modded_code = "
// Security
(function() {
var whitelist = ['version', 'print'];
for (var i in this)
if (whitelist.indexOf(i) === -1)
delete this[i];
})();
(function(load,readline,help,quit,gc,gcParam,trap,untrap,clear,sleep,snarf,timeout,elapsed) {
// Pretty Printing functions courtesy of inimino
// Source: http://boshi.inimino.org/3box/3box/util.js
function pp(o,depth){return pp_r(o,depth==undefined?8:depth)}
function pp_r(o,d){var a=[],p
if(!d)return '...'
if(o===undefined)return 'undefined'
if(o===null)return 'null'
if(typeof o=='boolean')return o.toString()
if(typeof o=='string')return '\"'+o.replace(/\\n/g,'\\\\n').replace(/\"/g,'\\\\\"')+'\"'
if(typeof o=='number')return o.toString()
if(o instanceof RegExp)return '/'+o.source+'/'
if(typeof o=='function')return '<'+o.toString().replace(/(.{32}).*/,'$1\\u2026').replace(/\\s+/g,' ')+'>'
if(typeof o=='xml')return o.toXMLString()
if(o.constructor==Array){
o.forEach(function(e,i){
a.push(pp_r(o[i],d-1))})
return '['+a.join(',')+']'}
if(o.constructor==Date){
return o.toString()}
for(p in o) if(Object.prototype.hasOwnProperty.call(o,p))
a.push(p+':'+pp_r(o[p],d-1))
return '{'+a.join(',')+'}'}
// Run the Code and Pretty Print it
var res = eval(\"#{code}\");
print(pp(res));
})();
"
File.open(@tmp_js_file, 'w') { |f| f.write(modded_code) }
engines = [engine]
prefixes = ['']
if (engine == ALL)
engines = [JSC, V8, RHINO, SPIDERMONKEY]
prefixes = engines.collect { |e| e + ': ' }
elsif (engine == MOZ)
engines = [RHINO, SPIDERMONKEY]
prefixes = engines.collect { |e| e + ': ' }
end
engines.each_with_index do |engine, index|
interpreter = @interpreters[engine]
pre = prefixes[index];
next if interpreter.nil? or interpreter.empty?
begin
timeout = (engine == RHINO ? @timeout+3 : @timeout)
res = run("#{interpreter} -f #{@tmp_js_file}", timeout, (engine == V8));
lines = res.split(/[\n\r]+/)
res = (lines.size <= 1 ? lines[0] : lines[0..lines.size-2].join(''))
res = res[0..200] + '...' if res.size > 200
parsedmsg(personSending, channel, pre+res, to)
rescue RuntimeError => e
if e.message =~ /timeout/
parsedmsg(personSending, channel, pre+"Timeout.", to)
else
errorMessage = e.message.sub(/^.*?\d+.*?\s/, '')
errorMessage.gsub!(/line 1: uncaught JavaScript runtime exception:/, '')
errorMessage.gsub!(/\/tmp\/jsevalbot.js#\d+\(eval\)/, '')
errorMessage.gsub!(/\/tmp\/jsevalbot.js:\d+:/, '')
errorMessage = errorMessage[0..200]
errorMessage = '[interpreter shows no error messages]' if errorMessage.size == 0
parsedmsg(personSending, channel, pre+"Error: "+errorMessage, to)
end
rescue Object => e
p e
parsedmsg(personSending, channel, pre+"Unhandled Error, Please Contact JoePeck.", to)
end
end
end
def jsc(a,b,c,d); jseval(JSC,a,b,c,d); end
def v8(a,b,c,d); jseval(V8,a,b,c,d); end
def rhino(a,b,c,d); jseval(RHINO,a,b,c,d); end
def spidermonkey(a,b,c,d); jseval(SPIDERMONKEY,a,b,c,d); end
def all(a,b,c,d); jseval(ALL,a,b,c,d); end
def moz(a,b,c,d); jseval(MOZ,a,b,c,d); end
def handle_server_input(s)
case s.strip
when /^PING :(.+)$/i
silent_send "PONG :#{$1}"
when /JOIN :(.+)$/i
puts "<-- JOINED: #{$1}"
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s.+\s:[\001]PING (.+)[\001]$/i
send "NOTICE #{$1} :\001PING #{$4}\001"
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s.+\s:[\001]VERSION[\001]$/i
send "NOTICE #{$1} :\001VERSION Ruby-irc v0.042\001"
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s(.+)\s:`(list|commands)$/i
parsedmsg($1, $1, @commands.keys.sort.join(', '), $7)
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s(.+)\s:(.*?)[>:]\s+(.+?)$/i
case $5
when "jsc" then jsc($1, $4, $6, $1)
when "r", "rhino" then rhino($1, $4, $6, $1)
when "sm", "spider", "js" then spidermonkey($1, $4, $6, $1)
when "v8" then v8($1, $4, $6, $1)
when "all" then all($1, $4, $6, $1)
when "moz" then moz($1, $4, $6, $1)
when "strict" then strict($1, $4, $6, $1)
end
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s(.+)\s:`(?:g|lucky)\s+(.+?)(\s*@\s*(\S+))?$/i
lucky($1, $4, $5, $7)
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s(.+)\s:`admin\s+(.*?)\s+(ADD|REMOVE)\s+(\S+)(?:\s+(.*?))?$/i
admin($1, $5, $6, $7, $8)
when /^:(.+?)!(.+?)@(.+?)\sPRIVMSG\s(.+)\s:`(\S+)(?:\s+(.*?))?(\s*@\s*(\S+))?$/i
result = parse_cmd($5, $6)
parsedmsg($1, $4, result, $8) unless result.nil?
else
puts s.strip
end
true
end
def main_loop()
while true
ready = select([@irc,$stdin], nil, nil, nil)
next if !ready
for s in ready[0]
if s == $stdin then
return if $stdin.eof
s = $stdin.gets
msg s
elsif s == @irc then
return if @irc.eof
s = @irc.gets
unless handle_server_input(s)
@irc.close
return
end
end
end
end
end
end
if ARGV.size < 5
puts "usage: jsircbot server port nick password channels..."
puts "example: jsircbot irc.freenode.net 6667 JoePeckBotTest NickServPassword #JoePeck ##javascript"
puts
puts " To NOT send a NickServ password, provide a - as your password."
puts
puts " Channels must be prefixed with their hash symbol. You may need to"
puts " escape them in your shell, for example: \\#\\#javascript"
else
server, port, nick, nickpassword = ARGV
channels = ARGV.drop(4)
puts "Connecting to: #{server}:#{port}"
puts "Nickname: #{nick}"
puts "Channels: #{channels.join(' ')}"
puts
irc = IRC.new(server, port, nick, nickpassword, channels)
irc.connect()
irc.main_loop()
end