xmpp4em 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ == 0.2.0 / 2010-04-19
2
+
3
+ Added new Component connection.
4
+ Made base classes for Client, Component and connections.
5
+ Added class Lazy Loading.
6
+ Add Credits.
data/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # XMPP4EM
2
+
3
+ Simple XMPP client built on EventMachine.
4
+
5
+ Requires eventmachine-xmlpushparser (in vendor/) and eventmachine 0.12.1
6
+
7
+ # CREDIT
8
+
9
+ * by Aman Gupta
10
+ * by Kokorin Denis (http://blog.mccoder.name)
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :default do
2
+ Dir.chdir('spec'){
3
+ sh 'bacon spec_runner.rb'
4
+ }
5
+ end
@@ -0,0 +1,39 @@
1
+ #encoding: utf-8
2
+ require 'xmpp4em'
3
+
4
+ started = Time.now
5
+ users = {}
6
+ connected = 0
7
+
8
+ EM.epoll
9
+
10
+ EM.run{
11
+ user = XMPP4EM::Component.new("test.localhost", 'secret')
12
+ user.on(:login) do
13
+ connected += 1
14
+ p ['connected', "#{connected}"]
15
+
16
+ p ['done', Time.now - started]
17
+ #EM.stop_event_loop
18
+ end
19
+ user.on(:iq) do |stanza|
20
+ p ["IQ",stanza]
21
+ stanza.to,stanza.from=stanza.from,stanza.to
22
+ user.send stanza
23
+ end
24
+ user.on(:message) do |stanza|
25
+ p ["message", stanza]
26
+ stanza.to,stanza.from=stanza.from,stanza.to
27
+ user.send stanza
28
+ end
29
+ user.on(:presence) do |stanza|
30
+ p ["presence", stanza]
31
+ stanza.to,stanza.from=stanza.from,stanza.to
32
+ user.send stanza
33
+ end
34
+ user.on(:disconnect) do
35
+ p ['disconnected']
36
+ end
37
+
38
+ user.connect('127.0.0.1', 5555)
39
+ }
@@ -0,0 +1,40 @@
1
+ require 'xmpp4em'
2
+
3
+ started = Time.now
4
+ users = {}
5
+ connected = 0
6
+ num = Integer(ARGV[0])
7
+ num = 1000 if num < 1
8
+ EM.epoll
9
+
10
+ EM.run{
11
+ num.times do |i|
12
+ users[i] = XMPP4EM::Client.new("test_#{i}@localhost", 'test', :auto_register => true)
13
+ users[i].on(:login) do
14
+ connected += 1
15
+ p ['connected', i, "#{connected} of #{num}"]
16
+
17
+ if connected == num
18
+ p ['done', Time.now - started]
19
+ EM.stop_event_loop
20
+ end
21
+ end
22
+ users[i].on(:iq) do |stanza|
23
+ p "IQ"
24
+ p stanza
25
+ end
26
+ users[i].on(:message) do |stanza|
27
+ p "IQ"
28
+ p stanza
29
+ end
30
+ users[i].on(:presence) do |stanza|
31
+ p "IQ"
32
+ p stanza
33
+ end
34
+ users[i].on(:disconnect) do
35
+ p ['disconnected', i]
36
+ end
37
+
38
+ users[i].connect('localhost', 5222)
39
+ end
40
+ }
@@ -0,0 +1,122 @@
1
+ #encoding: utf-8
2
+ require 'rubygems'
3
+
4
+ require 'stringio'
5
+ require 'rexml/parsers/sax2parser'
6
+
7
+ require 'xmpp4r/idgenerator'
8
+ require 'xmpp4r/xmppstanza'
9
+ require 'xmpp4r/iq'
10
+ require 'xmpp4r/message'
11
+ require 'xmpp4r/presence'
12
+ require 'resolv'
13
+
14
+ require 'eventmachine'
15
+ require 'evma_xmlpushparser'
16
+ EM.epoll
17
+
18
+ module XMPP4EM
19
+
20
+ class BaseClient
21
+ include EM::Deferrable
22
+
23
+ def initialize user, pass, logger=nil,opts = {}
24
+ @user = user
25
+ @pass = pass
26
+ @logger=logger
27
+ @deferred_status =:succeeded
28
+ @connection = nil
29
+ @authenticated = false
30
+ @opts = opts
31
+ @auth_callback = nil
32
+ @id_callbacks = {}
33
+ @on_stanza=nil
34
+ @events_callbacks = {
35
+ :message => [],
36
+ :presence => [],
37
+ :iq => [],
38
+ :exception => [],
39
+ :login => [],
40
+ :disconnect => [],
41
+ :connected => []
42
+ }
43
+
44
+ on(:disconnect) do
45
+ @deferred_status = nil
46
+ @authenticated=false
47
+ end
48
+ on(:login) do
49
+ succeed
50
+ end
51
+ end
52
+ attr_reader :connection, :user
53
+
54
+ def reconnect
55
+ @connection.close_connection_after_writing
56
+ @deferred_status = nil
57
+ connect
58
+ end
59
+
60
+ def connected?
61
+ @connection and !@connection.error?
62
+ end
63
+
64
+ def register_stanza &blk
65
+ @on_stanza = blk if block_given?
66
+ end
67
+
68
+ def send data, safe=false, &blk
69
+
70
+ if block_given? and data.is_a? Jabber::XMPPStanza
71
+ if data.id.nil?
72
+ data.id = Jabber::IdGenerator.instance.generate_id
73
+ end
74
+ @id_callbacks[ data.id ] = blk
75
+ end
76
+ if safe
77
+ callback { @connection.send(data) }
78
+ else
79
+ @connection.send(data)
80
+ end
81
+ end
82
+
83
+ def close
84
+ @connection.close_connection_after_writing
85
+ @deferred_status = nil
86
+ @connection = nil
87
+ end
88
+ alias :disconnect :close
89
+
90
+ def receive stanza
91
+ if stanza.kind_of?(Jabber::XMPPStanza) and stanza.id and blk = @id_callbacks[ stanza.id ]
92
+ @id_callbacks.delete stanza.id
93
+ blk.call(stanza)
94
+ return
95
+ end
96
+
97
+ return if receive_stanza(stanza)
98
+ return if @on_stanza && @on_stanza.call(stanza)
99
+
100
+ case stanza
101
+ when Jabber::Message then on(:message, stanza)
102
+ when Jabber::Iq then on(:iq, stanza)
103
+ when Jabber::Presence then on(:presence, stanza)
104
+ end
105
+ end
106
+
107
+ def on type, *args, &blk
108
+ if blk
109
+ @events_callbacks[type] << blk
110
+ else
111
+ @events_callbacks[type].each do |blk|
112
+ blk.call(*args)
113
+ end
114
+ end
115
+ end
116
+
117
+ def add_message_callback (&blk) on :message, &blk end
118
+ def add_presence_callback (&blk) on :presence, &blk end
119
+ def add_iq_callback (&blk) on :iq, &blk end
120
+ def on_exception (&blk) on :exception, &blk end
121
+ end
122
+ end
@@ -0,0 +1,129 @@
1
+ #encoding: utf-8
2
+ require 'stringio'
3
+ require 'rexml/parsers/sax2parser'
4
+
5
+ require 'xmpp4r/idgenerator'
6
+ require 'xmpp4r/xmppstanza'
7
+ require 'xmpp4r/iq'
8
+ require 'xmpp4r/message'
9
+ require 'xmpp4r/presence'
10
+
11
+ module XMPP4EM
12
+
13
+ class BaseConnection < EventMachine::Connection
14
+
15
+ def initialize host, port=5222
16
+ @host, @port = host, port
17
+ @client = nil
18
+ end
19
+ attr_accessor :client, :host, :port, :logger
20
+
21
+ def connection_completed
22
+ @logger.debug{'connected'} if @logger
23
+ @stream_features, @stream_mechanisms = {}, []
24
+ @keepalive = EM::Timer.new(60){ send_data("\n") }
25
+ @client.on(:connected)
26
+ init
27
+ end
28
+ attr_reader :stream_features
29
+
30
+ include EventMachine::XmlPushParser
31
+
32
+ def encode2utf8(text)
33
+ text.respond_to?(:force_encoding) ? text.force_encoding("utf-8") : text
34
+ end
35
+
36
+ def start_element name, attrs
37
+ e = REXML::Element.new(name)
38
+ attrs.each { |name, value|
39
+ e.add_attribute(name, encode2utf8(value))
40
+ }
41
+
42
+ @current = @current.nil? ? e : @current.add_element(e)
43
+
44
+ if @current.name == 'stream' and not @started
45
+ @started = true
46
+ process
47
+ @current = nil
48
+ end
49
+ end
50
+
51
+ def end_element name
52
+ if name == 'stream:stream' and @current.nil?
53
+ @started = false
54
+ else
55
+ if @current.parent
56
+ @current = @current.parent
57
+ else
58
+ process
59
+ @current = nil
60
+ end
61
+ end
62
+ end
63
+
64
+ def characters text
65
+ @current.text = @current.text.to_s + encode2utf8(text) if @current
66
+ end
67
+
68
+ def error *args
69
+ p ['error', *args]
70
+ end
71
+
72
+ def receive_data data
73
+ @logger.debug{"<< #{data}"} if @logger
74
+ super
75
+ end
76
+
77
+ def send data, &blk
78
+ @logger.debug{ ">> #{data}"} if @logger
79
+ send_data data.to_s
80
+ end
81
+
82
+ def unbind
83
+ if @keepalive
84
+ @keepalive.cancel
85
+ @keepalive = nil
86
+ end
87
+ @client.on(:disconnect)
88
+ @logger.debug{'disconnected'} if @logger
89
+ end
90
+
91
+ def reconnect host = @host, port = @port
92
+ super
93
+ end
94
+
95
+ def init
96
+ send "<?xml version='1.0' ?>" unless @started
97
+ @started = false
98
+ send "<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='#{namespace;}' xml:lang='en' version='1.0' to='#{@host}'>"
99
+ end
100
+
101
+ private
102
+
103
+ def pre_process_stanza(stanza)
104
+ end
105
+
106
+ def process
107
+ if @current.namespace('').to_s == '' # REXML namespaces are always strings
108
+ @current.add_namespace(@streamns)
109
+ end
110
+ stanza = @current
111
+ if 'stream'==stanza.prefix
112
+ if 'stream'==stanza.name
113
+ @streamid = stanza.attributes['id']
114
+ #All connections has the same namespace
115
+ @streamns = 'jabber:client'
116
+ end
117
+ pre_process_stanza(stanza)
118
+ end
119
+ # Any stanza, classes are registered by XMPPElement::name_xmlns
120
+ begin
121
+ stanza = Jabber::XMPPStanza::import(@current)
122
+ rescue Jabber::NoNameXmlnsRegistered
123
+ stanza = @current
124
+ end
125
+ @client.receive(stanza)
126
+ end
127
+ end
128
+
129
+ end
@@ -0,0 +1,138 @@
1
+ #encoding: utf-8
2
+ require 'xmpp4r/sasl'
3
+ require 'resolv'
4
+
5
+ module XMPP4EM
6
+
7
+ class Client < BaseClient
8
+
9
+ def initialize user, pass, logger=nil, opts = {}
10
+ super
11
+ @opts = { :auto_register => false }.merge(opts)
12
+ end
13
+
14
+ def jid
15
+ @jid ||= if @user.kind_of?(Jabber::JID)
16
+ @user
17
+ else
18
+ @user =~ /@/ ? Jabber::JID.new(@user) : Jabber::JID.new(@user, 'localhost')
19
+ end
20
+ end
21
+
22
+ def connect host = jid.domain, port = 5222
23
+ if host=='localhost' || host=='127.0.0.1' || %r{^([0-9]{1,3}.){3}[0-9]{1,3}$}.match(host)
24
+ target_host, target_port= host, port
25
+ else
26
+ target_host, target_port = resolve_host(host)
27
+ end
28
+ EM.connect target_host, target_port, ClientConnection, jid.domain, port do |conn|
29
+ @connection = conn
30
+ conn.client = self
31
+ conn.logger=@logger
32
+ end
33
+ end
34
+
35
+ def resolve_host(domain)
36
+ srv = []
37
+ begin
38
+ Resolv::DNS.open { |dns|
39
+ # If ruby version is too old and SRV is unknown, this will raise a NameError
40
+ # which is catched below
41
+ #debug("RESOLVING:\n_xmpp-client._tcp.#{domain} (SRV)")
42
+ srv = dns.getresources("_xmpp-client._tcp.#{domain}", Resolv::DNS::Resource::IN::SRV)
43
+ }
44
+ rescue NameError
45
+
46
+ end
47
+
48
+ unless srv.blank?
49
+ # Sort SRV records: lowest priority first, highest weight first
50
+ srv.sort! { |a,b| (a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight) }
51
+ #debug "USING #{srv.first.target.to_s}"
52
+ return srv.first.target.to_s, srv.first.port
53
+ else
54
+ #debug "USING #{domain}:5222"
55
+ return domain, 5222
56
+ end
57
+
58
+ end
59
+
60
+ def login &blk
61
+ Jabber::SASL::new(self, 'PLAIN').auth(@pass)
62
+ @auth_callback = blk if block_given?
63
+ end
64
+
65
+ def register &blk
66
+ reg = Jabber::Iq.new_register(jid.node, @pass)
67
+ reg.to = jid.domain
68
+
69
+ send(reg){ |reply|
70
+ blk.call( reply.type == :result ? :success : reply.type )
71
+ }
72
+ end
73
+
74
+ def send_msg to, msg
75
+ send_safe Jabber::Message::new(to, msg).set_type(:chat)
76
+ end
77
+
78
+ def receive_stanza(stanza)
79
+
80
+ case stanza.name
81
+ when 'features'
82
+ unless @authenticated
83
+ login do |res|
84
+ # log ['login response', res].inspect
85
+ if res == :failure and @opts[:auto_register]
86
+ register do |res|
87
+ #p ['register response', res]
88
+ login unless res == :error
89
+ end
90
+ end
91
+ end
92
+
93
+ else
94
+ if @connection.stream_features.has_key? 'bind'
95
+ iq = Jabber::Iq.new(:set)
96
+ bind = iq.add REXML::Element.new('bind')
97
+ bind.add_namespace @connection.stream_features['bind']
98
+ resource = bind.add REXML::Element.new('resource')
99
+ resource.text=jid.resource
100
+
101
+ send(iq){ |reply|
102
+ if reply.type == :result and jid = reply.first_element('//jid') and jid.text
103
+ # log ['new jid is', jid.text].inspect
104
+ @jid = Jabber::JID.new(jid.text)
105
+ end
106
+ }
107
+ end
108
+
109
+ if @connection.stream_features.has_key? 'session'
110
+ iq = Jabber::Iq.new(:set)
111
+ session = iq.add REXML::Element.new('session')
112
+ session.add_namespace @connection.stream_features['session']
113
+
114
+ send(iq){ |reply|
115
+ if reply.type == :result
116
+ on(:login, stanza)
117
+ end
118
+ }
119
+ end
120
+ end
121
+
122
+ return true
123
+
124
+ when 'success', 'failure'
125
+ if stanza.name == 'success'
126
+ @authenticated = true
127
+ @connection.reset_parser
128
+ @connection.init
129
+ end
130
+
131
+ @auth_callback.call(stanza.name.to_sym) if @auth_callback
132
+ return true
133
+ end
134
+ false
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,24 @@
1
+ #encoding: utf-8
2
+
3
+ module XMPP4EM
4
+
5
+ class ClientConnection < BaseConnection
6
+ def namespace; 'jabber:client'; end;
7
+
8
+ private
9
+ def pre_process_stanza(stanza)
10
+ return if 'stream'!=stanza.prefix && 'features'!=stanza.name
11
+ @stream_features, @stream_mechanisms = {}, []
12
+ stanza.each do |e|
13
+ if e.name == 'mechanisms' and e.namespace == 'urn:ietf:params:xml:ns:xmpp-sasl'
14
+ e.each_element('mechanism') do |mech|
15
+ @stream_mechanisms.push(mech.text)
16
+ end
17
+ else
18
+ @stream_features[e.name] = e.namespace
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,45 @@
1
+ #encoding: utf-8
2
+
3
+ module XMPP4EM
4
+
5
+ class Component < BaseClient
6
+ def initialize user, pass, opts = {}
7
+ super
8
+ end
9
+
10
+ def jid
11
+ @jid ||= @user.kind_of?(Jabber::JID) ? @user : Jabber::JID.new(@user)
12
+ end
13
+
14
+ def connect host = jid.domain, port = 5222
15
+ EM.connect host, port, ComponentConnection, jid.domain, port do |conn|
16
+ @connection = conn
17
+ conn.client = self
18
+ end
19
+ end
20
+
21
+ def receive_stanza(stanza)
22
+
23
+ case stanza.name
24
+ when 'stream'
25
+ if !@authenticated && jid.domain == stanza.attributes['from']
26
+ streamid = stanza.attributes['id']
27
+ hash = Digest::SHA1::hexdigest(streamid.to_s + @pass)
28
+ send("<handshake>#{hash}</handshake>")
29
+ end
30
+ return true
31
+ when 'not-authorized'
32
+ on(:error, 'not-authorized')
33
+ return true
34
+ when 'handshake'
35
+ @authenticated = true
36
+ on(:login, stanza)
37
+ return true
38
+ end
39
+
40
+ false
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ #encoding: utf-8
2
+
3
+ module XMPP4EM
4
+
5
+ class ComponentConnection < BaseConnection
6
+ def namespace; 'jabber:component:accept'; end;
7
+ end
8
+
9
+ end
data/lib/xmpp4em.rb ADDED
@@ -0,0 +1,10 @@
1
+ module XMPP4EM
2
+ class NotConnected < Exception; end
3
+ VERSION='0.2.0'
4
+ autoload :ClientConnection, 'xmpp4em/client_connection'
5
+ autoload :Client, 'xmpp4em/client'
6
+ autoload :BaseClient, 'xmpp4em/base_client'
7
+ autoload :ComponentConnection, 'xmpp4em/component_connection'
8
+ autoload :BaseConnection, 'xmpp4em/base_connection'
9
+ autoload :Component, 'xmpp4em/component'
10
+ end
@@ -0,0 +1,26 @@
1
+ require 'bacon'
2
+ $:.unshift File.dirname(__FILE__) + '/..'
3
+ require 'xmpp4em'
4
+
5
+ shared 'eventmachine' do
6
+ $bacon_thread = Thread.current
7
+ def wait
8
+ Thread.stop
9
+ @timer = EM::Timer.new(10) do
10
+ wake
11
+ should.flunk('waited too long')
12
+ end
13
+ end
14
+ def wake
15
+ $bacon_thread.wakeup
16
+ @timer.cancel if @timer
17
+ end
18
+ end
19
+
20
+ EM.run{
21
+ Thread.new{
22
+ Thread.abort_on_exception = true
23
+ require 'xmpp4em_spec'
24
+ EM.stop_event_loop
25
+ }
26
+ }
@@ -0,0 +1,57 @@
1
+ describe 'XMPP4EM' do
2
+ behaves_like 'eventmachine'
3
+
4
+ @foo = XMPP4EM::Client.new('foo@localhost', 'test', :auto_register => true)
5
+ @bar = XMPP4EM::Client.new('bar@localhost', 'test', :auto_register => true)
6
+
7
+ should 'login to an xmpp server' do
8
+ @foo.on(:login) do
9
+ @foo.send Jabber::Presence.new
10
+ wake
11
+ end
12
+
13
+ @foo.connect
14
+ wait
15
+
16
+ @foo.should.be.connected?
17
+ end
18
+
19
+ should 'send messages to others' do
20
+ @bar.on(:login) do
21
+ @bar.send Jabber::Presence.new do
22
+ wake
23
+ end
24
+ end
25
+
26
+ received = nil
27
+ @bar.on(:message) do |msg|
28
+ received = msg.first_element_text('//body')
29
+ wake
30
+ end
31
+
32
+ @bar.connect
33
+ wait
34
+
35
+ @foo.send_msg 'bar@localhost', 'hello'
36
+ wait
37
+
38
+ received.should == 'hello'
39
+ end
40
+
41
+ should 'fire disconnect callback and reconnect' do
42
+ user = XMPP4EM::Client.new('user@localhost', 'user', :auto_register => true)
43
+ user.on(:disconnect){ wake }
44
+ user.connect 'localhost', 5333 # invalid port
45
+ wait
46
+
47
+ user.should.not.be.connected?
48
+
49
+ user.instance_variable_get('@callbacks')[:disconnect] = []
50
+ user.connection.port = 5222
51
+ user.on(:login){ wake }
52
+ user.reconnect
53
+ wait
54
+
55
+ user.should.be.connected?
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xmpp4em
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Aman Gupta
13
+ - Kokorin Denis
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-04-21 00:00:00 +04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 12
31
+ - 10
32
+ version: 0.12.10
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: xmpp4r
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 5
45
+ version: "0.5"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ description: Simple XMPP client and component built on EventMachine.
49
+ email: mccoder-nospam@ya.ru
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files: []
55
+
56
+ files:
57
+ - README.md
58
+ - Rakefile
59
+ - History.txt
60
+ - lib/xmpp4em/base_client.rb
61
+ - lib/xmpp4em/base_connection.rb
62
+ - lib/xmpp4em/client.rb
63
+ - lib/xmpp4em/client_connection.rb
64
+ - lib/xmpp4em/component.rb
65
+ - lib/xmpp4em/component_connection.rb
66
+ - lib/xmpp4em.rb
67
+ - examples/component_test.rb
68
+ - examples/stress_test.rb
69
+ - spec/spec_runner.rb
70
+ - spec/xmpp4em_spec.rb
71
+ has_rdoc: true
72
+ homepage: http://github.com/mccoder/xmpp4em
73
+ licenses: []
74
+
75
+ post_install_message:
76
+ rdoc_options: []
77
+
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 1
86
+ - 9
87
+ version: "1.9"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.3.6
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: EventMachine based XMPP client and component
102
+ test_files: []
103
+