xmpp4em 0.2.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.
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
+