diff options
-rwxr-xr-x | examples/proxy_echo_client.py | 167 | ||||
-rw-r--r-- | sleekxmpp/__init__.py | 4 | ||||
-rw-r--r-- | sleekxmpp/clientxmpp.py | 19 | ||||
-rw-r--r-- | sleekxmpp/plugins/__init__.py | 5 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0066/oob.py | 69 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0082.py | 204 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0092/version.py | 2 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0202.py | 117 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0202/__init__.py | 11 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0202/stanza.py | 126 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0202/time.py | 92 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0203/__init__.py | 12 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0203/delay.py | 36 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0203/stanza.py | 41 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0224/__init__.py | 11 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0224/attention.py | 72 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0224/stanza.py | 40 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 94 | ||||
-rw-r--r-- | tests/test_stream_presence.py | 51 | ||||
-rw-r--r-- | tests/test_stream_xep_0066.py | 28 |
20 files changed, 1037 insertions, 164 deletions
diff --git a/examples/proxy_echo_client.py b/examples/proxy_echo_client.py new file mode 100755 index 00000000..4db9a552 --- /dev/null +++ b/examples/proxy_echo_client.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import logging +import time +import getpass +from optparse import OptionParser + +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') + + +class EchoBot(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that will echo messages it + receives, along with a short thank you message. + """ + + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can intialize + # our roster. + self.add_event_handler("session_start", self.start) + + # The message event is triggered whenever a message + # stanza is received. Be aware that that includes + # MUC messages and error messages. + self.add_event_handler("message", self.message) + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an intial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.send_presence() + self.get_roster() + + def message(self, msg): + """ + Process incoming message stanzas. Be aware that this also + includes MUC messages and error messages. It is usually + a good idea to check the messages's type before processing + or sending replies. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + msg.reply("Thanks for sending\n%(body)s" % msg).send() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + + # Output verbosity options. + optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + optp.add_option("--phost", dest="proxy_host", + help="Proxy hostname") + optp.add_option("--pport", dest="proxy_port", + help="Proxy port") + optp.add_option("--puser", dest="proxy_user", + help="Proxy username") + optp.add_option("--ppass", dest="proxy_pass", + help="Proxy password") + + + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if opts.jid is None: + opts.jid = raw_input("Username: ") + if opts.password is None: + opts.password = getpass.getpass("Password: ") + if opts.proxy_host is None: + opts.proxy_host = raw_input("Proxy host: ") + if opts.proxy_port is None: + opts.proxy_port = raw_input("Proxy port: ") + if opts.proxy_user is None: + opts.proxy_user = raw_input("Proxy username: ") + if opts.proxy_pass is None and opts.proxy_user: + opts.proxy_pass = getpass.getpass("Proxy password: ") + + # Setup the EchoBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = EchoBot(opts.jid, opts.password) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0004') # Data Forms + xmpp.register_plugin('xep_0060') # PubSub + xmpp.register_plugin('xep_0199') # XMPP Ping + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + xmpp.use_proxy = True + xmpp.proxy_config = { + 'host': opts.proxy_host, + 'port': int(opts.proxy_port), + 'username': opts.proxy_user, + 'password': opts.proxy_pass} + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the pydns library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(threaded=False) + print("Done") + else: + print("Unable to connect.") diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 5ad11742..a53cfb0e 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -15,5 +15,5 @@ from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET -__version__ = '1.0beta5' -__version_info__ = (1, 0, 0, 'beta5', 0) +__version__ = '1.0beta6' +__version_info__ = (1, 0, 0, 'beta6', 0) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 77710541..91106076 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -168,18 +168,23 @@ class ClientXMPP(BaseXMPP): addresses = {} intmax = 0 + topprio = 65535 for answer in answers: - intmax += answer.priority - addresses[intmax] = (answer.target.to_text()[:-1], + topprio = min(topprio, answer.priority) + for answer in answers: + if answer.priority == topprio: + intmax += answer.weight + addresses[intmax] = (answer.target.to_text()[:-1], answer.port) + #python3 returns a generator for dictionary keys - priorities = [x for x in addresses.keys()] - priorities.sort() + items = [x for x in addresses.keys()] + items.sort() picked = random.randint(0, intmax) - for priority in priorities: - if picked <= priority: - address = addresses[priority] + for item in items: + if picked <= item: + address = addresses[item] break if not address: diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index d27937ae..b48a4c03 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -6,5 +6,6 @@ See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', - 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] + 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', + 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', + 'xep_0202', 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py index b4322351..98cb81cd 100644 --- a/sleekxmpp/plugins/xep_0066/oob.py +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -6,8 +6,10 @@ See the file LICENSE for copying permission. """ +import logging from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -15,6 +17,9 @@ from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0066 import stanza +log = logging.getLogger(__name__) + + class xep_0066(base_plugin): """ @@ -43,6 +48,9 @@ class xep_0066(base_plugin): self.description = 'Out-of-Band Transfer' self.stanza = stanza + self.url_handlers = {'global': self._default_handler, + 'jid': {}} + register_stanza_plugin(Iq, stanza.OOBTransfer) register_stanza_plugin(Message, stanza.OOB) register_stanza_plugin(Presence, stanza.OOB) @@ -58,6 +66,28 @@ class xep_0066(base_plugin): self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) + def register_url_handler(self, jid=None, handler=None): + """ + Register a handler to process download requests, either for all + JIDs or a single JID. + + Arguments: + jid -- If None, then set the handler as a global default. + handler -- If None, then remove the existing handler for the + given JID, or reset the global handler if the JID + is None. + """ + if jid is None: + if handler is not None: + self.url_handlers['global'] = handler + else: + self.url_handlers['global'] = self._default_handler + else: + if handler is not None: + self.url_handlers['jid'][jid] = handler + else: + del self.url_handlers['jid'][jid] + def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): """ Initiate a basic file transfer by sending the URL of @@ -84,6 +114,41 @@ class xep_0066(base_plugin): iq['oob_transfer']['desc'] = desc return iq.send(**iqargs) + def _run_url_handler(self, iq): + """ + Execute the appropriate handler for a transfer request. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + if iq['to'] in self.url_handlers['jid']: + return self.url_handlers['jid'][jid](iq) + else: + if self.url_handlers['global']: + self.url_handlers['global'](iq) + else: + raise XMPPError('service-unavailable') + + def _default_handler(self, iq): + """ + As a safe default, don't actually download files. + + Register a new handler using self.register_url_handler to + screen requests and download files. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + raise XMPPError('service-unavailable') + def _handle_transfer(self, iq): - """Handle receiving an out-of-band transfer request.""" - self.xmpp.event('oob_transfer', iq) + """ + Handle receiving an out-of-band transfer request. + + Arguments: + iq -- An Iq stanza containing an OOB transfer request. + """ + log.debug('Received out-of-band data request for %s from %s:' % ( + iq['oob_transfer']['url'], iq['from'])) + self._run_url_handler(iq) + iq.reply().send() diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py new file mode 100644 index 00000000..785ba36b --- /dev/null +++ b/sleekxmpp/plugins/xep_0082.py @@ -0,0 +1,204 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt +from dateutil import parser +from dateutil.tz import tzoffset, tzutc +from sleekxmpp.plugins.base import base_plugin + + +# ===================================================================== +# To make it easier for stanzas without direct access to plugin objects +# to use the XEP-0082 utility methods, we will define them as top-level +# functions and then just reference them in the plugin itself. + +def parse(time_str): + """ + Convert a string timestamp into a datetime object. + + Arguments: + time_str -- A formatted timestamp string. + """ + return parser.parse(time_str) + +def format_date(time_obj): + """ + Return a formatted string version of a date object. + + Format: + YYYY-MM-DD + + Arguments: + time_obj -- A date or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.date() + return time_obj.isoformat() + +def format_time(time_obj): + """ + Return a formatted string version of a time object. + + format: + hh:mm:ss[.sss][TZD + + arguments: + time_obj -- A time or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.timetz() + timestamp = time_obj.isoformat() + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def format_datetime(time_obj): + """ + Return a formatted string version of a datetime object. + + Format: + YYYY-MM-DDThh:mm:ss[.sss]TZD + + arguments: + time_obj -- A datetime object. + """ + timestamp = time_obj.isoformat('T') + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def date(year=None, month=None, day=None): + """ + Create a date only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + """ + today = dt.datetime.today() + if year is None: + year = today.year + if month is None: + month = today.month + if day is None: + day = today.day + return format_date(dt.date(year, month, day)) + +def time(hour=None, min=None, sec=None, micro=None, offset=None): + """ + Create a time only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + time = dt.time(hour, min, sec, micro, offset) + return format_time(time) + +def datetime(year=None, month=None, day=None, hour=None, + min=None, sec=None, micro=None, offset=None, + separators=True): + """ + Create a datetime timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + + date = dt.datetime(year, month, day, hour, + min, sec, micro, offset) + return format_datetime(date) + +class xep_0082(base_plugin): + + """ + XEP-0082: XMPP Date and Time Profiles + + XMPP uses a subset of the formats allowed by ISO 8601 as a matter of + pragmatism based on the relatively few formats historically used by + the XMPP. + + Also see <http://www.xmpp.org/extensions/xep-0082.html>. + + Methods: + date -- Create a time stamp using the Date profile. + datetime -- Create a time stamp using the DateTime profile. + time -- Create a time stamp using the Time profile. + format_date -- Format an existing date object. + format_datetime -- Format an existing datetime object. + format_time -- Format an existing time object. + parse -- Convert a time string into a Python datetime object. + """ + + def plugin_init(self): + """Start the XEP-0082 plugin.""" + self.xep = '0082' + self.description = 'XMPP Date and Time Profiles' + + self.date = date + self.datetime = datetime + self.time = time + self.format_date = format_date + self.format_datetime = format_datetime + self.format_time = format_time + self.parse = parse diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index 1ca6c15e..ac0924b8 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -35,7 +35,7 @@ class xep_0092(base_plugin): self.stanza = sleekxmpp.plugins.xep_0092.stanza self.name = self.config.get('name', 'SleekXMPP') - self.version = self.config.get('version', '0.1-dev') + self.version = self.config.get('version', sleekxmpp.__version__) self.os = self.config.get('os', '') self.getVersion = self.get_version diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py deleted file mode 100644 index 3b31c97a..00000000 --- a/sleekxmpp/plugins/xep_0202.py +++ /dev/null @@ -1,117 +0,0 @@ -"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-
-from datetime import datetime, tzinfo
-import logging
-import time
-
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
-
-
-log = logging.getLogger(__name__)
-
-
-class EntityTime(ElementBase):
- name = 'time'
- namespace = 'urn:xmpp:time'
- plugin_attrib = 'entity_time'
- interfaces = set(('tzo', 'utc'))
- sub_interfaces = set(('tzo', 'utc'))
-
- #def get_tzo(self):
- # TODO: Right now it returns a string but maybe it should
- # return a datetime.tzinfo object or maybe a datetime.timedelta?
- #pass
-
- def set_tzo(self, tzo):
- if isinstance(tzo, tzinfo):
- td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
- seconds = td.seconds + td.days * 24 * 3600
- sign = ('+' if seconds >= 0 else '-')
- minutes = abs(seconds // 60)
- tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
- elif not isinstance(tzo, str):
- raise TypeError('The time should be a string or a datetime.tzinfo object.')
- self._set_sub_text('tzo', tzo)
-
- def get_utc(self):
- # Returns a datetime object instead the string. Is this a good idea?
- value = self._get_sub_text('utc')
- if '.' in value:
- return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
- else:
- return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
-
- def set_utc(self, tim=None):
- if isinstance(tim, datetime):
- if tim.utcoffset():
- tim = tim - tim.utcoffset()
- tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
- elif isinstance(tim, time.struct_time):
- tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
- elif not isinstance(tim, str):
- raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
-
- self._set_sub_text('utc', tim)
-
-
-class xep_0202(base.base_plugin):
- """
- XEP-0202 Entity Time
- """
- def plugin_init(self):
- self.description = "Entity Time"
- self.xep = "0202"
-
- self.xmpp.registerHandler(
- Callback('Time Request',
- MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
- EntityTime.namespace)),
- self.handle_entity_time_query))
- register_stanza_plugin(Iq, EntityTime)
-
- self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
-
-
- def post_init(self):
- base.base_plugin.post_init(self)
-
- self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
-
- def handle_entity_time_query(self, iq):
- if iq['type'] == 'get':
- log.debug("Entity time requested by %s" % iq['from'])
- self.xmpp.event('entity_time_request', iq)
- elif iq['type'] == 'result':
- log.debug("Entity time result from %s" % iq['from'])
- self.xmpp.event('entity_time', iq)
-
- def handle_entity_time(self, iq):
- iq = iq.reply()
- iq.enable('entity_time')
- tzo = time.strftime('%z') # %z is not on all ANSI C libraries
- tzo = tzo[:3] + ':' + tzo[3:]
- iq['entity_time']['tzo'] = tzo
- iq['entity_time']['utc'] = datetime.utcnow()
- iq.send()
-
- def get_entity_time(self, jid):
- iq = self.xmpp.makeIqGet()
- iq.enable('entity_time')
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq.get('id')
- result = iq.send()
- if result and result is not None and result.get('type', 'error') != 'error':
- return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
- else:
- return False
diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py new file mode 100644 index 00000000..82338d3a --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0202 import stanza +from sleekxmpp.plugins.xep_0202.stanza import EntityTime +from sleekxmpp.plugins.xep_0202.time import xep_0202 diff --git a/sleekxmpp/plugins/xep_0202/stanza.py b/sleekxmpp/plugins/xep_0202/stanza.py new file mode 100644 index 00000000..bb27692a --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/stanza.py @@ -0,0 +1,126 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt +from dateutil.tz import tzoffset, tzutc + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class EntityTime(ElementBase): + + """ + The <time> element represents the local time for an XMPP agent. + The time is expressed in UTC to make synchronization easier + between entities, but the offset for the local timezone is also + included. + + Example <time> stanzas: + <iq type="result"> + <time xmlns="urn:xmpp:time"> + <utc>2011-07-03T11:37:12.234569</utc> + <tzo>-07:00</tzo> + </time> + </iq> + + Stanza Interface: + time -- The local time for the entity (updates utc and tzo). + utc -- The UTC equivalent to local time. + tzo -- The local timezone offset from UTC. + + Methods: + get_time -- Return local time datetime object. + set_time -- Set UTC and TZO fields. + del_time -- Remove both UTC and TZO fields. + get_utc -- Return datetime object of UTC time. + set_utc -- Set the UTC time. + get_tzo -- Return tzinfo object. + set_tzo -- Set the local timezone offset. + """ + + name = 'time' + namespace = 'urn:xmpp:time' + plugin_attrib = 'entity_time' + interfaces = set(('tzo', 'utc', 'time')) + sub_interfaces = interfaces + + def set_time(self, value): + """ + Set both the UTC and TZO fields given a time object. + + Arguments: + value -- A datetime object or properly formatted + string equivalent. + """ + date = value + if not isinstance(value, dt.datetime): + date = xep_0082.parse(value) + self['utc'] = date + self['tzo'] = date.tzinfo + + def get_time(self): + """ + Return the entity's local time based on the UTC and TZO data. + """ + date = self['utc'] + tz = self['tzo'] + return date.astimezone(tz) + + def del_time(self): + """Remove both the UTC and TZO fields.""" + del self['utc'] + del self['tzo'] + + def get_tzo(self): + """ + Return the timezone offset from UTC as a tzinfo object. + """ + tzo = self._get_sub_text('tzo') + if tzo == '': + tzo = 'Z' + time = xep_0082.parse('00:00:00%s' % tzo) + return time.tzinfo + + def set_tzo(self, value): + """ + Set the timezone offset from UTC. + + Arguments: + value -- Either a tzinfo object or the number of + seconds (positive or negative) to offset. + """ + time = xep_0082.time(offset=value) + if xep_0082.parse(time).tzinfo == tzutc(): + self._set_sub_text('tzo', 'Z') + else: + self._set_sub_text('tzo', time[-6:]) + + def get_utc(self): + """ + Return the time in UTC as a datetime object. + """ + value = self._get_sub_text('utc') + if value == '': + return xep_0082.parse(xep_0082.datetime()) + return xep_0082.parse('%sZ' % value) + + def set_utc(self, value): + """ + Set the time in UTC. + + Arguments: + value -- A datetime object or properly formatted + string equivalent. + """ + date = value + if not isinstance(value, dt.datetime): + date = xep_0082.parse(value) + date = date.astimezone(tzutc()) + value = xep_0082.format_datetime(date)[:-1] + self._set_sub_text('utc', value) diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py new file mode 100644 index 00000000..bcad8bc8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -0,0 +1,92 @@ +"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import xep_0082
+from sleekxmpp.plugins.xep_0202 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0202(base_plugin):
+
+ """
+ XEP-0202: Entity Time
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+ self.xep = '0202'
+ self.description = 'Entity Time'
+ self.stanza = stanza
+
+ self.tz_offset = self.config.get('tz_offset', 0)
+
+ # As a default, respond to time requests with the
+ # local time returned by XEP-0082. However, a
+ # custom function can be supplied which accepts
+ # the JID of the entity to query for the time.
+ self.local_time = self.config.get('local_time', None)
+ if not self.local_time:
+ self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
+
+ self.xmpp.registerHandler(
+ Callback('Entity Time',
+ StanzaPath('iq/entity_time'),
+ self._handle_time_request))
+ register_stanza_plugin(Iq, stanza.EntityTime)
+
+ def post_init(self):
+ """Handle cross-plugin interactions."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
+
+
+ def _handle_time_request(self, iq):
+ """
+ Respond to a request for the local time.
+
+ The time is taken from self.local_time(), which may be replaced
+ during plugin configuration with a function that maps JIDs to
+ times.
+
+ Arguments:
+ iq -- The Iq time request stanza.
+ """
+ iq.reply()
+ iq['entity_time']['time'] = self.local_time(iq['to'])
+ iq.send()
+
+ def get_entity_time(self, to, ifrom=None, **iqargs):
+ """
+ Request the time from another entity.
+
+ Arguments:
+ to -- JID of the entity to query.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = 'to'
+ if ifrom:
+ iq['from'] = 'ifrom'
+ iq.enable('entity_time')
+ return iq.send(**iqargs)
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py new file mode 100644 index 00000000..445ccf37 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0203 import stanza +from sleekxmpp.plugins.xep_0203.stanza import Delay +from sleekxmpp.plugins.xep_0203.delay import xep_0203 + diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py new file mode 100644 index 00000000..8ff14d18 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/delay.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0203 import stanza + + +class xep_0203(base_plugin): + + """ + XEP-0203: Delayed Delivery + + XMPP stanzas are sometimes withheld for delivery due to the recipient + being offline, or are resent in order to establish recent history as + is the case with MUCS. In any case, it is important to know when the + stanza was originally sent, not just when it was last received. + + Also see <http://www.xmpp.org/extensions/xep-0203.html>. + """ + + def plugin_init(self): + """Start the XEP-0203 plugin.""" + self.xep = '0203' + self.description = 'Delayed Delivery' + self.stanza = stanza + + register_stanza_plugin(Message, stanza.Delay) + register_stanza_plugin(Presence, stanza.Delay) diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py new file mode 100644 index 00000000..baae4cd3 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -0,0 +1,41 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Delay(ElementBase): + + """ + """ + + name = 'delay' + namespace = 'urn:xmpp:delay' + plugin_attrib = 'delay' + interfaces = set(('from', 'stamp', 'text')) + + def get_stamp(self): + timestamp = self._get_attr('stamp') + return xep_0082.parse(timestamp) + + def set_stamp(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('stamp', value) + + def get_text(self): + return self.xml.text + + def set_text(self, value): + self.xml.text = value + + def del_text(self): + self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py new file mode 100644 index 00000000..62f5bf82 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0224 import stanza +from sleekxmpp.plugins.xep_0224.stanza import Attention +from sleekxmpp.plugins.xep_0224.attention import xep_0224 diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py new file mode 100644 index 00000000..41d7a0f1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/attention.py @@ -0,0 +1,72 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0224 import stanza + + +log = logging.getLogger(__name__) + + +class xep_0224(base_plugin): + + """ + XEP-0224: Attention + """ + + def plugin_init(self): + """Start the XEP-0224 plugin.""" + self.xep = '0224' + self.description = 'Attention' + self.stanza = stanza + + register_stanza_plugin(Message, stanza.Attention) + + self.xmpp.register_handler( + Callback('Attention', + StanzaPath('message/attention'), + self._handle_attention)) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace) + + def request_attention(self, to, mfrom=None, mbody=''): + """ + Send an attention message with an optional body. + + Arguments: + to -- The attention request recipient's JID. + mfrom -- Optionally specify the sender of the attention request. + mbody -- An optional message body to include in the request. + """ + m = self.xmpp.Message() + m['to'] = to + m['type'] = 'headline' + m['attention'] = True + if mfrom: + m['from'] = mfrom + m['body'] = mbody + m.send() + + def _handle_attention(self, msg): + """ + Raise an event after receiving a message with an attention request. + + Arguments: + msg -- A message stanza with an attention element. + """ + log.debug("Received attention request from: %s" % msg['from']) + self.xmpp.event('attention', msg) diff --git a/sleekxmpp/plugins/xep_0224/stanza.py b/sleekxmpp/plugins/xep_0224/stanza.py new file mode 100644 index 00000000..f15172d9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/stanza.py @@ -0,0 +1,40 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class Attention(ElementBase): + + """ + """ + + name = 'attention' + namespace = 'urn:xmpp:attention:0' + plugin_attrib = 'attention' + interfaces = set(('attention',)) + is_extension = True + + def setup(self, xml): + return True + + def set_attention(self, value): + if value: + xml = ET.Element(self.tag_name()) + self.parent().xml.append(xml) + else: + self.del_attention() + + def get_attention(self): + xml = self.parent().xml.find(self.tag_name()) + return xml is not None + + def del_attention(self): + xml = self.parent().xml.find(self.tag_name()) + if xml is not None: + self.parent().xml.remove(xml) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index c7d0d3a8..15bbe655 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -8,6 +8,7 @@ from __future__ import with_statement, unicode_literals +import base64 import copy import logging import signal @@ -23,6 +24,7 @@ try: except ImportError: import Queue as queue +import sleekxmpp from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET @@ -107,7 +109,13 @@ class XMLStream(object): stream_header -- The closing tag of the stream's root element. use_ssl -- Flag indicating if SSL should be used. use_tls -- Flag indicating if TLS should be used. + use_proxy -- Flag indicating that an HTTP Proxy should be used. stop -- threading Event used to stop all threads. + proxy_config -- An optional dictionary with the following entries: + host -- The host offering proxy services. + port -- The port for the proxy service. + username -- Optional username for the proxy. + password -- Optional password for the proxy. auto_reconnect -- Flag to determine whether we auto reconnect. reconnect_max_delay -- Maximum time to delay between connection @@ -180,6 +188,9 @@ class XMLStream(object): self.use_ssl = False self.use_tls = False + self.use_proxy = False + + self.proxy_config = {} self.default_ns = '' self.stream_header = "<stream>" @@ -322,6 +333,12 @@ class XMLStream(object): log.debug('Waiting %s seconds before connecting.' % delay) time.sleep(delay) + if self.use_proxy: + connected = self._connect_proxy() + if not connected: + self.reconnect_delay = delay + return False + if self.use_ssl and self.ssl_support: log.debug("Socket Wrapped for SSL") if self.ca_certs is None: @@ -341,8 +358,10 @@ class XMLStream(object): self.socket = ssl_socket try: - log.debug("Connecting to %s:%s" % self.address) - self.socket.connect(self.address) + if not self.use_proxy: + log.debug("Connecting to %s:%s" % self.address) + self.socket.connect(self.address) + self.set_socket(self.socket, ignore=True) #this event is where you should set your application state self.event("connected", direct=True) @@ -356,22 +375,86 @@ class XMLStream(object): self.reconnect_delay = delay return False - def disconnect(self, reconnect=False): + def _connect_proxy(self): + """Attempt to connect using an HTTP Proxy.""" + + # Extract the proxy address, and optional credentials + address = (self.proxy_config['host'], int(self.proxy_config['port'])) + cred = None + if self.proxy_config['username']: + username = self.proxy_config['username'] + password = self.proxy_config['password'] + + cred = '%s:%s' % (username, password) + if sys.version_info < (3, 0): + cred = bytes(cred) + else: + cred = bytes(cred, 'utf-8') + cred = base64.b64encode(cred).decode('utf-8') + + # Build the HTTP headers for connecting to the XMPP server + headers = ['CONNECT %s:%s HTTP/1.0' % self.address, + 'Host: %s:%s' % self.address, + 'Proxy-Connection: Keep-Alive', + 'Pragma: no-cache', + 'User-Agent: SleekXMPP/%s' % sleekxmpp.__version__] + if cred: + headers.append('Proxy-Authorization: Basic %s' % cred) + headers = '\r\n'.join(headers) + '\r\n\r\n' + + try: + log.debug("Connecting to proxy: %s:%s" % address) + self.socket.connect(address) + self.send_raw(headers, now=True) + resp = '' + while '\r\n\r\n' not in resp: + resp += self.socket.recv(1024).decode('utf-8') + log.debug('RECV: %s' % resp) + + lines = resp.split('\r\n') + if '200' not in lines[0]: + self.event('proxy_error', resp) + log.error('Proxy Error: %s' % lines[0]) + return False + + # Proxy connection established, continue connecting + # with the XMPP server. + return True + except Socket.error as serr: + error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" + self.event('socket_error', serr) + log.error(error_msg % (self.address[0], self.address[1], + serr.errno, serr.strerror)) + return False + + def disconnect(self, reconnect=False, wait=False): """ Terminate processing and close the XML streams. Optionally, the connection may be reconnected and resume processing afterwards. + If the disconnect should take place after all items + in the send queue have been sent, use wait=True. However, + take note: If you are constantly adding items to the queue + such that it is never empty, then the disconnect will + not occur and the call will continue to block. + Arguments: reconnect -- Flag indicating if the connection and processing should be restarted. Defaults to False. + wait -- Flag indicating if the send queue should + be emptied before disconnecting. """ self.state.transition('connected', 'disconnected', wait=0.0, - func=self._disconnect, args=(reconnect,)) + func=self._disconnect, args=(reconnect, wait)) + + def _disconnect(self, reconnect=False, wait=False): + # Wait for the send queue to empty. + if wait: + self.send_queue.join() - def _disconnect(self, reconnect=False): # Send the end of stream marker. self.send_raw(self.stream_footer, now=True) self.session_started_event.clear() @@ -1036,6 +1119,7 @@ class XMLStream(object): log.debug("SEND: %s" % data) try: self.socket.send(data.encode('utf-8')) + self.send_queue.task_done() except Socket.error as serr: self.event('socket_error', serr) log.warning("Failed to send %s" % data) diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py index 1d5caa98..0b086266 100644 --- a/tests/test_stream_presence.py +++ b/tests/test_stream_presence.py @@ -184,5 +184,56 @@ class TestStreamPresence(SleekTest): self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) + def test_presence_events(self): + """Test that presence events are raised.""" + + events = [] + + self.stream_start() + + ptypes = ['available', 'away', 'dnd', 'xa', 'chat', + 'unavailable', 'subscribe', 'subscribed', + 'unsubscribe', 'unsubscribed'] + + for ptype in ptypes: + handler = lambda p: events.append(p['type']) + self.xmpp.add_event_handler('presence_%s' % ptype, handler) + + self.recv(""" + <presence /> + """) + self.recv(""" + <presence><show>away</show></presence> + """) + self.recv(""" + <presence><show>dnd</show></presence> + """) + self.recv(""" + <presence><show>xa</show></presence> + """) + self.recv(""" + <presence><show>chat</show></presence> + """) + self.recv(""" + <presence type="unavailable" /> + """) + self.recv(""" + <presence type="subscribe" /> + """) + self.recv(""" + <presence type="subscribed" /> + """) + self.recv(""" + <presence type="unsubscribe" /> + """) + self.recv(""" + <presence type="unsubscribed" /> + """) + + time.sleep(.5) + + self.assertEqual(events, ptypes, + "Not all events raised: %s" % events) + suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence) diff --git a/tests/test_stream_xep_0066.py b/tests/test_stream_xep_0066.py index 3dbaf840..e3f2ddfa 100644 --- a/tests/test_stream_xep_0066.py +++ b/tests/test_stream_xep_0066.py @@ -40,33 +40,5 @@ class TestOOB(SleekTest): t.join() - def testReceiveOOB(self): - """Test receiving an OOB request.""" - self.stream_start(plugins=['xep_0066', 'xep_0030']) - - events = [] - - def receive_oob(iq): - events.append(iq['oob_transfer']['url']) - - self.xmpp.add_event_handler('oob_transfer', receive_oob) - - self.recv(""" - <iq to="tester@localhost" - from="user@example.com" - type="set" id="1"> - <query xmlns="jabber:iq:oob"> - <url>http://github.com/fritzy/SleekXMPP/blob/master/README</url> - <desc>SleekXMPP README</desc> - </query> - </iq> - """) - - time.sleep(0.1) - - self.assertEqual(events, - ['http://github.com/fritzy/SleekXMPP/blob/master/README'], - 'URL was not received: %s' % events) - suite = unittest.TestLoader().loadTestsFromTestCase(TestOOB) |