diff options
Diffstat (limited to 'sleekxmpp/stanza')
-rw-r--r-- | sleekxmpp/stanza/__init__.py | 9 | ||||
-rw-r--r-- | sleekxmpp/stanza/atom.py | 2 | ||||
-rw-r--r-- | sleekxmpp/stanza/error.py | 179 | ||||
-rw-r--r-- | sleekxmpp/stanza/htmlim.py | 99 | ||||
-rw-r--r-- | sleekxmpp/stanza/iq.py | 238 | ||||
-rw-r--r-- | sleekxmpp/stanza/message.py | 190 | ||||
-rw-r--r-- | sleekxmpp/stanza/nick.py | 82 | ||||
-rw-r--r-- | sleekxmpp/stanza/presence.py | 193 | ||||
-rw-r--r-- | sleekxmpp/stanza/rootstanza.py | 80 | ||||
-rw-r--r-- | sleekxmpp/stanza/roster.py | 146 |
10 files changed, 867 insertions, 351 deletions
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index c3d8a318..8302c43d 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -3,6 +3,11 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -__all__ = ['presence'] + + +from sleekxmpp.stanza.error import Error +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.presence import Presence diff --git a/sleekxmpp/stanza/atom.py b/sleekxmpp/stanza/atom.py index 5e82cb98..9df85a2b 100644 --- a/sleekxmpp/stanza/atom.py +++ b/sleekxmpp/stanza/atom.py @@ -1,4 +1,4 @@ -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from xml.etree import cElementTree as ET class AtomEntry(ElementBase): diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index ee46722a..6d18c297 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -3,60 +3,131 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET + +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class Error(ElementBase): - namespace = 'jabber:client' - name = 'error' - plugin_attrib = 'error' - 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')) - interfaces = set(('code', 'condition', 'text', 'type')) - types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) - sub_interfaces = set(('text',)) - condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' - - def setup(self, xml=None): - if ElementBase.setup(self, xml): #if we had to generate xml - self['type'] = 'cancel' - self['condition'] = 'feature-not-implemented' - if self.parent is not None: - self.parent()['type'] = 'error' - - def getCondition(self): - for child in self.xml.getchildren(): - if "{%s}" % self.condition_ns in child.tag: - return child.tag.split('}', 1)[-1] - return '' - - def setCondition(self, value): - if value in self.conditions: - for child in self.xml.getchildren(): - if "{%s}" % self.condition_ns in child.tag: - self.xml.remove(child) - condition = ET.Element("{%s}%s" % (self.condition_ns, value)) - self.xml.append(condition) - return self - - def delCondition(self): - return self - - def getText(self): - text = '' - textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text") - if textxml is not None: - text = textxml.text - return text - - def setText(self, value): - self.delText() - textxml = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}text') - textxml.text = value - self.xml.append(textxml) - return self - - def delText(self): - textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text") - if textxml is not None: - self.xml.remove(textxml) + + """ + 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. + getCondition -- Retrieve the name of the condition element. + setCondition -- Add a condition element. + delCondition -- Remove the condition element. + getText -- Retrieve the contents of the <text> element. + setText -- Set the contents of the <text> element. + delText -- Remove the <text> element. + """ + + namespace = 'jabber:client' + name = 'error' + plugin_attrib = 'error' + interfaces = set(('code', 'condition', 'text', 'type')) + sub_interfaces = set(('text',)) + 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 getCondition(self): + """Return the condition element's name.""" + for child in self.xml.getchildren(): + if "{%s}" % self.condition_ns in child.tag: + return child.tag.split('}', 1)[-1] + return '' + + def setCondition(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 delCondition(self): + """Remove the condition element.""" + for child in self.xml.getchildren(): + 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 getText(self): + """Retrieve the contents of the <text> element.""" + return self._getSubText('{%s}text' % self.condition_ns) + + def setText(self, value): + """ + Set the contents of the <text> element. + + Arguments: + value -- The new contents for the <text> element. + """ + self._setSubText('{%s}text' % self.condition_ns, text=value) + return self + + def delText(self): + """Remove the <text> element.""" + self._delSub('{%s}text' % self.condition_ns) + return self diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 60686e4a..c2f2f0c8 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -3,33 +3,78 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET + +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class HTMLIM(ElementBase): - namespace = 'http://jabber.org/protocol/xhtml-im' - name = 'html' - plugin_attrib = 'html' - interfaces = set(('html',)) - plugin_attrib_map = set() - plugin_xml_map = set() - - def setHtml(self, html): - if isinstance(html, str): - html = ET.XML(html) - if html.tag != '{http://www.w3.org/1999/xhtml}body': - body = ET.Element('{http://www.w3.org/1999/xhtml}body') - body.append(html) - self.xml.append(body) - else: - self.xml.append(html) - - def getHtml(self): - html = self.xml.find('{http://www.w3.org/1999/xhtml}body') - if html is None: return '' - return html - - def delHtml(self): - if self.parent is not None: - self.parent().xml.remove(self.xml) + + """ + XEP-0071: XHTML-IM defines a method for embedding XHTML content + within a <message> stanza so that lightweight markup can be used + to format the message contents and to create links. + + Only a subset of XHTML is recommended for use with XHTML-IM. + See the full spec at 'http://xmpp.org/extensions/xep-0071.html' + for more information. + + Example stanza: + <message to="user@example.com"> + <body>Non-html message content.</body> + <html xmlns="http://jabber.org/protocol/xhtml-im"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <p><b>HTML!</b></p> + </body> + </html> + </message> + + Stanza Interface: + body -- The contents of the HTML body tag. + + Methods: + getBody -- Return the HTML body contents. + setBody -- Set the HTML body contents. + delBody -- Remove the HTML body contents. + """ + + namespace = 'http://jabber.org/protocol/xhtml-im' + name = 'html' + interfaces = set(('body',)) + plugin_attrib = name + + def setBody(self, html): + """ + Set the contents of the HTML body. + + Arguments: + html -- Either a string or XML object. If the top level + element is not <body> with a namespace of + 'http://www.w3.org/1999/xhtml', it will be wrapped. + """ + if isinstance(html, str): + html = ET.XML(html) + if html.tag != '{http://www.w3.org/1999/xhtml}body': + body = ET.Element('{http://www.w3.org/1999/xhtml}body') + body.append(html) + self.xml.append(body) + else: + self.xml.append(html) + + def getBody(self): + """Return the contents of the HTML body.""" + html = self.xml.find('{http://www.w3.org/1999/xhtml}body') + if html is None: + return '' + return html + + def delBody(self): + """Remove the HTML body contents.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) + + +registerStanzaPlugin(Message, HTMLIM) diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index ded7515f..c5ef8bb4 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -3,75 +3,175 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from .. xmlstream.handler.waiter import Waiter -from .. xmlstream.matcher.id import MatcherId -from . rootstanza import RootStanza + +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream import RESPONSE_TIMEOUT +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.xmlstream.handler import Waiter +from sleekxmpp.xmlstream.matcher import MatcherId + class Iq(RootStanza): - interfaces = set(('type', 'to', 'from', 'id','query')) - types = set(('get', 'result', 'set', 'error')) - name = 'iq' - plugin_attrib = name - namespace = 'jabber:client' - - def __init__(self, *args, **kwargs): - StanzaBase.__init__(self, *args, **kwargs) - if self['id'] == '': - if self.stream is not None: - self['id'] = self.stream.getNewId() - else: - self['id'] = '0' - - def unhandled(self): - if self['type'] in ('get', 'set'): - self.reply() - self['error']['condition'] = 'feature-not-implemented' - self['error']['text'] = 'No handlers registered for this request.' - self.send() - - def setPayload(self, value): - self.clear() - StanzaBase.setPayload(self, value) - return self - - def setQuery(self, value): - query = self.xml.find("{%s}query" % value) - if query is None and value: - self.clear() - query = ET.Element("{%s}query" % value) - self.xml.append(query) - return self - - def getQuery(self): - for child in self.xml.getchildren(): - if child.tag.endswith('query'): - ns =child.tag.split('}')[0] - if '{' in ns: - ns = ns[1:] - return ns - return '' - - def reply(self): - self['type'] = 'result' - StanzaBase.reply(self) - return self - - def delQuery(self): - for child in self.getchildren(): - if child.tag.endswith('query'): - self.xml.remove(child) - return self - - def send(self, block=True, timeout=10): - if block and self['type'] in ('get', 'set'): - waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) - self.stream.registerHandler(waitfor) - StanzaBase.send(self) - return waitfor.wait(timeout) - else: - return StanzaBase.send(self) + + """ + 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: + <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. + + Methods: + __init__ -- Overrides StanzaBase.__init__. + unhandled -- Send error if there are no handlers. + setPayload -- Overrides StanzaBase.setPayload. + setQuery -- Add or modify a <query> element. + getQuery -- Return the namespace of the <query> element. + delQuery -- Remove the <query> element. + reply -- Overrides StanzaBase.reply + send -- Overrides StanzaBase.send + """ + + 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.getNewId() + 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'): + self.reply() + self['error']['condition'] = 'feature-not-implemented' + self['error']['text'] = 'No handlers registered for this request.' + self.send() + + def setPayload(self, value): + """ + Set the XML contents of the <iq> stanza. + + Arguments: + value -- An XML object to use as the <iq> stanza's contents + """ + self.clear() + StanzaBase.setPayload(self, value) + return self + + def setQuery(self, value): + """ + Add or modify a <query> element. + + Query elements are differentiated by their namespace. + + Arguments: + value -- The namespace of the <query> element. + """ + query = self.xml.find("{%s}query" % value) + if query is None and value: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) + return self + + def getQuery(self): + """Return the namespace of the <query> element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + ns = child.tag.split('}')[0] + if '{' in ns: + ns = ns[1:] + return ns + return '' + + def delQuery(self): + """Remove the <query> element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + self.xml.remove(child) + return self + + def reply(self): + """ + Send a reply <iq> stanza. + + Overrides StanzaBase.reply + + Sets the 'type' to 'result' in addition to the default + StanzaBase.reply behavior. + """ + self['type'] = 'result' + StanzaBase.reply(self) + return self + + def send(self, block=True, timeout=RESPONSE_TIMEOUT): + """ + Send an <iq> stanza over the XML stream. + + The send call can optionally block until a response is received or + a timeout occurs. Be aware that using blocking in non-threaded event + handlers can drastically impact performance. + + Overrides StanzaBase.send + + Arguments: + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + """ + if block and self['type'] in ('get', 'set'): + waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) + self.stream.registerHandler(waitfor) + StanzaBase.send(self) + return waitfor.wait(timeout) + else: + return StanzaBase.send(self) diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 38341809..560e1d47 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -3,61 +3,141 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from . rootstanza import RootStanza + +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET + class Message(RootStanza): - interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick')) - types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) - sub_interfaces = set(('body', 'subject')) - name = 'message' - plugin_attrib = name - namespace = 'jabber:client' - - def getType(self): - return self.xml.attrib.get('type', 'normal') - - def chat(self): - self['type'] = 'chat' - return self - - def normal(self): - self['type'] = 'normal' - return self - - def reply(self, body=None): - StanzaBase.reply(self) - if self['type'] == 'groupchat': - self['to'] = self['to'].bare - del self['id'] - if body is not None: - self['body'] = body - return self - - def getMucroom(self): - if self['type'] == 'groupchat': - return self['from'].bare - else: - return '' - - def setMucroom(self, value): - pass - - def delMucroom(self): - pass - - def getMucnick(self): - if self['type'] == 'groupchat': - return self['from'].resource - else: - return '' - - def setMucnick(self, value): - pass - - def delMucnick(self): - pass + + """ + 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: + <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. + + Methods: + chat -- Set the message type to 'chat'. + normal -- Set the message type to 'normal'. + reply -- Overrides StanzaBase.reply + getType -- Overrides StanzaBase interface + getMucroom -- Return the name of the MUC room of the message. + setMucroom -- Dummy method to prevent assignment. + delMucroom -- Dummy method to prevent deletion. + getMucnick -- Return the MUC nickname of the message's sender. + setMucnick -- Dummy method to prevent assignment. + delMucnick -- Dummy method to prevent deletion. + """ + + namespace = 'jabber:client' + name = 'message' + interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', + 'mucroom', 'mucnick')) + sub_interfaces = set(('body', 'subject')) + plugin_attrib = name + types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) + + def getType(self): + """ + Return the message type. + + Overrides default stanza interface behavior. + + Returns 'normal' if no type attribute is present. + """ + return self._getAttr('type', 'normal') + + def chat(self): + """Set the message type to 'chat'.""" + self['type'] = 'chat' + return self + + def normal(self): + """Set the message type to 'chat'.""" + self['type'] = 'normal' + return self + + def reply(self, body=None): + """ + 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. + + Arguments: + body -- Optional text content for the message. + """ + StanzaBase.reply(self) + if self['type'] == 'groupchat': + self['to'] = self['to'].bare + + del self['id'] + + if body is not None: + self['body'] = body + return self + + def getMucroom(self): + """ + Return the name of the MUC room where the message originated. + + Read-only stanza interface. + """ + if self['type'] == 'groupchat': + return self['from'].bare + else: + return '' + + def getMucnick(self): + """ + Return the nickname of the MUC user that sent the message. + + Read-only stanza interface. + """ + if self['type'] == 'groupchat': + return self['from'].resource + else: + return '' + + def setMucroom(self, value): + """Dummy method to prevent modification.""" + pass + + def delMucroom(self): + """Dummy method to prevent deletion.""" + pass + + def setMucnick(self, value): + """Dummy method to prevent modification.""" + pass + + def delMucnick(self): + """Dummy method to prevent deletion.""" + pass diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index ac7e3604..de54b307 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -3,24 +3,70 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class Nick(ElementBase): - namespace = 'http://jabber.org/nick/nick' - name = 'nick' - plugin_attrib = 'nick' - interfaces = set(('nick')) - plugin_attrib_map = set() - plugin_xml_map = set() - - def setNick(self, nick): - self.xml.text = nick - - def getNick(self): - return self.xml.text - - def delNick(self): - if self.parent is not None: - self.parent().xml.remove(self.xml) + + """ + XEP-0172: User Nickname allows the addition of a <nick> element + in several stanza types, including <message> and <presence> stanzas. + + The nickname contained in a <nick> should be the global, friendly or + informal name chosen by the owner of a bare JID. The <nick> element + may be included when establishing communications with new entities, + such as normal XMPP users or MUC services. + + The nickname contained in a <nick> element will not necessarily be + the same as the nickname used in a MUC. + + Example stanzas: + <message to="user@example.com"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + <body>...</body> + </message> + + <presence to="otheruser@example.com" type="subscribe"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + </presence> + + Stanza Interface: + nick -- A global, friendly or informal name chosen by a user. + + Methods: + getNick -- Return the nickname in the <nick> element. + setNick -- Add a <nick> element with the given nickname. + delNick -- Remove the <nick> element. + """ + + namespace = 'http://jabber.org/nick/nick' + name = 'nick' + plugin_attrib = name + interfaces = set(('nick',)) + + def setNick(self, nick): + """ + Add a <nick> element with the given nickname. + + Arguments: + nick -- A human readable, informal name. + """ + self.xml.text = nick + + def getNick(self): + """Return the nickname in the <nick> element.""" + return self.xml.text + + def delNick(self): + """Remove the <nick> element.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) + + +registerStanzaPlugin(Message, Nick) +registerStanzaPlugin(Presence, Nick) diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index c66246c9..651bf34d 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -3,61 +3,144 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from . rootstanza import RootStanza + +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET + class Presence(RootStanza): - interfaces = set(('type', 'to', 'from', 'id', 'status', 'priority')) - types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed')) - showtypes = set(('dnd', 'chat', 'xa', 'away')) - sub_interfaces = set(('status', 'priority')) - name = 'presence' - plugin_attrib = name - namespace = 'jabber:client' - - def getShowElement(self): - return self.xml.find("{%s}show" % self.namespace) - - def setType(self, value): - show = self.getShowElement() - if value in self.types: - if show is not None: - self.xml.remove(show) - if value == 'available': - value = '' - self._setAttr('type', value) - elif value in self.showtypes: - if show is None: - show = ET.Element("{%s}show" % self.namespace) - self.xml.append(show) - show.text = value - return self - - def setPriority(self, value): - self._setSubText('priority', text = str(value)) - - def getPriority(self): - p = self._getSubText('priority') - if not p: p = 0 - return int(p) - - def getType(self): - out = self._getAttr('type') - if not out: - show = self.getShowElement() - if show is not None: - out = show.text - if not out or out is None: - out = 'available' - return out - - def reply(self): - if self['type'] == 'unsubscribe': - self['type'] = 'unsubscribed' - elif self['type'] == 'subscribe': - self['type'] = 'subscribed' - return StanzaBase.reply(self) + + """ + 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: + <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. + + Methods: + reply -- Overrides StanzaBase.reply + setShow -- Set the value of the <show> element. + getType -- Get the value of the type attribute or <show> element. + setType -- Set the value of the type attribute or <show> element. + getPriority -- Get the value of the <priority> element. + setPriority -- Set the value of the <priority> element. + """ + + namespace = 'jabber:client' + name = 'presence' + interfaces = set(('type', 'to', 'from', 'id', 'show', + 'status', 'priority')) + sub_interfaces = set(('show', 'status', 'priority')) + plugin_attrib = name + + types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', + 'subscribed', 'unsubscribe', 'unsubscribed')) + showtypes = set(('dnd', 'chat', 'xa', 'away')) + + def setShow(self, show): + """ + Set the value of the <show> element. + + Arguments: + show -- Must be one of: away, chat, dnd, or xa. + """ + if show in self.showtypes: + self._setSubText('show', text=show) + return self + + def setType(self, value): + """ + Set the type attribute's value, and the <show> element + if applicable. + + Arguments: + value -- Must be in either self.types or self.showtypes. + """ + if value in self.types: + self['show'] = None + if value == 'available': + value = '' + self._setAttr('type', value) + elif value in self.showtypes: + self['show'] = value + return self + + def setPriority(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. + + Arguments: + value -- An integer value greater than or equal to 0. + """ + self._setSubText('priority', text=str(value)) + + def getPriority(self): + """ + Return the value of the <presence> element as an integer. + """ + p = self._getSubText('priority') + if not p: + p = 0 + return int(p) + + def getType(self): + """ + Return the value of the <presence> stanza's type attribute, or + the value of the <show> element. + """ + out = self._getAttr('type') + if not out: + out = self['show'] + if not out or out is None: + out = 'available' + return out + + def reply(self): + """ + Set the appropriate presence reply type. + + Overrides StanzaBase.reply. + """ + if self['type'] == 'unsubscribe': + self['type'] = 'unsubscribed' + elif self['type'] == 'subscribe': + self['type'] = 'subscribed' + return StanzaBase.reply(self) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 3b4822d8..eafc79a2 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -3,34 +3,64 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from .. exceptions import XMPPError + +import logging import traceback import sys +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.stanza import Error +from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase, registerStanzaPlugin + + class RootStanza(StanzaBase): - def exception(self, e): #called when a handler raises an exception - self.reply() - if isinstance(e, XMPPError): # we raised this deliberately - self['error']['condition'] = e.condition - self['error']['text'] = e.text - if e.extension is not None: # extended error tag - extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args) - self['error'].xml.append(extxml) - self['error']['type'] = e.etype - else: # we probably didn't raise this on purpose, so send back a traceback - self['error']['condition'] = 'undefined-condition' - if sys.version_info < (3,0): - self['error']['text'] = "SleekXMPP got into trouble." - else: - self['error']['text'] = traceback.format_tb(e.__traceback__) - self.send() - -# all jabber:client root stanzas should have the error plugin -RootStanza.plugin_attrib_map['error'] = Error -RootStanza.plugin_tag_map["{%s}%s" % (Error.namespace, Error.name)] = Error + """ + 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 + """ + self.reply() + if isinstance(e, XMPPError): + # We raised this deliberately + self['error']['condition'] = e.condition + self['error']['text'] = e.text + if e.extension is not None: + # Extended error tag + extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), + e.extension_args) + self['error'].append(extxml) + self['error']['type'] = e.etype + else: + # We probably didn't raise this on purpose, so send a traceback + self['error']['condition'] = 'undefined-condition' + if sys.version_info < (3, 0): + self['error']['text'] = "SleekXMPP got into trouble." + else: + self['error']['text'] = traceback.format_tb(e.__traceback__) + logging.exception('Error handling {%s}%s stanza' % + (self.namespace, self.name)) + self.send() + + +registerStanzaPlugin(RootStanza, Error) diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 1fefc180..292c8956 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -3,51 +3,107 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET, JID -import logging + +from sleekxmpp.stanza import Iq +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ET, ElementBase + class Roster(ElementBase): - namespace = 'jabber:iq:roster' - name = 'query' - plugin_attrib = 'roster' - interfaces = set(('items',)) - sub_interfaces = set() - - def setItems(self, items): - self.delItems() - for jid in items: - ijid = str(jid) - item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) - if 'subscription' in items[jid]: - item.attrib['subscription'] = items[jid]['subscription'] - if 'name' in items[jid]: - item.attrib['name'] = items[jid]['name'] - if 'groups' in items[jid]: - for group in items[jid]['groups']: - groupxml = ET.Element('{jabber:iq:roster}group') - groupxml.text = group - item.append(groupxml) - self.xml.append(item) - return self - - def getItems(self): - items = {} - itemsxml = self.xml.findall('{jabber:iq:roster}item') - if itemsxml is not None: - for itemxml in itemsxml: - item = {} - item['name'] = itemxml.get('name', '') - item['subscription'] = itemxml.get('subscription', '') - item['groups'] = [] - groupsxml = itemxml.findall('{jabber:iq:roster}group') - if groupsxml is not None: - for groupxml in groupsxml: - item['groups'].append(groupxml.text) - items[itemxml.get('jid')] = item - return items - - def delItems(self): - for child in self.xml.getchildren(): - self.xml.remove(child) + + """ + 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: + getItems -- Return a dictionary of roster entries. + setItems -- Add <item> elements. + delItems -- Remove all <item> elements. + """ + + namespace = 'jabber:iq:roster' + name = 'query' + plugin_attrib = 'roster' + interfaces = set(('items',)) + + def setItems(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.delItems() + for jid in items: + ijid = str(jid) + item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) + if 'subscription' in items[jid]: + item.attrib['subscription'] = items[jid]['subscription'] + if 'name' in items[jid]: + name = items[jid]['name'] + if name is not None: + item.attrib['name'] = name + if 'groups' in items[jid]: + for group in items[jid]['groups']: + groupxml = ET.Element('{jabber:iq:roster}group') + groupxml.text = group + item.append(groupxml) + self.xml.append(item) + return self + + def getItems(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 = {} + itemsxml = self.xml.findall('{jabber:iq:roster}item') + if itemsxml is not None: + for itemxml in itemsxml: + item = {} + item['name'] = itemxml.get('name', '') + item['subscription'] = itemxml.get('subscription', '') + item['groups'] = [] + groupsxml = itemxml.findall('{jabber:iq:roster}group') + if groupsxml is not None: + for groupxml in groupsxml: + item['groups'].append(groupxml.text) + items[itemxml.get('jid')] = item + return items + + def delItems(self): + """ + Remove all <item> elements from the roster stanza. + """ + for child in self.xml.getchildren(): + self.xml.remove(child) + + +registerStanzaPlugin(Iq, Roster) |