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.
@@ -0,0 +1,147 @@
1
+ module Blather
2
+ class Stanza
3
+ class Iq
4
+
5
+ # # Vcard Stanza
6
+ #
7
+ # [XEP-0054 vcard-temp](http://xmpp.org/extensions/xep-0054.html)
8
+ #
9
+ # This is a base class for any vcard based Iq stanzas. It provides a base set
10
+ # of methods for working with vcard stanzas
11
+ #
12
+ # @example Retrieving One's vCard
13
+ # iq = Blather::Stanza::Iq::Vcard.new :get
14
+ # client.write_with_handler iq do |response|
15
+ # puts response.vcard
16
+ # end
17
+ #
18
+ # @example Updating One's vCard
19
+ # iq = Blather::Stanza::Iq::Vcard.new :set
20
+ # iq.vcard['NICKNAME'] = 'Romeo'
21
+ # client.write_with_handler iq do |response|
22
+ # puts response
23
+ # end
24
+ #
25
+ # @example Viewing Another User's vCard
26
+ # iq = Blather::Stanza::Iq::Vcard.new :get, 'mercutio@example.org'
27
+ # client.write_with_handler iq do |response|
28
+ # puts response.vcard
29
+ # end
30
+ #
31
+ # @handler :vcard
32
+ class Vcard < Iq
33
+
34
+ register :vcard, :vCard, 'vcard-temp'
35
+
36
+ # Overrides the parent method to ensure a vcard node is created
37
+ #
38
+ # @see Blather::Stanza::Iq.new
39
+ def self.new(type = nil, to = nil, id = nil)
40
+ node = super
41
+ node.vcard
42
+ node
43
+ end
44
+
45
+ # Overrides the parent method to ensure the current vcard node is destroyed
46
+ #
47
+ # @see Blather::Stanza::Iq#inherit
48
+ def inherit(node)
49
+ vcard.remove
50
+ super
51
+ self
52
+ end
53
+
54
+ # Find or create vcard node
55
+ #
56
+ # @return [Vcard::Vcard]
57
+ def vcard
58
+ Vcard.find_or_create self
59
+ end
60
+
61
+ # Replaces vcard node
62
+ #
63
+ # @param [Vcard::Vcard, XML::Node] info the stanza's new vcard node
64
+ #
65
+ # @return [Vcard::Vcard]
66
+ def vcard=(info)
67
+ vcard.remove
68
+ self << info
69
+ Vcard.find_or_create self
70
+ end
71
+
72
+ class Vcard < XMPPNode
73
+
74
+ VCARD_NS = 'vcard-temp'
75
+
76
+ # Create a new Vcard::Vcard object
77
+ #
78
+ # @param [XML::Node, nil] node a node to inherit from
79
+ #
80
+ # @return [Vcard::Vcard]
81
+ def self.new(node = nil)
82
+ new_node = super :vCard
83
+ new_node.namespace = VCARD_NS
84
+ new_node.inherit node if node
85
+ new_node
86
+ end
87
+
88
+ # Find or create vCard node in Vcard Iq and converts it to Vcard::Vcard
89
+ #
90
+ # @param [Vcard] parent a Vcard Iq where to find or create vCard
91
+ #
92
+ # @return [Vcard::Vcard]
93
+ def self.find_or_create(parent)
94
+ if found_vcard = parent.find_first('//ns:vCard', :ns => VCARD_NS)
95
+ vcard = self.new found_vcard
96
+ found_vcard.remove
97
+ else
98
+ vcard = self.new
99
+ end
100
+ parent << vcard
101
+
102
+ vcard
103
+ end
104
+
105
+ # Find the element's value by name
106
+ #
107
+ # @param [String] name the name of the element
108
+ #
109
+ # @return [String, nil]
110
+ def [](name)
111
+ name = name.split("/").map{|child| "ns:#{child}"}.join("/")
112
+
113
+ if elem = find_first(name, :ns => VCARD_NS)
114
+ elem.content
115
+ else
116
+ nil
117
+ end
118
+ end
119
+
120
+ # Set the element's value
121
+ #
122
+ # @param [String] name the name of the element
123
+ # @param [String, nil] value the new value of element
124
+ #
125
+ # @return [String, nil]
126
+ def []=(name, value)
127
+ elem = nil
128
+ parent = self
129
+
130
+ name.split("/").each do |child|
131
+ elem = parent.find_first("ns:#{child}", :ns => VCARD_NS)
132
+ unless elem
133
+ elem = XMPPNode.new(child, parent.document)
134
+ parent << elem
135
+ parent = elem
136
+ else
137
+ parent = elem
138
+ end
139
+ end
140
+
141
+ elem.content = value
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -159,6 +159,9 @@ class Stanza
159
159
  class Message < Stanza
