-
Notifications
You must be signed in to change notification settings - Fork 3
/
xmpp4r-simple.rb
506 lines (446 loc) · 15.5 KB
/
xmpp4r-simple.rb
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
# 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
# NOTICE: This is a slightly patched and modified xmpp4r-simple
# Run a diff against the original if you wish to see what's changed.
require 'rubygems'
require 'xmpp4r'
require 'xmpp4r/roster'
require 'xmpp4r/vcard'
require 'xmpp4r/muc/helper/simplemucclient'
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))
# patch start
roster_item = client.roster.items[jid]
if roster_item
roster_item.remove()
end
# patch end
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
#return
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:
deferred_delivery_thread = 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
}
}
deferred_delivery_thread[:name] = "defdel"
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