blather 0.4.7 → 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/README.md +162 -0
  2. data/examples/{print_heirarchy.rb → print_hierarchy.rb} +5 -5
  3. data/examples/stream_only.rb +27 -0
  4. data/lib/blather.rb +4 -0
  5. data/lib/blather/client/client.rb +91 -73
  6. data/lib/blather/client/dsl.rb +156 -32
  7. data/lib/blather/client/dsl/pubsub.rb +86 -54
  8. data/lib/blather/core_ext/active_support.rb +9 -9
  9. data/lib/blather/core_ext/active_support/inheritable_attributes.rb +2 -2
  10. data/lib/blather/core_ext/nokogiri.rb +12 -7
  11. data/lib/blather/errors.rb +25 -14
  12. data/lib/blather/errors/sasl_error.rb +21 -3
  13. data/lib/blather/errors/stanza_error.rb +37 -21
  14. data/lib/blather/errors/stream_error.rb +27 -17
  15. data/lib/blather/jid.rb +79 -24
  16. data/lib/blather/roster.rb +39 -21
  17. data/lib/blather/roster_item.rb +43 -21
  18. data/lib/blather/stanza.rb +88 -40
  19. data/lib/blather/stanza/disco.rb +12 -2
  20. data/lib/blather/stanza/disco/disco_info.rb +112 -20
  21. data/lib/blather/stanza/disco/disco_items.rb +81 -12
  22. data/lib/blather/stanza/iq.rb +94 -38
  23. data/lib/blather/stanza/iq/query.rb +16 -22
  24. data/lib/blather/stanza/iq/roster.rb +98 -20
  25. data/lib/blather/stanza/message.rb +266 -111
  26. data/lib/blather/stanza/presence.rb +118 -42
  27. data/lib/blather/stanza/presence/status.rb +140 -60
  28. data/lib/blather/stanza/presence/subscription.rb +44 -10
  29. data/lib/blather/stanza/pubsub.rb +70 -15
  30. data/lib/blather/stanza/pubsub/affiliations.rb +36 -7
  31. data/lib/blather/stanza/pubsub/create.rb +26 -4
  32. data/lib/blather/stanza/pubsub/errors.rb +13 -4
  33. data/lib/blather/stanza/pubsub/event.rb +56 -10
  34. data/lib/blather/stanza/pubsub/items.rb +46 -6
  35. data/lib/blather/stanza/pubsub/publish.rb +52 -7
  36. data/lib/blather/stanza/pubsub/retract.rb +45 -6
  37. data/lib/blather/stanza/pubsub/subscribe.rb +30 -4
  38. data/lib/blather/stanza/pubsub/subscription.rb +74 -6
  39. data/lib/blather/stanza/pubsub/subscriptions.rb +35 -9
  40. data/lib/blather/stanza/pubsub/unsubscribe.rb +30 -4
  41. data/lib/blather/stanza/pubsub_owner.rb +17 -7
  42. data/lib/blather/stanza/pubsub_owner/delete.rb +23 -5
  43. data/lib/blather/stanza/pubsub_owner/purge.rb +23 -5
  44. data/lib/blather/stream.rb +96 -29
  45. data/lib/blather/stream/parser.rb +6 -9
  46. data/lib/blather/xmpp_node.rb +101 -153
  47. data/spec/blather/client/client_spec.rb +1 -1
  48. data/spec/blather/errors_spec.rb +5 -5
  49. data/spec/blather/stanza/message_spec.rb +56 -0
  50. data/spec/blather/stanza/presence/status_spec.rb +1 -1
  51. data/spec/blather/stanza_spec.rb +3 -3
  52. data/spec/blather/xmpp_node_spec.rb +19 -74
  53. metadata +6 -10
  54. data/README.rdoc +0 -185
  55. data/examples/drb_client.rb +0 -5
  56. data/examples/ping.rb +0 -11
  57. data/examples/pong.rb +0 -6
  58. data/examples/pubsub/cli.rb +0 -64
  59. data/examples/pubsub/ping_pong.rb +0 -18
