xmpp-agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+