cancer 0.1.0.a1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/cancer/xep.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module XEP
|
4
|
+
|
5
|
+
def self.xeps
|
6
|
+
@xeps ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.register(name, mod_name)
|
10
|
+
self.xeps[name.to_s] = mod_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.[](name)
|
14
|
+
self.xeps[name.to_s]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.const_for(name)
|
18
|
+
mod_name = self[name]
|
19
|
+
raise Cancer::XEP::LoadError.new(name) unless mod_name
|
20
|
+
|
21
|
+
mod_name.sub!(/^[:]{2}/, '')
|
22
|
+
eval("::#{mod_name}")
|
23
|
+
|
24
|
+
rescue NameError => e
|
25
|
+
if e.message.include?(mod_name)
|
26
|
+
raise Cancer::XEP::LoadError.new(name, mod_name)
|
27
|
+
else
|
28
|
+
raise e
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.send(:include, Cancer::Dependencies)
|
34
|
+
end
|
35
|
+
|
36
|
+
class LoadError < Cancer::CancerError
|
37
|
+
def initialize(xep_name, mod_name=nil)
|
38
|
+
super("failed to load #{xep_name}"+(mod_name ? " (#{mod_name})" : ''))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
# Core XMPP protocol
|
4
|
+
# http://tools.ietf.org/html/draft-ietf-xmpp-3920bis-02
|
5
|
+
module Core
|
6
|
+
include Cancer::XEP
|
7
|
+
|
8
|
+
autoload :Stream, 'cancer/xeps/core/stream'
|
9
|
+
autoload :TLS, 'cancer/xeps/core/tls'
|
10
|
+
autoload :SASL, 'cancer/xeps/core/sasl'
|
11
|
+
autoload :Bind, 'cancer/xeps/core/bind'
|
12
|
+
autoload :Session, 'cancer/xeps/core/session'
|
13
|
+
autoload :Terminator, 'cancer/xeps/core/terminator'
|
14
|
+
|
15
|
+
def self.enhance_stream(stream)
|
16
|
+
stream.extend_stream do
|
17
|
+
include Cancer::Core::StreamHelper
|
18
|
+
end
|
19
|
+
stream.extend_builder do
|
20
|
+
include Cancer::Core::BuilderHelper
|
21
|
+
end
|
22
|
+
|
23
|
+
stream.install_controller(Cancer::Core::Stream)
|
24
|
+
stream.install_controller(Cancer::Core::TLS)
|
25
|
+
stream.install_controller(Cancer::Core::SASL)
|
26
|
+
stream.install_controller(Cancer::Core::Bind)
|
27
|
+
stream.install_controller(Cancer::Core::Session)
|
28
|
+
stream.install_controller(Cancer::Core::Terminator)
|
29
|
+
end
|
30
|
+
|
31
|
+
module StreamHelper
|
32
|
+
|
33
|
+
def restart_stream
|
34
|
+
send_stream_header
|
35
|
+
throw :halt
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_stream_header
|
39
|
+
send %(<stream:stream xmlns="#{CLIENT_NS}" xmlns:stream="#{STREAM_NS}" to="#{options[:jid].server_jid}" from="#{options[:jid].bare_jid}" version="1.0">)
|
40
|
+
end
|
41
|
+
|
42
|
+
def start_tls
|
43
|
+
@adapter.start_tls
|
44
|
+
end
|
45
|
+
|
46
|
+
def send_iq(to, type=:get, id=nil, from=nil, &proc)
|
47
|
+
iq = build do |x|
|
48
|
+
x.iq(to, type, id, from) do
|
49
|
+
proc.call(x) if proc
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
if type == :result or type == :error
|
54
|
+
send iq
|
55
|
+
else
|
56
|
+
send_and_receive(iq) { |x| x.named?('iq', CLIENT_NS) and (x[:id] == iq[:id]) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def send_iq_response(original_iq, type = :result, &proc)
|
61
|
+
original_iq = (Cancer::Events::XMLEvent === original_iq ? original_iq.xml : original_iq)
|
62
|
+
to = original_iq['from']
|
63
|
+
id = original_iq['iq']
|
64
|
+
send_iq(to, type, id, &proc)
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_message(to=nil, type=nil, id=nil, &proc)
|
68
|
+
send do |x|
|
69
|
+
x.message(to, type) do
|
70
|
+
proc.call(x) if proc
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def next_stanza_id
|
76
|
+
@stanza_id ||= 0
|
77
|
+
@stanza_id += 1
|
78
|
+
"cancer-#{@stanza_id}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module BuilderHelper
|
83
|
+
def iq(to=nil, type=nil, id=nil, from=nil, &proc)
|
84
|
+
attributes = {}
|
85
|
+
attributes[:to] = to.to_s if to
|
86
|
+
attributes[:from] = from.to_s if from
|
87
|
+
attributes[:type] = type.to_s if type
|
88
|
+
attributes[:id] = id || @stream.next_stanza_id
|
89
|
+
iq_(attributes, &proc)
|
90
|
+
end
|
91
|
+
|
92
|
+
def iq_response(original_iq, type = :result, &proc)
|
93
|
+
original_iq = (Cancer::Events::XMLEvent === original_iq ? original_iq.xml : original_iq)
|
94
|
+
to = original_iq['from']
|
95
|
+
id = original_iq['iq']
|
96
|
+
iq(to, type, id, &proc)
|
97
|
+
end
|
98
|
+
|
99
|
+
def message(to=nil, type=nil, id=nil, &proc)
|
100
|
+
attributes = {}
|
101
|
+
attributes[:to] = to.to_s if to
|
102
|
+
attributes[:type] = type.to_s if type
|
103
|
+
attributes[:id] = id || @stream.next_stanza_id
|
104
|
+
message_(attributes, &proc)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module Core
|
4
|
+
class Bind < Cancer::Controller
|
5
|
+
|
6
|
+
NS = "urn:ietf:params:xml:ns:xmpp-bind"
|
7
|
+
|
8
|
+
priority 300
|
9
|
+
|
10
|
+
on { |e| e.xpath('/s:features/b:bind', 'b' => NS, 's' => STREAM_NS) }
|
11
|
+
def bind_resource(e)
|
12
|
+
resource = options[:jid].resource
|
13
|
+
result = send_iq(nil, :set) do |x|
|
14
|
+
x.bind(:xmlns => NS) do
|
15
|
+
x.resource { x.text resource } if resource
|
16
|
+
end
|
17
|
+
end
|
18
|
+
jid_element = result.first('b:bind/b:jid', 'b' => NS)
|
19
|
+
if jid_element.nil? or result[:type] == 'error'
|
20
|
+
raise "Bind failed: #{result}"
|
21
|
+
end
|
22
|
+
|
23
|
+
options[:jid].merge! Cancer::JID.parse(jid_element.text).to_hash
|
24
|
+
end
|
25
|
+
|
26
|
+
on { |e| e.name(:initialized) }
|
27
|
+
def unregister_ourself
|
28
|
+
unregister!
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
require 'sasl/base'
|
4
|
+
require 'sasl'
|
5
|
+
autoload :Base64, 'base64'
|
6
|
+
|
7
|
+
module Cancer
|
8
|
+
module Core
|
9
|
+
class SASL < Cancer::Controller
|
10
|
+
|
11
|
+
NS = "urn:ietf:params:xml:ns:xmpp-sasl"
|
12
|
+
|
13
|
+
priority 200
|
14
|
+
|
15
|
+
on { |e| e.xpath('/t:features/s:mechanisms/s:mechanism', 's' => NS, 't' => STREAM_NS) }
|
16
|
+
def find_sasl_mechanisms(e)
|
17
|
+
mechanisms = e.xml.all('/stream:features/s:mechanisms/s:mechanism', 's' => NS)
|
18
|
+
@mechanisms = mechanisms.to_a.collect { |e| e.text.upcase }
|
19
|
+
return if @mechanisms.empty?
|
20
|
+
|
21
|
+
try_sasl_mechanism
|
22
|
+
end
|
23
|
+
|
24
|
+
on { |e| e.xpath('/n:success', 'n' => NS) }
|
25
|
+
def on_success(e)
|
26
|
+
restart_stream
|
27
|
+
end
|
28
|
+
|
29
|
+
on { |e| e.xpath('/n:challenge', 'n' => NS) }
|
30
|
+
def on_challenge(e)
|
31
|
+
message, content = *@sasl.receive('challenge', Base64.decode64(e.xml.text))
|
32
|
+
|
33
|
+
unless message == 'response'
|
34
|
+
try_sasl_mechanism
|
35
|
+
end
|
36
|
+
|
37
|
+
send do |x|
|
38
|
+
x.response :xmlns => NS do |x|
|
39
|
+
x.text Base64.encode64(content) if content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
throw :halt
|
44
|
+
end
|
45
|
+
|
46
|
+
on { |e| e.name(:initialized) }
|
47
|
+
def unregister_ourself
|
48
|
+
unregister!
|
49
|
+
end
|
50
|
+
|
51
|
+
def try_sasl_mechanism
|
52
|
+
if @mechanisms.empty?
|
53
|
+
raise "SASL autentication failed!"
|
54
|
+
end
|
55
|
+
|
56
|
+
@sasl = ::SASL.new(@mechanisms, Preferences.new(options))
|
57
|
+
@mechanisms.delete(@sasl.mechanism)
|
58
|
+
|
59
|
+
message, content = *@sasl.start
|
60
|
+
unless message == 'auth'
|
61
|
+
try_sasl_mechanism
|
62
|
+
end
|
63
|
+
|
64
|
+
send do |x|
|
65
|
+
x.auth :mechanism => @sasl.mechanism, :xmlns => NS do |x|
|
66
|
+
x.text Base64.encode64(content) if content
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
throw :halt
|
71
|
+
end
|
72
|
+
|
73
|
+
class Preferences < ::SASL::Preferences
|
74
|
+
def initialize(options={})
|
75
|
+
@options = options
|
76
|
+
end
|
77
|
+
|
78
|
+
def authzid
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def realm
|
83
|
+
@options[:jid].domain
|
84
|
+
end
|
85
|
+
|
86
|
+
def digest_uri
|
87
|
+
"xmpp/#{@options[:jid].domain}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def username
|
91
|
+
@options[:jid].bare_jid.to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
def has_password?
|
95
|
+
!!@options[:password]
|
96
|
+
end
|
97
|
+
|
98
|
+
def allow_plaintext?
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def password
|
103
|
+
@options[:password]
|
104
|
+
end
|
105
|
+
|
106
|
+
def want_anonymous?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module Core
|
4
|
+
class Session < Cancer::Controller
|
5
|
+
|
6
|
+
NS = "urn:ietf:params:xml:ns:xmpp-session"
|
7
|
+
|
8
|
+
priority 400
|
9
|
+
|
10
|
+
on { |e| e.xpath('/t:features/s:session', 's' => NS, 't' => STREAM_NS) }
|
11
|
+
def initiate_session(e)
|
12
|
+
send_iq(nil, :set) { |x| x.session(:xmlns => NS) }
|
13
|
+
end
|
14
|
+
|
15
|
+
on { |e| e.name(:initialized) }
|
16
|
+
def unregister_ourself
|
17
|
+
unregister!
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module Core
|
4
|
+
class Stream < Cancer::Controller
|
5
|
+
|
6
|
+
on { |e| e.xpath('/n:stream', 'n' => STREAM_NS) }
|
7
|
+
def set_session_id(e)
|
8
|
+
options[:session_id] = e.xml['id']
|
9
|
+
end
|
10
|
+
|
11
|
+
on { |e| e.name(:initialized) }
|
12
|
+
def unregister_ourself
|
13
|
+
unregister!
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module Core
|
4
|
+
class Terminator < Cancer::Controller
|
5
|
+
|
6
|
+
priority 500
|
7
|
+
|
8
|
+
on { |e| e.xpath('/t:features', 't' => STREAM_NS) }
|
9
|
+
def initialization_completed(e)
|
10
|
+
fire! Cancer::Events::NamedEvent, :initialized
|
11
|
+
initialization_completed!
|
12
|
+
end
|
13
|
+
|
14
|
+
on { |e| e.name(:initialized) }
|
15
|
+
def unregister_ourself
|
16
|
+
unregister!
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
module Core
|
4
|
+
class TLS < Cancer::Controller
|
5
|
+
|
6
|
+
NS = "urn:ietf:params:xml:ns:xmpp-tls"
|
7
|
+
|
8
|
+
priority 100
|
9
|
+
|
10
|
+
on { |e| e.xpath('/s:features/t:starttls', 't' => NS, 's' => STREAM_NS) }
|
11
|
+
def try_start_tls(e)
|
12
|
+
send { |x| x.starttls :xmlns => NS }
|
13
|
+
throw :halt
|
14
|
+
end
|
15
|
+
|
16
|
+
on { |e| e.xpath('/t:proceed', 't' => NS) }
|
17
|
+
def start_tls_proceed(e)
|
18
|
+
start_tls
|
19
|
+
restart_stream
|
20
|
+
end
|
21
|
+
|
22
|
+
on { |e| e.xpath('/t:failure', 't' => NS) }
|
23
|
+
def start_tls_failed(e)
|
24
|
+
raise "TLS connection failed: #{e.xml}"
|
25
|
+
end
|
26
|
+
|
27
|
+
on { |e| e.name(:initialized) }
|
28
|
+
def unregister_ourself
|
29
|
+
unregister!
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
|
2
|
+
module Cancer
|
3
|
+
# XMPP IM protocol
|
4
|
+
module IM
|
5
|
+
include Cancer::XEP
|
6
|
+
|
7
|
+
ROSTER_NS = 'jabber:iq:roster'
|
8
|
+
|
9
|
+
def self.enhance_stream(stream)
|
10
|
+
stream.extend_stream do
|
11
|
+
include Cancer::IM::StreamHelper
|
12
|
+
end
|
13
|
+
stream.install_event_helper Cancer::IM::EventDSL
|
14
|
+
stream.install_controller Cancer::IM::Roster::Controller
|
15
|
+
|
16
|
+
stream.presence_enhancer_for :priority do |x|
|
17
|
+
x.priority_(self.presence_priority) if self.presence_priority
|
18
|
+
end
|
19
|
+
stream.presence_enhancer_for :show do |x|
|
20
|
+
x.show_(self.presence_show) if self.presence_show
|
21
|
+
end
|
22
|
+
stream.presence_enhancer_for :status do |x|
|
23
|
+
x.status_(self.presence_status) if self.presence_status
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module StreamHelper
|
28
|
+
|
29
|
+
def roster
|
30
|
+
@roster ||= Roster.new(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def peers
|
34
|
+
@peers ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def presence_enhancers
|
38
|
+
@presence_enhancers ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def presence_enhancer_for(name, &proc)
|
42
|
+
method_name = "enhance_presence_with_#{name}"
|
43
|
+
if self.respond_to?(method_name)
|
44
|
+
raise "#{self} already has a presence enhancer for #{name} (#{method_name})"
|
45
|
+
end
|
46
|
+
|
47
|
+
metaklass = (class << self ; self; end)
|
48
|
+
metaklass.send(:define_method, method_name, &proc)
|
49
|
+
metaklass.send(:private, method_name)
|
50
|
+
self.presence_enhancers.push(method_name)
|
51
|
+
|
52
|
+
name
|
53
|
+
end
|
54
|
+
|
55
|
+
def remove_precense_enhancer_for(name)
|
56
|
+
method_name = "enhance_presence_with_#{name}"
|
57
|
+
|
58
|
+
if self.respond_to?(method_name)
|
59
|
+
metaklass = (class << self ; self; end)
|
60
|
+
metaklass.send(:remove_method, method_name)
|
61
|
+
self.precense_enhancers.delete(method_name)
|
62
|
+
name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def update_presence!
|
67
|
+
send do |x|
|
68
|
+
x.presence do
|
69
|
+
|
70
|
+
self.presence_enhancers.each do |method_name|
|
71
|
+
self.__send__(method_name, x)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_accessor :presence_priority
|
79
|
+
attr_accessor :presence_show
|
80
|
+
attr_accessor :presence_status
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
class Roster
|
85
|
+
include Enumerable
|
86
|
+
|
87
|
+
def initialize(stream)
|
88
|
+
@stream = stream
|
89
|
+
pull!
|
90
|
+
end
|
91
|
+
|
92
|
+
def [](jid)
|
93
|
+
@items[jid.to_s]
|
94
|
+
end
|
95
|
+
|
96
|
+
def each(&proc)
|
97
|
+
@items.values.each(&proc)
|
98
|
+
end
|
99
|
+
|
100
|
+
def pull!
|
101
|
+
@items = {}
|
102
|
+
|
103
|
+
result = @stream.send_iq(nil) do |x|
|
104
|
+
x.query :xmlns => ROSTER_NS
|
105
|
+
end
|
106
|
+
|
107
|
+
namespaces = { 'c' => CLIENT_NS, 'r' => ROSTER_NS }
|
108
|
+
result.all('/c:iq/r:query/r:item', namespaces) do |i|
|
109
|
+
item = Roster::Item.new(i)
|
110
|
+
@items[item.jid] = item
|
111
|
+
end
|
112
|
+
|
113
|
+
@items
|
114
|
+
end
|
115
|
+
|
116
|
+
def push!(iq)
|
117
|
+
namespaces = { 'c' => CLIENT_NS, 'r' => ROSTER_NS }
|
118
|
+
iq.all('/c:iq/r:query/r:item', namespaces) do |i|
|
119
|
+
if i[:subscription].to_s == 'remove'
|
120
|
+
@items.delete(i[:jid].to_s)
|
121
|
+
else
|
122
|
+
item = Roster::Item.new(i)
|
123
|
+
@items[item.jid] = item
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def save(jid, options={})
|
129
|
+
subscription = options.delete(:subscription)
|
130
|
+
needs_roster = (!options.empty?) or (@items[jid.to_s].nil?)
|
131
|
+
|
132
|
+
if needs_roster
|
133
|
+
groups = options.delete(:groups) || []
|
134
|
+
@stream.send_iq(nil, :set) do |x|
|
135
|
+
x.query :xmlns => ROSTER_NS do
|
136
|
+
x.item(options.merge(:jid => jid)) do
|
137
|
+
groups.each { |group| x.group(group) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
unless subscription.nil?
|
144
|
+
subscription = case subscription
|
145
|
+
when TrueClass then :subscribe
|
146
|
+
when FalseClass then :unsubscribe
|
147
|
+
else subscription
|
148
|
+
end
|
149
|
+
@stream.send { |x| x.presence(:to => jid, :type => subscription.to_s) }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def create(jid, options={})
|
154
|
+
raise "Roster item #{jid} already exists!" if @items[jid.to_s]
|
155
|
+
save(jid, options)
|
156
|
+
end
|
157
|
+
|
158
|
+
def update(jid, options={})
|
159
|
+
raise "Roster item #{jid} doesn't exists yet!" unless @items[jid.to_s]
|
160
|
+
save(jid, options)
|
161
|
+
end
|
162
|
+
|
163
|
+
def delete(jid_or_item)
|
164
|
+
jid = (Roster::Item === jid_or_item ? jid_or_item.jid.to_s : jid_or_item.to_s)
|
165
|
+
|
166
|
+
raise "Roster item #{jid} doesn't exists yet!" unless @items[jid]
|
167
|
+
|
168
|
+
@stream.send_iq(nil, :set) do |x|
|
169
|
+
x.query :xmlns => ROSTER_NS do
|
170
|
+
x.item(:jid => jid, :subscription => 'remove')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class Item
|
176
|
+
attr_reader :jid, :name, :subscription, :groups, :peers
|
177
|
+
def initialize(options={})
|
178
|
+
@jid = (options[:jid].to_s rescue nil)
|
179
|
+
@name = (options[:name].to_s rescue nil)
|
180
|
+
@subscription = (options[:subscription].to_sym rescue :none)
|
181
|
+
@pending_subscription = ('subscribe' == options[:ask])
|
182
|
+
@peers = []
|
183
|
+
@groups = case options
|
184
|
+
when Nokogiri::XML::Node
|
185
|
+
options.all('r:group', 'r' => ROSTER_NS).collect { |g| g.text.to_s }
|
186
|
+
else options[:groups]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def pending_subscription?
|
191
|
+
@pending_subscription || false
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class Controller < Cancer::Controller
|
196
|
+
|
197
|
+
on(:priority => 1000) { |e| e.name(:initialized) }
|
198
|
+
def broadcast
|
199
|
+
update_presence!
|
200
|
+
end
|
201
|
+
|
202
|
+
NAMESPACES = { 'c' => CLIENT_NS, 'r' => ROSTER_NS }
|
203
|
+
on { |e| e.xpath('/c:iq[@type="set"]/r:query/r:item', NAMESPACES) }
|
204
|
+
def roster_push(e)
|
205
|
+
roster.push!(e.xml)
|
206
|
+
|
207
|
+
send_iq(e.sender, :result, e.id)
|
208
|
+
|
209
|
+
throw :halt
|
210
|
+
end
|
211
|
+
|
212
|
+
on(:priority => 5) { |e| e.xpath('/c:presence', 'c' => CLIENT_NS) }
|
213
|
+
def received_precense(e)
|
214
|
+
peer = self.peers[e.sender] || Peer.load(e.xml)
|
215
|
+
|
216
|
+
case (e.xml[:type] || :available).to_sym
|
217
|
+
|
218
|
+
when :available
|
219
|
+
peer = Peer.load(e.xml)
|
220
|
+
self.peers[e.sender] = peer
|
221
|
+
self.roster[e.sender.bare_jid].peers.push(peer) if self.roster[e.sender.bare_jid]
|
222
|
+
fire! PrecenceEvent, self, peer, :available, e.xml
|
223
|
+
when :unavailable
|
224
|
+
self.peers.delete(e.sender)
|
225
|
+
self.roster[e.sender.bare_jid].peers.delete(peer) if self.roster[e.sender.bare_jid]
|
226
|
+
fire! PrecenceEvent, self, peer, :unavailable, e.xml
|
227
|
+
|
228
|
+
when :subscribe
|
229
|
+
fire! PrecenceEvent, self, peer, :subscribe, e.xml
|
230
|
+
when :subscribed
|
231
|
+
fire! PrecenceEvent, self, peer, :subscribed, e.xml
|
232
|
+
|
233
|
+
when :unsubscribe
|
234
|
+
fire! PrecenceEvent, self, peer, :unsubscribe, e.xml
|
235
|
+
when :unsubscribed
|
236
|
+
fire! PrecenceEvent, self, peer, :unsubscribed, e.xml
|
237
|
+
|
238
|
+
when :probe
|
239
|
+
when :error
|
240
|
+
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
class Peer
|
248
|
+
attr_accessor :jid, :show, :status
|
249
|
+
|
250
|
+
def self.load(presence)
|
251
|
+
new.load(presence)
|
252
|
+
end
|
253
|
+
|
254
|
+
def load(presence)
|
255
|
+
@jid = presence[:from].to_s.to_jid
|
256
|
+
@show = (presence.first('./c:show/text()').to_sym rescue nil)
|
257
|
+
@status = (presence.first('./c:status/text()').to_s rescue nil)
|
258
|
+
self
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
class PrecenceEvent < Cancer::Events::AbstractEvent
|
263
|
+
attr_reader :peer, :action, :xml
|
264
|
+
def initialize(controller, peer, action, xml)
|
265
|
+
@controller, @peer, @action, @xml = controller, peer, action, xml
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class PrecenceEventMatcher < Cancer::Events::Matcher
|
270
|
+
|
271
|
+
def initialize(options={})
|
272
|
+
@action = options[:action]
|
273
|
+
end
|
274
|
+
|
275
|
+
def match?(event)
|
276
|
+
Cancer::IM::PrecenceEvent === event and match_action?(event)
|
277
|
+
end
|
278
|
+
|
279
|
+
def match_action?(event, pattern=@action)
|
280
|
+
case pattern
|
281
|
+
when Symbol then event.action.to_sym == pattern
|
282
|
+
when String then event.action.to_s == pattern
|
283
|
+
when Array then pattern.any? { |p| match_profile?(event, p) }
|
284
|
+
when NilClass then true
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
module EventDSL
|
291
|
+
|
292
|
+
def presence(options={})
|
293
|
+
Cancer::IM::PrecenceEventMatcher.new(options)
|
294
|
+
end
|
295
|
+
|
296
|
+
def peer_available(options={})
|
297
|
+
presence(options.merge(:action => :available))
|
298
|
+
end
|
299
|
+
|
300
|
+
def peer_unavailable(options={})
|
301
|
+
presence(options.merge(:action => :unavailable))
|
302
|
+
end
|
303
|
+
|
304
|
+
def subscription_request(options={})
|
305
|
+
presence(options.merge(:action => :subscribe))
|
306
|
+
end
|
307
|
+
|
308
|
+
def subscription_granted(options={})
|
309
|
+
presence(options.merge(:action => :subscribed))
|
310
|
+
end
|
311
|
+
|
312
|
+
def unsubscription_request(options={})
|
313
|
+
presence(options.merge(:action => :unsubscribe))
|
314
|
+
end
|
315
|
+
|
316
|
+
def subscription_revoked(options={})
|
317
|
+
presence(options.merge(:action => :unsubscribed))
|
318
|
+
end
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|