shingara-blather 0.4.8

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.
Files changed (100) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +162 -0
  3. data/examples/echo.rb +18 -0
  4. data/examples/execute.rb +16 -0
  5. data/examples/ping_pong.rb +37 -0
  6. data/examples/print_hierarchy.rb +76 -0
  7. data/examples/rosterprint.rb +14 -0
  8. data/examples/stream_only.rb +27 -0
  9. data/examples/xmpp4r/echo.rb +35 -0
  10. data/lib/blather/client/client.rb +310 -0
  11. data/lib/blather/client/dsl/pubsub.rb +170 -0
  12. data/lib/blather/client/dsl.rb +264 -0
  13. data/lib/blather/client.rb +87 -0
  14. data/lib/blather/core_ext/nokogiri.rb +40 -0
  15. data/lib/blather/errors/sasl_error.rb +43 -0
  16. data/lib/blather/errors/stanza_error.rb +107 -0
  17. data/lib/blather/errors/stream_error.rb +82 -0
  18. data/lib/blather/errors.rb +69 -0
  19. data/lib/blather/jid.rb +142 -0
  20. data/lib/blather/roster.rb +111 -0
  21. data/lib/blather/roster_item.rb +122 -0
  22. data/lib/blather/stanza/disco/disco_info.rb +176 -0
  23. data/lib/blather/stanza/disco/disco_items.rb +132 -0
  24. data/lib/blather/stanza/disco.rb +25 -0
  25. data/lib/blather/stanza/iq/query.rb +53 -0
  26. data/lib/blather/stanza/iq/roster.rb +179 -0
  27. data/lib/blather/stanza/iq.rb +138 -0
  28. data/lib/blather/stanza/message.rb +332 -0
  29. data/lib/blather/stanza/presence/status.rb +212 -0
  30. data/lib/blather/stanza/presence/subscription.rb +101 -0
  31. data/lib/blather/stanza/presence.rb +163 -0
  32. data/lib/blather/stanza/pubsub/affiliations.rb +79 -0
  33. data/lib/blather/stanza/pubsub/create.rb +65 -0
  34. data/lib/blather/stanza/pubsub/errors.rb +18 -0
  35. data/lib/blather/stanza/pubsub/event.rb +123 -0
  36. data/lib/blather/stanza/pubsub/items.rb +103 -0
  37. data/lib/blather/stanza/pubsub/publish.rb +103 -0
  38. data/lib/blather/stanza/pubsub/retract.rb +92 -0
  39. data/lib/blather/stanza/pubsub/subscribe.rb +68 -0
  40. data/lib/blather/stanza/pubsub/subscription.rb +134 -0
  41. data/lib/blather/stanza/pubsub/subscriptions.rb +81 -0
  42. data/lib/blather/stanza/pubsub/unsubscribe.rb +68 -0
  43. data/lib/blather/stanza/pubsub.rb +129 -0
  44. data/lib/blather/stanza/pubsub_owner/delete.rb +52 -0
  45. data/lib/blather/stanza/pubsub_owner/purge.rb +52 -0
  46. data/lib/blather/stanza/pubsub_owner.rb +51 -0
  47. data/lib/blather/stanza.rb +149 -0
  48. data/lib/blather/stream/client.rb +31 -0
  49. data/lib/blather/stream/component.rb +38 -0
  50. data/lib/blather/stream/features/resource.rb +63 -0
  51. data/lib/blather/stream/features/sasl.rb +187 -0
  52. data/lib/blather/stream/features/session.rb +44 -0
  53. data/lib/blather/stream/features/tls.rb +28 -0
  54. data/lib/blather/stream/features.rb +53 -0
  55. data/lib/blather/stream/parser.rb +102 -0
  56. data/lib/blather/stream.rb +231 -0
  57. data/lib/blather/xmpp_node.rb +218 -0
  58. data/lib/blather.rb +78 -0
  59. data/spec/blather/client/client_spec.rb +559 -0
  60. data/spec/blather/client/dsl/pubsub_spec.rb +462 -0
  61. data/spec/blather/client/dsl_spec.rb +143 -0
  62. data/spec/blather/core_ext/nokogiri_spec.rb +83 -0
  63. data/spec/blather/errors/sasl_error_spec.rb +33 -0
  64. data/spec/blather/errors/stanza_error_spec.rb +129 -0
  65. data/spec/blather/errors/stream_error_spec.rb +108 -0
  66. data/spec/blather/errors_spec.rb +33 -0
  67. data/spec/blather/jid_spec.rb +87 -0
  68. data/spec/blather/roster_item_spec.rb +96 -0
  69. data/spec/blather/roster_spec.rb +103 -0
  70. data/spec/blather/stanza/discos/disco_info_spec.rb +226 -0
  71. data/spec/blather/stanza/discos/disco_items_spec.rb +148 -0
  72. data/spec/blather/stanza/iq/query_spec.rb +64 -0
  73. data/spec/blather/stanza/iq/roster_spec.rb +140 -0
  74. data/spec/blather/stanza/iq_spec.rb +45 -0
  75. data/spec/blather/stanza/message_spec.rb +132 -0
  76. data/spec/blather/stanza/presence/status_spec.rb +132 -0
  77. data/spec/blather/stanza/presence/subscription_spec.rb +105 -0
  78. data/spec/blather/stanza/presence_spec.rb +66 -0
  79. data/spec/blather/stanza/pubsub/affiliations_spec.rb +57 -0
  80. data/spec/blather/stanza/pubsub/create_spec.rb +56 -0
  81. data/spec/blather/stanza/pubsub/event_spec.rb +84 -0
  82. data/spec/blather/stanza/pubsub/items_spec.rb +79 -0
  83. data/spec/blather/stanza/pubsub/publish_spec.rb +83 -0
  84. data/spec/blather/stanza/pubsub/retract_spec.rb +75 -0
  85. data/spec/blather/stanza/pubsub/subscribe_spec.rb +61 -0
  86. data/spec/blather/stanza/pubsub/subscription_spec.rb +97 -0
  87. data/spec/blather/stanza/pubsub/subscriptions_spec.rb +59 -0
  88. data/spec/blather/stanza/pubsub/unsubscribe_spec.rb +61 -0
  89. data/spec/blather/stanza/pubsub_owner/delete_spec.rb +50 -0
  90. data/spec/blather/stanza/pubsub_owner/purge_spec.rb +50 -0
  91. data/spec/blather/stanza/pubsub_owner_spec.rb +27 -0
  92. data/spec/blather/stanza/pubsub_spec.rb +67 -0
  93. data/spec/blather/stanza_spec.rb +116 -0
  94. data/spec/blather/stream/client_spec.rb +1011 -0
  95. data/spec/blather/stream/component_spec.rb +95 -0
  96. data/spec/blather/stream/parser_spec.rb +145 -0
  97. data/spec/blather/xmpp_node_spec.rb +231 -0
  98. data/spec/fixtures/pubsub.rb +311 -0
  99. data/spec/spec_helper.rb +43 -0
  100. metadata +249 -0
