aboutsummaryrefslogtreecommitdiff
path: root/lib/mauve/notifiers/xmpp.rb
blob: 18df6b2ed87cafe9a0064bd0254b9f3eac1b3d4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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
          @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
          
          @client.add_message_callback do |m|
            receive_message(m)
          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

      #
      # TODO parse message and ack as needed..?  The trick is here to
      # understand what the person sending the message wants.  Could be
      # difficult.
      def receive_message(message)
        @logger.debug "Received message from #{message.from}.. Ignoring for now."
      end
    end
  end
end