From 0fee27928ddaa893dd0ce1a2a03696521fde5a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 30 Jul 2017 23:14:50 +0200 Subject: [PATCH] Add a new XEP-0082 parsing method This handles every possible XEP-0082 timestamp It has some additional options: - Check if a timestring is a valid UTC timestamp, as required by some XEPs (for example: XEP-0203) - Return timestamp as datetime in UTC - Return timestamp as datetime in localtime - Return timestamp as epoch --- gajim/common/helpers.py | 124 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 00a4e9dd5..9bc28900d 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -43,7 +43,7 @@ import shlex from common import caps_cache import socket import time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone, tzinfo from encodings.punycode import punycode_encode from string import Template @@ -81,6 +81,78 @@ log = logging.getLogger('gajim.c.helpers') special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats')) +# Patterns for DateTime parsing XEP-0082 +PATTERN_DATETIME = re.compile( + r'([0-9]{4}-[0-9]{2}-[0-9]{2})' + r'T' + r'([0-9]{2}:[0-9]{2}:[0-9]{2})' + r'(\.[0-9]{0,6})?' + r'(?:[0-9]+)?' + r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$' + ) + +PATTERN_DELAY = re.compile( + r'([0-9]{4}-[0-9]{2}-[0-9]{2})' + r'T' + r'([0-9]{2}:[0-9]{2}:[0-9]{2})' + r'(\.[0-9]{0,6})?' + r'(?:[0-9]+)?' + r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$' + ) + +ZERO = timedelta(0) +HOUR = timedelta(hours=1) +SECOND = timedelta(seconds=1) + +STDOFFSET = timedelta(seconds=-time.timezone) +if time.daylight: + DSTOFFSET = timedelta(seconds=-time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +class LocalTimezone(tzinfo): + ''' + A class capturing the platform's idea of local time. + May result in wrong values on historical times in + timezones where UTC offset and/or the DST rules had + changed in the past. + ''' + def fromutc(self, dt): + assert dt.tzinfo is self + stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND + args = time.localtime(stamp)[:6] + dst_diff = DSTDIFF // SECOND + # Detect fold + fold = (args == time.localtime(stamp - dst_diff)) + return datetime(*args, microsecond=dt.microsecond, + tzinfo=self, fold=fold) + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return 'local' + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, 0) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + class InvalidFormat(Exception): pass @@ -629,6 +701,56 @@ def parse_delay(timestamp): raise return datetime_.timestamp() +def parse_datetime(timestring, check_utc=False, convert='utc', epoch=False): + ''' + Parse a XEP-0082 DateTime Profile String + https://xmpp.org/extensions/xep-0082.html + + :param timestring: a XEP-0082 DateTime profile formated string + + :param check_utc: if True, returns None if timestring is not + a timestring expressing UTC + + :param convert: convert the given timestring to utc or local time + + :param epoch: if True, returns the time in epoch + + Examples: + '2017-11-05T01:41:20Z' + '2017-11-05T01:41:20.123Z' + '2017-11-05T01:41:20.123+05:00' + + return a datetime or epoch + ''' + if convert not in (None, 'utc', 'local'): + raise TypeError('"%s" is not a valid value for convert') + if check_utc: + match = PATTERN_DELAY.match(timestring) + else: + match = PATTERN_DATETIME.match(timestring) + + if match: + timestring = ''.join(match.groups('')) + strformat = '%Y-%m-%d%H:%M:%S%z' + if match.group(3): + # Fractional second addendum to Time + strformat = '%Y-%m-%d%H:%M:%S.%f%z' + if match.group(4): + # UTC string denoted by addition of the character 'Z' + timestring = timestring[:-1] + '+0000' + try: + date_time = datetime.strptime(timestring, strformat) + except ValueError: + pass + else: + if not check_utc and convert == 'utc': + date_time = date_time.astimezone(timezone.utc) + if convert == 'local': + date_time = date_time.astimezone(LocalTimezone()) + if epoch: + return date_time.timestamp() + return date_time + return None from common import gajim if gajim.HAVE_PYCURL: