diff options
Diffstat (limited to 'slixmpp/stanza')
-rw-r--r-- | slixmpp/stanza/__init__.py | 15 | ||||
-rw-r--r-- | slixmpp/stanza/atom.py | 43 | ||||
-rw-r--r-- | slixmpp/stanza/error.py | 164 | ||||
-rw-r--r-- | slixmpp/stanza/htmlim.py | 14 | ||||
-rw-r--r-- | slixmpp/stanza/iq.py | 261 | ||||
-rw-r--r-- | slixmpp/stanza/message.py | 188 | ||||
-rw-r--r-- | slixmpp/stanza/nick.py | 17 | ||||
-rw-r--r-- | slixmpp/stanza/presence.py | 173 | ||||
-rw-r--r-- | slixmpp/stanza/rootstanza.py | 90 | ||||
-rw-r--r-- | slixmpp/stanza/roster.py | 152 | ||||
-rw-r--r-- | slixmpp/stanza/stream_error.py | 83 | ||||
-rw-r--r-- | slixmpp/stanza/stream_features.py | 57 |
12 files changed, 1257 insertions, 0 deletions
diff --git a/slixmpp/stanza/__init__.py b/slixmpp/stanza/__init__.py new file mode 100644 index 00000000..6cd6a2c5 --- /dev/null +++ b/slixmpp/stanza/__init__.py @@ -0,0 +1,15 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + + +from slixmpp.stanza.error import Error +from slixmpp.stanza.iq import Iq +from slixmpp.stanza.message import Message +from slixmpp.stanza.presence import Presence +from slixmpp.stanza.stream_features import StreamFeatures +from slixmpp.stanza.stream_error import StreamError diff --git a/slixmpp/stanza/atom.py b/slixmpp/stanza/atom.py new file mode 100644 index 00000000..ccded724 --- /dev/null +++ b/slixmpp/stanza/atom.py @@ -0,0 +1,43 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase, register_stanza_plugin + +class AtomEntry(ElementBase): + + """ + A simple Atom feed entry. + + Stanza Interface: + title -- The title of the Atom feed entry. + summary -- The summary of the Atom feed entry. + """ + + namespace = 'http://www.w3.org/2005/Atom' + name = 'entry' + plugin_attrib = 'entry' + interfaces = set(('title', 'summary', 'id', 'published', 'updated')) + sub_interfaces = set(('title', 'summary', 'id', 'published', + 'updated')) + +class AtomAuthor(ElementBase): + + """ + An Atom author. + + Stanza Interface: + name -- The printable author name + uri -- The bare jid of the author + """ + + name = 'author' + plugin_attrib = 'author' + interfaces = set(('name', 'uri')) + sub_interfaces = set(('name', 'uri')) + +register_stanza_plugin(AtomEntry, AtomAuthor) diff --git a/slixmpp/stanza/error.py b/slixmpp/stanza/error.py new file mode 100644 index 00000000..67f736c0 --- /dev/null +++ b/slixmpp/stanza/error.py @@ -0,0 +1,164 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase, ET + + +class Error(ElementBase): + + """ + XMPP stanzas of type 'error' should include an <error> stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + Example error stanza: + <error type="cancel" code="404"> + <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /> + <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"> + The item was not found. + </text> + </error> + + Stanza Interface: + code -- The error code used in older XMPP versions. + condition -- The name of the condition element. + text -- Human readable description of the error. + type -- Error type indicating how the error should be handled. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + types -- A set of values indicating how the error + should be treated. + + Methods: + setup -- Overrides ElementBase.setup. + get_condition -- Retrieve the name of the condition element. + set_condition -- Add a condition element. + del_condition -- Remove the condition element. + get_text -- Retrieve the contents of the <text> element. + set_text -- Set the contents of the <text> element. + del_text -- Remove the <text> element. + """ + + namespace = 'jabber:client' + name = 'error' + plugin_attrib = 'error' + interfaces = set(('code', 'condition', 'text', 'type', + 'gone', 'redirect', 'by')) + sub_interfaces = set(('text',)) + plugin_attrib_map = {} + plugin_tag_map = {} + conditions = set(('bad-request', 'conflict', 'feature-not-implemented', + 'forbidden', 'gone', 'internal-server-error', + 'item-not-found', 'jid-malformed', 'not-acceptable', + 'not-allowed', 'not-authorized', 'payment-required', + 'recipient-unavailable', 'redirect', + 'registration-required', 'remote-server-not-found', + 'remote-server-timeout', 'resource-constraint', + 'service-unavailable', 'subscription-required', + 'undefined-condition', 'unexpected-request')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' + types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup. + + Sets a default error type and condition, and changes the + parent stanza's type to 'error'. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + if ElementBase.setup(self, xml): + #If we had to generate XML then set default values. + self['type'] = 'cancel' + self['condition'] = 'feature-not-implemented' + if self.parent is not None: + self.parent()['type'] = 'error' + + def get_condition(self): + """Return the condition element's name.""" + for child in self.xml: + if "{%s}" % self.condition_ns in child.tag: + cond = child.tag.split('}', 1)[-1] + if cond in self.conditions: + return cond + return '' + + def set_condition(self, value): + """ + Set the tag name of the condition element. + + Arguments: + value -- The tag name of the condition element. + """ + if value in self.conditions: + del self['condition'] + self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value))) + return self + + def del_condition(self): + """Remove the condition element.""" + for child in self.xml: + if "{%s}" % self.condition_ns in child.tag: + tag = child.tag.split('}', 1)[-1] + if tag in self.conditions: + self.xml.remove(child) + return self + + def get_text(self): + """Retrieve the contents of the <text> element.""" + return self._get_sub_text('{%s}text' % self.condition_ns) + + def set_text(self, value): + """ + Set the contents of the <text> element. + + Arguments: + value -- The new contents for the <text> element. + """ + self._set_sub_text('{%s}text' % self.condition_ns, text=value) + return self + + def del_text(self): + """Remove the <text> element.""" + self._del_sub('{%s}text' % self.condition_ns) + return self + + def get_gone(self): + return self._get_sub_text('{%s}gone' % self.condition_ns, '') + + def get_redirect(self): + return self._get_sub_text('{%s}redirect' % self.condition_ns, '') + + def set_gone(self, value): + if value: + del self['condition'] + return self._set_sub_text('{%s}gone' % self.condition_ns, value) + elif self['condition'] == 'gone': + del self['condition'] + + def set_redirect(self, value): + if value: + del self['condition'] + ns = self.condition_ns + return self._set_sub_text('{%s}redirect' % ns, value) + elif self['condition'] == 'redirect': + del self['condition'] + + def del_gone(self): + self._del_sub('{%s}gone' % self.condition_ns) + + def del_redirect(self): + self._del_sub('{%s}redirect' % self.condition_ns) diff --git a/slixmpp/stanza/htmlim.py b/slixmpp/stanza/htmlim.py new file mode 100644 index 00000000..a5a3e5f3 --- /dev/null +++ b/slixmpp/stanza/htmlim.py @@ -0,0 +1,14 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza import Message +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins.xep_0071 import XHTML_IM as HTMLIM + + +register_stanza_plugin(Message, HTMLIM) diff --git a/slixmpp/stanza/iq.py b/slixmpp/stanza/iq.py new file mode 100644 index 00000000..a64dfa7f --- /dev/null +++ b/slixmpp/stanza/iq.py @@ -0,0 +1,261 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza.rootstanza import RootStanza +from slixmpp.xmlstream import StanzaBase, ET +from slixmpp.xmlstream.handler import Waiter, Callback, CoroutineCallback +from slixmpp.xmlstream.asyncio import asyncio +from slixmpp.xmlstream.matcher import MatchIDSender, MatcherId +from slixmpp.exceptions import IqTimeout, IqError + + +class Iq(RootStanza): + + """ + XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of + requesting and modifying information, similar to HTTP's GET and + POST methods. + + Each <iq> stanza must have an 'id' value which associates the + stanza with the response stanza. XMPP entities must always + be given a response <iq> stanza with a type of 'result' after + sending a stanza of type 'get' or 'set'. + + Most uses cases for <iq> stanzas will involve adding a <query> + element whose namespace indicates the type of information + desired. However, some custom XMPP applications use <iq> stanzas + as a carrier stanza for an application-specific protocol instead. + + Example <iq> Stanzas: + + .. code-block:: xml + + <iq to="user@example.com" type="get" id="314"> + <query xmlns="http://jabber.org/protocol/disco#items" /> + </iq> + + <iq to="user@localhost" type="result" id="17"> + <query xmlns='jabber:iq:roster'> + <item jid='otheruser@example.net' + name='John Doe' + subscription='both'> + <group>Friends</group> + </item> + </query> + </iq> + + Stanza Interface: + - **query**: The namespace of the <query> element if one exists. + Attributes: + - **types**: May be one of: get, set, result, or error. + """ + + namespace = 'jabber:client' + name = 'iq' + interfaces = set(('type', 'to', 'from', 'id', 'query')) + types = set(('get', 'result', 'set', 'error')) + plugin_attrib = name + + def __init__(self, *args, **kwargs): + """ + Initialize a new <iq> stanza with an 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None: + self['id'] = self.stream.new_id() + else: + self['id'] = '0' + + def unhandled(self): + """ + Send a feature-not-implemented error if the stanza is not handled. + + Overrides StanzaBase.unhandled. + """ + if self['type'] in ('get', 'set'): + reply = self.reply() + reply['error']['condition'] = 'feature-not-implemented' + reply['error']['text'] = 'No handlers registered for this request.' + reply.send() + + def set_payload(self, value): + """ + Set the XML contents of the <iq> stanza. + + :param value: An XML object or a list of XML objects to use as the <iq> + stanza's contents + :type value: list or XML object + """ + self.clear() + StanzaBase.set_payload(self, value) + return self + + def set_query(self, value): + """ + Add or modify a <query> element. + + Query elements are differentiated by their namespace. + + :param str value: The namespace of the <query> element. + """ + query = self.xml.find("{%s}query" % value) + if query is None and value: + plugin = self.plugin_tag_map.get('{%s}query' % value, None) + if plugin: + self.enable(plugin.plugin_attrib) + else: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) + return self + + def get_query(self): + """Return the namespace of the <query> element. + + :rtype: str""" + for child in self.xml: + if child.tag.endswith('query'): + ns = child.tag.split('}')[0] + if '{' in ns: + ns = ns[1:] + return ns + return '' + + def del_query(self): + """Remove the <query> element.""" + for child in self.xml: + if child.tag.endswith('query'): + self.xml.remove(child) + return self + + def reply(self, clear=True): + """ + Create a new <iq> stanza replying to ``self``. + + Overrides StanzaBase.reply + + Sets the 'type' to 'result' in addition to the default + StanzaBase.reply behavior. + + :param bool clear: Indicates if existing content should be + removed before replying. Defaults to True. + """ + new_iq = StanzaBase.reply(self, clear=clear) + new_iq['type'] = 'result' + return new_iq + + def send(self, callback=None, timeout=None, timeout_callback=None): + """Send an <iq> stanza over the XML stream. + + A callback handler can be provided that will be executed when the Iq + stanza's result reply is received. + + Returns a future which result will be set to the result Iq if it is of type 'get' or 'set' + (when it is received), or a future with the result set to None if it has another type. + + Overrides StanzaBase.send + + :param function callback: Optional reference to a stream handler + function. Will be executed when a reply + stanza is received. + :param int timeout: The length of time (in seconds) to wait for a + response before the timeout_callback is called, + instead of the regular callback + :param function timeout_callback: Optional reference to a stream handler + function. Will be executed when the + timeout expires before a response has + been received for the originally-sent + IQ stanza. + :rtype: asyncio.Future + """ + if self.stream.session_bind_event.is_set(): + matcher = MatchIDSender({ + 'id': self['id'], + 'self': self.stream.boundjid, + 'peer': self['to'] + }) + else: + matcher = MatcherId(self['id']) + + future = asyncio.Future() + + def callback_success(result): + if result['type'] == 'error': + future.set_exception(IqError(result)) + else: + future.set_result(result) + + if timeout is not None: + self.stream.cancel_schedule('IqTimeout_%s' % self['id']) + if callback is not None: + callback(result) + + def callback_timeout(): + future.set_exception(IqTimeout(self)) + self.stream.remove_handler('IqCallback_%s' % self['id']) + if timeout_callback is not None: + timeout_callback(self) + + if self['type'] in ('get', 'set'): + handler_name = 'IqCallback_%s' % self['id'] + if asyncio.iscoroutinefunction(callback): + constr = CoroutineCallback + else: + constr = Callback + if timeout is not None: + self.stream.schedule('IqTimeout_%s' % self['id'], + timeout, + callback_timeout, + repeat=False) + handler = constr(handler_name, + matcher, + callback_success, + once=True) + self.stream.register_handler(handler) + else: + future.set_result(None) + StanzaBase.send(self) + return future + + def _handle_result(self, iq): + # we got the IQ, so don't fire the timeout + self.stream.cancel_schedule('IqTimeout_%s' % self['id']) + self.callback(iq) + + def _fire_timeout(self): + # don't fire the handler for the IQ, if it finally does come in + self.stream.remove_handler('IqCallback_%s' % self['id']) + self.timeout_callback(self) + + def _set_stanza_values(self, values): + """ + Set multiple stanza interface values using a dictionary. + + Stanza plugin values may be set usind nested dictionaries. + + If the interface 'query' is given, then it will be set + last to avoid duplication of the <query /> element. + + Overrides ElementBase._set_stanza_values. + + Arguments: + values -- A dictionary mapping stanza interface with values. + Plugin interfaces may accept a nested dictionary that + will be used recursively. + """ + query = values.get('query', '') + if query: + del values['query'] + StanzaBase._set_stanza_values(self, values) + self['query'] = query + else: + StanzaBase._set_stanza_values(self, values) + return self diff --git a/slixmpp/stanza/message.py b/slixmpp/stanza/message.py new file mode 100644 index 00000000..cbb170fa --- /dev/null +++ b/slixmpp/stanza/message.py @@ -0,0 +1,188 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza.rootstanza import RootStanza +from slixmpp.xmlstream import StanzaBase, ET + + +class Message(RootStanza): + + """ + XMPP's <message> stanzas are a "push" mechanism to send information + to other XMPP entities without requiring a response. + + Chat clients will typically use <message> stanzas that have a type + of either "chat" or "groupchat". + + When handling a message event, be sure to check if the message is + an error response. + + Example <message> stanzas: + + .. code-block:: xml + + <message to="user1@example.com" from="user2@example.com"> + <body>Hi!</body> + </message> + + <message type="groupchat" to="room@conference.example.com"> + <body>Hi everyone!</body> + </message> + + Stanza Interface: + - **body**: The main contents of the message. + - **subject**: An optional description of the message's contents. + - **mucroom**: (Read-only) The name of the MUC room that sent the message. + - **mucnick**: (Read-only) The MUC nickname of message's sender. + + Attributes: + - **types**: May be one of: normal, chat, headline, groupchat, or error. + """ + + name = 'message' + namespace = 'jabber:client' + plugin_attrib = name + interfaces = set(['type', 'to', 'from', 'id', 'body', 'subject', + 'thread', 'parent_thread', 'mucroom', 'mucnick']) + sub_interfaces = set(['body', 'subject', 'thread']) + lang_interfaces = sub_interfaces + types = set(['normal', 'chat', 'headline', 'error', 'groupchat']) + + def __init__(self, *args, **kwargs): + """ + Initialize a new <message /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_message_ids: + self['id'] = self.stream.new_id() + + def get_type(self): + """ + Return the message type. + + Overrides default stanza interface behavior. + + Returns 'normal' if no type attribute is present. + + :rtype: str + """ + return self._get_attr('type', 'normal') + + def get_parent_thread(self): + """Return the message thread's parent thread. + + :rtype: str + """ + thread = self.xml.find('{%s}thread' % self.namespace) + if thread is not None: + return thread.attrib.get('parent', '') + return '' + + def set_parent_thread(self, value): + """Add or change the message thread's parent thread. + + :param str value: identifier of the thread""" + thread = self.xml.find('{%s}thread' % self.namespace) + if value: + if thread is None: + thread = ET.Element('{%s}thread' % self.namespace) + self.xml.append(thread) + thread.attrib['parent'] = value + else: + if thread is not None and 'parent' in thread.attrib: + del thread.attrib['parent'] + + def del_parent_thread(self): + """Delete the message thread's parent reference.""" + thread = self.xml.find('{%s}thread' % self.namespace) + if thread is not None and 'parent' in thread.attrib: + del thread.attrib['parent'] + + def chat(self): + """Set the message type to 'chat'.""" + self['type'] = 'chat' + return self + + def normal(self): + """Set the message type to 'normal'.""" + self['type'] = 'normal' + return self + + def reply(self, body=None, clear=True): + """ + Create a message reply. + + Overrides StanzaBase.reply. + + Sets proper 'to' attribute if the message is from a MUC, and + adds a message body if one is given. + + :param str body: Optional text content for the message. + :param bool clear: Indicates if existing content should be removed + before replying. Defaults to True. + + :rtype: :class:`~.Message` + """ + new_message = StanzaBase.reply(self, clear) + + if self['type'] == 'groupchat': + new_message['to'] = new_message['to'].bare + + new_message['thread'] = self['thread'] + new_message['parent_thread'] = self['parent_thread'] + + del new_message['id'] + + if body is not None: + new_message['body'] = body + return new_message + + def get_mucroom(self): + """ + Return the name of the MUC room where the message originated. + + Read-only stanza interface. + + :rtype: str + """ + if self['type'] == 'groupchat': + return self['from'].bare + else: + return '' + + def get_mucnick(self): + """ + Return the nickname of the MUC user that sent the message. + + Read-only stanza interface. + + :rtype: str + """ + if self['type'] == 'groupchat': + return self['from'].resource + else: + return '' + + def set_mucroom(self, value): + """Dummy method to prevent modification.""" + pass + + def del_mucroom(self): + """Dummy method to prevent deletion.""" + pass + + def set_mucnick(self, value): + """Dummy method to prevent modification.""" + pass + + def del_mucnick(self): + """Dummy method to prevent deletion.""" + pass diff --git a/slixmpp/stanza/nick.py b/slixmpp/stanza/nick.py new file mode 100644 index 00000000..3bb7d63d --- /dev/null +++ b/slixmpp/stanza/nick.py @@ -0,0 +1,17 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +# The nickname stanza has been moved to its own plugin, but the existing +# references are kept for backwards compatibility. + +from slixmpp.stanza import Message, Presence +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins.xep_0172 import UserNick as Nick + +register_stanza_plugin(Message, Nick) +register_stanza_plugin(Presence, Nick) diff --git a/slixmpp/stanza/presence.py b/slixmpp/stanza/presence.py new file mode 100644 index 00000000..1e8a940e --- /dev/null +++ b/slixmpp/stanza/presence.py @@ -0,0 +1,173 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza.rootstanza import RootStanza +from slixmpp.xmlstream import StanzaBase + + +class Presence(RootStanza): + + """ + XMPP's <presence> stanza allows entities to know the status of other + clients and components. Since it is currently the only multi-cast + stanza in XMPP, many extensions add more information to <presence> + stanzas to broadcast to every entry in the roster, such as + capabilities, music choices, or locations (XEP-0115: Entity Capabilities + and XEP-0163: Personal Eventing Protocol). + + Since <presence> stanzas are broadcast when an XMPP entity changes + its status, the bulk of the traffic in an XMPP network will be from + <presence> stanzas. Therefore, do not include more information than + necessary in a status message or within a <presence> stanza in order + to help keep the network running smoothly. + + Example <presence> stanzas: + + .. code-block:: xml + + <presence /> + + <presence from="user@example.com"> + <show>away</show> + <status>Getting lunch.</status> + <priority>5</priority> + </presence> + + <presence type="unavailable" /> + + <presence to="user@otherhost.com" type="subscribe" /> + + Stanza Interface: + - **priority**: A value used by servers to determine message routing. + - **show**: The type of status, such as away or available for chat. + - **status**: Custom, human readable status message. + + Attributes: + - **types**: One of: available, unavailable, error, probe, + subscribe, subscribed, unsubscribe, and unsubscribed. + - **showtypes**: One of: away, chat, dnd, and xa. + """ + + name = 'presence' + namespace = 'jabber:client' + plugin_attrib = name + interfaces = set(['type', 'to', 'from', 'id', 'show', + 'status', 'priority']) + sub_interfaces = set(['show', 'status', 'priority']) + lang_interfaces = set(['status']) + + types = set(['available', 'unavailable', 'error', 'probe', 'subscribe', + 'subscribed', 'unsubscribe', 'unsubscribed']) + showtypes = set(['dnd', 'chat', 'xa', 'away']) + + def __init__(self, *args, **kwargs): + """ + Initialize a new <presence /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_presence_ids: + self['id'] = self.stream.new_id() + + def exception(self, e): + """ + Override exception passback for presence. + """ + pass + + def set_show(self, show): + """ + Set the value of the <show> element. + + :param str show: Must be one of: away, chat, dnd, or xa. + """ + if show is None: + self._del_sub('show') + elif show in self.showtypes: + self._set_sub_text('show', text=show) + return self + + def get_type(self): + """ + Return the value of the <presence> stanza's type attribute, or + the value of the <show> element. + """ + out = self._get_attr('type') + if not out: + out = self['show'] + if not out or out is None: + out = 'available' + return out + + def set_type(self, value): + """ + Set the type attribute's value, and the <show> element + if applicable. + + :param str value: Must be in either self.types or self.showtypes. + """ + if value in self.types: + self['show'] = None + if value == 'available': + value = '' + self._set_attr('type', value) + elif value in self.showtypes: + self['show'] = value + return self + + def del_type(self): + """ + Remove both the type attribute and the <show> element. + """ + self._del_attr('type') + self._del_sub('show') + + def set_priority(self, value): + """ + Set the entity's priority value. Some server use priority to + determine message routing behavior. + + Bot clients should typically use a priority of 0 if the same + JID is used elsewhere by a human-interacting client. + + :param int value: An integer value greater than or equal to 0. + """ + self._set_sub_text('priority', text=str(value)) + + def get_priority(self): + """ + Return the value of the <presence> element as an integer. + + :rtype: int + """ + p = self._get_sub_text('priority') + if not p: + p = 0 + try: + return int(p) + except ValueError: + # The priority is not a number: we consider it 0 as a default + return 0 + + def reply(self, clear=True): + """ + Create a new reply <presence/> stanza from ``self``. + + Overrides StanzaBase.reply. + + :param bool clear: Indicates if the stanza contents should be removed + before replying. Defaults to True. + """ + new_presence = StanzaBase.reply(self, clear) + if self['type'] == 'unsubscribe': + new_presence['type'] = 'unsubscribed' + elif self['type'] == 'subscribe': + new_presence['type'] = 'subscribed' + return new_presence diff --git a/slixmpp/stanza/rootstanza.py b/slixmpp/stanza/rootstanza.py new file mode 100644 index 00000000..a6dd958e --- /dev/null +++ b/slixmpp/stanza/rootstanza.py @@ -0,0 +1,90 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging + +from slixmpp.exceptions import XMPPError, IqError, IqTimeout +from slixmpp.stanza import Error +from slixmpp.xmlstream import ET, StanzaBase, register_stanza_plugin + + +log = logging.getLogger(__name__) + + +class RootStanza(StanzaBase): + + """ + A top-level XMPP stanza in an XMLStream. + + The RootStanza class provides a more XMPP specific exception + handler than provided by the generic StanzaBase class. + + Methods: + exception -- Overrides StanzaBase.exception + """ + + def exception(self, e): + """ + Create and send an error reply. + + Typically called when an event handler raises an exception. + The error's type and text content are based on the exception + object's type and content. + + Overrides StanzaBase.exception. + + Arguments: + e -- Exception object + """ + if isinstance(e, IqError): + # We received an Iq error reply, but it wasn't caught + # locally. Using the condition/text from that error + # response could leak too much information, so we'll + # only use a generic error here. + reply = self.reply() + reply['error']['condition'] = 'undefined-condition' + reply['error']['text'] = 'External error' + reply['error']['type'] = 'cancel' + log.warning('You should catch IqError exceptions') + reply.send() + elif isinstance(e, IqTimeout): + reply = self.reply() + reply['error']['condition'] = 'remote-server-timeout' + reply['error']['type'] = 'wait' + log.warning('You should catch IqTimeout exceptions') + reply.send() + elif isinstance(e, XMPPError): + # We raised this deliberately + keep_id = self['id'] + reply = self.reply(clear=e.clear) + reply['id'] = keep_id + reply['error']['condition'] = e.condition + reply['error']['text'] = e.text + reply['error']['type'] = e.etype + if e.extension is not None: + # Extended error tag + extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), + e.extension_args) + reply['error'].append(extxml) + reply.send() + else: + # We probably didn't raise this on purpose, so send an error stanza + keep_id = self['id'] + reply = self.reply() + reply['id'] = keep_id + reply['error']['condition'] = 'undefined-condition' + reply['error']['text'] = "Slixmpp got into trouble." + reply['error']['type'] = 'cancel' + reply.send() + # log the error + log.exception('Error handling {%s}%s stanza', + self.namespace, self.name) + # Finally raise the exception to a global exception handler + self.stream.exception(e) + +register_stanza_plugin(RootStanza, Error) diff --git a/slixmpp/stanza/roster.py b/slixmpp/stanza/roster.py new file mode 100644 index 00000000..c017c33f --- /dev/null +++ b/slixmpp/stanza/roster.py @@ -0,0 +1,152 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza import Iq +from slixmpp.xmlstream import JID +from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class Roster(ElementBase): + + """ + Example roster stanzas: + <iq type="set"> + <query xmlns="jabber:iq:roster"> + <item jid="user@example.com" subscription="both" name="User"> + <group>Friends</group> + </item> + </query> + </iq> + + Stanza Inteface: + items -- A dictionary of roster entries contained + in the stanza. + + Methods: + get_items -- Return a dictionary of roster entries. + set_items -- Add <item> elements. + del_items -- Remove all <item> elements. + """ + + namespace = 'jabber:iq:roster' + name = 'query' + plugin_attrib = 'roster' + interfaces = set(('items', 'ver')) + + def get_ver(self): + """ + Ensure handling an empty ver attribute propery. + + The ver attribute is special in that the presence of the + attribute with an empty value is important for boostrapping + roster versioning. + """ + return self.xml.attrib.get('ver', None) + + def set_ver(self, ver): + """ + Ensure handling an empty ver attribute propery. + + The ver attribute is special in that the presence of the + attribute with an empty value is important for boostrapping + roster versioning. + """ + if ver is not None: + self.xml.attrib['ver'] = ver + else: + del self.xml.attrib['ver'] + + def set_items(self, items): + """ + Set the roster entries in the <roster> stanza. + + Uses a dictionary using JIDs as keys, where each entry is itself + a dictionary that contains: + name -- An alias or nickname for the JID. + subscription -- The subscription type. Can be one of 'to', + 'from', 'both', 'none', or 'remove'. + groups -- A list of group names to which the JID + has been assigned. + + Arguments: + items -- A dictionary of roster entries. + """ + self.del_items() + for jid in items: + item = RosterItem() + item.values = items[jid] + item['jid'] = jid + self.append(item) + return self + + def get_items(self): + """ + Return a dictionary of roster entries. + + Each item is keyed using its JID, and contains: + name -- An assigned alias or nickname for the JID. + subscription -- The subscription type. Can be one of 'to', + 'from', 'both', 'none', or 'remove'. + groups -- A list of group names to which the JID has + been assigned. + """ + items = {} + for item in self['substanzas']: + if isinstance(item, RosterItem): + items[item['jid']] = item.values + # Remove extra JID reference to keep everything + # backward compatible + del items[item['jid']]['jid'] + del items[item['jid']]['lang'] + return items + + def del_items(self): + """ + Remove all <item> elements from the roster stanza. + """ + for item in self['substanzas']: + if isinstance(item, RosterItem): + self.xml.remove(item.xml) + + +class RosterItem(ElementBase): + namespace = 'jabber:iq:roster' + name = 'item' + plugin_attrib = 'item' + interfaces = set(('jid', 'name', 'subscription', 'ask', + 'approved', 'groups')) + + def get_jid(self): + return JID(self._get_attr('jid', '')) + + def set_jid(self, jid): + self._set_attr('jid', str(jid)) + + def get_groups(self): + groups = [] + for group in self.xml.findall('{%s}group' % self.namespace): + if group.text: + groups.append(group.text) + else: + groups.append('') + return groups + + def set_groups(self, values): + self.del_groups() + for group in values: + group_xml = ET.Element('{%s}group' % self.namespace) + group_xml.text = group + self.xml.append(group_xml) + + def del_groups(self): + for group in self.xml.findall('{%s}group' % self.namespace): + self.xml.remove(group) + + +register_stanza_plugin(Iq, Roster) +register_stanza_plugin(Roster, RosterItem, iterable=True) diff --git a/slixmpp/stanza/stream_error.py b/slixmpp/stanza/stream_error.py new file mode 100644 index 00000000..d8b8bb5a --- /dev/null +++ b/slixmpp/stanza/stream_error.py @@ -0,0 +1,83 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza.error import Error +from slixmpp.xmlstream import StanzaBase + + +class StreamError(Error, StanzaBase): + + """ + XMPP stanzas of type 'error' should include an <error> stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + The stream:error stanza is used to provide more information for + error that occur with the underlying XML stream itself, and not + a particular stanza. + + Note: The StreamError stanza is mostly the same as the normal + Error stanza, but with different namespaces and + condition names. + + Example error stanza: + <stream:error> + <not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" /> + <text xmlns="urn:ietf:params:xml:ns:xmpp-streams"> + XML was not well-formed. + </text> + </stream:error> + + Stanza Interface: + condition -- The name of the condition element. + text -- Human readable description of the error. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + + Methods: + setup -- Overrides ElementBase.setup. + get_condition -- Retrieve the name of the condition element. + set_condition -- Add a condition element. + del_condition -- Remove the condition element. + get_text -- Retrieve the contents of the <text> element. + set_text -- Set the contents of the <text> element. + del_text -- Remove the <text> element. + """ + + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('condition', 'text', 'see_other_host')) + conditions = set(( + 'bad-format', 'bad-namespace-prefix', 'conflict', + 'connection-timeout', 'host-gone', 'host-unknown', + 'improper-addressing', 'internal-server-error', 'invalid-from', + 'invalid-namespace', 'invalid-xml', 'not-authorized', + 'not-well-formed', 'policy-violation', 'remote-connection-failed', + 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', + 'system-shutdown', 'undefined-condition', 'unsupported-encoding', + 'unsupported-feature', 'unsupported-stanza-type', + 'unsupported-version')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' + + def get_see_other_host(self): + ns = self.condition_ns + return self._get_sub_text('{%s}see-other-host' % ns, '') + + def set_see_other_host(self, value): + if value: + del self['condition'] + ns = self.condition_ns + return self._set_sub_text('{%s}see-other-host' % ns, value) + elif self['condition'] == 'see-other-host': + del self['condition'] + + def del_see_other_host(self): + self._del_sub('{%s}see-other-host' % self.condition_ns) diff --git a/slixmpp/stanza/stream_features.py b/slixmpp/stanza/stream_features.py new file mode 100644 index 00000000..05788771 --- /dev/null +++ b/slixmpp/stanza/stream_features.py @@ -0,0 +1,57 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from collections import OrderedDict +from slixmpp.xmlstream import StanzaBase + + +class StreamFeatures(StanzaBase): + + """ + """ + + name = 'features' + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('features', 'required', 'optional')) + sub_interfaces = interfaces + plugin_tag_map = {} + plugin_attrib_map = {} + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.values = self.values + + def get_features(self): + """ + """ + features = OrderedDict() + for (name, lang), plugin in self.plugins.items(): + features[name] = plugin + return features + + def set_features(self, value): + """ + """ + pass + + def del_features(self): + """ + """ + pass + + def get_required(self): + """ + """ + features = self['features'] + return [f for n, f in features.items() if f['required']] + + def get_optional(self): + """ + """ + features = self['features'] + return [f for n, f in features.items() if not f['required']] |