@@ -2,27 +2,37 @@ module Blather
2
2
  class Stanza
3
3
  class Iq
4
4
 
5
+ # # Query Stanza
6
+ #
7
+ # This is a base class for any query based Iq stanzas. It provides a base set
8
+ # of methods for working with query stanzas
9
+ #
10
+ # @handler :query
5
11
  class Query < Iq
6
12
  register :query, :query
7
13
 
8
- ##
9
- # Ensure the query node is created
14
+ # Overrides the parent method to ensure a query node is created
15
+ #
16
+ # @see Blather::Stanza::Iq.new
10
17
  def self.new(type = nil)
11
18
  node = super
12
19
  node.query
13
20
  node
14
21
  end
15
22
 
16
- ##
17
- # Kill the query node before running inherit
23
+ # Overrides the parent method to ensure the current query node is destroyed
24
+ #
25
+ # @see Blather::Stanza::Iq#inherit
18
26
  def inherit(node)
19
27
  query.remove
20
28
  super
21
29
  end
22
30
 
23
- ##
24
31
  # Query node accessor
25
- # This will ensure there actually is a query node
32
+ # If a query node exists it will be returned.
33
+ # Otherwise a new node will be created and returned
34
+ #
35
+ # @return [Balather::XMPPNode]
26
36
  def query
27
37
  q = if self.class.registered_ns
28
38
  find_first('query_ns:query', :query_ns => self.class.registered_ns)
@@ -36,22 +46,6 @@ class Iq
36
46
  end
37
47
  q
38
48
  end
39
-
40
- ##
41
- # A query reply should have type set to "result"
42
- def reply
43
- elem = super
44
- elem.type = :result
45
- elem
46
- end
47
-
48
- ##
49
- # A query reply should have type set to "result"
50
- def reply!
51
- super
52
- self.type = :result
53
- self
54
- end
55
49
  end #Query
56
50
 
57
51
  end #Iq
@@ -2,20 +2,28 @@ module Blather
2
2
  class Stanza
3
3
  class Iq
4
4
 
5
+ # # Roster Stanza
6
+ #
7
+ # [RFC 3921 Section 7 - Roster Management](http://xmpp.org/rfcs/rfc3921.html#roster)
8
+ #
9
+ # @handler :roster
5
10
  class Roster < Query
6
11
  register :roster, nil, 'jabber:iq:roster'
7
12
 
8
- ##
9
- # Any new items are added to the query
13
+ # Create a new roster stanza and (optionally) load it with an item
14
+ #
15
+ # @param [<Blather::Stanza::Iq::VALID_TYPES>] type the stanza type
16
+ # @param [Blather::XMPPNode] item a roster item
10
17
  def self.new(type = nil, item = nil)
11
18
  node = super type
12
19
  node.query << item if item
13
20
  node
14
21
  end
15
22
 
16
- ##
17
23
  # Inherit the XMPPNode to create a proper Roster object.
18
24
  # Creates RosterItem objects out of each roster item as well.
25
+ #
26
+ # @param [Blather::XMPPNode] node a node to inherit
19
27
  def inherit(node)
20
28
  # remove the current set of nodes
21
29
  remove_children :item
@@ -25,18 +33,41 @@ class Iq
25
33
  self
26
34
  end
27
35
 
28
- ##
29
- # Roster items
36
+ # The list of roster items
37
+ #
38
+ # @return [Array<Blather::Stanza::Iq::Roster::RosterItem>]
30
39
  def items
31
- query.find('//ns:item', :ns => self.class.registered_ns).map { |i| RosterItem.new(i) }
40
+ query.find('//ns:item', :ns => self.class.registered_ns).map do |i|
41
+ RosterItem.new i
42
+ end
32
43
  end
33
44
 
45
+ # # RosterItem Fragment
46
+ #
47
+ # Individual roster items.
48
+ # This is a convenience class to attach methods to the node
34
49
  class RosterItem < XMPPNode
