diff options
author | James Carter <james.carter@bytemark.co.uk> | 2017-08-01 15:56:35 +0100 |
---|---|---|
committer | James Carter <james.carter@bytemark.co.uk> | 2017-08-01 15:56:35 +0100 |
commit | 4eff930c3f01414bb454d7bcb5501827cb60289b (patch) | |
tree | a398edd68c7fd30cf3987538e41e9b4df9f561b8 /lib/mauve/notifiers/xmpp.rb | |
parent | 0be1fa0ebadf9435a760582d17f47ff96dc0851c (diff) | |
parent | 814ed65fd415cc62b2f6f661a7f6d1629562544b (diff) |
Merge branch '27-package-and-publish-in-gitlab-ci-retire-maker2-job' into 'develop'
Added CI
Closes #27
See merge request !2
Diffstat (limited to 'lib/mauve/notifiers/xmpp.rb')
-rw-r--r-- | lib/mauve/notifiers/xmpp.rb | 837 |
1 files changed, 0 insertions, 837 deletions
diff --git a/lib/mauve/notifiers/xmpp.rb b/lib/mauve/notifiers/xmpp.rb deleted file mode 100644 index 8ab6d48..0000000 --- a/lib/mauve/notifiers/xmpp.rb +++ /dev/null @@ -1,837 +0,0 @@ -require 'log4r' -require 'xmpp4r' -require 'xmpp4r/roster' -require 'xmpp4r/muc' -# require 'xmpp4r/xhtml' -# require 'xmpp4r/discovery/helper/helper' -require 'mauve/notifiers/debug' - - - -# -# A couple of monkey patches to fix up all this nonsense. -# -module Jabber - # - # Monkey patch of the close commands. For good reasons, though I can't - # remember why. - # - class Stream - def close - # - # Just close - # - close! - end - - def close! - 10.times do - pr = 0 - @tbcbmutex.synchronize { pr = @processing } - break if pr == 0 - Thread::pass if pr > 0 - sleep 1 - end - - # Order Matters here! If this method is called from within - # @parser_thread then killing @parser_thread first would - # mean the other parts of the method fail to execute. - # That would be bad. So kill parser_thread last - @tbcbmutex.synchronize { @processing = 0 } - if @fd and !@fd.closed? - @fd.close - stop - end - @status = DISCONNECTED - end - end -end - - - - -module Mauve - module Notifiers - - # - # This is the Jabber/XMMP notifiers module. - # - module Xmpp - - # - # The default provider is XMMP, although this should really be broken out - # into its own provider to allow multple ways of doing XMPP. - # - class Default - - include Jabber - - # Atrtribute. - attr_reader :name - - # Atrtribute. - attr_accessor :password - - def initialize(name) - Jabber::logger = self.logger -# Jabber::debug = true -# Jabber::warnings = true - - @name = name - @mucs = {} - @roster = nil - @closing = false - @client = nil - end - - # The logger instance - # - # @return [Log4r::Logger] - def logger - # Give the logger a sane name - @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") - end - - # Sets the client's JID - # - # @param [String] jid The JID required. - # @return [Jabber::JID] The client JID. - def jid=(jid) - @jid = JID.new(jid) - end - - # Connects to the XMPP server, and sets up the roster - # - # @return [Jabber::Client, NilClass] The connected client, or nil in the case of failure - def connect - logger.debug "Starting connection to #{@jid}" - - # Make sure we're disconnected. - self.close if @client.is_a?(Client) - - @client = Client.new(@jid) - - @closing = false - @client.connect - @client.auth_nonsasl(@password, false) - @roster = Roster::Helper.new(@client) - - # Unconditionally accept all roster add requests, and respond with a - # roster add + subscription request of our own if we're not subscribed - # already - @roster.add_subscription_request_callback do |ri, presence| - Thread.new do - if is_known_contact?(presence.from) - logger.info("Accepting subscription request from #{presence.from}") - @roster.accept_subscription(presence.from) - ensure_roster_and_subscription!(presence.from) - else - logger.info("Declining subscription request from #{presence.from}") - @roster.decline_subscription(presence.from) - end - end.join - end - - @client.add_message_callback do |m| - receive_message(m) - end - - @roster.wait_for_roster - - @client.send(Presence.new(nil, "Woo!").set_type(nil)) - - logger.info "Connected as #{@jid}" - - # @client.on_exception do |ex, stream, where| - # - # The XMPP4R exception clauses in Stream all close the stream, so - # we just need to reconnect. - # - # unless ex.nil? or @closing - # logger.warn(["Caught",ex.class,ex.to_s,"during XMPP",where].join(" ")) - # logger.debug ex.backtrace.join("\n") - # self.close - # end - # end - rescue StandardError => ex - logger.error "Connect failed #{ex.to_s}" - logger.debug ex.backtrace.join("\n") - self.close - @client = nil - end - - def stop - @client.stop - end - - # - # Closes the XMPP connection, if possible. Sets @client to nil. - # - # @return [NilClass] - def close - @closing = true - if @client - if @client.is_connected? - @mucs.each do |jid, muc| - muc[:client].exit("Goodbye!") if muc[:client].active? - end - @client.send(Presence.new(nil, "Goodbye!").set_type(:unavailable)) - end - @client.close! - end - @client = nil - end - - # Determines if the client is ready. - # - # @return [Boolean] - def ready? - @client.is_a?(Jabber::Client) and @client.is_connected? - end - - # Attempt to send an alert using XMPP. - # - # @param [String] destination 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). - # - # @param [Mauve::Alert] alert This is turned into a pretty - # message and sent to the destination as a message, if +conditions+ - # are met. - # - # @param [Array] all_alerts Currently ignored. - # - # @param [Hash] conditions Conditions that determine if an alert should be sent - # - # @option conditions [Array] :if_presence Checks whether the jid in question - # has a presence matching one or more of the choices - see - # Mauve::Notifiers::Xmpp::Default#check_jid_has_presence for options. - # - # @return [Boolean] - def send_alert(destination, alert, all_alerts, conditions = {}) - destination_jid = JID.new(destination) - - was_suppressed = conditions[:was_suppressed] || false - will_suppress = conditions[:will_suppress] || false - - if conditions && !check_alert_conditions(destination_jid, conditions) - logger.info("Alert conditions not met, not sending XMPP alert to #{destination_jid}") - return false - end - - template_file = File.join(File.dirname(__FILE__),"templates","xmpp.txt.erb") - - txt = if File.exists?(template_file) - ERB.new(File.read(template_file)).result(binding).chomp - else - logger.error("Could not find xmpp.txt.erb template") - alert.to_s - end - - template_file = File.join(File.dirname(__FILE__),"templates","xmpp.html.erb") - - xhtml = if File.exists?(template_file) - ERB.new(File.read(template_file)).result(binding).chomp - else - logger.error("Could not find xmpp.txt.erb template") - alert.to_s - end - - msg_type = (is_muc?(destination_jid) ? :groupchat : :chat) - - send_message(destination_jid, txt, xhtml, msg_type) - end - - # Sends a message to the destionation. - def send_message(jid, msg, html_msg=nil, msg_type=:chat) - return false unless self.ready? - - jid = JID.new(jid) unless jid.is_a?(JID) - - message = Message.new(jid) - message.body = msg - if html_msg - begin - html_msg = REXML::Document.new(html_msg) unless html_msg.is_a?(REXML::Document) - message.add_element(html_msg) - rescue REXML::ParseException - logger.error "Bad XHTML: #{html_msg.inspect}" - end - end - - message.to = jid - message.type = msg_type - - if message.type == :groupchat and is_muc?(jid) - jid = join_muc(jid.strip) - muc = @mucs[jid][:client] - - if muc - muc.send(message) - true - else - logger.warn "Failed to join MUC #{jid} when trying to send a message" - false - end - else - # - # We aren't interested in sending things to people who aren't online. - # - ensure_roster_and_subscription!(jid) - - if check_jid_has_presence(jid) - @client.send(message) - true - else - false - end - end - end - - # - # Joins a chat, and returns the stripped JID of the chat joined. - # - def join_muc(jid, password=nil) - self.connect unless self.ready? - - return unless self.ready? - - if jid.is_a?(String) and jid =~ /^muc:(.*)/ - jid = JID.new($1) - end - - unless jid.is_a?(JID) - logger.warn "#{jid} is not a MUC" - return - end - - jid.resource = @client.jid.resource if jid.resource.to_s.empty? - - if !@mucs[jid.strip] - - logger.debug("Adding new MUC client for #{jid}") - - @mucs[jid.strip] = {:jid => jid, :password => password, :client => Jabber::MUC::MUCClient.new(@client)} - - # Add some callbacks - @mucs[jid.strip][:client].add_message_callback do |m| - receive_message(m) - end - - @mucs[jid.strip][:client].add_private_message_callback do |m| - receive_message(m) - end - - end - - if !@mucs[jid.strip][:client].active? - # - # Make sure we have a resource. - # - @mucs[jid.strip][:client].join(jid, password) - - logger.info("Joined #{jid.strip}") - else - logger.debug("Already joined #{jid.strip}.") - end - - # - # Return the JID object - # - jid.strip - end - - # - # Checks whether the destination JID is a MUC. - # - def is_muc?(jid) - (jid.is_a?(JID) and @mucs.keys.include?(jid.strip)) or - (jid.to_s =~ /^muc:(.*)/) - - # - # It would be nice to use service discovery to determin this, but it - # turns out that it is shite in xmpp4r. It doesn't return straight - # away with an answer, making it a bit useless. Some sort of weird - # threading issue, I think. - # - # begin - # logger.warn caller.join("\n") - # cl = Discovery::Helper.new(@client) - # res = cl.get_info_for(jid.strip) - # @client.wait - # logger.warn "hello #{res.inspect}" - # res.is_a?(Discovery::IqQueryDiscoInfo) and res.identity.category == :conference - # rescue Jabber::ServerError => ex - # false - # end - end - - # - # Checks to see if the JID is in our roster, and whether we are - # subscribed to it or not. Will add to the roster and subscribe as - # is necessary to ensure both are true. - # - def ensure_roster_and_subscription!(jid) - self.connect unless self.ready? - - return unless self.ready? - - return jid if is_muc?(jid) - - jid = JID.new(jid) unless jid.is_a?(JID) - - ri = @roster.find(jid).values.first - @roster.add(jid, nil, true) if ri.nil? - - ri = @roster.find(jid).values.first - ri.subscribe unless [:to, :both, :remove].include?(ri.subscription) - ri.jid - rescue StandardError => ex - logger.error("Problem ensuring that #{jid} is subscribed and in mauve's roster: #{ex.inspect}") - nil - end - - protected - - def receive_message(msg) - # - # Don't talk to self - # - if @jid == msg.from or @mucs.any?{|jid, muc| muc.is_a?(Hash) and muc.has_key?(:client) and muc[:client].jid == msg.from} - return nil - end - - # We only want to hear messages from known contacts. - unless is_known_contact?(msg.from) - # ignore message - logger.info "Ignoring message from unknown contact #{msg.from}" - return nil - end - - case msg.type - when :error - receive_error_message(msg) - when :groupchat - receive_groupchat_message(msg) - else - receive_normal_message(msg) - end - end - - def receive_error_message(msg) - logger.warn("Caught XMPP error #{msg}") - nil - end - - def receive_normal_message(msg) - # - # Treat invites specially - # - if msg.x("jabber:x:conference") - # - # recieved an invite. Need to mangle the jid. - # - jid =JID.new(msg.x("jabber:x:conference").attribute("jid")) - # jid.resource = @client.jid.resource - logger.info "Received an invite to #{jid}" - unless join_muc(jid) - logger.warn "Failed to join MUC #{jid} following invitation" - return nil - end - elsif msg.body - # - # Received a message with a body. - # - jid = msg.from - end - - if jid - reply = parse_command(msg) - send_message(jid, reply, nil, msg.type) - end - end - - def receive_groupchat_message(msg) - # - # We only want group chat messages from MUCs we're already joined to, - # that we've not sent ourselves, that are not historical, and that - # match our resource or node in the body. - # - if @mucs[msg.from.strip][:client].is_a?(MUC::MUCClient) and - msg.x("jabber:x:delay") == nil and - (msg.body =~ /\b#{Regexp.escape(@mucs[msg.from.strip][:client].jid.resource)}\b/i or - msg.body =~ /\b#{Regexp.escape(@client.jid.node)}\b/i) - - receive_normal_message(msg) - end - end - - def parse_command(msg) - case msg.body - when /help(\s+\w+)?/i - do_parse_help(msg) - when /show\s?/i - do_parse_show(msg) - when /ack/i - do_parse_ack(msg) - when /clear/i - do_parse_clear(msg) - when /destroy\s?/i - "Sorry -- destroy has been disabled. Try \"clear\" instead." - else - File.executable?('/usr/games/fortune') ? `/usr/games/fortune -s -n 60`.chomp : "I'd love to stay and chat, but I'm really quite busy" - end - end - - def do_parse_help(msg) - msg.body =~ /help\s+(\w+)/i - cmd = $1 - - return case cmd - when /^show/ - <<EOF -Show command: Lists all raised or acknowledged alerts, or the first or last few. - -e.g. - show -- shows all raised alerts - show ack -- shows all acknowledged alerts - show first 10 acknowledged -- shows first 10 acknowledged - show last 5 raised -- shows last 5 raised alerts -EOF - when /^ack/ - <<EOF -Acknowledge command: Acknowledges one or more alerts for a set period of time. - -The syntax is - - acknowledge <alert list> for <time period> because <note> - - * The alert list is a comma separated list. - * The time period can be spefied in terms of days, hours, minutes, seconds, - which can be wall-clock (default), working, or daytime (see the examples). - * The note is appended to the acknowledgement. - -e.g. - acknowledge 1 for 2 hours -- acknowledges alert no. 1 for 2 wall-clock hours - ack 1,2,3 for 2 working hours -- acknowledges alerts 1, 2, and 3 for 2 working hours - ack 4 for 3 days because something bad happened -- acknowledge alert 4 for 3 wall-clock days with the note "something bad happened" -EOF - when /^destroy/ - <<EOF -Destroy command: Destroys one or more alerts. - -The syntax is - - destroy <alert list> - -where <alert list> is a comma separated list of alert IDs. - -e.g. - destroy 1,2,3 -- destroys alerts 1, 2, and 3. -EOF - - else - "I am Mauve #{Mauve::VERSION}. I understand \"help\", \"show\", \"acknowledge\", and \"destroy\" commands. Try \"help show\"." - end - end - - def do_parse_show(msg) - return "Sorry -- I don't understand your show command." unless - msg.body =~ /show(?:\s+(first|last)\s+(\d+))?(?:\s+(events|raised|ack(?:d|nowledged)?))?/i - - first_or_last = $1 - n_items = ($2 || -1).to_i - - type = $3 || "raised" - type = "acknowledged" if type =~ /^ack/ - - msg = [] - - items = case type - when "acknowledged" - Alert.all_acknowledged.all(:order => [:acknowledged_at.asc]) - when "events" - History.all(:created_at.gte => Time.now - 24.hours) - else - Alert.all_unacknowledged.all(:order => [:raised_at.asc]) - end - - if first_or_last == "first" - items = items.first(n_items) if n_items >= 0 - elsif first_or_last == "last" - items = items.last(n_items) if n_items >= 0 - end - - return "Nothing to show" if items.length == 0 - - template_file = File.join(File.dirname(__FILE__),"templates","xmpp.txt.erb") - if File.exists?(template_file) - template = File.read(template_file) - else - logger.error("Could not find xmpp.txt.erb template") - template = nil - end - - (["Alerts #{type}:"] + items.collect do |alert| - ERB.new(template).result(binding).chomp - end).join("\n") - end - - def do_parse_ack(msg) - return "Sorry -- I don't understand your acknowledge command." unless - msg.body =~ /ack(?:nowledge)?\s+([\d\D]+)\s+for\s+(\d+(?:\.\d+)?)\s+(work(?:ing)?|day(?:time)?|wall(?:-?clock)?)?\s*(day|hour|min(?:ute)?|sec(?:ond))s?(?:\s+(?:cos|cause|as|because)?\s*(.*))?/i - - alerts, n_hours, type_hours, dhms, note = [$1,$2, $3, $4, $5] - - alerts = alerts.split(/\D+/) - - n_hours = case dhms - when /^day/ - n_hours.to_f * 24.0 - when /^min/ - n_hours.to_f / 60.0 - when /^sec/ - n_hours.to_f / 3600.0 - else - n_hours.to_f - end - - type_hours = case type_hours - when /^day/ - "daytime" - when /^work/ - "working" - else - "wallclock" - end - - begin - now = Time.now - ack_until = now.in_x_hours(n_hours, type_hours) - rescue RangeError - return "I'm sorry, you tried to acknowedge for far too long, and my buffers overflowed!" - end - - username = get_username_for(msg.from) - - if is_muc?(Configuration.current.people[username].xmpp) - return "I'm sorry -- if you want to acknowledge alerts, please do it from a private chat" - end - - msg = [] - msg << "Acknowledgement results:" if alerts.length > 1 - - succeeded = [] - - alerts.each do |alert_id| - alert = Alert.get(alert_id) - - if alert.nil? - msg << "#{alert_id}: alert not found" - next - end - - if alert.cleared? - msg << "#{alert_id}: alert already cleared" if alert.cleared? - next - end - - if alert.acknowledge!(Configuration.current.people[username], ack_until) - msg << "#{alert_id}: Acknowledged until #{alert.will_unacknowledge_at.to_s_human}" - succeeded << alert - else - msg << "#{alert_id}: Acknowledgement failed." - end - end - - # - # Add the note. - # - unless note.to_s.empty? - note = Alert.remove_html(note) - h = History.new(:alerts => succeeded, :type => "note", :event => note.to_s, :user => username) - logger.debug h.errors unless h.save - end - - return msg.join("\n") - end - - def do_parse_clear(msg) - return "Sorry -- I don't understand your clear command." unless - msg.body =~ /clear\s+([\d\D]+)(?:\s+(?:coz|cause|cos|because|as)?\s*(.*))?/i - - alerts = $1.split(/\D+/) - note = $2 - - username = get_username_for(msg.from) - - if is_muc?(Configuration.current.people[username].xmpp) - return "I'm sorry -- if you want to clear alerts, please do it from a private chat" - end - - msg = [] - msg << "Clearing results:" if alerts.length > 1 - - alerts.each do |alert_id| - alert = Alert.get(alert_id) - - if alert.nil? - msg << "#{alert_id}: alert not found." - next - end - - if alert.cleared? - msg << "#{alert_id}: alert already cleared." - next - end - - if alert.clear! - msg << "#{alert.to_s} cleared." - else - msg << "#{alert.to_s}: clearing failed." - end - end - - # - # Add the note. - # - unless note.to_s.empty? - note = Alert.remove_html(note) - h = History.new(:alerts => succeeded, :type => "note", :event => note.to_s, :user => username) - logger.debug h.errors unless h.save - end - - return msg.join("\n") - 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) - # - # Returns true if at least one of the presence specifiers for the jid - # is met, false otherwise. Note that if the alerter can't see the alertee's - # presence, only 'unknown' will match - generally, you'll want [:online, :unknown] - def check_jid_has_presence(jid, presence_or_presences = [:online, :unknown]) - return true if is_muc?(jid) - - jid = JID.new(jid) unless jid.is_a?(JID) - - self.connect unless self.ready? - - return false unless self.ready? - - 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 - - # - # Returns the username of the jid, if any - # - def get_username_for(jid) - jid = JID.new(jid) unless jid.is_a?(JID) - - # - # Resolve MUC JIDs. - # - if is_muc?(jid) - muc_jid = get_jid_from_muc_jid(jid) - jid = muc_jid unless muc_jid.nil? - end - - ans = Configuration.current.people.find do |username, person| - next unless person.xmpp.is_a?(JID) - person.xmpp.strip == jid.strip - end - - ans.nil? ? ans : ans.first - end - - # - # Tries to establish a real JID from a MUC JID. - # - def get_jid_from_muc_jid(jid) - # - # Resolve the JID for MUCs. - # - jid = JID.new(jid) unless jid.is_a?(JID) - return nil unless @mucs.has_key?(jid.strip) - return nil unless @mucs[jid.strip].has_key?(:client) - return nil unless @mucs[jid.strip][:client].active? - - roster = @mucs[jid.strip][:client].roster[jid.resource] - return nil unless roster - - x = roster.x('http://jabber.org/protocol/muc#user') - return nil unless x - - items = x.items - return nil if items.nil? or items.empty? - - jids = items.collect{|item| item.jid} - return nil if jids.empty? - - jids.first - end - - def is_known_contact?(jid) - !get_username_for(jid).nil? - end - - end - end - end -end - |