def human_time(self, full=False, preposition=True): assert self.reminder_time is not None user_tz = self.get_user().timezone now = util.now_utc() delta = self.reminder_time - now # Default timezone to US/Eastern TODO magic string used in a couple places tz = timezone(user_tz) if user_tz else timezone('US/Eastern') needs_date = full or delta.total_seconds() > 60 * 60 * 16 # today-ish needs_day = full or (needs_date and delta.days > 7) needs_year = full or (needs_day and self.reminder_time.year != now.year) # now consider the repetition needs_dow = needs_date and self.repetition.interval in (None, INTERVAL_WEEK) needs_day = needs_day and self.repetition.interval in (None, INTERVAL_YEAR, INTERVAL_MONTH) needs_month = needs_day and self.repetition.interval != INTERVAL_MONTH needs_year = needs_year and not self.repeats( ) # no repeating intervals state the year needs_time = self.repetition.interval not in ("hour", "minute") needs_date = any((needs_dow, needs_day, needs_month, needs_year)) fmt = "" if needs_date and preposition: fmt += "on " if needs_dow: fmt += "%A " # on Monday if needs_month and needs_day: fmt += "%B %-d " # April 10 if needs_day and not needs_month: fmt += "the {S} " # the 10th if needs_year: fmt += "%Y " # 2018 if needs_time: if needs_date or preposition: fmt += "at " fmt += "%-I:%M %p" # at 10:30 AM if not user_tz: fmt += " %Z" # EDT or EST # TODO maybe this (or something nearby) will throw pytz.exceptions.AmbiguousTimeError # near DST transition? local = util.to_local(self.reminder_time, tz) formatted_time = util.strftime(fmt, local) if self.repeats(): rp = "every " if self.repetition.nth > 1: rp += str( self.repetition.nth) + " " + self.repetition.interval + "s" else: rp += self.repetition.interval if self.repetition.interval in ["hour", "minute"]: return rp # exclude the "at 9:00 PM" portion return rp + " " + formatted_time return formatted_time
def __init__(self, body, time, repetition, username, conv_id, db): # time is a datetime in utc self.reminder_time = time self.created_time = util.now_utc() self.body = body self.repetition = repetition if repetition else Repetition(None, None) self.username = username self.conv_id = conv_id self.deleted = False self.id = None # when it's from the DB self.db = db
def set_active(self, when=None): if when is None: when = util.now_utc() self.last_active_time = when #print "Setting last active time!", self.last_active_time with sqlite3.connect(self.db) as c: cur = c.cursor() cur.execute( 'update conversations set last_active_time=? where id=?', (util.to_ts(self.last_active_time), self.id)) assert cur.rowcount == 1
def get_due_reminders(db): reminders = [] now_ts = util.to_ts(util.now_utc()) with sqlite3.connect(db) as c: c.row_factory = sqlite3.Row cur = c.cursor() cur.execute( 'SELECT rowid, * FROM reminders WHERE reminder_time<=? AND deleted=0 LIMIT 100', (now_ts, )) for row in cur: reminders.append(Reminder.from_row(row, db)) return reminders
def set_next_reminder(self): if not self.repeats(): return new_time = INTERVALS[self.repetition.interval](self.reminder_time, self.repetition.nth) # In case the reminder was older than now (maybe bot was offline), make sure the next reminder is in the future: while new_time < util.now_utc(): new_time = INTERVALS[self.repetition.interval](new_time, self.repetition.nth) new_reminder = Reminder(self.body, new_time, self.repetition, self.username, self.conv_id, self.db) new_reminder.store()
def get_all_reminders(self): reminders = [] with sqlite3.connect(self.db) as c: c.row_factory = sqlite3.Row cur = c.cursor() cur.execute( '''select rowid, * from reminders where conv_id=? and reminder_time>=? and deleted=0 order by reminder_time''', (self.id, util.to_ts(util.now_utc()))) for row in cur: reminders.append(Reminder.from_row(row, self.db)) return reminders
def is_recently_active(self): MINUTES = 30 return self.last_active_time and \ (util.now_utc() - self.last_active_time).total_seconds() < 60 * MINUTES
def try_parse_when(when, user): def fixup_times(when_str, relative_base): # When there is no explicit AM/PM assume the next upcoming one # H:MM (AM|PM)? def hhmm_explicit_ampm(when_str, relative_base): # looks for HH:MM without an AM or PM and adds 12 to the HH if necessary. # Returns str if the returned str is good to go, None if it's not fixed. time_with_minutes = regex( '(?:[^\w]|^)(\d\d?(:\d\d))(\s?[ap]\.?m)?') results = re.findall(time_with_minutes, when_str) if len(results) != 1: # I don't expect to find more than one time. Rather not do anything. return None time_match, mins_match, ampm_match = results[0] if ampm_match: # AM/PM is explicit return when_str time = datetime.strptime(time_match, '%I:%M') if time.hour > 12: return when_str # you explicitly are after noon e.g. 23:00 if relative_base.hour > time.hour: new_hour = str(time.hour + 12) return when_str.replace(time_match, new_hour + mins_match) return None def at_hh_explicit_ampm(when_str, relative_base): # looks for "at HH" without AM/PM and adds 12 to HH and :00 if necessary. at_hh_regex = regex( '(?:(?:^|\s)at\s|^)(\d\d?)($|\s?[ap]\.?m\.?(?:$|[^\w]))') results = re.findall(at_hh_regex, when_str) if len(results) != 1: # I don't expect to find more than one time. Rather not do anything. return None hour_match, ampm_match = results[0] when_with_minutes = when_str.replace(hour_match, hour_match + ":00") if ampm_match: # AM/PM is explicit return when_with_minutes time = datetime.strptime(hour_match, '%I') if time.hour > 12: return when_with_minutes # you explicitly are after noon e.g. 23:00 if relative_base.hour > time.hour: new_hour = str(time.hour + 12) return when_with_minutes.replace(hour_match, new_hour) return when_with_minutes new_when = hhmm_explicit_ampm(when_str, relative_base) if new_when: return new_when new_when = at_hh_explicit_ampm(when_str, relative_base) if new_when: return new_when # else... none of the fixes worked return when_str def extract_repetition(when_str): # Returns a new when_str, Repetition if not "every" in when_str: return when_str, Repetition(None, None) when_str = when_str.replace("week day", "weekday") days = set([ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ]) intervals_day = set( ["day", "night", "evening", "morning", "afternoon"]) intervals = set(INTERVALS) # 0: nth; 1: interval; 2: rest (e.g. "at 2pm") regexes = [ regex("(?:on )?every (?:(?P<nth>\w+) )?(?P<interval>" + i + ")s?(?:$|(?: (?P<rest>.+)))") for i in days | intervals_day | intervals ] for r in regexes: match = r.search(when_str) if match: nth_text = match.group('nth') interval = match.group('interval') rest = match.group('rest') if nth_text: nth_text = nth_text.lower() nths = {"other": 2, "second": 2, "third": 3, "fourth": 4} if nth_text in nths: nth = nths[nth_text] else: nth_regex = regex("(\d+)[a-z]*") nth_match = nth_regex.search(nth_text) if nth_match: nth = int(match.group(1)) else: # an nth that I don't understand. # TODO this is what posting to debug channel is for nth = 1 else: nth = 1 if interval: interval = interval.lower() if interval in intervals_day: interval = "day" if interval in days: if rest: rest = interval + " " + rest else: rest = "on " + interval interval = "week" if not rest: # TODO "weekday" doesn't work well here. rest = "in " + str(nth) + " " + interval + ("s" if nth > 1 else "") return rest, Repetition(interval, nth) return when_str, Repetition(None, None) # include RELATIVE_BASE explicitly so we can mock now in tests local_timezone_str = user.timezone if user.timezone else 'US/Eastern' relative_base = util.now_local(local_timezone_str).replace(tzinfo=None) when = fixup_times(when, relative_base) when, repetition = extract_repetition(when) parse_date_settings = { 'PREFER_DATES_FROM': 'future', 'PREFER_DAY_OF_MONTH': 'first', 'TO_TIMEZONE': 'UTC', 'TIMEZONE': local_timezone_str, 'RETURN_AS_TIMEZONE_AWARE': True, 'RELATIVE_BASE': relative_base } dt = dateparser.parse(when, settings=parse_date_settings) if dt != None and (dt - util.now_utc()).total_seconds() < 0: return None, None return dt, repetition