Add system time zone designation. Closes #21.
This commit is contained in:
parent
e8d1c5bf90
commit
570899928a
@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
import re
|
import re
|
||||||
@ -37,14 +38,15 @@ class CommandError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class CommandContext:
|
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.client = client
|
||||||
self.message = message
|
self.message = message
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.args = args
|
self.args = args
|
||||||
|
self._system = system
|
||||||
|
|
||||||
async def get_system(self) -> Optional[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:
|
async def ensure_system(self) -> System:
|
||||||
system = await self.get_system()
|
system = await self.get_system()
|
||||||
@ -57,6 +59,11 @@ class CommandContext:
|
|||||||
def has_next(self) -> bool:
|
def has_next(self) -> bool:
|
||||||
return bool(self.args)
|
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]:
|
def pop_str(self, error: CommandError = None) -> Optional[str]:
|
||||||
if not self.args:
|
if not self.args:
|
||||||
if error:
|
if error:
|
||||||
@ -211,7 +218,8 @@ async def command_dispatch(client: discord.Client, message: discord.Message, con
|
|||||||
client=client,
|
client=client,
|
||||||
message=message,
|
message=message,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
args=remaining_string
|
args=remaining_string,
|
||||||
|
system=await System.get_by_account(conn, message.author.id)
|
||||||
)
|
)
|
||||||
await run_command(ctx, command_root)
|
await run_command(ctx, command_root)
|
||||||
return True
|
return True
|
||||||
|
@ -124,14 +124,14 @@ async def switch_move(ctx: CommandContext):
|
|||||||
last_fronters = await last_switch.fetch_members(ctx.conn)
|
last_fronters = await last_switch.fetch_members(ctx.conn)
|
||||||
|
|
||||||
members = ", ".join([member.name for member in last_fronters]) or "nobody"
|
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)
|
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)
|
new_relative = display_relative(new_time)
|
||||||
|
|
||||||
# Confirm with user
|
# Confirm with user
|
||||||
switch_confirm_message = await ctx.reply(
|
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_absolute,
|
||||||
last_relative,
|
last_relative,
|
||||||
new_absolute,
|
new_absolute,
|
||||||
|
@ -2,6 +2,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import dateparser
|
import dateparser
|
||||||
import humanize
|
import humanize
|
||||||
|
import pytz
|
||||||
|
|
||||||
import pluralkit.bot.embeds
|
import pluralkit.bot.embeds
|
||||||
from pluralkit.bot.commands import *
|
from pluralkit.bot.commands import *
|
||||||
@ -29,6 +30,8 @@ async def system_root(ctx: CommandContext):
|
|||||||
await system_fronthistory(ctx, await ctx.ensure_system())
|
await system_fronthistory(ctx, await ctx.ensure_system())
|
||||||
elif ctx.match("frontpercent") or ctx.match("frontbreakdown") or ctx.match("frontpercentage"):
|
elif ctx.match("frontpercent") or ctx.match("frontbreakdown") or ctx.match("frontpercentage"):
|
||||||
await system_frontpercent(ctx, await ctx.ensure_system())
|
await system_frontpercent(ctx, await ctx.ensure_system())
|
||||||
|
elif ctx.match("timezone") or ctx.match("tz"):
|
||||||
|
await system_timezone(ctx)
|
||||||
elif ctx.match("set"):
|
elif ctx.match("set"):
|
||||||
await system_set(ctx)
|
await system_set(ctx)
|
||||||
elif not ctx.has_next():
|
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"))
|
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):
|
async def system_tag(ctx: CommandContext):
|
||||||
system = await ctx.ensure_system()
|
system = await ctx.ensure_system()
|
||||||
new_tag = ctx.remaining() or None
|
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])
|
name = ", ".join([member.name for member in members])
|
||||||
|
|
||||||
# Make proper date string
|
# Make proper date string
|
||||||
time_text = timestamp.isoformat(sep=" ", timespec="seconds")
|
time_text = ctx.format_time(timestamp)
|
||||||
rel_text = display_relative(timestamp)
|
rel_text = display_relative(timestamp)
|
||||||
|
|
||||||
delta_text = ""
|
delta_text = ""
|
||||||
if i > 0:
|
if i > 0:
|
||||||
last_switch_time = front_history[i - 1][0]
|
last_switch_time = front_history[i - 1][0]
|
||||||
delta_text = ", for {}".format(display_relative(timestamp - last_switch_time))
|
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 = embeds.status("\n".join(lines) or "(none)")
|
||||||
embed.title = "Past switches"
|
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)",
|
embed.add_field(name=member.name if member else "(no fronter)",
|
||||||
value="{}% ({})".format(percent, humanize.naturaldelta(front_time)))
|
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)))
|
display_relative(span_start)))
|
||||||
await ctx.reply(embed=embed)
|
await ctx.reply(embed=embed)
|
||||||
|
@ -172,7 +172,7 @@ async def member_card(conn, member: Member) -> discord.Embed:
|
|||||||
return card
|
return card
|
||||||
|
|
||||||
|
|
||||||
async def front_status(switch: Switch, conn) -> discord.Embed:
|
async def front_status(ctx: "CommandContext", switch: Switch, conn) -> discord.Embed:
|
||||||
if switch:
|
if switch:
|
||||||
embed = status("")
|
embed = status("")
|
||||||
fronter_names = [member.name for member in await switch.fetch_members(conn)]
|
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:
|
if switch.timestamp:
|
||||||
embed.add_field(name="Since",
|
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)))
|
display_relative(switch.timestamp)))
|
||||||
else:
|
else:
|
||||||
embed = error("No switches logged.")
|
embed = error("No switches logged.")
|
||||||
|
@ -334,7 +334,8 @@ async def create_tables(conn):
|
|||||||
tag text,
|
tag text,
|
||||||
avatar_url text,
|
avatar_url text,
|
||||||
token 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 (
|
await conn.execute("""create table if not exists members (
|
||||||
id serial primary key,
|
id serial primary key,
|
||||||
|
@ -93,3 +93,8 @@ class MembersAlreadyFrontingError(PluralKitError):
|
|||||||
class DuplicateSwitchMembersError(PluralKitError):
|
class DuplicateSwitchMembersError(PluralKitError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("Duplicate members in member list.")
|
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))
|
||||||
|
@ -5,13 +5,29 @@ from collections.__init__ import namedtuple
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
|
||||||
from pluralkit import db, errors
|
from pluralkit import db, errors
|
||||||
from pluralkit.member import Member
|
from pluralkit.member import Member
|
||||||
from pluralkit.switch import Switch
|
from pluralkit.switch import Switch
|
||||||
from pluralkit.utils import generate_hid, contains_custom_emoji, validate_avatar_url_or_raise
|
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
|
id: int
|
||||||
hid: str
|
hid: str
|
||||||
name: str
|
name: str
|
||||||
@ -20,6 +36,8 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a
|
|||||||
avatar_url: str
|
avatar_url: str
|
||||||
token: str
|
token: str
|
||||||
created: datetime
|
created: datetime
|
||||||
|
# pytz-compatible time zone name, usually Olson-style (eg. Europe/Amsterdam)
|
||||||
|
ui_tz: str
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_by_id(conn, system_id: int) -> Optional["System"]:
|
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
|
inner_message = leading_mentions + inner_message
|
||||||
return member, 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):
|
def to_json(self):
|
||||||
return {
|
return {
|
||||||
"id": self.hid,
|
"id": self.hid,
|
||||||
|
@ -6,3 +6,4 @@ https://github.com/Rapptz/discord.py/archive/860d6a9ace8248dfeec18b8b159e7b757d9
|
|||||||
humanize
|
humanize
|
||||||
uvloop; sys.platform != 'win32' and sys.platform != 'cygwin' and sys.platform != 'cli'
|
uvloop; sys.platform != 'win32' and sys.platform != 'cygwin' and sys.platform != 'cli'
|
||||||
ciso8601
|
ciso8601
|
||||||
|
pytz
|
Loading…
Reference in New Issue
Block a user