35
- ##
36
- # [jid] may be either a JID or XMPPNode.
37
- # [name] name alias of the given JID
38
- # [subscription] subscription type
39
- # [ask] ask subscription sub-state
50
+
51
+ # Create a new RosterItem
52
+ # @overload new(XML::Node)
53
+ # Create a RosterItem by inheriting a node
54
+ # @param [XML::Node] node an xml node to inherit
55
+ # @overload new(opts)
56
+ # Create a RosterItem through a hash of options
57
+ # @param [Hash] opts the options
58
+ # @option opts [Blather::JID, String, nil] :jid the JID of the item
59
+ # @option opts [String, nil] :name the alias to give the JID
60
+ # @option opts [Symbol, nil] :subscription the subscription status of
61
+ # the RosterItem must be one of
62
+ # Blather::RosterItem::VALID_SUBSCRIPTION_TYPES
63
+ # @option opts [:subscribe, nil] :ask the ask value of the RosterItem
64
+ # @overload new(jid = nil, name = nil, subscription = nil, ask = nil)
65
+ # @param [Blather::JID, String, nil] jid the JID of the item
66
+ # @param [String, nil] name the alias to give the JID
67
+ # @param [Symbol, nil] subscription the subscription status of the
68
+ # RosterItem must be one of
69
+ # Blather::RosterItem::VALID_SUBSCRIPTION_TYPES
70
+ # @param [:subscribe, nil] ask the ask value of the RosterItem
40
71
  def self.new(jid = nil, name = nil, subscription = nil, ask = nil)
41
72
  new_node = super :item
42
73
 
@@ -57,26 +88,72 @@ class Iq
57
88
  new_node
58
89
  end
59
90
 
60
- ##
61
- # Roster item's JID
91
+ # Get the JID attached to the item
92
+ #
93
+ # @return [Blather::JID, nil]
62
94
  def jid
63
95
  (j = self[:jid]) ? JID.new(j) : nil
64
96
  end
65
- attribute_writer :jid
66
97
 
67
- attribute_accessor :name
98
+ # Set the JID of the item
99
+ #
100
+ # @param [Blather::JID, String, nil] jid the new JID
101
+ def jid=(jid)
102
+ write_attr :jid, jid
103
+ end
104
+
105
+ # Get the item name
106
+ #
107
+ # @return [String, nil]
108
+ def name
109
+ read_attr :name
110
+ end
111
+
112
+ # Set the item name
113
+ #
114
+ # @param [#to_s] name the name of the item
115
+ def name=(name)
116
+ write_attr :name, name
117
+ end
118
+
119
+ # Get the subscription value of the item
120
+ #
121
+ # @return [<:both, :from, :none, :remove, :to>]
122
+ def subscription
123
+ read_attr :subscription, :to_sym
124
+ end
125
+
126
+ # Set the subscription value of the item
127
+ #
128
+ # @param [<:both, :from, :none, :remove, :to>] subscription
129
+ def subscription=(subscription)
130
+ write_attr :subscription, subscription
131
+ end
68
132
 
69
- attribute_accessor :subscription, :ask, :call => :to_sym
133
+ # Get the ask value of the item
134
+ #
135
+ # @return [<:subscribe, nil>]
136
+ def ask
137
+ read_attr :ask, :to_sym
138
+ end
139
+
140
+ # Set the ask value of the item
141
+ #
142
+ # @param [<:subscribe, nil>] ask
143
+ def ask=(ask)
144
+ write_attr :ask, ask
145
+ end
70
146
 
71
- ##
72
147
  # The groups roster item belongs to
148
+ #
149
+ # @return [Array<String>]
73
150
  def groups
74
151
  find('child::*[local-name()="group"]').map { |g| g.content }
75
152
  end
76
153
 
77
- ##
78
154
  # Set the roster item's groups
79
- # must be an array
155
+ #
156
+ # @param [Array<#to_s>] new_groups an array of group names
80
157
  def groups=(new_groups)
