Add system time zone designation. Closes #21.

This commit is contained in:
Ske 2018-12-18 19:38:53 +01:00
parent e8d1c5bf90
commit 570899928a
8 changed files with 89 additions and 13 deletions

View File

@ -1,4 +1,5 @@
import asyncio
from datetime import datetime
import discord
import re
@ -37,14 +38,15 @@ class CommandError(Exception):
class CommandContext:
def __init__(self, client: discord.Client, message: discord.Message, conn, args: str):
def __init__(self, client: discord.Client, message: discord.Message, conn, args: str, system: Optional[System]):
self.client = client
self.message = message
self.conn = conn
self.args = args
self._system = system
async def get_system(self) -> Optional[System]:
return await db.get_system_by_account(self.conn, self.message.author.id)
return self._system
async def ensure_system(self) -> System:
system = await self.get_system()
@ -57,6 +59,11 @@ class CommandContext:
def has_next(self) -> bool:
return bool(self.args)
def format_time(self, dt: datetime):
if self._system:
return self._system.format_time(dt)
return dt.isoformat(sep=" ", timespec="seconds") + " UTC"
def pop_str(self, error: CommandError = None) -> Optional[str]:
if not self.args:
if error:
@ -211,7 +218,8 @@ async def command_dispatch(client: discord.Client, message: discord.Message, con
client=client,
message=message,
conn=conn,
args=remaining_string
args=remaining_string,
system=await System.get_by_account(conn, message.author.id)
)
await run_command(ctx, command_root)
return True

View File

