xmpp-agent 0.1.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 (3) hide show
  1. data/lib/xmpp-agent.rb +58 -0
  2. data/lib/xmpp4r-simple.rb +494 -0
  3. metadata +57 -0
data/lib/xmpp-agent.rb ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # file: xmpp-agent.rb
4
+
5
+ require 'xmpp4r-simple'
6
+ require 'app-routes'
7
+
8
+ class XMPPAgent
9
+ include AppRoutes
10
+
11
+ def initialize()
12
+ @route = {}; @params = {}
13
+ end
14
+
15
+ def run(user, password)
16
+
17
+ begin
18
+ xmpp_connect(user, password)
19
+ rescue
20
+ puts ($!).to_s
21
+ sleep 5
22
+ retry
23
+ end
24
+ end
25
+
26
+ def routes(params, messenger, msg)
27
+
28
+ get %r{(send_to|send2)\s+([^\s]+)\s+(.*)} do
29
+ user, message = params[:captures].values_at 1,2
30
+ messenger.deliver(user, message)
31
+ end
32
+
33
+ get 'help' do
34
+ messenger.deliver(msg.from, "available commands: help, send_to")
35
+ end
36
+
37
+ get '*' do
38
+ messenger.deliver(msg.from, "need some help? type help")
39
+ end
40
+
41
+ end
42
+
43
+ def xmpp_connect(user, password)
44
+
45
+ puts "connecting to jabber server.."
46
+ messenger = Jabber::Simple.new(user,password)
47
+ puts "connected."
48
+
49
+ while true
50
+ messenger.received_messages do |msg|
51
+ routes(@params, messenger, msg)
52
+ run_route msg.body.strip
53
+ end
54
+ sleep 1
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,494 @@
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 'rubygems'
19
+ require 'xmpp4r'
20
+ require 'xmpp4r/roster'
21
+ require 'xmpp4r/vcard'
22
+
23
+ module Jabber
24
+
25
+ class ConnectionError < StandardError #:nodoc:
26
+ end
27
+
28
+ class Contact #:nodoc:
29
+
30
+ include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
31
+
32
+ def initialize(client, jid)
33
+ @jid = jid.respond_to?(:resource) ? jid : JID.new(jid)
34
+ @client = client
35
+ end
36
+
37
+ def inspect
38
+ "Jabber::Contact #{jid.to_s}"
39
+ end
40
+
41
+ def subscribed?
42
+ [:to, :both].include?(subscription)
43
+ end
44
+
45
+ def subscription
46
+ roster_item && roster_item.subscription
47
+ end
48
+
49
+ def ask_for_authorization!
50
+ subscription_request = Presence.new.set_type(:subscribe)
51
+ subscription_request.to = jid
52
+ client.send!(subscription_request)
53
+ end
54
+
55
+ def unsubscribe!
56
+ unsubscription_request = Presence.new.set_type(:unsubscribe)
57
+ unsubscription_request.to = jid
58
+ client.send!(unsubscription_request)
59
+ client.send!(unsubscription_request.set_type(:unsubscribed))
60
+ end
61
+
62
+ def jid(bare=true)
63
+ bare ? @jid.strip : @jid
64
+ end
65
+
66
+ private
67
+
68
+ def roster_item
69
+ client.roster.items[jid]
70
+ end
71
+
72
+ def client
73
+ @client
74
+ end
75
+ end
76
+
77
+ class Simple
78
+
79
+ include DRb::DRbUndumped if defined?(DRb::DRbUndumped)
80
+
81
+ # Create a new Jabber::Simple client. You will be automatically connected
82
+ # to the Jabber server and your status message will be set to the string
83
+ # passed in as the status_message argument.
84
+ #
85
+ # jabber = Jabber::Simple.new("me@example.com", "password", "Chat with me - Please!")
86
+ def initialize(jid, password, status = nil, status_message = "Available")
87
+ @jid = jid
88
+ @password = password
89
+ @disconnected = false
90
+ status(status, status_message)
91
+ start_deferred_delivery_thread
92
+ end
93
+
94
+ def inspect #:nodoc:
95
+ "Jabber::Simple #{@jid}"
96
+ end
97
+
98
+ # Send a message to jabber user jid.
99
+ #
100
+ # Valid message types are:
101
+ #
102
+ # * :normal (default): a normal message.
103
+ # * :chat: a one-to-one chat message.
104
+ # * :groupchat: a group-chat message.
105
+ # * :headline: a "headline" message.
106
+ # * :error: an error message.
107
+ #
108
+ # If the recipient is not in your contacts list, the message will be queued
109
+ # for later delivery, and the Contact will be automatically asked for
110
+ # authorization (see Jabber::Simple#add).
111
+ #
112
+ # message should be a string or a valid Jabber::Message object. In either case,
113
+ # the message recipient will be set to jid.
114
+ def deliver(jid, message, type=:chat)
115
+ contacts(jid) do |friend|
116
+ unless subscribed_to? friend
117
+ add(friend.jid)
118
+ return deliver_deferred(friend.jid, message, type)
119
+ end
120
+ if message.kind_of?(Jabber::Message)
121
+ msg = message
122
+ msg.to = friend.jid
123
+ else
124
+ msg = Message.new(friend.jid)
125
+ msg.type = type
126
+ msg.body = message
127
+ end
128
+ send!(msg)
129
+ end
130
+ end
131
+
132
+ # Set your presence, with a message.
133
+ #
134
+ # Available values for presence are:
135
+ #
136
+ # * nil: online.
137
+ # * :chat: free for chat.
138
+ # * :away: away from the computer.
139
+ # * :dnd: do not disturb.
140
+ # * :xa: extended away.
141
+ #
142
+ # It's not possible to set an offline status - to do that, disconnect! :-)
143
+ def status(presence, message)
144
+ @presence = presence
145
+ @status_message = message
146
+ stat_msg = Presence.new(@presence, @status_message)
147
+ send!(stat_msg)
148
+ end
149
+
150
+ # Ask the users specified by jids for authorization (i.e., ask them to add
151
+ # you to their contact list). If you are already in the user's contact list,
152
+ # add() will not attempt to re-request authorization. In order to force
153
+ # re-authorization, first remove() the user, then re-add them.
154
+ #
155
+ # Example usage:
156
+ #
157
+ # jabber_simple.add("friend@friendosaurus.com")
158
+ #
159
+ # Because the authorization process might take a few seconds, or might
160
+ # never happen depending on when (and if) the user accepts your
161
+ # request, results are placed in the Jabber::Simple#new_subscriptions queue.
162
+ def add(*jids)
163
+ contacts(*jids) do |friend|
164
+ next if subscribed_to? friend
165
+ friend.ask_for_authorization!
166
+ end
167
+ end
168
+
169
+ # Remove the jabber users specified by jids from the contact list.
170
+ def remove(*jids)
171
+ contacts(*jids) do |unfriend|
172
+ unfriend.unsubscribe!
173
+ end
174
+ end
175
+
176
+ # Returns true if this Jabber account is subscribed to status updates for
177
+ # the jabber user jid, false otherwise.
178
+ def subscribed_to?(jid)
179
+ contacts(jid) do |contact|
180
+ return contact.subscribed?
181
+ end
182
+ end
183
+
184
+ # If contacts is a single contact, returns a Jabber::Contact object
185
+ # representing that user; if contacts is an array, returns an array of
186
+ # Jabber::Contact objects.
187
+ #
188
+ # When called with a block, contacts will yield each Jabber::Contact object
189
+ # in turn. This is mainly used internally, but exposed as an utility
190
+ # function.
191
+ def contacts(*contacts, &block)
192
+ @contacts ||= {}
193
+ contakts = []
194
+ contacts.each do |contact|
195
+ jid = contact.to_s
196
+ unless @contacts[jid]
197
+ @contacts[jid] = contact.respond_to?(:ask_for_authorization!) ? contact : Contact.new(self, contact)
198
+ end
199
+ yield @contacts[jid] if block_given?
200
+ contakts << @contacts[jid]
201
+ end
202
+ contakts.size > 1 ? contakts : contakts.first
203
+ end
204
+
205
+ # Returns true if the Jabber client is connected to the Jabber server,
206
+ # false otherwise.
207
+ def connected?
208
+ @client ||= nil
209
+ connected = @client.respond_to?(:is_connected?) && @client.is_connected?
210
+ return connected
211
+ end
212
+
213
+ # Returns an array of messages received since the last time
214
+ # received_messages was called. Passing a block will yield each message in
215
+ # turn, allowing you to break part-way through processing (especially
216
+ # useful when your message handling code is not thread-safe (e.g.,
217
+ # ActiveRecord).
218
+ #
219
+ # e.g.:
220
+ #
221
+ # jabber.received_messages do |message|
222
+ # puts "Received message from #{message.from}: #{message.body}"
223
+ # end
224
+ def received_messages(&block)
225
+ dequeue(:received_messages, &block)
226
+ end
227
+
228
+ # Returns true if there are unprocessed received messages waiting in the
229
+ # queue, false otherwise.
230
+ def received_messages?
231
+ !queue(:received_messages).empty?
232
+ end
233
+
234
+ # Returns an array of presence updates received since the last time
235
+ # presence_updates was called. Passing a block will yield each update in
236
+ # turn, allowing you to break part-way through processing (especially
237
+ # useful when your presence handling code is not thread-safe (e.g.,
238
+ # ActiveRecord).
239
+ #
240
+ # e.g.:
241
+ #
242
+ # jabber.presence_updates do |friend, new_presence|
243
+ # puts "Received presence update from #{friend}: #{new_presence}"
244
+ # end
245
+ def presence_updates(&block)
246
+ updates = []
247
+ @presence_mutex.synchronize do
248
+ dequeue(:presence_updates) do |friend|
249
+ presence = @presence_updates[friend]
250
+ next unless presence
251
+ new_update = [friend, presence[0], presence[1]]
252
+ yield new_update if block_given?
253
+ updates << new_update
254
+ @presence_updates.delete(friend)
255
+ end
256
+ end
257
+ return updates
258
+ end
259
+
260
+ # Returns true if there are unprocessed presence updates waiting in the
261
+ # queue, false otherwise.
262
+ def presence_updates?
263
+ !queue(:presence_updates).empty?
264
+ end
265
+
266
+ # Returns an array of subscription notifications received since the last
267
+ # time new_subscriptions was called. Passing a block will yield each update
268
+ # in turn, allowing you to break part-way through processing (especially
269
+ # useful when your subscription handling code is not thread-safe (e.g.,
270
+ # ActiveRecord).
271
+ #
272
+ # e.g.:
273
+ #
274
+ # jabber.new_subscriptions do |friend, presence|
275
+ # puts "Received presence update from #{friend.to_s}: #{presence}"
276
+ # end
277
+ def new_subscriptions(&block)
278
+ dequeue(:new_subscriptions, &block)
279
+ end
280
+
281
+ # Returns true if there are unprocessed presence updates waiting in the
282
+ # queue, false otherwise.
283
+ def new_subscriptions?
284
+ !queue(:new_subscriptions).empty?
285
+ end
286
+
287
+ # Returns an array of subscription notifications received since the last
288
+ # time subscription_requests was called. Passing a block will yield each update
289
+ # in turn, allowing you to break part-way through processing (especially
290
+ # useful when your subscription handling code is not thread-safe (e.g.,
291
+ # ActiveRecord).
292
+ #
293
+ # e.g.:
294
+ #
295
+ # jabber.subscription_requests do |friend, presence|
296
+ # puts "Received presence update from #{friend.to_s}: #{presence}"
297
+ # end
298
+ def subscription_requests(&block)
299
+ dequeue(:subscription_requests, &block)
300
+ end
301
+
302
+ # Returns true if auto-accept subscriptions (friend requests) is enabled
303
+ # (default), false otherwise.
304
+ def accept_subscriptions?
305
+ @accept_subscriptions = true if @accept_subscriptions.nil?
306
+ @accept_subscriptions
307
+ end
308
+
309
+ # Change whether or not subscriptions (friend requests) are automatically accepted.
310
+ def accept_subscriptions=(accept_status)
311
+ @accept_subscriptions = accept_status
312
+ end
313
+
314
+ # Direct access to the underlying Roster helper.
315
+ def roster
316
+ return @roster if @roster
317
+ self.roster = Roster::Helper.new(client)
318
+ end
319
+
320
+ # Direct access to the underlying Jabber client.
321
+ def client
322
+ connect!() unless connected?
323
+ @client
324
+ end
325
+
326
+ # Send a Jabber stanza over-the-wire.
327
+ def send!(msg)
328
+ attempts = 0
329
+ begin
330
+ attempts += 1
331
+ client.send(msg)
332
+ rescue Errno::EPIPE, IOError => e
333
+ sleep 1
334
+ disconnect
335
+ reconnect
336
+ retry unless attempts > 3
337
+ raise e
338
+ rescue Errno::ECONNRESET => e
339
+ sleep (attempts^2) * 60 + 60
340
+ disconnect
341
+ reconnect
342
+ retry unless attempts > 3
343
+ raise e
344
+ end
345
+ end
346
+
347
+ # Use this to force the client to reconnect after a force_disconnect.
348
+ def reconnect
349
+ @disconnected = false
350
+ connect!
351
+ end
352
+
353
+ # Use this to force the client to disconnect and not automatically
354
+ # reconnect.
355
+ def disconnect
356
+ disconnect!
357
+ end
358
+
359
+ # Queue messages for delivery once a user has accepted our authorization
360
+ # request. Works in conjunction with the deferred delivery thread.
361
+ #
362
+ # You can use this method if you want to manually add friends and still
363
+ # have the message queued for later delivery.
364
+ def deliver_deferred(jid, message, type)
365
+ msg = {:to => jid, :message => message, :type => type}
366
+ queue(:pending_messages) << [msg]
367
+ end
368
+
369
+ private
370
+
371
+ def client=(client)
372
+ self.roster = nil # ensure we clear the roster, since that's now associated with a different client.
373
+ @client = client
374
+ end
375
+
376
+ def roster=(new_roster)
377
+ @roster = new_roster
378
+ end
379
+
380
+ def connect!
381
+ raise ConnectionError, "Connections are disabled - use Jabber::Simple::force_connect() to reconnect." if @disconnected
382
+ # Pre-connect
383
+ @connect_mutex ||= Mutex.new
384
+
385
+ # don't try to connect if another thread is already connecting.
386
+ return if @connect_mutex.locked?
387
+
388
+ @connect_mutex.lock
389
+ disconnect!(false) if connected?
390
+
391
+ # Connect
392
+ jid = JID.new(@jid)
393
+ my_client = Client.new(@jid)
394
+ my_client.connect
395
+ my_client.auth(@password)
396
+ self.client = my_client
397
+
398
+ # Post-connect
399
+ register_default_callbacks
400
+ status(@presence, @status_message)
401
+ @connect_mutex.unlock
402
+ end
403
+
404
+ def disconnect!(auto_reconnect = true)
405
+ if client.respond_to?(:is_connected?) && client.is_connected?
406
+ begin
407
+ client.close
408
+ rescue Errno::EPIPE, IOError => e
409
+ # probably should log this.
410
+ nil
411
+ end
412
+ end
413
+ client = nil
414
+ @disconnected = auto_reconnect
415
+ end
416
+
417
+ def register_default_callbacks
418
+ client.add_message_callback do |message|
419
+ queue(:received_messages) << message unless message.body.nil?
420
+ end
421
+
422
+ roster.add_subscription_callback do |roster_item, presence|
423
+ if presence.type == :subscribed
424
+ queue(:new_subscriptions) << [roster_item, presence]
425
+ end
426
+ end
427
+
428
+ roster.add_subscription_request_callback do |roster_item, presence|
429
+ if accept_subscriptions?
430
+ roster.accept_subscription(presence.from)
431
+ else
432
+ queue(:subscription_requests) << [roster_item, presence]
433
+ end
434
+ end
435
+
436
+ @presence_updates = {}
437
+ @presence_mutex = Mutex.new
438
+ roster.add_presence_callback do |roster_item, old_presence, new_presence|
439
+ simple_jid = roster_item.jid.strip.to_s
440
+ presence = case new_presence.type
441
+ when nil
442
+ new_presence.show || :online
443
+ when :unavailable
444
+ :unavailable
445
+ else
446
+ nil
447
+ end
448
+
449
+ if presence && @presence_updates[simple_jid] != presence
450
+ queue(:presence_updates) << simple_jid
451
+ @presence_mutex.synchronize { @presence_updates[simple_jid] = [presence, new_presence.status] }
452
+ end
453
+ end
454
+ end
455
+
456
+ # This thread facilitates the delivery of messages to users who haven't yet
457
+ # accepted an invitation from us. When we attempt to deliver a message, if
458
+ # the user hasn't subscribed, we place the message in a queue for later
459
+ # delivery. Once a user has accepted our authorization request, we deliver
460
+ # any messages that have been queued up in the meantime.
461
+ def start_deferred_delivery_thread #:nodoc:
462
+ Thread.new {
463
+ loop {
464
+ messages = [queue(:pending_messages).pop].flatten
465
+ messages.each do |message|
466
+ if subscribed_to?(message[:to])
467
+ deliver(message[:to], message[:message], message[:type])
468
+ else
469
+ queue(:pending_messages) << message
470
+ end
471
+ end
472
+ }
473
+ }
474
+ end
475
+
476
+ def queue(queue)
477
+ @queues ||= Hash.new { |h,k| h[k] = Queue.new }
478
+ @queues[queue]
479
+ end
480
+
481
+ def dequeue(queue, non_blocking = true, max_items = 100, &block)
482
+ queue_items = []
483
+ max_items.times do
484
+ queue_item = queue(queue).pop(non_blocking) rescue nil
485
+ break if queue_item.nil?
486
+ queue_items << queue_item
487
+ yield queue_item if block_given?
488
+ end
489
+ queue_items
490
+ end
491
+ end
492
+ end
493
+
494
+ true
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xmpp-agent
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors: []
8
+
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-04 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies: []
16
+
17
+ description:
18
+ email:
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - lib/xmpp-agent.rb
27
+ - lib/xmpp4r-simple.rb
28
+ has_rdoc: true
29
+ homepage:
30
+ licenses: []
31
+
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: "0"
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: "0"
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.5.2
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: xmpp-agent
56
+ test_files: []
57
+