81
158
  remove_children :group
82
159
  if new_groups
@@ -87,9 +164,10 @@ class Iq
87
164
  end
88
165
  end
89
166
 
90
- ##
91
167
  # Convert the roster item to a proper stanza all wrapped up
92
168
  # This facilitates new subscriptions
169
+ #
170
+ # @return [Blather::Stanza::Iq::Roster]
93
171
  def to_stanza
94
172
  Roster.new(:set, self)
95
173
  end
@@ -1,138 +1,176 @@
1
1
  module Blather
2
2
  class Stanza
3
3
 
4
- # Exchanging messages is a basic use of XMPP and occurs when a user generates a message stanza
5
- # that is addressed to another entity. The sender's server is responsible for delivering the
6
- # message to the intended recipient (if the recipient is on the same local server) or for routing
7
- # the message to the recipient's server (if the recipient is on a remote server). Thus a message
8
- # stanza is used to "push" information to another entity.
9
- #
10
- # == To Attribute
11
- #
12
- # An instant messaging client specifies an intended recipient for a message by providing the JID
13
- # of an entity other than the sender in the +to+ attribute of the Message stanza. If the message
14
- # is being sent outside the context of any existing chat session or received message, the value
15
- # of the +to+ address SHOULD be of the form "user@domain" rather than of the form "user@domain/resource".
16
- #
17
- # msg = Message.new 'user@domain.tld/resource'
18
- # msg.to == 'user@domain.tld/resource'
19
- #
20
- # msg.to = 'another-user@some-domain.tld/resource'
21
- # msg.to == 'another-user@some-domain.tld/resource'
22
- #
23
- # The +to+ attribute on a Message stanza works like any regular ruby object attribute
24
- #
25
- # == Type Attribute
26
- #
27
- # Common uses of the message stanza in instant messaging applications include: single messages;
28
- # messages sent in the context of a one-to-one chat session; messages sent in the context of a
29
- # multi-user chat room; alerts, notifications, or other information to which no reply is expected;
30
- # and errors. These uses are differentiated via the +type+ attribute. If included, the +type+
31
- # attribute MUST have one of the following values:
32
- #
33
- # * +:chat+ -- The message is sent in the context of a one-to-one chat session. Typically a receiving
34
- # client will present message of type +chat+ in an interface that enables one-to-one chat between
35
- # the two parties, including an appropriate conversation history.
36
- # * +:error+ -- The message is generated by an entity that experiences an error in processing a message
37
- # received from another entity. A client that receives a message of type +error+ SHOULD present an
38
- # appropriate interface informing the sender of the nature of the error.
39
- # * +:groupchat+ -- The message is sent in the context of a multi-user chat environment (similar to that
40
- # of [IRC]). Typically a receiving client will present a message of type +groupchat+ in an interface
41
- # that enables many-to-many chat between the parties, including a roster of parties in the chatroom
42
- # and an appropriate conversation history.
43
- # * +:headline+ -- The message provides an alert, a notification, or other information to which no reply
44
- # is expected (e.g., news headlines, sports updates, near-real-time market data, and syndicated content).
45
- # Because no reply to the message is expected, typically a receiving client will present a message of
46
- # type "headline" in an interface that appropriately differentiates the message from standalone messages,
47
- # chat messages, or groupchat messages (e.g., by not providing the recipient with the ability to reply).
48
- # * +:normal+ -- The message is a standalone message that is sent outside the context of a one-to-one
49
- # conversation or groupchat, and to which it is expected that the recipient will reply. Typically a receiving
50
- # client will present a message of type +normal+ in an interface that enables the recipient to reply, but
51
- # without a conversation history. The default value of the +type+ attribute is +normal+.
4
+ # # Message Stanza
5
+ #
6
+ # [RFC 3921 Section 2.1 - Message Syntax](http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.1)
7
+ #
8
+ # Exchanging messages is a basic use of XMPP and occurs when a user
9
+ # generates a message stanza that is addressed to another entity. The
10
+ # sender's server is responsible for delivering the message to the intended
11
+ # recipient (if the recipient is on the same local server) or for routing
12
+ # the message to the recipient's server (if the recipient is on a remote
13
+ # server). Thus a message stanza is used to "push" information to another
14
+ # entity.
15
+ #
16
+ # ## "To" Attribute
17
+ #
18
+ # An instant messaging client specifies an intended recipient for a message
19
+ # by providing the JID of an entity other than the sender in the `to`
20
+ # attribute of the Message stanza. If the message is being sent outside the
21
+ # context of any existing chat session or received message, the value of the
22
+ # `to` address SHOULD be of the form "user@domain" rather than of the form
23
+ # "user@domain/resource".
24
+ #
25
+ # msg = Message.new 'user@domain.tld/resource'
26
+ # msg.to == 'user@domain.tld/resource'
27
+ #
28
+ # msg.to = 'another-user@some-domain.tld/resource'
29
+ # msg.to == 'another-user@some-domain.tld/resource'
30
+ #
31
+ # The `to` attribute on a Message stanza works like any regular ruby object
32
+ # attribute
33
+ #
34
+ # ## "Type" Attribute
35
+ #
36
+ # Common uses of the message stanza in instant messaging applications
37
+ # include: single messages; messages sent in the context of a one-to-one
38
+ # chat session; messages sent in the context of a multi-user chat room;
39
+ # alerts, notifications, or other information to which no reply is expected;
40
+ # and errors. These uses are differentiated via the `type` attribute. If
41
+ # included, the `type` attribute MUST have one of the following values:
42
+ #
43
+ # * `:chat` -- The message is sent in the context of a one-to-one chat
44
+ # session. Typically a receiving client will present message of type
45
+ # `chat` in an interface that enables one-to-one chat between the two
46
+ # parties, including an appropriate conversation history.
47
+ #
48
+ # * `:error` -- The message is generated by an entity that experiences an
49
+ # error in processing a message received from another entity. A client
50
+ # that receives a message of type `error` SHOULD present an appropriate
51
+ # interface informing the sender of the nature of the error.
52
+ #
53
+ # * `:groupchat` -- The message is sent in the context of a multi-user chat
54
+ # environment (similar to that of [IRC]). Typically a receiving client
55
+ # will present a message of type `groupchat` in an interface that enables
56
+ # many-to-many chat between the parties, including a roster of parties in
57
+ # the chatroom and an appropriate conversation history.
58
+ #
59
+ # * `:headline` -- The message provides an alert, a notification, or other
60
+ # information to which no reply is expected (e.g., news headlines, sports
61
+ # updates, near-real-time market data, and syndicated content). Because no
62
+ # reply to the message is expected, typically a receiving client will
63
+ # present a message of type "headline" in an interface that appropriately
64
+ # differentiates the message from standalone messages, chat messages, or
65
+ # groupchat messages (e.g., by not providing the recipient with the
66
+ # ability to reply).
67
+ #
68
+ # * `:normal` -- The message is a standalone message that is sent outside
69
+ # the context of a one-to-one conversation or groupchat, and to which it
70
+ # is expected that the recipient will reply. Typically a receiving client
71
+ # will present a message of type `normal` in an interface that enables the
72
+ # recipient to reply, but without a conversation history. The default
73
+ # value of the `type` attribute is `normal`.
52
74
  #
