module Blather class Stanza # # Message Stanza # # [RFC 3921 Section 2.1 - Message Syntax](http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.1) # # Exchanging messages is a basic use of XMPP and occurs when a user # generates a message stanza that is addressed to another entity. The # sender's server is responsible for delivering the message to the intended # recipient (if the recipient is on the same local server) or for routing # the message to the recipient's server (if the recipient is on a remote # server). Thus a message stanza is used to "push" information to another # entity. # # ## "To" Attribute # # An instant messaging client specifies an intended recipient for a message # by providing the JID of an entity other than the sender in the `to` # attribute of the Message stanza. If the message is being sent outside the # context of any existing chat session or received message, the value of the # `to` address SHOULD be of the form "user@domain" rather than of the form # "user@domain/resource". # # msg = Message.new 'user@domain.tld/resource' # msg.to == 'user@domain.tld/resource' # # msg.to = 'another-user@some-domain.tld/resource' # msg.to == 'another-user@some-domain.tld/resource' # # The `to` attribute on a Message stanza works like any regular ruby object # attribute # # ## "Type" Attribute # # Common uses of the message stanza in instant messaging applications # include: single messages; messages sent in the context of a one-to-one # chat session; messages sent in the context of a multi-user chat room; # alerts, notifications, or other information to which no reply is expected; # and errors. These uses are differentiated via the `type` attribute. If # included, the `type` attribute MUST have one of the following values: # # * `:chat` -- The message is sent in the context of a one-to-one chat # session. Typically a receiving client will present message of type # `chat` in an interface that enables one-to-one chat between the two # parties, including an appropriate conversation history. # # * `:error` -- The message is generated by an entity that experiences an # error in processing a message received from another entity. A client # that receives a message of type `error` SHOULD present an appropriate # interface informing the sender of the nature of the error. # # * `:groupchat` -- The message is sent in the context of a multi-user chat # environment (similar to that of [IRC]). Typically a receiving client # will present a message of type `groupchat` in an interface that enables # many-to-many chat between the parties, including a roster of parties in # the chatroom and an appropriate conversation history. # # * `:headline` -- The message provides an alert, a notification, or other # information to which no reply is expected (e.g., news headlines, sports # updates, near-real-time market data, and syndicated content). Because no # reply to the message is expected, typically a receiving client will # present a message of type "headline" in an interface that appropriately # differentiates the message from standalone messages, chat messages, or # groupchat messages (e.g., by not providing the recipient with the # ability to reply). # # * `:normal` -- The message is a standalone message that is sent outside # the context of a one-to-one conversation or groupchat, and to which it # is expected that the recipient will reply. Typically a receiving client # will present a message of type `normal` in an interface that enables the # recipient to reply, but without a conversation history. The default # value of the `type` attribute is `normal`. # # Blather provides a helper for each possible type: # # Message#chat? # Message#error? # Message#groupchat? # Message#headline? # Message#normal? # # Blather treats the `type` attribute like a normal ruby object attribute # providing a getter and setter. The default `type` is `chat`. # # msg = Message.new # msg.type # => :chat # msg.chat? # => true # msg.type = :normal # msg.normal? # => true # msg.chat? # => false # # msg.type = :invalid # => RuntimeError # # # ## "Body" Element # # The `body` element contains human-readable XML character data that # specifies the textual contents of the message; this child element is # normally included but is optional. # # Blather provides an attribute-like syntax for Message `body` elements. # # msg = Message.new 'user@domain.tld', 'message body' # msg.body # => 'message body' # # msg.body = 'other message' # msg.body # => 'other message' # # ## "Subject" Element # # The `subject` element contains human-readable XML character data that # specifies the topic of the message. # # Blather provides an attribute-like syntax for Message `subject` elements. # # msg = Message.new 'user@domain.tld', 'message body' # msg.subject = 'message subject' # msg.subject # => 'message subject' # # ## "Thread" Element # # The primary use of the XMPP `thread` element is to uniquely identify a # conversation thread or "chat session" between two entities instantiated by # Message stanzas of type `chat`. However, the XMPP thread element can also # be used to uniquely identify an analogous thread between two entities # instantiated by Message stanzas of type `headline` or `normal`, or among # multiple entities in the context of a multi-user chat room instantiated by # Message stanzas of type `groupchat`. It MAY also be used for Message # stanzas not related to a human conversation, such as a game session or an # interaction between plugins. The `thread` element is not used to identify # individual messages, only conversations or messagingg sessions. The # inclusion of the `thread` element is optional. # # The value of the `thread` element is not human-readable and MUST be # treated as opaque by entities; no semantic meaning can be derived from it, # and only exact comparisons can be made against it. The value of the # `thread` element MUST be a universally unique identifier (UUID) as # described in [UUID]. # # The `thread` element MAY possess a 'parent' attribute that identifies # another thread of which the current thread is an offshoot or child; the # value of the 'parent' must conform to the syntax of the `thread` element # itself. # # Blather provides an attribute-like syntax for Message `thread` elements. # # msg = Message.new # msg.thread = '12345' # msg.thread # => '12345' # # Parent threads can be set using a hash: # # msg.thread = {'parent-id' => 'thread-id'} # msg.thread # => 'thread-id' # msg.parent_thread # => 'parent-id' # # @handler :message class Message < Stanza # @private VALID_TYPES = [:chat, :error, :groupchat, :headline, :normal].freeze # @private VALID_CHAT_STATES = [:active, :composing, :gone, :inactive, :paused].freeze # @private CHAT_STATE_NS = 'http://jabber.org/protocol/chatstates'.freeze # @private HTML_NS = 'http://jabber.org/protocol/xhtml-im'.freeze # @private HTML_BODY_NS = 'http://www.w3.org/1999/xhtml'.freeze register :message # @private def self.import(node) klass = nil node.children.detect do |e| ns = e.namespace ? e.namespace.href : nil klass = class_from_registration(e.element_name, ns) end if klass == Blather::Stanza::Presence::MUCUser klass = Blather::Stanza::Message::MUCUser end if klass && klass != self && ![Blather::Stanza::X, Blather::Stanza::Iq].include?(klass) klass.import(node) else new(node[:type]).inherit(node) end end # Create a new Message stanza # # @param [#to_s] to the JID to send the message to # @param [#to_s] body the body of the message # @param [Symbol] type the message type. Must be one of VALID_TYPES def self.new(to = nil, body = nil, type = :chat) node = super :message node.to = to node.type = type node.body = body node.chat_state = :active if [:chat, :groupchat].include?(type) node end # Overrides the parent method to ensure the current chat state is removed # # @see Blather::Stanza::Iq#inherit def inherit(node) xpath('ns:*', :ns => CHAT_STATE_NS).remove super end # Check if the Message is of type :chat # # @return [true, false] def chat? self.type == :chat end # Check if the Message is of type :error # # @return [true, false] def error? self.type == :error end # Check if the Message is of type :groupchat # # @return [true, false] def groupchat? self.type == :groupchat end # Check if the Message is of type :headline # # @return [true, false] def headline? self.type == :headline end # Check if the Message is of type :normal # # @return [true, false] def normal? self.type == :normal end # Ensures type is :get, :set, :result or :error # # @param [#to_sym] type the Message type. Must be one of VALID_TYPES def type=(type) if type && !VALID_TYPES.include?(type.to_sym) raise ArgumentError, "Invalid Type (#{type}), use: #{VALID_TYPES*' '}" end super end # Get the message body # # @return [String] def body read_content :body end # Set the message body # # @param [#to_s] body the message body def body=(body) set_content_for :body, body end # Get the message xhtml node # This will create the node if it doesn't exist # # @return [XML::Node] def xhtml_node unless h = find_first('ns:html', :ns => HTML_NS) || find_first('ns:html', :ns => HTML_BODY_NS) self << (h = XMPPNode.new('html', self.document)) h.namespace = HTML_NS end unless b = h.find_first('ns:body', :ns => HTML_BODY_NS) b = XMPPNode.new('body', self.document) b.namespace = HTML_BODY_NS h << b end b end # Get the message xhtml # # @return [String] def xhtml self.xhtml_node.inner_html.strip end # Set the message xhtml # This will use Nokogiri to ensure the xhtml is valid # # @param [#to_s] valid xhtml def xhtml=(xhtml_body) self.xhtml_node.inner_html = Nokogiri::XML::DocumentFragment.parse(xhtml_body) end # Get the message subject # # @return [String] def subject read_content :subject end # Set the message subject # # @param [#to_s] body the message subject def subject=(subject) set_content_for :subject, subject end # Get the message thread # # @return [String] def thread read_content :thread end # Get the parent thread # # @return [String, nil] def parent_thread n = find_first('thread') n[:parent] if n end # Set the thread # # @overload thread=(hash) # Set a thread with a parent # @param [Hash thread-id>] thread # @overload thread=(thread) # Set a thread id # @param [#to_s] thread the new thread id def thread=(thread) parent, thread = thread.to_a.flatten if thread.is_a?(Hash) set_content_for :thread, thread find_first('thread')[:parent] = parent end # Returns the message's x:data form child def form X.find_or_create self end # Get the message chat state # # @return [Symbol] def chat_state if (elem = find_first('ns:*', :ns => CHAT_STATE_NS)) && VALID_CHAT_STATES.include?(name = elem.name.to_sym) name end end # Set the message chat state # # @param [#to_s] chat_state the message chat state. Must be one of VALID_CHAT_STATES def chat_state=(chat_state) if chat_state && !VALID_CHAT_STATES.include?(chat_state.to_sym) raise ArgumentError, "Invalid Chat State (#{chat_state}), use: #{VALID_CHAT_STATES*' '}" end xpath('ns:*', :ns => CHAT_STATE_NS).remove if chat_state state = XMPPNode.new(chat_state, self.document) state.namespace = CHAT_STATE_NS self << state end end def delay if d = find_first('ns:delay', :ns => "urn:xmpp:delay") Delay.new d end end def delayed? !!delay end class Delay < XMPPNode def self.new(stamp = nil, from = nil, description = nil) new_node = super :delay case stamp when Nokogiri::XML::Node new_node.inherit stamp when Hash new_node.stamp = stamp[:stamp] new_node.from = stamp[:from] new_node.description = stamp[:description] else new_node.stamp = stamp new_node.from = from new_node.description = description end new_node end def from read_attr :from end def stamp s = read_attr :stamp s && Time.parse(s) end def description content.strip end end end end end