cancer 0.1.0.a1
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/.autotest +3 -0
- data/.gitignore +5 -0
- data/LICENSE.txt +22 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/cancer.gemspec +156 -0
- data/documentation/STREAM_INITIATION.markdown +18 -0
- data/examples/example.rb +80 -0
- data/examples/example_2.rb +20 -0
- data/examples/example_em.rb +11 -0
- data/examples/example_em_helper.rb +23 -0
- data/examples/example_im_roster.rb +26 -0
- data/examples/example_xep_0004.rb +124 -0
- data/examples/example_xep_0047.rb +35 -0
- data/examples/example_xep_0050.rb +78 -0
- data/examples/example_xep_0065.rb +66 -0
- data/examples/example_xep_0096.dup.rb +40 -0
- data/examples/example_xep_0096_xep_0047.rb +42 -0
- data/examples/example_xep_0096_xep_0065.rb +40 -0
- data/lib/cancer.rb +122 -0
- data/lib/cancer/adapter.rb +33 -0
- data/lib/cancer/adapters/em.rb +88 -0
- data/lib/cancer/adapters/socket.rb +122 -0
- data/lib/cancer/builder.rb +28 -0
- data/lib/cancer/controller.rb +32 -0
- data/lib/cancer/dependencies.rb +187 -0
- data/lib/cancer/events/abstract_event.rb +10 -0
- data/lib/cancer/events/base_matchers.rb +63 -0
- data/lib/cancer/events/binary_matchers.rb +30 -0
- data/lib/cancer/events/eventable.rb +92 -0
- data/lib/cancer/events/exception_events.rb +28 -0
- data/lib/cancer/events/handler.rb +33 -0
- data/lib/cancer/events/manager.rb +130 -0
- data/lib/cancer/events/named_events.rb +25 -0
- data/lib/cancer/events/proc_matcher.rb +16 -0
- data/lib/cancer/events/xml_events.rb +44 -0
- data/lib/cancer/exceptions.rb +53 -0
- data/lib/cancer/jid.rb +215 -0
- data/lib/cancer/lock.rb +32 -0
- data/lib/cancer/spec.rb +35 -0
- data/lib/cancer/spec/error.rb +18 -0
- data/lib/cancer/spec/extentions.rb +79 -0
- data/lib/cancer/spec/matcher.rb +30 -0
- data/lib/cancer/spec/mock_adapter.rb +139 -0
- data/lib/cancer/spec/mock_stream.rb +15 -0
- data/lib/cancer/spec/transcript.rb +107 -0
- data/lib/cancer/stream.rb +182 -0
- data/lib/cancer/stream/builder.rb +20 -0
- data/lib/cancer/stream/controller.rb +36 -0
- data/lib/cancer/stream/event_helper.rb +12 -0
- data/lib/cancer/stream/xep.rb +52 -0
- data/lib/cancer/stream_parser.rb +144 -0
- data/lib/cancer/support.rb +27 -0
- data/lib/cancer/support/hash.rb +32 -0
- data/lib/cancer/support/string.rb +22 -0
- data/lib/cancer/synchronized_stanza.rb +79 -0
- data/lib/cancer/thread_pool.rb +118 -0
- data/lib/cancer/xep.rb +43 -0
- data/lib/cancer/xeps/core.rb +109 -0
- data/lib/cancer/xeps/core/bind.rb +33 -0
- data/lib/cancer/xeps/core/sasl.rb +113 -0
- data/lib/cancer/xeps/core/session.rb +22 -0
- data/lib/cancer/xeps/core/stream.rb +18 -0
- data/lib/cancer/xeps/core/terminator.rb +21 -0
- data/lib/cancer/xeps/core/tls.rb +34 -0
- data/lib/cancer/xeps/im.rb +323 -0
- data/lib/cancer/xeps/xep_0004_x_data.rb +692 -0
- data/lib/cancer/xeps/xep_0020_feature_neg.rb +35 -0
- data/lib/cancer/xeps/xep_0030_disco.rb +167 -0
- data/lib/cancer/xeps/xep_0047_ibb.rb +322 -0
- data/lib/cancer/xeps/xep_0050_commands.rb +256 -0
- data/lib/cancer/xeps/xep_0065_bytestreams.rb +306 -0
- data/lib/cancer/xeps/xep_0066_oob.rb +69 -0
- data/lib/cancer/xeps/xep_0095_si.rb +211 -0
- data/lib/cancer/xeps/xep_0096_si_filetransfer.rb +173 -0
- data/lib/cancer/xeps/xep_0114_component.rb +73 -0
- data/lib/cancer/xeps/xep_0115_caps.rb +180 -0
- data/lib/cancer/xeps/xep_0138_compress.rb +134 -0
- data/lib/cancer/xeps/xep_0144_rosterx.rb +250 -0
- data/lib/cancer/xeps/xep_0184_receipts.rb +40 -0
- data/lib/cancer/xeps/xep_0199_ping.rb +41 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/stream/stanza_errors_spec.rb +47 -0
- data/spec/stream/stream_errors_spec.rb +38 -0
- data/spec/stream/stream_initialization_spec.rb +160 -0
- data/spec/xep_0050/local_spec.rb +165 -0
- data/spec/xep_0050/remote_spec.rb +44 -0
- metadata +200 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
# Jabber Component Protocol
|
4
|
+
# http://xmpp.org/extensions/xep-0114.html
|
5
|
+
module XEP_0114
|
6
|
+
include Cancer::XEP
|
7
|
+
|
8
|
+
dependency 'core'
|
9
|
+
dependency 'xep-0138', :weak => true
|
10
|
+
|
11
|
+
def self.enhance_stream(stream)
|
12
|
+
stream.extend_stream do
|
13
|
+
include Cancer::XEP_0114::Helpers
|
14
|
+
|
15
|
+
alias_method_chain :send_stream_header, :xep_0114
|
16
|
+
alias_method_chain :process_send_data, :xep_0114
|
17
|
+
alias_method_chain :process_received_data, :xep_0114
|
18
|
+
alias_method_chain :resolve_options, :xep_0114
|
19
|
+
end
|
20
|
+
|
21
|
+
stream.install_controller(Cancer::XEP_0114::Controller)
|
22
|
+
end
|
23
|
+
|
24
|
+
module Helpers
|
25
|
+
def send_stream_header_with_xep_0114
|
26
|
+
send %(<stream:stream xmlns="jabber:component:accept" xmlns:stream="#{STREAM_NS}" to="#{options[:jid].bare_jid}" version="1.0">)
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_send_data_with_xep_0114(data)
|
30
|
+
data = data.to_s
|
31
|
+
data.gsub!("jabber:client", "jabber:component:accept")
|
32
|
+
process_send_data_without_xep_0114(data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_received_data_with_xep_0114(data)
|
36
|
+
data = process_received_data_without_xep_0114(data)
|
37
|
+
data.gsub!("jabber:component:accept", "jabber:client") if data
|
38
|
+
data
|
39
|
+
end
|
40
|
+
|
41
|
+
def resolve_options_with_xep_0114(options)
|
42
|
+
options[:default_port] ||= 5347
|
43
|
+
options[:username] ||= options[:jid].bare_jid.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Controller < Cancer::Controller
|
48
|
+
|
49
|
+
priority 300
|
50
|
+
|
51
|
+
on { |e| e.xpath('/n:stream', 'n' => STREAM_NS) }
|
52
|
+
def send_handshake(e)
|
53
|
+
send do |x|
|
54
|
+
x.handshake { x.text Digest::SHA1.hexdigest(options[:session_id]+options[:password]) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# we use jabber:client here because we replace jabber:component:accept with jabber:client
|
59
|
+
on { |e| e.xpath('/c:handshake', 'c' => "jabber:client") }
|
60
|
+
def handshake_was_accespted
|
61
|
+
fire! Cancer::Events::NamedEvent, :initialized
|
62
|
+
initialization_completed!
|
63
|
+
end
|
64
|
+
|
65
|
+
on { |e| e.name(:initialized) }
|
66
|
+
def unregister_ourself
|
67
|
+
unregister!
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Cancer
|
6
|
+
# Entity Capabilities
|
7
|
+
# http://xmpp.org/extensions/xep-0115.html
|
8
|
+
module XEP_0115
|
9
|
+
include Cancer::XEP
|
10
|
+
|
11
|
+
dependency 'core'
|
12
|
+
dependency 'im'
|
13
|
+
dependency 'xep-0030'
|
14
|
+
|
15
|
+
NS = 'http://jabber.org/protocol/caps'
|
16
|
+
NAMESPACES = { 'c' => CLIENT_NS, 'caps' => NS, 'disco' => Cancer::XEP_0030::INFO_NS }
|
17
|
+
|
18
|
+
def self.enhance_stream(stream)
|
19
|
+
stream.extend_stream do
|
20
|
+
include Cancer::XEP_0115::StreamHelpers
|
21
|
+
end
|
22
|
+
stream.install_controller Cancer::XEP_0115::Controller
|
23
|
+
stream.disco.feature NS
|
24
|
+
|
25
|
+
stream.presence_enhancer_for :status do |x|
|
26
|
+
controller = self.controllers_by_name['Cancer::XEP_0115::Controller']
|
27
|
+
controller.cache_disco_data(self.disco)
|
28
|
+
x.c(:xmlns => NS, :hash => 'sha-1', :node => CLIENT,
|
29
|
+
:ver => controller.verification_code(self.disco))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module StreamHelpers
|
34
|
+
def send_capabilities(to=nil, item=self.disco)
|
35
|
+
controller = self.controllers_by_name['Cancer::XEP_0115::Controller']
|
36
|
+
send do |x|
|
37
|
+
|
38
|
+
controller.cache_disco_data(item)
|
39
|
+
|
40
|
+
x.presence(to ? {:to => to} : {}) do
|
41
|
+
x.c(:xmlns => NS, :hash => 'sha-1', :node => CLIENT, :ver => controller.verification_code(item))
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Controller < Cancer::Controller
|
49
|
+
|
50
|
+
def capabilities
|
51
|
+
@capabilities ||= {}
|
52
|
+
end
|
53
|
+
|
54
|
+
on(:priority => 5) { |e| e.xpath('/c:iq[@type="get"]/disco:query', NAMESPACES) }
|
55
|
+
def catch_capabilities_query(e)
|
56
|
+
query_node = e.xml.first('/c:iq/disco:query', NAMESPACES)
|
57
|
+
|
58
|
+
if (node = query_node[:node]) and (capabilities = self.capabilities[node.to_s])
|
59
|
+
|
60
|
+
send_iq(e.sender, :result) do |x|
|
61
|
+
x.query(:xmlns => Cancer::XEP_0030::INFO_NS, :node => node) do
|
62
|
+
|
63
|
+
capabilities.identities.each do |identity|
|
64
|
+
x.identity(identity)
|
65
|
+
end
|
66
|
+
|
67
|
+
capabilities.features.each do |feature|
|
68
|
+
x.feature(:var => feature)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
throw :halt
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
on { |e| e.xpath('/c:presence/caps:c', NAMESPACES) }
|
79
|
+
def received_caps(e)
|
80
|
+
caps = e.xml.first('/c:presence/caps:c', NAMESPACES)
|
81
|
+
self.peers[e.sender].client = caps[:node].to_s
|
82
|
+
self.peers[e.sender].capabilities_id = caps[:ver].to_s
|
83
|
+
|
84
|
+
capabilities_node = "#{caps[:node]}##{caps[:ver]}"
|
85
|
+
unless self.capabilities[capabilities_node]
|
86
|
+
item = disco_info_for(e.sender, capabilities_node)
|
87
|
+
item ||= disco_info_for(e.sender, capabilities_node)
|
88
|
+
if item and verify_disco_item(item, caps[:ver].to_s, caps[:hash].to_s)
|
89
|
+
self.capabilities[capabilities_node] = item
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
self.peers[e.sender].disco = self.capabilities[capabilities_node]
|
94
|
+
end
|
95
|
+
|
96
|
+
def cache_disco_data(item=self.disco, hash='sha-1')
|
97
|
+
verification_string = verification_code(item, hash)
|
98
|
+
capabilities_node = "#{CLIENT}##{verification_string}"
|
99
|
+
|
100
|
+
self.capabilities[capabilities_node] = item
|
101
|
+
|
102
|
+
item
|
103
|
+
end
|
104
|
+
|
105
|
+
def verification_code(item, hash='sha-1')
|
106
|
+
|
107
|
+
identities = item.identities.dup
|
108
|
+
features = item.features.dup
|
109
|
+
|
110
|
+
fields = [:category, :type, :lang, :name]
|
111
|
+
identities.sort! do |a,b|
|
112
|
+
cmp_result = nil
|
113
|
+
fields.each do |field|
|
114
|
+
value_a = a[field]
|
115
|
+
value_b = b[field]
|
116
|
+
|
117
|
+
if value_a != value_b
|
118
|
+
cmp_result = value_a.cmp_ioctet(value_b)
|
119
|
+
break
|
120
|
+
end
|
121
|
+
end
|
122
|
+
cmp_result
|
123
|
+
end
|
124
|
+
|
125
|
+
identities.collect! do |identity|
|
126
|
+
"#{identity[:category]}/#{identity[:type]}/#{identity[:lang]}/#{identity[:name]}<"
|
127
|
+
end
|
128
|
+
|
129
|
+
if identities.uniq.size < identities.size
|
130
|
+
raise InvalidVerificationString, "this is invalid disco info (duplicate identity)"
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
features.sort! do |a,b|
|
135
|
+
a.cmp_ioctet(b)
|
136
|
+
end
|
137
|
+
|
138
|
+
features.collect! { |f| f + "<" }
|
139
|
+
|
140
|
+
if features.uniq.size < features.size
|
141
|
+
raise InvalidVerificationString, "this is invalid disco info (duplicate feature)"
|
142
|
+
end
|
143
|
+
|
144
|
+
verification_string = (identities + features).join('')
|
145
|
+
|
146
|
+
digest_klass = case hash
|
147
|
+
# when 'md2' then nil
|
148
|
+
when 'md5' then Digest::MD5
|
149
|
+
when 'sha-1' then Digest::SHA1
|
150
|
+
# when 'sha-224' then nil
|
151
|
+
when 'sha-256' then Digest::SHA256
|
152
|
+
when 'sha-384' then Digest::SHA384
|
153
|
+
when 'sha-512' then Digest::SHA512
|
154
|
+
else raise UnknownHashMethod, "the hash method #{hash} is not supported"
|
155
|
+
end
|
156
|
+
|
157
|
+
verification_string = digest_klass.digest(verification_string)
|
158
|
+
|
159
|
+
return Base64.encode64(verification_string).gsub(/\s|\n/, '')
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
def verify_disco_item(item, received_ver, hash)
|
164
|
+
generated_ver = verification_code(item, hash)
|
165
|
+
return generated_ver == received_ver
|
166
|
+
rescue InvalidVerificationString
|
167
|
+
return false
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
class InvalidVerificationString < RuntimeError ; end
|
173
|
+
class UnknownHashMethod < RuntimeError ; end
|
174
|
+
|
175
|
+
class ::Cancer::IM::Peer
|
176
|
+
attr_accessor :client, :capabilities_id, :disco
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Cancer
|
5
|
+
# Stream Compression
|
6
|
+
# http://xmpp.org/extensions/xep-0138.html
|
7
|
+
module XEP_0138
|
8
|
+
include Cancer::XEP
|
9
|
+
|
10
|
+
dependency 'core'
|
11
|
+
|
12
|
+
def self.enhance_stream(stream)
|
13
|
+
stream.extend_stream do
|
14
|
+
include Cancer::XEP_0138::Helpers
|
15
|
+
|
16
|
+
alias_method_chain :process_send_data, :xep_0138
|
17
|
+
alias_method_chain :process_received_data, :xep_0138
|
18
|
+
alias_method_chain :close, :xep_0138
|
19
|
+
end
|
20
|
+
|
21
|
+
stream.install_controller(Controller)
|
22
|
+
stream.register_compressor('zlib', Zlib)
|
23
|
+
end
|
24
|
+
|
25
|
+
module Helpers
|
26
|
+
def process_send_data_with_xep_0138(data)
|
27
|
+
data = data.to_s
|
28
|
+
data = @compressor.deflate(data) if @compressor
|
29
|
+
process_send_data_without_xep_0138(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_received_data_with_xep_0138(data)
|
33
|
+
data = process_received_data_without_xep_0138(data)
|
34
|
+
data = @compressor.inflate(data) if @compressor and data
|
35
|
+
data
|
36
|
+
end
|
37
|
+
|
38
|
+
def close_with_xep_0138
|
39
|
+
close_without_xep_0138
|
40
|
+
if @compressor and @compressor.respond_to?(:close)
|
41
|
+
@compressor.close
|
42
|
+
end
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def install_compressor(compressor)
|
47
|
+
@compressor = compressor
|
48
|
+
end
|
49
|
+
|
50
|
+
def available_compressors
|
51
|
+
compressors.keys
|
52
|
+
end
|
53
|
+
|
54
|
+
def register_compressor(name, klass)
|
55
|
+
compressors[name] = klass
|
56
|
+
end
|
57
|
+
|
58
|
+
def compressor_class(name)
|
59
|
+
compressors[name]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def compressors
|
65
|
+
@compressors ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
class Controller < Cancer::Controller
|
71
|
+
|
72
|
+
FEATURES_NS = "http://jabber.org/features/compress"
|
73
|
+
PROTOCOL_NS = "http://jabber.org/protocol/compress"
|
74
|
+
|
75
|
+
priority 250
|
76
|
+
|
77
|
+
on { |e| e.xpath('/s:features/c:compression/c:method', 'c' => FEATURES_NS, 's' => STREAM_NS) }
|
78
|
+
def try_compression(e)
|
79
|
+
methods = e.xml.all('/stream:features/c:compression/c:method', 'c' => FEATURES_NS)
|
80
|
+
methods = methods.to_a.collect { |m| m.text }
|
81
|
+
@methods = (methods & available_compressors)
|
82
|
+
return if @methods.empty?
|
83
|
+
|
84
|
+
try_compressor
|
85
|
+
end
|
86
|
+
|
87
|
+
on { |e| e.xpath('/n:compressed', 'n' => PROTOCOL_NS) }
|
88
|
+
def restart_stream_on_compressed(e)
|
89
|
+
install_compressor(compressor_class(@current_method).new(options))
|
90
|
+
restart_stream
|
91
|
+
end
|
92
|
+
|
93
|
+
on { |e| e.xpath('/n:failure', 'n' => PROTOCOL_NS) }
|
94
|
+
def retry_other_compression_method_on_failure(e)
|
95
|
+
try_compressor
|
96
|
+
end
|
97
|
+
|
98
|
+
def try_compressor
|
99
|
+
@current_method = @methods.shift
|
100
|
+
throw :skip unless @current_method
|
101
|
+
|
102
|
+
send do |x|
|
103
|
+
x.compress(:xmlns => PROTOCOL_NS) do
|
104
|
+
x.method_ { x.text @current_method }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
throw :halt
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
class Zlib
|
114
|
+
def initialize(options={})
|
115
|
+
@inflate_io = ::Zlib::Inflate.new
|
116
|
+
@deflate_io = ::Zlib::Deflate.new(options[:compression_level] || ::Zlib::BEST_COMPRESSION)
|
117
|
+
end
|
118
|
+
|
119
|
+
def inflate(data)
|
120
|
+
@inflate_io.inflate(data)
|
121
|
+
end
|
122
|
+
|
123
|
+
def deflate(data)
|
124
|
+
@deflate_io.deflate(data, ::Zlib::FULL_FLUSH)
|
125
|
+
end
|
126
|
+
|
127
|
+
def close
|
128
|
+
@inflate_io.close
|
129
|
+
@deflate_io.close
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
# Roster Item Exchange
|
4
|
+
# http://xmpp.org/extensions/xep-0144.html
|
5
|
+
module XEP_0144
|
6
|
+
include Cancer::XEP
|
7
|
+
|
8
|
+
NS = 'http://jabber.org/protocol/rosterx'
|
9
|
+
NAMESPACES = { 'c' => CLIENT_NS, 'rx' => NS }
|
10
|
+
|
11
|
+
dependency 'core'
|
12
|
+
dependency 'im'
|
13
|
+
dependency 'xep-0030'
|
14
|
+
dependency 'xep-0115'
|
15
|
+
|
16
|
+
def self.enhance_stream(stream)
|
17
|
+
stream.extend_stream do
|
18
|
+
include Cancer::XEP_0144::StreamHelpers
|
19
|
+
end
|
20
|
+
stream.install_controller Cancer::XEP_0144::Controller
|
21
|
+
stream.install_event_helper Cancer::XEP_0144::EventDSL
|
22
|
+
stream.disco.feature(NS)
|
23
|
+
end
|
24
|
+
|
25
|
+
module StreamHelpers
|
26
|
+
|
27
|
+
def exchange_roster(jid, message=nil, &proc)
|
28
|
+
if contact = roster[jid.to_jid.bare_jid]
|
29
|
+
if contact.peers.empty? or !peer_capabilities(jid).features.include?(NS)
|
30
|
+
send_roster_items_exchange_messages(jid.to_jid.bare_jid, message, &proc)
|
31
|
+
else
|
32
|
+
send_roster_items_exchange_iqs(jid, &proc)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def send_roster_items_exchange_messages(jid, message, &proc)
|
40
|
+
|
41
|
+
builder = Builder.new(&proc)
|
42
|
+
|
43
|
+
if builder.send(:build?, :add)
|
44
|
+
send_message(jid) do |x|
|
45
|
+
x.body message if message
|
46
|
+
builder.send(:build, x, :add)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if builder.send(:build?, :delete)
|
51
|
+
send_message(jid) do |x|
|
52
|
+
x.body message if message
|
53
|
+
builder.send(:build, x, :delete)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if builder.send(:build?, :modify)
|
58
|
+
send_message(jid) do |x|
|
59
|
+
x.body message if message
|
60
|
+
builder.send(:build, x, :modify)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def send_roster_items_exchange_iqs(jid, &proc)
|
67
|
+
|
68
|
+
builder = Builder.new(&proc)
|
69
|
+
|
70
|
+
if builder.send(:build?, :add)
|
71
|
+
send_iq(jid, :set) do |x|
|
72
|
+
builder.send(:build, x, :add)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if builder.send(:build?, :delete)
|
77
|
+
send_iq(jid, :set) do |x|
|
78
|
+
builder.send(:build, x, :delete)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if builder.send(:build?, :modify)
|
83
|
+
send_iq(jid, :set) do |x|
|
84
|
+
builder.send(:build, x, :modify)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
class Builder
|
93
|
+
|
94
|
+
def initialize(builder)
|
95
|
+
@actions = {
|
96
|
+
:add => [],
|
97
|
+
:delete => [],
|
98
|
+
:modify => []
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def add(jid, options={})
|
103
|
+
action(:add, jid, options)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete(jid, options={})
|
107
|
+
action(:jid, jid, options)
|
108
|
+
end
|
109
|
+
|
110
|
+
def modify(jid, options={})
|
111
|
+
action(:modify, jid, options)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def build?(action)
|
117
|
+
@actions[actions.to_sym] and !@actions[actions.to_sym].empty?
|
118
|
+
end
|
119
|
+
|
120
|
+
def build(builder, action)
|
121
|
+
builder.x_(:xmlns => NS) do
|
122
|
+
@actions[actions.to_sym].each do |item|
|
123
|
+
build_item(builder, action, item)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_item(builder, action, item)
|
129
|
+
jid, options = *item
|
130
|
+
builder.item({:action => action.to_s, :jid => jid.to_s, :name => options[:name]}.clean) do
|
131
|
+
options[:groups].each do |group|
|
132
|
+
builder.group(group)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def action(action, jid, options={})
|
138
|
+
options[:groups] = [options.delete(:groups)], options.delete(:group)].flatten.compact.uniq
|
139
|
+
@actions[actions.to_sym].push([jid, options])
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
class Controller < Cancer::Controller
|
145
|
+
|
146
|
+
on { |e| e.xpath('/c:iq[@type="set"]/rx:x', NAMESPACES) }
|
147
|
+
def handle_iq(e)
|
148
|
+
handle_roster_items(e, e.xml.all('/c:iq/rx:x/rx:item', NAMESPACES))
|
149
|
+
send_iq(e.sender, :result, e.id)
|
150
|
+
end
|
151
|
+
|
152
|
+
on { |e| e.xpath('/c:message/rx:x', NAMESPACES) }
|
153
|
+
def handle_message(e)
|
154
|
+
handle_roster_items(e, e.xml.all('/c:iq/rx:x/rx:item', NAMESPACES))
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_roster_items(e, items)
|
158
|
+
items.each do |item|
|
159
|
+
name = (item[:name] ? item[:name].to_s : nil)
|
160
|
+
groups = item.all('./rx:group', NAMESPACES).collect { |g| g.text.to_s }
|
161
|
+
fire! Cancer::XEP_0144::Event, e.sender, item[:action].to_s, item[:jid].to_s, name, groups
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
class Event < Cancer::Events::AbstractEvent
|
168
|
+
attr_reader :sender, :jid, :name, :groups, :action
|
169
|
+
def initialize(sender, action, jid, name, groups)
|
170
|
+
@sender, @action, @jid, @name, @groups = sender.to_jid, action.to_sym, jid.to_jid, name, groups
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class EventMatcher < Cancer::Events::Matcher
|
175
|
+
|
176
|
+
def initialize(options={})
|
177
|
+
@sender = options.delete(:sender)
|
178
|
+
@action = options.delete(:action)
|
179
|
+
@jid = options.delete(:jid)
|
180
|
+
@name = options.delete(:name)
|
181
|
+
@groups = options.delete(:groups)
|
182
|
+
end
|
183
|
+
|
184
|
+
def match?(event)
|
185
|
+
(
|
186
|
+
Cancer::XEP_0144::Event === event and
|
187
|
+
match_sender?(event) and
|
188
|
+
match_action?(event) and
|
189
|
+
match_jid?(event) and
|
190
|
+
match_name?(event) and
|
191
|
+
match_groups?(event)
|
192
|
+
)
|
193
|
+
end
|
194
|
+
|
195
|
+
def match_sender?(event, pattern=@sender)
|
196
|
+
case pattern
|
197
|
+
when String then event.sender.to_s == pattern
|
198
|
+
when Regexp then event.sender.to_s =~ pattern
|
199
|
+
when Array then pattern.any? { |p| match_sender?(event, p) }
|
200
|
+
when NilClass then true
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def match_action?(event, pattern=@action)
|
205
|
+
case pattern
|
206
|
+
when Symbol then event.action == pattern
|
207
|
+
when Array then pattern.any? { |p| match_action?(event, p) }
|
208
|
+
when NilClass then true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def match_jid?(event, pattern=@jid)
|
213
|
+
case pattern
|
214
|
+
when String then event.jid.to_s == pattern
|
215
|
+
when Regexp then event.jid.to_s =~ pattern
|
216
|
+
when Array then pattern.any? { |p| match_jid?(event, p) }
|
217
|
+
when NilClass then true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def match_name?(event, pattern=@name)
|
222
|
+
case pattern
|
223
|
+
when String then event.name == pattern
|
224
|
+
when Regexp then event.name =~ pattern
|
225
|
+
when Array then pattern.any? { |p| match_name?(event, p) }
|
226
|
+
when NilClass then true
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def match_groups?(event, pattern=@groups)
|
231
|
+
case pattern
|
232
|
+
when String then event.groups.include?(pattern)
|
233
|
+
when Regexp then event.groups.any? { |group| group =~ pattern }
|
234
|
+
when Array then pattern.any? { |p| match_groups?(event, p) }
|
235
|
+
when NilClass then true
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
module EventDSL
|
242
|
+
|
243
|
+
def roster_exchange(options={})
|
244
|
+
Cancer::XEP_0144::EventMatcher.new(options)
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|