summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sleekxmpp/basexmpp.py8
-rw-r--r--sleekxmpp/plugins/__init__.py2
-rw-r--r--sleekxmpp/plugins/alt_0004.py330
-rw-r--r--sleekxmpp/plugins/gmail_notify.py193
-rw-r--r--sleekxmpp/plugins/xep_0030.py17
-rw-r--r--sleekxmpp/plugins/xep_0033.py161
-rw-r--r--sleekxmpp/plugins/xep_0085.py101
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py7
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py14
-rw-r--r--tests/sleektest.py299
-rw-r--r--tests/test_addresses.py110
-rw-r--r--tests/test_chatstates.py44
-rw-r--r--tests/test_disco.py113
-rw-r--r--tests/test_forms.py90
-rw-r--r--tests/test_gmail.py88
-rw-r--r--tests/test_stream.py34
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)