shingara-blather 0.4.9 → 0.4.14

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