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
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
|