160
160
  VALID_TYPES = [:chat, :error, :groupchat, :headline, :normal].freeze
161
161
 
162
+ VALID_CHAT_STATES = [:active, :composing, :gone, :inactive, :paused].freeze
163
+ CHAT_STATE_NS = 'http://jabber.org/protocol/chatstates'.freeze
164
+
162
165
  HTML_NS = 'http://jabber.org/protocol/xhtml-im'.freeze
163
166
  HTML_BODY_NS = 'http://www.w3.org/1999/xhtml'.freeze
164
167
 
@@ -172,7 +175,7 @@ class Stanza
172
175
  klass = class_from_registration(e.element_name, ns)
173
176
  end
174
177
 
175
- if klass && klass != self
178
+ if klass && klass != self && klass != Blather::Stanza::X
176
179
  klass.import(node)
177
180
  else
178
181
  new(node[:type]).inherit(node)
@@ -189,9 +192,18 @@ class Stanza
189
192
  node.to = to
190
193
  node.type = type
191
194
  node.body = body
195
+ node.chat_state = :active if [:chat, :groupchat].include?(type)
192
196
  node
193
197
  end
194
198
 
199
+ # Overrides the parent method to ensure the current chat state is removed
200
+ #
201
+ # @see Blather::Stanza::Iq#inherit
202
+ def inherit(node)
203
+ xpath('ns:*', :ns => CHAT_STATE_NS).remove
204
+ super
205
+ end
206
+
195
207
  # Check if the Message is of type :chat
196
208
  #
197
209
  # @return [true, false]
@@ -262,8 +274,9 @@ class Stanza
262
274
  end
263
275
 
264
276
  unless b = h.find_first('ns:body', :ns => HTML_BODY_NS)
265
- h << (b = XMPPNode.new('body', self.document))
277
+ b = XMPPNode.new('body', self.document)
266
278
  b.namespace = HTML_BODY_NS
279
+ h << b
267
280
  end
268
281
 
269
282
  b
@@ -273,7 +286,7 @@ class Stanza
273
286
  #
274
287
  # @return [String]
275
288
  def xhtml
276
- self.xhtml_node.content.strip
289
+ self.xhtml_node.inner_html.strip
277
290
  end
278
291
 
279
292
  # Set the message xhtml
@@ -281,7 +294,7 @@ class Stanza
281
294
  #
282
295
  # @param [#to_s] valid xhtml
283
296
  def xhtml=(xhtml_body)
284
- self.xhtml_node.content = Nokogiri::XML(xhtml_body).to_xhtml
297
+ self.xhtml_node.inner_html = Nokogiri::XML::DocumentFragment.parse(xhtml_body)
285
298
  end
286
299
 
287
300
  # Get the message subject
@@ -326,7 +339,38 @@ class Stanza
326
339
  set_content_for :thread, thread
327
340
  find_first('thread')[:parent] = parent
328
341
  end