53
75
  # Blather provides a helper for each possible type:
54
76
  #
55
- # Message#chat?
56
- # Message#error?
57
- # Message#groupchat?
58
- # Message#headline?
59
- # Message#normal?
77
+ # Message#chat?
78
+ # Message#error?
79
+ # Message#groupchat?
80
+ # Message#headline?
81
+ # Message#normal?
82
+ #
83
+ # Blather treats the `type` attribute like a normal ruby object attribute
84
+ # providing a getter and setter. The default `type` is `chat`.
60
85
  #
61
- # Blather treats the +type+ attribute like a normal ruby object attribute providing a getter and setter.
62
- # The default +type+ is +chat+.
86
+ # msg = Message.new
87
+ # msg.type # => :chat
88
+ # msg.chat? # => true
89
+ # msg.type = :normal
90
+ # msg.normal? # => true
91
+ # msg.chat? # => false
63
92
  #
64
- # msg = Message.new
65
- # msg.type # => :chat
66
- # msg.chat? # => true
67
- # msg.type = :normal
68
- # msg.normal? # => true
69
- # msg.chat? # => false
93
+ # msg.type = :invalid # => RuntimeError
70
94
  #
71
- # msg.type = :invalid # => RuntimeError
72
95
  #
73
- # == Body Element
96
+ # ## "Body" Element
74
97
  #
