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 + |