jabber4r-revive 0.9.0 → 0.10.0

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 (45) hide show
  1. data/.gitignore +5 -4
  2. data/.rspec +3 -3
  3. data/.travis.yml +7 -7
  4. data/CHANGELOG +11 -1
  5. data/Gemfile +3 -3
  6. data/README.md +29 -29
  7. data/Rakefile +70 -70
  8. data/jabber4r-revive.gemspec +25 -25
  9. data/lib/jabber4r.rb +38 -33
  10. data/lib/jabber4r/bosh.rb +21 -0
  11. data/lib/jabber4r/bosh/authentication.rb +13 -0
  12. data/lib/jabber4r/bosh/authentication/non_sasl.rb +219 -0
  13. data/lib/jabber4r/bosh/authentication/sasl.rb +239 -0
  14. data/lib/jabber4r/bosh/session.rb +144 -0
  15. data/lib/jabber4r/connection.rb +259 -258
  16. data/lib/jabber4r/debugger.rb +60 -60
  17. data/lib/jabber4r/jid.rb +20 -19
  18. data/lib/jabber4r/protocol.rb +249 -257
  19. data/lib/jabber4r/protocol/authentication.rb +14 -0
  20. data/lib/jabber4r/protocol/authentication/non_sasl.rb +138 -0
  21. data/lib/jabber4r/protocol/authentication/sasl.rb +88 -0
  22. data/lib/jabber4r/protocol/iq.rb +259 -259
  23. data/lib/jabber4r/protocol/message.rb +245 -245
  24. data/lib/jabber4r/protocol/parsed_xml_element.rb +207 -207
  25. data/lib/jabber4r/protocol/presence.rb +160 -160
  26. data/lib/jabber4r/protocol/xml_element.rb +143 -143
  27. data/lib/jabber4r/rexml_1.8_patch.rb +15 -15
  28. data/lib/jabber4r/roster.rb +38 -38
  29. data/lib/jabber4r/session.rb +615 -615
  30. data/lib/jabber4r/version.rb +10 -3
  31. data/spec/lib/jabber4r/bosh/authentication/non_sasl_spec.rb +79 -0
  32. data/spec/lib/jabber4r/bosh/authentication/sasl_spec.rb +42 -0
  33. data/spec/lib/jabber4r/bosh/session_spec.rb +406 -0
  34. data/spec/lib/jabber4r/bosh_spec.rb +0 -0
  35. data/spec/lib/jabber4r/connection_spec.rb +174 -174
  36. data/spec/lib/jabber4r/debugger_spec.rb +35 -35
  37. data/spec/lib/jabber4r/jid_spec.rb +197 -197
  38. data/spec/lib/jabber4r/protocol/authentication/non_sasl_spec.rb +79 -0
  39. data/spec/lib/jabber4r/protocol/authentication/sasl_spec.rb +42 -0
  40. data/spec/spec_helper.rb +11 -11
  41. data/spec/support/mocks/tcp_socket_mock.rb +8 -8
  42. metadata +61 -45
  43. data/Gemfile.lock +0 -45
  44. data/lib/jabber4r/bosh_session.rb +0 -224
  45. data/spec/lib/jabber4r/bosh_session_spec.rb +0 -150
