class Flapjack::Gateways::Jabber::Bot

Attributes

siblings[RW]

Public Class Methods

new(opts = {}) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 520
def initialize(opts = {})
  @lock = opts[:lock]
  @stop_cond = opts[:stop_condition]
  @config = opts[:config]
  @boot_time = opts[:boot_time]

  @say_buffer = []
  @announce_buffer = []
  @hostname = Socket.gethostname

  @alias = @config['alias'] || 'flapjack'
  @identifiers = ((@config['identifiers'] || []) + [@alias]).uniq

  @state_buffer = []
  @should_quit = false
end

Public Instance Methods

_announce(muc_clients) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 809
def _announce(muc_clients)
  @announce_buffer.each do |announce|
    if (muc_client = muc_clients[announce[:room]]) && muc_client.active?
      muc_client.say(announce[:msg])
      announce[:sent] = true
    end
  end
  @announce_buffer.delete_if {|announce| announce[:sent] }
end
_deactivate(muc_clients) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 803
def _deactivate(muc_clients)
  # send method has been overridden in MUCClient class
  # without this MUC clients will still think they are active
  muc_clients.values.each {|muc_client| muc_client.__send__(:deactivate) }
end
_join(client, muc_clients, opts = {}) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 751
def _join(client, muc_clients, opts = {})
  client.connect
  client.auth(@config['password'])
  client.send(::Jabber::Presence.new)
  muc_clients.each_pair do |room, muc_client|
    attempts_allowed = 3
    attempts_remaining = attempts_allowed
    joined = nil
    while !joined && (attempts_remaining > 0)
      @lock.synchronize do
        unless @should_quit || (attempts_remaining == attempts_allowed)
          # The only thing that should be interrupting this wait is
          # a pikelet.stop, which would set @should_quit to true;
          # thus we shouldn't see multiple connection attempts happening
          # too quickly.
          @stop_cond.wait(3)
        end
      end

      # may have changed during previous wait
      sq = nil
      @lock.synchronize do
        sq = @should_quit
      end

      unless sq
        attempts_remaining -= 1
        begin
          muc_client.join(room + '/' + @alias, nil, :history => false)
          t = Time.now
          msg = opts[:rejoin] ? "flapjack jabber gateway rejoining at #{t}, hello again!" :
                                "flapjack jabber gateway started at #{t}, hello! Try typing 'help'."
          muc_client.say(msg) if @config['chatbot_announce']
          joined = true
        rescue Errno::ECONNREFUSED, ::Jabber::JabberError => muc_je
          report_error("Couldn't join MUC room #{room}, #{attempts_remaining} attempts remaining", muc_je)
          raise if attempts_remaining <= 0
          joined = false
        end
      end
    end
  end
end
_leave(client, muc_clients) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 795
def _leave(client, muc_clients)
  if @joined
    muc_clients.values.each {|muc_client| muc_client.exit if muc_client.active? }
    client.close
  end
  @joined = false
end
_say(client) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 819
def _say(client)
  while speak = @say_buffer.pop
    msg = ::Jabber::Message::new(speak[:nick], speak[:msg])
    msg.type = :chat
    client.send( msg )
  end
end
alias() click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 537
def alias
  ret = nil
  @lock.synchronize do
    ret = @alias
  end
  ret
end
announce(room, msg) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 701
def announce(room, msg)
  @lock.synchronize do
    @announce_buffer += [{:room => room, :msg => msg}]
    @state_buffer << 'announce'
    @stop_cond.signal
  end
end
handle_state_change(client, muc_clients) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 717
def handle_state_change(client, muc_clients)
  connected = client.is_connected?
  Flapjack.logger.debug "connected? #{connected}"

  while state = @state_buffer.pop
    Flapjack.logger.debug "state change #{state}"
    case state
    when 'announce'
      _announce(muc_clients) if connected
    when 'say'
      _say(client) if connected
    when 'leave'
      connected ? _leave(client, muc_clients) : _deactivate(muc_clients)
    when 'rejoin'
      _join(client, muc_clients, :rejoin => true) unless connected
    else
      Flapjack.logger.warn "unknown state change #{state}"
    end
  end
end
identifiers() click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 545
def identifiers
  ret = nil
  @lock.synchronize do
    ret = @identifiers
  end
  ret
end
report_error(error_msg, je) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 742
def report_error(error_msg, je)
  Flapjack.logger.error error_msg
  message = je.respond_to?(:message) ? je.message : '-'
  Flapjack.logger.error "#{je.class.name} #{message}"
  # if je.respond_to?(:backtrace) && trace = je.backtrace
  #   Flapjack.logger.error trace.join("\n")
  # end
end
say(nick, msg) click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 709
def say(nick, msg)
  @lock.synchronize do
    @say_buffer += [{:nick => nick, :msg => msg}]
    @state_buffer << 'say'
    @stop_cond.signal
  end
