Main bot class, which manages the various components, receives messages, handles them or passes them to plugins, and contains core functionality.
COPYRIGHT_NOTICE | = | "(c) Tom Gilbert and the rbot development team" |
SOURCE_URL | = | "http://ruby-rbot.org" |
auth | [R] | the bot‘s Auth data |
botclass | [R] | the botclass for this bot (determines configdir among other things) |
config | [R] | the bot‘s Config data |
httputil | [RW] | bot‘s httputil help object, for fetching resources via http. Sets up proxies etc as defined by the bot configuration/environment |
lang | [R] | bot‘s Language data |
plugins | [R] | bot‘s plugins. This is an instance of class Plugins |
registry | [R] | bot‘s object registry, plugins get an interface to this for persistant storage (hash interface tied to a bdb file, plugins use Accessors to store and restore objects in their own namespaces.) |
save_mutex | [R] | synchronize with this mutex while touching permanent data files: saving, flushing, cleaning up … |
socket | [R] | bot‘s irc socket TODO multiserver |
timer | [R] | used to perform actions periodically (saves configuration once per minute by default) |
create a new Bot with botclass botclass
# File lib/rbot/ircbot.rb, line 271 271: def initialize(botclass, params = {}) 272: # Config for the core bot 273: # TODO should we split socket stuff into ircsocket, etc? 274: Config.register Config::ArrayValue.new('server.list', 275: :default => ['irc://localhost'], :wizard => true, 276: :requires_restart => true, 277: :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.") 278: Config.register Config::BooleanValue.new('server.ssl', 279: :default => false, :requires_restart => true, :wizard => true, 280: :desc => "Use SSL to connect to this server?") 281: Config.register Config::StringValue.new('server.password', 282: :default => false, :requires_restart => true, 283: :desc => "Password for connecting to this server (if required)", 284: :wizard => true) 285: Config.register Config::StringValue.new('server.bindhost', 286: :default => false, :requires_restart => true, 287: :desc => "Specific local host or IP for the bot to bind to (if required)", 288: :wizard => true) 289: Config.register Config::IntegerValue.new('server.reconnect_wait', 290: :default => 5, :validate => Proc.new{|v| v >= 0}, 291: :desc => "Seconds to wait before attempting to reconnect, on disconnect") 292: Config.register Config::IntegerValue.new('server.ping_timeout', 293: :default => 30, :validate => Proc.new{|v| v >= 0}, 294: :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)") 295: Config.register Config::ArrayValue.new('server.nocolor_modes', 296: :default => ['c'], :wizard => false, 297: :requires_restart => false, 298: :desc => "List of channel modes that require messages to be without colors") 299: 300: Config.register Config::StringValue.new('irc.nick', :default => "rbot", 301: :desc => "IRC nickname the bot should attempt to use", :wizard => true, 302: :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" }) 303: Config.register Config::StringValue.new('irc.name', 304: :default => "Ruby bot", :requires_restart => true, 305: :desc => "IRC realname the bot should use") 306: Config.register Config::BooleanValue.new('irc.name_copyright', 307: :default => true, :requires_restart => true, 308: :desc => "Append copyright notice to bot realname? (please don't disable unless it's really necessary)") 309: Config.register Config::StringValue.new('irc.user', :default => "rbot", 310: :requires_restart => true, 311: :desc => "local user the bot should appear to be", :wizard => true) 312: Config.register Config::ArrayValue.new('irc.join_channels', 313: :default => [], :wizard => true, 314: :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'") 315: Config.register Config::ArrayValue.new('irc.ignore_users', 316: :default => [], 317: :desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people") 318: Config.register Config::ArrayValue.new('irc.ignore_channels', 319: :default => [], 320: :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)") 321: 322: Config.register Config::IntegerValue.new('core.save_every', 323: :default => 60, :validate => Proc.new{|v| v >= 0}, 324: :on_change => Proc.new { |bot, v| 325: if @save_timer 326: if v > 0 327: @timer.reschedule(@save_timer, v) 328: @timer.unblock(@save_timer) 329: else 330: @timer.block(@save_timer) 331: end 332: else 333: if v > 0 334: @save_timer = @timer.add(v) { bot.save } 335: end 336: # Nothing to do when v == 0 337: end 338: }, 339: :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)") 340: 341: Config.register Config::BooleanValue.new('core.run_as_daemon', 342: :default => false, :requires_restart => true, 343: :desc => "Should the bot run as a daemon?") 344: 345: Config.register Config::StringValue.new('log.file', 346: :default => false, :requires_restart => true, 347: :desc => "Name of the logfile to which console messages will be redirected when the bot is run as a daemon") 348: Config.register Config::IntegerValue.new('log.level', 349: :default => 1, :requires_restart => false, 350: :validate => Proc.new { |v| (0..5).include?(v) }, 351: :on_change => Proc.new { |bot, v| 352: $logger.level = v 353: }, 354: :desc => "The minimum logging level (0=DEBUG,1=INFO,2=WARN,3=ERROR,4=FATAL) for console messages") 355: Config.register Config::IntegerValue.new('log.keep', 356: :default => 1, :requires_restart => true, 357: :validate => Proc.new { |v| v >= 0 }, 358: :desc => "How many old console messages logfiles to keep") 359: Config.register Config::IntegerValue.new('log.max_size', 360: :default => 10, :requires_restart => true, 361: :validate => Proc.new { |v| v > 0 }, 362: :desc => "Maximum console messages logfile size (in megabytes)") 363: 364: Config.register Config::ArrayValue.new('plugins.path', 365: :wizard => true, :default => ['(default)', '(default)/games', '(default)/contrib'], 366: :requires_restart => false, 367: :on_change => Proc.new { |bot, v| bot.setup_plugins_path }, 368: :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") 369: 370: Config.register Config::EnumValue.new('send.newlines', 371: :values => ['split', 'join'], :default => 'split', 372: :on_change => Proc.new { |bot, v| 373: bot.set_default_send_options :newlines => v.to_sym 374: }, 375: :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") 376: Config.register Config::StringValue.new('send.join_with', 377: :default => ' ', 378: :on_change => Proc.new { |bot, v| 379: bot.set_default_send_options :join_with => v.dup 380: }, 381: :desc => "String used to replace newlines when send.newlines is set to join") 382: Config.register Config::IntegerValue.new('send.max_lines', 383: :default => 5, 384: :validate => Proc.new { |v| v >= 0 }, 385: :on_change => Proc.new { |bot, v| 386: bot.set_default_send_options :max_lines => v 387: }, 388: :desc => "Maximum number of IRC lines to send for each message (set to 0 for no limit)") 389: Config.register Config::EnumValue.new('send.overlong', 390: :values => ['split', 'truncate'], :default => 'split', 391: :on_change => Proc.new { |bot, v| 392: bot.set_default_send_options :overlong => v.to_sym 393: }, 394: :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") 395: Config.register Config::StringValue.new('send.split_at', 396: :default => '\s+', 397: :on_change => Proc.new { |bot, v| 398: bot.set_default_send_options :split_at => Regexp.new(v) 399: }, 400: :desc => "A regular expression that should match the split points for overlong messages (see send.overlong)") 401: Config.register Config::BooleanValue.new('send.purge_split', 402: :default => true, 403: :on_change => Proc.new { |bot, v| 404: bot.set_default_send_options :purge_split => v 405: }, 406: :desc => "Set to true if the splitting boundary (set in send.split_at) should be removed when splitting overlong messages (see send.overlong)") 407: Config.register Config::StringValue.new('send.truncate_text', 408: :default => "#{Reverse}...#{Reverse}", 409: :on_change => Proc.new { |bot, v| 410: bot.set_default_send_options :truncate_text => v.dup 411: }, 412: :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") 413: Config.register Config::IntegerValue.new('send.penalty_pct', 414: :default => 100, 415: :validate => Proc.new { |v| v >= 0 }, 416: :on_change => Proc.new { |bot, v| 417: bot.socket.penalty_pct = v 418: }, 419: :desc => "Percentage of IRC penalty to consider when sending messages to prevent being disconnected for excess flood. Set to 0 to disable penalty control.") 420: Config.register Config::StringValue.new('core.db', 421: :default => "bdb", 422: :wizard => true, :default => "bdb", 423: :validate => Proc.new { |v| ["bdb", "tc"].include? v }, 424: :requires_restart => true, 425: :desc => "DB adaptor to use for storing settings and plugin data. Options are: bdb (Berkeley DB, stable adaptor, but troublesome to install and unmaintained), tc (Tokyo Cabinet, new adaptor, fast and furious, but may be not available and contain bugs)") 426: 427: @argv = params[:argv] 428: @run_dir = params[:run_dir] || Dir.pwd 429: 430: unless FileTest.directory? Config::coredir 431: error "core directory '#{Config::coredir}' not found, did you setup.rb?" 432: exit 2 433: end 434: 435: unless FileTest.directory? Config::datadir 436: error "data directory '#{Config::datadir}' not found, did you setup.rb?" 437: exit 2 438: end 439: 440: unless botclass and not botclass.empty? 441: # We want to find a sensible default. 442: # * On POSIX systems we prefer ~/.rbot for the effective uid of the process 443: # * On Windows (at least the NT versions) we want to put our stuff in the 444: # Application Data folder. 445: # We don't use any particular O/S detection magic, exploiting the fact that 446: # Etc.getpwuid is nil on Windows 447: if Etc.getpwuid(Process::Sys.geteuid) 448: botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup 449: else 450: if ENV.has_key?('APPDATA') 451: botclass = ENV['APPDATA'].dup 452: botclass.gsub!("\\","/") 453: end 454: end 455: botclass = File.join(botclass, ".rbot") 456: end 457: botclass = File.expand_path(botclass) 458: @botclass = botclass.gsub(/\/$/, "") 459: 460: repopulate_botclass_directory 461: 462: registry_dir = File.join(@botclass, 'registry') 463: Dir.mkdir(registry_dir) unless File.exist?(registry_dir) 464: unless FileTest.directory? registry_dir 465: error "registry storage location #{registry_dir} is not a directory" 466: exit 2 467: end 468: save_dir = File.join(@botclass, 'safe_save') 469: Dir.mkdir(save_dir) unless File.exist?(save_dir) 470: unless FileTest.directory? save_dir 471: error "safe save location #{save_dir} is not a directory" 472: exit 2 473: end 474: 475: # Time at which the last PING was sent 476: @last_ping = nil 477: # Time at which the last line was RECV'd from the server 478: @last_rec = nil 479: 480: @startup_time = Time.new 481: 482: begin 483: @config = Config.manager 484: @config.bot_associate(self) 485: rescue Exception => e 486: fatal e 487: log_session_end 488: exit 2 489: end 490: 491: if @config['core.run_as_daemon'] 492: $daemonize = true 493: end 494: case @config["core.db"] 495: when "bdb" 496: require 'rbot/registry/bdb' 497: when "tc" 498: require 'rbot/registry/tc' 499: else 500: raise _("Unknown DB adaptor: %s") % @config["core.db"] 501: end 502: 503: @logfile = @config['log.file'] 504: if @logfile.class!=String || @logfile.empty? 505: logfname = File.basename(botclass).gsub(/^\.+/,'') 506: logfname << ".log" 507: @logfile = File.join(botclass, logfname) 508: debug "Using `#{@logfile}' as debug log" 509: end 510: 511: # See http://blog.humlab.umu.se/samuel/archives/000107.html 512: # for the backgrounding code 513: if $daemonize 514: begin 515: exit if fork 516: Process.setsid 517: exit if fork 518: rescue NotImplementedError 519: warning "Could not background, fork not supported" 520: rescue SystemExit 521: exit 0 522: rescue Exception => e 523: warning "Could not background. #{e.pretty_inspect}" 524: end 525: Dir.chdir botclass 526: # File.umask 0000 # Ensure sensible umask. Adjust as needed. 527: end 528: 529: logger = Logger.new(@logfile, 530: @config['log.keep'], 531: @config['log.max_size']*1024*1024) 532: logger.datetime_format= $dateformat 533: logger.level = @config['log.level'] 534: logger.level = $cl_loglevel if defined? $cl_loglevel 535: logger.level = 0 if $debug 536: 537: restart_logger(logger) 538: 539: log_session_start 540: 541: if $daemonize 542: log "Redirecting standard input/output/error" 543: [$stdin, $stdout, $stderr].each do |fd| 544: begin 545: fd.reopen "/dev/null" 546: rescue Errno::ENOENT 547: # On Windows, there's not such thing as /dev/null 548: fd.reopen "NUL" 549: end 550: end 551: 552: def $stdout.write(str=nil) 553: log str, 2 554: return str.to_s.size 555: end 556: def $stdout.write(str=nil) 557: if str.to_s.match(/:\d+: warning:/) 558: warning str, 2 559: else 560: error str, 2 561: end 562: return str.to_s.size 563: end 564: end 565: 566: File.open($opts['pidfile'] || File.join(@botclass, 'rbot.pid'), 'w') do |pf| 567: pf << "#{$$}\n" 568: end 569: 570: @registry = Registry.new self 571: 572: @timer = Timer.new 573: @save_mutex = Mutex.new 574: if @config['core.save_every'] > 0 575: @save_timer = @timer.add(@config['core.save_every']) { save } 576: else 577: @save_timer = nil 578: end 579: @quit_mutex = Mutex.new 580: 581: @plugins = nil 582: @lang = Language.new(self, @config['core.language']) 583: 584: begin 585: @auth = Auth::manager 586: @auth.bot_associate(self) 587: # @auth.load("#{botclass}/botusers.yaml") 588: rescue Exception => e 589: fatal e 590: log_session_end 591: exit 2 592: end 593: @auth.everyone.set_default_permission("*", true) 594: @auth.botowner.password= @config['auth.password'] 595: 596: @plugins = Plugins::manager 597: @plugins.bot_associate(self) 598: setup_plugins_path() 599: 600: if @config['server.name'] 601: debug "upgrading configuration (server.name => server.list)" 602: srv_uri = 'irc://' + @config['server.name'] 603: srv_uri += ":#{@config['server.port']}" if @config['server.port'] 604: @config.items['server.list'.to_sym].set_string(srv_uri) 605: @config.delete('server.name'.to_sym) 606: @config.delete('server.port'.to_sym) 607: debug "server.list is now #{@config['server.list'].inspect}" 608: end 609: 610: @socket = Irc::Socket.new(@config['server.list'], @config['server.bindhost'], :ssl => @config['server.ssl'], :penalty_pct =>@config['send.penalty_pct']) 611: @client = Client.new 612: 613: @plugins.scan 614: 615: # Channels where we are quiet 616: # Array of channels names where the bot should be quiet 617: # '*' means all channels 618: # 619: @quiet = Set.new 620: # but we always speak here 621: @not_quiet = Set.new 622: 623: # the nick we want, if it's different from the irc.nick config value 624: # (e.g. as set by a !nick command) 625: @wanted_nick = nil 626: 627: @client[:welcome] = proc {|data| 628: m = WelcomeMessage.new(self, server, data[:source], data[:target], data[:message]) 629: 630: @plugins.delegate("welcome", m) 631: @plugins.delegate("connect") 632: } 633: 634: # TODO the next two @client should go into rfc2812.rb, probably 635: # Since capabs are two-steps processes, server.supports[:capab] 636: # should be a three-state: nil, [], [....] 637: asked_for = { "identify-msg""identify-msg" => false } 638: @client[:isupport] = proc { |data| 639: if server.supports[:capab] and !asked_for["identify-msg""identify-msg"] 640: sendq "CAPAB IDENTIFY-MSG" 641: asked_for["identify-msg""identify-msg"] = true 642: end 643: } 644: @client[:datastr] = proc { |data| 645: if data[:text] == "IDENTIFY-MSG" 646: server.capabilities["identify-msg""identify-msg"] = true 647: else 648: debug "Not handling RPL_DATASTR #{data[:servermessage]}" 649: end 650: } 651: 652: @client[:privmsg] = proc { |data| 653: m = PrivMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true) 654: # debug "Message source is #{data[:source].inspect}" 655: # debug "Message target is #{data[:target].inspect}" 656: # debug "Bot is #{myself.inspect}" 657: 658: @config['irc.ignore_channels'].each { |channel| 659: if m.target.downcase == channel.downcase 660: m.ignored = true 661: break 662: end 663: } 664: @config['irc.ignore_users'].each { |mask| 665: if m.source.matches?(server.new_netmask(mask)) 666: m.ignored = true 667: break 668: end 669: } unless m.ignored 670: 671: @plugins.irc_delegate('privmsg', m) 672: } 673: @client[:notice] = proc { |data| 674: message = NoticeMessage.new(self, server, data[:source], data[:target], data[:message], :handle_id => true) 675: # pass it off to plugins that want to hear everything 676: @plugins.irc_delegate "notice", message 677: } 678: @client[:motd] = proc { |data| 679: m = MotdMessage.new(self, server, data[:source], data[:target], data[:motd]) 680: @plugins.delegate "motd", m 681: } 682: @client[:nicktaken] = proc { |data| 683: new = "#{data[:nick]}_" 684: nickchg new 685: # If we're setting our nick at connection because our choice was taken, 686: # we have to fix our nick manually, because there will be no NICK message 687: # to inform us that our nick has been changed. 688: if data[:target] == '*' 689: debug "setting my connection nick to #{new}" 690: nick = new 691: end 692: @plugins.delegate "nicktaken", data[:nick] 693: } 694: @client[:badnick] = proc {|data| 695: warning "bad nick (#{data[:nick]})" 696: } 697: @client[:ping] = proc {|data| 698: sendq "PONG #{data[:pingid]}" 699: } 700: @client[:pong] = proc {|data| 701: @last_ping = nil 702: } 703: @client[:nick] = proc {|data| 704: # debug "Message source is #{data[:source].inspect}" 705: # debug "Bot is #{myself.inspect}" 706: source = data[:source] 707: old = data[:oldnick] 708: new = data[:newnick] 709: m = NickMessage.new(self, server, source, old, new) 710: m.is_on = data[:is_on] 711: if source == myself 712: debug "my nick is now #{new}" 713: end 714: @plugins.irc_delegate("nick", m) 715: } 716: @client[:quit] = proc {|data| 717: source = data[:source] 718: message = data[:message] 719: m = QuitMessage.new(self, server, source, source, message) 720: m.was_on = data[:was_on] 721: @plugins.irc_delegate("quit", m) 722: } 723: @client[:mode] = proc {|data| 724: m = ModeChangeMessage.new(self, server, data[:source], data[:target], data[:modestring]) 725: m.modes = data[:modes] 726: @plugins.delegate "modechange", m 727: } 728: @client[:whois] = proc {|data| 729: source = data[:source] 730: target = server.get_user(data[:whois][:nick]) 731: m = WhoisMessage.new(self, server, source, target, data[:whois]) 732: @plugins.delegate "whois", m 733: } 734: @client[:join] = proc {|data| 735: m = JoinMessage.new(self, server, data[:source], data[:channel], data[:message]) 736: sendq("MODE #{data[:channel]}", nil, 0) if m.address? 737: @plugins.irc_delegate("join", m) 738: sendq("WHO #{data[:channel]}", data[:channel], 2) if m.address? 739: } 740: @client[:part] = proc {|data| 741: m = PartMessage.new(self, server, data[:source], data[:channel], data[:message]) 742: @plugins.irc_delegate("part", m) 743: } 744: @client[:kick] = proc {|data| 745: m = KickMessage.new(self, server, data[:source], data[:target], data[:channel],data[:message]) 746: @plugins.irc_delegate("kick", m) 747: } 748: @client[:invite] = proc {|data| 749: m = InviteMessage.new(self, server, data[:source], data[:target], data[:channel]) 750: @plugins.irc_delegate("invite", m) 751: } 752: @client[:changetopic] = proc {|data| 753: m = TopicMessage.new(self, server, data[:source], data[:channel], data[:topic]) 754: m.info_or_set = :set 755: @plugins.irc_delegate("topic", m) 756: } 757: # @client[:topic] = proc { |data| 758: # irclog "@ Topic is \"#{data[:topic]}\"", data[:channel] 759: # } 760: @client[:topicinfo] = proc { |data| 761: channel = data[:channel] 762: topic = channel.topic 763: m = TopicMessage.new(self, server, data[:source], channel, topic) 764: m.info_or_set = :info 765: @plugins.irc_delegate("topic", m) 766: } 767: @client[:names] = proc { |data| 768: m = NamesMessage.new(self, server, server, data[:channel]) 769: m.users = data[:users] 770: @plugins.delegate "names", m 771: } 772: @client[:banlist] = proc { |data| 773: m = BanlistMessage.new(self, server, server, data[:channel]) 774: m.bans = data[:bans] 775: @plugins.delegate "banlist", m 776: } 777: @client[:nosuchtarget] = proc { |data| 778: m = NoSuchTargetMessage.new(self, server, server, data[:target], data[:message]) 779: @plugins.delegate "nosuchtarget", m 780: } 781: @client[:error] = proc { |data| 782: raise ServerError, data[:message] 783: } 784: @client[:unknown] = proc { |data| 785: #debug "UNKNOWN: #{data[:serverstring]}" 786: m = UnknownMessage.new(self, server, server, nil, data[:serverstring]) 787: @plugins.delegate "unknown_message", m 788: } 789: 790: set_default_send_options :newlines => @config['send.newlines'].to_sym, 791: :join_with => @config['send.join_with'].dup, 792: :max_lines => @config['send.max_lines'], 793: :overlong => @config['send.overlong'].to_sym, 794: :split_at => Regexp.new(@config['send.split_at']), 795: :purge_split => @config['send.purge_split'], 796: :truncate_text => @config['send.truncate_text'].dup 797: 798: trap_sigs 799: end
connect the bot to IRC
# File lib/rbot/ircbot.rb, line 929 929: def connect 930: # make sure we don't have any spurious ping checks running 931: # (and initialize the vars if this is the first time we connect) 932: stop_server_pings 933: begin 934: quit if $interrupted > 0 935: @socket.connect 936: @last_rec = Time.now 937: rescue => e 938: raise e.class, "failed to connect to IRC server at #{@socket.server_uri}: #{e}" 939: end 940: quit if $interrupted > 0 941: 942: realname = @config['irc.name'].clone || 'Ruby bot' 943: realname << ' ' + COPYRIGHT_NOTICE if @config['irc.name_copyright'] 944: 945: @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password'] 946: @socket.emergency_puts "NICK #{@config['irc.nick']}\nUSER #{@config['irc.user']} 4 #{@socket.server_uri.host} :#{realname}" 947: quit if $interrupted > 0 948: myself.nick = @config['irc.nick'] 949: myself.user = @config['irc.user'] 950: end
# File lib/rbot/ircbot.rb, line 1187 1187: def ctcp_notice(where, command, message, options={}) 1188: return if where.kind_of?(Channel) and quiet_on?(where) 1189: sendmsg "NOTICE", where, "\001#{command} #{message}\001", options 1190: end
# File lib/rbot/ircbot.rb, line 1192 1192: def ctcp_say(where, command, message, options={}) 1193: return if where.kind_of?(Channel) and quiet_on?(where) 1194: sendmsg "PRIVMSG", where, "\001#{command} #{message}\001", options 1195: end
# File lib/rbot/ircbot.rb, line 1218 1218: def disconnect(message=nil) 1219: message = @lang.get("quit") if (!message || message.empty?) 1220: if @socket.connected? 1221: begin 1222: debug "Clearing socket" 1223: @socket.clearq 1224: debug "Sending quit message" 1225: @socket.emergency_puts "QUIT :#{message}" 1226: debug "Logging quits" 1227: delegate_sent('QUIT', myself, message) 1228: debug "Flushing socket" 1229: @socket.flush 1230: rescue SocketError => e 1231: error "error while disconnecting socket: #{e.pretty_inspect}" 1232: end 1233: debug "Shutting down socket" 1234: @socket.shutdown 1235: end 1236: stop_server_pings 1237: @client.reset 1238: end
things to do when we receive a signal
# File lib/rbot/ircbot.rb, line 901 901: def got_sig(sig, func=:quit) 902: debug "received #{sig}, queueing #{func}" 903: # this is not an interruption if we just need to reconnect 904: $interrupted += 1 unless func == :reconnect 905: self.send(func) unless @quit_mutex.locked? 906: debug "interrupted #{$interrupted} times" 907: if $interrupted >= 3 908: debug "drastic!" 909: log_session_end 910: exit 2 911: end 912: end
m: | message asking for help |
topic: | optional topic help is requested for |
respond to online help requests
# File lib/rbot/ircbot.rb, line 1363 1363: def help(topic=nil) 1364: topic = nil if topic == "" 1365: case topic 1366: when nil 1367: helpstr = _("help topics: ") 1368: helpstr += @plugins.helptopics 1369: helpstr += _(" (help <topic> for more info)") 1370: else 1371: unless(helpstr = @plugins.help(topic)) 1372: helpstr = _("no help for topic %{topic}") % { :topic => topic } 1373: end 1374: end 1375: return helpstr 1376: end
bot inspection TODO multiserver
# File lib/rbot/ircbot.rb, line 240 240: def inspect 241: ret = self.to_s[0..-2] 242: ret << ' version=' << $version.inspect 243: ret << ' botclass=' << botclass.inspect 244: ret << ' lang="' << lang.language 245: if defined?(GetText) 246: ret << '/' << locale 247: end 248: ret << '"' 249: ret << ' nick=' << nick.inspect 250: ret << ' server=' 251: if server 252: ret << (server.to_s + (socket ? 253: ' [' << socket.server_uri.to_s << ']' : '')).inspect 254: unless server.channels.empty? 255: ret << " channels=" 256: ret << server.channels.map { |c| 257: "%s%s" % [c.modes_of(nick).map { |m| 258: server.prefix_for_mode(m) 259: }, c.name] 260: }.inspect 261: end 262: else 263: ret << '(none)' 264: end 265: ret << ' plugins=' << plugins.inspect 266: ret << ">" 267: end
kicking a user
# File lib/rbot/ircbot.rb, line 1356 1356: def kick(channel, user, msg) 1357: sendq "KICK #{channel} #{user} :#{msg}", channel, 2 1358: end
begin event handling loop
# File lib/rbot/ircbot.rb, line 983 983: def mainloop 984: while true 985: too_fast = false 986: begin 987: quit_msg = nil 988: reconnect(quit_msg, too_fast) 989: quit if $interrupted > 0 990: while @socket.connected? 991: quit if $interrupted > 0 992: 993: # Wait for messages and process them as they arrive. If nothing is 994: # received, we call the ping_server() method that will PING the 995: # server if appropriate, or raise a TimeoutError if no PONG has been 996: # received in the user-chosen timeout since the last PING sent. 997: if @socket.select(1) 998: break unless reply = @socket.gets 999: @last_rec = Time.now 1000: @client.process reply 1001: else 1002: ping_server 1003: end 1004: end 1005: 1006: # I despair of this. Some of my users get "connection reset by peer" 1007: # exceptions that ARENT SocketError's. How am I supposed to handle 1008: # that? 1009: rescue SystemExit 1010: log_session_end 1011: exit 0 1012: rescue Errno::ETIMEDOUT, Errno::ECONNABORTED, TimeoutError, SocketError => e 1013: error "network exception: #{e.pretty_inspect}" 1014: quit_msg = e.to_s 1015: rescue ServerError => e 1016: # received an ERROR from the server 1017: quit_msg = "server ERROR: " + e.message 1018: too_fast = e.message.index("reconnect too fast") 1019: retry 1020: rescue BDB::Fatal => e 1021: fatal "fatal bdb error: #{e.pretty_inspect}" 1022: DBTree.stats 1023: # Why restart? DB problems are serious stuff ... 1024: # restart("Oops, we seem to have registry problems ...") 1025: log_session_end 1026: exit 2 1027: rescue Exception => e 1028: error "non-net exception: #{e.pretty_inspect}" 1029: quit_msg = e.to_s 1030: rescue => e 1031: fatal "unexpected exception: #{e.pretty_inspect}" 1032: log_session_end 1033: exit 2 1034: end 1035: end 1036: end
We want to respond to a hung server in a timely manner. If nothing was received in the user-selected timeout and we haven‘t PINGed the server yet, we PING the server. If the PONG is not received within the user-defined timeout, we assume we‘re in ping timeout and act accordingly.
# File lib/rbot/ircbot.rb, line 1394 1394: def ping_server 1395: act_timeout = @config['server.ping_timeout'] 1396: return if act_timeout <= 0 1397: now = Time.now 1398: if @last_rec && now > @last_rec + act_timeout 1399: if @last_ping.nil? 1400: # No previous PING pending, send a new one 1401: sendq "PING :rbot" 1402: @last_ping = Time.now 1403: else 1404: diff = now - @last_ping 1405: if diff > act_timeout 1406: debug "no PONG from server in #{diff} seconds, reconnecting" 1407: # the actual reconnect is handled in the main loop: 1408: raise TimeoutError, "no PONG from server in #{diff} seconds" 1409: end 1410: end 1411: end 1412: end
checks if we should be quiet on a channel
# File lib/rbot/ircbot.rb, line 872 872: def quiet_on?(channel) 873: ch = channel.downcase 874: return (@quiet.include?('*') && !@not_quiet.include?(ch)) || @quiet.include?(ch) 875: end
disconnect the bot from IRC, if connected, and then connect (again)
# File lib/rbot/ircbot.rb, line 953 953: def reconnect(message=nil, too_fast=false) 954: # we will wait only if @last_rec was not nil, i.e. if we were connected or 955: # got disconnected by a network error 956: # if someone wants to manually call disconnect() _and_ reconnect(), they 957: # will have to take care of the waiting themselves 958: will_wait = !!@last_rec 959: 960: if @socket.connected? 961: disconnect(message) 962: end 963: 964: begin 965: if will_wait 966: log "\n\nDisconnected\n\n" 967: 968: quit if $interrupted > 0 969: 970: log "\n\nWaiting to reconnect\n\n" 971: sleep @config['server.reconnect_wait'] 972: sleep 10*@config['server.reconnect_wait'] if too_fast 973: end 974: 975: connect 976: rescue Exception => e 977: will_wait = true 978: retry 979: end 980: end
# File lib/rbot/ircbot.rb, line 801 801: def repopulate_botclass_directoryrepopulate_botclass_directoryrepopulate_botclass_directory 802: template_dir = File.join Config::datadir, 'templates' 803: if FileTest.directory? @botclass 804: # compare the templates dir with the current botclass dir, filling up the 805: # latter with any missing file. Sadly, FileUtils.cp_r doesn't have an 806: # :update option, so we have to do it manually. 807: # Note that we use the */** pattern because we don't want to match 808: # keywords.rbot, which gets deleted on load and would therefore be missing 809: # always 810: missing = Dir.chdir(template_dir) { Dir.glob('*/**') } - Dir.chdir(@botclass) { Dir.glob('*/**') } 811: missing.map do |f| 812: dest = File.join(@botclass, f) 813: FileUtils.mkdir_p(File.dirname(dest)) 814: FileUtils.cp File.join(template_dir, f), dest 815: end 816: else 817: log "no #{@botclass} directory found, creating from templates..." 818: if FileTest.exist? @botclass 819: error "file #{@botclass} exists but isn't a directory" 820: exit 2 821: end 822: FileUtils.cp_r template_dir, @botclass 823: end 824: end
# File lib/rbot/ircbot.rb, line 889 889: def reset_quiet(channel = nil) 890: if channel 891: ch = channel.downcase.dup 892: @quiet.delete(ch) 893: @not_quiet << ch 894: else 895: @quiet.clear 896: @not_quiet.clear 897: end 898: end
totally shutdown and respawn the bot
# File lib/rbot/ircbot.rb, line 1283 1283: def restart(message=nil) 1284: message = _("restarting, back in %{wait}...") % { 1285: :wait => @config['server.reconnect_wait'] 1286: } if (!message || message.empty?) 1287: shutdown(message) 1288: sleep @config['server.reconnect_wait'] 1289: begin 1290: # now we re-exec 1291: # Note, this fails on Windows 1292: debug "going to exec #{$0} #{@argv.inspect} from #{@run_dir}" 1293: log_session_end 1294: Dir.chdir(@run_dir) 1295: exec($0, *@argv) 1296: rescue Errno::ENOENT 1297: log_session_end 1298: exec("ruby", *(@argv.unshift $0)) 1299: rescue Exception => e 1300: $interrupted += 1 1301: raise e 1302: end 1303: end
type: | message type |
where: | message target |
message: | message text |
send message message of type type to target where Type can be PRIVMSG, NOTICE, etc, but those you should really use the relevant say() or notice() methods. This one should be used for IRCd extensions you want to use in modules.
# File lib/rbot/ircbot.rb, line 1045 1045: def sendmsg(original_type, original_where, original_message, options={}) 1046: 1047: # filter message with sendmsg filters 1048: ds = DataStream.new original_message.to_s.dup, 1049: :type => original_type, :dest => original_where, 1050: :options => @default_send_options.merge(options) 1051: filters = filter_names(:sendmsg) 1052: filters.each do |fname| 1053: debug "filtering #{ds[:text]} with sendmsg filter #{fname}" 1054: ds.merge! filter(self.global_filter_name(fname, :sendmsg), ds) 1055: end 1056: 1057: opts = ds[:options] 1058: type = ds[:type] 1059: where = ds[:dest] 1060: filtered = ds[:text] 1061: 1062: # For starters, set up appropriate queue channels and rings 1063: mchan = opts[:queue_channel] 1064: mring = opts[:queue_ring] 1065: if mchan 1066: chan = mchan 1067: else 1068: chan = where 1069: end 1070: if mring 1071: ring = mring 1072: else 1073: case where 1074: when User 1075: ring = 1 1076: else 1077: ring = 2 1078: end 1079: end 1080: 1081: multi_line = filtered.gsub(/[\r\n]+/, "\n") 1082: 1083: # if target is a channel with nocolor modes, strip colours 1084: if where.kind_of?(Channel) and where.mode.any?(*config['server.nocolor_modes']) 1085: multi_line.replace BasicUserMessage.strip_formatting(multi_line) 1086: end 1087: 1088: messages = Array.new 1089: case opts[:newlines] 1090: when :join 1091: messages << [multi_line.gsub("\n", opts[:join_with])] 1092: when :split 1093: multi_line.each_line { |line| 1094: line.chomp! 1095: next unless(line.size > 0) 1096: messages << line 1097: } 1098: else 1099: raise "Unknown :newlines option #{opts[:newlines]} while sending #{original_message.inspect}" 1100: end 1101: 1102: # The IRC protocol requires that each raw message must be not longer 1103: # than 512 characters. From this length with have to subtract the EOL 1104: # terminators (CR+LF) and the length of ":botnick!botuser@bothost " 1105: # that will be prepended by the server to all of our messages. 1106: 1107: # The maximum raw message length we can send is therefore 512 - 2 - 2 1108: # minus the length of our hostmask. 1109: 1110: max_len = 508 - myself.fullform.size 1111: 1112: # On servers that support IDENTIFY-MSG, we have to subtract 1, because messages 1113: # will have a + or - prepended 1114: if server.capabilities["identify-msg""identify-msg"] 1115: max_len -= 1 1116: end 1117: 1118: # When splitting the message, we'll be prefixing the following string: 1119: # (e.g. "PRIVMSG #rbot :") 1120: fixed = "#{type} #{where} :" 1121: 1122: # And this is what's left 1123: left = max_len - fixed.size 1124: 1125: truncate = opts[:truncate_text] 1126: truncate = @default_send_options[:truncate_text] if truncate.size > left 1127: truncate = "" if truncate.size > left 1128: 1129: all_lines = messages.map { |line| 1130: if line.size < left 1131: line 1132: else 1133: case opts[:overlong] 1134: when :split 1135: msg = line.dup 1136: sub_lines = Array.new 1137: begin 1138: sub_lines << msg.slice!(0, left) 1139: break if msg.empty? 1140: lastspace = sub_lines.last.rindex(opts[:split_at]) 1141: if lastspace 1142: msg.replace sub_lines.last.slice!(lastspace, sub_lines.last.size) + msg 1143: msg.gsub!(/^#{opts[:split_at]}/, "") if opts[:purge_split] 1144: end 1145: end until msg.empty? 1146: sub_lines 1147: when :truncate 1148: line.slice(0, left - truncate.size) << truncate 1149: else 1150: raise "Unknown :overlong option #{opts[:overlong]} while sending #{original_message.inspect}" 1151: end 1152: end 1153: }.flatten 1154: 1155: if opts[:max_lines] > 0 and all_lines.length > opts[:max_lines] 1156: lines = all_lines[0...opts[:max_lines]] 1157: new_last = lines.last.slice(0, left - truncate.size) << truncate 1158: lines.last.replace(new_last) 1159: else 1160: lines = all_lines 1161: end 1162: 1163: lines.each { |line| 1164: sendq "#{fixed}#{line}", chan, ring 1165: delegate_sent(type, where, line) 1166: } 1167: end
# File lib/rbot/ircbot.rb, line 852 852: def set_default_send_options(opts={}) 853: # Default send options for NOTICE and PRIVMSG 854: unless defined? @default_send_options 855: @default_send_options = { 856: :queue_channel => nil, # use default queue channel 857: :queue_ring => nil, # use default queue ring 858: :newlines => :split, # or :join 859: :join_with => ' ', # by default, use a single space 860: :max_lines => 0, # maximum number of lines to send with a single command 861: :overlong => :split, # or :truncate 862: # TODO an array of splitpoints would be preferrable for this option: 863: :split_at => /\s+/, # by default, split overlong lines at whitespace 864: :purge_split => true, # should the split string be removed? 865: :truncate_text => "#{Reverse}...#{Reverse}" # text to be appened when truncating 866: } 867: end 868: @default_send_options.update opts unless opts.empty? 869: end
# File lib/rbot/ircbot.rb, line 877 877: def set_quiet(channel = nil) 878: if channel 879: ch = channel.downcase.dup 880: @not_quiet.delete(ch) 881: @quiet << ch 882: else 883: @quiet.clear 884: @not_quiet.clear 885: @quiet << '*' 886: end 887: end
# File lib/rbot/ircbot.rb, line 832 832: def setup_plugins_path 833: plugdir_default = File.join(Config::datadir, 'plugins') 834: plugdir_local = File.join(@botclass, 'plugins') 835: Dir.mkdir(plugdir_local) unless File.exist?(plugdir_local) 836: 837: @plugins.clear_botmodule_dirs 838: @plugins.add_core_module_dir(File.join(Config::coredir, 'utils')) 839: @plugins.add_core_module_dir(Config::coredir) 840: if FileTest.directory? plugdir_local 841: @plugins.add_plugin_dir(plugdir_local) 842: else 843: warning "local plugin location #{plugdir_local} is not a directory" 844: end 845: 846: @config['plugins.path'].each do |_| 847: path = _.sub(/^\(default\)/, plugdir_default) 848: @plugins.add_plugin_dir(path) 849: end 850: end
disconnect from the server and cleanup all plugins and modules
# File lib/rbot/ircbot.rb, line 1241 1241: def shutdown(message=nil) 1242: @quit_mutex.synchronize do 1243: debug "Shutting down: #{message}" 1244: ## No we don't restore them ... let everything run through 1245: # begin 1246: # trap("SIGINT", "DEFAULT") 1247: # trap("SIGTERM", "DEFAULT") 1248: # trap("SIGHUP", "DEFAULT") 1249: # rescue => e 1250: # debug "failed to restore signals: #{e.inspect}\nProbably running on windows?" 1251: # end 1252: debug "\tdisconnecting..." 1253: disconnect(message) 1254: debug "\tstopping timer..." 1255: @timer.stop 1256: debug "\tsaving ..." 1257: save 1258: debug "\tcleaning up ..." 1259: @save_mutex.synchronize do 1260: @plugins.cleanup 1261: end 1262: # debug "\tstopping timers ..." 1263: # @timer.stop 1264: # debug "Closing registries" 1265: # @registry.close 1266: debug "\t\tcleaning up the db environment ..." 1267: DBTree.cleanup_env 1268: log "rbot quit (#{message})" 1269: end 1270: end
returns a string describing the current status of the bot (uptime etc)
# File lib/rbot/ircbot.rb, line 1379 1379: def status 1380: secs_up = Time.new - @startup_time 1381: uptime = Utils.secs_to_string secs_up 1382: # return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received." 1383: return (_("Uptime %{up}, %{plug} plugins active, %{sent} lines sent, %{recv} received.") % 1384: { 1385: :up => uptime, :plug => @plugins.length, 1386: :sent => @socket.lines_sent, :recv => @socket.lines_received 1387: }) 1388: end
# File lib/rbot/ircbot.rb, line 1414 1414: def stop_server_pings 1415: # cancel previous PINGs and reset time of last RECV 1416: @last_ping = nil 1417: @last_rec = nil 1418: end
set topic of channel where to topic can also be used to retrieve the topic of channel where by omitting the last argument
# File lib/rbot/ircbot.rb, line 1210 1210: def topic(where, topic=nil) 1211: if topic.nil? 1212: sendq "TOPIC #{where}", where, 2 1213: else 1214: sendq "TOPIC #{where} :#{topic}", where, 2 1215: end 1216: end
trap signals
# File lib/rbot/ircbot.rb, line 915 915: def trap_sigs 916: begin 917: trap("SIGINT") { got_sig("SIGINT") } 918: trap("SIGTERM") { got_sig("SIGTERM") } 919: trap("SIGHUP") { got_sig("SIGHUP", :restart) } 920: trap("SIGUSR1") { got_sig("SIGUSR1", :reconnect) } 921: rescue ArgumentError => e 922: debug "failed to trap signals (#{e.pretty_inspect}): running on Windows?" 923: rescue Exception => e 924: debug "failed to trap signals: #{e.pretty_inspect}" 925: end 926: end
delegate sent messages
# File lib/rbot/ircbot.rb, line 1423 1423: def delegate_sent(type, where, message) 1424: args = [self, server, myself, server.user_or_channel(where.to_s), message] 1425: case type 1426: when "NOTICE" 1427: m = NoticeMessage.new(*args) 1428: when "PRIVMSG" 1429: m = PrivMessage.new(*args) 1430: when "QUIT" 1431: m = QuitMessage.new(*args) 1432: m.was_on = myself.channels 1433: end 1434: @plugins.delegate('sent', m) 1435: end