diff options
-rw-r--r-- | sleekxmpp/basexmpp.py | 8 | ||||
-rw-r--r-- | sleekxmpp/plugins/__init__.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/alt_0004.py | 330 | ||||
-rw-r--r-- | sleekxmpp/plugins/gmail_notify.py | 193 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030.py | 17 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0033.py | 161 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0085.py | 101 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/stanzabase.py | 7 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 14 | ||||
-rw-r--r-- | tests/sleektest.py | 299 | ||||
-rw-r--r-- | tests/test_addresses.py | 110 | ||||
-rw-r--r-- | tests/test_chatstates.py | 44 | ||||
-rw-r--r-- | tests/test_disco.py | 113 | ||||
-rw-r--r-- | tests/test_forms.py | 90 | ||||
-rw-r--r-- | tests/test_gmail.py | 88 | ||||
-rw-r--r-- | tests/test_stream.py | 34 |
16 files changed, 1495 insertions, 116 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 907067fa..9728c3f4 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -27,6 +27,7 @@ from . stanza.error import Error import logging import threading +import copy import sys @@ -152,7 +153,7 @@ class basexmpp(object): return waitfor.wait(timeout) def makeIq(self, id=0, ifrom=None): - return self.Iq().setValues({'id': id, 'from': ifrom}) + return self.Iq().setValues({'id': str(id), 'from': ifrom}) def makeIqGet(self, queryxmlns = None): iq = self.Iq().setValues({'type': 'get'}) @@ -205,12 +206,13 @@ class basexmpp(object): def event(self, name, eventdata = {}): # called on an event for handler in self.event_handlers.get(name, []): + handlerdata = copy.copy(eventdata) if handler[1]: #if threaded #thread.start_new(handler[0], (eventdata,)) - x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,)) + x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(handlerdata,)) x.start() else: - handler[0](eventdata) + handler[0](handlerdata) if handler[2]: #disposable with self.lock: self.event_handlers[name].pop(self.event_handlers[name].index(handler)) diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 1868365e..fbc5e014 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -17,4 +17,4 @@ along with SleekXMPP; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ -__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] +__all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] diff --git a/sleekxmpp/plugins/alt_0004.py b/sleekxmpp/plugins/alt_0004.py new file mode 100644 index 00000000..b38a4918 --- /dev/null +++ b/sleekxmpp/plugins/alt_0004.py @@ -0,0 +1,330 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permission. +""" + +import logging +import copy +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class Form(ElementBase): + namespace = 'jabber:x:data' + name = 'x' + plugin_attrib = 'form' + interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) + sub_interfaces = set(('title',)) + form_types = set(('cancel', 'form', 'result', 'submit')) + + def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): + field = FormField(parent=self) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + field['required'] = required + field['value'] = value + if options is not None: + field['options'] = options + return field + + def addItem(self, values): + itemXML = ET.Element('{%s}item' % self.namespace) + self.xml.append(itemXML) + reported_vars = self['reported'].keys() + for var in reported_vars: + fieldXML = ET.Element('{%s}field' % FormField.namespace) + itemXML.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['value'] = values.get(var, None) + + def addReported(self, var, ftype='text-single', label='', desc=''): + reported = self.xml.find('{%s}reported' % self.namespace) + if reported is None: + reported = ET.Element('{%s}reported' % self.namespace) + self.xml.append(reported) + fieldXML = ET.Element('{%s}field' % FormField.namespace) + reported.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + return field + + def cancel(self): + self['type'] = 'cancel' + + def delFields(self): + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + self.xml.remove(fieldXML) + + def delInstructions(self): + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + self.xml.remove(instXML) + + def delItems(self): + itemsXML = self.xml.find('{%s}item' % self.namespace) + for itemXML in itemsXML: + self.xml.remove(itemXML) + + def delReported(self): + reportedXML = self.xml.find('{%s}reported' % self.namespace) + if reportedXML is not None: + self.xml.remove(reportedXML) + + def getFields(self): + fields = {} + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getInstructions(self): + instructions = '' + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + instructions += instXML.text + + def getItems(self): + items = [] + itemsXML = self.xml.findall('{%s}item' % self.namespace) + for itemXML in itemsXML: + item = {} + fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + item[field['var']] = field['value'] + items.append(item) + return items + + def getReported(self): + fields = {} + fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, + FormField.namespace)) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getValues(self): + values = {} + fields = self.getFields() + for var in fields: + values[var] = fields[var]['value'] + return values + + def reply(self): + if self['type'] == 'form': + self['type'] = 'submit' + elif self['type'] == 'submit': + self['type'] = 'result' + + def setFields(self, fields): + del self['fields'] + for var in fields: + field = fields[var] + + # Remap 'type' to 'ftype' to match the addField method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addField(var, **field) + + def setInstructions(self, instructions): + instructions = instructions.split('\n') + for instruction in instructions: + inst = ET.Element('{%s}instructions' % self.namespace) + inst.text = instruction + self.xml.append(inst) + + def setItems(self, items): + for item in items: + self.addItem(item) + + def setReported(self, reported): + for var in reported: + field = reported[var] + + # Remap 'type' to 'ftype' to match the addReported method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addReported(var, **field) + + def setValues(self, values): + fields = self.getFields() + for field in values: + fields[field]['value'] = values[field] + + +class FormField(ElementBase): + namespace = 'jabber:x:data' + name = 'field' + plugin_attrib = 'field' + interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) + sub_interfaces = set(('desc',)) + field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', + 'list-single', 'text-multi', 'text-private', 'text-single')) + multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) + multi_line_types = set(('hidden', 'text-multi')) + option_types = set(('list-multi', 'list-single')) + true_values = set((True, '1', 'true')) + + def addOption(self, label='', value=''): + if self['type'] in self.option_types: + opt = FieldOption(parent=self) + opt['label'] = label + opt['value'] = value + else: + raise ValueError("Cannot add options to a %s field." % self['type']) + + def delOptions(self): + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + self.xml.remove(optXML) + + def delRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + if reqXML is not None: + self.xml.remove(reqXML) + + def delValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + for valXML in valsXML: + self.xml.remove(valXML) + + def getAnswer(self): + return self.getValue() + + def getOptions(self): + options = [] + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + opt = FieldOption(xml=optXML) + options.append({'label': opt['label'], 'value':opt['value']}) + return options + + def getRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + return reqXML is not None + + def getValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + if len(valsXML) == 0: + return None + elif self['type'] == 'boolean': + return valsXML[0].text in self.true_values + elif self['type'] in self.multi_value_types: + values = [] + for valXML in valsXML: + if valXML.text is None: + valXML.text = '' + values.append(valXML.text) + if self['type'] == 'text-multi': + values = "\n".join(values) + return values + else: + return valsXML[0].text + + def setAnswer(self, answer): + self.setValue(answer) + + def setFalse(self): + self.setValue(False) + + def setOptions(self, options): + for value in options: + if isinstance(value, dict): + self.addOption(**value) + else: + self.addOption(value=value) + + def setRequired(self, required): + exists = self.getRequired() + if not exists and required: + self.xml.append(ET.Element('{%s}required' % self.namespace)) + elif exists and not required: + self.delRequired() + + def setTrue(self): + self.setValue(True) + + def setValue(self, value): + self.delValue() + valXMLName = '{%s}value' % self.namespace + + if self['type'] == 'boolean': + if value in self.true_values: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + else: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + if self['type'] in self.multi_value_types: + if self['type'] in self.multi_line_types and isinstance(value, str): + value = value.split('\n') + if not isinstance(value, list): + value = [value] + for val in value: + valXML = ET.Element(valXMLName) + valXML.text = val + self.xml.append(valXML) + else: + if isinstance(value, list): + raise ValueError("Cannot add multiple values to a %s field." % self['type']) + valXML = ET.Element(valXMLName) + valXML.text = value + self.xml.append(valXML) + + +class FieldOption(ElementBase): + namespace = 'jabber:x:data' + name = 'option' + plugin_attrib = 'option' + interfaces = set(('label', 'value')) + sub_interfaces = set(('value',)) + + +class alt_0004(base.base_plugin): + """ + XEP-0004: Data Forms + """ + + def plugin_init(self): + self.xep = '0004' + self.description = 'Data Forms' + + self.xmpp.registerHandler( + Callback('Data Form', + MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, + Form.namespace)), + self.handle_form)) + + self.xmpp.stanzaPlugin(FormField, FieldOption) + self.xmpp.stanzaPlugin(Form, FormField) + self.xmpp.stanzaPlugin(Message, Form) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') + + def handle_form(self, message): + self.xmpp.event("message_xform", message) diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index b709ef69..acfc38b0 100644 --- a/sleekxmpp/plugins/gmail_notify.py +++ b/sleekxmpp/plugins/gmail_notify.py @@ -1,57 +1,146 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permission. """ -from __future__ import with_statement -from . import base + import logging -from xml.etree import cElementTree as ET -import traceback -import time +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.iq import Iq + + +class GmailQuery(ElementBase): + namespace = 'google:mail:notify' + name = 'query' + plugin_attrib = 'gmail' + interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search')) + + def getSearch(self): + return self['q'] + + def setSearch(self, search): + self['q'] = search + + def delSearch(self): + del self['q'] + + +class MailBox(ElementBase): + namespace = 'google:mail:notify' + name = 'mailbox' + plugin_attrib = 'mailbox' + interfaces = set(('result-time', 'total-matched', 'total-estimate', + 'url', 'threads', 'matched', 'estimate')) + + def getThreads(self): + threads = [] + for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace, + MailThread.name)): + threads.append(MailThread(xml=threadXML, parent=None)) + return threads + + def getMatched(self): + return self['total-matched'] + + def getEstimate(self): + return self['total-estimate'] == '1' + + +class MailThread(ElementBase): + namespace = 'google:mail:notify' + name = 'mail-thread-info' + plugin_attrib = 'thread' + interfaces = set(('tid', 'participation', 'messages', 'date', + 'senders', 'url', 'labels', 'subject', 'snippet')) + sub_interfaces = set(('labels', 'subject', 'snippet')) + + def getSenders(self): + senders = [] + sendersXML = self.xml.find('{%s}senders' % self.namespace) + if sendersXML is not None: + for senderXML in sendersXML.findall('{%s}sender' % self.namespace): + senders.append(MailSender(xml=senderXML, parent=None)) + return senders + + +class MailSender(ElementBase): + namespace = 'google:mail:notify' + name = 'sender' + plugin_attrib = 'sender' + interfaces = set(('address', 'name', 'originator', 'unread')) + + def getOriginator(self): + return self.xml.attrib.get('originator', '0') == '1' + + def getUnread(self): + return self.xml.attrib.get('unread', '0') == '1' + + +class NewMail(ElementBase): + namespace = 'google:mail:notify' + name = 'new-mail' + plugin_attrib = 'new-mail' + class gmail_notify(base.base_plugin): - - def plugin_init(self): - self.description = 'Google Talk Gmail Notification' - self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True) - self.emails = [] - - def handler_gmailcheck(self, payload): - #TODO XEP 30 should cache results and have getFeature - result = self.xmpp['xep_0030'].getInfo(self.xmpp.server) - features = [] - for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'): - features.append(feature.get('var')) - if 'google:mail:notify' in features: - logging.debug("Server supports Gmail Notify") - self.xmpp.add_handler("<iq type='set' xmlns='%s'><new-mail xmlns='google:mail:notify' /></iq>" % self.xmpp.default_ns, self.handler_notify) - self.getEmail() - - def handler_notify(self, xml): - logging.info("New Gmail recieved!") - self.xmpp.event('gmail_notify') - - def getEmail(self): - iq = self.xmpp.makeIqGet() - iq.attrib['from'] = self.xmpp.fulljid - iq.attrib['to'] = self.xmpp.jid - self.xmpp.makeIqQuery(iq, 'google:mail:notify') - emails = iq.send() - mailbox = emails.find('{google:mail:notify}mailbox') - total = int(mailbox.get('total-matched', 0)) - logging.info("%s New Gmail Messages" % total) + """ + Google Talk: Gmail Notifications + """ + + def plugin_init(self): + self.description = 'Google Talk: Gmail Notifications' + + self.xmpp.registerHandler( + Callback('Gmail Result', + MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, + MailBox.namespace, + MailBox.name)), + self.handle_gmail)) + + self.xmpp.registerHandler( + Callback('Gmail New Mail', + MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, + NewMail.namespace, + NewMail.name)), + self.handle_new_mail)) + + self.xmpp.stanzaPlugin(Iq, GmailQuery) + self.xmpp.stanzaPlugin(Iq, MailBox) + self.xmpp.stanzaPlugin(Iq, NewMail) + + self.last_result_time = None + + def handle_gmail(self, iq): + mailbox = iq['mailbox'] + approx = ' approximately' if mailbox['estimated'] else '' + logging.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) + self.last_result_time = mailbox['result-time'] + self.xmpp.event('gmail_messages', iq) + + def handle_new_mail(self, iq): + logging.info("Gmail: New emails received!") + self.xmpp.event('gmail_notify') + self.checkEmail() + + def getEmail(self, query=None): + return self.search(query) + + def checkEmail(self): + return self.search(newer=self.last_result_time) + + def search(self, query=None, newer=None): + if query is None: + logging.info("Gmail: Checking for new emails") + else: + logging.info('Gmail: Searching for emails matching: "%s"' % query) + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = self.xmpp.jid + iq['gmail']['q'] = query + iq['gmail']['newer-than-time'] = newer + return iq.send() diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 6a31d243..93e094f2 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permissio + See the file license.txt for copying permission. """ import logging @@ -138,6 +138,9 @@ class DiscoNode(object): self.info = DiscoInfo() self.items = DiscoItems() + self.info['node'] = name + self.items['node'] = name + # This is a bit like poor man's inheritance, but # to simplify adding information to the node we # map node functions to either the info or items @@ -290,21 +293,21 @@ class xep_0030(base.base_plugin): # Older interface methods for backwards compatibility - def getInfo(self, jid, node=''): + def getInfo(self, jid, node='', dfrom=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid - iq['from'] = self.xmpp.fulljid + iq['from'] = dfrom iq['disco_info']['node'] = node - iq.send() + return iq.send() - def getItems(self, jid, node=''): + def getItems(self, jid, node='', dfrom=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid - iq['from'] = self.xmpp.fulljid + iq['from'] = dfrom iq['disco_items']['node'] = node - iq.send() + return iq.send() def add_feature(self, feature, node='main'): self.add_node(node) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py new file mode 100644 index 00000000..df8bb88d --- /dev/null +++ b/sleekxmpp/plugins/xep_0033.py @@ -0,0 +1,161 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permission. +""" + +import logging +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class Addresses(ElementBase): + namespace = 'http://jabber.org/protocol/address' + name = 'addresses' + plugin_attrib = 'addresses' + interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): + address = Address(parent=self) + address['type'] = atype + address['jid'] = jid + address['node'] = node + address['uri'] = uri + address['desc'] = desc + address['delivered'] = delivered + return address + + def getAddresses(self, atype=None): + addresses = [] + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if atype is None or addrXML.attrib.get('type') == atype: + addresses.append(Address(xml=addrXML, parent=None)) + return addresses + + def setAddresses(self, addresses, set_type=None): + self.delAddresses(set_type) + for addr in addresses: + addr = dict(addr) + # Remap 'type' to 'atype' to match the add method + if set_type is not None: + addr['type'] = set_type + curr_type = addr.get('type', None) + if curr_type is not None: + del addr['type'] + addr['atype'] = curr_type + self.addAddress(**addr) + + def delAddresses(self, atype=None): + if atype is None: + return + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if addrXML.attrib.get('type') == atype: + self.xml.remove(addrXML) + + # -------------------------------------------------------------- + + def delBcc(self): + self.delAddresses('bcc') + + def delCc(self): + self.delAddresses('cc') + + def delNoreply(self): + self.delAddresses('noreply') + + def delReplyroom(self): + self.delAddresses('replyroom') + + def delReplyto(self): + self.delAddresses('replyto') + + def delTo(self): + self.delAddresses('to') + + # -------------------------------------------------------------- + + def getBcc(self): + return self.getAddresses('bcc') + + def getCc(self): + return self.getAddresses('cc') + + def getNoreply(self): + return self.getAddresses('noreply') + + def getReplyroom(self): + return self.getAddresses('replyroom') + + def getReplyto(self): + return self.getAddresses('replyto') + + def getTo(self): + return self.getAddresses('to') + + # -------------------------------------------------------------- + + def setBcc(self, addresses): + self.setAddresses(addresses, 'bcc') + + def setCc(self, addresses): + self.setAddresses(addresses, 'cc') + + def setNoreply(self, addresses): + self.setAddresses(addresses, 'noreply') + + def setReplyroom(self, addresses): + self.setAddresses(addresses, 'replyroom') + + def setReplyto(self, addresses): + self.setAddresses(addresses, 'replyto') + + def setTo(self, addresses): + self.setAddresses(addresses, 'to') + + +class Address(ElementBase): + namespace = 'http://jabber.org/protocol/address' + name = 'address' + plugin_attrib = 'address' + interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def getDelivered(self): + return self.xml.attrib.get('delivered', False) + + def setDelivered(self, delivered): + if delivered: + self.xml.attrib['delivered'] = "true" + else: + del self['delivered'] + + def setUri(self, uri): + if uri: + del self['jid'] + del self['node'] + self.xml.attrib['uri'] = uri + elif 'uri' in self.xml.attrib: + del self.xml.attrib['uri'] + + +class xep_0030(base.base_plugin): + """ + XEP-0033: Extended Stanza Addressing + """ + + def plugin_init(self): + self.xep = '0033' + self.description = 'Extended Stanza Addressing' + + self.xmpp.stanzaPlugin(Message, Addresses) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py new file mode 100644 index 00000000..e24e9db0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0085.py @@ -0,0 +1,101 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permissio +""" + +import logging +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class ChatState(ElementBase): + namespace = 'http://jabber.org/protocol/chatstates' + plugin_attrib = 'chat_state' + interface = set(('state',)) + states = set(('active', 'composing', 'gone', 'inactive', 'paused')) + + def active(self): + self.setState('active') + + def composing(self): + self.setState('composing') + + def gone(self): + self.setState('gone') + + def inactive(self): + self.setState('inactive') + + def paused(self): + self.setState('paused') + + def setState(self, state): + if state in self.states: + self.name = state + self.xml.tag = '{%s}%s' % (self.namespace, state) + else: + raise ValueError('Invalid chat state') + + def getState(self): + return self.name + +# In order to match the various chat state elements, +# we need one stanza object per state, even though +# they are all the same except for the initial name +# value. Do not depend on the type of the chat state +# stanza object for the actual state. + +class Active(ChatState): + name = 'active' +class Composing(ChatState): + name = 'composing' +class Gone(ChatState): + name = 'gone' +class Inactive(ChatState): + name = 'inactive' +class Paused(ChatState): + name = 'paused' + + +class xep_0085(base.base_plugin): + """ + XEP-0085 Chat State Notifications + """ + + def plugin_init(self): + self.xep = '0085' + self.description = 'Chat State Notifications' + + handlers = [('Active Chat State', 'active'), + ('Composing Chat State', 'composing'), + ('Gone Chat State', 'gone'), + ('Inactive Chat State', 'inactive'), + ('Paused Chat State', 'paused')] + for handler in handlers: + self.xmpp.registerHandler( + Callback(handler[0], + MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns, + ChatState.namespace, + handler[1])), + self._handleChatState)) + + self.xmpp.stanzaPlugin(Message, Active) + self.xmpp.stanzaPlugin(Message, Composing) + self.xmpp.stanzaPlugin(Message, Gone) + self.xmpp.stanzaPlugin(Message, Inactive) + self.xmpp.stanzaPlugin(Message, Paused) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates') + + def _handleChatState(self, msg): + state = msg['chat_state'].name + logging.debug("Chat State: %s, %s" % (state, msg['from'].jid)) + self.xmpp.event('chatstate_%s' % state, msg) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 64020c8f..024fe6cf 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -10,6 +10,7 @@ import logging import traceback import sys import weakref +import copy if sys.version_info < (3,0): from . import tostring26 as tostring @@ -308,6 +309,9 @@ class ElementBase(tostring.ToString): def appendxml(self, xml): self.xml.append(xml) return self + + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) #def __del__(self): #prevents garbage collection of reference cycle # if self.parent is not None: @@ -386,3 +390,6 @@ class StanzaBase(ElementBase): def send(self): self.stream.sendRaw(self.__str__()) + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) + diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6b92abca..003ead15 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -21,6 +21,7 @@ import threading import time import traceback import types +import copy import xml.sax.saxutils from . import scheduler @@ -305,19 +306,18 @@ class XMLStream(object): #convert XML into Stanza logging.debug("RECV: %s" % cElementTree.tostring(xmlobj)) xmlobj = self.incoming_filter(xmlobj) - stanza = None + stanza_type = StanzaBase for stanza_class in self.__root_stanza: if xmlobj.tag == "{%s}%s" % (self.default_ns, stanza_class.name): - #if self.__root_stanza[stanza_class].match(xmlobj): - stanza = stanza_class(self, xmlobj) + stanza_type = stanza_class break - if stanza is None: - stanza = StanzaBase(self, xmlobj) unhandled = True + stanza = stanza_type(self, xmlobj) for handler in self.__handlers: if handler.match(stanza): - handler.prerun(stanza) - self.eventqueue.put(('stanza', handler, stanza)) + stanza_copy = stanza_type(self, copy.deepcopy(xmlobj)) + handler.prerun(stanza_copy) + self.eventqueue.put(('stanza', handler, stanza_copy)) if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler)) unhandled = False if unhandled: diff --git a/tests/sleektest.py b/tests/sleektest.py new file mode 100644 index 00000000..eef3b900 --- /dev/null +++ b/tests/sleektest.py @@ -0,0 +1,299 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permission. +""" + +import unittest +import socket +try: + import queue +except ImportError: + import Queue as queue +from xml.etree import cElementTree as ET +from sleekxmpp import ClientXMPP +from sleekxmpp import Message, Iq +from sleekxmpp.stanza.presence import Presence +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath + +class TestSocket(object): + + def __init__(self, *args, **kwargs): + self.socket = socket.socket(*args, **kwargs) + self.recv_queue = queue.Queue() + self.send_queue = queue.Queue() + + def __getattr__(self, name): + """Pass requests through to actual socket""" + # Override a few methods to prevent actual socket connections + overrides = {'connect': lambda *args: None, + 'close': lambda *args: None, + 'shutdown': lambda *args: None} + return overrides.get(name, getattr(self.socket, name)) + + # ------------------------------------------------------------------ + # Testing Interface + + def nextSent(self, timeout=None): + """Get the next stanza that has been 'sent'""" + args = {'block': False} + if timeout is not None: + args = {'block': True, 'timeout': timeout} + try: + return self.send_queue.get(**args) + except: + return None + + def recvData(self, data): + """Add data to the receiving queue""" + self.recv_queue.put(data) + + # ------------------------------------------------------------------ + # Socket Interface + + def recv(self, *args, **kwargs): + return self.read(block=True) + + def send(self, data): + self.send_queue.put(data) + + # ------------------------------------------------------------------ + # File Socket + + def makefile(self, mode='r', bufsize=-1): + """File socket version to use with ElementTree""" + return self + + def read(self, size=4096, block=True, timeout=None): + """Implement the file socket interface""" + if timeout is not None: + block = True + try: + return self.recv_queue.get(block, timeout) + except: + return None + +class TestStream(object): + """Dummy class to pass a stream object to created stanzas""" + + def __init__(self): + self.default_ns = 'jabber:client' + + +class SleekTest(unittest.TestCase): + """ + A SleekXMPP specific TestCase class that provides + methods for comparing message, iq, and presence stanzas. + """ + + def stanzaPlugin(self, stanza, plugin): + """ + Associate a stanza object as a plugin for another stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin + + # ------------------------------------------------------------------ + # Shortcut methods for creating stanza objects + + def Message(self, *args, **kwargs): + """Create a message stanza.""" + return Message(None, *args, **kwargs) + + def Iq(self, *args, **kwargs): + """Create an iq stanza.""" + return Iq(None, *args, **kwargs) + + def Presence(self, *args, **kwargs): + """Create a presence stanza.""" + return Presence(None, *args, **kwargs) + + # ------------------------------------------------------------------ + # Methods for comparing stanza objects to XML strings + + def checkMessage(self, msg, xml_string, use_values=True): + """ + Create and compare several message stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + + self.fix_namespaces(msg.xml, 'jabber:client') + debug = "Given Stanza:\n%s\n" % ET.tostring(msg.xml) + + xml = ET.fromstring(xml_string) + self.fix_namespaces(xml, 'jabber:client') + + debug += "XML String:\n%s\n" % ET.tostring(xml) + + msg2 = self.Message(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) + + if use_values: + # Ugly, but need to make sure the type attribute is set. + msg['type'] = msg['type'] + if xml.attrib.get('type', None) is None: + xml.attrib['type'] = 'normal' + + values = msg2.getValues() + msg3 = self.Message() + msg3.setValues(values) + + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) + debug = "Three methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), + debug) + else: + debug = "Two methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, msg.xml, msg2.xml]), debug) + + def checkIq(self, iq, xml_string, use_values=True): + """ + Create and compare several iq stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + + self.fix_namespaces(iq.xml, 'jabber:client') + debug = "Given Stanza:\n%s\n" % ET.tostring(iq.xml) + + xml = ET.fromstring(xml_string) + self.fix_namespaces(xml, 'jabber:client') + debug += "XML String:\n%s\n" % ET.tostring(xml) + + iq2 = self.Iq(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(iq2.xml) + + if use_values: + values = iq.getValues() + iq3 = self.Iq() + iq3.setValues(values) + + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(iq3.xml) + debug = "Three methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), + debug) + else: + debug = "Two methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, iq.xml, iq2.xml]), debug) + + def checkPresence(self, pres, xml_string, use_values=True): + """ + Create and compare several presence stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + pass + + # ------------------------------------------------------------------ + # Methods for simulating stanza streams. + + def streamStart(self, mode='client', skip=True): + if mode == 'client': + self.xmpp = ClientXMPP('tester@localhost', 'test') + self.xmpp.setSocket(TestSocket()) + + self.xmpp.state.set('reconnect', False) + self.xmpp.state.set('is client', True) + self.xmpp.state.set('connected', True) + + # Must have the stream header ready for xmpp.process() to work + self.xmpp.socket.recvData(self.xmpp.stream_header) + + self.xmpp.connectTCP = lambda a, b, c, d: True + self.xmpp.startTLS = lambda: True + self.xmpp.process(threaded=True) + if skip: + # Clear startup stanzas + self.xmpp.socket.nextSent(timeout=1) + + def streamRecv(self, data): + data = str(data) + self.xmpp.socket.recvData(data) + + def streamSendMessage(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Message(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout=1) + self.checkMessage(data, sent, use_values) + + def streamSendIq(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Iq(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkIq(data, sent, use_values) + + def streamSendPresence(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Presence(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkPresence(data, sent, use_values) + + def streamClose(self): + if self.xmpp is not None: + self.xmpp.disconnect() + self.xmpp.socket.recvData(self.xmpp.stream_footer) + + # ------------------------------------------------------------------ + # XML Comparison and Cleanup + + def fix_namespaces(self, xml, ns): + """ + Assign a namespace to an element and any children that + don't have a namespace. + """ + if xml.tag.startswith('{'): + return + xml.tag = '{%s}%s' % (ns, xml.tag) + for child in xml.getchildren(): + self.fix_namespaces(child, ns) + + def compare(self, xml1, xml2=None): + """ + Compare XML objects. + + If given a list of XML objects, then + all of the elements in the list will be + compared. + """ + + # Compare multiple objects + if type(xml1) is list: + xmls = xml1 + xml1 = xmls[0] + for xml in xmls[1:]: + xml2 = xml + if not self.compare(xml1, xml2): + return False + return True + + # Step 1: Check tags + if xml1.tag != xml2.tag: + return False + + # Step 2: Check attributes + if xml1.attrib != xml2.attrib: + return False + + # Step 3: Recursively check children + for child in xml1: + child2s = xml2.findall("%s" % child.tag) + if child2s is None: + return False + for child2 in child2s: + if self.compare(child, child2): + break + else: + return False + + # Everything matches + return True diff --git a/tests/test_addresses.py b/tests/test_addresses.py new file mode 100644 index 00000000..2718bb19 --- /dev/null +++ b/tests/test_addresses.py @@ -0,0 +1,110 @@ +from sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 + + +class TestAddresses(SleekTest): + + def setUp(self): + self.stanzaPlugin(Message, xep_0033.Addresses) + + def testAddAddress(self): + """Testing adding extended stanza address.""" + msg = self.Message() + msg['addresses'].addAddress(atype='to', jid='to@header1.org') + self.checkMessage(msg, """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address jid="to@header1.org" type="to" /> + </addresses> + </message> + """) + + msg = self.Message() + msg['addresses'].addAddress(atype='replyto', + jid='replyto@header1.org', + desc='Reply address') + self.checkMessage(msg, """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address jid="replyto@header1.org" type="replyto" desc="Reply address" /> + </addresses> + </message> + """) + + def testAddAddresses(self): + """Testing adding multiple extended stanza addresses.""" + + xmlstring = """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address jid="replyto@header1.org" type="replyto" desc="Reply address" /> + <address jid="cc@header2.org" type="cc" /> + <address jid="bcc@header2.org" type="bcc" /> + </addresses> + </message> + """ + + msg = self.Message() + msg['addresses'].setAddresses([{'type':'replyto', + 'jid':'replyto@header1.org', + 'desc':'Reply address'}, + {'type':'cc', + 'jid':'cc@header2.org'}, + {'type':'bcc', + 'jid':'bcc@header2.org'}]) + self.checkMessage(msg, xmlstring) + + msg = self.Message() + msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', + 'desc':'Reply address'}] + msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] + msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] + self.checkMessage(msg, xmlstring) + + def testAddURI(self): + """Testing adding URI attribute to extended stanza address.""" + + msg = self.Message() + addr = msg['addresses'].addAddress(atype='to', + jid='to@header1.org', + node='foo') + self.checkMessage(msg, """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address node="foo" jid="to@header1.org" type="to" /> + </addresses> + </message> + """) + + addr['uri'] = 'mailto:to@header2.org' + self.checkMessage(msg, """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address type="to" uri="mailto:to@header2.org" /> + </addresses> + </message> + """) + + def testDelivered(self): + """Testing delivered attribute of extended stanza addresses.""" + + xmlstring = """ + <message> + <addresses xmlns="http://jabber.org/protocol/address"> + <address %s jid="to@header1.org" type="to" /> + </addresses> + </message> + """ + + msg = self.Message() + addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') + self.checkMessage(msg, xmlstring % '') + + addr['delivered'] = True + self.checkMessage(msg, xmlstring % 'delivered="true"') + + addr['delivered'] = False + self.checkMessage(msg, xmlstring % '') + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py new file mode 100644 index 00000000..1e585be4 --- /dev/null +++ b/tests/test_chatstates.py @@ -0,0 +1,44 @@ +from sleektest import * +import sleekxmpp.plugins.xep_0085 as xep_0085 + +class TestChatStates(SleekTest): + + def setUp(self): + self.stanzaPlugin(Message, xep_0085.Active) + self.stanzaPlugin(Message, xep_0085.Composing) + self.stanzaPlugin(Message, xep_0085.Gone) + self.stanzaPlugin(Message, xep_0085.Inactive) + self.stanzaPlugin(Message, xep_0085.Paused) + + def testCreateChatState(self): + """Testing creating chat states.""" + + xmlstring = """ + <message> + <%s xmlns="http://jabber.org/protocol/chatstates" /> + </message> + """ + + msg = self.Message() + msg['chat_state'].active() + self.checkMessage(msg, xmlstring % 'active', + use_values=False) + + msg['chat_state'].composing() + self.checkMessage(msg, xmlstring % 'composing', + use_values=False) + + + msg['chat_state'].gone() + self.checkMessage(msg, xmlstring % 'gone', + use_values=False) + + msg['chat_state'].inactive() + self.checkMessage(msg, xmlstring % 'inactive', + use_values=False) + + msg['chat_state'].paused() + self.checkMessage(msg, xmlstring % 'paused', + use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) diff --git a/tests/test_disco.py b/tests/test_disco.py index bbe285a6..6daad13e 100644 --- a/tests/test_disco.py +++ b/tests/test_disco.py @@ -1,96 +1,118 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from sleektest import * +import sleekxmpp.plugins.xep_0030 as xep_0030 -import sleekxmpp.plugins.xep_0030 as sd -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin - -class testdisco(unittest.TestCase): +class TestDisco(SleekTest): def setUp(self): - self.sd = sd - stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo) - stanzaPlugin(self.sd.Iq, self.sd.DiscoItems) - - def try3Methods(self, xmlstring, iq): - iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring)) - values = iq2.getValues() - iq3 = self.sd.Iq() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match") + self.stanzaPlugin(Iq, xep_0030.DiscoInfo) + self.stanzaPlugin(Iq, xep_0030.DiscoItems) def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = '' - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" /></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#info" /> + </iq> + """) def testCreateInfoQueryWithNode(self): """Testing disco#info query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo" /></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#info" node="foo" /> + </iq> + """) def testCreateInfoQueryNoNode(self): """Testing disco#items query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = '' - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" /></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#items" /> + </iq> + """) def testCreateItemsQueryWithNode(self): """Testing disco#items query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = 'foo' - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo" /></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#items" node="foo" /> + </iq> + """) def testInfoIdentities(self): """Testing adding identities to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><identity category="conference" type="text" name="Chatroom" /></query></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#info" node="foo"> + <identity category="conference" type="text" name="Chatroom" /> + </query> + </iq> + """) def testInfoFeatures(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' iq['disco_info'].addFeature('foo') iq['disco_info'].addFeature('bar') - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><feature var="foo" /><feature var="bar" /></query></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#info" node="foo"> + <feature var="foo" /> + <feature var="bar" /> + </query> + </iq> + """) def testItems(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = 'foo' iq['disco_items'].addItem('user@localhost') iq['disco_items'].addItem('user@localhost', 'foo') iq['disco_items'].addItem('user@localhost', 'bar', 'Testing') - xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo"><item jid="user@localhost" /><item node="foo" jid="user@localhost" /><item node="bar" jid="user@localhost" name="Testing" /></query></iq>""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + <iq id="0"> + <query xmlns="http://jabber.org/protocol/disco#items" node="foo"> + <item jid="user@localhost" /> + <item node="foo" jid="user@localhost" /> + <item node="bar" jid="user@localhost" name="Testing" /> + </query> + </iq> + """) def testAddRemoveIdentities(self): """Test adding and removing identities to disco#info stanza""" ids = [('automation', 'commands', 'AdHoc'), ('conference', 'text', 'ChatRoom')] - info = self.sd.DiscoInfo() + info = xep_0030.DiscoInfo() info.addIdentity(*ids[0]) self.failUnless(info.getIdentities() == [ids[0]]) @@ -110,7 +132,7 @@ class testdisco(unittest.TestCase): """Test adding and removing features to disco#info stanza""" features = ['foo', 'bar', 'baz'] - info = self.sd.DiscoInfo() + info = xep_0030.DiscoInfo() info.addFeature(features[0]) self.failUnless(info.getFeatures() == [features[0]]) @@ -132,7 +154,7 @@ class testdisco(unittest.TestCase): ('user@localhost', 'foo', None), ('user@localhost', 'bar', 'Test')] - info = self.sd.DiscoItems() + info = xep_0030.DiscoItems() self.failUnless(True, ""+str(items[0])) info.addItem(*(items[0])) @@ -151,5 +173,4 @@ class testdisco(unittest.TestCase): self.failUnless(info.getItems() == []) - -suite = unittest.TestLoader().loadTestsFromTestCase(testdisco) +suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 00000000..981d8870 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,90 @@ +from sleektest import * +import sleekxmpp.plugins.alt_0004 as xep_0004 + + +class TestDataForms(SleekTest): + + def setUp(self): + self.stanzaPlugin(Message, xep_0004.Form) + self.stanzaPlugin(xep_0004.Form, xep_0004.FormField) + self.stanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) + + def testMultipleInstructions(self): + """Testing using multiple instructions elements in a data form.""" + msg = self.Message() + msg['form']['instructions'] = "Instructions\nSecond batch" + + self.checkMessage(msg, """ + <message> + <x xmlns="jabber:x:data"> + <instructions>Instructions</instructions> + <instructions>Second batch</instructions> + </x> + </message> + """, use_values=False) + + def testAddField(self): + """Testing adding fields to a data form.""" + + msg = self.Message() + form = msg['form'] + form.addField('f1', + ftype='text-single', + label='Text', + desc='A text field', + required=True, + value='Some text!') + + self.checkMessage(msg, """ + <message> + <x xmlns="jabber:x:data"> + <field var="f1" type="text-single" label="Text"> + <desc>A text field</desc> + <required /> + <value>Some text!</value> + </field> + </x> + </message> + """, use_values=False) + + form['fields'] = {'f1': {'type': 'text-single', + 'label': 'Username', + 'required': True}, + 'f2': {'type': 'text-private', + 'label': 'Password', + 'required': True}, + 'f3': {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'}, + 'f4': {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]}} + self.checkMessage(msg, """ + <message> + <x xmlns="jabber:x:data"> + <field var="f1" type="text-single" label="Username"> + <required /> + </field> + <field var="f2" type="text-private" label="Password"> + <required /> + </field> + <field var="f3" type="text-multi" label="Message"> + <value>Enter message.</value> + <value>A long one even.</value> + </field> + <field var="f4" type="list-single" label="Message Type"> + <option label="Cool!"> + <value>cool</value> + </option> + <option label="Urgh!"> + <value>urgh</value> + </option> + </field> + </x> + </message> + """, use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) diff --git a/tests/test_gmail.py b/tests/test_gmail.py new file mode 100644 index 00000000..199b76ae --- /dev/null +++ b/tests/test_gmail.py @@ -0,0 +1,88 @@ +from sleektest import * +import sleekxmpp.plugins.gmail_notify as gmail + + +class TestGmail(SleekTest): + + def setUp(self): + self.stanzaPlugin(Iq, gmail.GmailQuery) + self.stanzaPlugin(Iq, gmail.MailBox) + self.stanzaPlugin(Iq, gmail.NewMail) + + def testCreateQuery(self): + """Testing querying Gmail for emails.""" + + iq = self.Iq() + iq['type'] = 'get' + iq['gmail']['search'] = 'is:starred' + iq['gmail']['newer-than-time'] = '1140638252542' + iq['gmail']['newer-than-tid'] = '11134623426430234' + + self.checkIq(iq, """ + <iq type="get"> + <query xmlns="google:mail:notify" + newer-than-time="1140638252542" + newer-than-tid="11134623426430234" + q="is:starred" /> + </iq> + """) + + def testMailBox(self): + """Testing reading from Gmail mailbox result""" + + # Use the example from Google's documentation at + # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications + xml = ET.fromstring(""" + <iq type="result"> + <mailbox xmlns="google:mail:notify" + result-time='1118012394209' + url='http://mail.google.com/mail' + total-matched='95' + total-estimate='0'> + <mail-thread-info tid='1172320964060972012' + participation='1' + messages='28' + date='1118012394209' + url='http://mail.google.com/mail?view=cv'> + <senders> + <sender name='Me' address='romeo@gmail.com' originator='1' /> + <sender name='Benvolio' address='benvolio@gmail.com' /> + <sender name='Mercutio' address='mercutio@gmail.com' unread='1'/> + </senders> + <labels>act1scene3</labels> + <subject>Put thy rapier up.</subject> + <snippet>Ay, ay, a scratch, a scratch; marry, 'tis enough.</snippet> + </mail-thread-info> + </mailbox> + </iq> + """) + + iq = self.Iq(xml=xml) + mailbox = iq['mailbox'] + self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match") + self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match") + self.failUnless(mailbox['matched'] == '95', "total-matched incorrect") + self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect") + self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads") + + thread = mailbox['threads'][0] + self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match") + self.failUnless(thread['participation'] == '1', "thread participation incorrect") + self.failUnless(thread['messages'] == '28', "thread message count incorrect") + self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match") + self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match") + self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect") + self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match") + self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match") + self.failUnless(len(thread['senders']) == 3, "could not extract senders") + + sender1 = thread['senders'][0] + self.failUnless(sender1['name'] == 'Me', "sender name doesn't match") + self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match") + self.failUnless(sender1['originator'] == True, "sender originator incorrect") + self.failUnless(sender1['unread'] == False, "sender unread incorrectly True") + + sender2 = thread['senders'][2] + self.failUnless(sender2['unread'] == True, "sender unread incorrectly False") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail) diff --git a/tests/test_stream.py b/tests/test_stream.py new file mode 100644 index 00000000..eb4aaa59 --- /dev/null +++ b/tests/test_stream.py @@ -0,0 +1,34 @@ +from sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 + + +class TestStreamTester(SleekTest): + """ + Test that we can simulate and test a stanza stream. + """ + + def setUp(self): + self.streamStart() + + def tearDown(self): + self.streamClose() + + def testEcho(self): + def echo(msg): + msg.reply('Thanks for sending: %(body)s' % msg).send() + + self.xmpp.add_event_handler('message', echo) + + self.streamRecv(""" + <message to="tester@localhost" from="user@localhost"> + <body>Hi!</body> + </message> + """) + + self.streamSendMessage(""" + <message to="user@localhost" from="tester@localhost"> + <body>Thanks for sending: Hi!</body> + </message> + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester) |