75
- # The +body+ element contains human-readable XML character data that specifies the textual contents of the message;
76
- # this child element is normally included but is optional.
98
+ # The `body` element contains human-readable XML character data that
99
+ # specifies the textual contents of the message; this child element is
100
+ # normally included but is optional.
77
101
  #
78
- # Blather provides an attribute-like syntax for Message +body+ elements.
102
+ # Blather provides an attribute-like syntax for Message `body` elements.
79
103
  #
80
- # msg = Message.new 'user@domain.tld', 'message body'
81
- # msg.body # => 'message body'
104
+ # msg = Message.new 'user@domain.tld', 'message body'
105
+ # msg.body # => 'message body'
82
106
  #
83
- # msg.body = 'other message'
84
- # msg.body # => 'other message'
107
+ # msg.body = 'other message'
108
+ # msg.body # => 'other message'
85
109
  #
86
- # == Subject Element
110
+ # ## "Subject" Element
87
111
  #
88
- # The +subject+ element contains human-readable XML character data that specifies the topic of the message.
112
+ # The `subject` element contains human-readable XML character data that
113
+ # specifies the topic of the message.
89
114
  #
90
- # Blather provides an attribute-like syntax for Message +subject+ elements.
115
+ # Blather provides an attribute-like syntax for Message `subject` elements.
91
116
  #
92
- # msg = Message.new 'user@domain.tld', 'message subject'
93
- # msg.subject # => 'message subject'
117
+ # msg = Message.new 'user@domain.tld', 'message body'
118
+ # msg.subject = 'message subject'
119
+ # msg.subject # => 'message subject'
94
120
  #
95
- # msg.subject = 'other subject'
96
- # msg.subject # => 'other subject'
121
+ # ## "Thread" Element
97
122
  #
98
- # == Thread Element
123
+ # The primary use of the XMPP `thread` element is to uniquely identify a
124
+ # conversation thread or "chat session" between two entities instantiated by
125
+ # Message stanzas of type `chat`. However, the XMPP thread element can also
126
+ # be used to uniquely identify an analogous thread between two entities
127
+ # instantiated by Message stanzas of type `headline` or `normal`, or among
128
+ # multiple entities in the context of a multi-user chat room instantiated by
129
+ # Message stanzas of type `groupchat`. It MAY also be used for Message
130
+ # stanzas not related to a human conversation, such as a game session or an
131
+ # interaction between plugins. The `thread` element is not used to identify
132
+ # individual messages, only conversations or messagingg sessions. The
133
+ # inclusion of the `thread` element is optional.
99
134
  #
100
- # The primary use of the XMPP +thread+ element is to uniquely identify a conversation thread or "chat session"
101
- # between two entities instantiated by Message stanzas of type +chat+. However, the XMPP thread element can
102
- # also be used to uniquely identify an analogous thread between two entities instantiated by Message stanzas
103
- # of type +headline+ or +normal+, or among multiple entities in the context of a multi-user chat room instantiated
104
- # by Message stanzas of type +groupchat+. It MAY also be used for Message stanzas not related to a human
105
- # conversation, such as a game session or an interaction between plugins. The +thread+ element is not used to
106
- # identify individual messages, only conversations or messagingg sessions. The inclusion of the +thread+ element
107
- # is optional.
135
+ # The value of the `thread` element is not human-readable and MUST be
136
+ # treated as opaque by entities; no semantic meaning can be derived from it,
137
+ # and only exact comparisons can be made against it. The value of the
138
+ # `thread` element MUST be a universally unique identifier (UUID) as
139
+ # described in [UUID].
108
140
  #
