require 'blather/client/client' require 'nokogiri' module Safubot class KnownUser # Extensions to KnownUser providing XMPP-specific functionality. key :jid, String class << self # Retrieves or creates a KnownUser by an XMPP JID string. def by_xmpp(jid) jid = jid.split('/')[0] KnownUser.where('jid' => jid).first || KnownUser.create(:jid => jid, :name => jid.split('@')[0]) end end end class Request # XMPP-specific extensions to Request. scope :xmpp, :source_type => "Safubot::XMPP::Message" end # XMPP-specific functionality. module XMPP # A Message is a Request source representing a single XMPP chat stanza. class Message include MongoMapper::Document safe set_collection_name 'xmpp.message' key :xml, String # The original XML. key :xmpp_id, String key :text, String key :to, String key :from, String one :request, :as => :source, :class_name => "Safubot::Request" class << self # Creates a new Message. # @param raw The Blather stanza from which to source data. def from(raw) Message.find_by_xmpp_id(raw.id) || Message.create({ :xml => raw.to_xml, :xmpp_id => raw.id, :text => raw.body.strip, :to => raw.to.to_s, :from => raw.from.to_s }) end end # Retrieves the sender's username from the JID. def username self.from.split('@')[0] end # Retrieves the KnownUser associated with this Message. def user KnownUser.by_xmpp(self.from) end # Retrieves or creates an associated Request. def make_request self.request || self.request = Request.create(:user => user, :source => self, :text => self.text) end end class Bot include Evented attr_reader :jid, :client, :state, :pid # Sets our Blather::Client event processor running. def init_blather @client = Blather::Client.setup(@jid, @password) @client.register_handler(:ready) do Log.info "XMPP client is online at #{@client.jid.stripped} :D" emit(:ready) end @client.register_handler(:subscription, :request?) do |s| Log.info "Approving subscription request from: #{s.from}" @client.write s.approve! end @client.register_handler(:message, :chat?, :body) do |msg| unless msg.body.match(/^\?OTR:/) emit(:request, Message.from(msg).make_request) end end @client.register_handler(:disconnected) do sleep 2 # HACK (Mispy): Give the state a chance to change when we're stopped. if @state == :running Log.warn("XMPP disconnected; attempting reconnection in 5 seconds.") sleep 5; @client.connect end end @client.register_handler(:error) do |e| Log.error "Unhandled Blather error: #{error_report(e)}" end @state = :running end # Runs the Blather client. def run_blather begin EM::run { @client.run } rescue Exception => e if e.is_a?(Interrupt) || e.is_a?(SignalException) stop elsif @state == :running Log.error "XMPP client exited unexpectedly: #{error_report(e)}" Log.error "Restarting XMPP client in 5 seconds." sleep 5; init_blather; run_blather end end end # Starts our Blather client running. def run init_blather run_blather end # Starts our Blather client running in a new process. def fork @pid = Process.fork do Signal.trap("TERM") { stop } run end end # Shuts down the Blather client. def stop if @client @state = :stopped @client.close @client = nil Log.info "XMPP client shutdown complete." elsif @pid Process.kill("KILL", @pid) end end def tell(jid, text) msg = Blather::Stanza::Message.new msg.to = jid msg.body = text @client.write msg end # Dispatch a Response via XMPP. def send(resp) if @state == :running tell(resp.request.source.from, resp.text) else on(:ready) { send(resp) } end end def initialize(opts) @jid = opts[:jid] @password = opts[:password] @state = :stopped end end end end