| 1 | #-- vim:sw=2:et |
|---|
| 2 | #++ |
|---|
| 3 | # |
|---|
| 4 | # :title: rbot core |
|---|
| 5 | |
|---|
| 6 | require 'thread' |
|---|
| 7 | |
|---|
| 8 | require 'etc' |
|---|
| 9 | require 'fileutils' |
|---|
| 10 | require 'logger' |
|---|
| 11 | |
|---|
| 12 | $debug = false unless $debug |
|---|
| 13 | $daemonize = false unless $daemonize |
|---|
| 14 | |
|---|
| 15 | $dateformat = "%Y/%m/%d %H:%M:%S" |
|---|
| 16 | $logger = Logger.new($stderr) |
|---|
| 17 | $logger.datetime_format = $dateformat |
|---|
| 18 | $logger.level = $cl_loglevel if defined? $cl_loglevel |
|---|
| 19 | $logger.level = 0 if $debug |
|---|
| 20 | |
|---|
| 21 | $log_queue = Queue.new |
|---|
| 22 | $log_thread = nil |
|---|
| 23 | |
|---|
| 24 | require 'pp' |
|---|
| 25 | |
|---|
| 26 | unless Kernel.respond_to? :pretty_inspect |
|---|
| 27 | def pretty_inspect |
|---|
| 28 | PP.pp(self, '') |
|---|
| 29 | end |
|---|
| 30 | public :pretty_inspect |
|---|
| 31 | end |
|---|
| 32 | |
|---|
| 33 | class Exception |
|---|
| 34 | def pretty_print(q) |
|---|
| 35 | q.group(1, "#<%s: %s" % [self.class, self.message], ">") { |
|---|
| 36 | if self.backtrace and not self.backtrace.empty? |
|---|
| 37 | q.text "\n" |
|---|
| 38 | q.seplist(self.backtrace, lambda { q.text "\n" } ) { |l| q.text l } |
|---|
| 39 | end |
|---|
| 40 | } |
|---|
| 41 | end |
|---|
| 42 | end |
|---|
| 43 | |
|---|
| 44 | class ServerError < RuntimeError |
|---|
| 45 | end |
|---|
| 46 | |
|---|
| 47 | def rawlog(level, message=nil, who_pos=1) |
|---|
| 48 | call_stack = caller |
|---|
| 49 | if call_stack.length > who_pos |
|---|
| 50 | who = call_stack[who_pos].sub(%r{(?:.+)/([^/]+):(\d+)(:in .*)?}) { "#{$1}:#{$2}#{$3}" } |
|---|
| 51 | else |
|---|
| 52 | who = "(unknown)" |
|---|
| 53 | end |
|---|
| 54 | # Output each line. To distinguish between separate messages and multi-line |
|---|
| 55 | # messages originating at the same time, we blank #{who} after the first message |
|---|
| 56 | # is output. |
|---|
| 57 | # Also, we output strings as-is but for other objects we use pretty_inspect |
|---|
| 58 | case message |
|---|
| 59 | when String |
|---|
| 60 | str = message |
|---|
| 61 | else |
|---|
| 62 | str = message.pretty_inspect |
|---|
| 63 | end |
|---|
| 64 | qmsg = Array.new |
|---|
| 65 | str.each_line { |l| |
|---|
| 66 | qmsg.push [level, l.chomp, who] |
|---|
| 67 | who = ' ' * who.size |
|---|
| 68 | } |
|---|
| 69 | $log_queue.push qmsg |
|---|
| 70 | end |
|---|
| 71 | |
|---|
| 72 | def halt_logger |
|---|
| 73 | if $log_thread && $log_thread.alive? |
|---|
| 74 | $log_queue << nil |
|---|
| 75 | $log_thread.join |
|---|
| 76 | $log_thread = nil |
|---|
| 77 | end |
|---|
| 78 | end |
|---|
| 79 | |
|---|
| 80 | END { halt_logger } |
|---|
| 81 | |
|---|
| 82 | def restart_logger(newlogger = false) |
|---|
| 83 | halt_logger |
|---|
| 84 | |
|---|
| 85 | $logger = newlogger if newlogger |
|---|
| 86 | |
|---|
| 87 | $log_thread = Thread.new do |
|---|
| 88 | ls = nil |
|---|
| 89 | while ls = $log_queue.pop |
|---|
| 90 | ls.each { |l| $logger.add(*l) } |
|---|
| 91 | end |
|---|
| 92 | end |
|---|
| 93 | end |
|---|
| 94 | |
|---|
| 95 | restart_logger |
|---|
| 96 | |
|---|
| 97 | def log_session_start |
|---|
| 98 | $logger << "\n\n=== #{botclass} session started on #{Time.now.strftime($dateformat)} ===\n\n" |
|---|
| 99 | restart_logger |
|---|
| 100 | end |
|---|
| 101 | |
|---|
| 102 | def log_session_end |
|---|
| 103 | $logger << "\n\n=== #{botclass} session ended on #{Time.now.strftime($dateformat)} ===\n\n" |
|---|
| 104 | $log_queue << nil |
|---|
| 105 | end |
|---|
| 106 | |
|---|
| 107 | def debug(message=nil, who_pos=1) |
|---|
| 108 | rawlog(Logger::Severity::DEBUG, message, who_pos) |
|---|
| 109 | end |
|---|
| 110 | |
|---|
| 111 | def log(message=nil, who_pos=1) |
|---|
| 112 | rawlog(Logger::Severity::INFO, message, who_pos) |
|---|
| 113 | end |
|---|
| 114 | |
|---|
| 115 | def warning(message=nil, who_pos=1) |
|---|
| 116 | rawlog(Logger::Severity::WARN, message, who_pos) |
|---|
| 117 | end |
|---|
| 118 | |
|---|
| 119 | def error(message=nil, who_pos=1) |
|---|
| 120 | rawlog(Logger::Severity::ERROR, message, who_pos) |
|---|
| 121 | end |
|---|
| 122 | |
|---|
| 123 | def fatal(message=nil, who_pos=1) |
|---|
| 124 | rawlog(Logger::Severity::FATAL, message, who_pos) |
|---|
| 125 | end |
|---|
| 126 | |
|---|
| 127 | debug "debug test" |
|---|
| 128 | log "log test" |
|---|
| 129 | warning "warning test" |
|---|
| 130 | error "error test" |
|---|
| 131 | fatal "fatal test" |
|---|
| 132 | |
|---|
| 133 | # The following global is used for the improved signal handling. |
|---|
| 134 | $interrupted = 0 |
|---|
| 135 | |
|---|
| 136 | # these first |
|---|
| 137 | require 'rbot/rbotconfig' |
|---|
| 138 | begin |
|---|
| 139 | require 'rubygems' |
|---|
| 140 | rescue LoadError |
|---|
| 141 | log "rubygems unavailable" |
|---|
| 142 | end |
|---|
| 143 | |
|---|
| 144 | require 'rbot/load-gettext' |
|---|
| 145 | require 'rbot/config' |
|---|
| 146 | require 'rbot/config-compat' |
|---|
| 147 | |
|---|
| 148 | require 'rbot/irc' |
|---|
| 149 | require 'rbot/rfc2812' |
|---|
| 150 | require 'rbot/ircsocket' |
|---|
| 151 | require 'rbot/botuser' |
|---|
| 152 | require 'rbot/timer' |
|---|
| 153 | require 'rbot/plugins' |
|---|
| 154 | require 'rbot/message' |
|---|
| 155 | require 'rbot/language' |
|---|
| 156 | require 'rbot/dbhash' |
|---|
| 157 | require 'rbot/registry' |
|---|
| 158 | |
|---|
| 159 | module Irc |
|---|
| 160 | |
|---|
| 161 | # Main bot class, which manages the various components, receives messages, |
|---|
| 162 | # handles them or passes them to plugins, and contains core functionality. |
|---|
| 163 | class Bot |
|---|
| 164 | COPYRIGHT_NOTICE = "(c) Tom Gilbert and the rbot development team" |
|---|
| 165 | SOURCE_URL = "http://ruby-rbot.org" |
|---|
| 166 | # the bot's Auth data |
|---|
| 167 | attr_reader :auth |
|---|
| 168 | |
|---|
| 169 | # the bot's Config data |
|---|
| 170 | attr_reader :config |
|---|
| 171 | |
|---|
| 172 | # the botclass for this bot (determines configdir among other things) |
|---|
| 173 | attr_reader :botclass |
|---|
| 174 | |
|---|
| 175 | # used to perform actions periodically (saves configuration once per minute |
|---|
| 176 | # by default) |
|---|
| 177 | attr_reader :timer |
|---|
| 178 | |
|---|
| 179 | # synchronize with this mutex while touching permanent data files: |
|---|
| 180 | # saving, flushing, cleaning up ... |
|---|
| 181 | attr_reader :save_mutex |
|---|
| 182 | |
|---|
| 183 | # bot's Language data |
|---|
| 184 | attr_reader :lang |
|---|
| 185 | |
|---|
| 186 | # bot's irc socket |
|---|
| 187 | # TODO multiserver |
|---|
| 188 | attr_reader :socket |
|---|
| 189 | |
|---|
| 190 | # bot's object registry, plugins get an interface to this for persistant |
|---|
| 191 | # storage (hash interface tied to a bdb file, plugins use Accessors to store |
|---|
| 192 | # and restore objects in their own namespaces.) |
|---|
| 193 | attr_reader :registry |
|---|
| 194 | |
|---|
| 195 | # bot's plugins. This is an instance of class Plugins |
|---|
| 196 | attr_reader :plugins |
|---|
| 197 | |
|---|
| 198 | # bot's httputil help object, for fetching resources via http. Sets up |
|---|
| 199 | # proxies etc as defined by the bot configuration/environment |
|---|
| 200 | attr_accessor :httputil |
|---|
| 201 | |
|---|
| 202 | # server we are connected to |
|---|
| 203 | # TODO multiserver |
|---|
| 204 | def server |
|---|
| 205 | @client.server |
|---|
| 206 | end |
|---|
| 207 | |
|---|
| 208 | # bot User in the client/server connection |
|---|
| 209 | # TODO multiserver |
|---|
| 210 | def myself |
|---|
| 211 | @client.user |
|---|
| 212 | end |
|---|
| 213 | |
|---|
| 214 | # bot nick in the client/server connection |
|---|
| 215 | def nick |
|---|
| 216 | myself.nick |
|---|
| 217 | end |
|---|
| 218 | |
|---|
| 219 | # bot channels in the client/server connection |
|---|
| 220 | def channels |
|---|
| 221 | myself.channels |
|---|
| 222 | end |
|---|
| 223 | |
|---|
| 224 | # nick wanted by the bot. This defaults to the irc.nick config value, |
|---|
| 225 | # but may be overridden by a manual !nick command |
|---|
| 226 | def wanted_nick |
|---|
| 227 | @wanted_nick || config['irc.nick'] |
|---|
| 228 | end |
|---|
| 229 | |
|---|
| 230 | # set the nick wanted by the bot |
|---|
| 231 | def wanted_nick=(wn) |
|---|
| 232 | if wn.nil? or wn.to_s.downcase == config['irc.nick'].downcase |
|---|
| 233 | @wanted_nick = nil |
|---|
| 234 | else |
|---|
| 235 | @wanted_nick = wn.to_s.dup |
|---|
| 236 | end |
|---|
| 237 | end |
|---|
| 238 | |
|---|
| 239 | |
|---|
| 240 | # bot inspection |
|---|
| 241 | # TODO multiserver |
|---|
| 242 | def inspect |
|---|
| 243 | ret = self.to_s[0..-2] |
|---|
| 244 | ret << ' version=' << $version.inspect |
|---|
| 245 | ret << ' botclass=' << botclass.inspect |
|---|
| 246 | ret << ' lang="' << lang.language |
|---|
| 247 | if defined?(GetText) |
|---|
| 248 | ret << '/' << locale |
|---|
| 249 | end |
|---|
| 250 | ret << '"' |
|---|
| 251 | ret << ' nick=' << nick.inspect |
|---|
| 252 | ret << ' server=' |
|---|
| 253 | if server |
|---|
| 254 | ret << (server.to_s + (socket ? |
|---|
| 255 | ' [' << socket.server_uri.to_s << ']' : '')).inspect |
|---|
| 256 | unless server.channels.empty? |
|---|
| 257 | ret << " channels=" |
|---|
| 258 | ret << server.channels.map { |c| |
|---|
| 259 | "%s%s" % [c.modes_of(nick).map { |m| |
|---|
| 260 | server.prefix_for_mode(m) |
|---|
| 261 | }, c.name] |
|---|
| 262 | }.inspect |
|---|
| 263 | end |
|---|
| 264 | else |
|---|
| 265 | ret << '(none)' |
|---|
| 266 | end |
|---|
| 267 | ret << ' plugins=' << plugins.inspect |
|---|
| 268 | ret << ">" |
|---|
| 269 | end |
|---|
| 270 | |
|---|
| 271 | |
|---|
| 272 | # create a new Bot with botclass +botclass+ |
|---|
| 273 | def initialize(botclass, params = {}) |
|---|
| 274 | # Config for the core bot |
|---|
| 275 | # TODO should we split socket stuff into ircsocket, etc? |
|---|
| 276 | Config.register Config::ArrayValue.new('server.list', |
|---|
| 277 | :default => ['irc://localhost'], :wizard => true, |
|---|
| 278 | :requires_restart => true, |
|---|
| 279 | :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.") |
|---|
| 280 | Config.register Config::BooleanValue.new('server.ssl', |
|---|
| 281 | :default => false, :requires_restart => true, :wizard => true, |
|---|
| 282 | :desc => "Use SSL to connect to this server?") |
|---|
| 283 | Config.register Config::StringValue.new('server.password', |
|---|
| 284 | :default => false, :requires_restart => true, |
|---|
| 285 | :desc => "Password for connecting to this server (if required)", |
|---|
| 286 | :wizard => true) |
|---|
| 287 | Config.register Config::StringValue.new('server.bindhost', |
|---|
| 288 | :default => false, :requires_restart => true, |
|---|
| 289 | :desc => "Specific local host or IP for the bot to bind to (if required)", |
|---|
| 290 | :wizard => true) |
|---|
| 291 | Config.register Config::IntegerValue.new('server.reconnect_wait', |
|---|
| 292 | :default => 5, :validate => Proc.new{|v| v >= 0}, |
|---|
| 293 | :desc => "Seconds to wait before attempting to reconnect, on disconnect") |
|---|
| 294 | Config.register Config::IntegerValue.new('server.ping_timeout', |
|---|
| 295 | :default => 30, :validate => Proc.new{|v| v >= 0}, |
|---|
| 296 | :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)") |
|---|
| 297 | Config.register Config::ArrayValue.new('server.nocolor_modes', |
|---|
| 298 | :default => ['c'], :wizard => false, |
|---|
| 299 | :requires_restart => false, |
|---|
| 300 | :desc => "List of channel modes that require messages to be without colors") |
|---|
| 301 | |
|---|
| 302 | Config.register Config::StringValue.new('irc.nick', :default => "rbot", |
|---|
| 303 | :desc => "IRC nickname the bot should attempt to use", :wizard => true, |
|---|
| 304 | :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" }) |
|---|
| 305 | Config.register Config::StringValue.new('irc.name', |
|---|
| 306 | :default => "Ruby bot", :requires_restart => true, |
|---|
| 307 | :desc => "IRC realname the bot should use") |
|---|
| 308 | Config.register Config::BooleanValue.new('irc.name_copyright', |
|---|
| 309 | :default => true, :requires_restart => true, |
|---|
| 310 | :desc => "Append copyright notice to bot realname? (please don't disable unless it's really necessary)") |
|---|
| 311 | Config.register Config::StringValue.new('irc.user', :default => "rbot", |
|---|
| 312 | :requires_restart => true, |
|---|
| 313 | :desc => "local user the bot should appear to be", :wizard => true) |
|---|
| 314 | Config.register Config::ArrayValue.new('irc.join_channels', |
|---|
| 315 | :default => [], :wizard => true, |
|---|
| 316 | :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'") |
|---|
| 317 | Config.register Config::ArrayValue.new('irc.ignore_users', |
|---|
| 318 | :default => [], |
|---|
| 319 | :desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people") |
|---|
| 320 | Config.register Config::ArrayValue.new('irc.ignore_channels', |
|---|
| 321 | :default => [], |
|---|
| 322 | :desc => "Which channels to ignore input in. This is mainly to turn the bot into a logbot that doesn't interact with users in any way (in the specified channels)") |
|---|
| 323 | |
|---|
| 324 | Config.register Config::IntegerValue.new('core.save_every', |
|---|
| 325 | :default => 60, :validate => Proc.new{|v| v >= 0}, |
|---|
| 326 | :on_change => Proc.new { |bot, v| |
|---|
| 327 | if @save_timer |
|---|
| 328 | if v > 0 |
|---|
| 329 | @timer.reschedule(@save_timer, v) |
|---|
| 330 | @timer.unblock(@save_timer) |
|---|
| 331 | else |
|---|
| 332 | @timer.block(@save_timer) |
|---|
| 333 | end |
|---|
| 334 | else |
|---|
| 335 | if v > 0 |
|---|
| 336 | @save_timer = @timer.add(v) { bot.save } |
|---|
| 337 | end |
|---|
| 338 | # Nothing to do when v == 0 |
|---|
| 339 | end |
|---|
| 340 | }, |
|---|
| 341 | :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)") |
|---|
| 342 | |
|---|
| 343 | Config.register Config::BooleanValue.new('core.run_as_daemon', |
|---|
| 344 | :default => false, :requires_restart => true, |
|---|
| 345 | :desc => "Should the bot run as a daemon?") |
|---|
| 346 | |
|---|
| 347 | Config.register Config::StringValue.new('log.file', |
|---|
| 348 | :default => false, :requires_restart => true, |
|---|
| 349 | :desc => "Name of the logfile to which console messages will be redirected when the bot is run as a daemon") |
|---|
| 350 | Config.register Config::IntegerValue.new('log.level', |
|---|
| 351 | :default => 1, :requires_restart => false, |
|---|
| 352 | :validate => Proc.new { |v| (0..5).include?(v) }, |
|---|
| 353 | :on_change => Proc.new { |bot, v| |
|---|
| 354 | $logger.level = v |
|---|
| 355 | }, |
|---|
| 356 | :desc => "The minimum logging level (0=DEBUG,1=INFO,2=WARN,3=ERROR,4=FATAL) for console messages") |
|---|
| 357 | Config.register Config::IntegerValue.new('log.keep', |
|---|
| 358 | :default => 1, :requires_restart => true, |
|---|
| 359 | :validate => Proc.new { |v| v >= 0 }, |
|---|
| 360 | :desc => "How many old console messages logfiles to keep") |
|---|
| 361 | Config.register Config::IntegerValue.new('log.max_size', |
|---|
| 362 | :default => 10, :requires_restart => true, |
|---|
| 363 | :validate => Proc.new { |v| v > 0 }, |
|---|
| 364 | :desc => "Maximum console messages logfile size (in megabytes)") |
|---|
| 365 | |
|---|
| 366 | Config.register Config::ArrayValue.new('plugins.path', |
|---|
| 367 | :wizard => true, :default => ['(default)', '(default)/games', '(default)/contrib'], |
|---|
| 368 | :requires_restart => false, |
|---|
| 369 | :on_change => Proc.new { |bot, v| bot.setup_plugins_path }, |
|---|
| 370 | :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") |
|---|
| 371 | |
|---|
| 372 | Config.register Config::EnumValue.new('send.newlines', |
|---|
| 373 | :values => ['split', 'join'], :default => 'split', |
|---|
| 374 | :on_change => Proc.new { |bot, v| |
|---|
| 375 | bot.set_default_send_options :newlines => v.to_sym |
|---|
| 376 | }, |
|---|
| 377 | :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") |
|---|
| 378 | Config.register Config::StringValue.new('send.join_with', |
|---|
| 379 | :default => ' ', |
|---|
| 380 | :on_change => Proc.new { |bot, v| |
|---|
| 381 | bot.set_default_send_options :join_with => v.dup |
|---|
| 382 | }, |
|---|
| 383 | :desc => "String used to replace newlines when send.newlines is set to join") |
|---|
| 384 | Config.register Config::IntegerValue.new('send.max_lines', |
|---|
| 385 | :default => 5, |
|---|
| 386 | :validate => Proc.new { |v| v >= 0 }, |
|---|
| 387 | :on_change => Proc.new { |bot, v| |
|---|
| 388 | bot.set_default_send_options :max_lines => v |
|---|
| 389 | }, |
|---|
| 390 | :desc => "Maximum number of IRC lines to send for each message (set to 0 for no limit)") |
|---|
| 391 | Config.register Config::EnumValue.new('send.overlong', |
|---|
| 392 | :values => ['split', 'truncate'], :default => 'split', |
|---|
| 393 | :on_change => Proc.new { |bot, v| |
|---|
| 394 | bot.set_default_send_options :overlong => v.to_sym |
|---|
| 395 | }, |
|---|
| 396 | :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") |
|---|
| 397 | Config.register Config::StringValue.new('send.split_at', |
|---|
| 398 | :default => '\s+', |
|---|
| 399 | :on_change => Proc.new { |bot, v| |
|---|
| 400 | bot.set_default_send_options :split_at => Regexp.new(v) |
|---|
| 401 | }, |
|---|
| 402 | :desc => "A regular expression that should match the split points for overlong messages (see send.overlong)") |
|---|
| 403 | Config.register Config::BooleanValue.new('send.purge_split', |
|---|
| 404 | :default => true, |
|---|
| 405 | :on_change => Proc.new { |bot, v| |
|---|
| 406 | bot.set_default_send_options :purge_split => v |
|---|
| 407 | }, |
|---|
| 408 | :desc => "Set to true if the splitting boundary (set in send.split_at) should be removed when splitting overlong messages (see send.overlong)") |
|---|
| 409 | Config.register Config::StringValue.new('send.truncate_text', |
|---|
| 410 | :default => "#{Reverse}...#{Reverse}", |
|---|
| 411 | :on_change => Proc.new { |bot, v| |
|---|
| 412 | bot.set_default_send_options :truncate_text => v.dup |
|---|
| 413 | }, |
|---|
| 414 | :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") |
|---|
| 415 | Config.register Config::IntegerValue.new('send.penalty_pct', |
|---|
| 416 | :default => 100, |
|---|
| 417 | :validate => Proc.new { |v| v >= 0 }, |
|---|
| 418 | :on_change => Proc.new { |bot, v| |
|---|
| 419 | bot.socket.penalty_pct = v |
|---|
| 420 | }, |
|---|
| 421 | :desc => "Percentage of IRC penalty to consider when sending messages to prevent being disconnected for excess flood. Set to 0 to disable penalty control.") |
|---|
| 422 | |
|---|
| 423 | @argv = params[:argv] |
|---|
| 424 | @run_dir = params[:run_dir] || Dir.pwd |
|---|
| 425 | |
|---|
| 426 | unless FileTest.directory? Config::coredir |
|---|
| 427 | error "core directory '#{Config::coredir}' not found, did you setup.rb?" |
|---|
| 428 | exit 2 |
|---|
| 429 | end |
|---|
| 430 | |
|---|
| 431 | unless FileTest.directory? Config::datadir |
|---|
| 432 | error "data directory '#{Config::datadir}' not found, did you setup.rb?" |
|---|
| 433 | exit 2 |
|---|
| 434 | end |
|---|
| 435 | |
|---|
| 436 | unless botclass and not botclass.empty? |
|---|
| 437 | # We want to find a sensible default. |
|---|
| 438 | # * On POSIX systems we prefer ~/.rbot for the effective uid of the process |
|---|
| 439 | # * On Windows (at least the NT versions) we want to put our stuff in the |
|---|
| 440 | # Application Data folder. |
|---|
| 441 | # We don't use any particular O/S detection magic, exploiting the fact that |
|---|
| 442 | # Etc.getpwuid is nil on Windows |
|---|
| 443 | if Etc.getpwuid(Process::Sys.geteuid) |
|---|
| 444 | botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup |
|---|
| 445 | else |
|---|
| 446 | if ENV.has_key?('APPDATA') |
|---|
| 447 | botclass = ENV['APPDATA'].dup |
|---|
| 448 | botclass.gsub!("\\","/") |
|---|
| 449 | end |
|---|
| 450 | end |
|---|
| 451 | botclass = File.join(botclass, ".rbot") |
|---|
| 452 | end |
|---|
| 453 | botclass = File.expand_path(botclass) |
|---|
| 454 | @botclass = botclass.gsub(/\/$/, "") |
|---|
| 455 | |
|---|
| 456 | repopulate_botclass_directory |
|---|
| 457 | |
|---|
| 458 | registry_dir = File.join(@botclass, 'registry') |
|---|
| 459 | Dir.mkdir(registry_dir) unless File.exist?(registry_dir) |
|---|
| 460 | unless FileTest.directory? registry_dir |
|---|
| 461 | error "registry storage location #{registry_dir} is not a directory" |
|---|
| 462 | exit 2 |
|---|
| 463 | end |
|---|
| 464 | save_dir = File.join(@botclass, 'safe_save') |
|---|
| 465 | Dir.mkdir(save_dir) unless File.exist?(save_dir) |
|---|
| 466 | unless FileTest.directory? save_dir |
|---|
| 467 | error "safe save location #{save_dir} is not a directory" |
|---|
| 468 | exit 2 |
|---|
| 469 | end |
|---|
| 470 | |
|---|
| 471 | # Time at which the last PING was sent |
|---|
| 472 | @last_ping = nil |
|---|
| 473 | # Time at which the last line was RECV'd from the server |
|---|
| 474 | @last_rec = nil |
|---|
| 475 | |
|---|
| 476 | @startup_time = Time.new |
|---|
| 477 | |
|---|
| 478 | begin |
|---|
| 479 | @config = Config.manager |
|---|
| 480 | @config.bot_associate(self) |
|---|
| 481 | rescue Exception => e |
|---|
| 482 | fatal e |
|---|
| 483 | log_session_end |
|---|
| 484 | exit 2 |
|---|
| 485 | end |
|---|
| 486 | |
|---|
| 487 | if @config['core.run_as_daemon'] |
|---|
| 488 | $daemonize = true |
|---|
| 489 | end |
|---|
| 490 | |
|---|
| 491 | @logfile = @config['log.file'] |
|---|
| 492 | if @logfile.class!=String || @logfile.empty? |
|---|
| 493 | logfname = File.basename(botclass).gsub(/^\.+/,'') |
|---|
| 494 | logfname << ".log" |
|---|
| 495 | @logfile = File.join(botclass, logfname) |
|---|
| 496 | debug "Using `#{@logfile}' as debug log" |
|---|
| 497 | end |
|---|
| 498 | |
|---|
| 499 | # See http://blog.humlab.umu.se/samuel/archives/000107.html |
|---|
| 500 | # for the backgrounding code |
|---|
| 501 | if $daemonize |
|---|
| 502 | begin |
|---|
| 503 | exit if fork |
|---|
| 504 | Process.setsid |
|---|
| 505 | exit if fork |
|---|
| 506 | rescue NotImplementedError |
|---|
| 507 | warning "Could not background, fork not supported" |
|---|
| 508 | rescue SystemExit |
|---|
| 509 | exit 0 |
|---|
| 510 | rescue Exception => e |
|---|
| 511 | warning "Could not background. #{e.pretty_inspect}" |
|---|
| 512 | end |
|---|
| 513 | Dir.chdir botclass |
|---|
| 514 | # File.umask 0000 # Ensure sensible umask. Adjust as needed. |
|---|
| 515 | end |
|---|
| 516 | |
|---|
| 517 | logger = Logger.new(@logfile, |
|---|
| 518 | @config['log.keep'], |
|---|
| 519 | @config['log.max_size']*1024*1024) |
|---|
| 520 | logger.datetime_format= $dateformat |
|---|
| 521 | logger.level = @config['log.level'] |
|---|
| 522 | logger.level = $cl_loglevel if defined? $cl_loglevel |
|---|
| 523 | logger.level = 0 if $debug |
|---|
| 524 | |
|---|
| 525 | restart_logger(logger) |
|---|
| 526 | |
|---|
| 527 | log_session_start |
|---|
| 528 | |
|---|
| 529 | if $daemonize |
|---|
| 530 | log "Redirecting standard input/output/error" |
|---|
| 531 | [$stdin, $stdout, $stderr].each do |fd| |
|---|
| 532 | begin |
|---|
| 533 | fd.reopen "/dev/null" |
|---|
| 534 | rescue Errno::ENOENT |
|---|
| 535 | # On Windows, there's not such thing as /dev/null |
|---|
| 536 | fd.reopen "NUL" |
|---|
| 537 | end |
|---|
| 538 | end |
|---|
| 539 | |
|---|
| 540 | def $stdout.write(str=nil) |
|---|
| 541 | log str, 2 |
|---|
| 542 | return str.to_s.size |
|---|
| 543 | end |
|---|
| 544 | def $stdout.write(str=nil) |
|---|
| 545 | if str.to_s.match(/:\d+: warning:/) |
|---|
| 546 | warning str, 2 |
|---|
| 547 | else |
|---|
| 548 | error str, 2 |
|---|
| 549 | end |
|---|
| 550 | return str.to_s.size |
|---|
| 551 | end |
|---|
| 552 | end |
|---|
| 553 | |
|---|
| 554 | File.open($opts['pidfile'] || File.join(@botclass, 'rbot.pid'), 'w') do |pf| |
|---|
| 555 | pf << "#{$$}\n" |
|---|
| 556 | end |
|---|
| 557 | |
|---|
| 558 | @registry = Registry.new self |
|---|
| 559 | |
|---|
| 560 | @timer = Timer.new |
|---|
| 561 | @save_mutex = Mutex.new |
|---|
| 562 | if @config['core.save_every'] > 0 |
|---|
| 563 | @save_timer = @timer.add(@config['core.save_every']) { save } |
|---|
| 564 | else |
|---|
| 565 | @save_timer = nil |
|---|
| 566 | end |
|---|
| 567 | @quit_mutex = Mutex.new |
|---|
| 568 | |
|---|
| 569 | @plugins = nil |
|---|
| 570 | @lang = Language.new(self, @config['core.language']) |
|---|
| 571 | |
|---|
| 572 | begin |
|---|
| 573 | @auth = Auth::manager |
|---|
| 574 | @auth.bot_associate(self) |
|---|
| 575 | # @auth.load("#{botclass}/botusers.yaml") |
|---|
| 576 | rescue Exception => e |
|---|
| 577 | fatal e |
|---|
| 578 | log_session_end |
|---|
| 579 | exit 2 |
|---|
| 580 | end |
|---|
| 581 | @auth.everyone.set_default_permission("*", true) |
|---|
| 582 | @auth.botowner.password= @config['auth.password'] |
|---|
| 583 | |
|---|
| 584 | @plugins = Plugins::manager |
|---|
| 585 | @plugins.bot_associate(self) |
|---|
| 586 | setup_plugins_path() |
|---|
| 587 | |
|---|
| 588 | if @config['server.name'] |
|---|
| 589 | debug "upgrading configuration (server.name => server.list)" |
|---|
| 590 | srv_uri = 'irc://' + @config['server.name'] |
|---|
| 591 | srv_uri += ":#{@config['server.port']}" if @config['server.port'] |
|---|
| 592 | @config.items['server.list'.to_sym].set_string(srv_uri) |
|---|
| 593 | @config.delete('server.name'.to_sym) |
|---|
| 594 | @config.delete('server.port'.to_sym) |
|---|
| 595 | debug "server.list is now #{@config['server.list'].inspect}" |
|---|
| 596 | end |
|---|
| 597 | |
|---|
| 598 | @socket = Irc::Socket.new(@config['server.list'], @config['server.bindhost'], :ssl => @config['server.ssl'], :penalty_pct =>@config['send.penalty_pct']) |
|---|
| 599 | @client = Client.new |
|---|
| 600 | |
|---|
| 601 | @plugins.scan |
|---|
| 602 | |
|---|
| 603 | # Channels where we are quiet |
|---|
| 604 | # Array of channels names where the bot should be quiet |
|---|
| 605 | # '*' means all channels |
|---|
| 606 | # |
|---|
| 607 | @quiet = Set.new |
|---|
| 608 | # but we always speak here |
|---|
| 609 | @not_quiet = Set.new |
|---|
| 610 | |
|---|
| 611 | # the nick we want, if it's different from the irc.nick config value |
|---|
| 612 | # (e.g. as set by a !nick command) |
|---|
| 613 | @wanted_nick = nil |
|---|
| 614 | |
|---|
| 615 | @client[:welcome] = proc {|data| |
|---|
| 616 | m = WelcomeMessage.new(self, server, data[:source], data[:target], data[:message]) |
|---|
| 617 | |
|---|
| 618 | @plugins.delegate("welcome", m) |
|---|
| 619 | @plugins.delegate("connect") |
|---|
| 620 | } |
|---|
| 621 | |
|---|
| 622 | # TODO the next two @client should go into rfc2812.rb, probably |
|---|
| 623 | # Since capabs are two-steps processes, server.supports[:capab] |
|---|
| 624 | # should be a three-state: nil, [], [....] |
|---|
| 625 | asked_for = { :"identify-msg" => false } |
|---|
| 626 | @client[:isupport] = proc { |data| |
|---|
| 627 | if server.supports[:capab] and !asked_for[:"identify-msg"] |
|---|
| 628 | sendq "CAPAB IDENTIFY-MSG" |
|---|
| 629 | asked_for[:"identify-msg"] = true |
|---|
| 630 | end |
|---|
| 631 | } |
|---|
| 632 | @client[:datastr] = proc { |data| |
|---|
| 633 | if data[:text] == "IDENTIFY-MSG" |
|---|
| 634 | server.capabilities[:"identify-msg"] = true |
|---|
| 635 | else |
|---|
| 636 | debug "Not handling RPL_DATASTR #{data[:servermessage]}" |
|---|
| 637 | end |
|---|
| 638 | } |
|---|
| 639 | |
|---|
| 640 | @client[:privmsg] = proc { |data| |
|---|
| 641 | m = PrivMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true) |
|---|
| 642 | # debug "Message source is #{data[:source].inspect}" |
|---|
| 643 | # debug "Message target is #{data[:target].inspect}" |
|---|
| 644 | # debug "Bot is #{myself.inspect}" |
|---|
| 645 | |
|---|
| 646 | @config['irc.ignore_channels'].each { |channel| |
|---|
| 647 | if m.target.downcase == channel.downcase |
|---|
| 648 | m.ignored = true |
|---|
| 649 | break |
|---|
| 650 | end |
|---|
| 651 | } |
|---|
| 652 | @config['irc.ignore_users'].each { |mask| |
|---|
| 653 | if m.source.matches?(server.new_netmask(mask)) |
|---|
| 654 | m.ignored = true |
|---|
| 655 | break |
|---|
| 656 | end |
|---|
| 657 | } unless m.ignored |
|---|
| 658 | |
|---|
| 659 | @plugins.irc_delegate('privmsg', m) |
|---|
| 660 | } |
|---|
| 661 | @client[:notice] = proc { |data| |
|---|
| 662 | message = NoticeMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true) |
|---|
| 663 | # pass it off to plugins that want to hear everything |
|---|
| 664 | @plugins.irc_delegate "notice", message |
|---|
| 665 | } |
|---|
| 666 | @client[:motd] = proc { |data| |
|---|
| 667 | m = MotdMessage.new(self, server, data[:source], data[:target], data[:motd]) |
|---|
| 668 | @plugins.delegate "motd", m |
|---|
| 669 | } |
|---|
| 670 | @client[:nicktaken] = proc { |data| |
|---|
| 671 | new = "#{data[:nick]}_" |
|---|
| 672 | nickchg new |
|---|
| 673 | # If we're setting our nick at connection because our choice was taken, |
|---|
| 674 | # we have to fix our nick manually, because there will be no NICK message |
|---|
| 675 | # to inform us that our nick has been changed. |
|---|
| 676 | if data[:target] == '*' |
|---|
| 677 | debug "setting my connection nick to #{new}" |
|---|
| 678 | nick = new |
|---|
| 679 | end |
|---|
| 680 | @plugins.delegate "nicktaken", data[:nick] |
|---|
| 681 | } |
|---|
| 682 | @client[:badnick] = proc {|data| |
|---|
| 683 | warning "bad nick (#{data[:nick]})" |
|---|
| 684 | } |
|---|
| 685 | @client[:ping] = proc {|data| |
|---|
| 686 | sendq "PONG #{data[:pingid]}" |
|---|
| 687 | } |
|---|
| 688 | @client[:pong] = proc {|data| |
|---|
| 689 | @last_ping = nil |
|---|
| 690 | } |
|---|
| 691 | @client[:nick] = proc {|data| |
|---|
| 692 | # debug "Message source is #{data[:source].inspect}" |
|---|
| 693 | # debug "Bot is #{myself.inspect}" |
|---|
| 694 | source = data[:source] |
|---|
| 695 | old = data[:oldnick] |
|---|
| 696 | new = data[:newnick] |
|---|
| 697 | m = NickMessage.new(self, server, source, old, new) |
|---|
| 698 | m.is_on = data[:is_on] |
|---|
| 699 | if source == myself |
|---|
| 700 | debug "my nick is now #{new}" |
|---|
| 701 | end |
|---|
| 702 | @plugins.irc_delegate("nick", m) |
|---|
| 703 | } |
|---|
| 704 | @client[:quit] = proc {|data| |
|---|
| 705 | source = data[:source] |
|---|
| 706 | message = data[:message] |
|---|
| 707 | m = QuitMessage.new(self, server, source, source, message) |
|---|
| 708 | m.was_on = data[:was_on] |
|---|
| 709 | @plugins.irc_delegate("quit", m) |
|---|
| 710 | } |
|---|
| 711 | @client[:mode] = proc {|data| |
|---|
| 712 | m = ModeChangeMessage.new(self, server, data[:source], data[:target], data[:modestring]) |
|---|
| 713 | m.modes = data[:modes] |
|---|
| 714 | @plugins.delegate "modechange", m |
|---|
| 715 | } |
|---|
| 716 | @client[:whois] = proc {|data| |
|---|
| 717 | source = data[:source] |
|---|
| 718 | target = server.get_user(data[:whois][:nick]) |
|---|
| 719 | m = WhoisMessage.new(self, server, source, target, data[:whois]) |
|---|
| 720 | @plugins.delegate "whois", m |
|---|
| 721 | } |
|---|
| 722 | @client[:join] = proc {|data| |
|---|
| 723 | m = JoinMessage.new(self, server, data[:source], data[:channel], data[:message]) |
|---|
| 724 | sendq("MODE #{data[:channel]}", nil, 0) if m.address? |
|---|
| 725 | @plugins.irc_delegate("join", m) |
|---|
| 726 | sendq("WHO #{data[:channel]}", data[:channel], 2) if m.address? |
|---|
| 727 | } |
|---|
| 728 | @client[:part] = proc {|data| |
|---|
| 729 | m = PartMessage.new(self, server, data[:source], data[:channel], data[:message]) |
|---|
| 730 | @plugins.irc_delegate("part", m) |
|---|
| 731 | } |
|---|
| 732 | @client[:kick] = proc {|data| |
|---|
| 733 | m = KickMessage.new(self, server, data[:source], data[:target], data[:channel],data[:message]) |
|---|
| 734 | @plugins.irc_delegate("kick", m) |
|---|
| 735 | } |
|---|
| 736 | @client[:invite] = proc {|data| |
|---|
| 737 | m = InviteMessage.new(self, server, data[:source], data[:target], data[:channel]) |
|---|
| 738 | @plugins.irc_delegate("invite", m) |
|---|
| 739 | } |
|---|
| 740 | @client[:changetopic] = proc {|data| |
|---|
| 741 | m = TopicMessage.new(self, server, data[:source], data[:channel], data[:topic]) |
|---|
| 742 | m.info_or_set = :set |
|---|
| 743 | @plugins.irc_delegate("topic", m) |
|---|
| 744 | } |
|---|
| 745 | # @client[:topic] = proc { |data| |
|---|
| 746 | # irclog "@ Topic is \"#{data[:topic]}\"", data[:channel] |
|---|
| 747 | # } |
|---|
| 748 | @client[:topicinfo] = proc { |data| |
|---|
| 749 | channel = data[:channel] |
|---|
| 750 | topic = channel.topic |
|---|
| 751 | m = TopicMessage.new(self, server, data[:source], channel, topic) |
|---|
| 752 | m.info_or_set = :info |
|---|
| 753 | @plugins.irc_delegate("topic", m) |
|---|
| 754 | } |
|---|
| 755 | @client[:names] = proc { |data| |
|---|
| 756 | m = NamesMessage.new(self, server, server, data[:channel]) |
|---|
| 757 | m.users = data[:users] |
|---|
| 758 | @plugins.delegate "names", m |
|---|
| 759 | } |
|---|
| 760 | @client[:banlist] = proc { |data| |
|---|
| 761 | m = BanlistMessage.new(self, server, server, data[:channel]) |
|---|
| 762 | m.bans = data[:bans] |
|---|
| 763 | @plugins.delegate "banlist", m |
|---|
| 764 | } |
|---|
| 765 | @client[:nosuchtarget] = proc { |data| |
|---|
| 766 | m = NoSuchTargetMessage.new(self, server, server, data[:target], data[:message]) |
|---|
| 767 | @plugins.delegate "nosuchtarget", m |
|---|
| 768 | } |
|---|
| 769 | @client[:error] = proc { |data| |
|---|
| 770 | raise ServerError, data[:message] |
|---|
| 771 | } |
|---|
| 772 | @client[:unknown] = proc { |data| |
|---|
| 773 | #debug "UNKNOWN: #{data[:serverstring]}" |
|---|
| 774 | m = UnknownMessage.new(self, server, server, nil, data[:serverstring]) |
|---|
| 775 | @plugins.delegate "unknown_message", m |
|---|
| 776 | } |
|---|
| 777 | |
|---|
| 778 | set_default_send_options :newlines => @config['send.newlines'].to_sym, |
|---|
| 779 | :join_with => @config['send.join_with'].dup, |
|---|
| 780 | :max_lines => @config['send.max_lines'], |
|---|
| 781 | :overlong => @config['send.overlong'].to_sym, |
|---|
| 782 | :split_at => Regexp.new(@config['send.split_at']), |
|---|
| 783 | :purge_split => @config['send.purge_split'], |
|---|
| 784 | :truncate_text => @config['send.truncate_text'].dup |
|---|
| 785 | |
|---|
| 786 | trap_sigs |
|---|
| 787 | end |
|---|
| 788 | |
|---|
| 789 | def repopulate_botclass_directory |
|---|
| 790 | template_dir = File.join Config::datadir, 'templates' |
|---|
| 791 | if FileTest.directory? @botclass |
|---|
| 792 | # compare the templates dir with the current botclass dir, filling up the |
|---|
| 793 | # latter with any missing file. Sadly, FileUtils.cp_r doesn't have an |
|---|
| 794 | # :update option, so we have to do it manually. |
|---|
| 795 | # Note that we use the */** pattern because we don't want to match |
|---|
| 796 | # keywords.rbot, which gets deleted on load and would therefore be missing |
|---|
| 797 | # always |
|---|
| 798 | missing = Dir.chdir(template_dir) { Dir.glob('*/**') } - Dir.chdir(@botclass) { Dir.glob('*/**') } |
|---|
| 799 | missing.map do |f| |
|---|
| 800 | dest = File.join(@botclass, f) |
|---|
| 801 | FileUtils.mkdir_p(File.dirname(dest)) |
|---|
| 802 | FileUtils.cp File.join(template_dir, f), dest |
|---|
| 803 | end |
|---|
| 804 | else |
|---|
| 805 | log "no #{@botclass} directory found, creating from templates..." |
|---|
| 806 | if FileTest.exist? @botclass |
|---|
| 807 | error "file #{@botclass} exists but isn't a directory" |
|---|
| 808 | exit 2 |
|---|
| 809 | end |
|---|
| 810 | FileUtils.cp_r template_dir, @botclass |
|---|
| 811 | end |
|---|
| 812 | end |
|---|
| 813 | |
|---|
| 814 | # Return a path under the current botclass by joining the mentioned |
|---|
| 815 | # components. The components are automatically converted to String |
|---|
| 816 | def path(*components) |
|---|
| 817 | File.join(@botclass, *(components.map {|c| c.to_s})) |
|---|
| 818 | end |
|---|
| 819 | |
|---|
| 820 | def setup_plugins_path |
|---|
| 821 | plugdir_default = File.join(Config::datadir, 'plugins') |
|---|
| 822 | plugdir_local = File.join(@botclass, 'plugins') |
|---|
| 823 | Dir.mkdir(plugdir_local) unless File.exist?(plugdir_local) |
|---|
| 824 | |
|---|
| 825 | @plugins.clear_botmodule_dirs |
|---|
| 826 | @plugins.add_core_module_dir(File.join(Config::coredir, 'utils')) |
|---|
| 827 | @plugins.add_core_module_dir(Config::coredir) |
|---|
| 828 | if FileTest.directory? plugdir_local |
|---|
| 829 | @plugins.add_plugin_dir(plugdir_local) |
|---|
| 830 | else |
|---|
| 831 | warning "local plugin location #{plugdir_local} is not a directory" |
|---|
| 832 | end |
|---|
| 833 | |
|---|
| 834 | @config['plugins.path'].each do |_| |
|---|
| 835 | path = _.sub(/^\(default\)/, plugdir_default) |
|---|
| 836 | @plugins.add_plugin_dir(path) |
|---|
| 837 | end |
|---|
| 838 | end |
|---|
| 839 | |
|---|
| 840 | def set_default_send_options(opts={}) |
|---|
| 841 | # Default send options for NOTICE and PRIVMSG |
|---|
| 842 | unless defined? @default_send_options |
|---|
| 843 | @default_send_options = { |
|---|
| 844 | :queue_channel => nil, # use default queue channel |
|---|
| 845 | :queue_ring => nil, # use default queue ring |
|---|
| 846 | :newlines => :split, # or :join |
|---|
| 847 | :join_with => ' ', # by default, use a single space |
|---|
| 848 | :max_lines => 0, # maximum number of lines to send with a single command |
|---|
| 849 | :overlong => :split, # or :truncate |
|---|
| 850 | # TODO an array of splitpoints would be preferrable for this option: |
|---|
| 851 | :split_at => /\s+/, # by default, split overlong lines at whitespace |
|---|
| 852 | :purge_split => true, # should the split string be removed? |
|---|
| 853 | :truncate_text => "#{Reverse}...#{Reverse}" # text to be appened when truncating |
|---|
| 854 | } |
|---|
| 855 | end |
|---|
| 856 | @default_send_options.update opts unless opts.empty? |
|---|
| 857 | end |
|---|
| 858 | |
|---|
| 859 | # checks if we should be quiet on a channel |
|---|
| 860 | def quiet_on?(channel) |
|---|
| 861 | ch = channel.downcase |
|---|
| 862 | return (@quiet.include?('*') && !@not_quiet.include?(ch)) || @quiet.include?(ch) |
|---|
| 863 | end |
|---|
| 864 | |
|---|
| 865 | def set_quiet(channel = nil) |
|---|
| 866 | if channel |
|---|
| 867 | ch = channel.downcase.dup |
|---|
| 868 | @not_quiet.delete(ch) |
|---|
| 869 | @quiet << ch |
|---|
| 870 | else |
|---|
| 871 | @quiet.clear |
|---|
| 872 | @not_quiet.clear |
|---|
| 873 | @quiet << '*' |
|---|
| 874 | end |
|---|
| 875 | end |
|---|
| 876 | |
|---|
| 877 | def reset_quiet(channel = nil) |
|---|
| 878 | if channel |
|---|
| 879 | ch = channel.downcase.dup |
|---|
| 880 | @quiet.delete(ch) |
|---|
| 881 | @not_quiet << ch |
|---|
| 882 | else |
|---|
| 883 | @quiet.clear |
|---|
| 884 | @not_quiet.clear |
|---|
| 885 | end |
|---|
| 886 | end |
|---|
| 887 | |
|---|
| 888 | # things to do when we receive a signal |
|---|
| 889 | def got_sig(sig, func=:quit) |
|---|
| 890 | debug "received #{sig}, queueing #{func}" |
|---|
| 891 | # this is not an interruption if we just need to reconnect |
|---|
| 892 | $interrupted += 1 unless func == :reconnect |
|---|
| 893 | self.send(func) unless @quit_mutex.locked? |
|---|
| 894 | debug "interrupted #{$interrupted} times" |
|---|
| 895 | if $interrupted >= 3 |
|---|
| 896 | debug "drastic!" |
|---|
| 897 | log_session_end |
|---|
| 898 | exit 2 |
|---|
| 899 | end |
|---|
| 900 | end |
|---|
| 901 | |
|---|
| 902 | # trap signals |
|---|
| 903 | def trap_sigs |
|---|
| 904 | begin |
|---|
| 905 | trap("SIGINT") { got_sig("SIGINT") } |
|---|
| 906 | trap("SIGTERM") { got_sig("SIGTERM") } |
|---|
| 907 | trap("SIGHUP") { got_sig("SIGHUP", :restart) } |
|---|
| 908 | trap("SIGUSR1") { got_sig("SIGUSR1", :reconnect) } |
|---|
| 909 | rescue ArgumentError => e |
|---|
| 910 | debug "failed to trap signals (#{e.pretty_inspect}): running on Windows?" |
|---|
| 911 | rescue Exception => e |
|---|
| 912 | debug "failed to trap signals: #{e.pretty_inspect}" |
|---|
| 913 | end |
|---|
| 914 | end |
|---|
| 915 | |
|---|
| 916 | # connect the bot to IRC |
|---|
| 917 | def connect |
|---|
| 918 | # make sure we don't have any spurious ping checks running |
|---|
| 919 | # (and initialize the vars if this is the first time we connect) |
|---|
| 920 | stop_server_pings |
|---|
| 921 | begin |
|---|
| 922 | quit if $interrupted > 0 |
|---|
| 923 | @socket.connect |
|---|
| 924 | @last_rec = Time.now |
|---|
| 925 | rescue => e |
|---|
| 926 | raise e.class, "failed to connect to IRC server at #{@socket.server_uri}: #{e}" |
|---|
| 927 | end |
|---|
| 928 | quit if $interrupted > 0 |
|---|
| 929 | |
|---|
| 930 | realname = @config['irc.name'].clone || 'Ruby bot' |
|---|
| 931 | realname << ' ' + COPYRIGHT_NOTICE if @config['irc.name_copyright'] |
|---|
| 932 | |
|---|
| 933 | @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password'] |
|---|
| 934 | @socket.emergency_puts "NICK #{@config['irc.nick']}\nUSER #{@config['irc.user']} 4 #{@socket.server_uri.host} :#{realname}" |
|---|
| 935 | quit if $interrupted > 0 |
|---|
| 936 | myself.nick = @config['irc.nick'] |
|---|
| 937 | myself.user = @config['irc.user'] |
|---|
| 938 | end |
|---|
| 939 | |
|---|
| 940 | # disconnect the bot from IRC, if connected, and then connect (again) |
|---|
| 941 | def reconnect(message=nil, too_fast=false) |
|---|
| 942 | # we will wait only if @last_rec was not nil, i.e. if we were connected or |
|---|
| 943 | # got disconnected by a network error |
|---|
| 944 | # if someone wants to manually call disconnect() _and_ reconnect(), they |
|---|
| 945 | # will have to take care of the waiting themselves |
|---|
| 946 | will_wait = !!@last_rec |
|---|
| 947 | |
|---|
| 948 | if @socket.connected? |
|---|
| 949 | disconnect(message) |
|---|
| 950 | end |
|---|
| 951 | |
|---|
| 952 | begin |
|---|
| 953 | if will_wait |
|---|
| 954 | log "\n\nDisconnected\n\n" |
|---|
| 955 | |
|---|
| 956 | quit if $interrupted > 0 |
|---|
| 957 | |
|---|
| 958 | log "\n\nWaiting to reconnect\n\n" |
|---|
| 959 | sleep @config['server.reconnect_wait'] |
|---|
| 960 | sleep 10*@config['server.reconnect_wait'] if too_fast |
|---|
| 961 | end |
|---|
| 962 | |
|---|
| 963 | connect |
|---|
| 964 | rescue Exception => e |
|---|
| 965 | will_wait = true |
|---|
| 966 | retry |
|---|
| 967 | end |
|---|
| 968 | end |
|---|
| 969 | |
|---|
| 970 | # begin event handling loop |
|---|
| 971 | def mainloop |
|---|
| 972 | while true |
|---|
| 973 | too_fast = false |
|---|
| 974 | begin |
|---|
| 975 | quit_msg = nil |
|---|
| 976 | reconnect(quit_msg, too_fast) |
|---|
| 977 | quit if $interrupted > 0 |
|---|
| 978 | while @socket.connected? |
|---|
| 979 | quit if $interrupted > 0 |
|---|
| 980 | |
|---|
| 981 | # Wait for messages and process them as they arrive. If nothing is |
|---|
| 982 | # received, we call the ping_server() method that will PING the |
|---|
| 983 | # server if appropriate, or raise a TimeoutError if no PONG has been |
|---|
| 984 | # received in the user-chosen timeout since the last PING sent. |
|---|
| 985 | if @socket.select(1) |
|---|
| 986 | break unless reply = @socket.gets |
|---|
| 987 | @last_rec = Time.now |
|---|
| 988 | @client.process reply |
|---|
| 989 | else |
|---|
| 990 | ping_server |
|---|
| 991 | end |
|---|
| 992 | end |
|---|
| 993 | |
|---|
| 994 | # I despair of this. Some of my users get "connection reset by peer" |
|---|
| 995 | # exceptions that ARENT SocketError's. How am I supposed to handle |
|---|
| 996 | # that? |
|---|
| 997 | rescue SystemExit |
|---|
| 998 | log_session_end |
|---|
| 999 | exit 0 |
|---|
| 1000 | rescue Errno::ETIMEDOUT, Errno::ECONNABORTED, TimeoutError, SocketError => e |
|---|
| 1001 | error "network exception: #{e.pretty_inspect}" |
|---|
| 1002 | quit_msg = e.to_s |
|---|
| 1003 | rescue ServerError => e |
|---|
| 1004 | # received an ERROR from the server |
|---|
| 1005 | quit_msg = "server ERROR: " + e.message |
|---|
| 1006 | too_fast = e.message.index("reconnect too fast") |
|---|
| 1007 | retry |
|---|
| 1008 | rescue BDB::Fatal => e |
|---|
| 1009 | fatal "fatal bdb error: #{e.pretty_inspect}" |
|---|
| 1010 | DBTree.stats |
|---|
| 1011 | # Why restart? DB problems are serious stuff ... |
|---|
| 1012 | # restart("Oops, we seem to have registry problems ...") |
|---|
| 1013 | log_session_end |
|---|
| 1014 | exit 2 |
|---|
| 1015 | rescue Exception => e |
|---|
| 1016 | error "non-net exception: #{e.pretty_inspect}" |
|---|
| 1017 | quit_msg = e.to_s |
|---|
| 1018 | rescue => e |
|---|
| 1019 | fatal "unexpected exception: #{e.pretty_inspect}" |
|---|
| 1020 | log_session_end |
|---|
| 1021 | exit 2 |
|---|
| 1022 | end |
|---|
| 1023 | end |
|---|
| 1024 | end |
|---|
| 1025 | |
|---|
| 1026 | # type:: message type |
|---|
| 1027 | # where:: message target |
|---|
| 1028 | # message:: message text |
|---|
| 1029 | # send message +message+ of type +type+ to target +where+ |
|---|
| 1030 | # Type can be PRIVMSG, NOTICE, etc, but those you should really use the |
|---|
| 1031 | # relevant say() or notice() methods. This one should be used for IRCd |
|---|
| 1032 | # extensions you want to use in modules. |
|---|
| 1033 | def sendmsg(original_type, original_where, original_message, options={}) |
|---|
| 1034 | |
|---|
| 1035 | # filter message with sendmsg filters |
|---|
| 1036 | ds = DataStream.new original_message.to_s.dup, |
|---|
| 1037 | :type => original_type, :dest => original_where, |
|---|
| 1038 | :options => @default_send_options.merge(options) |
|---|
| 1039 | filters = filter_names(:sendmsg) |
|---|
| 1040 | filters.each do |fname| |
|---|
| 1041 | debug "filtering #{ds[:text]} with sendmsg filter #{fname}" |
|---|
| 1042 | ds.merge! filter(self.global_filter_name(fname, :sendmsg), ds) |
|---|
| 1043 | end |
|---|
| 1044 | |
|---|
| 1045 | opts = ds[:options] |
|---|
| 1046 | type = ds[:type] |
|---|
| 1047 | where = ds[:dest] |
|---|
| 1048 | filtered = ds[:text] |
|---|
| 1049 | |
|---|
| 1050 | # For starters, set up appropriate queue channels and rings |
|---|
| 1051 | mchan = opts[:queue_channel] |
|---|
| 1052 | mring = opts[:queue_ring] |
|---|
| 1053 | if mchan |
|---|
| 1054 | chan = mchan |
|---|
| 1055 | else |
|---|
| 1056 | chan = where |
|---|
| 1057 | end |
|---|
| 1058 | if mring |
|---|
| 1059 | ring = mring |
|---|
| 1060 | else |
|---|
| 1061 | case where |
|---|
| 1062 | when User |
|---|
| 1063 | ring = 1 |
|---|
| 1064 | else |
|---|
| 1065 | ring = 2 |
|---|
| 1066 | end |
|---|
| 1067 | end |
|---|
| 1068 | |
|---|
| 1069 | multi_line = filtered.gsub(/[\r\n]+/, "\n") |
|---|
| 1070 | |
|---|
| 1071 | # if target is a channel with nocolor modes, strip colours |
|---|
| 1072 | if where.kind_of?(Channel) and where.mode.any?(*config['server.nocolor_modes']) |
|---|
| 1073 | multi_line.replace BasicUserMessage.strip_formatting(multi_line) |
|---|
| 1074 | end |
|---|
| 1075 | |
|---|
| 1076 | messages = Array.new |
|---|
| 1077 | case opts[:newlines] |
|---|
| 1078 | when :join |
|---|
| 1079 | messages << [multi_line.gsub("\n", opts[:join_with])] |
|---|
| 1080 | when :split |
|---|
| 1081 | multi_line.each_line { |line| |
|---|
| 1082 | line.chomp! |
|---|
| 1083 | next unless(line.size > 0) |
|---|
| 1084 | messages << line |
|---|
| 1085 | } |
|---|
| 1086 | else |
|---|
| 1087 | raise "Unknown :newlines option #{opts[:newlines]} while sending #{original_message.inspect}" |
|---|
| 1088 | end |
|---|
| 1089 | |
|---|
| 1090 | # The IRC protocol requires that each raw message must be not longer |
|---|
| 1091 | # than 512 characters. From this length with have to subtract the EOL |
|---|
| 1092 | # terminators (CR+LF) and the length of ":botnick!botuser@bothost " |
|---|
| 1093 | # that will be prepended by the server to all of our messages. |
|---|
| 1094 | |
|---|
| 1095 | # The maximum raw message length we can send is therefore 512 - 2 - 2 |
|---|
| 1096 | # minus the length of our hostmask. |
|---|
| 1097 | |
|---|
| 1098 | max_len = 508 - myself.fullform.size |
|---|
| 1099 | |
|---|
| 1100 | # On servers that support IDENTIFY-MSG, we have to subtract 1, because messages |
|---|
| 1101 | # will have a + or - prepended |
|---|
| 1102 | if server.capabilities[:"identify-msg"] |
|---|
| 1103 | max_len -= 1 |
|---|
| 1104 | end |
|---|
| 1105 | |
|---|
| 1106 | # When splitting the message, we'll be prefixing the following string: |
|---|
| 1107 | # (e.g. "PRIVMSG #rbot :") |
|---|
| 1108 | fixed = "#{type} #{where} :" |
|---|
| 1109 | |
|---|
| 1110 | # And this is what's left |
|---|
| 1111 | left = max_len - fixed.size |
|---|
| 1112 | |
|---|
| 1113 | truncate = opts[:truncate_text] |
|---|
| 1114 | truncate = @default_send_options[:truncate_text] if truncate.size > left |
|---|
| 1115 | truncate = "" if truncate.size > left |
|---|
| 1116 | |
|---|
| 1117 | all_lines = messages.map { |line| |
|---|
| 1118 | if line.size < left |
|---|
| 1119 | line |
|---|
| 1120 | else |
|---|
| 1121 | case opts[:overlong] |
|---|
| 1122 | when :split |
|---|
| 1123 | msg = line.dup |
|---|
| 1124 | sub_lines = Array.new |
|---|
| 1125 | begin |
|---|
| 1126 | sub_lines << msg.slice!(0, left) |
|---|
| 1127 | break if msg.empty? |
|---|
| 1128 | lastspace = sub_lines.last.rindex(opts[:split_at]) |
|---|
| 1129 | if lastspace |
|---|
| 1130 | msg.replace sub_lines.last.slice!(lastspace, sub_lines.last.size) + msg |
|---|
| 1131 | msg.gsub!(/^#{opts[:split_at]}/, "") if opts[:purge_split] |
|---|
| 1132 | end |
|---|
| 1133 | end until msg.empty? |
|---|
| 1134 | sub_lines |
|---|
| 1135 | when :truncate |
|---|
| 1136 | line.slice(0, left - truncate.size) << truncate |
|---|
| 1137 | else |
|---|
| 1138 | raise "Unknown :overlong option #{opts[:overlong]} while sending #{original_message.inspect}" |
|---|
| 1139 | end |
|---|
| 1140 | end |
|---|
| 1141 | }.flatten |
|---|
| 1142 | |
|---|
| 1143 | if opts[:max_lines] > 0 and all_lines.length > opts[:max_lines] |
|---|
| 1144 | lines = all_lines[0...opts[:max_lines]] |
|---|
| 1145 | new_last = lines.last.slice(0, left - truncate.size) << truncate |
|---|
| 1146 | lines.last.replace(new_last) |
|---|
| 1147 | else |
|---|
| 1148 | lines = all_lines |
|---|
| 1149 | end |
|---|
| 1150 | |
|---|
| 1151 | lines.each { |line| |
|---|
| 1152 | sendq "#{fixed}#{line}", chan, ring |
|---|
| 1153 | delegate_sent(type, where, line) |
|---|
| 1154 | } |
|---|
| 1155 | end |
|---|
| 1156 | |
|---|
| 1157 | # queue an arbitraty message for the server |
|---|
| 1158 | def sendq(message="", chan=nil, ring=0) |
|---|
| 1159 | # temporary |
|---|
| 1160 | @socket.queue(message, chan, ring) |
|---|
| 1161 | end |
|---|
| 1162 | |
|---|
| 1163 | # send a notice message to channel/nick +where+ |
|---|
| 1164 | def notice(where, message, options={}) |
|---|
| 1165 | return if where.kind_of?(Channel) and quiet_on?(where) |
|---|
| 1166 | sendmsg "NOTICE", where, message, options |
|---|
| 1167 | end |
|---|
| 1168 | |
|---|
| 1169 | # say something (PRIVMSG) to channel/nick +where+ |
|---|
| 1170 | def say(where, message, options={}) |
|---|
| 1171 | return if where.kind_of?(Channel) and quiet_on?(where) |
|---|
| 1172 | sendmsg "PRIVMSG", where, message, options |
|---|
| 1173 | end |
|---|
| 1174 | |
|---|
| 1175 | def ctcp_notice(where, command, message, options={}) |
|---|
| 1176 | return if where.kind_of?(Channel) and quiet_on?(where) |
|---|
| 1177 | sendmsg "NOTICE", where, "\001#{command} #{message}\001", options |
|---|
| 1178 | end |
|---|
| 1179 | |
|---|
| 1180 | def ctcp_say(where, command, message, options={}) |
|---|
| 1181 | return if where.kind_of?(Channel) and quiet_on?(where) |
|---|
| 1182 | sendmsg "PRIVMSG", where, "\001#{command} #{message}\001", options |
|---|
| 1183 | end |
|---|
| 1184 | |
|---|
| 1185 | # perform a CTCP action with message +message+ to channel/nick +where+ |
|---|
| 1186 | def action(where, message, options={}) |
|---|
| 1187 | ctcp_say(where, 'ACTION', message, options) |
|---|
| 1188 | end |
|---|
| 1189 | |
|---|
| 1190 | # quick way to say "okay" (or equivalent) to +where+ |
|---|
| 1191 | def okay(where) |
|---|
| 1192 | say where, @lang.get("okay") |
|---|
| 1193 | end |
|---|
| 1194 | |
|---|
| 1195 | # set topic of channel +where+ to +topic+ |
|---|
| 1196 | # can also be used to retrieve the topic of channel +where+ |
|---|
| 1197 | # by omitting the last argument |
|---|
| 1198 | def topic(where, topic=nil) |
|---|
| 1199 | if topic.nil? |
|---|
| 1200 | sendq "TOPIC #{where}", where, 2 |
|---|
| 1201 | else |
|---|
| 1202 | sendq "TOPIC #{where} :#{topic}", where, 2 |
|---|
| 1203 | end |
|---|
| 1204 | end |
|---|
| 1205 | |
|---|
| 1206 | def disconnect(message=nil) |
|---|
| 1207 | message = @lang.get("quit") if (!message || message.empty?) |
|---|
| 1208 | if @socket.connected? |
|---|
| 1209 | begin |
|---|
| 1210 | debug "Clearing socket" |
|---|
| 1211 | @socket.clearq |
|---|
| 1212 | debug "Sending quit message" |
|---|
| 1213 | @socket.emergency_puts "QUIT :#{message}" |
|---|
| 1214 | debug "Logging quits" |
|---|
| 1215 | delegate_sent('QUIT', myself, message) |
|---|
| 1216 | debug "Flushing socket" |
|---|
| 1217 | @socket.flush |
|---|
| 1218 | rescue SocketError => e |
|---|
| 1219 | error "error while disconnecting socket: #{e.pretty_inspect}" |
|---|
| 1220 | end |
|---|
| 1221 | debug "Shutting down socket" |
|---|
| 1222 | @socket.shutdown |
|---|
| 1223 | end |
|---|
| 1224 | stop_server_pings |
|---|
| 1225 | @client.reset |
|---|
| 1226 | end |
|---|
| 1227 | |
|---|
| 1228 | # disconnect from the server and cleanup all plugins and modules |
|---|
| 1229 | def shutdown(message=nil) |
|---|
| 1230 | @quit_mutex.synchronize do |
|---|
| 1231 | debug "Shutting down: #{message}" |
|---|
| 1232 | ## No we don't restore them ... let everything run through |
|---|
| 1233 | # begin |
|---|
| 1234 | # trap("SIGINT", "DEFAULT") |
|---|
| 1235 | # trap("SIGTERM", "DEFAULT") |
|---|
| 1236 | # trap("SIGHUP", "DEFAULT") |
|---|
| 1237 | # rescue => e |
|---|
| 1238 | # debug "failed to restore signals: #{e.inspect}\nProbably running on windows?" |
|---|
| 1239 | # end |
|---|
| 1240 | debug "\tdisconnecting..." |
|---|
| 1241 | disconnect(message) |
|---|
| 1242 | debug "\tstopping timer..." |
|---|
| 1243 | @timer.stop |
|---|
| 1244 | debug "\tsaving ..." |
|---|
| 1245 | save |
|---|
| 1246 | debug "\tcleaning up ..." |
|---|
| 1247 | @save_mutex.synchronize do |
|---|
| 1248 | @plugins.cleanup |
|---|
| 1249 | end |
|---|
| 1250 | # debug "\tstopping timers ..." |
|---|
| 1251 | # @timer.stop |
|---|
| 1252 | # debug "Closing registries" |
|---|
| 1253 | # @registry.close |
|---|
| 1254 | debug "\t\tcleaning up the db environment ..." |
|---|
| 1255 | DBTree.cleanup_env |
|---|
| 1256 | log "rbot quit (#{message})" |
|---|
| 1257 | end |
|---|
| 1258 | end |
|---|
| 1259 | |
|---|
| 1260 | # message:: optional IRC quit message |
|---|
| 1261 | # quit IRC, shutdown the bot |
|---|
| 1262 | def quit(message=nil) |
|---|
| 1263 | begin |
|---|
| 1264 | shutdown(message) |
|---|
| 1265 | ensure |
|---|
| 1266 | exit 0 |
|---|
| 1267 | end |
|---|
| 1268 | end |
|---|
| 1269 | |
|---|
| 1270 | # totally shutdown and respawn the bot |
|---|
| 1271 | def restart(message=nil) |
|---|
| 1272 | message = _("restarting, back in %{wait}...") % { |
|---|
| 1273 | :wait => @config['server.reconnect_wait'] |
|---|
| 1274 | } if (!message || message.empty?) |
|---|
| 1275 | shutdown(message) |
|---|
| 1276 | sleep @config['server.reconnect_wait'] |
|---|
| 1277 | begin |
|---|
| 1278 | # now we re-exec |
|---|
| 1279 | # Note, this fails on Windows |
|---|
| 1280 | debug "going to exec #{$0} #{@argv.inspect} from #{@run_dir}" |
|---|
| 1281 | log_session_end |
|---|
| 1282 | Dir.chdir(@run_dir) |
|---|
| 1283 | exec($0, *@argv) |
|---|
| 1284 | rescue Errno::ENOENT |
|---|
| 1285 | log_session_end |
|---|
| 1286 | exec("ruby", *(@argv.unshift $0)) |
|---|
| 1287 | rescue Exception => e |
|---|
| 1288 | $interrupted += 1 |
|---|
| 1289 | raise e |
|---|
| 1290 | end |
|---|
| 1291 | end |
|---|
| 1292 | |
|---|
| 1293 | # call the save method for all of the botmodules |
|---|
| 1294 | def save |
|---|
| 1295 | @save_mutex.synchronize do |
|---|
| 1296 | @plugins.save |
|---|
| 1297 | DBTree.cleanup_logs |
|---|
| 1298 | end |
|---|
| 1299 | end |
|---|
| 1300 | |
|---|
| 1301 | # call the rescan method for all of the botmodules |
|---|
| 1302 | def rescan |
|---|
| 1303 | debug "\tstopping timer..." |
|---|
| 1304 | @timer.stop |
|---|
| 1305 | @save_mutex.synchronize do |
|---|
| 1306 | @lang.rescan |
|---|
| 1307 | @plugins.rescan |
|---|
| 1308 | end |
|---|
| 1309 | @timer.start |
|---|
| 1310 | end |
|---|
| 1311 | |
|---|
| 1312 | # channel:: channel to join |
|---|
| 1313 | # key:: optional channel key if channel is +s |
|---|
| 1314 | # join a channel |
|---|
| 1315 | def join(channel, key=nil) |
|---|
| 1316 | if(key) |
|---|
| 1317 | sendq "JOIN #{channel} :#{key}", channel, 2 |
|---|
| 1318 | else |
|---|
| 1319 | sendq "JOIN #{channel}", channel, 2 |
|---|
| 1320 | end |
|---|
| 1321 | end |
|---|
| 1322 | |
|---|
| 1323 | # part a channel |
|---|
| 1324 | def part(channel, message="") |
|---|
| 1325 | sendq "PART #{channel} :#{message}", channel, 2 |
|---|
| 1326 | end |
|---|
| 1327 | |
|---|
| 1328 | # attempt to change bot's nick to +name+ |
|---|
| 1329 | def nickchg(name) |
|---|
| 1330 | sendq "NICK #{name}" |
|---|
| 1331 | end |
|---|
| 1332 | |
|---|
| 1333 | # changing mode |
|---|
| 1334 | def mode(channel, mode, target=nil) |
|---|
| 1335 | sendq "MODE #{channel} #{mode} #{target}", channel, 2 |
|---|
| 1336 | end |
|---|
| 1337 | |
|---|
| 1338 | # asking whois |
|---|
| 1339 | def whois(nick, target=nil) |
|---|
| 1340 | sendq "WHOIS #{target} #{nick}", nil, 0 |
|---|
| 1341 | end |
|---|
| 1342 | |
|---|
| 1343 | # kicking a user |
|---|
| 1344 | def kick(channel, user, msg) |
|---|
| 1345 | sendq "KICK #{channel} #{user} :#{msg}", channel, 2 |
|---|
| 1346 | end |
|---|
| 1347 | |
|---|
| 1348 | # m:: message asking for help |
|---|
| 1349 | # topic:: optional topic help is requested for |
|---|
| 1350 | # respond to online help requests |
|---|
| 1351 | def help(topic=nil) |
|---|
| 1352 | topic = nil if topic == "" |
|---|
| 1353 | case topic |
|---|
| 1354 | when nil |
|---|
| 1355 | helpstr = _("help topics: ") |
|---|
| 1356 | helpstr += @plugins.helptopics |
|---|
| 1357 | helpstr += _(" (help <topic> for more info)") |
|---|
| 1358 | else |
|---|
| 1359 | unless(helpstr = @plugins.help(topic)) |
|---|
| 1360 | helpstr = _("no help for topic %{topic}") % { :topic => topic } |
|---|
| 1361 | end |
|---|
| 1362 | end |
|---|
| 1363 | return helpstr |
|---|
| 1364 | end |
|---|
| 1365 | |
|---|
| 1366 | # returns a string describing the current status of the bot (uptime etc) |
|---|
| 1367 | def status |
|---|
| 1368 | secs_up = Time.new - @startup_time |
|---|
| 1369 | uptime = Utils.secs_to_string secs_up |
|---|
| 1370 | # return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received." |
|---|
| 1371 | return (_("Uptime %{up}, %{plug} plugins active, %{sent} lines sent, %{recv} received.") % |
|---|
| 1372 | { |
|---|
| 1373 | :up => uptime, :plug => @plugins.length, |
|---|
| 1374 | :sent => @socket.lines_sent, :recv => @socket.lines_received |
|---|
| 1375 | }) |
|---|
| 1376 | end |
|---|
| 1377 | |
|---|
| 1378 | # We want to respond to a hung server in a timely manner. If nothing was received |
|---|
| 1379 | # in the user-selected timeout and we haven't PINGed the server yet, we PING |
|---|
| 1380 | # the server. If the PONG is not received within the user-defined timeout, we |
|---|
| 1381 | # assume we're in ping timeout and act accordingly. |
|---|
| 1382 | def ping_server |
|---|
| 1383 | act_timeout = @config['server.ping_timeout'] |
|---|
| 1384 | return if act_timeout <= 0 |
|---|
| 1385 | now = Time.now |
|---|
| 1386 | if @last_rec && now > @last_rec + act_timeout |
|---|
| 1387 | if @last_ping.nil? |
|---|
| 1388 | # No previous PING pending, send a new one |
|---|
| 1389 | sendq "PING :rbot" |
|---|
| 1390 | @last_ping = Time.now |
|---|
| 1391 | else |
|---|
| 1392 | diff = now - @last_ping |
|---|
| 1393 | if diff > act_timeout |
|---|
| 1394 | debug "no PONG from server in #{diff} seconds, reconnecting" |
|---|
| 1395 | # the actual reconnect is handled in the main loop: |
|---|
| 1396 | raise TimeoutError, "no PONG from server in #{diff} seconds" |
|---|
| 1397 | end |
|---|
| 1398 | end |
|---|
| 1399 | end |
|---|
| 1400 | end |
|---|
| 1401 | |
|---|
| 1402 | def stop_server_pings |
|---|
| 1403 | # cancel previous PINGs and reset time of last RECV |
|---|
| 1404 | @last_ping = nil |
|---|
| 1405 | @last_rec = nil |
|---|
| 1406 | end |
|---|
| 1407 | |
|---|
| 1408 | private |
|---|
| 1409 | |
|---|
| 1410 | # delegate sent messages |
|---|
| 1411 | def delegate_sent(type, where, message) |
|---|
| 1412 | args = [self, server, myself, server.user_or_channel(where.to_s), message] |
|---|
| 1413 | case type |
|---|
| 1414 | when "NOTICE" |
|---|
| 1415 | m = NoticeMessage.new(*args) |
|---|
| 1416 | when "PRIVMSG" |
|---|
| 1417 | m = PrivMessage.new(*args) |
|---|
| 1418 | when "QUIT" |
|---|
| 1419 | m = QuitMessage.new(*args) |
|---|
| 1420 | m.was_on = myself.channels |
|---|
| 1421 | end |
|---|
| 1422 | @plugins.delegate('sent', m) |
|---|
| 1423 | end |
|---|
| 1424 | |
|---|
| 1425 | end |
|---|
| 1426 | |
|---|
| 1427 | end |
|---|