From 7ccc67c06d12a1390558636526e6750caf874680 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:14:59 -0700 Subject: [PATCH 01/15] Added XEP-0082 plugin. This should make things much easier for any stanza that uses timestamps. --- sleekxmpp/plugins/xep_0082.py | 202 ++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0082.py diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py new file mode 100644 index 0000000..e36e062 --- /dev/null +++ b/sleekxmpp/plugins/xep_0082.py @@ -0,0 +1,202 @@ +""" + 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 -- A positive or negative number of seconds to + offset from UTC to match a desired timezone. + """ + 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() + else: + 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 -- A positive or negative number of seconds to + offset from UTC to match a desired timezone. + """ + 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() + else: + offset = tzoffset(None, offset) + + date = dt.datetime(year, month, day, hour, + sec, min, 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 . + + 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 From 2e8e542bc9391e49cb901217f77915f42cdd8c17 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 12:43:34 -0700 Subject: [PATCH 02/15] Added XEP-0203 Delayed Delivery plugin. --- sleekxmpp/plugins/__init__.py | 5 ++-- sleekxmpp/plugins/xep_0203/__init__.py | 12 ++++++++ sleekxmpp/plugins/xep_0203/delay.py | 36 ++++++++++++++++++++++ sleekxmpp/plugins/xep_0203/stanza.py | 41 ++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 sleekxmpp/plugins/xep_0203/__init__.py create mode 100644 sleekxmpp/plugins/xep_0203/delay.py create mode 100644 sleekxmpp/plugins/xep_0203/stanza.py diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index d27937a..5630c1d 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', 'gmail_notify'] diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py new file mode 100644 index 0000000..445ccf3 --- /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 0000000..8ff14d1 --- /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 . + """ + + 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 0000000..baae4cd --- /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 = '' From c98f5d44509f5b6fdfdbf7408dae3282caccc9db Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 13:40:57 -0700 Subject: [PATCH 03/15] Fix some bugs in time handling. Namely, minutes and seconds were reversed. --- sleekxmpp/plugins/xep_0082.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index e36e062..785ba36 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -105,8 +105,9 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): 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 -- A positive or negative number of seconds to - offset from UTC to match a desired timezone. + 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: @@ -119,7 +120,7 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): micro = now.microsecond if offset is None: offset = tzutc() - else: + elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) time = dt.time(hour, min, sec, micro, offset) return format_time(time) @@ -140,8 +141,9 @@ def datetime(year=None, month=None, day=None, hour=None, 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 -- A positive or negative number of seconds to - offset from UTC to match a desired timezone. + 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: @@ -160,11 +162,11 @@ def datetime(year=None, month=None, day=None, hour=None, micro = now.microsecond if offset is None: offset = tzutc() - else: + elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) date = dt.datetime(year, month, day, hour, - sec, min, micro, offset) + min, sec, micro, offset) return format_datetime(date) class xep_0082(base_plugin): From ec3a14e6d91e61c76147d8415f7246086fd9d435 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 3 Jul 2011 15:30:06 -0700 Subject: [PATCH 04/15] Updated XEP-0202 plugin to new format and use XEP-0082. --- sleekxmpp/plugins/xep_0202.py | 117 ----------------------- sleekxmpp/plugins/xep_0202/__init__.py | 11 +++ sleekxmpp/plugins/xep_0202/stanza.py | 126 +++++++++++++++++++++++++ sleekxmpp/plugins/xep_0202/time.py | 90 ++++++++++++++++++ 4 files changed, 227 insertions(+), 117 deletions(-) delete mode 100644 sleekxmpp/plugins/xep_0202.py create mode 100644 sleekxmpp/plugins/xep_0202/__init__.py create mode 100644 sleekxmpp/plugins/xep_0202/stanza.py create mode 100644 sleekxmpp/plugins/xep_0202/time.py diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py deleted file mode 100644 index 3b31c97..0000000 --- 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 0000000..82338d3 --- /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 0000000..bb27692 --- /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