shingara-blather 0.4.9 → 0.4.14
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +15 -1
- data/lib/blather.rb +5 -0
- data/lib/blather/core_ext/nokogiri.rb +2 -0
- data/lib/blather/roster.rb +3 -3
- data/lib/blather/roster_item.rb +14 -3
- data/lib/blather/stanza/disco/disco_info.rb +20 -2
- data/lib/blather/stanza/disco/disco_items.rb +10 -1
- data/lib/blather/stanza/iq/command.rb +324 -0
- data/lib/blather/stanza/iq/vcard.rb +147 -0
- data/lib/blather/stanza/message.rb +49 -5
- data/lib/blather/stanza/presence/status.rb +11 -3
- data/lib/blather/stanza/pubsub.rb +9 -21
- data/lib/blather/stanza/x.rb +398 -0
- data/lib/blather/stream.rb +3 -2
- data/lib/blather/stream/parser.rb +6 -5
- data/lib/blather/xmpp_node.rb +1 -1
- data/spec/blather/roster_item_spec.rb +39 -1
- data/spec/blather/stanza/discos/disco_info_spec.rb +23 -0
- data/spec/blather/stanza/discos/disco_items_spec.rb +11 -0
- data/spec/blather/stanza/iq/command_spec.rb +206 -0
- data/spec/blather/stanza/iq/vcard_query_spec.rb +96 -0
- data/spec/blather/stanza/message_spec.rb +89 -14
- data/spec/blather/stanza/pubsub/retract_spec.rb +1 -1
- data/spec/blather/stanza/pubsub_spec.rb +0 -5
- data/spec/blather/stanza/x_spec.rb +235 -0
- data/spec/blather/stream/client_spec.rb +14 -1
- data/spec/blather/stream/parser_spec.rb +6 -0
- data/spec/blather/xmpp_node_spec.rb +4 -3
- data/spec/spec_helper.rb +28 -0
- metadata +35 -5
data/README.md
CHANGED
@@ -148,6 +148,17 @@ Command line options:
|
|
148
148
|
-h, --help Show this message
|
149
149
|
-v, --version Show version
|
150
150
|
|
151
|
+
## Health warning:
|
152
|
+
|
153
|
+
Some parts of Blather will allow you to do stupid things that don't conform to XMPP
|
154
|
+
spec. You should exercise caution and read the relevant specifications (indicated in
|
155
|
+
the preamble to most relevant classes).
|
156
|
+
|
157
|
+
# Contributions
|
158
|
+
|
159
|
+
All contributions are welcome, even encouraged. However, contributions must be
|
160
|
+
well tested. If you send me a branch name to merge that'll get my attention faster
|
161
|
+
than a change set made directly on master.
|
151
162
|
|
152
163
|
# Author
|
153
164
|
|
@@ -155,7 +166,10 @@ Command line options:
|
|
155
166
|
|
156
167
|
### Contributors
|
157
168
|
|
158
|
-
[Nolan Darilek](http://github.com/thewordnerd)
|
169
|
+
* [Nolan Darilek](http://github.com/thewordnerd)
|
170
|
+
* [Tim Carey-Smith](http://github.com/halorgium)
|
171
|
+
* [Ben Langfeld](http://github.com/benlangfeld)
|
172
|
+
* [Anton Mironov](http://github.com/mironov)
|
159
173
|
|
160
174
|
# Copyright
|
161
175
|
|
data/lib/blather.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
eventmachine
|
5
5
|
nokogiri
|
6
6
|
digest/md5
|
7
|
+
digest/sha1
|
7
8
|
logger
|
8
9
|
active_support/core_ext/class/inheritable_attributes
|
9
10
|
active_support/core_ext/object/blank
|
@@ -22,7 +23,9 @@
|
|
22
23
|
blather/stanza
|
23
24
|
blather/stanza/iq
|
24
25
|
blather/stanza/iq/query
|
26
|
+
blather/stanza/iq/command
|
25
27
|
blather/stanza/iq/roster
|
28
|
+
blather/stanza/iq/vcard
|
26
29
|
blather/stanza/disco
|
27
30
|
blather/stanza/disco/disco_info
|
28
31
|
blather/stanza/disco/disco_items
|
@@ -47,6 +50,8 @@
|
|
47
50
|
blather/stanza/pubsub_owner/delete
|
48
51
|
blather/stanza/pubsub_owner/purge
|
49
52
|
|
53
|
+
blather/stanza/x
|
54
|
+
|
50
55
|
blather/stream
|
51
56
|
blather/stream/client
|
52
57
|
blather/stream/component
|
@@ -22,9 +22,11 @@ module XML
|
|
22
22
|
# and namespace designation
|
23
23
|
def xpath(*paths)
|
24
24
|
paths[0] = paths[0].to_s
|
25
|
+
|
25
26
|
if paths.size > 1 && (namespaces = paths.pop).is_a?(Hash)
|
26
27
|
paths << namespaces.inject({}) { |h,v| h[v[0].to_s] = v[1]; h }
|
27
28
|
end
|
29
|
+
|
28
30
|
nokogiri_xpath *paths
|
29
31
|
end
|
30
32
|
alias_method :find, :xpath
|
data/lib/blather/roster.rb
CHANGED
@@ -90,8 +90,8 @@ module Blather
|
|
90
90
|
#
|
91
91
|
# @return [Hash<group => Array<RosterItem>>]
|
92
92
|
def grouped
|
93
|
-
|
94
|
-
item
|
93
|
+
@items.values.sort.inject(Hash.new{|h,k|h[k]=[]}) do |hash, item|
|
94
|
+
item.groups.each { |group| hash[group] << item }
|
95
95
|
hash
|
96
96
|
end
|
97
97
|
end
|
@@ -108,4 +108,4 @@ module Blather
|
|
108
108
|
end
|
109
109
|
end # Roster
|
110
110
|
|
111
|
-
end # Blather
|
111
|
+
end # Blather
|
data/lib/blather/roster_item.rb
CHANGED
@@ -90,7 +90,7 @@ module Blather
|
|
90
90
|
#
|
91
91
|
# @param [Blather::Stanza::Status] the new status
|
92
92
|
def status=(presence)
|
93
|
-
@statuses.delete_if { |s| s.from == presence.from }
|
93
|
+
@statuses.delete_if { |s| s.from == presence.from || s.state == :unavailable }
|
94
94
|
@statuses << presence
|
95
95
|
@statuses.sort!
|
96
96
|
end
|
@@ -102,7 +102,7 @@ module Blather
|
|
102
102
|
top = if resource
|
103
103
|
@statuses.detect { |s| s.from.resource == resource }
|
104
104
|
else
|
105
|
-
@statuses.
|
105
|
+
@statuses.last
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
@@ -117,6 +117,17 @@ module Blather
|
|
117
117
|
n.groups = groups
|
118
118
|
r
|
119
119
|
end
|
120
|
+
|
121
|
+
def <=>(o)
|
122
|
+
self.jid.to_s <=> o.jid.to_s
|
123
|
+
end
|
124
|
+
|
125
|
+
def eql?(o)
|
126
|
+
o.is_a?(RosterItem) &&
|
127
|
+
o.jid == self.jid &&
|
128
|
+
o.groups == self.groups
|
129
|
+
end
|
130
|
+
alias_method :==, :eql?
|
120
131
|
end #RosterItem
|
121
132
|
|
122
|
-
end
|
133
|
+
end
|
@@ -22,8 +22,8 @@ class Stanza
|
|
22
22
|
def self.new(type = nil, node = nil, identities = [], features = [])
|
23
23
|
new_node = super type
|
24
24
|
new_node.node = node
|
25
|
-
|
26
|
-
|
25
|
+
new_node.identities = [identities]
|
26
|
+
new_node.features = [features]
|
27
27
|
new_node
|
28
28
|
end
|
29
29
|
|
@@ -34,6 +34,15 @@ class Stanza
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
# Add an array of identities
|
38
|
+
# @param identities the array of identities, passed directly to Identity.new
|
39
|
+
def identities=(identities)
|
40
|
+
query.find('//ns:identity', :ns => self.class.registered_ns).each &:remove
|
41
|
+
if identities
|
42
|
+
[identities].flatten.each { |i| self.query << Identity.new(i) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
37
46
|
# List of feature objects
|
38
47
|
def features
|
39
48
|
query.find('//ns:feature', :ns => self.class.registered_ns).map do |f|
|
@@ -41,6 +50,15 @@ class Stanza
|
|
41
50
|
end
|
42
51
|
end
|
43
52
|
|
53
|
+
# Add an array of features
|
54
|
+
# @param features the array of features, passed directly to Feature.new
|
55
|
+
def features=(features)
|
56
|
+
query.find('//ns:feature', :ns => self.class.registered_ns).each &:remove
|
57
|
+
if features
|
58
|
+
[features].flatten.each { |f| self.query << Feature.new(f) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
44
62
|
class Identity < XMPPNode
|
45
63
|
# Create a new DiscoInfo Identity
|
46
64
|
# @overload new(node)
|
@@ -21,7 +21,7 @@ class Stanza
|
|
21
21
|
def self.new(type = nil, node = nil, items = [])
|
22
22
|
new_node = super type
|
23
23
|
new_node.node = node
|
24
|
-
|
24
|
+
new_node.items = [items]
|
25
25
|
new_node
|
26
26
|
end
|
27
27
|
|
@@ -34,6 +34,15 @@ class Stanza
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
+
# Add an array of items
|
38
|
+
# @param items the array of items, passed directly to Item.new
|
39
|
+
def items=(items)
|
40
|
+
query.find('//ns:item', :ns => self.class.registered_ns).each &:remove
|
41
|
+
if items
|
42
|
+
[items].flatten.each { |i| self.query << Item.new(i) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
37
46
|
# An individual Disco Item
|
38
47
|
class Item < XMPPNode
|
39
48
|
# Create a new Blather::Stanza::DiscoItems::Item
|
@@ -0,0 +1,324 @@
|
|
1
|
+
module Blather
|
2
|
+
class Stanza
|
3
|
+
class Iq
|
4
|
+
|
5
|
+
# # Command Stanza
|
6
|
+
#
|
7
|
+
# [XEP-0050 Ad-Hoc Commands](http://xmpp.org/extensions/xep-0050.html)
|
8
|
+
#
|
9
|
+
# This is a base class for any command based Iq stanzas. It provides a base set
|
10
|
+
# of methods for working with command stanzas
|
11
|
+
#
|
12
|
+
# @handler :command
|
13
|
+
class Command < Iq
|
14
|
+
VALID_ACTIONS = [:cancel, :execute, :complete, :next, :prev].freeze
|
15
|
+
VALID_STATUS = [:executing, :completed, :canceled].freeze
|
16
|
+
VALID_NOTE_TYPES = [:info, :warn, :error].freeze
|
17
|
+
|
18
|
+
register :command, :command, 'http://jabber.org/protocol/commands'
|
19
|
+
|
20
|
+
# Overrides the parent method to ensure a command node is created
|
21
|
+
#
|
22
|
+
# @param [:get, :set, :result, :error, nil] type the IQ type
|
23
|
+
# @param [String] node the name of the node
|
24
|
+
# @param [:cancel, :execute, :complete, :next, :prev, nil] action the command's action
|
25
|
+
# @return [Command] a new Command stanza
|
26
|
+
def self.new(type = :set, node = nil, action = :execute)
|
27
|
+
new_node = super type
|
28
|
+
new_node.command
|
29
|
+
new_node.node = node
|
30
|
+
new_node.action = action
|
31
|
+
new_node
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides the parent method to ensure the current command node is destroyed
|
35
|
+
# and the action is set to execute if no action provided
|
36
|
+
#
|
37
|
+
# @see Blather::Stanza::Iq#inherit
|
38
|
+
def inherit(node)
|
39
|
+
command.remove
|
40
|
+
super
|
41
|
+
self.action = :execute unless self.action
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Overrides the parent method to ensure the reply has no action
|
46
|
+
#
|
47
|
+
# @return [self]
|
48
|
+
def reply!
|
49
|
+
super
|
50
|
+
self.action = nil
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Command node accessor
|
55
|
+
# If a command node exists it will be returned.
|
56
|
+
# Otherwise a new node will be created and returned
|
57
|
+
#
|
58
|
+
# @return [Blather::XMPPNode]
|
59
|
+
def command
|
60
|
+
c = if self.class.registered_ns
|
61
|
+
find_first('ns:command', :ns => self.class.registered_ns)
|
62
|
+
else
|
63
|
+
find_first('command')
|
64
|
+
end
|
65
|
+
|
66
|
+
unless c
|
67
|
+
(self << (c = XMPPNode.new('command', self.document)))
|
68
|
+
c.namespace = self.class.registered_ns
|
69
|
+
end
|
70
|
+
c
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get the name of the node
|
74
|
+
#
|
75
|
+
# @return [String, nil]
|
76
|
+
def node
|
77
|
+
command[:node]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set the name of the node
|
81
|
+
#
|
82
|
+
# @param [String, nil] node the new node name
|
83
|
+
def node=(node)
|
84
|
+
command[:node] = node
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the sessionid of the command
|
88
|
+
#
|
89
|
+
# @return [String, nil]
|
90
|
+
def sessionid
|
91
|
+
command[:sessionid]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Check if there is a sessionid set
|
95
|
+
#
|
96
|
+
# @return [true, false]
|
97
|
+
def sessionid?
|
98
|
+
!sessionid.nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set the sessionid of the command
|
102
|
+
#
|
103
|
+
# @param [String, nil] sessionid the new sessionid
|
104
|
+
def sessionid=(sessionid)
|
105
|
+
command[:sessionid] = Digest::SHA1.hexdigest(sessionid)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Generate a new session ID (SHA-1 hash)
|
109
|
+
def new_sessionid!
|
110
|
+
self.sessionid = "commandsession-#{id}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get the action of the command
|
114
|
+
#
|
115
|
+
# @return [Symbol, nil]
|
116
|
+
def action
|
117
|
+
(val = command[:action]) && val.to_sym
|
118
|
+
end
|
119
|
+
|
120
|
+
# Check if the command action is :cancel
|
121
|
+
#
|
122
|
+
# @return [true, false]
|
123
|
+
def cancel?
|
124
|
+
self.action == :cancel
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check if the command action is :execute
|
128
|
+
#
|
129
|
+
# @return [true, false]
|
130
|
+
def execute?
|
131
|
+
self.action == :execute
|
132
|
+
end
|
133
|
+
|
134
|
+
# Check if the command action is :complete
|
135
|
+
#
|
136
|
+
# @return [true, false]
|
137
|
+
def complete?
|
138
|
+
self.action == :complete
|
139
|
+
end
|
140
|
+
|
141
|
+
# Check if the command action is :next
|
142
|
+
#
|
143
|
+
# @return [true, false]
|
144
|
+
def next?
|
145
|
+
self.action == :next
|
146
|
+
end
|
147
|
+
|
148
|
+
# Check if the command action is :prev
|
149
|
+
#
|
150
|
+
# @return [true, false]
|
151
|
+
def prev?
|
152
|
+
self.action == :prev
|
153
|
+
end
|
154
|
+
|
155
|
+
# Set the action of the command
|
156
|
+
#
|
157
|
+
# @param [:cancel, :execute, :complete, :next, :prev] action the new action
|
158
|
+
def action=(action)
|
159
|
+
if action && !VALID_ACTIONS.include?(action.to_sym)
|
160
|
+
raise ArgumentError, "Invalid Action (#{action}), use: #{VALID_ACTIONS*' '}"
|
161
|
+
end
|
162
|
+
command[:action] = action
|
163
|
+
end
|
164
|
+
|
165
|
+
# Get the status of the command
|
166
|
+
#
|
167
|
+
# @return [Symbol, nil]
|
168
|
+
def status
|
169
|
+
((val = command[:status]) && val.to_sym) || :executing
|
170
|
+
end
|
171
|
+
|
172
|
+
# Check if the command status is :executing
|
173
|
+
#
|
174
|
+
# @return [true, false]
|
175
|
+
def executing?
|
176
|
+
self.status == :executing
|
177
|
+
end
|
178
|
+
|
179
|
+
# Check if the command status is :completed
|
180
|
+
#
|
181
|
+
# @return [true, false]
|
182
|
+
def completed?
|
183
|
+
self.status == :completed
|
184
|
+
end
|
185
|
+
|
186
|
+
# Check if the command status is :canceled
|
187
|
+
#
|
188
|
+
# @return [true, false]
|
189
|
+
def canceled?
|
190
|
+
self.status == :canceled
|
191
|
+
end
|
192
|
+
|
193
|
+
# Set the status of the command
|
194
|
+
#
|
195
|
+
# @param [:executing, :completed, :canceled] status the new status
|
196
|
+
def status=(status)
|
197
|
+
if status && !VALID_STATUS.include?(status.to_sym)
|
198
|
+
raise ArgumentError, "Invalid Action (#{status}), use: #{VALID_STATUS*' '}"
|
199
|
+
end
|
200
|
+
command[:status] = status
|
201
|
+
end
|
202
|
+
|
203
|
+
# Command actions accessor
|
204
|
+
# If a command actions element exists it will be returned.
|
205
|
+
# Otherwise a new actions element will be created and returned
|
206
|
+
#
|
207
|
+
# @return [Blather::XMPPNode]
|
208
|
+
def actions
|
209
|
+
unless a = self.command.find_first('ns:actions', :ns => self.class.registered_ns)
|
210
|
+
(self.command << (a = XMPPNode.new('actions', self.document)))
|
211
|
+
a.namespace = self.command.namespace
|
212
|
+
end
|
213
|
+
a
|
214
|
+
end
|
215
|
+
|
216
|
+
# Get the command's allowed actions
|
217
|
+
#
|
218
|
+
# @return [[Symbol]]
|
219
|
+
def allowed_actions
|
220
|
+
([:execute] + actions.children.map { |action| action.name.to_sym }).uniq
|
221
|
+
end
|
222
|
+
|
223
|
+
def primary_allowed_action
|
224
|
+
(actions[:execute] || :execute).to_sym
|
225
|
+
end
|
226
|
+
|
227
|
+
def primary_allowed_action=(a)
|
228
|
+
if a && ![:prev, :next, :complete, :execute].include?(a.to_sym)
|
229
|
+
raise ArgumentError, "Invalid Action (#{a}), use: #{[:prev, :next, :complete, :execute]*' '}"
|
230
|
+
end
|
231
|
+
actions[:execute] = a
|
232
|
+
end
|
233
|
+
|
234
|
+
# Add allowed actions to the command
|
235
|
+
#
|
236
|
+
# @param [[:prev, :next, :complete]] allowed_actions the new allowed actions
|
237
|
+
def allowed_actions=(allowed_actions)
|
238
|
+
allowed_actions = ([allowed_actions].flatten.map(&:to_sym) + [:execute]).uniq
|
239
|
+
if (invalid_actions = allowed_actions - VALID_ACTIONS).size > 0
|
240
|
+
raise ArgumentError, "Invalid Action(s) (#{invalid_actions*' '}), use: #{VALID_ACTIONS*' '}"
|
241
|
+
end
|
242
|
+
actions.children.map(&:remove)
|
243
|
+
allowed_actions.each { |action| actions << XMPPNode.new(action.to_s) }
|
244
|
+
end
|
245
|
+
|
246
|
+
# Remove allowed actions from the command
|
247
|
+
#
|
248
|
+
# @param [[:prev, :next, :complete]] disallowed_actions the allowed actions to remove
|
249
|
+
def remove_allowed_actions!
|
250
|
+
actions.remove
|
251
|
+
end
|
252
|
+
|
253
|
+
# Command note accessor
|
254
|
+
# If a command note exists it will be returned.
|
255
|
+
# Otherwise a new note will be created and returned
|
256
|
+
#
|
257
|
+
# @return [Blather::XMPPNode]
|
258
|
+
def note
|
259
|
+
unless n = self.command.find_first('ns:note', :ns => self.class.registered_ns)
|
260
|
+
(self.command << (n = XMPPNode.new('note', self.document)))
|
261
|
+
n.namespace = self.command.namespace
|
262
|
+
end
|
263
|
+
n
|
264
|
+
end
|
265
|
+
|
266
|
+
# Get the note_type of the command
|
267
|
+
#
|
268
|
+
# @return [Symbol, nil]
|
269
|
+
def note_type
|
270
|
+
(val = note[:type]) && val.to_sym
|
271
|
+
end
|
272
|
+
|
273
|
+
# Check if the command status is :info
|
274
|
+
#
|
275
|
+
# @return [true, false]
|
276
|
+
def info?
|
277
|
+
self.note_type == :info
|
278
|
+
end
|
279
|
+
|
280
|
+
# Check if the command status is :warn
|
281
|
+
#
|
282
|
+
# @return [true, false]
|
283
|
+
def warn?
|
284
|
+
self.status == :warn
|
285
|
+
end
|
286
|
+
|
287
|
+
# Check if the command status is :error
|
288
|
+
#
|
289
|
+
# @return [true, false]
|
290
|
+
def error?
|
291
|
+
self.status == :error
|
292
|
+
end
|
293
|
+
|
294
|
+
# Set the note_type of the command
|
295
|
+
#
|
296
|
+
# @param [:executing, :completed, :canceled] note_type the new note_type
|
297
|
+
def note_type=(note_type)
|
298
|
+
if note_type && !VALID_NOTE_TYPES.include?(note_type.to_sym)
|
299
|
+
raise ArgumentError, "Invalid Action (#{note_type}), use: #{VALID_NOTE_TYPES*' '}"
|
300
|
+
end
|
301
|
+
note[:type] = note_type
|
302
|
+
end
|
303
|
+
|
304
|
+
# Get the text of the command's note
|
305
|
+
def note_text
|
306
|
+
content_from :note
|
307
|
+
end
|
308
|
+
|
309
|
+
# Set the command's note text
|
310
|
+
#
|
311
|
+
# @param [String] note_text the command's new note text
|
312
|
+
def note_text=(note_text)
|
313
|
+
set_content_for :note, note_text
|
314
|
+
end
|
315
|
+
|
316
|
+
# Returns the command's x:data form child
|
317
|
+
def form
|
318
|
+
X.find_or_create command
|
319
|
+
end
|
320
|
+
end #Command
|
321
|
+
|
322
|
+
end #Iq
|
323
|
+
end #Stanza
|
324
|
+
end #Blather
|