aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve/notifiers
diff options
context:
space:
mode:
authorPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
committerPatrick J Cherry <patrick@bytemark.co.uk>2011-04-13 17:03:16 +0100
commit89a67770e66d11740948e90a41db6cee0482cf8e (patch)
treebe858515fb789a89d68f94975690ab019813726c /lib/mauve/notifiers
new version.
Diffstat (limited to 'lib/mauve/notifiers')
-rw-r--r--lib/mauve/notifiers/debug.rb68
-rw-r--r--lib/mauve/notifiers/email.rb138
-rw-r--r--lib/mauve/notifiers/sms_aql.rb90
-rw-r--r--lib/mauve/notifiers/sms_default.rb12
-rw-r--r--lib/mauve/notifiers/templates/email.html.erb0
-rw-r--r--lib/mauve/notifiers/templates/email.txt.erb0
-rw-r--r--lib/mauve/notifiers/templates/sms.txt.erb0
-rw-r--r--lib/mauve/notifiers/templates/xmpp.html.erb0
-rw-r--r--lib/mauve/notifiers/templates/xmpp.txt.erb1
-rw-r--r--lib/mauve/notifiers/xmpp-smack.rb395
-rw-r--r--lib/mauve/notifiers/xmpp.rb296
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
+