shingara-blather 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
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