pyu-xmpp4r-simple 0.8.8
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.
- data/CHANGELOG +73 -0
- data/COPYING +281 -0
- data/README +96 -0
- data/lib/xmpp4r-simple.rb +819 -0
- data/test/test_xmpp4r_simple.rb +313 -0
- metadata +87 -0
@@ -0,0 +1,819 @@
|
|
1
|
+
# Jabber::Simple - An extremely easy-to-use Jabber client library.
|
2
|
+
# Copyright 2006 Blaine Cook <blaine@obvious.com>, Obvious Corp.
|
3
|
+
#
|
4
|
+
# Jabber::Simple is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# Jabber::Simple is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with Jabber::Simple; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
17
|
+
|
18
|
+
require 'time'
|
19
|
+
require 'rubygems'
|
20
|
+
require 'xmpp4r'
|
21
|
+
require 'xmpp4r/roster'
|
22
|
+
require 'xmpp4r/vcard'
|
23
|
+
require 'xmpp4r/pubsub'
|
24
|
+
require 'xmpp4r/pubsub/helper/servicehelper'
|
25
|
+
require 'xmpp4r/pubsub/helper/nodebrowser'
|
26
|
+
require 'xmpp4r/pubsub/helper/nodehelper'
|
27
|
+
|
28
|
+
module Jabber
|
29
|
+
|
30
|
+
class ConnectionError < StandardError #:nodoc:
|
31
|
+
end
|
32
|
+
|
33
|
+
class NotConnected < StandardError #:nodoc:
|
34
|
+
end
|
35
|
+
|
36
|
+
class NoPubSubService < StandardError #:nodoc:
|
37
|
+
end
|
38
|
+
|
39
|
+
class AlreadySet < StandardError #:nodoc:
|
40
|
+
end
|
41
|
+
|
42
|
+
class RegistrationError < StandardError #:nodoc:
|
43
|
+
end
|
44
|
+
|
45
|
+
module Roster
|
46
|
+
class RosterItem
|
47
|
+
def chat_state
|
48
|
+
@chat_state
|
49
|
+
end
|
50
|
+
|
51
|
+
def chat_state=(state)
|
52
|
+
@chat_state = state
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
|
59
|
+
class Contact #:nodoc:
|
60
|
+
|
61
|
+
include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
|
62
|
+
|
63
|
+
def initialize(client, jid)
|
64
|
+
@jid = jid.respond_to?(:resource) ? jid : JID.new(jid)
|
65
|
+
@client = client
|
66
|
+
end
|
67
|
+
|
68
|
+
def inspect
|
69
|
+
"Jabber::Contact #{jid.to_s}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def subscribed?
|
73
|
+
[:to, :both].include?(subscription)
|
74
|
+
end
|
75
|
+
|
76
|
+
def subscription
|
77
|
+
roster_item && roster_item.subscription
|
78
|
+
end
|
79
|
+
|
80
|
+
def ask_for_authorization!
|
81
|
+
subscription_request = Presence.new.set_type(:subscribe)
|
82
|
+
subscription_request.to = jid
|
83
|
+
client.send!(subscription_request)
|
84
|
+
end
|
85
|
+
|
86
|
+
def unsubscribe!
|
87
|
+
unsubscription_request = Presence.new.set_type(:unsubscribe)
|
88
|
+
unsubscription_request.to = jid
|
89
|
+
client.send!(unsubscription_request)
|
90
|
+
client.send!(unsubscription_request.set_type(:unsubscribed))
|
91
|
+
end
|
92
|
+
|
93
|
+
def jid(bare=true)
|
94
|
+
bare ? @jid.strip : @jid
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def roster_item
|
100
|
+
client.roster.items[jid]
|
101
|
+
end
|
102
|
+
|
103
|
+
def client
|
104
|
+
@client
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Simple
|
109
|
+
|
110
|
+
include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
|
111
|
+
|
112
|
+
def self.register(jid, password, status = nil, status_message = "Available")
|
113
|
+
new(jid, password, status, status_message, nil, 5222, nil, true)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Create a new Jabber::Simple client. You will be automatically connected
|
117
|
+
# to the Jabber server and your status message will be set to the string
|
118
|
+
# passed in as the status_message argument.
|
119
|
+
# If you'd like to connect to a different talk server than the one which would
|
120
|
+
# be guessed from your jid, you may provide a server. For example, to connect
|
121
|
+
# to the gmail talk servers with a jid that doesn't end in @gmail.com, just provide
|
122
|
+
# 'talk.l.google.com' as the server. You may leave server as nil to use the default.
|
123
|
+
#
|
124
|
+
# jabber = Jabber::Simple.new("me@example.com", "password", "Chat with me - Please!")
|
125
|
+
def initialize(jid, password, status = nil, status_message = "Available", host = nil, port = 5222, server=nil, register = false)
|
126
|
+
@jid = jid
|
127
|
+
@password = password
|
128
|
+
@host = host
|
129
|
+
@port = port
|
130
|
+
@disconnected = false
|
131
|
+
@server = server
|
132
|
+
register!(password) if @register = register
|
133
|
+
status(status, status_message)
|
134
|
+
start_deferred_delivery_thread
|
135
|
+
|
136
|
+
@pubsub = @pubsub_jid = nil
|
137
|
+
begin
|
138
|
+
domain = Jabber::JID.new(@jid).domain
|
139
|
+
@pubsub_jid = "pubsub." + domain
|
140
|
+
set_pubsub_service(@pubsub_jid)
|
141
|
+
rescue
|
142
|
+
@pubsub = @pubsub_jid = nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def inspect #:nodoc:
|
147
|
+
"Jabber::Simple #{@jid}"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Send a message to jabber user jid.
|
151
|
+
# It is possible to send message to multiple users at once,
|
152
|
+
# just supply array of jids.
|
153
|
+
#
|
154
|
+
# Example usage:
|
155
|
+
#
|
156
|
+
# jabber_simple.deliver("foo@example.com", "blabla")
|
157
|
+
# jabber_simple.deliver(["foo@example.com", "bar@example.com"], "blabla")
|
158
|
+
#
|
159
|
+
# Valid message types are:
|
160
|
+
#
|
161
|
+
# * :normal (default): a normal message.
|
162
|
+
# * :chat: a one-to-one chat message.
|
163
|
+
# * :groupchat: a group-chat message.
|
164
|
+
# * :headline: a "headline" message.
|
165
|
+
# * :error: an error message.
|
166
|
+
#
|
167
|
+
# If the recipient is not in your contacts list, the message will be queued
|
168
|
+
# for later delivery, and the Contact will be automatically asked for
|
169
|
+
# authorization (see Jabber::Simple#add).
|
170
|
+
#
|
171
|
+
# message should be a string or a valid Jabber::Message object. In either case,
|
172
|
+
# the message recipient will be set to jid.
|
173
|
+
def deliver(jids, message, type = :chat, chat_state = :active)
|
174
|
+
contacts(jids) do |friend|
|
175
|
+
unless subscribed_to? friend
|
176
|
+
add(friend.jid)
|
177
|
+
deliver_deferred(friend.jid, message, type)
|
178
|
+
next
|
179
|
+
end
|
180
|
+
if message.kind_of?(Jabber::Message)
|
181
|
+
msg = message
|
182
|
+
msg.to = friend.jid
|
183
|
+
else
|
184
|
+
msg = Message.new(friend.jid)
|
185
|
+
msg.type = type
|
186
|
+
msg.chat_state = chat_state
|
187
|
+
msg.body = message
|
188
|
+
end
|
189
|
+
send!(msg)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Set your presence, with a message.
|
194
|
+
#
|
195
|
+
# Available values for presence are:
|
196
|
+
#
|
197
|
+
# * nil: online.
|
198
|
+
# * :chat: free for chat.
|
199
|
+
# * :away: away from the computer.
|
200
|
+
# * :dnd: do not disturb.
|
201
|
+
# * :xa: extended away.
|
202
|
+
#
|
203
|
+
# It's not possible to set an offline status - to do that, disconnect! :-)
|
204
|
+
def status(presence, message)
|
205
|
+
@presence = presence
|
206
|
+
@status_message = message
|
207
|
+
stat_msg = Presence.new(@presence, @status_message)
|
208
|
+
|
209
|
+
if !avatar_hash.nil?
|
210
|
+
x = Jabber::X.new
|
211
|
+
x.add_namespace('vcard-temp:x:update')
|
212
|
+
photo = REXML::Element::new("photo")
|
213
|
+
photo.add(REXML::Text.new(avatar_hash))
|
214
|
+
x.add(photo)
|
215
|
+
stat_msg.add_element(x)
|
216
|
+
end
|
217
|
+
|
218
|
+
send!(stat_msg)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Ask the users specified by jids for authorization (i.e., ask them to add
|
222
|
+
# you to their contact list). If you are already in the user's contact list,
|
223
|
+
# add() will not attempt to re-request authorization. In order to force
|
224
|
+
# re-authorization, first remove() the user, then re-add them.
|
225
|
+
#
|
226
|
+
# Example usage:
|
227
|
+
#
|
228
|
+
# jabber_simple.add("friend@friendosaurus.com")
|
229
|
+
#
|
230
|
+
# Because the authorization process might take a few seconds, or might
|
231
|
+
# never happen depending on when (and if) the user accepts your
|
232
|
+
# request, results are placed in the Jabber::Simple#new_subscriptions queue.
|
233
|
+
def add(*jids)
|
234
|
+
contacts(*jids) do |friend|
|
235
|
+
next if subscribed_to? friend
|
236
|
+
friend.ask_for_authorization!
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Remove the jabber users specified by jids from the contact list.
|
241
|
+
def remove(*jids)
|
242
|
+
contacts(*jids) do |unfriend|
|
243
|
+
unfriend.unsubscribe!
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns true if this Jabber account is subscribed to status updates for
|
248
|
+
# the jabber user jid, false otherwise.
|
249
|
+
def subscribed_to?(jid)
|
250
|
+
contacts(jid) do |contact|
|
251
|
+
return contact.subscribed?
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns array of roster items.
|
256
|
+
def contact_list
|
257
|
+
roster.items.values
|
258
|
+
end
|
259
|
+
|
260
|
+
# Retrieve vCard of an entity
|
261
|
+
#
|
262
|
+
# Raises exception upon retrieval error, please catch that!
|
263
|
+
#
|
264
|
+
# Usage of Threads is suggested here as vCards can be very
|
265
|
+
# big.
|
266
|
+
#
|
267
|
+
# jid:: [Jabber::JID] or nil (should be stripped, nil for the client's own vCard)
|
268
|
+
# result:: [Jabber::IqVcard] or nil (nil results may be handled as empty vCards)
|
269
|
+
def get_info(jid = nil)
|
270
|
+
Jabber::Vcard::Helper.new(client).get(jid)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Update your own vCard
|
274
|
+
#
|
275
|
+
# Raises exception when setting fails
|
276
|
+
#
|
277
|
+
# Usage of Threads suggested here, too. The function
|
278
|
+
# waits for approval from the server.
|
279
|
+
# e.g.:
|
280
|
+
#
|
281
|
+
# info = Jabber::Vcard::IqVcard.new
|
282
|
+
# info["NICKNAME"] = "amay"
|
283
|
+
# jabber.update_info(info)
|
284
|
+
def update_info(vcard)
|
285
|
+
Jabber::Vcard::Helper.new(client).set(vcard)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns SHA1 hash of the avatar image data.
|
289
|
+
#
|
290
|
+
# This hash is then included in the user's presence information.
|
291
|
+
def avatar_hash
|
292
|
+
vcard = get_info
|
293
|
+
if !vcard.nil? && !vcard["PHOTO/BINVAL"].nil?
|
294
|
+
Digest::SHA1.hexdigest(Base64.decode64(vcard["PHOTO/BINVAL"]))
|
295
|
+
else
|
296
|
+
nil
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Returns true if the Jabber client is connected to the Jabber server,
|
301
|
+
# false otherwise.
|
302
|
+
def connected?
|
303
|
+
@client ||= nil
|
304
|
+
connected = @client.respond_to?(:is_connected?) && @client.is_connected?
|
305
|
+
return connected
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns an array of messages received since the last time
|
309
|
+
# received_messages was called. Passing a block will yield each message in
|
310
|
+
# turn, allowing you to break part-way through processing (especially
|
311
|
+
# useful when your message handling code is not thread-safe (e.g.,
|
312
|
+
# ActiveRecord).
|
313
|
+
#
|
314
|
+
# e.g.:
|
315
|
+
#
|
316
|
+
# jabber.received_messages do |message|
|
317
|
+
# puts "Received message from #{message.from}: #{message.body}"
|
318
|
+
# end
|
319
|
+
def received_messages(&block)
|
320
|
+
dequeue(:received_messages, &block)
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns true if there are unprocessed received messages waiting in the
|
324
|
+
# queue, false otherwise.
|
325
|
+
def received_messages?
|
326
|
+
!queue(:received_messages).empty?
|
327
|
+
end
|
328
|
+
|
329
|
+
# Returns an array of iq stanzas received since the last time
|
330
|
+
# iq_stanzas was called. There will be no stanzas in this queue
|
331
|
+
# unless you have enabled capture of <iq/> stanzas using the
|
332
|
+
# capture_iq_stanzas! method. Passing a block will yield each stanza in
|
333
|
+
# turn, allowing you to break part-way through processing (especially
|
334
|
+
# useful when your message handling code is not thread-safe (e.g.,
|
335
|
+
# ActiveRecord).
|
336
|
+
#
|
337
|
+
# e.g.:
|
338
|
+
#
|
339
|
+
# jabber.capture_iq_stanzas!
|
340
|
+
# jabber.iq_stanzas do |iq|
|
341
|
+
# puts "Received iq stanza from #{iq.from}"
|
342
|
+
# end
|
343
|
+
def iq_stanza(&block)
|
344
|
+
dequeue(:iq_stanzas, &block)
|
345
|
+
end
|
346
|
+
|
347
|
+
# Returns true if there are unprocessed iq stanzas waiting in the
|
348
|
+
# queue, false otherwise.
|
349
|
+
def iq_stanzas?
|
350
|
+
!queue(:iq_stanzas).empty?
|
351
|
+
end
|
352
|
+
|
353
|
+
# Tell the client to capture (or stop capturing) <iq/> stanzas
|
354
|
+
# addressed to it. When the client is initially started, this
|
355
|
+
# is false.
|
356
|
+
def capture_iq_stanzas!(capture=true)
|
357
|
+
@iq_mutex.synchronize { @capture_iq_stanzas = capture }
|
358
|
+
end
|
359
|
+
|
360
|
+
# Returns true if iq stanzas will be captured in the queue, as
|
361
|
+
# they arrive, false otherwise.
|
362
|
+
def capture_iq_stanzas?
|
363
|
+
@capture_iq_stanzas
|
364
|
+
end
|
365
|
+
|
366
|
+
# Returns an array of presence updates received since the last time
|
367
|
+
# presence_updates was called. Passing a block will yield each update in
|
368
|
+
# turn, allowing you to break part-way through processing (especially
|
369
|
+
# useful when your presence handling code is not thread-safe (e.g.,
|
370
|
+
# ActiveRecord).
|
371
|
+
#
|
372
|
+
# e.g.:
|
373
|
+
#
|
374
|
+
# jabber.presence_updates do |friend, new_presence|
|
375
|
+
# puts "Received presence update from #{friend}: #{new_presence}"
|
376
|
+
# end
|
377
|
+
def presence_updates(&block)
|
378
|
+
updates = []
|
379
|
+
@presence_mutex.synchronize do
|
380
|
+
dequeue(:presence_updates) do |friend|
|
381
|
+
presence = @presence_updates[friend]
|
382
|
+
next unless presence
|
383
|
+
new_update = [friend, presence[0], presence[1]]
|
384
|
+
yield new_update if block_given?
|
385
|
+
updates << new_update
|
386
|
+
@presence_updates.delete(friend)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
return updates
|
390
|
+
end
|
391
|
+
|
392
|
+
# Returns true if there are unprocessed presence updates waiting in the
|
393
|
+
# queue, false otherwise.
|
394
|
+
def presence_updates?
|
395
|
+
!queue(:presence_updates).empty?
|
396
|
+
end
|
397
|
+
|
398
|
+
# Returns an array of subscription notifications received since the last
|
399
|
+
# time new_subscriptions was called. Passing a block will yield each update
|
400
|
+
# in turn, allowing you to break part-way through processing (especially
|
401
|
+
# useful when your subscription handling code is not thread-safe (e.g.,
|
402
|
+
# ActiveRecord).
|
403
|
+
#
|
404
|
+
# e.g.:
|
405
|
+
#
|
406
|
+
# jabber.new_subscriptions do |friend, presence|
|
407
|
+
# puts "Received presence update from #{friend.to_s}: #{presence}"
|
408
|
+
# end
|
409
|
+
def new_subscriptions(&block)
|
410
|
+
dequeue(:new_subscriptions, &block)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Returns true if there are unprocessed presence updates waiting in the
|
414
|
+
# queue, false otherwise.
|
415
|
+
def new_subscriptions?
|
416
|
+
!queue(:new_subscriptions).empty?
|
417
|
+
end
|
418
|
+
|
419
|
+
# Returns an array of subscription notifications received since the last
|
420
|
+
# time subscription_requests was called. Passing a block will yield each update
|
421
|
+
# in turn, allowing you to break part-way through processing (especially
|
422
|
+
# useful when your subscription handling code is not thread-safe (e.g.,
|
423
|
+
# ActiveRecord).
|
424
|
+
#
|
425
|
+
# e.g.:
|
426
|
+
#
|
427
|
+
# jabber.subscription_requests do |friend, presence|
|
428
|
+
# puts "Received presence update from #{friend.to_s}: #{presence}"
|
429
|
+
# end
|
430
|
+
def subscription_requests(&block)
|
431
|
+
dequeue(:subscription_requests, &block)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Returns true if auto-accept subscriptions (friend requests) is enabled
|
435
|
+
# (default), false otherwise.
|
436
|
+
def accept_subscriptions?
|
437
|
+
@accept_subscriptions = true if @accept_subscriptions.nil?
|
438
|
+
@accept_subscriptions
|
439
|
+
end
|
440
|
+
|
441
|
+
# Change whether or not subscriptions (friend requests) are automatically accepted.
|
442
|
+
def accept_subscriptions=(accept_status)
|
443
|
+
@accept_subscriptions = accept_status
|
444
|
+
end
|
445
|
+
|
446
|
+
# Direct access to the underlying Roster helper.
|
447
|
+
def roster
|
448
|
+
return @roster if @roster
|
449
|
+
self.roster = Roster::Helper.new(client)
|
450
|
+
end
|
451
|
+
|
452
|
+
# Direct access to the underlying Jabber client.
|
453
|
+
def client
|
454
|
+
connect!() unless connected?
|
455
|
+
@client
|
456
|
+
end
|
457
|
+
def register!(password)
|
458
|
+
attempt! {
|
459
|
+
begin
|
460
|
+
client.register(password)
|
461
|
+
@register = false
|
462
|
+
disconnect
|
463
|
+
reconnect
|
464
|
+
rescue Exception => e
|
465
|
+
error_msg = "Error registering: #{e.message}\n\n"
|
466
|
+
if e.respond_to?('error') && e.error.type == :modify
|
467
|
+
error_msg += "Accepted registration information:\n"
|
468
|
+
instructions, fields = client.register_info
|
469
|
+
fields.each { |info|
|
470
|
+
error_msg += "* #{info}\n"
|
471
|
+
}
|
472
|
+
error_msg += "(#{instructions})"
|
473
|
+
end
|
474
|
+
raise RegistrationError, error_msg
|
475
|
+
end
|
476
|
+
}
|
477
|
+
end
|
478
|
+
# Send a Jabber stanza over-the-wire.
|
479
|
+
def send!(msg)
|
480
|
+
attempt! {
|
481
|
+
client.send(msg)
|
482
|
+
}
|
483
|
+
end
|
484
|
+
def attempt!
|
485
|
+
attempts = 0
|
486
|
+
begin
|
487
|
+
attempts += 1
|
488
|
+
yield
|
489
|
+
rescue Errno::EPIPE, IOError => e
|
490
|
+
sleep 1
|
491
|
+
disconnect
|
492
|
+
reconnect
|
493
|
+
retry unless attempts > 3
|
494
|
+
raise e
|
495
|
+
rescue Errno::ECONNRESET => e
|
496
|
+
sleep (attempts^2) * 60 + 60
|
497
|
+
disconnect
|
498
|
+
reconnect
|
499
|
+
retry unless attempts > 3
|
500
|
+
raise e
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
# Use this to force the client to reconnect after a force_disconnect.
|
505
|
+
def reconnect
|
506
|
+
@disconnected = false
|
507
|
+
connect!
|
508
|
+
end
|
509
|
+
|
510
|
+
# Use this to force the client to disconnect and not automatically
|
511
|
+
# reconnect.
|
512
|
+
def disconnect
|
513
|
+
disconnect!
|
514
|
+
end
|
515
|
+
|
516
|
+
# Queue messages for delivery once a user has accepted our authorization
|
517
|
+
# request. Works in conjunction with the deferred delivery thread.
|
518
|
+
#
|
519
|
+
# You can use this method if you want to manually add friends and still
|
520
|
+
# have the message queued for later delivery.
|
521
|
+
def deliver_deferred(jid, message, type)
|
522
|
+
msg = {:to => jid, :message => message, :type => type}
|
523
|
+
queue(:pending_messages) << [msg]
|
524
|
+
end
|
525
|
+
|
526
|
+
# Checks if the PubSub service is set
|
527
|
+
def has_pubsub?
|
528
|
+
! @pubsub.nil?
|
529
|
+
end
|
530
|
+
|
531
|
+
# Sets the PubSub service. Just one service is allowed.
|
532
|
+
def set_pubsub_service(service)
|
533
|
+
raise NotConnected, "You are not connected" if @disconnected
|
534
|
+
raise AlreadySet, "You already have a PubSub service. Currently it's not allowed to have more." if has_pubsub?
|
535
|
+
@pubsub = PubSub::ServiceHelper.new(@client, service)
|
536
|
+
@pubsub_jid = service
|
537
|
+
|
538
|
+
@pubsub.add_event_callback do |event|
|
539
|
+
queue(:received_events) << event
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# Subscribe to a node.
|
544
|
+
def pubsubscribe_to(node)
|
545
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
546
|
+
@pubsub.subscribe_to(node)
|
547
|
+
end
|
548
|
+
|
549
|
+
# Unsubscribe from a node.
|
550
|
+
def pubunsubscribe_from(node)
|
551
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
552
|
+
|
553
|
+
# FIXME
|
554
|
+
# @pubsub.unsubscribe_from(node)
|
555
|
+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
556
|
+
# The above should just work, but I had to reimplement it since XMPP4R doesn't support subids
|
557
|
+
# and OpenFire (the Jabber Server I am testing against) seems to require it.
|
558
|
+
|
559
|
+
subids = find_subids_for(node)
|
560
|
+
return if subids.empty?
|
561
|
+
|
562
|
+
subids.each do |subid|
|
563
|
+
iq = Jabber::Iq.new(:set, @pubsub_jid)
|
564
|
+
iq.add(Jabber::PubSub::IqPubSub.new)
|
565
|
+
iq.from = @jid
|
566
|
+
unsub = REXML::Element.new('unsubscribe')
|
567
|
+
unsub.attributes['node'] = node
|
568
|
+
unsub.attributes['jid'] = @jid
|
569
|
+
unsub.attributes['subid'] = subid
|
570
|
+
iq.pubsub.add(unsub)
|
571
|
+
res = nil
|
572
|
+
@client.send_with_id(iq) do |reply|
|
573
|
+
res = reply.kind_of?(Jabber::Iq) and reply.type == :result
|
574
|
+
end # @stream.send_with_id(iq)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
# Return the subscriptions we have in the configured PubSub service.
|
579
|
+
def pubsubscriptions
|
580
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
581
|
+
@pubsub.get_subscriptions_from_all_nodes()
|
582
|
+
end
|
583
|
+
|
584
|
+
# Just like #received_messages, but for PubSub events
|
585
|
+
def received_events(&block)
|
586
|
+
dequeue(:received_events, &block)
|
587
|
+
end
|
588
|
+
|
589
|
+
# Returns true if there are unprocessed received events waiting in the
|
590
|
+
# queue, false otherwise.
|
591
|
+
def received_events?
|
592
|
+
!queue(:received_events).empty?
|
593
|
+
end
|
594
|
+
|
595
|
+
# Create a PubSub node (Lots of options still have to be encoded!)
|
596
|
+
def create_node(node)
|
597
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
598
|
+
@pubsub.create_node(node)
|
599
|
+
end
|
600
|
+
|
601
|
+
# Return an array of nodes I own
|
602
|
+
def my_nodes
|
603
|
+
ret = []
|
604
|
+
pubsubscriptions.each do |sub|
|
605
|
+
ret << sub.node if sub.attributes['affiliation'] == 'owner'
|
606
|
+
end
|
607
|
+
return ret
|
608
|
+
end
|
609
|
+
|
610
|
+
# Delete a PubSub node (Lots of options still have to be encoded!)
|
611
|
+
def delete_node(node)
|
612
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
613
|
+
@pubsub.delete_node(node)
|
614
|
+
end
|
615
|
+
|
616
|
+
# Publish an Item. This infers an item of Jabber::PubSub::Item kind is passed
|
617
|
+
def publish_item(node, item)
|
618
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
619
|
+
@pubsub.publish_item_to(node, item)
|
620
|
+
end
|
621
|
+
|
622
|
+
# Publish Simple Item. This is an item with one element and some text to it.
|
623
|
+
def publish_simple_item(node, text)
|
624
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
625
|
+
|
626
|
+
item = Jabber::PubSub::Item.new
|
627
|
+
xml = REXML::Element.new('value')
|
628
|
+
xml.text = text
|
629
|
+
item.add(xml)
|
630
|
+
publish_item(node, item)
|
631
|
+
end
|
632
|
+
|
633
|
+
# Publish atom Item. This is an item with one atom entry with title, body and time.
|
634
|
+
def publish_atom_item(node, title, body, time = Time.now)
|
635
|
+
raise NoPubSubService, "Have you forgot to call #set_pubsub_service ?" if ! has_pubsub?
|
636
|
+
|
637
|
+
item = Jabber::PubSub::Item.new
|
638
|
+
entry = REXML::Element.new('entry')
|
639
|
+
entry.add_namespace("http://www.w3.org/2005/Atom")
|
640
|
+
mytitle = REXML::Element.new('title')
|
641
|
+
mytitle.text = title
|
642
|
+
entry.add(mytitle)
|
643
|
+
mybody = REXML::Element.new('body')
|
644
|
+
mybody.text = body
|
645
|
+
entry.add(mybody)
|
646
|
+
published = REXML::Element.new("published")
|
647
|
+
published.text = time.utc.iso8601
|
648
|
+
entry.add(published)
|
649
|
+
item.add(entry)
|
650
|
+
publish_item(node, item)
|
651
|
+
end
|
652
|
+
|
653
|
+
private
|
654
|
+
|
655
|
+
# If contacts is a single contact, returns a Jabber::Contact object
|
656
|
+
# representing that user; if contacts is an array, returns an array of
|
657
|
+
# Jabber::Contact objects.
|
658
|
+
#
|
659
|
+
# When called with a block, contacts will yield each Jabber::Contact object
|
660
|
+
# in turn. This is used internally.
|
661
|
+
def contacts(*contacts, &block)
|
662
|
+
contacts.flatten!
|
663
|
+
@contacts ||= {}
|
664
|
+
contakts = []
|
665
|
+
contacts.each do |contact|
|
666
|
+
jid = contact.to_s
|
667
|
+
unless @contacts[jid]
|
668
|
+
@contacts[jid] = contact.respond_to?(:ask_for_authorization!) ? contact : Contact.new(self, contact)
|
669
|
+
end
|
670
|
+
yield @contacts[jid] if block_given?
|
671
|
+
contakts << @contacts[jid]
|
672
|
+
end
|
673
|
+
contakts.size > 1 ? contakts : contakts.first
|
674
|
+
end
|
675
|
+
|
676
|
+
def find_subids_for(node)
|
677
|
+
ret = []
|
678
|
+
pubsubscriptions.each do |subscription|
|
679
|
+
if subscription.node == node
|
680
|
+
ret << subscription.subid
|
681
|
+
end
|
682
|
+
end
|
683
|
+
return ret
|
684
|
+
end
|
685
|
+
|
686
|
+
def client=(client)
|
687
|
+
self.roster = nil # ensure we clear the roster, since that's now associated with a different client.
|
688
|
+
@client = client
|
689
|
+
end
|
690
|
+
|
691
|
+
def roster=(new_roster)
|
692
|
+
@roster = new_roster
|
693
|
+
end
|
694
|
+
|
695
|
+
def connect!
|
696
|
+
raise ConnectionError, "Connections are disabled - use Jabber::Simple::force_connect() to reconnect." if @disconnected
|
697
|
+
# Pre-connect
|
698
|
+
@connect_mutex ||= Mutex.new
|
699
|
+
|
700
|
+
# don't try to connect if another thread is already connecting.
|
701
|
+
return if @connect_mutex.locked?
|
702
|
+
|
703
|
+
@connect_mutex.lock
|
704
|
+
disconnect!(false) if connected?
|
705
|
+
|
706
|
+
# Connect
|
707
|
+
jid = JID.new(@jid)
|
708
|
+
my_client = Client.new(@jid)
|
709
|
+
my_client.connect(@host, @port)
|
710
|
+
# (TODO: test w/ gtalk --danbri) my_client.connect(@server)
|
711
|
+
my_client.auth(@password) unless @register
|
712
|
+
self.client = my_client
|
713
|
+
|
714
|
+
# Post-connect
|
715
|
+
register_default_callbacks unless @register
|
716
|
+
status(@presence, @status_message) unless @register
|
717
|
+
@connect_mutex.unlock
|
718
|
+
end
|
719
|
+
|
720
|
+
def disconnect!(auto_reconnect = true)
|
721
|
+
if client.respond_to?(:is_connected?) && client.is_connected?
|
722
|
+
begin
|
723
|
+
client.close
|
724
|
+
rescue Errno::EPIPE, IOError => e
|
725
|
+
# probably should log this.
|
726
|
+
nil
|
727
|
+
end
|
728
|
+
end
|
729
|
+
client = nil
|
730
|
+
@disconnected = auto_reconnect
|
731
|
+
end
|
732
|
+
|
733
|
+
def register_default_callbacks
|
734
|
+
client.add_message_callback do |message|
|
735
|
+
queue(:received_messages) << message unless message.body.nil?
|
736
|
+
from = roster.find(message.from).values.first
|
737
|
+
if !message.chat_state.nil? && from.chat_state != message.chat_state
|
738
|
+
from.chat_state = message.chat_state
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
roster.add_subscription_callback do |roster_item, presence|
|
743
|
+
if presence.type == :subscribed
|
744
|
+
queue(:new_subscriptions) << [roster_item, presence]
|
745
|
+
end
|
746
|
+
end
|
747
|
+
|
748
|
+
roster.add_subscription_request_callback do |roster_item, presence|
|
749
|
+
if accept_subscriptions?
|
750
|
+
roster.accept_subscription(presence.from)
|
751
|
+
else
|
752
|
+
queue(:subscription_requests) << [roster_item, presence]
|
753
|
+
end
|
754
|
+
end
|
755
|
+
|
756
|
+
@iq_mutex = Mutex.new
|
757
|
+
capture_iq_stanzas!(false)
|
758
|
+
client.add_iq_callback do |iq|
|
759
|
+
queue(:iq_messages) << iq if capture_iq_stanzas?
|
760
|
+
end
|
761
|
+
|
762
|
+
@presence_updates = {}
|
763
|
+
@presence_mutex = Mutex.new
|
764
|
+
roster.add_presence_callback do |roster_item, old_presence, new_presence|
|
765
|
+
simple_jid = roster_item.jid.strip.to_s
|
766
|
+
presence = case new_presence.type
|
767
|
+
when nil then new_presence.show || :online
|
768
|
+
when :unavailable then :unavailable
|
769
|
+
else
|
770
|
+
nil
|
771
|
+
end
|
772
|
+
|
773
|
+
if presence && @presence_updates[simple_jid] != presence
|
774
|
+
queue(:presence_updates) << simple_jid
|
775
|
+
@presence_mutex.synchronize { @presence_updates[simple_jid] = [presence, new_presence.status] }
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
# This thread facilitates the delivery of messages to users who haven't yet
|
781
|
+
# accepted an invitation from us. When we attempt to deliver a message, if
|
782
|
+
# the user hasn't subscribed, we place the message in a queue for later
|
783
|
+
# delivery. Once a user has accepted our authorization request, we deliver
|
784
|
+
# any messages that have been queued up in the meantime.
|
785
|
+
def start_deferred_delivery_thread #:nodoc:
|
786
|
+
Thread.new {
|
787
|
+
loop {
|
788
|
+
messages = [queue(:pending_messages).pop].flatten
|
789
|
+
messages.each do |message|
|
790
|
+
if subscribed_to?(message[:to])
|
791
|
+
deliver(message[:to], message[:message], message[:type])
|
792
|
+
else
|
793
|
+
queue(:pending_messages) << message
|
794
|
+
end
|
795
|
+
end
|
796
|
+
sleep 60
|
797
|
+
}
|
798
|
+
}
|
799
|
+
end
|
800
|
+
|
801
|
+
def queue(queue)
|
802
|
+
@queues ||= Hash.new { |h,k| h[k] = Queue.new }
|
803
|
+
@queues[queue]
|
804
|
+
end
|
805
|
+
|
806
|
+
def dequeue(queue, non_blocking = true, max_items = 100, &block)
|
807
|
+
queue_items = []
|
808
|
+
max_items.times do
|
809
|
+
break if queue(queue).empty?
|
810
|
+
queue_item = queue(queue).pop(non_blocking)
|
811
|
+
queue_items << queue_item
|
812
|
+
yield queue_item if block_given?
|
813
|
+
end
|
814
|
+
queue_items
|
815
|
+
end
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
true
|