109
- # The value of the +thread+ element is not human-readable and MUST be treated as opaque by entities; no semantic
110
- # meaning can be derived from it, and only exact comparisons can be made against it. The value of the +thread+
111
- # element MUST be a universally unique identifier (UUID) as described in [UUID].
141
+ # The `thread` element MAY possess a 'parent' attribute that identifies
142
+ # another thread of which the current thread is an offshoot or child; the
143
+ # value of the 'parent' must conform to the syntax of the `thread` element
144
+ # itself.
112
145
  #
113
- # The +thread+ element MAY possess a 'parent' attribute that identifies another thread of which the current
114
- # thread is an offshoot or child; the value of the 'parent' must conform to the syntax of the +thread+ element itself.
146
+ # Blather provides an attribute-like syntax for Message `thread` elements.
115
147
  #
116
- # Blather provides an attribute-like syntax for Message +thread+ elements.
117
- #
118
- # msg = Message.new
119
- # msg.thread = '12345'
120
- # msg.thread # => '12345'
148
+ # msg = Message.new
149
+ # msg.thread = '12345'
150
+ # msg.thread # => '12345'
121
151
  #
122
152
  # Parent threads can be set using a hash:
123
153
  #
124
- # msg.thread = {'parent-id' => 'thread-id'}
125
- # msg.thread # => 'thread-id'
126
- # msg.parent_thread # => 'parent-id'
154
+ # msg.thread = {'parent-id' => 'thread-id'}
155
+ # msg.thread # => 'thread-id'
156
+ # msg.parent_thread # => 'parent-id'
127
157
  #
158
+ # @handler :message
128
159
  class Message < Stanza
129
- VALID_TYPES = [:chat, :error, :groupchat, :headline, :normal] # :nodoc:
160
+ VALID_TYPES = [:chat, :error, :groupchat, :headline, :normal].freeze
161
+
162
+ HTML_NS = 'http://jabber.org/protocol/xhtml-im'.freeze
163
+ HTML_BODY_NS = 'http://www.w3.org/1999/xhtml'.freeze
130
164
 
131
165
  register :message
132
166
 
133
- def self.import(node) # :nodoc:
167
+ # @private
168
+ def self.import(node)
134
169
  klass = nil
135
- node.children.each { |e| break if klass = class_from_registration(e.element_name, (e.namespace.href if e.namespace)) }
170
+ node.children.detect do |e|
171
+ ns = e.namespace ? e.namespace.href : nil
172
+ klass = class_from_registration(e.element_name, ns)
173
+ end
136
174
 
137
175
  if klass && klass != self
138
176
  klass.import(node)
@@ -141,6 +179,11 @@ class Stanza
141
179
  end
142
180
  end
143
181
 
182
+ # Create a new Message stanza
183
+ #
184
+ # @param [#to_s] to the JID to send the message to
185
+ # @param [#to_s] body the body of the message
186
+ # @param [Symbol] type the message type. Must be one of VALID_TYPES
144
187
  def self.new(to = nil, body = nil, type = :chat)
145
188
  node = super :message
146
189
  node.to = to
@@ -149,29 +192,141 @@ class Stanza
149
192
  node
150
193
  end
151
194
 
152
- attribute_helpers_for :type, VALID_TYPES
195
+ # Check if the Message is of type :chat
196
+ #
197
+ # @return [true, false]
198
+ def chat?
199
+ self.type == :chat
200
+ end
153
201
 