@ -124,14 +124,14 @@ async def switch_move(ctx: CommandContext):
last_fronters = await last_switch.fetch_members(ctx.conn)
members = ", ".join([member.name for member in last_fronters]) or "nobody"
last_absolute = last_switch.timestamp.isoformat(sep=" ", timespec="seconds")
last_absolute = ctx.format_time(last_switch.timestamp)
last_relative = display_relative(last_switch.timestamp)
new_absolute = new_time.isoformat(sep=" ", timespec="seconds")
new_absolute = ctx.format_time(new_time)
new_relative = display_relative(new_time)
# Confirm with user
switch_confirm_message = await ctx.reply(
"This will move the latest switch ({}) from {} UTC ({} ago) to {} UTC ({} ago). Is this OK?".format(members,
"This will move the latest switch ({}) from {} ({} ago) to {} ({} ago). Is this OK?".format(members,
last_absolute,
last_relative,
new_absolute,

View File

@ -2,6 +2,7 @@ from datetime import datetime, timedelta
import dateparser
import humanize
import pytz
import pluralkit.bot.embeds
from pluralkit.bot.commands import *
@ -29,6 +30,8 @@ async def system_root(ctx: CommandContext):
await system_fronthistory(ctx, await ctx.ensure_system())
elif ctx.match("frontpercent") or ctx.match("frontbreakdown") or ctx.match("frontpercentage"):
await system_frontpercent(ctx, await ctx.ensure_system())
elif ctx.match("timezone") or ctx.match("tz"):
await system_timezone(ctx)
elif ctx.match("set"):
await system_set(ctx)
elif not ctx.has_next():
@ -95,6 +98,16 @@ async def system_description(ctx: CommandContext):
await ctx.reply_ok("System description {}.".format("updated" if new_description else "cleared"))
async def system_timezone(ctx: CommandContext):
system = await ctx.ensure_system()
new_tz = ctx.remaining() or None
tz = await system.set_time_zone(ctx.conn, new_tz)
offset = tz.utcoffset(datetime.utcnow())
offset_str = "UTC{:+02d}:{:02d}".format(int(offset.total_seconds() // 3600), int(offset.total_seconds() // 60 % 60))
await ctx.reply_ok("System time zone set to {} ({}, {}).".format(tz.tzname(datetime.utcnow()), offset_str, tz.zone))
async def system_tag(ctx: CommandContext):
system = await ctx.ensure_system()
new_tag = ctx.remaining() or None
@ -196,14 +209,14 @@ async def system_fronthistory(ctx: CommandContext, system: System):
name = ", ".join([member.name for member in members])
# Make proper date string
time_text = timestamp.isoformat(sep=" ", timespec="seconds")
time_text = ctx.format_time(timestamp)
rel_text = display_relative(timestamp)
delta_text = ""
if i > 0:
last_switch_time = front_history[i - 1][0]
delta_text = ", for {}".format(display_relative(timestamp - last_switch_time))
lines.append("**{}** ({} UTC, {} ago{})".format(name, time_text, rel_text, delta_text))
lines.append("**{}** ({}, {} ago{})".format(name, time_text, rel_text, delta_text))
embed = embeds.status("\n".join(lines) or "(none)")
embed.title = "Past switches"
@ -302,6 +315,6 @@ async def system_frontpercent(ctx: CommandContext, system: System):
embed.add_field(name=member.name if member else "(no fronter)",
value="{}% ({})".format(percent, humanize.naturaldelta(front_time)))
embed.set_footer(text="Since {} UTC ({} ago)".format(span_start.isoformat(sep=" ", timespec="seconds"),
embed.set_footer(text="Since {} ({} ago)".format(ctx.format_time(span_start),
display_relative(span_start)))
await ctx.reply(embed=embed)

View File

@ -172,7 +172,7 @@ async def member_card(conn, member: Member) -> discord.Embed:
return card
async def front_status(switch: Switch, conn) -> discord.Embed:
async def front_status(ctx: "CommandContext", switch: Switch, conn) -> discord.Embed:
if switch:
embed = status("")
fronter_names = [member.name for member in await switch.fetch_members(conn)]
@ -186,7 +186,7 @@ async def front_status(switch: Switch, conn) -> discord.Embed:
if switch.timestamp:
embed.add_field(name="Since",
value="{} UTC ({})".format(switch.timestamp.isoformat(sep=" ", timespec="seconds"),
value="{} ({})".format(ctx.format_time(switch.timestamp),
display_relative(switch.timestamp)))
else:
embed = error("No switches logged.")

View File

@ -334,7 +334,8 @@ async def create_tables(conn):
tag text,
avatar_url text,
token text,
created timestamp not null default (current_timestamp at time zone 'utc')
created timestamp not null default (current_timestamp at time zone 'utc'),
ui_tz text not null default 'UTC'
)""")
await conn.execute("""create table if not exists members (
id serial primary key,

View File

@ -93,3 +93,8 @@ class MembersAlreadyFrontingError(PluralKitError):
class DuplicateSwitchMembersError(PluralKitError):
def __init__(self):
super().__init__("Duplicate members in member list.")
class InvalidTimeZoneError(PluralKitError):
def __init__(self, tz_name: str):
super().__init__("Invalid time zone designation \"{}\".\n\nFor a list of valid time zone designations, see the `TZ database name` column here: <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List>.".format(tz_name))

View File

@ -5,13 +5,29 @@ from collections.__init__ import namedtuple
from datetime import datetime
from typing import Optional, List, Tuple
import pytz
from pluralkit import db, errors
from pluralkit.member import Member
from pluralkit.switch import Switch
from pluralkit.utils import generate_hid, contains_custom_emoji, validate_avatar_url_or_raise
class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "avatar_url", "token", "created"])):
def canonicalize_tz_name(name: str) -> Optional[str]:
# First, try a direct search
try:
pytz.timezone(name)
return name
except pytz.UnknownTimeZoneError:
pass
# Then check last fragment of common time zone identifiers
name_map = {tz.split("/")[-1].replace("_", " "): tz for tz in pytz.common_timezones}
if name in name_map:
return name_map[name]
class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "avatar_url", "token", "created", "ui_tz"])):
id: int
hid: str
name: str
@ -20,6 +36,8 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
avatar_url: str
token: str
created: datetime
# pytz-compatible time zone name, usually Olson-style (eg. Europe/Amsterdam)
ui_tz: str
@staticmethod
async def get_by_id(conn, system_id: int) -> Optional["System"]:
@ -206,6 +224,36 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
inner_message = leading_mentions + inner_message
return member, inner_message
def format_time(self, dt: datetime) -> str:
"""
Localizes the given `datetime` to a string based on the system's preferred time zone.
Assumes `dt` is a naïve `datetime` instance set to UTC, which is consistent with the rest of PluralKit.
"""
tz = pytz.timezone(self.ui_tz)
# Set to aware (UTC), convert to tz, set to naive (tz), then format and append name
return tz.normalize(pytz.utc.localize(dt)).replace(tzinfo=None).isoformat(sep=" ", timespec="seconds") + " " + tz.tzname(dt)
async def set_time_zone(self, conn, tz_name: str) -> pytz.tzinfo:
"""
Sets the system time zone to the time zone represented by the given string.
If `tz_name` is None or an empty string, will default to UTC.
If `tz_name` does not represent a valid time zone string, will raise InvalidTimeZoneError.
:raises: InvalidTimeZoneError
:returns: The `pytz.tzinfo` instance of the newly set time zone.
"""
canonical_name = canonicalize_tz_name(tz_name)
if not canonical_name:
raise errors.InvalidTimeZoneError(tz_name)
tz = pytz.timezone(canonical_name)
await db.update_system_field(conn, self.id, "ui_tz", tz.zone)
return tz
def to_json(self):
return {
"id": self.hid,

View File

@ -6,3 +6,4 @@ https://github.com/Rapptz/discord.py/archive/860d6a9ace8248dfeec18b8b159e7b757d9
humanize
uvloop; sys.platform != 'win32' and sys.platform != 'cygwin' and sys.platform != 'cli'
ciso8601
pytz