342
+
343
+ # Returns the message's x:data form child
344
+ def form
345
+ X.find_or_create self
346
+ end
347
+
348
+ # Get the message chat state
349
+ #
350
+ # @return [Symbol]
351
+ def chat_state
352
+ if (elem = find_first('ns:*', :ns => CHAT_STATE_NS)) && VALID_CHAT_STATES.include?(name = elem.name.to_sym)
353
+ name
354
+ end
355
+ end
356
+
357
+ # Set the message chat state
358
+ #
359
+ # @param [#to_s] chat_state the message chat state. Must be one of VALID_CHAT_STATES
360
+ def chat_state=(chat_state)
361
+ if chat_state && !VALID_CHAT_STATES.include?(chat_state.to_sym)
362
+ raise ArgumentError, "Invalid Chat State (#{chat_state}), use: #{VALID_CHAT_STATES*' '}"
363
+ end
364
+
365
+ xpath('ns:*', :ns => CHAT_STATE_NS).remove
366
+
367
+ if chat_state
368
+ state = XMPPNode.new(chat_state, self.document)
369
+ state.namespace = CHAT_STATE_NS
370
+ self << state
371
+ end
372
+ end
329
373
  end
330
374
 
331
375
  end
332
- end
376
+ end
@@ -193,8 +193,9 @@ class Presence
193
193
  set_content_for :status, message
194
194
  end
195
195
 
196
- # Compare status based on priority
197
- # raises an error if the JIDs aren't the same
196
+ # Compare status based on priority and state:
197
+ # unavailable status is always less valuable than others
198
+ # Raises an error if the JIDs aren't the same
198
199
  #
199
200
  # @param [Blather::Stanza::Presence::Status] o
200
201
  # @return [true,false]
@@ -202,7 +203,14 @@ class Presence
202
203
  unless self.from && o.from && self.from.stripped == o.from.stripped
203
204
  raise ArgumentError, "Cannot compare status from different JIDs: #{[self.from, o.from].inspect}"
204
205
  end
205
- self.priority <=> o.priority
206
+
207
+ if (self.type.nil? && o.type.nil?) || (!self.type.nil? && !o.type.nil?)
208
+ self.priority <=> o.priority
209
+ elsif self.type.nil? && !o.type.nil?
210
+ 1
211
+ elsif !self.type.nil? && o.type.nil?
212
+ -1
213
+ end
206
214
  end
207
215
 
208
216
  end #Status
@@ -65,8 +65,6 @@ class Stanza
65
65
  # This fragment is found in many places throughout the pubsub spec
66
66
  # This is a convenience class to attach methods to the node
67
67
  class PubSubItem < XMPPNode
68
- ATOM_NS = 'http://www.w3.org/2005/Atom'.freeze
69
-
70
68
  # Create a new PubSubItem
71
69
  #
72
70
  # @param [String, nil] id the id of the stanza
@@ -96,32 +94,22 @@ class Stanza
96
94
 
97
95
  # Get the item's payload
98
96
  #
99
- # To get the XML representation use #entry
100
- #
101
- # @return [String, nil]
97
+ # @return [String, XMPPNode, nil]
102
98
  def payload
103
- self.entry.content.empty? ? nil : content
99
+ children.empty? ? nil : children.to_s
104
100
  end
105
101
 
106
102
  # Set the item's payload
107
103
  #
108
- # @param [String, nil] payload the payload
104
+ # @param [String, XMPPNode, nil] payload the payload
109
105
  def payload=(payload)
110
- self.entry.content = payload
111
- end
112
-
113
- # Get or create the entry node
114
- #
115
- # @return [Blather::XMPPNode]
116
- def entry
117
- e = find_first('ns:entry', :ns => ATOM_NS) ||
118
- find_first('entry', :ns => ATOM_NS)
119
-
120
- unless e
121
- self << (e = XMPPNode.new('entry', self.document))
122
- e.namespace = ATOM_NS
106
+ children.map &:remove
107
+ return unless payload
108
+ if payload.is_a?(String)
109
+ self.content = payload
110
+ else
111
+ self << payload
123
112
  end
124
- e
125
113
  end
126
114
  end # PubSubItem
127
115
 
