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