@@ -0,0 +1,53 @@
1
+ module Blather
2
+ class Stanza
3
+ class Iq
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
11
+ class Query < Iq
12
+ register :query, :query
13
+
14
+ # Overrides the parent method to ensure a query node is created
15
+ #
16
+ # @see Blather::Stanza::Iq.new
17
+ def self.new(type = nil)
18
+ node = super
19
+ node.query
20
+ node
21
+ end
22
+
23
+ # Overrides the parent method to ensure the current query node is destroyed
24
+ #
25
+ # @see Blather::Stanza::Iq#inherit
26
+ def inherit(node)
27
+ query.remove
28
+ super
29
+ end
30
+
31
+ # Query node accessor
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]
36
+ def query
37
+ q = if self.class.registered_ns
38
+ find_first('query_ns:query', :query_ns => self.class.registered_ns)
39
+ else
40
+ find_first('query')
41
+ end
42
+
43
+ unless q
44
+ (self << (q = XMPPNode.new('query', self.document)))
45
+ q.namespace = self.class.registered_ns
46
+ end
47
+ q
48
+ end
49
+ end #Query
50
+
51
+ end #Iq
52
+ end #Stanza
53
+ end
@@ -0,0 +1,179 @@
1
+ module Blather
2
+ class Stanza
3
+ class Iq
4
+
5
+ # # Roster Stanza
6
+ #
7
+ # [RFC 3921 Section 7 - Roster Management](http://xmpp.org/rfcs/rfc3921.html#roster)
8
+ #
9
+ # @handler :roster
10
+ class Roster < Query
11
+ register :roster, nil, 'jabber:iq:roster'
12
+
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
17
+ def self.new(type = nil, item = nil)
18
+ node = super type
19
+ node.query << item if item
20
+ node
21
+ end
22
+
23
+ # Inherit the XMPPNode to create a proper Roster object.
24
+ # Creates RosterItem objects out of each roster item as well.
25
+ #
26
+ # @param [Blather::XMPPNode] node a node to inherit
27
+ def inherit(node)
28
+ # remove the current set of nodes
29
+ remove_children :item
30
+ super
31
+ # transmogrify nodes into RosterItems
32
+ items.each { |i| query << RosterItem.new(i); i.remove }
33
+ self
34
+ end
35
+
36
+ # The list of roster items
37
+ #
38
+ # @return [Array<Blather::Stanza::Iq::Roster::RosterItem>]
39
+ def items
40
+ query.find('//ns:item', :ns => self.class.registered_ns).map do |i|
41
+ RosterItem.new i
42
+ end
43
+ end
44
+
45
+ # # RosterItem Fragment
46
+ #
47
+ # Individual roster items.
48
+ # This is a convenience class to attach methods to the node
49
+ class RosterItem < XMPPNode
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
71
+ def self.new(jid = nil, name = nil, subscription = nil, ask = nil)
72
+ new_node = super :item
73
+
74
+ case jid
75
+ when Nokogiri::XML::Node
76
+ new_node.inherit jid
77
+ when Hash
78
+ new_node.jid = jid[:jid]
79
+ new_node.name = jid[:name]
80
+ new_node.subscription = jid[:subscription]
81
+ new_node.ask = jid[:ask]
82
+ else
83
+ new_node.jid = jid
84
+ new_node.name = name
85
+ new_node.subscription = subscription
86
+ new_node.ask = ask
87
+ end
88
+ new_node
89
+ end
90
+
91
+ # Get the JID attached to the item
92
+ #
93
+ # @return [Blather::JID, nil]
94
+ def jid
95
+ (j = self[:jid]) ? JID.new(j) : nil
96
+ end
97
+
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
132
+
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
146
+
147
+ # The groups roster item belongs to
148
+ #
149
+ # @return [Array<String>]
150
+ def groups
151
+ find('child::*[local-name()="group"]').map { |g| g.content }
152
+ end
153
+
154
+ # Set the roster item's groups
155
+ #
156
+ # @param [Array<#to_s>] new_groups an array of group names
157
+ def groups=(new_groups)
158
+ remove_children :group
159
+ if new_groups
160
+ new_groups.uniq.each do |g|
161
+ self << (group = XMPPNode.new(:group, self.document))
162
+ group.content = g
163
+ end
164
+ end
165
+ end
166
+
167
+ # Convert the roster item to a proper stanza all wrapped up
168
+ # This facilitates new subscriptions
169
+ #
170
+ # @return [Blather::Stanza::Iq::Roster]
171
+ def to_stanza
172
+ Roster.new(:set, self)
173
+ end
174
+ end #RosterItem
175
+ end #Roster
176
+
177
+ end #Iq
178
+ end #Stanza
179
+ end
@@ -0,0 +1,138 @@
1
+ module Blather
2
+ class Stanza
3
+
4
+ # # Iq Stanza
5
+ #
6
+ # [RFC 3920 Section 9.2.3 - IQ Semantics](http://xmpp.org/rfcs/rfc3920.html#rfc.section.9.2.3)
7
+ #
8
+ # Info/Query, or IQ, is a request-response mechanism, similar in some ways
9
+ # to HTTP. The semantics of IQ enable an entity to make a request of, and
10
+ # receive a response from, another entity. The data content of the request
11
+ # and response is defined by the namespace declaration of a direct child
12
+ # element of the IQ element, and the interaction is tracked by the
13
+ # requesting entity through use of the 'id' attribute. Thus, IQ interactions
14
+ # follow a common pattern of structured data exchange such as get/result or
15
+ # set/result (although an error may be returned in reply to a request if
16
+ # appropriate).
17
+ #
18
+ # ## "ID" Attribute
19
+ #
20
+ # Iq Stanzas require the ID attribute be set. Blather will handle this
21
+ # automatically when a new Iq is created.
22
+ #
23
+ # ## "Type" Attribute
24
+ #
25
+ # * `:get` -- The stanza is a request for information or requirements.
26
+ #
27
+ # * `:set` -- The stanza provides required data, sets new values, or
28
+ # replaces existing values.
29
+ #
30
+ # * `:result` -- The stanza is a response to a successful get or set request.
31
+ #
32
+ # * `:error` -- An error has occurred regarding processing or delivery of a
33
+ # previously-sent get or set (see Stanza Errors).
34
+ #
35
+ # Blather provides a helper for each possible type:
36
+ #
37
+ # Iq#get?
38
+ # Iq#set?
39
+ # Iq#result?
40
+ # Iq#error?
41
+ #
42
+ # Blather treats the `type` attribute like a normal ruby object attribute
43
+ # providing a getter and setter. The default `type` is `get`.
44
+ #
45
+ # iq = Iq.new
46
+ # iq.type # => :get
47
+ # iq.get? # => true
48
+ # iq.type = :set
49
+ # iq.set? # => true
50
+ # iq.get? # => false
51
+ #
52
+ # iq.type = :invalid # => RuntimeError
53
+ #
54
+ # @handler :iq
55
+ class Iq < Stanza
56
+ VALID_TYPES = [:get, :set, :result, :error].freeze
57
+
58
+ register :iq
59
+
60
+ # @private
61
+ def self.import(node)
62
+ klass = nil
63
+ node.children.detect do |e|
64
+ ns = e.namespace ? e.namespace.href : nil
65
+ klass = class_from_registration(e.element_name, ns)
66
+ end
67
+
68
+ if klass && klass != self
69
+ klass.import(node)
70
+ else
71
+ new(node[:type]).inherit(node)
72
+ end
73
+ end
74
+
75
+ # Create a new Iq
76
+ #
77
+ # @param [Symbol, nil] type the type of stanza (:get, :set, :result, :error)
78
+ # @param [Blather::JID, String, nil] jid the JID of the inteded recipient
79
+ # @param [#to_s] id the stanza's ID. Leaving this nil will set the ID to
80
+ # the next unique number
81
+ def self.new(type = nil, to = nil, id = nil)
82
+ node = super :iq
83
+ node.type = type || :get
84
+ node.to = to
85
+ node.id = id || self.next_id
86
+ node
87
+ end
88
+
89
+ # Check if the IQ is of type :get
90
+ #
91
+ # @return [true, false]
92
+ def get?
93
+ self.type == :get
94
+ end
95
+
96
+ # Check if the IQ is of type :set
97
+ #
98
+ # @return [true, false]
99
+ def set?
100
+ self.type == :set
101
+ end
102
+
103
+ # Check if the IQ is of type :result
104
+ #
105
+ # @return [true, false]
106
+ def result?
107
+ self.type == :result
108
+ end
109
+
110
+ # Check if the IQ is of type :error
111
+ #
112
+ # @return [true, false]
113
+ def error?
114
+ self.type == :error
115
+ end
116
+
117
+ # Ensures type is :get, :set, :result or :error
118
+ #
119
+ # @param [#to_sym] type the Iq type. Must be one of VALID_TYPES
120
+ def type=(type)
121
+ if type && !VALID_TYPES.include?(type.to_sym)
122
+ raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}"
123
+ end
124
+ super
125
+ end
126
+
127
+ # Overrides the parent method to ensure the reply is of type :result
128
+ #
129
+ # @return [self]
130
+ def reply!
131
+ super
132
+ self.type = :result
133
+ self
134
+ end
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,332 @@
1
+ module Blather
2
+ class Stanza
3
+
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`.
74
+ #
75
+ # Blather provides a helper for each possible type:
76
+ #
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`.
85
+ #
86
+ # msg = Message.new
87
+ # msg.type # => :chat
88
+ # msg.chat? # => true
89
+ # msg.type = :normal
90
+ # msg.normal? # => true
91
+ # msg.chat? # => false
92
+ #
93
+ # msg.type = :invalid # => RuntimeError
94
+ #
95
+ #
96
+ # ## "Body" Element
97
+ #
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.
101
+ #
102
+ # Blather provides an attribute-like syntax for Message `body` elements.
103
+ #
104
+ # msg = Message.new 'user@domain.tld', 'message body'
105
+ # msg.body # => 'message body'
106
+ #
107
+ # msg.body = 'other message'
108
+ # msg.body # => 'other message'
109
+ #
110
+ # ## "Subject" Element
111
+ #
112
+ # The `subject` element contains human-readable XML character data that
113
+ # specifies the topic of the message.
114
+ #
115
+ # Blather provides an attribute-like syntax for Message `subject` elements.
116
+ #
117
+ # msg = Message.new 'user@domain.tld', 'message body'
118
+ # msg.subject = 'message subject'
119
+ # msg.subject # => 'message subject'
120
+ #
121
+ # ## "Thread" Element
122
+ #
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.
134
+ #
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].
140
+ #
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.
145
+ #
146
+ # Blather provides an attribute-like syntax for Message `thread` elements.
147
+ #
148
+ # msg = Message.new
149
+ # msg.thread = '12345'
150
+ # msg.thread # => '12345'
151
+ #
152
+ # Parent threads can be set using a hash:
153
+ #
154
+ # msg.thread = {'parent-id' => 'thread-id'}
155
+ # msg.thread # => 'thread-id'
156
+ # msg.parent_thread # => 'parent-id'
157
+ #
158
+ # @handler :message
159
+ class Message < Stanza
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
164
+
165
+ register :message
166
+
167
+ # @private
168
+ def self.import(node)
169
+ klass = nil
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
174
+
175
+ if klass && klass != self
176
+ klass.import(node)
177
+ else
178
+ new(node[:type]).inherit(node)
179
+ end
180
+ end
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
187
+ def self.new(to = nil, body = nil, type = :chat)
188
+ node = super :message
189
+ node.to = to
190
+ node.type = type
191
+ node.body = body
192
+ node
193
+ end
194
+
195
+ # Check if the Message is of type :chat
196
+ #
197
+ # @return [true, false]
198
+ def chat?
199
+ self.type == :chat
200
+ end
201
+
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
237
+ super
238
+ end
239
+
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
286
+
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
307
+
308
+ # Get the parent thread
309
+ #
310
+ # @return [String, nil]
311
+ def parent_thread
312
+ n = find_first('thread')
313
+ n[:parent] if n
314
+ end
315
+
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)
325
+ parent, thread = thread.to_a.flatten if thread.is_a?(Hash)
326
+ set_content_for :thread, thread
327
+ find_first('thread')[:parent] = parent
328
+ end
329
+ end
330
+
331
+ end
332
+ end