def initialize(botclass, params = {})
BotConfig.register BotConfigStringValue.new('server.name',
:default => "localhost", :requires_restart => true,
:desc => "What server should the bot connect to?",
:wizard => true)
BotConfig.register BotConfigIntegerValue.new('server.port',
:default => 6667, :type => :integer, :requires_restart => true,
:desc => "What port should the bot connect to?",
:validate => Proc.new {|v| v > 0}, :wizard => true)
BotConfig.register BotConfigStringValue.new('server.password',
:default => false, :requires_restart => true,
:desc => "Password for connecting to this server (if required)",
:wizard => true)
BotConfig.register BotConfigStringValue.new('server.bindhost',
:default => false, :requires_restart => true,
:desc => "Specific local host or IP for the bot to bind to (if required)",
:wizard => true)
BotConfig.register BotConfigIntegerValue.new('server.reconnect_wait',
:default => 5, :validate => Proc.new{|v| v >= 0},
:desc => "Seconds to wait before attempting to reconnect, on disconnect")
BotConfig.register BotConfigFloatValue.new('server.sendq_delay',
:default => 2.0, :validate => Proc.new{|v| v >= 0},
:desc => "(flood prevention) the delay between sending messages to the server (in seconds)",
:on_change => Proc.new {|bot, v| bot.socket.sendq_delay = v })
BotConfig.register BotConfigIntegerValue.new('server.sendq_burst',
:default => 4, :validate => Proc.new{|v| v >= 0},
:desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines",
:on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
BotConfig.register BotConfigStringValue.new('server.byterate',
:default => "400/2", :validate => Proc.new{|v| v.match(/\d+\/\d/)},
:desc => "(flood prevention) max bytes/seconds rate to send the server. Most ircd's have limits of 512 bytes/2 seconds",
:on_change => Proc.new {|bot, v| bot.socket.byterate = v })
BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
:default => 30, :validate => Proc.new{|v| v >= 0},
:on_change => Proc.new {|bot, v| bot.start_server_pings},
:desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
BotConfig.register BotConfigStringValue.new('irc.nick', :default => "rbot",
:desc => "IRC nickname the bot should attempt to use", :wizard => true,
:on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" })
BotConfig.register BotConfigStringValue.new('irc.user', :default => "rbot",
:requires_restart => true,
:desc => "local user the bot should appear to be", :wizard => true)
BotConfig.register BotConfigArrayValue.new('irc.join_channels',
:default => [], :wizard => true,
:desc => "What channels the bot should always join at startup. List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'")
BotConfig.register BotConfigArrayValue.new('irc.ignore_users',
:default => [],
:desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people")
BotConfig.register BotConfigIntegerValue.new('core.save_every',
:default => 60, :validate => Proc.new{|v| v >= 0},
:desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)")
BotConfig.register BotConfigBooleanValue.new('core.run_as_daemon',
:default => false, :requires_restart => true,
:desc => "Should the bot run as a daemon?")
BotConfig.register BotConfigStringValue.new('log.file',
:default => false, :requires_restart => true,
:desc => "Name of the logfile to which console messages will be redirected when the bot is run as a daemon")
BotConfig.register BotConfigIntegerValue.new('log.level',
:default => 1, :requires_restart => false,
:validate => Proc.new { |v| (0..5).include?(v) },
:on_change => Proc.new { |bot, v|
$logger.level = v
},
:desc => "The minimum logging level (0=DEBUG,1=INFO,2=WARN,3=ERROR,4=FATAL) for console messages")
BotConfig.register BotConfigIntegerValue.new('log.keep',
:default => 1, :requires_restart => true,
:validate => Proc.new { |v| v >= 0 },
:desc => "How many old console messages logfiles to keep")
BotConfig.register BotConfigIntegerValue.new('log.max_size',
:default => 10, :requires_restart => true,
:validate => Proc.new { |v| v > 0 },
:desc => "Maximum console messages logfile size (in megabytes)")
@argv = params[:argv]
unless FileTest.directory? Config::datadir
error "data directory '#{Config::datadir}' not found, did you setup.rb?"
exit 2
end
unless botclass and not botclass.empty?
if Etc.getpwuid(Process::Sys.geteuid)
botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup
else
if ENV.has_key?('APPDATA')
botclass = ENV['APPDATA'].dup
botclass.gsub!("\\","/")
end
end
botclass += "/.rbot"
end
botclass = File.expand_path(botclass)
@botclass = botclass.gsub(/\/$/, "")
unless FileTest.directory? botclass
log "no #{botclass} directory found, creating from templates.."
if FileTest.exist? botclass
error "file #{botclass} exists but isn't a directory"
exit 2
end
FileUtils.cp_r Config::datadir+'/templates', botclass
end
Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
Dir.mkdir("#{botclass}/registry") unless File.exist?("#{botclass}/registry")
@ping_timer = nil
@pong_timer = nil
@last_ping = nil
@startup_time = Time.new
@config = BotConfig.new(self)
if @config['core.run_as_daemon']
$daemonize = true
end
@logfile = @config['log.file']
if @logfile.class!=String || @logfile.empty?
@logfile = "#{botclass}/#{File.basename(botclass).gsub(/^\.+/,'')}.log"
end
if $daemonize
begin
exit if fork
Process.setsid
exit if fork
rescue NotImplementedError
warning "Could not background, fork not supported"
rescue => e
warning "Could not background. #{e.inspect}"
end
Dir.chdir botclass
log "Redirecting standard input/output/error"
begin
STDIN.reopen "/dev/null"
rescue Errno::ENOENT
STDIN.reopen "NUL"
end
def STDOUT.write(str=nil)
log str, 2
return str.to_s.length
end
def STDERR.write(str=nil)
if str.to_s.match(/:\d+: warning:/)
warning str, 2
else
error str, 2
end
return str.to_s.length
end
end
$logger = Logger.new(@logfile, @config['log.keep'], @config['log.max_size']*1024*1024)
$logger.datetime_format= $dateformat
$logger.level = @config['log.level']
$logger.level = $cl_loglevel if $cl_loglevel
$logger.level = 0 if $debug
log_session_start
@timer = Timer::Timer.new(1.0)
@registry = BotRegistry.new self
@save_mutex = Mutex.new
@timer.add(@config['core.save_every']) { save } if @config['core.save_every']
@channels = Hash.new
@logs = Hash.new
@httputil = Utils::HttpUtil.new(self)
@lang = Language::Language.new(@config['core.language'])
begin
@auth = IrcAuth.new(self)
rescue => e
fatal e.inspect
fatal e.backtrace.join("\n")
log_session_end
exit 2
end
Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
@plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"])
@socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
@nick = @config['irc.nick']
@client = IrcClient.new
@client[:isupport] = proc { |data|
if data[:capab]
sendq "CAPAB IDENTIFY-MSG"
end
}
@client[:datastr] = proc { |data|
debug data.inspect
if data[:text] == "IDENTIFY-MSG"
@capabilities["identify-msg".to_sym] = true
else
debug "Not handling RPL_DATASTR #{data[:servermessage]}"
end
}
@client[:privmsg] = proc { |data|
message = PrivMessage.new(self, data[:source], data[:target], data[:message])
onprivmsg(message)
}
@client[:notice] = proc { |data|
message = NoticeMessage.new(self, data[:source], data[:target], data[:message])
@plugins.delegate "listen", message
}
@client[:motd] = proc { |data|
data[:motd].each_line { |line|
irclog "MOTD: #{line}", "server"
}
}
@client[:nicktaken] = proc { |data|
nickchg "#{data[:nick]}_"
@plugins.delegate "nicktaken", data[:nick]
}
@client[:badnick] = proc {|data|
warning "bad nick (#{data[:nick]})"
}
@client[:ping] = proc {|data|
@socket.queue "PONG #{data[:pingid]}"
}
@client[:pong] = proc {|data|
@last_ping = nil
}
@client[:nick] = proc {|data|
sourcenick = data[:sourcenick]
nick = data[:nick]
m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick])
if(sourcenick == @nick)
debug "my nick is now #{nick}"
@nick = nick
end
@channels.each {|k,v|
if(v.users.has_key?(sourcenick))
irclog "@ #{sourcenick} is now known as #{nick}", k
v.users[nick] = v.users[sourcenick]
v.users.delete(sourcenick)
end
}
@plugins.delegate("listen", m)
@plugins.delegate("nick", m)
}
@client[:quit] = proc {|data|
source = data[:source]
sourcenick = data[:sourcenick]
sourceurl = data[:sourceaddress]
message = data[:message]
m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
else
@channels.each {|k,v|
if(v.users.has_key?(sourcenick))
irclog "@ Quit: #{sourcenick}: #{message}", k
v.users.delete(sourcenick)
end
}
end
@plugins.delegate("listen", m)
@plugins.delegate("quit", m)
}
@client[:mode] = proc {|data|
source = data[:source]
sourcenick = data[:sourcenick]
sourceurl = data[:sourceaddress]
channel = data[:channel]
targets = data[:targets]
modestring = data[:modestring]
irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
}
@client[:welcome] = proc {|data|
irclog "joined server #{data[:source]} as #{data[:nick]}", "server"
debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
if data[:nick] && data[:nick].length > 0
@nick = data[:nick]
end
@plugins.delegate("connect")
@config['irc.join_channels'].each {|c|
debug "autojoining channel #{c}"
if(c =~ /^(\S+)\s+(\S+)$/i)
join $1, $2
else
join c if(c)
end
}
}
@client[:join] = proc {|data|
m = JoinMessage.new(self, data[:source], data[:channel], data[:message])
onjoin(m)
}
@client[:part] = proc {|data|
m = PartMessage.new(self, data[:source], data[:channel], data[:message])
onpart(m)
}
@client[:kick] = proc {|data|
m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
onkick(m)
}
@client[:invite] = proc {|data|
if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
end
}
@client[:changetopic] = proc {|data|
channel = data[:channel]
sourcenick = data[:sourcenick]
topic = data[:topic]
timestamp = data[:unixtime] || Time.now.to_i
if(sourcenick == @nick)
irclog "@ I set topic \"#{topic}\"", channel
else
irclog "@ #{sourcenick} set topic \"#{topic}\"", channel
end
m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
ontopic(m)
@plugins.delegate("listen", m)
@plugins.delegate("topic", m)
}
@client[:topic] = @client[:topicinfo] = proc {|data|
channel = data[:channel]
m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic])
ontopic(m)
}
@client[:names] = proc {|data|
channel = data[:channel]
users = data[:users]
unless(@channels[channel])
warning "got names for channel '#{channel}' I didn't think I was in\n"
end
@channels[channel].users.clear
users.each {|u|
@channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
}
@plugins.delegate "names", data[:channel], data[:users]
}
@client[:unknown] = proc {|data|
irclog data[:serverstring], ".unknown"
}
end