154
- def type=(type) # :nodoc:
155
- raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" if type && !VALID_TYPES.include?(type.to_sym)
202
+ # Check if the Message is of type :error
203
+ #
204
+ # @return [true, false]
205
+ def error?
206
+ self.type == :error
207
+ end
208
+
209
+ # Check if the Message is of type :groupchat
210
+ #
211
+ # @return [true, false]
212
+ def groupchat?
213
+ self.type == :groupchat
214
+ end
215
+
216
+ # Check if the Message is of type :headline
217
+ #
218
+ # @return [true, false]
219
+ def headline?
220
+ self.type == :headline
221
+ end
222
+
223
+ # Check if the Message is of type :normal
224
+ #
225
+ # @return [true, false]
226
+ def normal?
227
+ self.type == :normal
228
+ end
229
+
230
+ # Ensures type is :get, :set, :result or :error
231
+ #
232
+ # @param [#to_sym] type the Message type. Must be one of VALID_TYPES
233
+ def type=(type)
234
+ if type && !VALID_TYPES.include?(type.to_sym)
235
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
236
+ end
156
237
  super
157
238
  end
158
239
 
159
- content_attr_accessor :body
160
- content_attr_accessor :subject
240
+ # Get the message body
241
+ #
242
+ # @return [String]
243
+ def body
244
+ read_content :body
245
+ end
246
+
247
+ # Set the message body
248
+ #
249
+ # @param [#to_s] body the message body
250
+ def body=(body)
251
+ set_content_for :body, body
252
+ end
253
+
254
+ # Get the message xhtml node
255
+ # This will create the node if it doesn't exist
256
+ #
257
+ # @return [XML::Node]
258
+ def xhtml_node
259
+ unless h = find_first('ns:html', :ns => HTML_NS)
260
+ self << (h = XMPPNode.new('html', self.document))
261
+ h.namespace = HTML_NS
262
+ end
263
+
264
+ unless b = h.find_first('ns:body', :ns => HTML_BODY_NS)
265
+ h << (b = XMPPNode.new('body', self.document))
266
+ b.namespace = HTML_BODY_NS
267
+ end
268
+
269
+ b
270
+ end
271
+
272
+ # Get the message xhtml
273
+ #
274
+ # @return [String]
275
+ def xhtml
276
+ self.xhtml_node.content.strip
277
+ end
278
+
279
+ # Set the message xhtml
280
+ # This will use Nokogiri to ensure the xhtml is valid
281
+ #
282
+ # @param [#to_s] valid xhtml
283
+ def xhtml=(xhtml_body)
284
+ self.xhtml_node.content = Nokogiri::XML(xhtml_body).to_xhtml
285
+ end
161
286
 
162
- content_attr_reader :thread
287
+ # Get the message subject
288
+ #
289
+ # @return [String]
290
+ def subject
291
+ read_content :subject
292
+ end
293
+
294
+ # Set the message subject
295
+ #
296
+ # @param [#to_s] body the message subject
297
+ def subject=(subject)
298
+ set_content_for :subject, subject
299
+ end
300
+
301
+ # Get the message thread
302
+ #
303
+ # @return [String]
304
+ def thread
305
+ read_content :thread
306
+ end
163
307
 
164
- def parent_thread # :nodoc:
308
+ # Get the parent thread
309
+ #
310
+ # @return [String, nil]
311
+ def parent_thread
165
312
  n = find_first('thread')
166
313
  n[:parent] if n
167
314
  end
168
315
 
169
- def thread=(thread) # :nodoc:
316
+ # Set the thread
317
+ #
318
+ # @overload thread=(hash)
319
+ # Set a thread with a parent
320
+ # @param [Hash<parent-id => thread-id>] thread
321
+ # @overload thread=(thread)
322
+ # Set a thread id
323
+ # @param [#to_s] thread the new thread id
324
+ def thread=(thread)
170
325
  parent, thread = thread.to_a.flatten if thread.is_a?(Hash)
171
326
  set_content_for :thread, thread
172
327
  find_first('thread')[:parent] = parent
173
328
  end
174
329
  end
175
330
 
176
- end #Stanza
331
+ end
177
332
  end