feat(schedule): Complete schedule cmd impl.
This commit is contained in:
@@ -29,7 +29,7 @@ from .settings import ScheduleSettings, ScheduleConfig
|
||||
from .ui.scheduleui import ScheduleUI
|
||||
from .ui.settingui import ScheduleSettingUI
|
||||
from .core import TimeSlot, ScheduledSession, SessionMember
|
||||
from .lib import slotid_to_utc, time_to_slotid
|
||||
from .lib import slotid_to_utc, time_to_slotid, format_until
|
||||
|
||||
_p, _np = babel._p, babel._np
|
||||
|
||||
@@ -732,12 +732,29 @@ class ScheduleCog(LionCog):
|
||||
"View and manage your scheduled session."
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
cancel=_p(
|
||||
'cmd:schedule|param:cancel', "cancel"
|
||||
),
|
||||
book=_p(
|
||||
'cmd:schedule|param:book', "book"
|
||||
),
|
||||
)
|
||||
@appcmds.describe(
|
||||
cancel=_p(
|
||||
'cmd:schedule|param:cancel|desc',
|
||||
"Select a booked timeslot to cancel."
|
||||
),
|
||||
book=_p(
|
||||
'cmd:schedule|param:book|desc',
|
||||
"Select a timeslot to schedule. (Times shown in your set timezone.)"
|
||||
),
|
||||
)
|
||||
@appcmds.guild_only
|
||||
async def schedule_cmd(self, ctx: LionContext):
|
||||
# TODO: Auotocomplete for book and cancel options
|
||||
# Will require TTL caching for member schedules.
|
||||
book = None
|
||||
cancel = None
|
||||
async def schedule_cmd(self, ctx: LionContext,
|
||||
cancel: Optional[str] = None,
|
||||
book: Optional[str] = None,
|
||||
):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
@@ -750,6 +767,9 @@ class ScheduleCog(LionCog):
|
||||
now = utc_now()
|
||||
lines: list[tuple[bool, str]] = [] # (error_status, msg)
|
||||
|
||||
if book or cancel:
|
||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
||||
|
||||
if cancel is not None:
|
||||
schedule = await self._fetch_schedule(ctx.author.id)
|
||||
# Validate provided
|
||||
@@ -759,7 +779,7 @@ class ScheduleCog(LionCog):
|
||||
'cmd:schedule|cancel_booking|error:parse_slot',
|
||||
"Time slot `{provided}` not recognised. "
|
||||
"Please select a session to cancel from the autocomplete options."
|
||||
))
|
||||
)).format(provided=cancel)
|
||||
line = (True, error)
|
||||
elif (slotid := int(cancel)) not in schedule:
|
||||
# Can't cancel slot because it isn't booked
|
||||
@@ -802,8 +822,8 @@ class ScheduleCog(LionCog):
|
||||
'cmd:schedule|create_booking|error:parse_slot',
|
||||
"Time slot `{provided}` not recognised. "
|
||||
"Please select a session to cancel from the autocomplete options."
|
||||
))
|
||||
lines = (True, error)
|
||||
)).format(provided=book)
|
||||
line = (True, error)
|
||||
elif (slotid := int(book)) in schedule:
|
||||
# Can't book because the slot is already booked
|
||||
error = t(_p(
|
||||
@@ -812,7 +832,7 @@ class ScheduleCog(LionCog):
|
||||
)).format(
|
||||
time=discord.utils.format_dt(slotid_to_utc(slotid), style='t')
|
||||
)
|
||||
lines = (True, error)
|
||||
line = (True, error)
|
||||
elif (slotid_to_utc(slotid) - now).total_seconds() < 60:
|
||||
# Can't book because it is running or about to start
|
||||
error = t(_p(
|
||||
@@ -826,7 +846,7 @@ class ScheduleCog(LionCog):
|
||||
# The slotid is valid and bookable
|
||||
# Run the booking
|
||||
try:
|
||||
await self.create_booking(guildid, ctx.author.id)
|
||||
await self.create_booking(guildid, ctx.author.id, slotid)
|
||||
ack = t(_p(
|
||||
'cmd:schedule|create_booking|success',
|
||||
"You have successfully scheduled a session at {time}."
|
||||
@@ -859,6 +879,155 @@ class ScheduleCog(LionCog):
|
||||
await ui.run(ctx.interaction)
|
||||
await ui.wait()
|
||||
|
||||
@schedule_cmd.autocomplete('book')
|
||||
async def schedule_cmd_book_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||
"""
|
||||
List the sessions available for the member to book.
|
||||
"""
|
||||
# TODO: Warning about setting timezone?
|
||||
userid = interaction.user.id
|
||||
schedule = await self._fetch_schedule(userid)
|
||||
t = self.bot.translator.t
|
||||
|
||||
|
||||
if not interaction.guild or not isinstance(interaction.user, discord.Member):
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
'cmd:schedule|acmpl:book|error:not_in_guild',
|
||||
"You need to be in a server to book sessions!"
|
||||
),
|
||||
value='None'
|
||||
)
|
||||
choices = [choice]
|
||||
else:
|
||||
member = interaction.user
|
||||
# Check blacklist role
|
||||
blacklist_role = (await self.settings.BlacklistRole.get(interaction.guild.id)).value
|
||||
if blacklist_role and blacklist_role in member.roles:
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
'cmd:schedule|acmpl:book|error:blacklisted',
|
||||
"Cannot Book -- Blacklisted"
|
||||
),
|
||||
value='None'
|
||||
)
|
||||
choices = [choice]
|
||||
else:
|
||||
nowid = self.nowid
|
||||
if ((slotid_to_utc(nowid + 3600) - utc_now()).total_seconds() < 60):
|
||||
# Start from next session instead
|
||||
nowid += 3600
|
||||
upcoming = [nowid + 3600 * i for i in range(1, 25)]
|
||||
upcoming = [slotid for slotid in upcoming if slotid not in schedule]
|
||||
choices = []
|
||||
# We can have a max of 25 acmpl choices
|
||||
# But there are at most 24 sessions to book
|
||||
# So we can use the top choice for a message
|
||||
|
||||
lion = await self.bot.core.lions.fetch_member(interaction.guild.id, member.id, member=member)
|
||||
tz = lion.timezone
|
||||
tzstring = t(_p(
|
||||
'cmd:schedule|acmpl:book|timezone_info',
|
||||
"Using timezone '{timezone}' where it is '{now}'. Change with '/my timezone'"
|
||||
)).format(
|
||||
timezone=str(tz),
|
||||
now=dt.datetime.now(tz).strftime('%H:%M')
|
||||
)
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=tzstring, value='None',
|
||||
)
|
||||
)
|
||||
|
||||
slot_format = t(_p(
|
||||
'cmd:schedule|acmpl:book|format',
|
||||
"{start} - {end} ({until})"
|
||||
))
|
||||
for slotid in upcoming:
|
||||
slot_start = slotid_to_utc(slotid).astimezone(tz).strftime('%H:%M')
|
||||
slot_end = slotid_to_utc(slotid + 3600).astimezone(tz).strftime('%H:%M')
|
||||
distance = int((slotid - nowid) // 3600)
|
||||
until = format_until(t, distance)
|
||||
name = slot_format.format(
|
||||
start=slot_start,
|
||||
end=slot_end,
|
||||
until=until
|
||||
)
|
||||
if partial.lower() in name.lower():
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=name,
|
||||
value=str(slotid)
|
||||
)
|
||||
)
|
||||
if len(choices) == 1:
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=t(_p(
|
||||
"cmd:schedule|acmpl:book|no_matching",
|
||||
"No bookable sessions matching '{partial}'"
|
||||
)).format(partial=partial[:25]),
|
||||
value=partial
|
||||
)
|
||||
)
|
||||
return choices
|
||||
|
||||
@schedule_cmd.autocomplete('cancel')
|
||||
async def schedule_cmd_cancel_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||
user = interaction.user
|
||||
schedule = await self._fetch_schedule(user.id)
|
||||
t = self.bot.translator.t
|
||||
|
||||
choices = []
|
||||
|
||||
minid = self.nowid
|
||||
if ((slotid_to_utc(self.nowid + 3600) - utc_now()).total_seconds() < 60):
|
||||
minid = minid + 3600
|
||||
can_cancel = list(slotid for slotid in schedule if slotid > minid)
|
||||
if not can_cancel:
|
||||
choice = appcmds.Choice(
|
||||
name=_p(
|
||||
'cmd:schedule|acmpl:cancel|error:empty_schedule',
|
||||
"You do not have any upcoming sessions to cancel!"
|
||||
),
|
||||
value='None'
|
||||
)
|
||||
choices.append(choice)
|
||||
else:
|
||||
lion = await self.bot.core.lions.fetch_member(interaction.guild.id, user.id)
|
||||
tz = lion.timezone
|
||||
for slotid in can_cancel:
|
||||
slot_format = t(_p(
|
||||
'cmd:schedule|acmpl:book|format',
|
||||
"{start} - {end} ({until})"
|
||||
))
|
||||
slot_start = slotid_to_utc(slotid).astimezone(tz).strftime('%H:%M')
|
||||
slot_end = slotid_to_utc(slotid + 3600).astimezone(tz).strftime('%H:%M')
|
||||
distance = int((slotid - minid) // 3600)
|
||||
until = format_until(t, distance)
|
||||
name = slot_format.format(
|
||||
start=slot_start,
|
||||
end=slot_end,
|
||||
until=until
|
||||
)
|
||||
if partial.lower() in name.lower():
|
||||
choices.append(
|
||||
appcmds.Choice(
|
||||
name=name,
|
||||
value=str(slotid)
|
||||
)
|
||||
)
|
||||
if not choices:
|
||||
choice = appcmds.Choice(
|
||||
name=t(_p(
|
||||
'cmd:schedule|acmpl:cancel|error:no_matching',
|
||||
"No cancellable sessions matching '{partial}'"
|
||||
)).format(partial=partial[:25]),
|
||||
value='None'
|
||||
)
|
||||
choices.append(choice)
|
||||
return choices
|
||||
|
||||
async def _fetch_schedule(self, userid, **kwargs):
|
||||
"""
|
||||
Fetch the given user's schedule (i.e. booking map)
|
||||
@@ -869,6 +1038,7 @@ class ScheduleCog(LionCog):
|
||||
bookings = await booking_model.fetch_where(
|
||||
booking_model.slotid >= nowid,
|
||||
userid=userid,
|
||||
**kwargs
|
||||
).order_by('slotid', ORDER.ASC)
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,9 +2,11 @@ import asyncio
|
||||
import itertools
|
||||
import datetime as dt
|
||||
|
||||
from . import logger
|
||||
from . import logger, babel
|
||||
from utils.ratelimits import Bucket
|
||||
|
||||
_p, _np = babel._p, babel._np
|
||||
|
||||
|
||||
def time_to_slotid(time: dt.datetime) -> int:
|
||||
"""
|
||||
@@ -71,3 +73,18 @@ async def limit_concurrency(aws, limit):
|
||||
while done:
|
||||
yield done.pop()
|
||||
logger.debug(f"Completed {count} tasks")
|
||||
|
||||
|
||||
def format_until(t, distance):
|
||||
if distance:
|
||||
return t(_np(
|
||||
'ui:schedule|format_until|positive',
|
||||
"in <1 hour",
|
||||
"in {number} hours",
|
||||
distance
|
||||
)).format(number=distance)
|
||||
else:
|
||||
return t(_p(
|
||||
'ui:schedule|format_until|now',
|
||||
"right now!"
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user