public
Description: Tomporary (hopefully) fork of XMPP4R-Simple
Homepage: http://code.google.com/p/xmpp4r-simple/
Clone URL: git://github.com/dysinger/xmpp4r-simple.git
Search Repo:
xmpp4r-simple / lib / xmpp4r-simple.rb
100644 492 lines (433 sloc) 15.44 kb
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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
# Jabber::Simple - An extremely easy-to-use Jabber client library.
# Copyright 2006 Blaine Cook <blaine@obvious.com>, Obvious Corp.
#
# Jabber::Simple is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Jabber::Simple is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Jabber::Simple; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 
require 'rubygems'
require 'xmpp4r'
require 'xmpp4r/roster'
require 'xmpp4r/vcard'
 
module Jabber
 
  class ConnectionError < StandardError #:nodoc:
  end
  
  class Contact #:nodoc:
 
    include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
 
    def initialize(client, jid)
      @jid = jid.respond_to?(:resource) ? jid : JID.new(jid)
      @client = client
    end
 
    def inspect
      "Jabber::Contact #{jid.to_s}"
    end
 
    def subscribed?
      [:to, :both].include?(subscription)
    end
 
    def subscription
      roster_item && roster_item.subscription
    end
 
    def ask_for_authorization!
      subscription_request = Presence.new.set_type(:subscribe)
      subscription_request.to = jid
      client.send!(subscription_request)
    end
    
    def unsubscribe!
      unsubscription_request = Presence.new.set_type(:unsubscribe)
      unsubscription_request.to = jid
      client.send!(unsubscription_request)
      client.send!(unsubscription_request.set_type(:unsubscribed))
    end
 
    def jid(bare=true)
      bare ? @jid.strip : @jid
    end
 
    private
 
    def roster_item
      client.roster.items[jid]
    end
   
    def client
      @client
    end
  end
 
  class Simple
 
    include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
 
    # Create a new Jabber::Simple client. You will be automatically connected
    # to the Jabber server and your status message will be set to the string
    # passed in as the status_message argument.
    #
    # jabber = Jabber::Simple.new("me@example.com", "password", "Chat with me - Please!")
    def initialize(jid, password, status = nil, status_message = "Available")
      @jid = jid
      @password = password
      @disconnected = false
      status(status, status_message)
      start_deferred_delivery_thread
    end
 
    def inspect #:nodoc:
      "Jabber::Simple #{@jid}"
    end
    
    # Send a message to jabber user jid.
    #
    # Valid message types are:
    #
    # * :normal (default): a normal message.
    # * :chat: a one-to-one chat message.
    # * :groupchat: a group-chat message.
    # * :headline: a "headline" message.
    # * :error: an error message.
    #
    # If the recipient is not in your contacts list, the message will be queued
    # for later delivery, and the Contact will be automatically asked for
    # authorization (see Jabber::Simple#add).
    #
    # message should be a string or a valid Jabber::Message object. In either case,
    # the message recipient will be set to jid.
    def deliver(jid, message, type=:chat)
      contacts(jid) do |friend|
        unless subscribed_to? friend
          add(friend.jid)
          return deliver_deferred(friend.jid, message, type)
        end
        if message.kind_of?(Jabber::Message)
          msg = message
          msg.to = friend.jid
        else
          msg = Message.new(friend.jid)
          msg.type = type
          msg.body = message
        end
        send!(msg)
      end
    end
 
    # Set your presence, with a message.
    #
    # Available values for presence are:
    #
    # * nil: online.
    # * :chat: free for chat.
    # * :away: away from the computer.
    # * :dnd: do not disturb.
    # * :xa: extended away.
    #
    # It's not possible to set an offline status - to do that, disconnect! :-)
    def status(presence, message)
      @presence = presence
      @status_message = message
      stat_msg = Presence.new(@presence, @status_message)
      send!(stat_msg)
    end
 
    # Ask the users specified by jids for authorization (i.e., ask them to add
    # you to their contact list). If you are already in the user's contact list,
    # add() will not attempt to re-request authorization. In order to force
    # re-authorization, first remove() the user, then re-add them.
    #
    # Example usage:
    #
    # jabber_simple.add("friend@friendosaurus.com")
    #
    # Because the authorization process might take a few seconds, or might
    # never happen depending on when (and if) the user accepts your
    # request, results are placed in the Jabber::Simple#new_subscriptions queue.
    def add(*jids)
      contacts(*jids) do |friend|
        next if subscribed_to? friend
        friend.ask_for_authorization!
      end
    end
 
    # Remove the jabber users specified by jids from the contact list.
    def remove(*jids)
      contacts(*jids) do |unfriend|
        unfriend.unsubscribe!
      end
    end
 
    # Returns true if this Jabber account is subscribed to status updates for
    # the jabber user jid, false otherwise.
    def subscribed_to?(jid)
      contacts(jid) do |contact|
        return contact.subscribed?
      end
    end
 
    # If contacts is a single contact, returns a Jabber::Contact object
    # representing that user; if contacts is an array, returns an array of
    # Jabber::Contact objects.
    #
    # When called with a block, contacts will yield each Jabber::Contact object
    # in turn. This is mainly used internally, but exposed as an utility
    # function.
    def contacts(*contacts, &block)
      @contacts ||= {}
      contakts = []
      contacts.each do |contact|
        jid = contact.to_s
        unless @contacts[jid]
          @contacts[jid] = contact.respond_to?(:ask_for_authorization!) ? contact : Contact.new(self, contact)
        end
        yield @contacts[jid] if block_given?
        contakts << @contacts[jid]
      end
      contakts.size > 1 ? contakts : contakts.first
    end
 
    # Returns true if the Jabber client is connected to the Jabber server,
    # false otherwise.
    def connected?
      @client ||= nil
      connected = @client.respond_to?(:is_connected?) && @client.is_connected?
      return connected
    end
 
    # Returns an array of messages received since the last time
    # received_messages was called. Passing a block will yield each message in
    # turn, allowing you to break part-way through processing (especially
    # useful when your message handling code is not thread-safe (e.g.,
    # ActiveRecord).
    #
    # e.g.:
    #
    # jabber.received_messages do |message|
    # puts "Received message from #{message.from}: #{message.body}"
    # end
    def received_messages(&block)
      dequeue(:received_messages, &block)
    end
 
    # Returns true if there are unprocessed received messages waiting in the
    # queue, false otherwise.
    def received_messages?
      !queue(:received_messages).empty?
    end
 
    # Returns an array of presence updates received since the last time
    # presence_updates was called. Passing a block will yield each update in
    # turn, allowing you to break part-way through processing (especially
    # useful when your presence handling code is not thread-safe (e.g.,
    # ActiveRecord).
    #
    # e.g.:
    #
    # jabber.presence_updates do |friend, new_presence|
    # puts "Received presence update from #{friend}: #{new_presence}"
    # end
    def presence_updates(&block)
      updates = []
      @presence_mutex.synchronize do
        dequeue(:presence_updates) do |friend|
          presence = @presence_updates[friend]
          next unless presence
          new_update = [friend, presence[0], presence[1]]
          yield new_update if block_given?
          updates << new_update
          @presence_updates.delete(friend)
        end
      end
      return updates
    end
    
    # Returns true if there are unprocessed presence updates waiting in the
    # queue, false otherwise.
    def presence_updates?
      !queue(:presence_updates).empty?
    end
 
    # Returns an array of subscription notifications received since the last
    # time new_subscriptions was called. Passing a block will yield each update
    # in turn, allowing you to break part-way through processing (especially
    # useful when your subscription handling code is not thread-safe (e.g.,
    # ActiveRecord).
    #
    # e.g.:
    #
    # jabber.new_subscriptions do |friend, presence|
    # puts "Received presence update from #{friend.to_s}: #{presence}"
    # end
    def new_subscriptions(&block)
      dequeue(:new_subscriptions, &block)
    end
    
    # Returns true if there are unprocessed presence updates waiting in the
    # queue, false otherwise.
    def new_subscriptions?
      !queue(:new_subscriptions).empty?
    end
    
    # Returns an array of subscription notifications received since the last
    # time subscription_requests was called. Passing a block will yield each update
    # in turn, allowing you to break part-way through processing (especially
    # useful when your subscription handling code is not thread-safe (e.g.,
    # ActiveRecord).
    #
    # e.g.:
    #
    # jabber.subscription_requests do |friend, presence|
    # puts "Received presence update from #{friend.to_s}: #{presence}"
    # end
    def subscription_requests(&block)
      dequeue(:subscription_requests, &block)
    end
 
    # Returns true if auto-accept subscriptions (friend requests) is enabled
    # (default), false otherwise.
    def accept_subscriptions?
      @accept_subscriptions = true if @accept_subscriptions.nil?
      @accept_subscriptions
    end
 
    # Change whether or not subscriptions (friend requests) are automatically accepted.
    def accept_subscriptions=(accept_status)
      @accept_subscriptions = accept_status
    end
    
    # Direct access to the underlying Roster helper.
    def roster
      return @roster if @roster
      self.roster = Roster::Helper.new(client)
    end
 
    # Direct access to the underlying Jabber client.
    def client
      connect!() unless connected?
      @client
    end
    
    # Send a Jabber stanza over-the-wire.
    def send!(msg)
      attempts = 0
      begin
        attempts += 1
        client.send(msg)
      rescue Errno::EPIPE, IOError => e
        sleep 1
        disconnect
        reconnect
        retry unless attempts > 3
        raise e
      rescue Errno::ECONNRESET => e
        sleep (attempts^2) * 60 + 60
        disconnect
        reconnect
        retry unless attempts > 3
        raise e
      end
    end
 
    # Use this to force the client to reconnect after a force_disconnect.
    def reconnect
      @disconnected = false
      connect!
    end
 
    # Use this to force the client to disconnect and not automatically
    # reconnect.
    def disconnect
      disconnect!
    end
    
    # Queue messages for delivery once a user has accepted our authorization
    # request. Works in conjunction with the deferred delivery thread.
    #
    # You can use this method if you want to manually add friends and still
    # have the message queued for later delivery.
    def deliver_deferred(jid, message, type)
      msg = {:to => jid, :message => message, :type => type}
      queue(:pending_messages) << [msg]
    end
 
    private
 
    def client=(client)
      self.roster = nil # ensure we clear the roster, since that's now associated with a different client.
      @client = client
    end
 
    def roster=(new_roster)
      @roster = new_roster
    end
 
    def connect!
      raise ConnectionError, "Connections are disabled - use Jabber::Simple::force_connect() to reconnect." if @disconnected
      # Pre-connect
      @connect_mutex ||= Mutex.new
 
      # don't try to connect if another thread is already connecting.
      return if @connect_mutex.locked?
 
      @connect_mutex.lock
      disconnect!(false) if connected?
 
      # Connect
      jid = JID.new(@jid)
      my_client = Client.new(@jid)
      my_client.connect
      my_client.auth(@password)
      self.client = my_client
 
      # Post-connect
      register_default_callbacks
      status(@presence, @status_message)
      @connect_mutex.unlock
    end
 
    def disconnect!(auto_reconnect = true)
      if client.respond_to?(:is_connected?) && client.is_connected?
        begin
          client.close
        rescue Errno::EPIPE, IOError => e
          # probably should log this.
          nil
        end
      end
      client = nil
      @disconnected = auto_reconnect
    end
 
    def register_default_callbacks
      client.add_message_callback do |message|
        queue(:received_messages) << message unless message.body.nil?
      end
 
      roster.add_subscription_callback do |roster_item, presence|
        if presence.type == :subscribed
          queue(:new_subscriptions) << [roster_item, presence]
        end
      end
 
      roster.add_subscription_request_callback do |roster_item, presence|
        if accept_subscriptions?
          roster.accept_subscription(presence.from)
        else
          queue(:subscription_requests) << [roster_item, presence]
        end
      end
 
      @presence_updates = {}
      @presence_mutex = Mutex.new
      roster.add_presence_callback do |roster_item, old_presence, new_presence|
        simple_jid = roster_item.jid.strip.to_s
        presence = case new_presence.type
                   when nil: new_presence.show || :online
                   when :unavailable: :unavailable
                   else
                     nil
                   end
 
        if presence && @presence_updates[simple_jid] != presence
          queue(:presence_updates) << simple_jid
          @presence_mutex.synchronize { @presence_updates[simple_jid] = [presence, new_presence.status] }
        end
      end
    end
 
    # This thread facilitates the delivery of messages to users who haven't yet
    # accepted an invitation from us. When we attempt to deliver a message, if
    # the user hasn't subscribed, we place the message in a queue for later
    # delivery. Once a user has accepted our authorization request, we deliver
    # any messages that have been queued up in the meantime.
    def start_deferred_delivery_thread #:nodoc:
      Thread.new {
        loop {
          messages = [queue(:pending_messages).pop].flatten
          messages.each do |message|
            if subscribed_to?(message[:to])
              deliver(message[:to], message[:message], message[:type])
            else
              queue(:pending_messages) << message
            end
          end
        }
      }
    end
 
    def queue(queue)
      @queues ||= Hash.new { |h,k| h[k] = Queue.new }
      @queues[queue]
    end
 
    def dequeue(queue, non_blocking = true, max_items = 100, &block)
      queue_items = []
      max_items.times do
        queue_item = queue(queue).pop(non_blocking) rescue nil
        break if queue_item.nil?
        queue_items << queue_item
        yield queue_item if block_given?
      end
      queue_items
    end
  end
end
 
true