def initialize(botclass, params = {})
Config.register Config::ArrayValue.new('server.list',
:default => ['irc://localhost'], :wizard => true,
:requires_restart => true,
:desc => "List of irc servers rbot should try to connect to. Use comma to separate values. Servers are in format 'server.doma.in:port'. If port is not specified, default value (6667) is used.")
Config.register Config::BooleanValue.new('server.ssl',
:default => false, :requires_restart => true, :wizard => true,
:desc => "Use SSL to connect to this server?")
Config.register Config::StringValue.new('server.password',
:default => false, :requires_restart => true,
:desc => "Password for connecting to this server (if required)",
:wizard => true)
Config.register Config::StringValue.new('server.bindhost',
:default => false, :requires_restart => true,
:desc => "Specific local host or IP for the bot to bind to (if required)",
:wizard => true)
Config.register Config::IntegerValue.new('server.reconnect_wait',
:default => 5, :validate => Proc.new{|v| v >= 0},
:desc => "Seconds to wait before attempting to reconnect, on disconnect")
Config.register Config::IntegerValue.new('server.ping_timeout',
:default => 30, :validate => Proc.new{|v| v >= 0},
:desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
Config.register Config::ArrayValue.new('server.nocolor_modes',
:default => ['c'], :wizard => false,
:requires_restart => false,
:desc => "List of channel modes that require messages to be without colors")
Config.register Config::StringValue.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}" })
Config.register Config::StringValue.new('irc.name',
:default => "Ruby bot", :requires_restart => true,
:desc => "IRC realname the bot should use")
Config.register Config::BooleanValue.new('irc.name_copyright',
:default => true, :requires_restart => true,
:desc => "Append copyright notice to bot realname? (please don't disable unless it's really necessary)")
Config.register Config::StringValue.new('irc.user', :default => "rbot",
:requires_restart => true,
:desc => "local user the bot should appear to be", :wizard => true)
Config.register Config::ArrayValue.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'")
Config.register Config::ArrayValue.new('irc.ignore_users',
:default => [],
:desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people")
Config.register Config::IntegerValue.new('core.save_every',
:default => 60, :validate => Proc.new{|v| v >= 0},
:on_change => Proc.new { |bot, v|
if @save_timer
if v > 0
@timer.reschedule(@save_timer, v)
@timer.unblock(@save_timer)
else
@timer.block(@save_timer)
end
else
if v > 0
@save_timer = @timer.add(v) { bot.save }
end
end
},
:desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)")
Config.register Config::BooleanValue.new('core.run_as_daemon',
:default => false, :requires_restart => true,
:desc => "Should the bot run as a daemon?")
Config.register Config::StringValue.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")
Config.register Config::IntegerValue.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")
Config.register Config::IntegerValue.new('log.keep',
:default => 1, :requires_restart => true,
:validate => Proc.new { |v| v >= 0 },
:desc => "How many old console messages logfiles to keep")
Config.register Config::IntegerValue.new('log.max_size',
:default => 10, :requires_restart => true,
:validate => Proc.new { |v| v > 0 },
:desc => "Maximum console messages logfile size (in megabytes)")
Config.register Config::ArrayValue.new('plugins.path',
:wizard => true, :default => ['(default)', '(default)/games', '(default)/contrib'],
:requires_restart => false,
:on_change => Proc.new { |bot, v| bot.setup_plugins_path },
:desc => "Where the bot should look for plugins. List multiple directories using commas to separate. Use '(default)' for default prepackaged plugins collection, '(default)/contrib' for prepackaged unsupported plugins collection")
Config.register Config::EnumValue.new('send.newlines',
:values => ['split', 'join'], :default => 'split',
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :newlines => v.to_sym
},
:desc => "When set to split, messages with embedded newlines will be sent as separate lines. When set to join, newlines will be replaced by the value of join_with")
Config.register Config::StringValue.new('send.join_with',
:default => ' ',
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :join_with => v.dup
},
:desc => "String used to replace newlines when send.newlines is set to join")
Config.register Config::IntegerValue.new('send.max_lines',
:default => 5,
:validate => Proc.new { |v| v >= 0 },
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :max_lines => v
},
:desc => "Maximum number of IRC lines to send for each message (set to 0 for no limit)")
Config.register Config::EnumValue.new('send.overlong',
:values => ['split', 'truncate'], :default => 'split',
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :overlong => v.to_sym
},
:desc => "When set to split, messages which are too long to fit in a single IRC line are split into multiple lines. When set to truncate, long messages are truncated to fit the IRC line length")
Config.register Config::StringValue.new('send.split_at',
:default => '\s+',
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :split_at => Regexp.new(v)
},
:desc => "A regular expression that should match the split points for overlong messages (see send.overlong)")
Config.register Config::BooleanValue.new('send.purge_split',
:default => true,
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :purge_split => v
},
:desc => "Set to true if the splitting boundary (set in send.split_at) should be removed when splitting overlong messages (see send.overlong)")
Config.register Config::StringValue.new('send.truncate_text',
:default => "#{Reverse}...#{Reverse}",
:on_change => Proc.new { |bot, v|
bot.set_default_send_options :truncate_text => v.dup
},
:desc => "When truncating overlong messages (see send.overlong) or when sending too many lines per message (see send.max_lines) replace the end of the last line with this text")
@argv = params[:argv]
@run_dir = params[:run_dir] || Dir.pwd
unless FileTest.directory? Config::coredir
error "core directory '#{Config::coredir}' not found, did you setup.rb?"
exit 2
end
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(/\/$/, "")
if FileTest.directory? botclass
template = File.join Config::datadir, 'templates'
missing = Dir.chdir(template) { Dir.glob('*/**') } - Dir.chdir(botclass) { Dir.glob('*/**') }
missing.map do |f|
dest = File.join(botclass, f)
FileUtils.mkdir_p File.dirname dest
FileUtils.cp File.join(template, f), dest
end
else
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}/registry") unless File.exist?("#{botclass}/registry")
Dir.mkdir("#{botclass}/safe_save") unless File.exist?("#{botclass}/safe_save")
@last_ping = nil
@last_rec = nil
@startup_time = Time.new
begin
@config = Config.manager
@config.bot_associate(self)
rescue Exception => e
fatal e
log_session_end
exit 2
end
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"
debug "Using `#{@logfile}' as debug log"
end
if $daemonize
begin
exit if fork
Process.setsid
exit if fork
rescue NotImplementedError
warning "Could not background, fork not supported"
rescue SystemExit
exit 0
rescue Exception => e
warning "Could not background. #{e.pretty_inspect}"
end
Dir.chdir botclass
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 defined? $cl_loglevel
logger.level = 0 if $debug
restart_logger(logger)
log_session_start
if $daemonize
log "Redirecting standard input/output/error"
[$stdin, $stdout, $stderr].each do |fd|
begin
fd.reopen "/dev/null"
rescue Errno::ENOENT
fd.reopen "NUL"
end
end
def $stdout.write(str=nil)
log str, 2
return str.to_s.size
end
def $stdout.write(str=nil)
if str.to_s.match(/:\d+: warning:/)
warning str, 2
else
error str, 2
end
return str.to_s.size
end
end
File.open($opts['pidfile'] || "#{@botclass}/rbot.pid", 'w') do |pf|
pf << "#{$$}\n"
end
@registry = Registry.new self
@timer = Timer.new
@save_mutex = Mutex.new
if @config['core.save_every'] > 0
@save_timer = @timer.add(@config['core.save_every']) { save }
else
@save_timer = nil
end
@quit_mutex = Mutex.new
@plugins = nil
@lang = Language.new(self, @config['core.language'])
begin
@auth = Auth::manager
@auth.bot_associate(self)
rescue Exception => e
fatal e
log_session_end
exit 2
end
@auth.everyone.set_default_permission("*", true)
@auth.botowner.password= @config['auth.password']
Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
@plugins = Plugins::manager
@plugins.bot_associate(self)
setup_plugins_path()
if @config['server.name']
debug "upgrading configuration (server.name => server.list)"
srv_uri = 'irc://' + @config['server.name']
srv_uri += ":#{@config['server.port']}" if @config['server.port']
@config.items['server.list'.to_sym].set_string(srv_uri)
@config.delete('server.name'.to_sym)
@config.delete('server.port'.to_sym)
debug "server.list is now #{@config['server.list'].inspect}"
end
@socket = Irc::Socket.new(@config['server.list'], @config['server.bindhost'], :ssl => @config['server.ssl'])
@client = Client.new
@plugins.scan
@quiet = Set.new
@not_quiet = Set.new
@wanted_nick = nil
@client[:welcome] = proc {|data|
m = WelcomeMessage.new(self, server, data[:source], data[:target], data[:message])
@plugins.delegate("welcome", m)
@plugins.delegate("connect")
}
asked_for = { "identify-msg""identify-msg" => false }
@client[:isupport] = proc { |data|
if server.supports[:capab] and !asked_for["identify-msg""identify-msg"]
sendq "CAPAB IDENTIFY-MSG"
asked_for["identify-msg""identify-msg"] = true
end
}
@client[:datastr] = proc { |data|
if data[:text] == "IDENTIFY-MSG"
server.capabilities["identify-msg""identify-msg"] = true
else
debug "Not handling RPL_DATASTR #{data[:servermessage]}"
end
}
@client[:privmsg] = proc { |data|
m = PrivMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true)
@config['irc.ignore_users'].each { |mask|
if m.source.matches?(server.new_netmask(mask))
m.ignored = true
end
}
@plugins.irc_delegate('privmsg', m)
}
@client[:notice] = proc { |data|
message = NoticeMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true)
@plugins.irc_delegate "notice", message
}
@client[:motd] = proc { |data|
m = MotdMessage.new(self, server, data[:source], data[:target], data[:motd])
@plugins.delegate "motd", m
}
@client[:nicktaken] = proc { |data|
new = "#{data[:nick]}_"
nickchg new
if data[:target] == '*'
debug "setting my connection nick to #{new}"
nick = new
end
@plugins.delegate "nicktaken", data[:nick]
}
@client[:badnick] = proc {|data|
warning "bad nick (#{data[:nick]})"
}
@client[:ping] = proc {|data|
sendq "PONG #{data[:pingid]}"
}
@client[:pong] = proc {|data|
@last_ping = nil
}
@client[:nick] = proc {|data|
source = data[:source]
old = data[:oldnick]
new = data[:newnick]
m = NickMessage.new(self, server, source, old, new)
m.is_on = data[:is_on]
if source == myself
debug "my nick is now #{new}"
end
@plugins.irc_delegate("nick", m)
}
@client[:quit] = proc {|data|
source = data[:source]
message = data[:message]
m = QuitMessage.new(self, server, source, source, message)
m.was_on = data[:was_on]
@plugins.irc_delegate("quit", m)
}
@client[:mode] = proc {|data|
m = ModeChangeMessage.new(self, server, data[:source], data[:target], data[:modestring])
m.modes = data[:modes]
@plugins.delegate "modechange", m
}
@client[:whois] = proc {|data|
source = data[:source]
target = server.get_user(data[:whois][:nick])
m = WhoisMessage.new(self, server, source, target, data[:whois])
@plugins.delegate "whois", m
}
@client[:join] = proc {|data|
m = JoinMessage.new(self, server, data[:source], data[:channel], data[:message])
sendq("MODE #{data[:channel]}", nil, 0) if m.address?
@plugins.irc_delegate("join", m)
sendq("WHO #{data[:channel]}", data[:channel], 2) if m.address?
}
@client[:part] = proc {|data|
m = PartMessage.new(self, server, data[:source], data[:channel], data[:message])
@plugins.irc_delegate("part", m)
}
@client[:kick] = proc {|data|
m = KickMessage.new(self, server, data[:source], data[:target], data[:channel],data[:message])
@plugins.irc_delegate("kick", m)
}
@client[:invite] = proc {|data|
m = InviteMessage.new(self, server, data[:source], data[:target], data[:channel])
@plugins.irc_delegate("invite", m)
}
@client[:changetopic] = proc {|data|
m = TopicMessage.new(self, server, data[:source], data[:channel], data[:topic])
m.info_or_set = :set
@plugins.irc_delegate("topic", m)
}
@client[:topicinfo] = proc { |data|
channel = data[:channel]
topic = channel.topic
m = TopicMessage.new(self, server, data[:source], channel, topic)
m.info_or_set = :info
@plugins.irc_delegate("topic", m)
}
@client[:names] = proc { |data|
m = NamesMessage.new(self, server, server, data[:channel])
m.users = data[:users]
@plugins.delegate "names", m
}
@client[:unknown] = proc { |data|
m = UnknownMessage.new(self, server, server, nil, data[:serverstring])
@plugins.delegate "unknown_message", m
}
set_default_send_options :newlines => @config['send.newlines'].to_sym,
:join_with => @config['send.join_with'].dup,
:max_lines => @config['send.max_lines'],
:overlong => @config['send.overlong'].to_sym,
:split_at => Regexp.new(@config['send.split_at']),
:purge_split => @config['send.purge_split'],
:truncate_text => @config['send.truncate_text'].dup
trap_sigs
end