File: roster.rb

package info (click to toggle)
ruby-xmpp4r 0.5.6-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, forky, sid, trixie
  • size: 1,384 kB
  • sloc: ruby: 17,382; xml: 74; sh: 12; makefile: 4
file content (532 lines) | stat: -rw-r--r-- 16,608 bytes parent folder | download
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
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# =XMPP4R - XMPP Library for Ruby
# License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
# Website::http://xmpp4r.github.io

require 'xmpp4r/callbacks'
require 'thread'
require 'xmpp4r/roster/iq/roster'

module Jabber
  module Roster
    ##
    # The Roster helper intercepts <tt><iq/></tt> stanzas with Jabber::IqQueryRoster
    # and <tt><presence/></tt> stanzas, but provides cbs which allow the programmer
    # to keep track of updates.
    #
    # A thread for any received stanza is spawned, so the user can invoke
    # accept_subscription et al in the callback blocks, without stopping
    # the current (= parser) thread when waiting for a reply.
    class Helper
      ##
      # All items in your roster
      # items:: [Hash] ([JID] => [Roster::Helper::RosterItem])
      attr_reader :items

      ##
      # Initialize a new Roster helper
      #
      # Registers its cbs (prio = 120, ref = self)
      #
      # Request a roster
      # (Remember to send initial presence afterwards!)
      #
      # The initialization will not wait for the roster being received,
      # use wait_for_roster.
      #
      # <b>Attention:</b> If you send presence and receive presences
      # before the roster has arrived, the Roster helper will let them
      # pass through and does *not* keep them!
      def initialize(stream, startnow = true)
        @stream = stream
        @items = {}
        @items_lock = Mutex.new
        @roster_wait = Semaphore.new
        @query_cbs = CallbackList.new
        @update_cbs = CallbackList.new
        @presence_cbs = CallbackList.new
        @subscription_cbs = CallbackList.new
        @subscription_request_cbs = CallbackList.new

        # Register cbs
        stream.add_iq_callback(120, self) { |iq|
          if iq.query.kind_of?(IqQueryRoster)
            Thread.new do
              Thread.current.abort_on_exception = true
              handle_iq_query_roster(iq)
            end

            true
          else
            false
          end
        }
        stream.add_presence_callback(120, self) { |pres|
          Thread.new do
            Thread.current.abort_on_exception = true
            handle_presence(pres)
          end
        }
        get_roster if startnow
      end

      def get_roster
        # Request the roster
        rosterget = Iq.new_rosterget
        @stream.send(rosterget)
      end

      ##
      # Wait for first roster query result to arrive
      def wait_for_roster
        @roster_wait.wait
        @roster_wait.run
      end

      ##
      # Add a callback to be called when a query has been processed
      #
      # Because update callbacks are called for each roster item,
      # this may be appropriate to notify that *anything* has updated.
      #
      # Arguments for callback block: The received <tt><iq/></tt> stanza
      def add_query_callback(prio = 0, ref = nil, &block)
        @query_cbs.add(prio, ref, block)
      end

      ##
      # Add a callback for Jabber::Roster::Helper::RosterItem updates
      #
      # Note that this will be called much after initialization
      # for the answer of the initial roster request
      #
      # The block receives two objects:
      # * the old Jabber::Roster::Helper::RosterItem
      # * the new Jabber::Roster::Helper::RosterItem
      def add_update_callback(prio = 0, ref = nil, &block)
        @update_cbs.add(prio, ref, block)
      end

      ##
      # Add a callback for Jabber::Presence updates
      #
      # This will be called for <tt><presence/></tt> stanzas for known RosterItems.
      # Unknown JIDs may still pass and can be caught via Jabber::Stream#add_presence_callback.
      #
      # The block receives three objects:
      # * the Jabber::Roster::Helper::RosterItem
      # * the old Jabber::Presence (or nil)
      # * the new Jabber::Presence (or nil)
      def add_presence_callback(prio = 0, ref = nil, &block)
        @presence_cbs.add(prio, ref, block)
      end

      ##
      # Add a callback for subscription updates,
      # which will be called upon receiving a <tt><presence/></tt> stanza
      # with type:
      # * :subscribed
      # * :unsubscribe
      # * :unsubscribed
      #
      # The block receives two objects:
      # * the Jabber::Roster::Helper::RosterItem (or nil)
      # * the <tt><presence/></tt> stanza
      def add_subscription_callback(prio = 0, ref = nil, &block)
        @subscription_cbs.add(prio, ref, block)
      end

      ##
      # Add a callback for subscription requests,
      # which will be called upon receiving a <tt><presence type='subscribe'/></tt> stanza
      #
      # The block receives two objects:
      # * the Jabber::Roster::Helper::RosterItem (or nil)
      # * the <tt><presence/></tt> stanza
      #
      # Response to this event can be taken with accept_subscription
      # and decline_subscription.
      #
      # Example usage:
      #  my_roster.add_subscription_request_callback do |item,presence|
      #    if accept_subscription_requests
      #      my_roster.accept_subscription(presence.from)
      #    else
      #      my_roster.decline_subscription(presence.from)
      #    end
      #  end
      def add_subscription_request_callback(prio = 0, ref = nil, &block)
        @subscription_request_cbs.add(prio, ref, block)
      end

      private

      ##
      # Handle received <tt><iq/></tt> stanzas,
      # used internally
      def handle_iq_query_roster(iq)
        # If the <iq/> contains <error/> we just ignore that
        # and assume an empty roster
        iq.query.each_element('item') do |item|
          olditem, newitem = nil, nil

          @items_lock.synchronize {
            olditem = @items[item.jid]

            # Handle deletion of item
            if item.subscription == :remove
              @items.delete(item.jid)
            else
              newitem = @items[item.jid] = RosterItem.new(@stream).import(item)
            end
          }
          @update_cbs.process(olditem, newitem)
        end

        @roster_wait.run
        @query_cbs.process(iq)
      end

      ##
      # Handle received <tt><presence/></tt> stanzas,
      # used internally
      def handle_presence(pres)
        item = self[pres.from]

        if [:subscribed, :unsubscribe, :unsubscribed].include?(pres.type)
          @subscription_cbs.process(item, pres)
          true

        elsif pres.type == :subscribe
          @subscription_request_cbs.process(item, pres)
          true

        else
          unless item.nil?
            update_presence(item, pres)
            true  # Callback consumed stanza
          else
            false # Callback did not consume stanza
          end
        end
      end

      ##
      # Update the presence of an item,
      # used internally
      #
      # Callbacks are called here
      def update_presence(item, pres)

        # This requires special handling, to announce all resources offline
        if pres.from.resource.nil? and pres.type == :error
          oldpresences = []
          item.each_presence do |oldpres|
            oldpresences << oldpres
          end

          item.add_presence(pres)
          oldpresences.each { |oldpres|
            @presence_cbs.process(item, oldpres, pres)
          }
        else
          oldpres = item.presence_of(pres.from).nil? ?
            nil :
            Presence.new.import(item.presence_of(pres.from))

          item.add_presence(pres)
          @presence_cbs.process(item, oldpres, pres)
        end
      end

      public

      ##
      # Get an item by jid
      #
      # If not available tries to look for it with the resource stripped
      def [](jid)
        jid = JID.new(jid) unless jid.kind_of? JID

        @items_lock.synchronize {
          if @items.has_key?(jid)
            @items[jid]
          elsif @items.has_key?(jid.strip)
            @items[jid.strip]
          else
            nil
          end
        }
      end

      ##
      # Returns the list of RosterItems which, stripped, are equal to the
      # one you are looking for.
      def find(jid)
        jid = JID.new(jid) unless jid.kind_of? JID

        j = jid.strip
        l = {}
        @items_lock.synchronize {
          @items.each_pair do |k, v|
            l[k] = v if k.strip == j
          end
        }
        l
      end

      ##
      # Groups in this Roster,
      # sorted by name
      #
      # Contains +nil+ if there are ungrouped items
      # result:: [Array] containing group names (String)
      def groups
        res = []
        @items_lock.synchronize {
          @items.each_pair do |jid,item|
            res += item.groups
            res += [nil] if item.groups == []
          end
        }
        res.uniq.sort { |a,b| a.to_s <=> b.to_s }
      end

      ##
      # Get items in a group
      #
      # When group is nil, return ungrouped items
      # group:: [String] Group name
      # result:: Array of [RosterItem]
      def find_by_group(group)
        res = []
        @items_lock.synchronize {
          @items.each_pair do |jid,item|
            res.push(item) if item.groups.include?(group)
            res.push(item) if item.groups == [] and group.nil?
          end
        }
        res
      end

      ##
      # Add a user to your roster
      #
      # Threading is encouraged as the function waits for
      # a result. ServerError is thrown upon error.
      #
      # See Jabber::Roster::Helper::RosterItem#subscribe for details
      # about subscribing. (This method isn't used here but the
      # same functionality applies.)
      #
      # If the item is already in the local roster
      # it will simply send itself
      # jid:: [JID] to add
      # iname:: [String] Optional item name
      # subscribe:: [Boolean] Whether to subscribe to this jid
      def add(jid, iname=nil, subscribe=false)
        if self[jid]
          self[jid].send
        else
          request = Iq.new_rosterset
          request.query.add(Jabber::Roster::RosterItem.new(jid, iname))
          @stream.send_with_id(request)
          # Adding to list is handled by handle_iq_query_roster
        end

        if subscribe
          # Actually the item *should* already be known now,
          # but we do it manually to exclude conditions.
          pres = Presence.new.set_type(:subscribe).set_to(jid.strip)
          @stream.send(pres)
        end
      end

      ##
      # Accept a subscription request
      # * Sends a <presence type='subscribed'/> stanza
      # * Adds the contact to your roster
      # jid:: [JID] of contact
      # iname:: [String] Optional roster item name
      def accept_subscription(jid, iname=nil)
        pres = Presence.new.set_type(:subscribed).set_to(jid.strip)
        @stream.send(pres)

        unless self[jid.strip]
          request = Iq.new_rosterset
          request.query.add(Jabber::Roster::RosterItem.new(jid.strip, iname))
          @stream.send_with_id(request)
        end
      end

      ##
      # Decline a subscription request
      # * Sends a <presence type='unsubscribed'/> stanza
      def decline_subscription(jid)
        pres = Presence.new.set_type(:unsubscribed).set_to(jid.strip)
        @stream.send(pres)
      end

      ##
      # These are extensions to RosterItem to carry presence information.
      # This information is *not* stored in XML!
      class RosterItem < Jabber::Roster::RosterItem
        ##
        # Tracked (online) presences of this RosterItem
        attr_reader :presences

        ##
        # Initialize an empty RosterItem
        def initialize(stream)
          super()
          @stream = stream
          @presences = []
          @presences_lock = Mutex.new
        end

        ##
        # Send the updated RosterItem to the server,
        # i.e. if you modified iname, groups, ...
        def send
          request = Iq.new_rosterset
          request.query.add(self)
          @stream.send(request)
        end

        ##
        # Remove item
        #
        # This cancels both subscription *from* the contact to you
        # and from you *to* the contact.
        #
        # The methods waits for a roster push from the server (success)
        # or throws ServerError upon failure.
        def remove
          request = Iq.new_rosterset
          request.query.add(Jabber::Roster::RosterItem.new(jid, nil, :remove))
          @stream.send_with_id(request)
          # Removing from list is handled by Roster#handle_iq_query_roster
        end

        ##
        # Is any presence of this person on-line?
        #
        # (Or is there any presence? Unavailable presences are
        # deleted.)
        def online?
          @presences_lock.synchronize {
            @presences.select { |pres|
              pres.type.nil?
            }.size > 0
          }
        end

        ##
        # Iterate through all received <tt><presence/></tt> stanzas
        def each_presence(&block)
          # Don't lock here, we don't know what block does...
          @presences.each { |pres|
            yield(pres)
          }
        end

        ##
        # Get specific presence
        # jid:: [JID] Full JID with resource
        def presence_of(jid)
          @presences_lock.synchronize {
            @presences.each { |pres|
              return(pres) if pres.from == jid
            }
          }
          nil
        end

        ##
        # Get presence of highest-priority available resource of this person
        #
        # Returns <tt>nil</tt> if contact is offline
        def presence
          @presences_lock.synchronize {
            @presences.select { |pres|
              pres.type.nil?
            }.max { |pres1, pres2| (pres1.priority || 0) <=> (pres2.priority || 0) }
          }
        end

        ##
        # Add presence and sort presences
        # (unless type is :unavailable or :error)
        #
        # This overwrites previous stanzas with the same destination
        # JID to keep track of resources. Old presence stanzas with
        # <tt>type == :unavailable</tt> will be deleted.
        #
        # If <tt>type == :error</tt> and the presence's origin has no
        # specific resource the contact is treated completely offline.
        def add_presence(newpres)
          @presences_lock.synchronize {
            # Delete old presences with the same JID
            @presences.delete_if do |pres|
              pres.from == newpres.from or pres.from.resource.nil? or pres.type == :unavailable
            end

            if newpres.type == :error and newpres.from.resource.nil?
              # Replace by single error presence
              @presences = [newpres]
            else
              # Add new presence
              @presences.push(newpres)
            end

            @presences.sort!
          }
        end

        ##
        # Send subscription request to the user
        #
        # The block given to Jabber::Roster::Roster#add_update_callback will
        # be called, carrying the RosterItem with ask="subscribe"
        #
        # This function returns immediately after sending the subscription
        # request and will not wait of approval or declination as it may
        # take months for the contact to decide. ;-)
        def subscribe
          pres = Presence.new.set_type(:subscribe).set_to(jid.strip)
          @stream.send(pres)
        end

        ##
        # Unsubscribe from a contact's presence
        #
        # This method waits for a presence with type='unsubscribed'
        # from the contact. It may throw ServerError upon failure.
        #
        # subscription attribute of the item is *from* or *none*
        # afterwards. As long as you don't remove that item and
        # subscription='from' the contact is subscribed to your
        # presence.
        def unsubscribe
          pres = Presence.new.set_type(:unsubscribe).set_to(jid.strip)
          @stream.send(pres) { |answer|
            answer.type == :unsubscribed and
            answer.from.strip == pres.to
          }
        end

        ##
        # Deny the contact to see your presence.
        #
        # This method will not wait and returns immediately
        # as you will need no confirmation for this action.
        #
        # Though, you will get a roster update for that item,
        # carrying either subscription='to' or 'none'.
        def cancel_subscription
          pres = Presence.new.set_type(:unsubscribed).set_to(jid)
          @stream.send(pres)
        end
      end
    end #Class Roster
  end #Module Roster
end #Module Jabber