@@ -0,0 +1,398 @@
1
+ module Blather
2
+ class Stanza
3
+ # # X Stanza
4
+ #
5
+ # [XEP-0004 Data Forms](http://xmpp.org/extensions/xep-0004.html)
6
+ #
7
+ # Data Form node that allows for semi-structured data exchange
8
+ #
9
+ # @handler :x
10
+ class X < XMPPNode
11
+ register :x, 'jabber:x:data'
12
+
13
+ VALID_TYPES = [:cancel, :form, :result, :submit].freeze
14
+
15
+ # Create a new X node
16
+ # @param [:cancel, :form, :result, :submit, nil] type the x:form type
17
+ # @param [Array<Array, X::Field>, nil] fields a list of fields.
18
+ # These are passed directly to X::Field.new
19
+ # @return [X] a new X stanza
20
+ def self.new(type = nil, fields = [])
21
+ new_node = super :x
22
+
23
+ case type
24
+ when Nokogiri::XML::Node
25
+ new_node.inherit type
26
+ when Hash
27
+ new_node.type = type[:type]
28
+ new_node.fields = type[:fields]
29
+ else
30
+ new_node.type = type
31
+ new_node.fields = fields
32
+ end
33
+ new_node
34
+ end
35
+
36
+ def self.find_or_create(parent)
37
+ if found_x = parent.find_first('//ns:x', :ns => self.registered_ns)
38
+ x = self.new found_x
39
+ found_x.remove
40
+ else
41
+ x = self.new
42
+ end
43
+ parent << x
44
+ x
45
+ end
46
+
47
+ # The Form's type
48
+ # @return [Symbol]
49
+ def type
50
+ read_attr :type, :to_sym
51
+ end
52
+
53
+ # Set the Form's type
54
+ # @param [:cancel, :form, :result, :submit] type the new type for the form
55
+ def type=(type)
56
+ if type && !VALID_TYPES.include?(type.to_sym)
57
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
58
+ end
59
+ write_attr :type, type
60
+ end
61
+
62
+ # List of field objects
63
+ # @return [Blather::Stanza::X::Field]
64
+ def fields
65
+ self.find('ns:field', :ns => self.class.registered_ns).map do |field|
66
+ Field.new(field)
67
+ end
68
+ end
69
+
70
+ # Find a field by var
71
+ # @param var the var for the field you wish to find
72
+ def field(var)
73
+ fields.detect { |f| f.var == var }
74
+ end
75
+
76
+ # Add an array of fields to form
77
+ # @param fields the array of fields, passed directly to Field.new
78
+ def fields=(fields)
79
+ remove_children :field
80
+ [fields].flatten.each do |field|
81
+ self << (f = Field.new(field))
82
+ f.namespace = self.namespace
83
+ end
84
+ end
85
+
86
+ # Check if the x is of type :cancel
87
+ #
88
+ # @return [true, false]
89
+ def cancel?
90
+ self.type == :cancel
91
+ end
92
+
93
+ # Check if the x is of type :form
94
+ #
95
+ # @return [true, false]
96
+ def form?
97
+ self.type == :form
98
+ end
99
+
100
+ # Check if the x is of type :result
101
+ #
102
+ # @return [true, false]
103
+ def result?
104
+ self.type == :result
105
+ end
106
+
107
+ # Check if the x is of type :submit
108
+ #
109
+ # @return [true, false]
110
+ def submit?
111
+ self.type == :submit
112
+ end
113
+
114
+ # Retrieve the form's instructions
115
+ #
116
+ # @return [String]
117
+ def instructions
118
+ content_from 'ns:instructions', :ns => self.registered_ns
119
+ end
120
+
121
+ # Set the form's instructions
122
+ #
123
+ # @param [String] instructions the form's instructions
124
+ def instructions=(instructions)
125
+ self.remove_children :instructions
126
+ if instructions
127
+ self << (i = XMPPNode.new(:instructions, self.document))
128
+ i.namespace = self.namespace
129
+ i << instructions
130
+ end
131
+ end
132
+
133
+ # Retrieve the form's title
134
+ #
135
+ # @return [String]
136
+ def title
137
+ content_from 'ns:title', :ns => self.registered_ns
138
+ end
139
+
140
+ # Set the form's title
141
+ #
142
+ # @param [String] title the form's title
143
+ def title=(title)
144
+ self.remove_children :title
145
+ if title
146
+ self << (t = XMPPNode.new(:title))
147
+ t.namespace = self.namespace
148
+ t << title
149
+ end
150
+ end
151
+
152
+ class Field < XMPPNode
153
+ VALID_TYPES = [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"].freeze
154
+
155
+ # Create a new X Field
156
+ # @overload new(node)
157
+ # Imports the XML::Node to create a Field object
158
+ # @param [XML::Node] node the node object to import
159
+ # @overload new(opts = {})
160
+ # Creates a new Field using a hash of options
161
+ # @param [Hash] opts a hash of options
162
+ # @option opts [String] :var the variable for the field
163
+ # @option opts [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] :type the type of the field
164
+ # @option opts [String] :label the label for the field
165
+ # @option [String, nil] :value the value for the field
166
+ # @option [String, nil] :description the description for the field
167
+ # @option [true, false, nil] :required the required flag for the field
168
+ # @param [Array<Array, X::Field::Option>, nil] :options a list of field options.
169
+ # These are passed directly to X::Field::Option.new
170
+ # @overload new(type, var = nil, label = nil)
171
+ # Create a new Field by name
172
+ # @param [String, nil] var the variable for the field
173
+ # @param [:boolean, :fixed, :hidden, :"jid-multi", :"jid-single", :"list-multi", :"list-single", :"text-multi", :"text-private", :"text-single"] type the type of the field
174
+ # @param [String, nil] label the label for the field
175
+ # @param [String, nil] value the value for the field
176
+ # @param [String, nil] description the description for the field
177
+ # @param [true, false, nil] required the required flag for the field
178
+ # @param [Array<Array, X::Field::Option>, nil] options a list of field options.
179
+ # These are passed directly to X::Field::Option.new
180
+ def self.new(var, type = nil, label = nil, value = nil, description = nil, required = false, options = [])
181
+ new_node = super :field
182
+
183
+ case var
184
+ when Nokogiri::XML::Node
185
+ new_node.inherit var
186
+ when Hash
187
+ new_node.var = var[:var]
188
+ new_node.type = var[:type]
189
+ new_node.label = var[:label]
190
+ new_node.value = var[:value]
191
+ new_node.desc = var[:description]
192
+ new_node.required = var[:required]
193
+ new_node.options = var[:options]
194
+ else
195
+ new_node.var = var
196
+ new_node.type = type
197
+ new_node.label = label
198
+ new_node.value = value
199
+ new_node.desc = description
200
+ new_node.required = required
201
+ new_node.options = options
202
+ end
203
+ new_node
204
+ end
205
+
206
+ # The Field's type
207
+ # @return [String]
208
+ def type
209
+ read_attr :type
210
+ end
211
+
212
+ # Set the Field's type
213
+ # @param [#to_sym] type the new type for the field
214
+ def type=(type)
215
+ if type && !VALID_TYPES.include?(type.to_sym)
216
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
217
+ end
218
+ write_attr :type, type
219
+ end
220
+
221
+ # The Field's var
222
+ # @return [String]
223
+ def var
224
+ read_attr :var
225
+ end
226
+
227
+ # Set the Field's var
228
+ # @param [String] var the new var for the field
229
+ def var=(var)
230
+ write_attr :var, var
231
+ end
232
+
233
+ # The Field's label
234
+ # @return [String]
235
+ def label
236
+ read_attr :label
237
+ end
238
+
239
+ # Set the Field's label
240
+ # @param [String] label the new label for the field
241
+ def label=(label)
242
+ write_attr :label, label
243
+ end
244
+
245
+ # Get the field's value
246
+ #
247
+ # @param [String]
248
+ def value
249
+ if self.namespace
250
+ content_from 'ns:value', :ns => self.namespace.href
251
+ else
252
+ content_from :value
253
+ end
254
+ end
255
+
256
+ # Set the field's value
257
+ #
258
+ # @param [String] value the field's value
259
+ def value=(value)
260
+ self.remove_children :value
261
+ if value
262
+ self << (v = XMPPNode.new(:value))
263
+ v.namespace = self.namespace
264
+ v << value
265
+ end
266
+ end
267
+
268
+ # Get the field's description
269
+ #
270
+ # @param [String]
271
+ def desc
272
+ if self.namespace
273
+ content_from 'ns:desc', :ns => self.namespace.href
274
+ else
275
+ content_from :desc
276
+ end
277
+ end
278
+
279
+ # Set the field's description
280
+ #
281
+ # @param [String] description the field's description
282
+ def desc=(description)
283
+ self.remove_children :desc
284
+ if description
285
+ self << (d = XMPPNode.new(:desc))
286
+ d.namespace = self.namespace
287
+ d << description
288
+ end
289
+ end
290
+
291
+ # Get the field's required flag
292
+ #
293
+ # @param [true, false]
294
+ def required?
295
+ !self.find_first('required').nil?
296
+ end
297
+
298
+ # Set the field's required flag
299
+ #
300
+ # @param [true, false] required the field's required flag
301
+ def required=(required)
302
+ self.remove_children(:required) unless required
303
+ self << XMPPNode.new(:required) if required
304
+ end
305
+
306
+ # Extract list of option objects
307
+ #
308
+ # @return [Blather::Stanza::X::Field::Option]
309
+ def options
310
+ self.find(:option).map { |f| Option.new(f) }
311
+ end
312
+
313
+ # Add an array of options to field
314
+ # @param options the array of options, passed directly to Option.new
315
+ def options=(options)
316
+ remove_children :option
317
+ if options
318
+ [options].flatten.each { |o| self << Option.new(o) }
319
+ end
320
+ end
321
+
322
+ # Compare two Field objects by type, var and label
323
+ # @param [X::Field] o the Field object to compare against
324
+ # @return [true, false]
325
+ def eql?(o)
326
+ raise "Cannot compare #{self.class} with #{o.class}" unless o.is_a?(self.class)
327
+ ![:type, :var, :label, :desc, :required?, :value].detect { |m| o.send(m) != self.send(m) }
328
+ end
329
+ alias_method :==, :eql?
330
+
331
+ class Option < XMPPNode
332
+ # Create a new X Field Option
333
+ # @overload new(node)
334
+ # Imports the XML::Node to create a Field option object
335
+ # @param [XML::Node] node the node object to import
336
+ # @overload new(opts = {})
337
+ # Creates a new Field option using a hash of options
338
+ # @param [Hash] opts a hash of options
339
+ # @option opts [String] :value the value of the field option
340
+ # @option opts [String] :label the human readable label for the field option
341
+ # @overload new(value, label = nil)
342
+ # Create a new Field option by name
343
+ # @param [String] value the value of the field option
344
+ # @param [String, nil] label the human readable label for the field option
345
+ def self.new(value, label = nil)
346
+ new_node = super :option
347
+
348
+ case value
349
+ when Nokogiri::XML::Node
350
+ new_node.inherit value
351
+ when Hash
352
+ new_node.value = value[:value]
353
+ new_node.label = value[:label]
354
+ else
355
+ new_node.value = value
356
+ new_node.label = label
357
+ end
358
+ new_node
359
+ end
360
+
361
+ # The Field Option's value
362
+ # @return [String]
363
+ def value
364
+ if self.namespace
365
+ content_from 'ns:value', :ns => self.namespace.href
366
+ else
367
+ content_from :value
368
+ end
369
+ end
370
+
371
+ # Set the Field Option's value
372
+ # @param [String] value the new value for the field option
373
+ def value=(value)
374
+ self.remove_children :value
375
+ if value
376
+ self << (v = XMPPNode.new(:value))
377
+ v.namespace = self.namespace
378
+ v << value
379
+ end
380
+ end
381
+
382
+ # The Field Option's label
383
+ # @return [String]
384
+ def label
385
+ read_attr :label
386
+ end
387
+
388
+ # Set the Field Option's label
389
+ # @param [String] label the new label for the field option
390
+ def label=(label)
391
+ write_attr :label, label
392
+ end
393
+ end # Option
394
+ end # Field
395
+ end # X
396
+
397
+ end #Stanza
398
+ end