em-xmpp 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/em-xmpp.gemspec +20 -0
- data/lib/em-xmpp/cert_store.rb +76 -0
- data/lib/em-xmpp/connection.rb +116 -0
- data/lib/em-xmpp/connector.rb +244 -0
- data/lib/em-xmpp/context.rb +249 -0
- data/lib/em-xmpp/handler.rb +262 -0
- data/lib/em-xmpp/jid.rb +27 -0
- data/lib/em-xmpp/namespaces.rb +31 -0
- data/lib/em-xmpp/nodes.rb +8 -0
- data/lib/em-xmpp/resolver.rb +23 -0
- data/lib/em-xmpp/version.rb +5 -0
- data/lib/em-xmpp.rb +9 -0
- data/samples/echo.rb +40 -0
- metadata +96 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 crapooze
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Em::Xmpp
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'em-xmpp'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install em-xmpp
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/em-xmpp.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/em-xmpp/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["crapooze"]
|
6
|
+
gem.email = ["crapooze@gmail.com"]
|
7
|
+
gem.description = %q{XMPP client for event machine}
|
8
|
+
gem.summary = %q{Easy to write and to extend XMPP client}
|
9
|
+
gem.homepage = "https://github.com/crapooze/em-xmpp"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "em-xmpp"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Em::Xmpp::VERSION
|
17
|
+
gem.add_dependency "eventmachine"
|
18
|
+
gem.add_dependency "nokogiri"
|
19
|
+
gem.add_dependency "ruby-sasl"
|
20
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
=begin
|
2
|
+
The code from this file was copied from Blather:
|
3
|
+
|
4
|
+
|
5
|
+
Copyright (c) 2011 Jeff Smick
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
8
|
+
a copy of this software and associated documentation files (the
|
9
|
+
"Software"), to deal in the Software without restriction, including
|
10
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
11
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
12
|
+
permit persons to whom the Software is furnished to do so, subject to
|
13
|
+
the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be
|
16
|
+
included in all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
21
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
22
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
23
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
24
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
=end
|
26
|
+
|
27
|
+
module EM::Xmpp
|
28
|
+
# An X509 certificate store that validates certificate trust chains.
|
29
|
+
# This uses the #{cert_directory}/*.crt files as the list of trusted root
|
30
|
+
# CA certificates.
|
31
|
+
class CertStore
|
32
|
+
@@certs = nil
|
33
|
+
@cert_directory = nil
|
34
|
+
|
35
|
+
def initialize(cert_directory)
|
36
|
+
@cert_directory = cert_directory
|
37
|
+
@store = OpenSSL::X509::Store.new
|
38
|
+
certs.each {|c| @store.add_cert(c) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return true if the certificate is signed by a CA certificate in the
|
42
|
+
# store. If the certificate can be trusted, it's added to the store so
|
43
|
+
# it can be used to trust other certs.
|
44
|
+
def trusted?(pem)
|
45
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
46
|
+
@store.verify(cert).tap do |trusted|
|
47
|
+
@store.add_cert(cert) if trusted rescue nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return true if the domain name matches one of the names in the
|
53
|
+
# certificate. In other words, is the certificate provided to us really
|
54
|
+
# for the domain to which we think we're connected?
|
55
|
+
def domain?(pem, domain)
|
56
|
+
if cert = OpenSSL::X509::Certificate.new(pem) rescue nil
|
57
|
+
OpenSSL::SSL.verify_certificate_identity(cert, domain) rescue false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return the trusted root CA certificates installed in the @cert_directory. These
|
62
|
+
# certificates are used to start the trust chain needed to validate certs
|
63
|
+
# we receive from clients and servers.
|
64
|
+
def certs
|
65
|
+
unless @@certs
|
66
|
+
pattern = /-{5}BEGIN CERTIFICATE-{5}\n.*?-{5}END CERTIFICATE-{5}\n/m
|
67
|
+
dir = @cert_directory
|
68
|
+
certs = Dir[File.join(dir, '*.crt')].map {|f| File.read(f) }
|
69
|
+
certs = certs.map {|c| c.scan(pattern) }.flatten
|
70
|
+
certs.map! {|c| OpenSSL::X509::Certificate.new(c) }
|
71
|
+
@@certs = certs.reject {|c| c.not_after < Time.now }
|
72
|
+
end
|
73
|
+
@@certs
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
require 'em-xmpp/namespaces'
|
3
|
+
require 'em-xmpp/connector'
|
4
|
+
require 'em-xmpp/handler'
|
5
|
+
require 'em-xmpp/jid'
|
6
|
+
require 'em-xmpp/cert_store'
|
7
|
+
require 'eventmachine'
|
8
|
+
|
9
|
+
module EM::Xmpp
|
10
|
+
class Connection < EM::Connection
|
11
|
+
include Namespaces
|
12
|
+
include Connector
|
13
|
+
|
14
|
+
attr_reader :jid, :pass
|
15
|
+
|
16
|
+
def initialize(jid, pass, mod=nil, cfg={})
|
17
|
+
@jid = jid
|
18
|
+
@pass = pass.dup.freeze
|
19
|
+
self.extend mod if mod
|
20
|
+
certdir = cfg[:certificates]
|
21
|
+
@certstore = if certdir
|
22
|
+
CertStore.new(certdir)
|
23
|
+
else
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def post_init
|
29
|
+
super
|
30
|
+
@handler = StreamNegotiation.new self
|
31
|
+
end
|
32
|
+
|
33
|
+
def stanza_start(node)
|
34
|
+
end
|
35
|
+
|
36
|
+
def stanza_end(node)
|
37
|
+
@handler.handle(node)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unhandled_stanza(node)
|
41
|
+
raise RuntimeError, "did not handle node:\n#{node}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def jid_received(jid)
|
45
|
+
@jid = JID.parse jid
|
46
|
+
end
|
47
|
+
|
48
|
+
def negotiation_finished
|
49
|
+
@pass = nil
|
50
|
+
@handler = Routine.new self
|
51
|
+
send_raw presence_stanza()
|
52
|
+
ready
|
53
|
+
end
|
54
|
+
|
55
|
+
def negotiation_failed(node)
|
56
|
+
raise RuntimeError, "could not negotiate a stream:\n#{node}"
|
57
|
+
end
|
58
|
+
|
59
|
+
#should add 'xml:lang'
|
60
|
+
|
61
|
+
def default_presence_config
|
62
|
+
{}
|
63
|
+
end
|
64
|
+
|
65
|
+
def default_message_config
|
66
|
+
{'to' => @jid.domain, 'id' => "em-xmpp.#{rand(65535)}"}
|
67
|
+
end
|
68
|
+
|
69
|
+
def default_iq_config
|
70
|
+
{'type' => 'get', 'id' => "em-xmpp.#{rand(65535)}"}
|
71
|
+
end
|
72
|
+
|
73
|
+
def presence_stanza(cfg={}, &blk)
|
74
|
+
cfg = default_presence_config.merge(cfg)
|
75
|
+
build_xml do |x|
|
76
|
+
x.presence(cfg, &blk)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def message_stanza(cfg={}, &blk)
|
81
|
+
cfg = default_message_config.merge(cfg)
|
82
|
+
build_xml do |x|
|
83
|
+
x.message(cfg, &blk)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def iq_stanza(cfg={}, &blk)
|
88
|
+
cfg = default_iq_config.merge(cfg)
|
89
|
+
build_xml do |x|
|
90
|
+
x.iq(cfg, &blk)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
%w{on on_exception on_presence on_iq on_message}.each do |meth|
|
95
|
+
define_method(meth) do |*args,&blk|
|
96
|
+
@handler.send meth, *args, &blk
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def ready
|
101
|
+
end
|
102
|
+
|
103
|
+
def start_using_tls_and_reset_stream
|
104
|
+
bool = !! @certstore
|
105
|
+
start_tls(:verify_peer => bool)
|
106
|
+
restart_xml_stream
|
107
|
+
end
|
108
|
+
|
109
|
+
def ssl_verify_peer(pem)
|
110
|
+
@certstore.trusted?(pem).tap do |trusted|
|
111
|
+
close_connection unless trusted
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'em-xmpp/context'
|
5
|
+
require 'em-xmpp/namespaces'
|
6
|
+
require 'em-xmpp/resolver'
|
7
|
+
|
8
|
+
module EM::Xmpp
|
9
|
+
module Connector
|
10
|
+
include Namespaces
|
11
|
+
|
12
|
+
#XML SAX document which delegates its method to a recipient object
|
13
|
+
class ForwardingDocument < Nokogiri::XML::SAX::Document
|
14
|
+
attr_accessor :recipient
|
15
|
+
%w{xmldecl start_document end_document start_element_namespace end_element characters
|
16
|
+
comment warning error cdata_block}.each do |meth|
|
17
|
+
meth2 = "xml_#{meth}"
|
18
|
+
define_method(meth) do |*args|
|
19
|
+
recipient.send(meth2, *args) if recipient
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(obj)
|
25
|
+
obj.extend ClassMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def start(jid, pass=nil, mod=nil, cfg={}, server=nil, port=5222, &blk)
|
30
|
+
jid = JID.parse jid
|
31
|
+
if server.nil?
|
32
|
+
record = Resolver.resolve jid.domain
|
33
|
+
if record
|
34
|
+
server = record.target.to_s
|
35
|
+
port = record.port
|
36
|
+
else
|
37
|
+
server = jid.domain
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
EM.connect(server, port, self, jid, pass, mod, cfg, &blk)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
extend ClassMethods
|
46
|
+
|
47
|
+
def send_raw(data)
|
48
|
+
puts ">> out\n#{data}\n" if $DEBUG
|
49
|
+
send_data data
|
50
|
+
end
|
51
|
+
|
52
|
+
def send_xml(&blk)
|
53
|
+
data = build_xml(&blk)
|
54
|
+
send_raw data
|
55
|
+
end
|
56
|
+
|
57
|
+
def restart_xml_stream
|
58
|
+
@xml_parser.document.recipient = nil
|
59
|
+
post_init
|
60
|
+
end
|
61
|
+
|
62
|
+
def post_init
|
63
|
+
doc = ForwardingDocument.new
|
64
|
+
doc.recipient = self
|
65
|
+
@xml_parser = Nokogiri::XML::SAX::PushParser.new doc
|
66
|
+
@stack = []
|
67
|
+
@stanza = nil
|
68
|
+
@streamdoc = nil
|
69
|
+
|
70
|
+
open_xml_stream
|
71
|
+
end
|
72
|
+
|
73
|
+
def receive_data(dat)
|
74
|
+
puts "<< in\n#{dat}\n" if $DEBUG
|
75
|
+
@xml_parser << dat
|
76
|
+
end
|
77
|
+
|
78
|
+
def unbind
|
79
|
+
puts "**** unbound ****" if $DEBUG
|
80
|
+
end
|
81
|
+
|
82
|
+
def build_xml(&blk)
|
83
|
+
n = Nokogiri::XML::Builder.new(&blk)
|
84
|
+
n.doc.root.to_xml
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def open_xml_stream_tag
|
90
|
+
domain = @jid.domain
|
91
|
+
version = '1.0'
|
92
|
+
lang = 'en'
|
93
|
+
start_stream = <<-STREAM
|
94
|
+
<stream:stream
|
95
|
+
to='#{domain}'
|
96
|
+
version='#{version}'
|
97
|
+
xml:lang='#{lang}'
|
98
|
+
xmlns='#{Client}'
|
99
|
+
xmlns:stream='#{Stream}'
|
100
|
+
>
|
101
|
+
STREAM
|
102
|
+
end
|
103
|
+
|
104
|
+
def close_xml_stream_tag
|
105
|
+
'</stream:stream>'
|
106
|
+
end
|
107
|
+
|
108
|
+
def open_xml_stream
|
109
|
+
send_raw open_xml_stream_tag
|
110
|
+
end
|
111
|
+
|
112
|
+
def close_xml_stream
|
113
|
+
send_raw close_xml_stream_tag
|
114
|
+
end
|
115
|
+
|
116
|
+
### XML world
|
117
|
+
|
118
|
+
def xml_xmldecl(version,encoding,standalone)
|
119
|
+
end
|
120
|
+
|
121
|
+
def xml_start_document
|
122
|
+
#XXX set namespaces and stream prefix
|
123
|
+
# namespace may depend on the type of connection ('jabber:client' or
|
124
|
+
# 'jabber:server')
|
125
|
+
# currently we do not set any stream's namespace, hence when builidng stanza,
|
126
|
+
# we must explicitely avoid writing the namespace of iq/presence/message XML nodes
|
127
|
+
@streamdoc = Nokogiri::XML::Document.new
|
128
|
+
end
|
129
|
+
|
130
|
+
def xml_end_document
|
131
|
+
@streamdoc = @stanza = @stack = @xml_parser = nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def xml_start_element_namespace(name, attrs=[],prefix=nil,uri=nil,ns=[])
|
135
|
+
node = Nokogiri::XML::Node.new(name, @streamdoc)
|
136
|
+
attrs.each do |attr|
|
137
|
+
#attr is a Struct with members localname/prefix/uri/value
|
138
|
+
node[attr.localname] = attr.value
|
139
|
+
end
|
140
|
+
#XXX - if prefix is there maybe we do not want to set uri as default
|
141
|
+
node.default_namespace = uri if uri
|
142
|
+
|
143
|
+
ns.each do |pfx,href|
|
144
|
+
node.add_namespace_definition pfx, href
|
145
|
+
end
|
146
|
+
|
147
|
+
puts "starting: #{name}, stack:#{@stack.size}" if $DEBUG
|
148
|
+
case @stack.size
|
149
|
+
when 0 #the streaming tag starts
|
150
|
+
stream_support(node)
|
151
|
+
when 1 #a stanza starts
|
152
|
+
set_current_stanza!(node)
|
153
|
+
stanza_start node
|
154
|
+
else
|
155
|
+
@stack.last.add_child node
|
156
|
+
end
|
157
|
+
|
158
|
+
@stack << node
|
159
|
+
end
|
160
|
+
|
161
|
+
def xml_end_element(name)
|
162
|
+
node = @stack.pop
|
163
|
+
puts "ending: #{name}, stack:#{@stack.size}" if $DEBUG
|
164
|
+
|
165
|
+
case @stack.size
|
166
|
+
when 0 #i.e., the stream support ends
|
167
|
+
xml_stream_closing
|
168
|
+
when 1 #i.e., we've finished a stanza
|
169
|
+
raise RuntimeError, "should end on a stanza" unless node == @stanza
|
170
|
+
stanza_end node
|
171
|
+
else
|
172
|
+
#the stanza keeps growing
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def xml_characters(txt)
|
177
|
+
@stack.last << Nokogiri::XML::Text.new(txt, @streamdoc)
|
178
|
+
end
|
179
|
+
|
180
|
+
def xml_error(err)
|
181
|
+
#raise RuntimeError, err
|
182
|
+
end
|
183
|
+
|
184
|
+
def xml_stream_closing
|
185
|
+
close_xml_stream
|
186
|
+
close_connection
|
187
|
+
end
|
188
|
+
|
189
|
+
def xml_comment(comm)
|
190
|
+
raise NotImplementedError
|
191
|
+
end
|
192
|
+
|
193
|
+
def xml_warning(warn)
|
194
|
+
raise NotImplementedError
|
195
|
+
end
|
196
|
+
|
197
|
+
def xml_cdata_block(data)
|
198
|
+
raise NotImplementedError
|
199
|
+
end
|
200
|
+
|
201
|
+
### XMPP World
|
202
|
+
|
203
|
+
def stream_support(node)
|
204
|
+
@stanza = Nokogiri::XML::Node.new('dummy', @streamdoc)
|
205
|
+
node << @stanza
|
206
|
+
|
207
|
+
@streamdoc.root = node
|
208
|
+
end
|
209
|
+
|
210
|
+
def set_current_stanza!(node)
|
211
|
+
@stanza.remove
|
212
|
+
|
213
|
+
@stanza = node
|
214
|
+
@streamdoc.root << @stanza
|
215
|
+
end
|
216
|
+
|
217
|
+
def stanza_start(node)
|
218
|
+
raise NotImplementedError
|
219
|
+
end
|
220
|
+
|
221
|
+
def stanza_end(node)
|
222
|
+
raise NotImplementedError
|
223
|
+
end
|
224
|
+
|
225
|
+
public
|
226
|
+
|
227
|
+
### TLS World
|
228
|
+
|
229
|
+
def ask_for_tls
|
230
|
+
send_xml do |x|
|
231
|
+
x.starttls(:xmlns => TLS)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def start_using_tls_and_reset_stream
|
236
|
+
start_tls(:verify_peer => false)
|
237
|
+
restart_xml_stream
|
238
|
+
end
|
239
|
+
|
240
|
+
def ssl_verify_peer(pem)
|
241
|
+
raise NotImplementedError
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
|
2
|
+
require 'em-xmpp/jid'
|
3
|
+
require 'em-xmpp/namespaces'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module EM::Xmpp
|
7
|
+
class Context
|
8
|
+
attr_reader :connection, :stanza, :env
|
9
|
+
|
10
|
+
def []key
|
11
|
+
env[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=key,val
|
15
|
+
env[key]= val
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_env
|
19
|
+
{'modules' => [], 'done' => false}
|
20
|
+
end
|
21
|
+
|
22
|
+
def done!
|
23
|
+
env['ctx.done'] = true
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def done?
|
28
|
+
env['ctx.done']
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete_xpath_handler!
|
32
|
+
env.delete 'xpath.handler'
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def reuse_handler?
|
37
|
+
env['xpath.handler']
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(conn, stanza, env={})
|
41
|
+
@connection = conn
|
42
|
+
@stanza = stanza
|
43
|
+
@env = default_env.merge env
|
44
|
+
end
|
45
|
+
|
46
|
+
def xpath(path, args={})
|
47
|
+
stanza.xpath(path, args) || []
|
48
|
+
end
|
49
|
+
|
50
|
+
def xpath?(path, args={})
|
51
|
+
xpath(path, args).any?
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_attr(node, name, sym=:to_s, &blk)
|
55
|
+
val = node[name]
|
56
|
+
if val
|
57
|
+
if blk
|
58
|
+
blk.call val
|
59
|
+
else
|
60
|
+
val.send sym
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def with(modname)
|
66
|
+
mod = if modname.is_a?(Module)
|
67
|
+
modname
|
68
|
+
else
|
69
|
+
self.class.const_get(modname.to_s.capitalize)
|
70
|
+
end
|
71
|
+
env['modules'] << mod
|
72
|
+
obj = self
|
73
|
+
obj.extend mod
|
74
|
+
obj
|
75
|
+
end
|
76
|
+
|
77
|
+
module Stanza
|
78
|
+
include Namespaces
|
79
|
+
%w{type id lang}.each do |w|
|
80
|
+
define_method w do
|
81
|
+
read_attr stanza, w
|
82
|
+
end
|
83
|
+
end
|
84
|
+
def to
|
85
|
+
read_attr(stanza, 'to'){|j| JID.parse(j)}
|
86
|
+
end
|
87
|
+
def from
|
88
|
+
read_attr(stanza, 'from'){|j| JID.parse(j)}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module Presence
|
93
|
+
include Stanza
|
94
|
+
def reply_default_params
|
95
|
+
jid = @connection.jid.full
|
96
|
+
{'from' => jid, 'to' => from, 'id' => id}
|
97
|
+
end
|
98
|
+
def reply(args={},&blk)
|
99
|
+
args = reply_default_params.merge args
|
100
|
+
@connection.presence_stanza(args,&blk)
|
101
|
+
end
|
102
|
+
def priority_node
|
103
|
+
xpath('//xmlns:priority',{'xmlns' => Client}).first
|
104
|
+
end
|
105
|
+
def status_node
|
106
|
+
xpath('//xmlns:status',{'xmlns' => Client}).first
|
107
|
+
end
|
108
|
+
def show_node
|
109
|
+
xpath('//xmlns:show',{'xmlns' => Client}).first
|
110
|
+
end
|
111
|
+
def priority
|
112
|
+
node = priority_node
|
113
|
+
node.content if node
|
114
|
+
end
|
115
|
+
def status
|
116
|
+
node = status_node
|
117
|
+
node.content if node
|
118
|
+
end
|
119
|
+
def show
|
120
|
+
node = show_node
|
121
|
+
node.content if node
|
122
|
+
end
|
123
|
+
def subscription_request?
|
124
|
+
type == 'subscribe'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module Message
|
129
|
+
include Stanza
|
130
|
+
def subject_node
|
131
|
+
xpath('//xmlns:subject',{'xmlns' => Client}).first
|
132
|
+
end
|
133
|
+
|
134
|
+
def subject
|
135
|
+
node = subject_node
|
136
|
+
node.text if node
|
137
|
+
end
|
138
|
+
|
139
|
+
def body_node
|
140
|
+
xpath('//xmlns:body',{'xmlns' => Client}).first
|
141
|
+
end
|
142
|
+
|
143
|
+
def body
|
144
|
+
node = body_node
|
145
|
+
node.text if node
|
146
|
+
end
|
147
|
+
|
148
|
+
def reply_default_params
|
149
|
+
h = {'to' => from, 'type' => type}
|
150
|
+
h['id'] = id.succ if id
|
151
|
+
h
|
152
|
+
end
|
153
|
+
|
154
|
+
def reply(args={},&blk)
|
155
|
+
args = reply_default_params.merge args
|
156
|
+
@connection.message_stanza(args,&blk)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
module Iq
|
161
|
+
include Stanza
|
162
|
+
def reply_default_params
|
163
|
+
jid = @connection.jid.full
|
164
|
+
{'from' => jid, 'to' => from, 'type' => 'result', 'id' => id}
|
165
|
+
end
|
166
|
+
|
167
|
+
def reply(args={},&blk)
|
168
|
+
args = reply_default_params.merge args
|
169
|
+
@connection.iq_stanza(args,&blk)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
module Delay
|
174
|
+
#does not handle legacy delay
|
175
|
+
def delay_node
|
176
|
+
xpath('//xmlns:delay',{'xmlns' => EM::Xmpp::Namespaces::Delay}).first
|
177
|
+
end
|
178
|
+
def stamp
|
179
|
+
n = delay_node
|
180
|
+
Time.parse read_attr(n, 'stamp') if n
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
module Discoveries
|
185
|
+
include Iq
|
186
|
+
%w{node}.each do |word|
|
187
|
+
define_method word do
|
188
|
+
n = query_node
|
189
|
+
read_attr(n, word) if n
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
module Discoinfos
|
195
|
+
include Discoveries
|
196
|
+
def query_node
|
197
|
+
xpath('//xmlns:query',{'xmlns' => DiscoverInfos}).first
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
module Discoitems
|
202
|
+
include Discoveries
|
203
|
+
def query_node
|
204
|
+
xpath('//xmlns:query',{'xmlns' => DiscoverItems}).first
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
module Command
|
209
|
+
def command_node
|
210
|
+
xpath('//xmlns:command',{'xmlns' => Commands}).first
|
211
|
+
end
|
212
|
+
|
213
|
+
%w{node sessionid action}.each do |word|
|
214
|
+
define_method word do
|
215
|
+
n = command_node
|
216
|
+
read_attr(n, word) if n
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def previous?
|
221
|
+
action == 'prev'
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
module Dataforms
|
226
|
+
def x_node
|
227
|
+
xpath('//xmlns:x',{'xmlns' => DataForms}).first
|
228
|
+
end
|
229
|
+
|
230
|
+
def x_type
|
231
|
+
n = x_node
|
232
|
+
read_attr(n, 'type') if n
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
module Capabilities
|
237
|
+
def c_node
|
238
|
+
xpath('//xmlns:c',{'xmlns' => EM::Xmpp::Namespaces::Capabilities}).first
|
239
|
+
end
|
240
|
+
|
241
|
+
%w{node ver ext}.each do |word|
|
242
|
+
define_method word do
|
243
|
+
n = c_node
|
244
|
+
read_attr(n, word) if n
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
|
2
|
+
require 'em-xmpp/namespaces'
|
3
|
+
require 'em-xmpp/context'
|
4
|
+
require 'base64'
|
5
|
+
require 'sasl/base'
|
6
|
+
require 'sasl'
|
7
|
+
|
8
|
+
module EM::Xmpp
|
9
|
+
class Handler
|
10
|
+
include Namespaces
|
11
|
+
|
12
|
+
Xpath = Struct.new(:path, :args, :blk) do
|
13
|
+
def match?(xml)
|
14
|
+
path == :anything or xml.xpath(path, args).any?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(conn)
|
19
|
+
@connection = conn
|
20
|
+
@xpaths = []
|
21
|
+
@exception_xpaths = []
|
22
|
+
|
23
|
+
stack_decorators
|
24
|
+
end
|
25
|
+
|
26
|
+
def on_presence(&blk)
|
27
|
+
on('//xmlns:presence', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
28
|
+
end
|
29
|
+
|
30
|
+
def on_message(&blk)
|
31
|
+
on('//xmlns:message', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_iq(&blk)
|
35
|
+
on('//xmlns:iq', 'xmlns' => EM::Xmpp::Namespaces::Client, &blk)
|
36
|
+
end
|
37
|
+
|
38
|
+
def stack_decorators
|
39
|
+
on_presence { |ctx| ctx.with(:presence) }
|
40
|
+
on_message { |ctx| ctx.with(:message) }
|
41
|
+
on_iq { |ctx| ctx.with(:iq) }
|
42
|
+
on('//xmlns:delay', 'xmlns' => Delay) do |ctx|
|
43
|
+
ctx.with(:delay)
|
44
|
+
end
|
45
|
+
on('//xmlns:query', 'xmlns' => DiscoverInfos) do |ctx|
|
46
|
+
ctx.with(:discoinfos)
|
47
|
+
end
|
48
|
+
on('//xmlns:query', 'xmlns' => DiscoverItems) do |ctx|
|
49
|
+
ctx.with(:discoitems)
|
50
|
+
end
|
51
|
+
on('//xmlns:command', 'xmlns' => Commands) do |ctx|
|
52
|
+
ctx.with(:command)
|
53
|
+
end
|
54
|
+
on('//xmlns:x', 'xmlns' => DataForms) do |ctx|
|
55
|
+
ctx.with(:dataforms)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def on(path, args={}, &blk)
|
60
|
+
@xpaths << Xpath.new(path, args, blk)
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_exception(path, args={}, &blk)
|
64
|
+
@exception_xpaths << Xpath.new(path, args, blk)
|
65
|
+
end
|
66
|
+
|
67
|
+
# wraps the stanza in a context and calls handle_context
|
68
|
+
def handle(stanza)
|
69
|
+
handle_context Context.new(@connection, stanza)
|
70
|
+
end
|
71
|
+
|
72
|
+
# runs all xpath_handlers against the stanza context
|
73
|
+
# catches all exception (in which case, the context gets passed to all
|
74
|
+
# exception_xpaths)
|
75
|
+
#
|
76
|
+
# an xpath handler can:
|
77
|
+
# - throw :halt to shortcircuit everything
|
78
|
+
# - set the context to "done!" to avoid invoking handlers
|
79
|
+
# - delete_xpath_handler from the history, this is useful in one-time
|
80
|
+
# handlers such as request/responses
|
81
|
+
def handle_context(ctx)
|
82
|
+
catch :halt do
|
83
|
+
@xpaths = run_xpath_handlers ctx, @xpaths
|
84
|
+
end
|
85
|
+
rescue => err
|
86
|
+
ctx['error'] = err
|
87
|
+
@exception_xpaths = run_xpath_handlers ctx, @exception_xpaths
|
88
|
+
end
|
89
|
+
|
90
|
+
# runs all handlers and returns a list of handlers for the next stanza
|
91
|
+
def run_xpath_handlers(ctx, handlers)
|
92
|
+
handlers.map do |x|
|
93
|
+
if (not ctx.done?) and (x.match?(ctx.stanza))
|
94
|
+
ctx['xpath.handler'] = x
|
95
|
+
ctx = x.blk.call(ctx)
|
96
|
+
raise RuntimeError, "xpath handlers should return a Context" unless ctx.is_a?(Context)
|
97
|
+
|
98
|
+
x if ctx.reuse_handler?
|
99
|
+
else
|
100
|
+
x
|
101
|
+
end
|
102
|
+
end.compact
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class XmppSASL < ::SASL::Preferences
|
107
|
+
attr_accessor :handler
|
108
|
+
def initialize(handler)
|
109
|
+
@handler = handler
|
110
|
+
end
|
111
|
+
def realm
|
112
|
+
handler.jid.domain
|
113
|
+
end
|
114
|
+
def digest_uri
|
115
|
+
'xmpp/' + handler.jid.domain
|
116
|
+
end
|
117
|
+
def username
|
118
|
+
handler.jid.node
|
119
|
+
end
|
120
|
+
def has_password?
|
121
|
+
true
|
122
|
+
end
|
123
|
+
def password
|
124
|
+
ret = handler.pass
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class Routine < Handler
|
129
|
+
end
|
130
|
+
|
131
|
+
class StreamNegotiation < Handler
|
132
|
+
attr_reader :sasl
|
133
|
+
|
134
|
+
def initialize(conn)
|
135
|
+
super conn
|
136
|
+
@sasl = nil
|
137
|
+
setup_handlers
|
138
|
+
end
|
139
|
+
|
140
|
+
def c
|
141
|
+
@connection
|
142
|
+
end
|
143
|
+
|
144
|
+
def jid
|
145
|
+
@connection.jid
|
146
|
+
end
|
147
|
+
|
148
|
+
def pass
|
149
|
+
@connection.pass
|
150
|
+
end
|
151
|
+
|
152
|
+
def setup_handlers
|
153
|
+
on('//xmlns:starttls', {'xmlns' => TLS}) do |ctx|
|
154
|
+
@connection.ask_for_tls
|
155
|
+
ctx.delete_xpath_handler!.done!
|
156
|
+
end
|
157
|
+
|
158
|
+
on('//xmlns:proceed', {'xmlns' => TLS }) do |ctx|
|
159
|
+
@connection.start_using_tls_and_reset_stream
|
160
|
+
ctx.delete_xpath_handler!.done!
|
161
|
+
end
|
162
|
+
|
163
|
+
on('//xmlns:mechanisms', {'xmlns' => SASL}) do |ctx|
|
164
|
+
search = ctx.xpath('//xmlns:mechanisms', {'xmlns' => SASL})
|
165
|
+
if search.first
|
166
|
+
mechanisms = search.first.children.map(&:content)
|
167
|
+
start_sasl mechanisms
|
168
|
+
ctx.delete_xpath_handler!.done!
|
169
|
+
else
|
170
|
+
raise RuntimeError, "how come there is no mechanism node?"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
on('//xmlns:challenge', {'xmlns' => SASL}) do |ctx|
|
175
|
+
sasl_step ctx.stanza
|
176
|
+
ctx.done!
|
177
|
+
end
|
178
|
+
|
179
|
+
on('//xmlns:success', {'xmlns' => SASL}) do |ctx|
|
180
|
+
@connection.restart_xml_stream
|
181
|
+
ctx.delete_xpath_handler!.done!
|
182
|
+
end
|
183
|
+
|
184
|
+
on('//xmlns:bind', {'xmlns' => Bind}) do |ctx|
|
185
|
+
bind_to_resource
|
186
|
+
ctx.delete_xpath_handler!.done!
|
187
|
+
end
|
188
|
+
|
189
|
+
on('//xmlns:bind', {'xmlns' => Bind}) do |ctx|
|
190
|
+
jid = extract_jid ctx.stanza
|
191
|
+
|
192
|
+
if jid
|
193
|
+
@connection.jid_received jid
|
194
|
+
start_session
|
195
|
+
else
|
196
|
+
raise RuntimeError, "no jid despite binding"
|
197
|
+
end
|
198
|
+
|
199
|
+
ctx.delete_xpath_handler!.done!
|
200
|
+
end
|
201
|
+
|
202
|
+
on('//xmlns:session', {'xmlns' => Session}) do |ctx|
|
203
|
+
@connection.negotiation_finished
|
204
|
+
ctx.delete_xpath_handler!.done!
|
205
|
+
end
|
206
|
+
|
207
|
+
on('//xmlns:failure', {'xmlns' => SASL}) do |ctx|
|
208
|
+
@connection.negotiation_failed(ctx.stanza)
|
209
|
+
ctx.done!
|
210
|
+
end
|
211
|
+
|
212
|
+
on(:anything) do |ctx|
|
213
|
+
@connection.unhandled ctx.stanza
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def extract_jid(stanza)
|
218
|
+
jid = stanza.xpath('//bind:jid', {'bind' => Bind})
|
219
|
+
jid.text if jid.any?
|
220
|
+
end
|
221
|
+
|
222
|
+
def bind_to_resource(wanted_res=nil)
|
223
|
+
c.send_raw(c.iq_stanza('type' => 'set') do |x|
|
224
|
+
x.bind('xmlns' => Bind) do |y|
|
225
|
+
y.resource(wanted_res) if wanted_res
|
226
|
+
end
|
227
|
+
end)
|
228
|
+
end
|
229
|
+
|
230
|
+
def start_session
|
231
|
+
c.send_raw(c.iq_stanza('type' => 'set', 'to' => jid.domain) do |x|
|
232
|
+
x.session('xmlns' => Session)
|
233
|
+
end)
|
234
|
+
end
|
235
|
+
|
236
|
+
def start_sasl(methods)
|
237
|
+
@sasl = ::SASL.new(methods, XmppSASL.new(self))
|
238
|
+
msg,val = sasl.start
|
239
|
+
mech = sasl.mechanism
|
240
|
+
reply_sasl(msg,val,mech)
|
241
|
+
end
|
242
|
+
|
243
|
+
def sasl_step(stanza)
|
244
|
+
msg = stanza.name
|
245
|
+
inStr = Base64.strict_decode64(stanza.text)
|
246
|
+
meth,str = sasl.receive msg, inStr
|
247
|
+
b64 = str ? Base64.strict_encode64(str) : ''
|
248
|
+
reply_sasl(meth, b64)
|
249
|
+
end
|
250
|
+
|
251
|
+
def reply_sasl(msg, val=nil, mech=nil)
|
252
|
+
c.send_xml do |x|
|
253
|
+
if val
|
254
|
+
x.send(msg, val, {'xmlns' => SASL, 'mechanism' => mech})
|
255
|
+
else
|
256
|
+
x.send(msg, {'xmlns' => SASL, 'mechanism' => mech})
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|
262
|
+
end
|
data/lib/em-xmpp/jid.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module EM::Xmpp
|
4
|
+
JID = Struct.new(:node, :domain, :resource) do
|
5
|
+
def self.parse(str)
|
6
|
+
s1,s2 = str.split('@',2)
|
7
|
+
if s2.nil?
|
8
|
+
self.new(nil, s1, nil)
|
9
|
+
else
|
10
|
+
s2,s3 = s2.split('/',2)
|
11
|
+
self.new(s1,s2,s3)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def bare
|
16
|
+
[node,domain].map(&:to_s).join('@')
|
17
|
+
end
|
18
|
+
|
19
|
+
def full
|
20
|
+
[bare,resource].map(&:to_s).join('/')
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
full
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module EM::Xmpp
|
3
|
+
# A handy module with the XMPP namespaces name.
|
4
|
+
module Namespaces
|
5
|
+
Client = 'jabber:client'
|
6
|
+
#In-band registration
|
7
|
+
Registration = 'http://jabber.org/features/iq-register'
|
8
|
+
#XMPP stream-level stanza
|
9
|
+
Stream = 'http://etherx.jabber.org/streams'
|
10
|
+
#TLS negotiation
|
11
|
+
TLS = 'urn:ietf:params:xml:ns:xmpp-tls'
|
12
|
+
#SASL authentication
|
13
|
+
SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
14
|
+
#XMPP resource binding
|
15
|
+
Bind = 'urn:ietf:params:xml:ns:xmpp-bind'
|
16
|
+
#XMPP session creation
|
17
|
+
Session = 'urn:ietf:params:xml:ns:xmpp-session'
|
18
|
+
#XMPP capabilities discovery
|
19
|
+
Capabilities = "http://jabber.org/protocol/caps"
|
20
|
+
#XMPP item discovery
|
21
|
+
DiscoverItems = "http://jabber.org/protocol/disco#items"
|
22
|
+
#XMPP info discovery
|
23
|
+
DiscoverInfos = "http://jabber.org/protocol/disco#infos"
|
24
|
+
#XMPP commands
|
25
|
+
Commands = "http://jabber.org/protocol/commands"
|
26
|
+
#XMPP delayed delivery
|
27
|
+
Delay = "urn:xmpp:delay"
|
28
|
+
#Jabber Data forms
|
29
|
+
DataForms = 'jabber:x:data'
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
require 'resolv'
|
3
|
+
module EM::Xmpp
|
4
|
+
module Resolver
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def resolve(domain)
|
8
|
+
resolve_all(domain).first
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve_all(domain)
|
12
|
+
srv = []
|
13
|
+
Resolv::DNS.open do |dns|
|
14
|
+
record = "_xmpp-client._tcp.#{domain}"
|
15
|
+
srv = dns.getresources(record, Resolv::DNS::Resource::IN::SRV)
|
16
|
+
end
|
17
|
+
|
18
|
+
srv.sort do |a,b|
|
19
|
+
(a.priority != b.priority) ? (a.priority <=> b.priority) : (b.weight <=> a.weight)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/em-xmpp.rb
ADDED
data/samples/echo.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
$LOAD_PATH.unshift './lib'
|
3
|
+
require 'em-xmpp'
|
4
|
+
require 'em-xmpp/namespaces'
|
5
|
+
require 'em-xmpp/nodes'
|
6
|
+
|
7
|
+
if ARGV.empty?
|
8
|
+
puts "usage: #{__FILE__} <jid> [<pass>]"
|
9
|
+
exit 0
|
10
|
+
end
|
11
|
+
|
12
|
+
jid = ARGV.first
|
13
|
+
pass = ARGV[1]
|
14
|
+
|
15
|
+
include EM::Xmpp::Namespaces
|
16
|
+
include EM::Xmpp::Nodes
|
17
|
+
|
18
|
+
module MyClient
|
19
|
+
def ready
|
20
|
+
puts "***** #{@jid} ready for #{self}"
|
21
|
+
|
22
|
+
on_presence do |s|
|
23
|
+
p "*presence> #{s.from} #{s.show} (#{s.status})"
|
24
|
+
send_raw(s.reply('type'=>'subscribed')) if s.subscription_request?
|
25
|
+
s
|
26
|
+
end
|
27
|
+
|
28
|
+
on_message do |s|
|
29
|
+
p "*message> #{s.from}\n#{s.body}\n"
|
30
|
+
send_raw(s.reply do |x|
|
31
|
+
x.body "you sent:#{s.body}"
|
32
|
+
end)
|
33
|
+
s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
EM.run do
|
39
|
+
EM::Xmpp::Connection.start(jid, pass, MyClient)
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-xmpp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- crapooze
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-27 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &2153101140 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153101140
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: nokogiri
|
27
|
+
requirement: &2153100720 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2153100720
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: ruby-sasl
|
38
|
+
requirement: &2153100300 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2153100300
|
47
|
+
description: XMPP client for event machine
|
48
|
+
email:
|
49
|
+
- crapooze@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE
|
57
|
+
- README.md
|
58
|
+
- Rakefile
|
59
|
+
- em-xmpp.gemspec
|
60
|
+
- lib/em-xmpp.rb
|
61
|
+
- lib/em-xmpp/cert_store.rb
|
62
|
+
- lib/em-xmpp/connection.rb
|
63
|
+
- lib/em-xmpp/connector.rb
|
64
|
+
- lib/em-xmpp/context.rb
|
65
|
+
- lib/em-xmpp/handler.rb
|
66
|
+
- lib/em-xmpp/jid.rb
|
67
|
+
- lib/em-xmpp/namespaces.rb
|
68
|
+
- lib/em-xmpp/nodes.rb
|
69
|
+
- lib/em-xmpp/resolver.rb
|
70
|
+
- lib/em-xmpp/version.rb
|
71
|
+
- samples/echo.rb
|
72
|
+
homepage: https://github.com/crapooze/em-xmpp
|
73
|
+
licenses: []
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 1.8.6
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Easy to write and to extend XMPP client
|
96
|
+
test_files: []
|