diff options
| author | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-04-13 17:03:16 +0100 | 
|---|---|---|
| committer | Patrick J Cherry <patrick@bytemark.co.uk> | 2011-04-13 17:03:16 +0100 | 
| commit | 89a67770e66d11740948e90a41db6cee0482cf8e (patch) | |
| tree | be858515fb789a89d68f94975690ab019813726c /lib/mauve/notifiers | |
new version.
Diffstat (limited to 'lib/mauve/notifiers')
| -rw-r--r-- | lib/mauve/notifiers/debug.rb | 68 | ||||
| -rw-r--r-- | lib/mauve/notifiers/email.rb | 138 | ||||
| -rw-r--r-- | lib/mauve/notifiers/sms_aql.rb | 90 | ||||
| -rw-r--r-- | lib/mauve/notifiers/sms_default.rb | 12 | ||||
| -rw-r--r-- | lib/mauve/notifiers/templates/email.html.erb | 0 | ||||
| -rw-r--r-- | lib/mauve/notifiers/templates/email.txt.erb | 0 | ||||
| -rw-r--r-- | lib/mauve/notifiers/templates/sms.txt.erb | 0 | ||||
| -rw-r--r-- | lib/mauve/notifiers/templates/xmpp.html.erb | 0 | ||||
| -rw-r--r-- | lib/mauve/notifiers/templates/xmpp.txt.erb | 1 | ||||
| -rw-r--r-- | lib/mauve/notifiers/xmpp-smack.rb | 395 | ||||
| -rw-r--r-- | lib/mauve/notifiers/xmpp.rb | 296 | 
11 files changed, 1000 insertions, 0 deletions
| diff --git a/lib/mauve/notifiers/debug.rb b/lib/mauve/notifiers/debug.rb new file mode 100644 index 0000000..889a428 --- /dev/null +++ b/lib/mauve/notifiers/debug.rb @@ -0,0 +1,68 @@ +require 'fileutils' + +module Mauve +  module Notifiers +    # +    # The Debug module adds two extra parameters to a notification method +    # for debugging and testing. +    # +    module Debug +      class << self +        def included(base) +          base.class_eval do +            alias_method :send_alert_without_debug, :send_alert +            alias_method :send_alert, :send_alert_to_debug_channels +             +            # Specifying deliver_to_file allows the administrator to ask for alerts +            # to be delivered to a particular file, which is assumed to be perused +            # by a person rather than a machine. +            # +            attr :deliver_to_file, true +             +            # Specifying deliver_to_queue allows a tester to ask for the send_alert +            # parameters to be appended to a Queue object (or anything else that  +            # responds to <<). +            #  +            attr :deliver_to_queue, true +          end +        end +      end +       +      def disable_normal_delivery! +        @disable_normal_delivery = true +      end +       +      def send_alert_to_debug_channels(destination, alert, all_alerts, conditions = nil) +        message = if respond_to?(:prepare_message) +          prepare_message(destination, alert, all_alerts, conditions) +        else +          [destination, alert, all_alerts].inspect +        end +         +        if deliver_to_file +          #lock_file = "#{deliver_to_file}.lock" +          #while File.exists?(lock_file) +          #  sleep 0.1 +          #end +          #FileUtils.touch(lock_file) +          File.open("#{deliver_to_file}", "a+") do |fh| +            fh.flock(File::LOCK_EX) +            fh.print("#{MauveTime.now} from #{self.class}: " + message + "\n") +            fh.flush() +          end +          #FileUtils.rm(lock_file) +        end +         +        deliver_to_queue << [destination, alert, all_alerts, conditions] if deliver_to_queue +         +        if  @disable_normal_delivery +          true # pretend it happened OK if we're just testing +        else +          send_alert_without_debug(destination, alert, all_alerts, conditions) +        end +      end +       +    end +  end +end + diff --git a/lib/mauve/notifiers/email.rb b/lib/mauve/notifiers/email.rb new file mode 100644 index 0000000..c445e09 --- /dev/null +++ b/lib/mauve/notifiers/email.rb @@ -0,0 +1,138 @@ +require 'time' +require 'net/smtp' +require 'rmail' +require 'mauve/notifiers/debug' + +module Mauve +  module Notifiers +    module Email +     +       +      class Default         +        attr_reader :name +        attr :server, true +        attr :port, true +        attr :username, true +        attr :password, true +        attr :login_method, true +        attr :from, true +        attr :subject_prefix, true +        attr :email_suffix, true +         +        def username=(username) +          @login_method ||= :plain +          @username = username +        end +         +        def initialize(name) +          @name = name +          @server = '127.0.0.1' +          @port = 25 +          @username = nil +          @password = nil +          @login_method = nil +          @from = "mauve@localhost"  +          @hostname = "localhost" +          @signature = "This is an automatic mailing, please do not reply." +          @subject_prefix = "" +          @suppressed_changed = nil +        end + +        def send_alert(destination, alert, all_alerts, conditions = nil) +          message = prepare_message(destination, alert, all_alerts, conditions) +          args  = [@server, @port] +          args += [@username, @password, @login_method.to_sym] if @login_method +          begin +            Net::SMTP.start(*args) do |smtp| +              smtp.send_message(message, @from, destination) +            end +          rescue Errno::ECONNREFUSED => e +            @logger = Log4r::Logger.new "mauve::email_send_alert" +            @logger.error("#{e.class}: #{e.message} raised. " + +                          "args = #{args.inspect} " +                         ) +            raise e +          rescue => e +            raise e +          end +        end +         +        protected +         +        def prepare_message(destination, alert, all_alerts, conditions = nil) +          if conditions +            @suppressed_changed = conditions[:suppressed_changed] +          end +           +          other_alerts = all_alerts - [alert] +           +          m = RMail::Message.new +           +          m.header.subject = subject_prefix +  +            case @suppressed_changed +            when true +              "Suppressing notifications (#{all_alerts.length} total)" +             +            else +              alert.summary_one_line.to_s  +          end +          m.header.to = destination +          m.header.from = @from +          m.header.date = MauveTime.now + +          summary_formatted = ""           +#          summary_formatted = "  * "+alert.summary_two_lines.join("\n  ") +                     +          case alert.update_type.to_sym +            when :cleared +              m.body = "An alert has been cleared:\n"+summary_formatted+"\n\n" +            when :raised +              m.body = "An alert has been raised:\n"+summary_formatted+"\n\n" +            when :acknowledged +              m.body = "An alert has been acknowledged by #{alert.acknowledged_by}:\n"+summary_formatted+"\n\n" +            when :changed +              m.body = "An alert has changed in nature:\n"+summary_formatted+"\n\n" +            else +              raise ArgumentError.new("Unknown update_type #{alert.update_type}") +          end +           +          # FIXME: include alert.detail as multipart mime +          ##Thread.abort_on_exception = true +          m.body += "\n" + '-'*10 + " This is the detail field " + '-'*44 + "\n\n" +          #m.body += alert.get_details() +          #m.body += alert.get_details_plain_text() +          m.body += "\n" + '-'*80 + "\n\n" +           +          if @suppressed_changed == true +            m.body += <<-END +IMPORTANT: I've been configured to suppress notification of individual changes +to alerts until their rate decreases.  If you still need notification of evrey +single alert, you must watch the web front-end instead. + +            END +          elsif @suppressed_changed == false +            m.body += "(Notifications have slowed down - you will now be notified of every change)\n\n" +          end +           +          if other_alerts.empty? +            m.body += (alert.update_type == :cleared ? "That was" : "This is")+ +              " currently the only alert outstanding\n\n" +          else +            m.body += other_alerts.length == 1 ?  +              "There is currently one other alert outstanding:\n\n" : +              "There are currently #{other_alerts.length} other alerts outstanding:\n\n" +             +#            other_alerts.each do |other| +#              m.body += "  * "+other.summary_two_lines.join("\n  ")+"\n\n" +#            end +          end +           +          m.body += @email_suffix +           +          m.to_s +        end +        include Debug +      end +    end +  end +end diff --git a/lib/mauve/notifiers/sms_aql.rb b/lib/mauve/notifiers/sms_aql.rb new file mode 100644 index 0000000..1cf8c04 --- /dev/null +++ b/lib/mauve/notifiers/sms_aql.rb @@ -0,0 +1,90 @@ +require 'mauve/notifiers/debug' +require 'cgi' + +module Mauve +  module Notifiers +    module Sms +       +      require 'net/https' +      class AQL +        GATEWAY = "https://gw1.aql.com/sms/sms_gw.php" + +        attr :username, true +        attr :password, true +        attr :from, true +        attr :max_messages_per_alert, true +        attr_reader :name + +        def initialize(name) +          @name = name +        end + +        def send_alert(destination, alert, all_alerts, conditions = nil) +          uri = URI.parse(GATEWAY) +                   +          opts_string = { +            :username => @username, +            :password => @password, +            :destination => normalize_number(destination), +            :message => prepare_message(destination, alert, all_alerts, conditions), +            :originator => @from, +            :flash => @flash ? 1 : 0 +          }.map { |k,v| "#{k}=#{CGI::escape(v.to_s)}" }.join("&") +           +          http = Net::HTTP.new(uri.host, uri.port) +          if uri.port == 443 +            http.use_ssl = true +            http.verify_mode = OpenSSL::SSL::VERIFY_NONE +          end +          response, data = http.post(uri.path, opts_string, { +            'Content-Type' => 'application/x-www-form-urlencoded', +            'Content-Length' => opts_string.length.to_s +          }) +           +          raise response unless response.kind_of?(Net::HTTPSuccess) +        end +         +        protected +        def prepare_message(destination, alert, all_alerts, conditions=nil) +          if conditions +            @suppressed_changed = conditions[:suppressed_changed] +          end +           +          txt = case @suppressed_changed +            when true then "TOO MUCH NOISE!  Last notification: " +            when false then "BACK TO NORMAL: " +            else  +              "" +          end +           +          txt += "#{alert.update_type.upcase}: " +          txt += alert.summary_one_line +           +          others = all_alerts-[alert] +          if !others.empty? +            txt += (1 == others.length)?  +              "and a lone other." :  +              "and #{others.length} others." +            #txt += "and #{others.length} others: " +            #txt += others.map { |alert| alert.summary_one_line }.join(", ") +          end + +          txt += "link: https://alert.bytemark.co.uk/alerts" + +          ## @TODO:  Add a link to acknowledge the alert in the text? +          #txt += "Acknoweledge alert: "+ +          #       "https://alert.bytemark.co.uk/alert/acknowledge/"+ +          #       "#{alert.id}/#{alert.get_default_acknowledge_time} + +          txt +        end +         +        def normalize_number(n) +          n.split("").select { |s| (?0..?9).include?(s[0]) }.join.gsub(/^0/, "44") +        end +        include Debug +      end +    end +  end +end + diff --git a/lib/mauve/notifiers/sms_default.rb b/lib/mauve/notifiers/sms_default.rb new file mode 100644 index 0000000..5afeedd --- /dev/null +++ b/lib/mauve/notifiers/sms_default.rb @@ -0,0 +1,12 @@ +module Mauve +  module Notifiers +    module Sms +      class Default +        def initialize(*args) +          raise ArgumentError.new("No default SMS provider, you must use the provider command to select one") +        end +      end +    end +  end +end + diff --git a/lib/mauve/notifiers/templates/email.html.erb b/lib/mauve/notifiers/templates/email.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/email.html.erb diff --git a/lib/mauve/notifiers/templates/email.txt.erb b/lib/mauve/notifiers/templates/email.txt.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/email.txt.erb diff --git a/lib/mauve/notifiers/templates/sms.txt.erb b/lib/mauve/notifiers/templates/sms.txt.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/sms.txt.erb diff --git a/lib/mauve/notifiers/templates/xmpp.html.erb b/lib/mauve/notifiers/templates/xmpp.html.erb new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/mauve/notifiers/templates/xmpp.html.erb diff --git a/lib/mauve/notifiers/templates/xmpp.txt.erb b/lib/mauve/notifiers/templates/xmpp.txt.erb new file mode 100644 index 0000000..881c197 --- /dev/null +++ b/lib/mauve/notifiers/templates/xmpp.txt.erb @@ -0,0 +1 @@ +<%=arse %> diff --git a/lib/mauve/notifiers/xmpp-smack.rb b/lib/mauve/notifiers/xmpp-smack.rb new file mode 100644 index 0000000..a160a35 --- /dev/null +++ b/lib/mauve/notifiers/xmpp-smack.rb @@ -0,0 +1,395 @@ +# encoding: utf-8 + +# Ruby. +require 'pp' +require 'log4r' +require 'monitor' + +# Java.  Note that paths are mangeled in jmauve_starter. +require 'java' +require 'smack.jar' +require 'smackx.jar' +include_class "org.jivesoftware.smack.XMPPConnection" +include_class "org.jivesoftware.smackx.muc.MultiUserChat" +include_class "org.jivesoftware.smack.RosterListener" + +module Mauve + +  module Notifiers     + +    module Xmpp +       +      class XMPPSmackException < StandardError +      end + +      ## Main wrapper to smack java library. +      # +      # @author Yann Golanski +      # @see http://www.igniterealtime.org/builds/smack/docs/3.1.0/javadoc/ +      # +      # This is a singleton which is not idea but works well for mauve's  +      # configuration file set up.  +      # +      # In general, this class is meant to be intialized then the method  +      # create_slave_thread must be called.  The latter will spawn a new  +      # thread that will do the connecting and sending of messages to  +      # the XMPP server.  Once this is done, messages can be send via the  +      # send_msg() method.  Those will be queued and depending on the load, +      # should be send quickly enough.  This is done so that the main thread +      # can not worry about sending messages and can do important work.  +      # +      # @example +      #  bot = Mauve::Notifiers::Xmpp::XMPPSmack.new() +      #  bot.run_slave_thread("chat.bytemark.co.uk", 'mauvealert', 'TopSecret') +      #  msg = "What fresh hell is this? -- Dorothy Parker." +      #  bot.send_msg("yann@chat.bytemark.co.uk", msg) +      #  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg) +      # +      # @FIXME  This won't quiet work with how mauve is set up.  +      # +      class XMPPSmack +         +        # Globals are evil. +        @@instance = nil + +        # Default constructor. +        # +        # A queue (@queue) is used to pass information between master/slave. +        def initialize () +          extend(MonitorMixin) +          @logger =  Log4r::Logger.new "mauve::XMPP_smack<#{Process.pid}>" +          @queue = Queue.new  +          @xmpp = nil +          @name = "mauve alert" +          @slave_thread = nil +          @regexp_muc = Regexp.compile(/^muc\:/) +          @regexp_tail = Regexp.compile(/\/.*$/) +          @jid_created_chat = Hash.new() +          @separator = '<->' +          @logger.info("Created XMPPSmack singleton") +        end + +        # Returns the instance of the XMPPSmack singleton. +        # +        # @param [String] login The JID as a full address. +        # @param [String] pwd The password corresponding to the JID. +        # @return [XMPPSmack] The singleton instance. +        def self.instance (login, pwd) +          if true == @@instance.nil? +            @@instance = XMPPSmack.new +            jid, tmp = login.split(/@/) +            srv, name = tmp.split(/\//) +            name = "Mauve Alert Bot" if true == name.nil? +            @@instance.run_slave_thread(srv, jid, pwd, name) +            sleep 5 # FIXME: This really should be synced... But how? +          end +          return @@instance +        end + +        # Create the thread that sends messages to the server. +        # +        # @param [String] srv The server address. +        # @param [String] jid The JID. +        # @param [String] pwd The password corresponding to the JID. +        # @param [String] name The bot name. +        # @return [NULL] nada +        def run_slave_thread (srv, jid, pwd, name) +          @srv = srv +          @jid = jid +          @pwd = pwd +          @name = name +          @logger.info("Creating slave thread on #{@jid}@#{@srv}/#{@name}.") +          @slave_thread = Thread.new do  +            self.create_slave_thread() +          end +          return nil +        end + +        # Returns whether instance is connected and authenticated. +        # +        # @return [Boolean] True or false. +        def is_connected_and_authenticated? () +          return false if true == @xmpp.nil? +          return (@xmpp.isConnected() and @xmpp.isAuthenticated()) +        end + +        # Creates the thread that does the actual sending to XMPP. +        # @return [NULL] nada +        def create_slave_thread () +          begin +            @logger.info("Slave thread is now alive.") +            self.open() +            loop do +              rcp, msg = @queue.deq().split(@separator, 2) +              @logger.debug("New message for '#{rcp}' saying '#{msg}'.") +              if rcp.match(@regexp_muc) +                room = rcp.gsub(@regexp_muc, '').gsub(@regexp_tail, '') +                self.send_to_muc(room, msg) +              else +                self.send_to_jid(rcp, msg) +              end +            end +          rescue XMPPSmackException +            @logger.fatal("Something is wrong") +          ensure  +            @logger.info("XMPP bot disconnect.") +            @xmpp.disconnect() +          end +          return nil +        end + +        # Send a message to the recipient. +        # +        # @param [String] rcp The recipent MUC or JID. +        # @param [String] msg The message. +        # @return [NULL] nada +        def send_msg(rcp, msg) +          #if @slave_thread.nil? or not self.is_connected_and_authenticated?() +          #  str = "There is either no slave thread running or a disconnect..." +          #  @logger.warn(str) +          #  self.reconnect() +          #end +          @queue.enq(rcp + @separator + msg) +          return nil +        end + +        # Sends a message to a room. +        # +        # @param [String] room The name of the room. +        # @param [String] mgs The message to send. +        # @return [NULL] nada +        def send_to_muc (room, msg) +          if not @jid_created_chat.has_key?(room) +            @jid_created_chat[room] = MultiUserChat.new(@xmpp, room) +            @jid_created_chat[room].join(@name) +          end +          @logger.debug("Sending to MUC '#{room}' message '#{msg}'.") +          @jid_created_chat[room].sendMessage(msg) +          return nil +        end + +        # Sends a message to a jid. +        # +        # Do not destroy the chat, we can reuse it when the user log back in again.  +        # Maybe? +        # +        # @param [String] jid The JID of the recipient. +        # @param [String] mgs The message to send. +        # @return [NULL] nada +        def send_to_jid (jid, msg) +          if true == jid_is_available?(jid) +            if not @jid_created_chat.has_key?(jid) +              @jid_created_chat[jid] = @xmpp.getChatManager.createChat(jid, nil) +            end +            @logger.debug("Sending to JID '#{jid}' message '#{msg}'.") +            @jid_created_chat[jid].sendMessage(msg) +          end +          return nil +        end + +        # Check to see if the jid is available or not. +        # +        # @param [String] jid The JID of the recipient. +        # @return [Boolean] Whether we can send a message or not. +        def jid_is_available?(jid) +          if true == @xmpp.getRoster().getPresence(jid).isAvailable() +            @logger.debug("#{jid} is available. Status is " + +                          "#{@xmpp.getRoster().getPresence(jid).getStatus()}") +            return true +          else +            @logger.warn("#{jid} is not available. Status is " + +                         "#{@xmpp.getRoster().getPresence(jid).getStatus()}") +            return false +          end +        end + +        # Opens a connection to the xmpp server at given port. +        # +        # @return [NULL] nada +        def open() +          @logger.info("XMPP bot is being created.") +          self.open_connection() +          self.open_authentication() +          self.create_roster() +          sleep 5 +          return nil +        end + +        # Connect to server. +        # +        # @return [NULL] nada +        def open_connection() +          @xmpp = XMPPConnection.new(@srv) +          if false == self.connect() +            str = "Connection refused" +            @logger.error(str) +            raise XMPPSmackException.new(str) +          end +          @logger.debug("XMPP bot connected successfully.") +          return nil +        end + +        # Authenticat connection. +        # +        # @return [NULL] nada +        def open_authentication() +          if false == self.login(@jid, @pwd) +            str = "Authentication failed" +            @logger.error(str) +            raise XMPPSmackException.new(str) +          end +          @logger.debug("XMPP bot authenticated successfully.") +          return nil +        end + +        # Create a new roster and listener. +        # +        # @return [NULL] nada +        def create_roster +          @xmpp.getRoster().addRosterListener(RosterListener.new()) +          @xmpp.getRoster().reload() +          @xmpp.getRoster().getPresence(@xmpp.getUser).setStatus( +            "Purple alert! Purple alert!") +          @logger.debug("XMPP bot roster aquired successfully.") +          return nil +        end + +        # Connects to the server. +        # +        # @return [Boolean] true (aka sucess) or false (aka failure). +        def connect () +          @xmpp.connect() +          return @xmpp.isConnected() +        end +         +        # Login onto the server. +        # +        # @param [String] jid The JID. +        # @param [String] pwd The password corresponding to the JID. +        # @return [Boolean] true (aka sucess) or false (aka failure). +        def login (jid, pwd) +          @xmpp.login(jid, pwd, @name) +          return @xmpp.isAuthenticated() +        end + +        # Reconnects in case of errors. +        # +        # @return [NULL] nada +        def reconnect() +          @xmpp.disconnect +          @slave_thread = Thread.new do  +            self.create_slave_thread() +          end +          return nil +        end + +        def presenceChanged () +        end + +      end # XMPPSmack + + +      ## This is the class that gets called in person.rb.  +      # +      # This class is a wrapper to XMPPSmack which does the hard work. It is +      # done this way to conform to the mauve configuration file way of  +      # defining notifications. +      # +      # @author Yann Golanski +      class Default + +        # Name of the class. +        attr_reader :name + +        # Atrtribute. +        attr_accessor :jid + +        # Atrtribute. +        attr_accessor :password + +        # Atrtribute. +        attr_accessor :initial_jid + +        # Atrtribute. +        attr_accessor :initial_messages +         +        # Default constructor. +        # +        # @param [String] name The name of the notifier. +        def initialize (name) +          extend(MonitorMixin) +          @name = name +          @logger = Log4r::Logger.new "mauve::XMPP_default<#{Process.pid}>" +        end + +        # Sends a message to the relevant jid or muc. +        # +        # We have no way to know if a messages was recieved, only that  +        # we send it. +        #  +        # @param [String] destionation +        # @param [Alert] alert A mauve alert class +        # @param [Array] all_alerts subset of current alerts +        # @param [Hash] conditions Supported conditions, see above. +        # @return [Boolean] Whether a message can be send or not.  +        def send_alert(destination, alert, all_alerts, conditions = nil) +          synchronize {  +            client = XMPPSmack.instance(@jid, @password)  +            if not destination.match(/^muc:/) +              if false == client.jid_is_available?(destination.gsub(/^muc:/, '')) +                return false +              end +            end +            client.send_msg(destination, convert_alert_to_message(alert)) +            return true +          } +        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 "<p>" + alert.summary_two_lines.join("<br />") + "</p>" +        end + +        # This is so unit tests can run fine. +        include Debug + +      end # Default + +    end +  end +end + +# This is a simple example of usage.  Run with: +#   ../../../jmauve_starter.rb xmpp-smack.rb  +# Clearly, the mauve jabber password is not correct.   +# +#   /!\ WARNING:   DO NOT COMMIT THE REAL PASSWORD TO MERCURIAL!!! +# +def send_msg() +  bot = Mauve::Notifiers::Xmpp::XMPPSmack.instance( +    "mauvealert@chat.bytemark.co.uk/testing1234", '') +  msg = "What fresh hell is this? -- Dorothy Parker." +  bot.send_msg("yann@chat.bytemark.co.uk", msg) +  bot.send_msg("muc:test@conference.chat.bytemark.co.uk", msg) +  sleep 2 +end + +if __FILE__ == './'+$0 +  Thread.abort_on_exception = true +  logger = Log4r::Logger.new('mauve') +  logger.level = Log4r::DEBUG +  logger.add Log4r::Outputter.stdout +  send_msg() +  send_msg() +  logger.info("START") +  logger.info("END") +end diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb new file mode 100644 index 0000000..d216788 --- /dev/null +++ b/lib/mauve/notifiers/xmpp.rb @@ -0,0 +1,296 @@ +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 +          logger.debug "Jabber authentication" + +          @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 + +          @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 "<p>" + alert.summary_two_lines.join("<br />") + "</p>" +        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:<room>@<server> 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("<p>" + +                                    convert_alert_to_message(alert)+ +#                                    alert.summary_three_lines.join("<br />") + +                                    #alert.summary_two_lines.join("<br />") + +                                    "</p>")  +            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 +    end +  end +end + | 