end
start() click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 553
def start
  Flapjack.logger.debug("I will respond to the following identifiers: #{@identifiers.join(', ')}")

  @lock.synchronize do
    interpreter = self.siblings ? self.siblings.detect {|sib| sib.respond_to?(:interpret)} : nil

    Flapjack.logger.info("starting")
    Flapjack.logger.debug("new jabber pikelet with the following options: #{@config.inspect}")

    # ::Jabber::debug = true

    jabber_id = @config['jabberid'] || 'flapjack'
    jabber_id += '/' + @hostname unless jabber_id.include?('/')
    flapjack_jid = ::Jabber::JID.new(jabber_id)
    client = ::Jabber::Client.new(flapjack_jid)

    client.on_exception do |exc, stream, loc|
      leave_and_rejoin = nil

      @lock.synchronize do

        # called with a nil exception on disconnect for some reason
        if exc
          Flapjack.logger.error exc.class.name
          Flapjack.logger.error ":#{loc.to_s}"
          Flapjack.logger.error exc.message
          Flapjack.logger.error exc.backtrace.join("\n")
        end

        leave_and_rejoin = @joined && !@should_quit

        if leave_and_rejoin
          @state_buffer << 'leave'
          @stop_cond.signal
        end
      end

      if leave_and_rejoin
        sleep 3
        @lock.synchronize do
          unless @should_quit
            @state_buffer << 'rejoin'
            @stop_cond.signal
          end
        end
      end
    end

    check_xml = Proc.new do |data|
      if data.nil?
        nil
      else
        Flapjack.logger.debug "xml_data: #{data}"
        text = ''
        begin
          enc_name = Encoding.default_external.name
          REXML::Document.new("<?xml version=\"1.0\" encoding=\"#{enc_name}\"?>" + data).
            each_element_with_text do |elem|

            text += elem.texts.join(" ")
          end
          text = data if text.empty? && !data.empty?
        rescue REXML::ParseException
          # invalid XML, so we'll just clear everything inside angled brackets
          text = data.gsub(/<[^>]+>/, '').strip
        end
        text
      end
    end

    client.add_message_callback do |m|
      unless (m.type != :chat) || m.body.nil? || m.body.strip.empty?
        Flapjack.logger.debug "received message #{m.inspect}"
        nick = m.from
        time = nil
        m.each_element('x') { |x|
          if x.kind_of?(::Jabber::Delay::XDelay)
            time = x.stamp
          end
        }

        unless interpreter.nil?
          interpreter.receive_message(nil, nick, time, check_xml.call(m.body))
        end
      end
    end

    muc_clients = @config['rooms'].inject({}) do |memo, room|
      muc_client = ::Jabber::MUC::SimpleMUCClient.new(client)
      muc_client.on_message do |time, nick, text|
        Flapjack.logger.debug("message #{text} -- #{time} -- #{nick}")
        next if nick == jabber_id
        identifier = identifiers.detect {|id| /^(?:@#{id}|#{id}:)\s*(.*)/m === check_xml.call(text) }

        unless identifier.nil?
          the_command = Regexp.last_match(1)
          Flapjack.logger.debug("matched identifier: #{identifier}, command: #{the_command.inspect}")
          if interpreter
            interpreter.receive_message(room, nick, time, the_command)
          end
        end
      end

      memo[room] = muc_client
      memo
    end

    attempts_allowed = 3
    attempts_remaining = attempts_allowed
    @joined = false

    loop do

      if @joined
        # block this thread until signalled to quit / leave / rejoin
        @stop_cond.wait_until { @should_quit || !@state_buffer.empty? }
      elsif attempts_remaining > 0
        unless @should_quit || (attempts_remaining == attempts_allowed)
          # The only thing that should be interrupting this wait is
          # a pikelet.stop, which would set @should_quit to true;
          # thus we shouldn't see multiple connection attempts happening
          # too quickly.
          @stop_cond.wait(3)
        end
        unless @should_quit # may have changed during previous wait
          begin
            attempts_remaining -= 1
            _join(client, muc_clients)
            @joined = true
          rescue Errno::ECONNREFUSED, ::Jabber::JabberError => je
            report_error("Couldn't join Jabber server #{@hostname}", je)
          end
        end
      else
        # TODO should we quit Flapjack entirely?
        Flapjack.logger.error "stopping jabber bot, couldn't connect in #{attempts_allowed} attempts"
        @should_quit = true
      end

      break if @should_quit
      handle_state_change(client, muc_clients) unless @state_buffer.empty?
    end

    # main loop has finished, stop() must have been called -- disconnect
    _leave(client, muc_clients) if client.is_connected?
  end
end
stop_type() click to toggle source
# File lib/flapjack/gateways/jabber.rb, line 738
def stop_type
  :signal
end