feat(schedule): Complete schedule cmd impl.

This commit is contained in:
2023-09-30 12:13:50 +03:00
parent ca963ee8b1
commit 1f92957393
2 changed files with 199 additions and 12 deletions

View File

@@ -29,7 +29,7 @@ from .settings import ScheduleSettings, ScheduleConfig
from .ui.scheduleui import ScheduleUI from .ui.scheduleui import ScheduleUI
from .ui.settingui import ScheduleSettingUI from .ui.settingui import ScheduleSettingUI
from .core import TimeSlot, ScheduledSession, SessionMember 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 _p, _np = babel._p, babel._np
@@ -732,12 +732,29 @@ class ScheduleCog(LionCog):
"View and manage your scheduled session." "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 @appcmds.guild_only
async def schedule_cmd(self, ctx: LionContext): async def schedule_cmd(self, ctx: LionContext,
# TODO: Auotocomplete for book and cancel options cancel: Optional[str] = None,
# Will require TTL caching for member schedules. book: Optional[str] = None,
book = None ):
cancel = None
if not ctx.guild: if not ctx.guild:
return return
if not ctx.interaction: if not ctx.interaction:
@@ -750,6 +767,9 @@ class ScheduleCog(LionCog):
now = utc_now() now = utc_now()
lines: list[tuple[bool, str]] = [] # (error_status, msg) 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: if cancel is not None:
schedule = await self._fetch_schedule(ctx.author.id) schedule = await self._fetch_schedule(ctx.author.id)
# Validate provided # Validate provided
@@ -759,7 +779,7 @@ class ScheduleCog(LionCog):
'cmd:schedule|cancel_booking|error:parse_slot', 'cmd:schedule|cancel_booking|error:parse_slot',
"Time slot `{provided}` not recognised. " "Time slot `{provided}` not recognised. "
"Please select a session to cancel from the autocomplete options." "Please select a session to cancel from the autocomplete options."
)) )).format(provided=cancel)
line = (True, error) line = (True, error)
elif (slotid := int(cancel)) not in schedule: elif (slotid := int(cancel)) not in schedule:
# Can't cancel slot because it isn't booked # Can't cancel slot because it isn't booked
@@ -802,8 +822,8 @@ class ScheduleCog(LionCog):
'cmd:schedule|create_booking|error:parse_slot', 'cmd:schedule|create_booking|error:parse_slot',
"Time slot `{provided}` not recognised. " "Time slot `{provided}` not recognised. "
"Please select a session to cancel from the autocomplete options." "Please select a session to cancel from the autocomplete options."
)) )).format(provided=book)
lines = (True, error) line = (True, error)
elif (slotid := int(book)) in schedule: elif (slotid := int(book)) in schedule:
# Can't book because the slot is already booked # Can't book because the slot is already booked
error = t(_p( error = t(_p(
@@ -812,7 +832,7 @@ class ScheduleCog(LionCog):
)).format( )).format(
time=discord.utils.format_dt(slotid_to_utc(slotid), style='t') 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: elif (slotid_to_utc(slotid) - now).total_seconds() < 60:
# Can't book because it is running or about to start # Can't book because it is running or about to start
error = t(_p( error = t(_p(
@@ -826,7 +846,7 @@ class ScheduleCog(LionCog):
# The slotid is valid and bookable # The slotid is valid and bookable
# Run the booking # Run the booking
try: try:
await self.create_booking(guildid, ctx.author.id) await self.create_booking(guildid, ctx.author.id, slotid)
ack = t(_p( ack = t(_p(
'cmd:schedule|create_booking|success', 'cmd:schedule|create_booking|success',
"You have successfully scheduled a session at {time}." "You have successfully scheduled a session at {time}."
@@ -859,6 +879,155 @@ class ScheduleCog(LionCog):
await ui.run(ctx.interaction) await ui.run(ctx.interaction)
await ui.wait() 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): async def _fetch_schedule(self, userid, **kwargs):
""" """
Fetch the given user's schedule (i.e. booking map) Fetch the given user's schedule (i.e. booking map)
@@ -869,6 +1038,7 @@ class ScheduleCog(LionCog):
bookings = await booking_model.fetch_where( bookings = await booking_model.fetch_where(
booking_model.slotid >= nowid, booking_model.slotid >= nowid,
userid=userid, userid=userid,
**kwargs
).order_by('slotid', ORDER.ASC) ).order_by('slotid', ORDER.ASC)
return { return {

View File

@@ -2,9 +2,11 @@ import asyncio
import itertools import itertools
import datetime as dt import datetime as dt
from . import logger from . import logger, babel
from utils.ratelimits import Bucket from utils.ratelimits import Bucket
_p, _np = babel._p, babel._np
def time_to_slotid(time: dt.datetime) -> int: def time_to_slotid(time: dt.datetime) -> int:
""" """
@@ -71,3 +73,18 @@ async def limit_concurrency(aws, limit):
while done: while done:
yield done.pop() yield done.pop()
logger.debug(f"Completed {count} tasks") 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!"
))