require 'log4r' require 'xmpp4r' require 'xmpp4r/xhtml' require 'xmpp4r/roster' require 'xmpp4r/muc/helper/simplemucclient' require 'mauve/notifiers/debug' #Jabber::debug = true 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 :jid, :password # Atrtribute. attr_accessor :initial_jid # Atrtribute. attr_accessor :initial_messages def initialize(name) @name = name @mucs = {} @roster = nil end def logger @logger ||= Log4r::Logger.new self.class.to_s end def reconnect if @client begin logger.debug "Jabber closing old client connection" @client.close @client = nil @roster = nil rescue Exception => ex logger.error "#{ex} when reconnecting" end end logger.debug "Jabber starting connection to #{@jid}" @client = Client.new(JID::new(@jid)) @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, stanza| Thread.new do logger.debug("Accepting subscription request from #{stanza.from}") @roster.accept_subscription(stanza.from) ensure_roster_and_subscription!(stanza.from) end.join end @client.add_message_callback do |m| receive_message(m) end @roster.wait_for_roster logger.debug "Jabber authenticated, setting presence" @client.send(Presence.new.set_type(:available)) @mucs = {} logger.debug "Jabber is ready in theory" end def reconnect_and_retry_on_error @already_reconnected = false begin yield rescue StandardError => ex logger.error "#{ex} during notification\n" logger.debug ex.backtrace if !@already_reconnected reconnect @already_reconnected = true retry else raise ex end end end def connect self.reconnect_and_retry_on_error { self.send_msg(@initial_jid, "Hello!") } end def close self.send_msg(@initial_jid, "Goodbye!") @client.close end # Takes an alert and converts it into a message. # # @param [Alert] alert The alert to convert. # @return [String] The message, either as HTML. def convert_alert_to_message(alert) arr = alert.summary_three_lines str = arr[0] + ": " + arr[1] str += " -- " + arr[2] if false == arr[2].nil? str += "." return str #return alert.summary_two_lines.join(" -- ") #return "

" + alert.summary_two_lines.join("
") + "

" 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 = nil) #message = Message.new(nil, alert.summary_two_lines.join("\n")) message = Message.new(nil, convert_alert_to_message(alert)) if conditions @suppressed_changed = conditions[:suppressed_changed] end # MUC JIDs are prefixed with muc: - we need to strip this out. destination_is_muc, dest_jid = self.is_muc?(destination) begin xhtml = XHTML::HTML.new("

" + convert_alert_to_message(alert)+ # alert.summary_three_lines.join("
") + #alert.summary_two_lines.join("
") + "

") message.add_element(xhtml) rescue REXML::ParseException => ex logger.warn("Can't send XMPP alert as valid XHTML-IM, falling back to plaintext") logger.debug(ex) end logger.debug "Jabber sending #{message} to #{destination}" reconnect unless @client ensure_roster_and_subscription!(dest_jid) unless destination_is_muc if conditions && !check_alert_conditions(dest_jid, conditions) logger.debug("Alert conditions not met, not sending XMPP alert to #{jid}") return false end if destination_is_muc if !@mucs[dest_jid] @mucs[dest_jid] = CountingMUCClient.new(@client) @mucs[dest_jid].join(JID.new(dest_jid)) end reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } else message.to = dest_jid reconnect_and_retry_on_error { @client.send(message) ; true } end end # Sends a message to the destionation. # # @param [String] destionation The (full) JID to send to. # @param [String] msg The (formatted) message to send. # @return [NIL] nada. def send_msg(destination, msg) reconnect unless @client message = Message.new(nil, msg) destination_is_muc, dest_jid = self.is_muc?(destination) if destination_is_muc if !@mucs[dest_jid] @mucs[dest_jid] = CountingMUCClient.new(@client) @mucs[dest_jid].join(JID.new(dest_jid)) end reconnect_and_retry_on_error { @mucs[dest_jid].send(message, nil) ; true } else message.to = dest_jid reconnect_and_retry_on_error { @client.send(message) ; true } end return nil end protected # Checks whether the destination JID is a MUC. # Returns [true/false, destination] def is_muc?(destination) if /^muc:(.*)/.match(destination) [true, $1] else [false, destination] 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) jid = JID.new(jid) ri = @roster.find(jid)[jid] if ri.nil? @roster.add(jid, nil, true) else ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) end rescue Exception => ex logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") 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) return true if jid.match(/^muc:/) 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 end # # TODO parse message and ack as needed..? The trick is here to # understand what the person sending the message wants. Could be # difficult. def receive_message(message) @logger.debug "Received message from #{message.from}.. Ignoring for now." end end end end