require 'xmpp4r'
require 'xmpp4r/muc/helper/simplemucclient'
require 'xmpp4r/roster/helper/roster'
require 'ostruct'
# Handles opening a connection to the HipChat server, and feeds all
# messages through our Robut::Plugin list.
class Robut::Connection
# The configuration used by the Robut connection.
#
# Parameters:
#
# [+jid+, +password+, +nick+] The HipChat credentials given on
# https://www.hipchat.com/account/xmpp
#
# [+room+] The chat room to join, in the format jabber_name@conference_server
#
# [+logger+] a logger instance to use for debug output.
attr_accessor :config
# The Jabber::Client that's connected to the HipChat server.
attr_accessor :client
# The MUC that wraps the Jabber Chat protocol.
attr_accessor :muc
# The storage instance that's available to plugins
attr_accessor :store
# The roster of currently available people
attr_accessor :roster
class << self
# Class-level config. This is set by the +configure+ class method,
# and is used if no configuration is passed to the +initialize+
# method.
attr_accessor :config
end
# Configures the connection at the class level. When the +robut+ bin
# file is loaded, it evals the file referenced by the first
# command-line parameter. This file can configure the connection
# instance later created by +robut+ by setting parameters in the
# Robut::Connection.configure block.
def self.configure
self.config = OpenStruct.new
yield config
end
# Sets the instance config to +config+, converting it into an
# OpenStruct if necessary.
def config=(config)
@config = config.kind_of?(Hash) ? OpenStruct.new(config) : config
end
# Initializes the connection. If no +config+ is passed, it defaults
# to the class_level +config+ instance variable.
def initialize(_config = nil)
self.config = _config || self.class.config
self.client = Jabber::Client.new(self.config.jid)
self.muc = Jabber::MUC::SimpleMUCClient.new(client)
self.store = self.config.store || Robut::Storage::HashStore # default to in-memory store only
if self.config.logger
Jabber.logger = self.config.logger
Jabber.debug = true
end
end
# Send +message+ to the room we're currently connected to, or
# directly to the person referenced by +to+. +to+ can be either a
# jid or the string name of the person.
def reply(message, to = nil)
if to
unless to.kind_of?(Jabber::JID)
to = find_jid_by_name(to)
end
msg = Jabber::Message.new(to || muc.room, message)
msg.type = :chat
client.send(msg)
else
muc.send(Jabber::Message.new(muc.room, message))
end
end
# Sends the chat message +message+ through +plugins+.
def handle_message(plugins, time, nick, message)
plugins.each do |plugin|
begin
rsp = plugin.handle(time, nick, message)
break if rsp == true
rescue => e
reply("UH OH! #{plugin.class.name} just crashed!")
if config.logger
config.logger.error e
config.logger.error e.backtrace.join("\n")
end
end
end
end
# Connects to the specified room with the given credentials, and
# enters an infinite loop. Any messages sent to the room will pass
# through all the included plugins.
def connect
client.connect
client.auth(config.password)
client.send(Jabber::Presence.new.set_type(:available))
self.roster = Jabber::Roster::Helper.new(client)
roster.wait_for_roster
# Add the callback from messages that occur inside the room
muc.on_message do |time, nick, message|
plugins = Robut::Plugin.plugins.map { |p| p.new(self, nil) }
handle_message(plugins, time, nick, message)
end
# Add the callback from direct messages. Turns out the
# on_private_message callback doesn't do what it sounds like, so I
# have to go a little deeper into xmpp4r to get this working.
client.add_message_callback(200, self) { |message|
if !muc.from_room?(message.from) && message.type == :chat && message.body
time = Time.now # TODO: get real timestamp? Doesn't seem like
# jabber gives it to us
sender_jid = message.from
plugins = Robut::Plugin.plugins.map { |p| p.new(self, sender_jid) }
handle_message(plugins, time, self.roster[sender_jid].iname, message.body)
true
else
false
end
}
muc.join(config.room + '/' + config.nick)
trap_signals
loop { sleep 1 }
end
private
# Since we're entering an infinite loop, we have to trap TERM and
# INT. If something like the Rdio plugin has started a server that
# has already trapped those signals, we want to run those signal
# handlers first.
def trap_signals
old_signal_callbacks = {}
signal_callback = Proc.new do |signal|
old_signal_callbacks[signal].call if old_signal_callbacks[signal]
exit
end
[:INT, :TERM].each do |sig|
old_signal_callbacks[sig] = trap(sig) { signal_callback.call(sig) }
end
end
# Find a jid in the roster with the given name, case-insensitively
def find_jid_by_name(name)
name = name.downcase
roster.items.detect {|jid, item| item.iname.downcase == name}.first
end
end