@@ -1,615 +1,615 @@
1
- # License: see LICENSE.txt
2
- # Jabber4R - Jabber Instant Messaging Library for Ruby
3
- # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
4
- #
5
-
6
-
7
- module Jabber
8
- HEX = "0123456789abcdef"
9
-
10
- ##
11
- # Generates a random hex string in the following format:
12
- # JRR_01234567
13
- #
14
- # return:: [String] The resource id
15
- #
16
- def Jabber.gen_random_resource
17
- return Jabber.gen_random_id("JRR_", 8)
18
- end
19
-
20
- ##
21
- # Generates a random thread as a hex string in the following format:
22
- # JRT_01234567890123456789
23
- #
24
- # return:: [String] The thread id
25
- #
26
- def Jabber.gen_random_thread
27
- return Jabber.gen_random_id("JRT_", 20)
28
- end
29
-
30
- ##
31
- # Generates a random id as a hex string
32
- #
33
- # prefix:: [String="Jabber4R_] The prefix for the random hex data
34
- # length:: [Integer=16] The number of hex characters
35
- # return:: [String] The random id
36
- #
37
- def Jabber.gen_random_id(prefix="Jabber4R_", length=16)
38
- length.times {prefix += HEX[rand(16),1]}
39
- prefix
40
- end
41
-
42
- class Subscription
43
- attr_accessor :type, :from, :id, :session
44
- def initialize(session, type, from, id)
45
- @session = session
46
- @type = type
47
- @from = from
48
- @id = id
49
- end
50
- def accept
51
- case type
52
- when :subscribe
53
- @session.connection.send(Jabber::Protocol::Presence.gen_accept_subscription(@id, @from))
54
- when :unsubscribe
55
- @session.connection.send(Jabber::Protocol::Presence.gen_accept_unsubscription(@id, @from))
56
- else
57
- raise "Cannot accept a subscription of type #{type.to_s}"
58
- end
59
- end
60
- end
61
-
62
- ##
63
- # This is a base class for subscription handlers
64
-
65
- class SubscriptionHandler
66
- def subscribe(subscription)
67
- end
68
-
69
- def subscribed(subscription)
70
- end
71
-
72
- def unsubscribe(subscription)
73
- end
74
-
75
- def unsubscribed(subscription)
76
- end
77
- end
78
-
79
- class AutoSubscriptionHandler < SubscriptionHandler
80
-
81
- def subscribe(subscription)
82
- subscription.accept
83
- end
84
-
85
- def unsubscribe(subscription)
86
- subscription.accept
87
- end
88
- end
89
-
90
-
91
- ##
92
- # The Jabber Session is the main class for dealing with a Jabber service.
93
- #
94
- class Session
95
-
96
- # The host this session is connected to
97
- attr_reader :host
98
-
99
- # The port (defaults to 5222) that this session is connected to
100
- attr_reader :port
101
-
102
- # The Jabber::Protocol::Connection instance
103
- attr_reader :connection
104
-
105
- # The Jabber::Roster instance
106
- attr_reader :roster
107
-
108
- # The session id sent from the Jabber service upon connection
109
- attr_reader :session_id
110
-
111
- # The Jabber::JID of the current session
112
- attr_reader :jid
113
-
114
- # The username to use for authenticating this session
115
- attr_accessor :username
116
-
117
- # The password to use for authenticating this session
118
- attr_accessor :password
119
-
120
- # The resource id for this session
121
- attr_accessor :resource
122
-
123
- # The iq handlers for this session
124
- attr_accessor :iqHandlers
125
-
126
- ##
127
- # Session creation factory that creates a session, logs in,
128
- # requests the roster, registers message and presence filters
129
- # and announces initial presence. Login is done via plaintext
130
- # password authentication.
131
- #
132
- # jid:: [String | JID] The account information ("account@host/resouce")
133
- # password:: [String] The account password
134
- # port:: [Integer = 5222] The host port
135
- # digest:: [Boolean = false] Use digest authentication?
136
- # return:: [Jabber::Session] The new session
137
- #
138
- def Session.bind(jid, password, port=5222, digest=false)
139
- jid = Jabber::JID.new(jid) if jid.kind_of? String
140
- session = Session.new(jid.host, port)
141
- raise "Authentication failed" unless session.authenticate(jid.node, password, jid.resource, digest)
142
- session.request_roster
143
- session.register_message_filter
144
- session.register_presence_filter
145
- session.register_iq_filter
146
- session.announce_initial_presence
147
- session
148
- end
149
-
150
- ##
151
- # Account registration method
152
- #
153
- def Session.register(jid, password, email="", name="", port=5222)
154
- jid = Jabber::JID.new(jid) if jid.kind_of? String
155
- session = Session.new(jid.host, port)
156
- msg_id = session.id
157
- registered = false
158
- current = Thread.current
159
- session.connection.send(Jabber::Protocol::Iq.gen_registration(session, msg_id, jid.node, password, email, name)) do |element|
160
- if element.element_tag=="iq" and element.attr_id==msg_id
161
- element.consume_element
162
- if element.attr_type=="result"
163
- registered = true
164
- elsif element.attr_type=="error"
165
- registered = false
166
- end
167
- current.wakeup
168
- end
169
- end
170
- Thread.stop
171
- session.release
172
- return registered
173
- end
174
-
175
- ##
176
- # Session creation factory that creates a session, logs in,
177
- # requests the roster, registers message and presence filters
178
- # and announces initial presence. Login is done via digest (SHA)
179
- # password authentication.
180
- #
181
- # jid:: [String | JID] The account information ("account@host/resouce")
182
- # password:: [String] The account password
183
- # port:: [Integer = 5222] The host port
184
- # return:: [Jabber::Session] The new session
185
- #
186
- def Session.bind_digest(jid, password, port=5222)
187
- Session.bind(jid, password, port, true)
188
- end
189
-
190
- # Creates a new session connected to the supplied host and port.
191
- # The method attempts to build a Jabber::Protocol::Connection
192
- # object and send the open_stream XML message. It then blocks
193
- # to recieve the coorisponding reply open_stream and sets the
194
- # session_id from that xml element.
195
- #
196
- # host:: [String] The hostname of the Jabber service
197
- # port:: [Integer=5222] The port of the Jabber service
198
- # raise:: [RuntimeException] If connection fails
199
- #
200
- def initialize(host, port=5222)
201
- @id = 1
202
- @host = host
203
- @port = port
204
- @roster = Roster.new(self)
205
- @messageListeners = Hash.new
206
- @iqHandlers=Hash.new
207
- @subscriptionHandler = nil
208
- @connection = Jabber::Connection.new(host, port)
209
- @connection.connect
210
- unless @connection.connected?
211
- raise "Session Error: Could not connected to #{host}:#{port}"
212
- else
213
- @connection.send(Jabber::Protocol.gen_open_stream(host)) do |element|
214
- if element.element_tag=="stream:stream"
215
- element.consume_element
216
- @session_id = element.attr_id
217
- end
218
- end
219
- @connection.on_connection_exception do |exception|
220
- @session_failure_block.call if @session_failure_block
221
-
222
- if exception.is_a? Jabber::ConnectionForceCloseError
223
- @connection.force_close!
224
- else
225
- self.release
226
- end
227
- end
228
- Thread.stop
229
- end
230
- end
231
-
232
- ##
233
- # Set a handler for session exceptions that get caught in
234
- # communicating with the Jabber server.
235
- #
236
- def on_session_failure(&block)
237
- @session_failure_block = block
238
- end
239
-
240
- ##
241
- # Counter for message IDs
242
- #
243
- # return:: [String] A unique message id for this session
244
- #
245
- def id
246
- @id = @id + 1
247
- return @id.to_s
248
- end
249
-
250
- ##
251
- # Authenticate (logs into) this session with the supplied credentials.
252
- # The method blocks waiting for a reply to the login message. Sets the
253
- # authenticated attribute based on result.
254
- #
255
- # username:: [String] The username to use for authentication
256
- # password:: [String] The password to use for authentication
257
- # resource:: [String] The resource ID for this session
258
- # digest:: [Boolean=false] True to use digest authentication (not sending password in the clear)
259
- # return:: [Boolean] Whether the authentication succeeded or failed
260
- #
261
- def authenticate(username, password, resource, digest=false)
262
- @username = username
263
- @password = password
264
- @resource = resource
265
- @jid = JID.new("#{username}@#{@host}/#{resource}")
266
- @roster.add(@jid, "both", "Me", "My Resources")
267
-
268
- msg_id = self.id
269
- authHandler = Proc.new do |element|
270
- if element.element_tag=="iq" and element.attr_id==msg_id
271
- element.consume_element
272
- if element.attr_type=="result"
273
- @authenticated = true
274
- elsif element.attr_type=="error"
275
- @authenticated = false
276
- end
277
- end
278
- end
279
- if digest
280
- require 'digest/sha1'
281
- authRequest = Jabber::Protocol::Iq.gen_auth_digest(self, msg_id, username, Digest::SHA1.new(@session_id + password).hexdigest, resource)
282
- else
283
- authRequest = Jabber::Protocol::Iq.gen_auth(self, msg_id, username, password, resource)
284
- end
285
- @connection.send(authRequest, &authHandler)
286
- Thread.stop
287
- return @authenticated
288
- end
289
-
290
- ##
291
- # Is this an authenticated session?
292
- #
293
- # return:: [Boolean] True if the session is authenticated
294
- #
295
- def is_authenticated?
296
- return @authenticated
297
- end
298
-
299
- ##
300
- # Sends the initial presence message to the Jabber service
301
- #
302
- def announce_initial_presence
303
- @connection.send(Jabber::Protocol::Presence.gen_initial(id))
304
- end
305
-
306
- ##
307
- # Sends an extended away presence message
308
- #
309
- # status:: [String] The status message
310
- #
311
- def announce_extended_away(status=nil)
312
- @connection.send(Jabber::Protocol::Presence.gen_xa(id, status))
313
- end
314
-
315
- ##
316
- # Sends a free for chat presence message
317
- #
318
- # status:: [String] The status message
319
- #
320
- def announce_free_for_chat(status=nil)
321
- @connection.send(Jabber::Protocol::Presence.gen_chat(id, status))
322
- end
323
-
324
- ##
325
- # Sends a 'normal' presence message
326
- #
327
- # status:: [String] The status message
328
- #
329
- def announce_normal(status=nil)
330
- @connection.send(Jabber::Protocol::Presence.gen_normal(id, status))
331
- end
332
-
333
- ##
334
- # Sends an away from computer presence message
335
- #
336
- # status:: [String] The status message
337
- #
338
- def announce_away_from_computer(status=nil)
339
- @connection.send(Jabber::Protocol::Presence.gen_away(id, status))
340
- end
341
-
342
- ##
343
- # Sends a do not disturb presence message
344
- #
345
- # status:: [String] The status message
346
- #
347
- def announce_do_not_disturb(status=nil)
348
- @connection.send(Jabber::Protocol::Presence.gen_dnd(id, status))
349
- end
350
-
351
- ##
352
- # Sets the handler for subscription requests, notifications, etc.
353
- #
354
- def set_subscription_handler(handler=nil, &block)
355
- @subscriptionHandler = handler.new(self) if handler
356
- @subscriptionHandler = block if block_given? and !handler
357
- end
358
-
359
- def enable_autosubscription
360
- set_subscription_handler AutoSubscriptionHandler
361
- end
362
-
363
- def subscribe(to, name="")
364
- to = JID.to_jid(to)
365
- roster_item = @roster[to]
366
-
367
- if roster_item #if you already have a roster item just send the subscribe request
368
- if roster_item.subscription=="to" or roster_item.subscription=="both"
369
- return
370
- end
371
- @connection.send(Jabber::Protocol::Presence.gen_new_subscription(to))
372
- return
373
- end
374
- myid = self.id
375
- @connection.send(Jabber::Protocol::Iq.gen_add_rosteritem(self, myid, to, name)) do |element|
376
- if element.attr_id==myid
377
- element.consume_element
378
- if element.attr_type=="result"
379
- @connection.send(Jabber::Protocol::Presence.gen_new_subscription(to))
380
- end
381
- end
382
- end
383
- end
384
-
385
- ##
386
- # Adds a filter to the Connection to manage tracking resources that come online/offline
387
- #
388
- def register_presence_filter
389
- @connection.add_filter("presenceAvailableFilter") do |element|
390
- if element.element_tag=="presence"
391
- type = element.attr_type
392
- type = nil if type.nil?
393
- case type
394
- when nil, "available"
395
- element.consume_element
396
- from = JID.new(element.attr_from)
397
- rItem = @roster[from]
398
- show = element.show.element_data
399
- show = "chat" unless show
400
- status = element.status.element_data
401
- status = "" unless status
402
- if rItem
403
- resource = rItem[from.resource]
404
- if resource
405
- resource.update(show, status)
406
- else
407
- rItem.add(from.resource, show, status)
408
- end
409
- end
410
- when "unavailable"
411
- element.consume_element
412
- from = JID.new(element.attr_from)
413
- rItem = @roster[from]
414
- resource = rItem.delete(from.resource) if rItem
415
- when "subscribe", "unsubscribe", "subscribed", "unsubscribed"
416
- element.consume_element
417
- from = JID.new(element.attr_from)
418
- break unless @subscriptionHandler
419
- if @subscriptionHandler.kind_of? Proc
420
- @subscriptionHandler.call(Subscription.new(self, type.intern, from, id))
421
- else
422
- @subscriptionHandler.send(Subscription.new(self, type.intern, from, id))
423
- end
424
- end
425
- end #if presence
426
- end #do
427
- end
428
-
429
- ##
430
- # Creates a new message to the supplied JID of type NORMAL
431
- #
432
- # to:: [Jabber::JID] Who to send the message to
433
- # type:: [String = Jabber::Protocol::Message::NORMAL] The type of message to send (see Jabber::Protocol::Message)
434
- # return:: [Jabber::Protocol::Message] The new message
435
- #
436
- def new_message(to, type=Jabber::Protocol::Message::NORMAL)
437
- msg = Jabber::Protocol::Message.new(to, type)
438
- msg.session=self
439
- return msg
440
- end
441
-
442
- ##
443
- # Creates a new message addressed to the supplied JID of type CHAT
444
- #
445
- # to:: [JID] Who to send the message to
446
- # return:: [Jabber::Protocol::Message] The new (chat) message
447
- #
448
- def new_chat_message(to)
449
- self.new_message(to, Jabber::Protocol::Message::CHAT)
450
- end
451
-
452
- ##
453
- # Creates a new message addressed to the supplied JID of type GROUPCHAT
454
- #
455
- # to:: [JID] Who to send the message to
456
- # return:: [Jabber::Protocol::Message] The new (group chat) message
457
- #
458
- def new_group_chat_message(to)
459
- self.new_message(to, Jabber::Protocol::Message::GROUPCHAT)
460
- end
461
-
462
- ##
463
- # Adds a filter to the Connection to manage tracking messages to forward
464
- # to registered message listeners.
465
- #
466
- def register_message_filter
467
- @connection.add_filter("messageFilter") do |element|
468
- if element.element_tag=="message" and @messageListeners.size > 0
469
- element.consume_element
470
- message = Jabber::Protocol::Message.from_element(self, element)
471
- notify_message_listeners(message)
472
- end #if message
473
- end #do
474
- end
475
-
476
- ##
477
- # Add a listener for new messages
478
- #
479
- # Usage::
480
- # id = session.add_message_listener do |message|
481
- # puts message
482
- # end
483
- #
484
- # &block [Block] The block to process a message
485
- # return:: [String] The listener ID...used to remove the listener
486
- #
487
- def add_message_listener(&block)
488
- id = Jabber.gen_random_id("", 10)
489
- @messageListeners[id]=block if block
490
- return id
491
- end
492
-
493
- ##
494
- # Deletes a message listener
495
- #
496
- # id:: [String] A messanger ID returned from add_message_listener
497
- #
498
- def delete_message_listener(lid)
499
- @messageListeners.delete(lid)
500
- end
501
-
502
- #Cleanup methods
503
-
504
- ##
505
- # Releases the connection and resets the session. The Session instance
506
- # is no longer usable after this method is called
507
- #
508
- def release
509
- begin
510
- @connection.on_connection_exception do
511
- #Do nothing...we are shutting down
512
- end
513
- @connection.send(Jabber::Protocol::Presence.gen_unavailable(id))
514
- @connection.send(Jabber::Protocol.gen_close_stream)
515
- rescue
516
- #ignore error
517
- end
518
- begin
519
- @connection.close
520
- rescue
521
- #ignore error
522
- end
523
- end
524
-
525
- ##
526
- # Same as _release
527
- #
528
- def close
529
- release
530
- end
531
-
532
- ##
533
- # Requests the Roster for the (authenticated) account. This method blocks
534
- # until a reply to the roster request is received.
535
- #
536
- def request_roster
537
- if @authenticated
538
- msg_id = id
539
- @connection.send(Jabber::Protocol::Iq.gen_roster(self, msg_id)) do |element|
540
- if element.attr_id == msg_id
541
- element.consume_element
542
- element.query.item.count.times do |i|
543
- item = element.query.item[i]
544
- @roster.add(item.attr_jid, item.attr_subscription, item.attr_name, item.group.element_data)
545
- end
546
- end
547
- end
548
- Thread.stop
549
- register_roster_filter
550
- end
551
- end
552
-
553
- ##
554
- # Registers the roster filter with the Connection to forward IQ requests
555
- # to the IQ listeners(they register by namespace)
556
- #
557
- def register_iq_filter()
558
- @connection.add_filter("iqFilter") do |element|
559
- if element.element_tag=="iq" then
560
- element.consume_element
561
- query=element.query
562
- h=@iqHandlers[query.attr_xmlns]
563
- h.call(Jabber::Protocol::Iq.from_element(self,element)) if h
564
- end
565
- end
566
- end
567
-
568
-
569
- ##
570
- # Registers the roster filter with the Connection to forward roster changes to
571
- # the roster listeners.
572
- #
573
- def register_roster_filter
574
- @connection.add_filter("rosterFilter") do |element|
575
- if element.element_tag=="iq" and element.query.attr_xmlns=="jabber:iq:roster" and element.attr_type=="set"
576
- element.consume_element
577
- item = element.query.item
578
- if item.attr_subscription=="remove" then
579
- @roster.remove(item.attr_jid)
580
- else
581
- @roster.add(item.attr_jid, item.attr_subscription, item.attr_name, item.group.element_data)
582
- end
583
- end
584
- end
585
- end
586
-
587
- ##
588
- # Registers a listener for roster events
589
- #
590
- # &block:: [Block] The listener block to process roster changes
591
- # return:: [String] A roster ID to use when removing this listener
592
- #
593
- def add_roster_listener(&block)
594
- roster.add_listener(&block)
595
- end
596
-
597
- ##
598
- # Deletes the roster listener
599
- #
600
- # id:: [String] A roster ID received from the add_roster_listener method
601
- #
602
- def delete_roster_listener(id)
603
- roster.delete_listener(id)
604
- end
605
-
606
- ##
607
- # Notifies message listeners of the received message
608
- #
609
- # message:: [Jabber::Protocol::Message] The received message
610
- #
611
- def notify_message_listeners(message)
612
- @messageListeners.each_value {|listener| listener.call(message)}
613
- end
614
- end
615
- end
1
+ # License: see LICENSE.txt
2
+ # Jabber4R - Jabber Instant Messaging Library for Ruby
3
+ # Copyright (C) 2002 Rich Kilmer <rich@infoether.com>
4
+ #
5
+
6
+
7
+ module Jabber
8
+ HEX = "0123456789abcdef"
9
+
10
+ ##
11
+ # Generates a random hex string in the following format:
12
+ # JRR_01234567
13
+ #
14
+ # return:: [String] The resource id
15
+ #
16
+ def Jabber.gen_random_resource
17
+ return Jabber.gen_random_id("JRR_", 8)
18
+ end
19
+
20
+ ##
21
+ # Generates a random thread as a hex string in the following format:
22
+ # JRT_01234567890123456789
23
+ #
24
+ # return:: [String] The thread id
25
+ #
26
+ def Jabber.gen_random_thread
27
+ return Jabber.gen_random_id("JRT_", 20)
28
+ end
29
+
30
+ ##
31
+ # Generates a random id as a hex string
32
+ #
33
+ # prefix:: [String="Jabber4R_] The prefix for the random hex data
34
+ # length:: [Integer=16] The number of hex characters
35
+ # return:: [String] The random id
36
+ #
37
+ def Jabber.gen_random_id(prefix="Jabber4R_", length=16)
38
+ length.times {prefix += HEX[rand(16),1]}
39
+ prefix
40
+ end
41
+
42
+ class Subscription
43
+ attr_accessor :type, :from, :id, :session
44
+ def initialize(session, type, from, id)
45
+ @session = session
46
+ @type = type
47
+ @from = from
48
+ @id = id
49
+ end
50
+ def accept
51
+ case type
52
+ when :subscribe
53
+ @session.connection.send(Jabber::Protocol::Presence.gen_accept_subscription(@id, @from))
54
+ when :unsubscribe
55
+ @session.connection.send(Jabber::Protocol::Presence.gen_accept_unsubscription(@id, @from))
56
+ else
57
+ raise "Cannot accept a subscription of type #{type.to_s}"
58
+ end
59
+ end
60
+ end
61
+
62
+ ##
63
+ # This is a base class for subscription handlers
64
+
65
+ class SubscriptionHandler
66
+ def subscribe(subscription)
67
+ end
68
+
69
+ def subscribed(subscription)
70
+ end
71
+
72
+ def unsubscribe(subscription)
73
+ end
74
+
75
+ def unsubscribed(subscription)
76
+ end
77
+ end
78
+
79
+ class AutoSubscriptionHandler < SubscriptionHandler
80
+
81
+ def subscribe(subscription)
82
+ subscription.accept
83
+ end
84
+
85
+ def unsubscribe(subscription)
86
+ subscription.accept
87
+ end
88
+ end
89
+
90
+
91
+ ##
92
+ # The Jabber Session is the main class for dealing with a Jabber service.
93
+ #
94
+ class Session
95
+
96
+ # The domain this session is connected to
97
+ attr_reader :domain
98
+
99
+ # The port (defaults to 5222) that this session is connected to
100
+ attr_reader :port
101
+
102
+ # The Jabber::Protocol::Connection instance
103
+ attr_reader :connection
104
+
105
+ # The Jabber::Roster instance
106
+ attr_reader :roster
107
+
108
+ # The session id sent from the Jabber service upon connection
109
+ attr_reader :session_id
110
+
111
+ # The Jabber::JID of the current session
112
+ attr_reader :jid
113
+
114
+ # The username to use for authenticating this session
115
+ attr_accessor :username
116
+
117
+ # The password to use for authenticating this session
118
+ attr_accessor :password
119
+
120
+ # The resource id for this session
121
+ attr_accessor :resource
122
+
123
+ # The iq handlers for this session
124
+ attr_accessor :iqHandlers
125
+
126
+ ##
127
+ # Session creation factory that creates a session, logs in,
128
+ # requests the roster, registers message and presence filters
129
+ # and announces initial presence. Login is done via plaintext
130
+ # password authentication.
131
+ #
132
+ # jid:: [String | JID] The account information ("account@domain/resouce")
133
+ # password:: [String] The account password
134
+ # port:: [Integer = 5222] The domain port
135
+ # digest:: [Boolean = false] Use digest authentication?
136
+ # return:: [Jabber::Session] The new session
137
+ #
138
+ def Session.bind(jid, password, port=5222, digest=false)
139
+ jid = Jabber::JID.new(jid) if jid.kind_of? String
140
+ session = Session.new(jid.domain, port)
141
+ raise "Authentication failed" unless session.authenticate(jid.node, password, jid.resource, digest)
142
+ session.request_roster
143
+ session.register_message_filter
144
+ session.register_presence_filter
145
+ session.register_iq_filter
146
+ session.announce_initial_presence
147
+ session
148
+ end
149
+
150
+ ##
151
+ # Account registration method
152
+ #
153
+ def Session.register(jid, password, email="", name="", port=5222)
154
+ jid = Jabber::JID.new(jid) if jid.kind_of? String
155
+ session = Session.new(jid.domain, port)
156
+ msg_id = session.id
157
+ registered = false
158
+ current = Thread.current
159
+ session.connection.send(Jabber::Protocol::Iq.gen_registration(session, msg_id, jid.node, password, email, name)) do |element|
160
+ if element.element_tag=="iq" and element.attr_id==msg_id
161
+ element.consume_element
162
+ if element.attr_type=="result"
163
+ registered = true
164
+ elsif element.attr_type=="error"
165
+ registered = false
166
+ end
167
+ current.wakeup
168
+ end
169
+ end
170
+ Thread.stop
171
+ session.release
172
+ return registered
173
+ end
174
+
175
+ ##
176
+ # Session creation factory that creates a session, logs in,
177
+ # requests the roster, registers message and presence filters
178
+ # and announces initial presence. Login is done via digest (SHA)
179
+ # password authentication.
180
+ #
181
+ # jid:: [String | JID] The account information ("account@domain/resouce")
182
+ # password:: [String] The account password
183
+ # port:: [Integer = 5222] The domain port
184
+ # return:: [Jabber::Session] The new session
185
+ #
186
+ def Session.bind_digest(jid, password, port=5222)
187
+ Session.bind(jid, password, port, true)
188
+ end
189
+
190
+ # Creates a new session connected to the supplied domain and port.
191
+ # The method attempts to build a Jabber::Protocol::Connection
192
+ # object and send the open_stream XML message. It then blocks
193
+ # to recieve the coorisponding reply open_stream and sets the
194
+ # session_id from that xml element.
195
+ #
196
+ # domain:: [String] The domain indentificator of the Jabber service
197
+ # port:: [Integer=5222] The port of the Jabber service
198
+ # raise:: [RuntimeException] If connection fails
199
+ #
200
+ def initialize(domain, port=5222)
201
+ @id = 1
202
+ @domain = domain
203
+ @port = port
204
+ @roster = Roster.new(self)
205
+ @messageListeners = Hash.new
206
+ @iqHandlers=Hash.new
207
+ @subscriptionHandler = nil
208
+ @connection = Jabber::Connection.new(domain, port)
209
+ @connection.connect
210
+ unless @connection.connected?
211
+ raise "Session Error: Could not connected to #{domain}:#{port}"
212
+ else
213
+ @connection.send(Jabber::Protocol.gen_open_stream(domain)) do |element|
214
+ if element.element_tag=="stream:stream"
215
+ element.consume_element
216
+ @session_id = element.attr_id
217
+ end
218
+ end
219
+ @connection.on_connection_exception do |exception|
220
+ @session_failure_block.call if @session_failure_block
221
+
222
+ if exception.is_a? Jabber::ConnectionForceCloseError
223
+ @connection.force_close!
224
+ else
225
+ self.release
226
+ end
227
+ end
228
+ Thread.stop
229
+ end
230
+ end
231
+
232
+ ##
233
+ # Set a handler for session exceptions that get caught in
234
+ # communicating with the Jabber server.
235
+ #
236
+ def on_session_failure(&block)
237
+ @session_failure_block = block
238
+ end
239
+
240
+ ##
241
+ # Counter for message IDs
242
+ #
243
+ # return:: [String] A unique message id for this session
244
+ #
245
+ def id
246
+ @id = @id + 1
247
+ return @id.to_s
248
+ end
249
+
250
+ ##
251
+ # Authenticate (logs into) this session with the supplied credentials.
252
+ # The method blocks waiting for a reply to the login message. Sets the
253
+ # authenticated attribute based on result.
254
+ #
255
+ # username:: [String] The username to use for authentication
256
+ # password:: [String] The password to use for authentication
257
+ # resource:: [String] The resource ID for this session
258
+ # digest:: [Boolean=false] True to use digest authentication (not sending password in the clear)
259
+ # return:: [Boolean] Whether the authentication succeeded or failed
260
+ #
261
+ def authenticate(username, password, resource, digest=false)
262
+ @username = username
263
+ @password = password
264
+ @resource = resource
265
+ @jid = JID.new("#{username}@#{@domain}/#{resource}")
266
+ @roster.add(@jid, "both", "Me", "My Resources")
267
+
268
+ msg_id = self.id
269
+ authHandler = Proc.new do |element|
270
+ if element.element_tag=="iq" and element.attr_id==msg_id
271
+ element.consume_element
272
+ if element.attr_type=="result"
273
+ @authenticated = true
274
+ elsif element.attr_type=="error"
275
+ @authenticated = false
276
+ end
277
+ end
278
+ end
279
+ if digest
280
+ require 'digest/sha1'
281
+ authRequest = Jabber::Protocol::Iq.gen_auth_digest(self, msg_id, username, Digest::SHA1.new(@session_id + password).hexdigest, resource)
282
+ else
283
+ authRequest = Jabber::Protocol::Iq.gen_auth(self, msg_id, username, password, resource)
284
+ end
285
+ @connection.send(authRequest, &authHandler)
286
+ Thread.stop
287
+ return @authenticated
288
+ end
289
+
290
+ ##
291
+ # Is this an authenticated session?
292
+ #
293
+ # return:: [Boolean] True if the session is authenticated
294
+ #
295
+ def is_authenticated?
296
+ return @authenticated
297
+ end
298
+
299
+ ##
300
+ # Sends the initial presence message to the Jabber service
301
+ #
302
+ def announce_initial_presence
303
+ @connection.send(Jabber::Protocol::Presence.gen_initial(id))
304
+ end
305
+
306
+ ##
307
+ # Sends an extended away presence message
308
+ #
309
+ # status:: [String] The status message
310
+ #
311
+ def announce_extended_away(status=nil)
312
+ @connection.send(Jabber::Protocol::Presence.gen_xa(id, status))
313
+ end
314
+
315
+ ##
316
+ # Sends a free for chat presence message
317
+ #
318
+ # status:: [String] The status message
319
+ #
320
+ def announce_free_for_chat(status=nil)
321
+ @connection.send(Jabber::Protocol::Presence.gen_chat(id, status))
322
+ end
323
+
324
+ ##
325
+ # Sends a 'normal' presence message
326
+ #
327
+ # status:: [String] The status message
328
+ #
329
+ def announce_normal(status=nil)
330
+ @connection.send(Jabber::Protocol::Presence.gen_normal(id, status))
331
+ end
332
+
333
+ ##
334
+ # Sends an away from computer presence message
335
+ #
336
+ # status:: [String] The status message
337
+ #
338
+ def announce_away_from_computer(status=nil)
339
+ @connection.send(Jabber::Protocol::Presence.gen_away(id, status))
340
+ end
341
+
342
+ ##
343
+ # Sends a do not disturb presence message
344
+ #
345
+ # status:: [String] The status message
346
+ #
347
+ def announce_do_not_disturb(status=nil)
348
+ @connection.send(Jabber::Protocol::Presence.gen_dnd(id, status))
349
+ end
350
+
351
+ ##
352
+ # Sets the handler for subscription requests, notifications, etc.
353
+ #
354
+ def set_subscription_handler(handler=nil, &block)
355
+ @subscriptionHandler = handler.new(self) if handler
356
+ @subscriptionHandler = block if block_given? and !handler
357
+ end
358
+
359
+ def enable_autosubscription
360
+ set_subscription_handler AutoSubscriptionHandler
361
+ end
362
+
363
+ def subscribe(to, name="")
364
+ to = JID.to_jid(to)
365
+ roster_item = @roster[to]
366
+
367
+ if roster_item #if you already have a roster item just send the subscribe request
368
+ if roster_item.subscription=="to" or roster_item.subscription=="both"
369
+ return
370
+ end
371
+ @connection.send(Jabber::Protocol::Presence.gen_new_subscription(to))
372
+ return
373
+ end
374
+ myid = self.id
375
+ @connection.send(Jabber::Protocol::Iq.gen_add_rosteritem(self, myid, to, name)) do |element|
376
+ if element.attr_id==myid
377
+ element.consume_element
378
+ if element.attr_type=="result"
379
+ @connection.send(Jabber::Protocol::Presence.gen_new_subscription(to))
380
+ end
381
+ end
382
+ end
383
+ end
384
+
385
+ ##
386
+ # Adds a filter to the Connection to manage tracking resources that come online/offline
387
+ #
388
+ def register_presence_filter
389
+ @connection.add_filter("presenceAvailableFilter") do |element|
390
+ if element.element_tag=="presence"
391
+ type = element.attr_type
392
+ type = nil if type.nil?
393
+ case type
394
+ when nil, "available"
395
+ element.consume_element
396
+ from = JID.new(element.attr_from)
397
+ rItem = @roster[from]
398
+ show = element.show.element_data
399
+ show = "chat" unless show
400
+ status = element.status.element_data
401
+ status = "" unless status
402
+ if rItem
403
+ resource = rItem[from.resource]
404
+ if resource
405
+ resource.update(show, status)
406
+ else
407
+ rItem.add(from.resource, show, status)
408
+ end
409
+ end
410
+ when "unavailable"
411
+ element.consume_element
412
+ from = JID.new(element.attr_from)
413
+ rItem = @roster[from]
414
+ resource = rItem.delete(from.resource) if rItem
415
+ when "subscribe", "unsubscribe", "subscribed", "unsubscribed"
416
+ element.consume_element
417
+ from = JID.new(element.attr_from)
418
+ break unless @subscriptionHandler
419
+ if @subscriptionHandler.kind_of? Proc
420
+ @subscriptionHandler.call(Subscription.new(self, type.intern, from, id))
421
+ else
422
+ @subscriptionHandler.send(Subscription.new(self, type.intern, from, id))
423
+ end
424
+ end
425
+ end #if presence
426
+ end #do
427
+ end
428
+
429
+ ##
430
+ # Creates a new message to the supplied JID of type NORMAL
431
+ #
432
+ # to:: [Jabber::JID] Who to send the message to
433
+ # type:: [String = Jabber::Protocol::Message::NORMAL] The type of message to send (see Jabber::Protocol::Message)
434
+ # return:: [Jabber::Protocol::Message] The new message
435
+ #
436
+ def new_message(to, type=Jabber::Protocol::Message::NORMAL)
437
+ msg = Jabber::Protocol::Message.new(to, type)
438
+ msg.session=self
439
+ return msg
440
+ end
441
+
442
+ ##
443
+ # Creates a new message addressed to the supplied JID of type CHAT
444
+ #
445
+ # to:: [JID] Who to send the message to
446
+ # return:: [Jabber::Protocol::Message] The new (chat) message
447
+ #
448
+ def new_chat_message(to)
449
+ self.new_message(to, Jabber::Protocol::Message::CHAT)
450
+ end
451
+
452
+ ##
453
+ # Creates a new message addressed to the supplied JID of type GROUPCHAT
454
+ #
455
+ # to:: [JID] Who to send the message to
456
+ # return:: [Jabber::Protocol::Message] The new (group chat) message
457
+ #
458
+ def new_group_chat_message(to)
459
+ self.new_message(to, Jabber::Protocol::Message::GROUPCHAT)
460
+ end
461
+
462
+ ##
463
+ # Adds a filter to the Connection to manage tracking messages to forward
464
+ # to registered message listeners.
465
+ #
466
+ def register_message_filter
467
+ @connection.add_filter("messageFilter") do |element|
468
+ if element.element_tag=="message" and @messageListeners.size > 0
469
+ element.consume_element
470
+ message = Jabber::Protocol::Message.from_element(self, element)
471
+ notify_message_listeners(message)
472
+ end #if message
473
+ end #do
474
+ end
475
+
476
+ ##
477
+ # Add a listener for new messages
478
+ #
479
+ # Usage::
480
+ # id = session.add_message_listener do |message|
481
+ # puts message
482
+ # end
483
+ #
484
+ # &block [Block] The block to process a message
485
+ # return:: [String] The listener ID...used to remove the listener
486
+ #
487
+ def add_message_listener(&block)
488
+ id = Jabber.gen_random_id("", 10)
489
+ @messageListeners[id]=block if block
490
+ return id
491
+ end
492
+
493
+ ##
494
+ # Deletes a message listener
495
+ #
496
+ # id:: [String] A messanger ID returned from add_message_listener
497
+ #
498
+ def delete_message_listener(lid)
499
+ @messageListeners.delete(lid)
500
+ end
501
+
502
+ #Cleanup methods
503
+
504
+ ##
505
+ # Releases the connection and resets the session. The Session instance
506
+ # is no longer usable after this method is called
507
+ #
508
+ def release
509
+ begin
510
+ @connection.on_connection_exception do
511
+ #Do nothing...we are shutting down
512
+ end
513
+ @connection.send(Jabber::Protocol::Presence.gen_unavailable(id))
514
+ @connection.send(Jabber::Protocol.gen_close_stream)
515
+ rescue
516
+ #ignore error
517
+ end
518
+ begin
519
+ @connection.close
520
+ rescue
521
+ #ignore error
522
+ end
523
+ end
524
+
525
+ ##
526
+ # Same as _release
527
+ #
528
+ def close
529
+ release
530
+ end
531
+
532
+ ##
533
+ # Requests the Roster for the (authenticated) account. This method blocks
534
+ # until a reply to the roster request is received.
535
+ #
536
+ def request_roster
537
+ if @authenticated
538
+ msg_id = id
539
+ @connection.send(Jabber::Protocol::Iq.gen_roster(self, msg_id)) do |element|
540
+ if element.attr_id == msg_id
541
+ element.consume_element
542
+ element.query.item.count.times do |i|
543
+ item = element.query.item[i]
544
+ @roster.add(item.attr_jid, item.attr_subscription, item.attr_name, item.group.element_data)
545
+ end
546
+ end
547
+ end
548
+ Thread.stop
549
+ register_roster_filter
550
+ end
551
+ end
552
+
553
+ ##
554
+ # Registers the roster filter with the Connection to forward IQ requests
555
+ # to the IQ listeners(they register by namespace)
556
+ #
557
+ def register_iq_filter()
558
+ @connection.add_filter("iqFilter") do |element|
559
+ if element.element_tag=="iq" then
560
+ element.consume_element
561
+ query=element.query
562
+ h=@iqHandlers[query.attr_xmlns]
563
+ h.call(Jabber::Protocol::Iq.from_element(self,element)) if h
564
+ end
565
+ end
566
+ end
567
+
568
+
569
+ ##
570
+ # Registers the roster filter with the Connection to forward roster changes to
571
+ # the roster listeners.
572
+ #
573
+ def register_roster_filter
574
+ @connection.add_filter("rosterFilter") do |element|
575
+ if element.element_tag=="iq" and element.query.attr_xmlns=="jabber:iq:roster" and element.attr_type=="set"
576
+ element.consume_element
577
+ item = element.query.item
578
+ if item.attr_subscription=="remove" then
579
+ @roster.remove(item.attr_jid)
580
+ else
581
+ @roster.add(item.attr_jid, item.attr_subscription, item.attr_name, item.group.element_data)
582
+ end
583
+ end
584
+ end
585
+ end
586
+
587
+ ##
588
+ # Registers a listener for roster events
589
+ #
590
+ # &block:: [Block] The listener block to process roster changes
591
+ # return:: [String] A roster ID to use when removing this listener
592
+ #
593
+ def add_roster_listener(&block)
594
+ roster.add_listener(&block)
595
+ end
596
+
597
+ ##
598
+ # Deletes the roster listener
599
+ #
600
+ # id:: [String] A roster ID received from the add_roster_listener method
601
+ #
602
+ def delete_roster_listener(id)
603
+ roster.delete_listener(id)
604
+ end
605
+
606
+ ##
607
+ # Notifies message listeners of the received message
608
+ #
609
+ # message:: [Jabber::Protocol::Message] The received message
610
+ #
611
+ def notify_message_listeners(message)
612
+ @messageListeners.each_value {|listener| listener.call(message)}
613
+ end
614
+ end
615
+ end