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 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 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 :password def initialize(name) Jabber::logger = self.logger # Jabber::debug = true # Jabber::warnings = true @name = name @mucs = {} @roster = nil @closing = false @client = nil end def logger # Give the logger a sane name @logger ||= Log4r::Logger.new self.class.to_s.sub(/::Default$/,"") end def jid=(jid) @jid = JID.new(jid) end 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") @closing = true connect @mucs.each do |jid, muc| @mucs.delete(jid) join_muc(muc[:jid], muc[:password]) end end end end # # Kills the processor thread # def stop @client.stop end 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 end def ready? @client.is_a?(Jabber::Client) and @client.is_connected? end # # Takes an alert and converts it into a message. # def convert_alert_to_message(alert) end # Attempt to send an alert using XMPP. # +destination+ is the JID you're sending the alert to. This should be # a bare JID in the case of an individual, or muc:@ for # chatrooms (XEP0045). The +alert+ object is turned into a pretty # message and sent to the destination as a message, if the +conditions+ # are met. all_alerts are currently ignored. # # The only suported condition at the moment is :if_presence => [choices] # which checks whether the jid in question has a presence matching one # or more of the choices - see +check_jid_has_presence+ for options. def send_alert(destination, alert, all_alerts, conditions = {}) 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) 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) 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.is_a?(String) and jid =~ /^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) 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) 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/ < for