require 'log4r' require 'xmpp4r' require 'xmpp4r/roster' require 'xmpp4r/muc' # require 'xmpp4r/xhtml' # require 'xmpp4r/discovery/helper/helper' require 'mauve/notifiers/debug' # # A couple of monkey patches to fix up all this nonsense. # module Jabber class Stream def close # # Just close # close! end def close! 10.times do pr = 0 @tbcbmutex.synchronize { pr = @processing } break if pr = 0 Thread::pass if pr > 0 sleep 1 end # Order Matters here! If this method is called from within # @parser_thread then killing @parser_thread first would # mean the other parts of the method fail to execute. # That would be bad. So kill parser_thread last @tbcbmutex.synchronize { @processing = 0 } if @fd and !@fd.closed? @fd.close stop end @status = DISCONNECTED end end end module Mauve module Notifiers module Xmpp # class CountingMUCClient < Jabber::MUC::SimpleMUCClient # # attr_reader :participants # # def initialize(*a) # super(*a) # @participants = 0 # self.on_join { @participants += 1 } # self.on_leave { @participants -= 1 } # end # # end # class Default include Jabber # Atrtribute. attr_reader :name # Atrtribute. attr_accessor :password def initialize(name) Jabber::logger = self.logger # Jabber::debug = true # Jabber::warnings = true @name = name @mucs = {} @roster = nil @closing = false @client = nil end def logger # Give the logger a sane name @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") end def jid=(jid) @jid = JID.new(jid) end def connect logger.debug "Starting connection to #{@jid}" # Make sure we're disconnected. self.close if @client.is_a?(Client) @client = Client.new(@jid) @closing = false @client.connect @client.auth_nonsasl(@password, false) @roster = Roster::Helper.new(@client) # Unconditionally accept all roster add requests, and respond with a # roster add + subscription request of our own if we're not subscribed # already @roster.add_subscription_request_callback do |ri, presence| Thread.new do if is_known_contact?(presence.from) logger.info("Accepting subscription request from #{presence.from}") @roster.accept_subscription(presence.from) ensure_roster_and_subscription!(presence.from) else logger.info("Declining subscription request from #{presence.from}") @roster.decline_subscription(presence.from) end end.join end @client.add_message_callback do |m| receive_message(m) end @roster.wait_for_roster @client.send(Presence.new(nil, "Woo!").set_type(nil)) logger.info "Connected as #{@jid}" @client.on_exception do |ex, stream, where| # # The XMPP4R exception clauses in Stream all close the stream, so # we just need to reconnect. # unless ex.nil? or @closing logger.warn(["Caught",ex.class,ex.to_s,"during XMPP",where].join(" ")) @closing = true connect @mucs.each do |jid, muc| @mucs.delete(jid) join_muc(muc[:jid], muc[:password]) end end end end # # Kills the processor thread # def stop @client.stop end def close @closing = true if @client if @client.is_connected? @mucs.each do |jid, muc| muc[:client].exit("Goodbye!") if muc[:client].active? end @client.send(Presence.new(nil, "Goodbye!").set_type(:unavailable)) end @client.close! end end def ready? @client.is_a?(Jabber::Client) and @client.is_connected? end # # Takes an alert and converts it into a message. # def convert_alert_to_message(alert) end # Attempt to send an alert using XMPP. # +destination+ is the JID you're sending the alert to. This should be # a bare JID in the case of an individual, or muc:@ for # chatrooms (XEP0045). The +alert+ object is turned into a pretty # message and sent to the destination as a message, if the +conditions+ # are met. all_alerts are currently ignored. # # The only suported condition at the moment is :if_presence => [choices] # which checks whether the jid in question has a presence matching one # or more of the choices - see +check_jid_has_presence+ for options. def send_alert(destination, alert, all_alerts, conditions = {}) destination_jid = JID.new(destination) was_suppressed = conditions[:was_suppressed] || false is_suppressed = conditions[:is_suppressed] || false if conditions && !check_alert_conditions(destination_jid, conditions) logger.info("Alert conditions not met, not sending XMPP alert to #{destination_jid}") return false end template_file = File.join(File.dirname(__FILE__),"templates","xmpp.txt.erb") txt = if File.exists?(template_file) ERB.new(File.read(template_file)).result(binding).chomp else logger.error("Could not find xmpp.txt.erb template") alert.to_s end template_file = File.join(File.dirname(__FILE__),"templates","xmpp.html.erb") xhtml = if File.exists?(template_file) ERB.new(File.read(template_file)).result(binding).chomp else logger.error("Could not find xmpp.txt.erb template") alert.to_s end send_message(destination_jid, txt, xhtml) end # Sends a message to the destionation. # # @param [String] destination The (full) JID to send to. # @param [String] msg The (formatted) message to send. # @return [NIL] nada. def send_message(jid, msg, html_msg=nil) jid = JID.new(jid) unless jid.is_a?(JID) message = Message.new(jid) message.body = msg if html_msg begin html_msg = REXML::Document.new(html_msg) unless html_msg.is_a?(REXML::Document) message.add_element(html_msg) rescue REXML::ParseException logger.error "Bad XHTML: #{html_msg.inspect}" end end if is_muc?(jid) jid = join_muc(jid.strip) muc = @mucs[jid][:client] if muc message.to = muc.jid muc.send(message) true else logger.warn "Failed to join MUC #{jid} when trying to send a message" false end else # # We aren't interested in sending things to people who aren't online. # ensure_roster_and_subscription!(jid) if check_jid_has_presence(jid) # # We set the chat type to chat # message.type = :chat message.to = jid @client.send(message) true else false end end end # # Joins a chat, and returns the stripped JID of the chat joined. # def join_muc(jid, password=nil) if jid.is_a?(String) and jid =~ /^muc:(.*)/ jid = JID.new($1) end unless jid.is_a?(JID) logger.warn "#{jid} is not a MUC" return end jid.resource = @client.jid.resource if jid.resource.to_s.empty? if !@mucs[jid.strip] logger.debug("Adding new MUC client for #{jid}") @mucs[jid.strip] = {:jid => jid, :password => password, :client => Jabber::MUC::MUCClient.new(@client)} # Add some callbacks @mucs[jid.strip][:client].add_message_callback do |m| receive_message(m) end @mucs[jid.strip][:client].add_private_message_callback do |m| receive_message(m) end end if !@mucs[jid.strip][:client].active? # # Make sure we have a resource. # @mucs[jid.strip][:client].join(jid, password) logger.info("Joined #{jid.strip}") else logger.debug("Already joined #{jid.strip}.") end # # Return the JID object # jid.strip end # # Checks whether the destination JID is a MUC. # def is_muc?(jid) (jid.is_a?(JID) and @mucs.keys.include?(jid.strip)) or (jid.is_a?(String) and jid =~ /^muc:(.*)/) # # It would be nice to use service discovery to determin this, but it # turns out that it is shite in xmpp4r. It doesn't return straight # away with an answer, making it a bit useless. Some sort of weird # threading issue, I think. # # begin # logger.warn caller.join("\n") # cl = Discovery::Helper.new(@client) # res = cl.get_info_for(jid.strip) # @client.wait # logger.warn "hello #{res.inspect}" # res.is_a?(Discovery::IqQueryDiscoInfo) and res.identity.category == :conference # rescue Jabber::ServerError => ex # false # end end # # Checks to see if the JID is in our roster, and whether we are # subscribed to it or not. Will add to the roster and subscribe as # is necessary to ensure both are true. # def ensure_roster_and_subscription!(jid) return jid if is_muc?(jid) jid = JID.new(jid) unless jid.is_a?(JID) ri = @roster.find(jid).values.first @roster.add(jid, nil, true) if ri.nil? ri = @roster.find(jid).values.first ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) ri.jid rescue StandardError => ex logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") nil end protected def receive_message(msg) # We only want to hear messages from known contacts. unless is_known_contact?(msg.from) # ignore message logger.info "Ignoring message from unknown contact #{msg.from}" return nil end case msg.type when :error receive_error_message(msg) when :groupchat receive_groupchat_message(msg) else receive_normal_message(msg) end end def receive_error_message(msg) logger.warn("Caught XMPP error #{msg}") nil end def receive_normal_message(msg) # # Treat invites specially # if msg.x("jabber:x:conference") # # recieved an invite. Need to mangle the jid. # jid =JID.new(msg.x("jabber:x:conference").attribute("jid")) # jid.resource = @client.jid.resource logger.info "Received an invite to #{jid}" unless join_muc(jid) logger.warn "Failed to join MUC #{jid} following invitation" return nil end elsif msg.body # # Received a message with a body. # jid = msg.from end # # I don't have time to talk to myself! # if jid and jid.strip != @client.jid.strip txt = File.executable?('/usr/games/fortune') ? `/usr/games/fortune -s -n 60`.chomp : "I'd love to stay and chat, but I'm really too busy." send_message(jid, txt) end end def receive_groupchat_message(msg) # # We only want group chat messages from MUCs we're already joined to, # that we've not sent ourselves, that are not historical, and that # match our resource or node in the body. # if @mucs[msg.from.strip][:client].is_a?(MUC::MUCClient) and msg.from != @mucs[msg.from.strip][:client].jid and msg.x("jabber:x:delay") == nil and (msg.body =~ /\b#{Regexp.escape(@mucs[msg.from.strip][:client].jid.resource)}\b/i or msg.body =~ /\b#{Regexp.escape(@client.jid.node)}\b/i) receive_normal_message(msg) end end def check_alert_conditions(destination, conditions) any_failed = conditions.keys.collect do |key| case key when :if_presence : check_jid_has_presence(destination, conditions[:if_presence]) else #raise ArgumentError.new("Unknown alert condition, #{key} => #{conditions[key]}") # FIXME - clean up this use of :conditions to pass arbitrary # parameters to notifiers; for now we need to ignore this. true end end.include?(false) !any_failed end # Checks our roster to see whether the jid has a resource with at least # one of the included presences. Acceptable +presence+ types and their # meanings for individuals: # # :online, :offline - user is logged in or out # :available - jabber status is nil (available) or chat # :unavailable - - jabber status is away, dnd or xa # :unknown - don't know (not in roster) # # For MUCs: TODO # Returns true if at least one of the presence specifiers for the jid # is met, false otherwise. Note that if the alerter can't see the alertee's # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] def check_jid_has_presence(jid, presence_or_presences = [:online, :unknown]) jid = JID.new(jid) unless jid.is_a?(JID) return true if is_muc?(jid) reconnect unless @client presences = [presence_or_presences].flatten roster_item = @roster.find(jid) roster_item = roster_item[roster_item.keys[0]] resource_presences = [] roster_item.each_presence {|p| resource_presences << p.show } if roster_item results = presences.collect do |need_presence| case need_presence when :online : (roster_item && [:to, :both].include?(roster_item.subscription) && roster_item.online?) when :offline : (roster_item && [:to, :both].include?(roster_item.subscription) && !roster_item.online?) when :available : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences.include?(nil) || resource_presences.include?(:chat))) # No resources are nil or chat when :unavailable : (roster_item && [:to, :both].include?(roster_item.subscription) && (resource_presences - [:away, :dnd, :xa]).empty?) # Not in roster or don't know subscription when :unknown : (roster_item.nil? || [:none, :from].include?(roster_item.subscription)) else raise ArgumentError.new("Unknown presence possibility: #{need_presence}") end end results.include?(true) end def is_known_contact?(jid) jid = JID.new(jid) unless jid.is_a?(JID) Configuration.current.people.any? do |username, person| next unless person.xmpp.is_a?(JID) person.xmpp.strip == jid.strip end end end end end end