xmpp4r 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. data/COPYING +340 -0
  2. data/ChangeLog +28 -0
  3. data/LICENSE +59 -0
  4. data/README +20 -0
  5. data/Rakefile +103 -0
  6. data/UPDATING +40 -0
  7. data/data/doc/xmpp4r/examples/advanced/adventure/README +57 -0
  8. data/data/doc/xmpp4r/examples/advanced/adventure/adventure.rb +23 -0
  9. data/data/doc/xmpp4r/examples/advanced/adventure/adventuremuc.rb +136 -0
  10. data/data/doc/xmpp4r/examples/advanced/adventure/cube.xml +15 -0
  11. data/data/doc/xmpp4r/examples/advanced/adventure/tower.xml +69 -0
  12. data/data/doc/xmpp4r/examples/advanced/adventure/world.rb +425 -0
  13. data/data/doc/xmpp4r/examples/advanced/fileserve.conf +11 -0
  14. data/data/doc/xmpp4r/examples/advanced/fileserve.rb +344 -0
  15. data/data/doc/xmpp4r/examples/advanced/getonline.rb +56 -0
  16. data/data/doc/xmpp4r/examples/advanced/gtkmucclient.rb +315 -0
  17. data/data/doc/xmpp4r/examples/advanced/migrate.rb +89 -0
  18. data/data/doc/xmpp4r/examples/advanced/minimuc.rb +266 -0
  19. data/data/doc/xmpp4r/examples/advanced/recvfile.rb +83 -0
  20. data/data/doc/xmpp4r/examples/advanced/rosterdiscovery.rb +130 -0
  21. data/data/doc/xmpp4r/examples/advanced/sendfile.conf +10 -0
  22. data/data/doc/xmpp4r/examples/advanced/sendfile.rb +72 -0
  23. data/data/doc/xmpp4r/examples/advanced/shellmgr/shellmgr.rb +51 -0
  24. data/data/doc/xmpp4r/examples/advanced/shellmgr/shellmgr_jabber.rb +43 -0
  25. data/data/doc/xmpp4r/examples/advanced/shellmgr/shellmgr_test.rb +10 -0
  26. data/data/doc/xmpp4r/examples/advanced/versionpoll.rb +90 -0
  27. data/data/doc/xmpp4r/examples/advanced/xmpping.rb +134 -0
  28. data/data/doc/xmpp4r/examples/advanced/xmppingrc.sample +9 -0
  29. data/data/doc/xmpp4r/examples/basic/change_password.rb +41 -0
  30. data/data/doc/xmpp4r/examples/basic/client.rb +68 -0
  31. data/data/doc/xmpp4r/examples/basic/component.rb +11 -0
  32. data/data/doc/xmpp4r/examples/basic/echo_nonthreaded.rb +32 -0
  33. data/data/doc/xmpp4r/examples/basic/echo_threaded.rb +32 -0
  34. data/data/doc/xmpp4r/examples/basic/jabbersend.rb +41 -0
  35. data/data/doc/xmpp4r/examples/basic/mass_sender.rb +67 -0
  36. data/data/doc/xmpp4r/examples/basic/mucinfo.rb +39 -0
  37. data/data/doc/xmpp4r/examples/basic/mucsimplebot.rb +83 -0
  38. data/data/doc/xmpp4r/examples/basic/register.rb +25 -0
  39. data/data/doc/xmpp4r/examples/basic/remove_registration.rb +18 -0
  40. data/data/doc/xmpp4r/examples/basic/roster.rb +42 -0
  41. data/data/doc/xmpp4r/examples/basic/rosterprint.rb +50 -0
  42. data/data/doc/xmpp4r/examples/basic/rosterrename.rb +34 -0
  43. data/data/doc/xmpp4r/examples/basic/rosterwatch.rb +172 -0
  44. data/data/doc/xmpp4r/examples/basic/send_vcard.rb +68 -0
  45. data/data/doc/xmpp4r/examples/basic/versionbot.rb +75 -0
  46. data/data/doc/xmpp4r/examples/buggy/jabber2jabber/jabber2jabber.rb +18 -0
  47. data/data/doc/xmpp4r/examples/buggy/miniedgarr_cgi.rb +192 -0
  48. data/data/doc/xmpp4r/examples/buggy/miniedgarr_watch.rb +82 -0
  49. data/lib/callbacks.rb +122 -0
  50. data/lib/xmpp4r/authenticationfailure.rb +13 -0
  51. data/lib/xmpp4r/bytestreams/helper/filetransfer.rb +315 -0
  52. data/lib/xmpp4r/bytestreams/helper/ibb/base.rb +256 -0
  53. data/lib/xmpp4r/bytestreams/helper/ibb/initiator.rb +30 -0
  54. data/lib/xmpp4r/bytestreams/helper/ibb/target.rb +46 -0
  55. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/base.rb +151 -0
  56. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/initiator.rb +85 -0
  57. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/server.rb +178 -0
  58. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/socks5.rb +56 -0
  59. data/lib/xmpp4r/bytestreams/helper/socks5bytestreams/target.rb +61 -0
  60. data/lib/xmpp4r/bytestreams/iq/bytestreams.rb +177 -0
  61. data/lib/xmpp4r/bytestreams/iq/si.rb +221 -0
  62. data/lib/xmpp4r/bytestreams.rb +11 -0
  63. data/lib/xmpp4r/client.rb +249 -0
  64. data/lib/xmpp4r/component.rb +103 -0
  65. data/lib/xmpp4r/connection.rb +166 -0
  66. data/lib/xmpp4r/dataforms/x/data.rb +248 -0
  67. data/lib/xmpp4r/dataforms.rb +1 -0
  68. data/lib/xmpp4r/debuglog.rb +34 -0
  69. data/lib/xmpp4r/delay/x/delay.rb +100 -0
  70. data/lib/xmpp4r/delay.rb +1 -0
  71. data/lib/xmpp4r/discovery/iq/discoinfo.rb +225 -0
  72. data/lib/xmpp4r/discovery/iq/discoitems.rb +162 -0
  73. data/lib/xmpp4r/discovery.rb +2 -0
  74. data/lib/xmpp4r/error.rb +230 -0
  75. data/lib/xmpp4r/errorexception.rb +32 -0
  76. data/lib/xmpp4r/feature_negotiation/iq/feature.rb +42 -0
  77. data/lib/xmpp4r/feature_negotiation.rb +1 -0
  78. data/lib/xmpp4r/idgenerator.rb +37 -0
  79. data/lib/xmpp4r/iq.rb +229 -0
  80. data/lib/xmpp4r/jid.rb +167 -0
  81. data/lib/xmpp4r/message.rb +171 -0
  82. data/lib/xmpp4r/muc/helper/mucbrowser.rb +107 -0
  83. data/lib/xmpp4r/muc/helper/mucclient.rb +382 -0
  84. data/lib/xmpp4r/muc/helper/simplemucclient.rb +222 -0
  85. data/lib/xmpp4r/muc/x/muc.rb +98 -0
  86. data/lib/xmpp4r/muc/x/mucuserinvite.rb +58 -0
  87. data/lib/xmpp4r/muc/x/mucuseritem.rb +148 -0
  88. data/lib/xmpp4r/muc.rb +6 -0
  89. data/lib/xmpp4r/presence.rb +255 -0
  90. data/lib/xmpp4r/query.rb +43 -0
  91. data/lib/xmpp4r/rexmladdons.rb +826 -0
  92. data/lib/xmpp4r/roster/helper/roster.rb +514 -0
  93. data/lib/xmpp4r/roster/iq/roster.rb +244 -0
  94. data/lib/xmpp4r/roster/x/roster.rb +155 -0
  95. data/lib/xmpp4r/roster.rb +4 -0
  96. data/lib/xmpp4r/sasl.rb +167 -0
  97. data/lib/xmpp4r/stream.rb +543 -0
  98. data/lib/xmpp4r/streamparser.rb +77 -0
  99. data/lib/xmpp4r/vcard/helper/vcard.rb +86 -0
  100. data/lib/xmpp4r/vcard/iq/vcard.rb +102 -0
  101. data/lib/xmpp4r/vcard.rb +3 -0
  102. data/lib/xmpp4r/version/helper/responder.rb +71 -0
  103. data/lib/xmpp4r/version/helper/simpleresponder.rb +44 -0
  104. data/lib/xmpp4r/version/iq/version.rb +118 -0
  105. data/lib/xmpp4r/version.rb +3 -0
  106. data/lib/xmpp4r/x.rb +43 -0
  107. data/lib/xmpp4r/xmlstanza.rb +174 -0
  108. data/lib/xmpp4r/xmpp4r.rb +16 -0
  109. data/lib/xmpp4r.rb +122 -0
  110. data/setup.rb +1360 -0
  111. data/test/bytestreams/tc_ibb.rb +186 -0
  112. data/test/bytestreams/tc_socks5bytestreams.rb +57 -0
  113. data/test/delay/tc_xdelay.rb +51 -0
  114. data/test/lib/clienttester.rb +110 -0
  115. data/test/muc/tc_muc_mucclient.rb +569 -0
  116. data/test/muc/tc_muc_simplemucclient.rb +72 -0
  117. data/test/roster/.tc_helper.rb.swp +0 -0
  118. data/test/roster/tc_helper.rb +389 -0
  119. data/test/roster/tc_iqqueryroster.rb +140 -0
  120. data/test/roster/tc_xroster.rb +70 -0
  121. data/test/tc_callbacks.rb +128 -0
  122. data/test/tc_class_names.rb +129 -0
  123. data/test/tc_client.rb +30 -0
  124. data/test/tc_error.rb +103 -0
  125. data/test/tc_idgenerator.rb +30 -0
  126. data/test/tc_iq.rb +109 -0
  127. data/test/tc_iqquery.rb +31 -0
  128. data/test/tc_jid.rb +202 -0
  129. data/test/tc_message.rb +114 -0
  130. data/test/tc_presence.rb +148 -0
  131. data/test/tc_stream.rb +182 -0
  132. data/test/tc_streamError.rb +87 -0
  133. data/test/tc_streamSend.rb +59 -0
  134. data/test/tc_streamThreaded.rb +168 -0
  135. data/test/tc_xmlstanza.rb +76 -0
  136. data/test/ts_xmpp4r.rb +34 -0
  137. data/test/vcard/tc_iqvcard.rb +52 -0
  138. data/test/version/tc_helper.rb +46 -0
  139. data/test/version/tc_iqqueryversion.rb +96 -0
  140. data/tools/doctoweb.bash +30 -0
  141. data/tools/gen_requires.bash +10 -0
  142. metadata +232 -0
