Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #--
- # Copyright 2007 by Stefan Rusterholz.
- # All rights reserved.
- # See LICENSE.txt for permissions.
- #++
- require 'butler'
- require 'butler/irc/client'
- require 'butler/debuglog'
- require 'configuration'
- require 'log'
- require 'butler/plugins'
- require 'butler/remote/connection'
- require 'butler/remote/message'
- require 'butler/remote/server'
- require 'butler/remote/user'
- require 'butler/services'
- require 'butler/session'
- require 'ruby/string/arguments'
- require 'ruby/string/post_arguments'
- require 'scheduler'
- require 'set'
- require 'thread'
- require 'timeout'
- class Butler
- class Bot < IRC::Client
- include Log::Comfort
- # The access framework for this bot. An instance of Access.
- attr_reader :access
- # This bot instances configuration. An instance of Configuration.
- attr_reader :config
- # The logging device, per default nil
- attr_reader :logger
- # An OpenStruct with all important paths of this bot instance.
- attr_reader :path
- # Services butler offers, instance of Butler::Plugins
- attr_reader :plugins
- # Remote connections server, instance of Butler::Remote::Server
- attr_reader :remote
- # Scheduler to hook up timed events, instance of Scheduler
- # For plugins and services, please look up the class methods for
- # Butler::Plugin and Butler::Service, they are to be prefered.
- attr_reader :scheduler
- # Services butler offers, instance of Butler::Services
- attr_reader :services
- def initialize(path, name, opts={})
- path ||= Butler.path
- @irc = nil # early inspects
- @name = name
- @base = "#{path.bots}/#{name}"
- @path = OpenStruct.new(
- :access => @base+'/access',
- :base => @base,
- :config => @base+'/config',
- :lib => @base+'/lib',
- :log => @base+'/log',
- :plugins => @base+'/plugins',
- :services => @base+'/services',
- :strings => @base+'/strings'
- )
- $LOAD_PATH.unshift(@path.lib)
- require 'butlerinit' if File.exist?(@path.lib+"/butlerinit.rb")
- @logger = $stderr
- @remote = Remote::Server.new(self)
- @config = Configuration.new(@base+'/config')
- @scheduler = Scheduler.new
- @access = Access.new(
- Access::YAMLBase.new(Access::User::Base, @base+'/access/user'),
- Access::YAMLBase.new(Access::Role::Base, @base+'/access/role'),
- Access::YAMLBase.new(Access::Privilege::Base, @base+'/access/privilege')
- #:channel => Access::YAMLBase.new("#{TestDir}/channel", Access::Location)
- )
- @on_disconnect = nil
- @on_reconnect = nil
- @reconnect = [
- opts.delete(:reconnect_delay) || 60,
- opts.delete(:reconnect_tries) || -1
- ]
- super(@config["connections/main/server"], {
- :host => @config["connections/main/host"],
- :port => @config["connections/main/port"],
- }.merge(opts))
- # { lang => { trigger => SortedSet[ *commands ] } }
- @commands = {}
- @services = Services.new(self, @path.services)
- @services.load_all
- @plugins = Plugins.new(self, @path.plugins)
- if $DEBUG then
- @irc.extend DebugLog
- @irc.raw_log = File.open(@path.log+'/debug.log', 'wb')
- @irc.raw_log.sync = true
- end
- subscribe(:PRIVMSG, -10, &method(:invoke_commands))
- subscribe(:NOTICE, -10, &method(:invoke_commands))
- end
- def connected?
- @irc.connected?
- end
- def invoke_commands(listener, message)
- return unless ((sequence = invocation(message.text)) || message.realm == :private)
- message.invocation = sequence || ""
- message.language = "en"
- trigger = message.arguments.first
- access = message.from && message.from.access.method(:authorized?)
- return unless access and trigger
- trigger = trigger.downcase
- commands = []
- if @commands[message.language] && @commands[message.language][trigger] then
- commands.concat(@commands[message.language][trigger].to_a)
- end
- if message.language != "en" && @commands["en"] && @commands["en"][trigger] then
- commands.concat(@commands["en"][trigger].to_a)
- end
- commands.each { |command|
- if args = (command.invoked_by?(message)) then
- if !command.authorization || access.call(command.authorization) then
- Thread.new {
- begin
- command.call(message, *args)
- rescue Exception => e
- exception(e)
- end
- }
- break if command.abort_invocations?
- else
- info("#{message.from} (#{message.from.access.oid}) had no authorization for 'plugin/#{command.plugin.base}'")
- end
- end
- }
- end
- def add_command(command)
- @commands[command.language] ||= {}
- @commands[command.language][command.trigger] ||= SortedSet.new
- @commands[command.language][command.trigger].add(command)
- end
- def delete_command(command)
- @commands[command.language][command.trigger].delete(command)
- if @commands[command.language][command.trigger].empty? then
- @commands[command.language].delete(command.trigger)
- @commands.delete(command.language) if @commands[command.language].empty?
- end
- end
- # returns the rest of the sequence if it was an invocation, nil else
- def invocation(sequence)
- (@myself && sequence[/^(?:!?#{@myself.nick}[:;,])\s+/i]) || sequence[/^#{@config['invocation']}/i]
- puts @config['invocation']
- end
- def login
- @access.default_user = @access["default_user"]
- @access.default_user.login
- nick = @config["connections/main/nick"]
- pass = @config["connections/main/password"]
- begin
- super(
- nick,
- @config["connections/main/user"],
- @config["connections/main/real"],
- @config["connections/main/serverpass"]
- )
- rescue Errno::ECONNREFUSED
- if @reconnect[1].zero? then
- warn("Connection was refused")
- return false
- else
- warn("Connection was refused, trying again in #{@reconnect.first} seconds")
- @reconnect[1] -= 1
- sleep(@reconnect[0])
- retry
- end
- end
- info("Logged in")
- if pass && !@parser.same_nick?(@myself.nick, nick) then
- if wait_for(:NOTICE, 60, :prepare => proc { @irc.ghost(nick, pass) } ) { |m|
- m.from && m.from.nick =~ /nickserv/i && m.text =~ /has been killed/
- } then
- @irc.nick(nick)
- else
- warn("Could not ghost the user already occupying my nick '#{nick}'")
- end
- end
- @irc.identify(pass) if pass
- join(*@config["connections/main/channels"])
- plugins_dispatch(:on_login, "main")
- true
- end
- def on_disconnect(reason)
- info("Disconnected due to #{reason}")
- return if reason == :quit
- plugins_dispatch(:on_disconnect, reason)
- unless @reconnect[1].zero? then
- @reconnect[1] -= 1
- sleep(@reconnect[0])
- login
- end
- end
- def quit(reason=nil, *args)
- timeout(3) {
- plugins_dispatch(:on_quit, reason, *args).join
- }
- ensure
- super(reason)
- end
- def plugins_dispatch(event, *args)
- Thread.new { # avoid total lockup due to an improperly written on_disconnect
- begin
- @plugins.instances.each { |plugin|
- plugin.send(event, *args)
- }
- rescue Exception => e
- exception(e)
- end
- }
- end
- def output_to_logfiles
- # Errors still go to $stderr, $stdin handles puts as "info" level $stderr prints
- @logger = Log.file(@path.log+'/error.log')
- $stdout = Log.forward(@logger, :warn)
- $stderr = Log.forward(@logger, :info)
- end
- def inspect # :nodoc:
- "#<%s:0x%08x %s (in %s) irc=%s>" % [
- self.class,
- object_id << 1,
- @name,
- @base,
- @irc.inspect
- ]
- end
- end # Bot
- class IRC::UserList
- attr_reader :client
- end
- class IRC::User
- attr_accessor :access
- attr_reader :session
- def authorized?(*args)
- @access.authorized?(*args)
- end
- alias butler_initialize initialize unless method_defined? :butler_initialize
- def initialize(users, *args, &block)
- butler_initialize(users, *args, &block)
- @access = users.client.access.default_user
- @session = Session.new
- end
- end # IRC::User
- class IRC::Message
- # the language of this message, as determined by butler
- attr_accessor :language
- # the invocation-sequence (e.g. "butler, "), nil if none was used (e.g. in
- # private messages)
- attr_accessor :invocation
- alias butler_initialize initialize unless method_defined? :butler_initialize
- def initialize(*args, &block)
- butler_initialize(*args, &block)
- @language = nil
- @invocation = nil
- @arguments = nil
- @formatted_arguments = nil
- @post_arguments = nil
- end
- # parses a given string into argument-tokens. a token is either a word or a quoted string. escapes are respected.
- # e.g. 'Hallo "this is token2" "and this \"token\" is token3"'
- # would be parsed into: ["hallo", "this is token2", "and this \"token\" is token3"]
- def arguments
- # only messages with text can be tokenized to parameters
- raise NoMethodError, "Message has no text, can't generate arguments" unless text
- # cached
- return @arguments if @arguments
- # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
- args = text[@invocation.length..-1].mirc_stripped.scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
- # sanitize the last argument in case it's not correctly quoted
- args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
- # unescape data and remove quotes
- @arguments = args.map { |arg|
- case arg[0,1]
- when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
- when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
- else arg.gsub(/\\(["'\\ ])/, '\1')
- end
- }
- end
- # like #arguments, but doesn't strip color information
- def formatted_arguments
- # only messages with text can be tokenized to parameters
- raise NoMethodError, "Message has no text, can't generate arguments" unless text
- # cached
- return @arguments if @arguments
- # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
- args = text[@invocation.length..-1].scan(/"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
- # sanitize the last argument in case it's not correctly quoted
- args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
- # unescape data and remove quotes
- @formatted_arguments = args.map { |arg|
- case arg[0,1]
- when "'": arg[1..-2].gsub(/\\(['\\])/, '\1')
- when '"': arg[1..-2].gsub(/\\(["\\])/, '\1')
- else arg.gsub(/\\(["'\\ ])/, '\1')
- end
- }
- end
- # Returns the rest of message text after argument
- # == Synopsis
- # "foo bar baz".post_arguments[0] # => "foo bar baz"
- # "foo bar baz".post_arguments[1] # => "bar baz"
- # "foo bar baz".post_arguments[2] # => "baz"
- def post_arguments
- # only messages with text can be tokenized to parameters
- raise NoMethodError, "Message has no text, can't generate arguments" unless text
- # cached
- return @post_arguments if @post_arguments
- # split the args into double-, single- and unquoted arguments, a lonely quote starts the last arg
- args = text[@invocation.length..-1].scan(/\s+|"(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*'|(?:\\.|[^\\'"\s])+|["'].*/)
- # sanitize arguments
- args.shift if args.first =~ /^\s+$/
- args.pop if args.last =~ /^\s+$/
- args[-1] = args[-1]+args[-1][0,1] if args[-1][0,1] =~ /'|"/ and args[-1][0,1] != args[-1][-1,1]
- # unescape data and remove quotes
- args.map! { |arg|
- case arg[0,1]
- when "'": arg.gsub(/\\(['\\])/, '\1')
- when '"': arg.gsub(/\\(["\\])/, '\1')
- else arg.gsub(/\\(["'\\ ])/, '\1')
- end
- }
- @post_arguments = []
- (0...args.length).step(2) { |i| @post_arguments << args[i..-1].join('') }
- @post_arguments
- end
- end # IRC::Message
- end # Butler
Add Comment
Please, Sign In to add comment