@@ -0,0 +1,514 @@
1
+ # =XMPP4R - XMPP Library for Ruby
2
+ # License:: Ruby's license (see the LICENSE file) or GNU GPL, at your option.
3
+ # Website::http://home.gna.org/xmpp4r/
4
+
5
+ require 'callbacks'
6
+ require 'thread'
7
+ require 'xmpp4r/roster/iq/roster'
8
+
9
+ module Jabber
10
+ module Roster
11
+ ##
12
+ # The Roster helper intercepts <tt><iq/></tt> stanzas with Jabber::IqQueryRoster
13
+ # and <tt><presence/></tt> stanzas, but provides cbs which allow the programmer
14
+ # to keep track of updates.
15
+ class Helper
16
+ ##
17
+ # All items in your roster
18
+ # items:: [Hash] ([JID] => [Roster::Helper::RosterItem])
19
+ attr_reader :items
20
+
21
+ ##
22
+ # Initialize a new Roster helper
23
+ #
24
+ # Registers its cbs (prio = 120, ref = self)
25
+ #
26
+ # Request a roster
27
+ # (Remember to send initial presence afterwards!)
28
+ def initialize(stream)
29
+ @stream = stream
30
+ @items = {}
31
+ @items_lock = Mutex.new
32
+ @query_cbs = CallbackList.new
33
+ @update_cbs = CallbackList.new
34
+ @presence_cbs = CallbackList.new
35
+ @subscription_cbs = CallbackList.new
36
+ @subscription_request_cbs = CallbackList.new
37
+
38
+ # Register cbs
39
+ stream.add_iq_callback(120, self) { |iq|
40
+ handle_iq(iq)
41
+ }
42
+ stream.add_presence_callback(120, self) { |pres|
43
+ handle_presence(pres)
44
+ }
45
+
46
+ # Request the roster
47
+ rosterget = Iq.new_rosterget
48
+ stream.send(rosterget)
49
+ end
50
+
51
+ ##
52
+ # Add a callback to be called when a query has been processed
53
+ #
54
+ # Because update callbacks are called for each roster item,
55
+ # this may be appropriate to notify that *anything* has updated.
56
+ #
57
+ # Arguments for callback block: The received <tt><iq/></tt> stanza
58
+ def add_query_callback(prio = 0, ref = nil, &block)
59
+ @query_cbs.add(prio, ref, block)
60
+ end
61
+
62
+ ##
63
+ # Add a callback for Jabber::Roster::Helper::RosterItem updates
64
+ #
65
+ # Note that this will be called much after initialization
66
+ # for the answer of the initial roster request
67
+ #
68
+ # The block receives two objects:
69
+ # * the old Jabber::Roster::Helper::RosterItem
70
+ # * the new Jabber::Roster::Helper::RosterItem
71
+ def add_update_callback(prio = 0, ref = nil, &block)
72
+ @update_cbs.add(prio, ref, block)
73
+ end
74
+
75
+ ##
76
+ # Add a callback for Jabber::Presence updates
77
+ #
78
+ # This will be called for <tt><presence/></tt> stanzas for known RosterItems.
79
+ # Unknown JIDs may still pass and can be caught via Jabber::Stream#add_presence_callback.
80
+ #
81
+ # The block receives three objects:
82
+ # * the Jabber::Roster::Helper::RosterItem
83
+ # * the old Jabber::Presence (or nil)
84
+ # * the new Jabber::Presence (or nil)
85
+ def add_presence_callback(prio = 0, ref = nil, &block)
86
+ @presence_cbs.add(prio, ref, block)
87
+ end
88
+
89
+ ##
90
+ # Add a callback for subscription updates,
91
+ # which will be called upon receiving a <tt><presence/></tt> stanza
92
+ # with type:
93
+ # * :subscribed
94
+ # * :unsubscribe
95
+ # * :unsubscribed
96
+ #
97
+ # The block receives two objects:
98
+ # * the Jabber::Roster::Helper::RosterItem (or nil)
99
+ # * the <tt><presence/></tt> stanza
100
+ def add_subscription_callback(prio = 0, ref = nil, &block)
101
+ @subscription_cbs.add(prio, ref, block)
102
+ end
103
+
104
+ ##
105
+ # Add a callback for subscription requests,
106
+ # which will be called upon receiving a <tt><presence type='subscribe'/></tt> stanza
107
+ #
108
+ # The block receives two objects:
109
+ # * the Jabber::Roster::Helper::RosterItem (or nil)
110
+ # * the <tt><presence/></tt> stanza
111
+ #
112
+ # Response to this event can be taken with accept_subscription
113
+ # and decline_subscription.
114
+ #
115
+ # Example usage:
116
+ # my_roster.add_subscription_request_callback do |item,presence|
117
+ # if accept_subscription_requests
118
+ # my_roster.accept_subscription(presence.from)
119
+ # else
120
+ # my_roster.decline_subscription(presence.from)
121
+ # end
122
+ # end
123
+ def add_subscription_request_callback(prio = 0, ref = nil, &block)
124
+ @subscription_request_cbs.add(prio, ref, block)
125
+ end
126
+
127
+ private
128
+
129
+ ##
130
+ # Handle received <tt><iq/></tt> stanzas,
131
+ # used internally
132
+ def handle_iq(iq)
133
+ if iq.query.kind_of?(IqQueryRoster)
134
+ # If the <iq/> contains <error/> we just ignore that
135
+ # and assume an empty roster
136
+ iq.query.each_element('item') do |item|
137
+ # Handle deletion of item
138
+ if item.subscription == :remove
139
+ @items_lock.synchronize {
140
+ @items.delete(item.jid)
141
+ }
142
+
143
+ else
144
+ olditem = nil
145
+ @items_lock.synchronize {
146
+ if @items.has_key?(item.jid)
147
+ olditem = RosterItem.new(@stream).import(@items[item.jid])
148
+
149
+ # Clear first, because import doesn't
150
+ @items[item.jid].iname = nil
151
+ @items[item.jid].subscription = nil
152
+ @items[item.jid].ask = nil
153
+
154
+ @items[item.jid].import(item)
155
+ else
156
+ @items[item.jid] = RosterItem.new(@stream).import(item)
157
+ end
158
+ }
159
+ @update_cbs.process(olditem, @items[item.jid])
160
+ end
161
+ end
162
+
163
+ @query_cbs.process(iq)
164
+ else
165
+ false
166
+ end
167
+ end
168
+
169
+ ##
170
+ # Handle received <tt><presence/></tt> stanzas,
171
+ # used internally
172
+ def handle_presence(pres)
173
+ item = self[pres.from]
174
+ if [:subscribed, :unsubscribe, :unsubscribed].include?(pres.type)
175
+ @subscription_cbs.process(item, pres)
176
+ true
177
+ elsif pres.type == :subscribe
178
+ @subscription_request_cbs.process(item, pres)
179
+ true
180
+ else
181
+ unless item.nil?
182
+ update_presence(item, pres)
183
+ true # Callback consumed stanza
184
+ else
185
+ false # Callback did not consume stanza
186
+ end
187
+ end
188
+ end
189
+
190
+ ##
191
+ # Update the presence of an item,
192
+ # used internally
193
+ #
194
+ # Callbacks are called here
195
+ def update_presence(item, pres)
196
+
197
+ # This requires special handling, to announce all resources offline
198
+ if pres.from.resource.nil? and pres.type == :error
199
+ oldpresences = []
200
+ item.each_presence do |oldpres|
201
+ oldpresences << oldpres
202
+ end
203
+
204
+ item.add_presence(pres)
205
+ oldpresences.each { |oldpres|
206
+ @presence_cbs.process(item, oldpres, pres)
207
+ }
208
+ else
209
+ oldpres = item.presence(pres.from).nil? ?
210
+ nil :
211
+ Presence.new.import(item.presence(pres.from))
212
+
213
+ item.add_presence(pres)
214
+ @presence_cbs.process(item, oldpres, pres)
215
+ end
216
+ end
217
+
218
+ public
219
+
220
+ ##
221
+ # Get an item by jid
222
+ #
223
+ # If not available tries to look for it with the resource stripped
224
+ def [](jid)
225
+ jid = JID.new(jid) unless jid.kind_of? JID
226
+
227
+ @items_lock.synchronize {
228
+ if @items.has_key?(jid)
229
+ @items[jid]
230
+ elsif @items.has_key?(jid.strip)
231
+ @items[jid.strip]
232
+ else
233
+ nil
234
+ end
235
+ }
236
+ end
237
+
238
+ ##
239
+ # Returns the list of RosterItems which, stripped, are equal to the
240
+ # one you are looking for.
241
+ def find(jid)
242
+ jid = JID.new(jid) unless jid.kind_of? JID
243
+
244
+ j = jid.strip
245
+ l = {}
246
+ @items_lock.synchronize {
247
+ @items.each_pair do |k, v|
248
+ l[k] = v if k.strip == j
249
+ end
250
+ }
251
+ l
252
+ end
253
+
254
+ ##
255
+ # Groups in this Roster,
256
+ # sorted by name
257
+ #
258
+ # Contains +nil+ if there are ungrouped items
259
+ # result:: [Array] containing group names (String)
260
+ def groups
261
+ res = []
262
+ @items_lock.synchronize {
263
+ @items.each_pair do |jid,item|
264
+ res += item.groups
265
+ res += [nil] if item.groups == []
266
+ end
267
+ }
268
+ res.uniq.sort { |a,b| a.to_s <=> b.to_s }
269
+ end
270
+
271
+ ##
272
+ # Get items in a group
273
+ #
274
+ # When group is nil, return ungrouped items
275
+ # group:: [String] Group name
276
+ # result:: Array of [RosterItem]
277
+ def find_by_group(group)
278
+ res = []
279
+ @items_lock.synchronize {
280
+ @items.each_pair do |jid,item|
281
+ res.push(item) if item.groups.include?(group)
282
+ res.push(item) if item.groups == [] and group.nil?
283
+ end
284
+ }
285
+ res
286
+ end
287
+
288
+ ##
289
+ # Add a user to your roster
290
+ #
291
+ # Threading is encouraged as the function waits for
292
+ # a result. ErrorException is thrown upon error.
293
+ #
294
+ # See Jabber::Roster::Helper::RosterItem#subscribe for details
295
+ # about subscribing. (This method isn't used here but the
296
+ # same functionality applies.)
297
+ #
298
+ # If the item is already in the local roster
299
+ # it will simply send itself
300
+ # jid:: [JID] to add
301
+ # iname:: [String] Optional item name
302
+ # subscribe:: [Boolean] Whether to subscribe to this jid
303
+ def add(jid, iname=nil, subscribe=false)
304
+ if self[jid]
305
+ self[jid].send
306
+ else
307
+ request = Iq.new_rosterset
308
+ request.query.add(Jabber::Roster::RosterItem.new(jid, iname))
309
+ @stream.send_with_id(request) { true }
310
+ # Adding to list is handled by handle_iq
311
+ end
312
+
313
+ if subscribe
314
+ # Actually the item *should* already be known now,
315
+ # but we do it manually to exclude conditions.
316
+ pres = Presence.new.set_type(:subscribe).set_to(jid.strip)
317
+ @stream.send(pres)
318
+ end
319
+ end
320
+
321
+ ##
322
+ # Accept a subscription request
323
+ # * Sends a <presence type='subscribed'/> stanza
324
+ # * Adds the contact to your roster
325
+ # jid:: [JID] of contact
326
+ # iname:: [String] Optional roster item name
327
+ def accept_subscription(jid, iname=nil)
328
+ pres = Presence.new.set_type(:subscribed).set_to(jid.strip)
329
+ @stream.send(pres)
330
+
331
+ unless self[jid.strip]
332
+ request = Iq.new_rosterset
333
+ request.query.add(Jabber::Roster::RosterItem.new(jid.strip, iname))
334
+ @stream.send_with_id(request) { true }
335
+ end
336
+ end
337
+
338
+ ##
339
+ # Decline a subscription request
340
+ # * Sends a <presence type='unsubscribed'/> stanza
341
+ def decline_subscription(jid)
342
+ pres = Presence.new.set_type(:unsubscribed).set_to(jid.strip)
343
+ @stream.send(pres)
344
+ end
345
+
346
+ ##
347
+ # These are extensions to RosterItem to carry presence information.
348
+ # This information is *not* stored in XML!
349
+ class RosterItem < Jabber::Roster::RosterItem
350
+ ##
351
+ # Tracked (online) presences of this RosterItem
352
+ attr_reader :presences
353
+
354
+ ##
355
+ # Initialize an empty RosterItem
356
+ def initialize(stream)
357
+ super()
358
+ @stream = stream
359
+ @presences = []
360
+ @presences_lock = Mutex.new
361
+ end
362
+
363
+ ##
364
+ # Import another element,
365
+ # also import presences if xe is a RosterItem
366
+ # return:: [RosterItem] self
367
+ def import(xe)
368
+ super
369
+ if xe.kind_of?(RosterItem)
370
+ xe.each_presence { |pres|
371
+ add_presence(Presence.new.import(pres))
372
+ }
373
+ end
374
+ self
375
+ end
376
+
377
+ ##
378
+ # Send the updated RosterItem to the server,
379
+ # i.e. if you modified iname, groups, ...
380
+ def send
381
+ request = Iq.new_rosterset
382
+ request.query.add(self)
383
+ @stream.send(request)
384
+ end
385
+
386
+ ##
387
+ # Remove item
388
+ #
389
+ # This cancels both subscription *from* the contact to you
390
+ # and from you *to* the contact.
391
+ #
392
+ # The methods waits for a roster push from the server (success)
393
+ # or throws ErrorException upon failure.
394
+ def remove
395
+ request = Iq.new_rosterset
396
+ request.query.add(Jabber::Roster::RosterItem.new(jid, nil, :remove))
397
+ @stream.send_with_id(request) { true }
398
+ # Removing from list is handled by Roster#handle_iq
399
+ end
400
+
401
+ ##
402
+ # Is any presence of this person on-line?
403
+ #
404
+ # (Or is there any presence? Unavailable presences are
405
+ # deleted.)
406
+ def online?
407
+ @presences_lock.synchronize {
408
+ @presences.select { |pres|
409
+ pres.type.nil?
410
+ }.size > 0
411
+ }
412
+ end
413
+
414
+ ##
415
+ # Iterate through all received <tt><presence/></tt> stanzas
416
+ def each_presence(&block)
417
+ # Don't lock here, we don't know what block does...
418
+ @presences.each { |pres|
419
+ yield(pres)
420
+ }
421
+ end
422
+
423
+ ##
424
+ # Get specific presence
425
+ # jid:: [JID] Full JID
426
+ def presence(jid)
427
+ @presences_lock.synchronize {
428
+ @presences.each { |pres|
429
+ return(pres) if pres.from == jid
430
+ }
431
+ }
432
+ nil
433
+ end
434
+
435
+ ##
436
+ # Add presence and sort presences
437
+ # (unless type is :unavailable or :error)
438
+ #
439
+ # This overwrites previous stanzas with the same destination
440
+ # JID to keep track of resources. Presence stanzas with
441
+ # <tt>type == :unavailable</tt> or <tt>type == :error</tt> will
442
+ # be deleted as this indicates that this resource has gone
443
+ # offline.
444
+ #
445
+ # If <tt>type == :error</tt> and the presence's origin has no
446
+ # specific resource the contact is treated completely offline.
447
+ def add_presence(newpres)
448
+ @presences_lock.synchronize {
449
+ # Delete old presences with the same JID
450
+ @presences.delete_if do |pres|
451
+ pres.from == newpres.from or pres.from.resource.nil?
452
+ end
453
+
454
+ if newpres.type == :error and newpres.from.resource.nil?
455
+ # Replace by single error presence
456
+ @presences = [newpres]
457
+ else
458
+ # Add new presence
459
+ @presences.push(newpres)
460
+ end
461
+
462
+ @presences.sort!
463
+ }
464
+ end
465
+
466
+ ##
467
+ # Send subscription request to the user
468
+ #
469
+ # The block given to Jabber::Roster::Roster#add_update_callback will
470
+ # be called, carrying the RosterItem with ask="subscribe"
471
+ #
472
+ # This function returns immediately after sending the subscription
473
+ # request and will not wait of approval or declination as it may
474
+ # take months for the contact to decide. ;-)
475
+ def subscribe
476
+ pres = Presence.new.set_type(:subscribe).set_to(jid.strip)
477
+ @stream.send(pres)
478
+ end
479
+
480
+ ##
481
+ # Unsubscribe from a contact's presence
482
+ #
483
+ # This method waits for a presence with type='unsubscribed'
484
+ # from the contact. It may throw ErrorException upon failure.
485
+ #
486
+ # subscription attribute of the item is *from* or *none*
487
+ # afterwards. As long as you don't remove that item and
488
+ # subscription='from' the contact is subscribed to your
489
+ # presence.
490
+ def unsubscribe
491
+ pres = Presence.new.set_type(:unsubscribe).set_to(jid.strip)
492
+ @stream.send(pres) { |answer|
493
+ answer.type == :unsubscribed and
494
+ answer.from.strip == pres.to
495
+ }
496
+ end
497
+
498
+ ##
499
+ # Deny the contact to see your presence.
500
+ #
501
+ # This method will not wait and returns immediately
502
+ # as you will need no confirmation for this action.
503
+ #
504
+ # Though, you will get a roster update for that item,
505
+ # carrying either subscription='to' or 'none'.
506
+ def cancel_subscription
507
+ pres = Presence.new.set_type(:unsubscribed).set_to(jid)
508
+ @stream.send(pres)
509
+ end
510
+ end
511
+ end #Class Roster
512
+ end #Module Roster
513
+ end #Module Jabber
514
+