2022-10-14 21:27:37 +01:00
|
|
|
import asyncio
|
2022-10-29 21:45:01 +01:00
|
|
|
from typing import Optional, Union, Dict
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
import discord
|
|
|
|
from discord.ext import commands, tasks
|
|
|
|
import json
|
|
|
|
from pathlib import Path
|
|
|
|
from utils import console
|
|
|
|
from datetime import time, datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
def schedule_times():
|
|
|
|
times = []
|
|
|
|
for h in range(24):
|
|
|
|
for m in range(0, 60, 15):
|
|
|
|
times.append(time(h, m, 0))
|
|
|
|
return times
|
|
|
|
|
|
|
|
|
|
|
|
class TimeTableCog(commands.Cog):
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
|
|
self.bot = bot
|
|
|
|
with (Path.cwd() / "utils" / "timetable.json").open() as file:
|
|
|
|
self.timetable = json.load(file)
|
|
|
|
self.update_status.start()
|
|
|
|
|
2022-10-29 21:45:01 +01:00
|
|
|
def are_on_break(self, date: datetime = None) -> Optional[Dict[str, Union[str, datetime]]]:
|
|
|
|
"""Checks if the date is one as a term break"""
|
|
|
|
date = date or datetime.now()
|
|
|
|
# That description made no sense what
|
|
|
|
for name, dates in self.timetable["breaks"].items():
|
|
|
|
start_date = datetime.strptime(dates["start"], "%d/%m/%Y")
|
|
|
|
end_date = datetime.strptime(dates["end"], "%d/%m/%Y")
|
2022-10-30 16:03:41 +00:00
|
|
|
if date.timestamp() <= end_date.timestamp() and date.timestamp() >= start_date.timestamp():
|
2022-10-29 21:45:01 +01:00
|
|
|
return {
|
|
|
|
"name": name,
|
|
|
|
"start": start_date,
|
|
|
|
"end": end_date
|
|
|
|
}
|
|
|
|
|
2022-10-14 21:27:37 +01:00
|
|
|
def cog_unload(self):
|
|
|
|
self.update_status.stop()
|
|
|
|
|
|
|
|
def current_lesson(self, date: datetime = None) -> Optional[dict]:
|
|
|
|
date = date or datetime.now()
|
2022-10-20 10:05:50 +01:00
|
|
|
lessons = self.timetable.get(date.strftime("%A").lower(), [])
|
2022-10-14 21:27:37 +01:00
|
|
|
if not lessons:
|
|
|
|
return
|
|
|
|
for lesson in lessons:
|
|
|
|
now = date.time()
|
|
|
|
start_time = time(*lesson["start"])
|
|
|
|
end_time = time(*lesson["end"])
|
|
|
|
if now >= end_time:
|
|
|
|
continue
|
|
|
|
elif now < start_time:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# We are now currently actively in the lesson.
|
|
|
|
lesson = lesson.copy()
|
|
|
|
end_datetime = date.replace(hour=end_time.hour, minute=end_time.minute)
|
|
|
|
lesson["end"] = end_time
|
|
|
|
lesson["start"] = start_time
|
|
|
|
lesson["end_datetime"] = end_datetime
|
|
|
|
return lesson
|
|
|
|
|
|
|
|
def next_lesson(self, date: datetime = None) -> Optional[dict]:
|
|
|
|
date = date or datetime.now()
|
|
|
|
lessons = self.timetable.get(date.strftime("%A").lower(), []).copy()
|
|
|
|
if not lessons:
|
|
|
|
return
|
|
|
|
for lesson in lessons:
|
|
|
|
now = date.time()
|
|
|
|
start_time = time(*lesson["start"])
|
|
|
|
end_time = time(*lesson["end"])
|
|
|
|
if now > end_time:
|
|
|
|
continue
|
|
|
|
elif now < start_time:
|
|
|
|
lesson = lesson.copy()
|
|
|
|
end_datetime = date.replace(hour=start_time.hour, minute=start_time.minute)
|
|
|
|
lesson["end"] = end_time
|
|
|
|
lesson["start"] = start_time
|
|
|
|
lesson["start_datetime"] = end_datetime
|
|
|
|
return lesson
|
|
|
|
|
2022-10-30 15:32:49 +00:00
|
|
|
def absolute_next_lesson(self, date: datetime = None, *, new_method: bool = False) -> dict:
|
2022-10-14 21:27:37 +01:00
|
|
|
lesson = None
|
|
|
|
date = date or datetime.now()
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] Date:", date)
|
2022-10-30 15:32:49 +00:00
|
|
|
if new_method is True:
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] Using new method.")
|
2022-10-30 15:32:49 +00:00
|
|
|
# Check if there's another lesson today
|
2022-10-14 21:27:37 +01:00
|
|
|
lesson = self.next_lesson(date)
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] Next lesson today:", lesson)
|
2022-10-30 15:32:49 +00:00
|
|
|
# If there's another lesson, great, return that
|
|
|
|
# Otherwise, we need to start looking ahead.
|
2022-10-14 21:27:37 +01:00
|
|
|
if lesson is None:
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] Next lesson was None")
|
2022-10-30 15:32:49 +00:00
|
|
|
# Loop until we find the next day when it isn't the weekend, and we aren't on break.
|
2022-10-30 16:15:06 +00:00
|
|
|
next_available_date = date.replace(hour=0, minute=0, second=0)
|
|
|
|
print("[absolute next lesson] next available date: ", next_available_date)
|
2022-10-30 16:27:14 +00:00
|
|
|
while self.are_on_break(next_available_date) or not self.timetable.get(date.strftime("%A").lower()):
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] Next available date is on break? ",
|
|
|
|
bool(self.are_on_break(next_available_date)))
|
2022-10-30 16:28:58 +00:00
|
|
|
print("[absolute next lesson] Next available date:", date.strftime("%A").lower(),
|
|
|
|
"(available:", ", ".join(self.timetable.keys()) + ")")
|
2022-10-30 16:27:14 +00:00
|
|
|
print("[absolute next lesson] Is timetabled date?", self.timetable.get(date.strftime("%A").lower()))
|
2022-10-30 15:32:49 +00:00
|
|
|
next_available_date += timedelta(days=1)
|
2022-10-30 16:18:42 +00:00
|
|
|
if next_available_date.year >= 2024:
|
|
|
|
raise RuntimeError("Failed to fetch absolute next lesson")
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] next available date: ", next_available_date)
|
2022-10-30 15:32:49 +00:00
|
|
|
# NOTE: This could be *even* more efficient but honestly as long as it works it's fine
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] fetching next lesson on", next_available_date)
|
2022-10-30 15:32:49 +00:00
|
|
|
lesson = self.next_lesson(next_available_date) # This *must* be a date given the second part of the
|
|
|
|
# while loop's `or` statement.
|
2022-10-30 16:15:06 +00:00
|
|
|
print("[absolute next lesson] next lesson on %s: %s" % (next_available_date, lesson))
|
2022-10-30 15:32:49 +00:00
|
|
|
assert lesson, "Unable to figure out the next lesson."
|
|
|
|
else:
|
|
|
|
# this function wastes so many CPU cycles.
|
|
|
|
# Its also so computationally expensive that its async and cast to a thread to avoid blocking.
|
|
|
|
# Why do I not just do this the smart way? I'm lazy and have a headache.
|
|
|
|
while lesson is None:
|
|
|
|
lesson = self.next_lesson(date)
|
|
|
|
if lesson is None:
|
|
|
|
date += timedelta(minutes=5)
|
|
|
|
else:
|
|
|
|
break
|
2022-10-14 21:27:37 +01:00
|
|
|
return lesson
|
|
|
|
|
|
|
|
async def update_timetable_message(
|
|
|
|
self,
|
2022-10-14 21:29:42 +01:00
|
|
|
message: Union[discord.Message, discord.ApplicationContext],
|
2022-10-14 21:27:37 +01:00
|
|
|
date: datetime = None,
|
|
|
|
*,
|
|
|
|
no_prefix: bool = False,
|
|
|
|
):
|
|
|
|
date = date or datetime.now()
|
2022-10-29 21:45:01 +01:00
|
|
|
_break = self.are_on_break(date)
|
|
|
|
if _break:
|
2022-10-30 16:10:54 +00:00
|
|
|
next_lesson = self.next_lesson(_break["end"] + timedelta(days=1, hours=7))
|
2022-10-29 21:45:01 +01:00
|
|
|
next_lesson = next_lesson or {
|
|
|
|
"name": "Unknown",
|
|
|
|
"tutor": "Unknown",
|
|
|
|
"room": "Unknown"
|
|
|
|
}
|
|
|
|
text = "[tt] On break {!r} from {} until {}. Break ends {}, and the first lesson back is " \
|
2022-10-29 21:46:28 +01:00
|
|
|
"{lesson[name]!r} with {lesson[tutor]} in {lesson[room]}.".format(
|
2022-10-29 21:45:01 +01:00
|
|
|
_break["name"],
|
|
|
|
discord.utils.format_dt(_break["start"], "d"),
|
|
|
|
discord.utils.format_dt(_break["end"], "d"),
|
|
|
|
discord.utils.format_dt(_break["end"], "R"),
|
|
|
|
lesson=next_lesson
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
lesson = self.current_lesson(date)
|
|
|
|
if not lesson:
|
|
|
|
next_lesson = self.next_lesson(date)
|
|
|
|
if not next_lesson:
|
2022-10-30 16:10:54 +00:00
|
|
|
next_lesson = await asyncio.to_thread(
|
|
|
|
self.absolute_next_lesson,
|
|
|
|
new_method=True
|
|
|
|
)
|
|
|
|
next_lesson = next_lesson or {
|
2022-10-30 15:53:36 +00:00
|
|
|
"name": "unknown",
|
|
|
|
"tutor": "unknown",
|
|
|
|
"room": "unknown",
|
|
|
|
"start_datetime": datetime.max
|
|
|
|
}
|
2022-10-29 21:45:01 +01:00
|
|
|
text = "[tt] No more lessons today!\n" \
|
|
|
|
f"[tt] Next Lesson: {next_lesson['name']!r} with {next_lesson['tutor']} in " \
|
|
|
|
f"{next_lesson['room']} - " \
|
|
|
|
f"Starts {discord.utils.format_dt(next_lesson['start_datetime'], 'R')}"
|
2022-10-14 21:27:37 +01:00
|
|
|
|
2022-10-29 21:45:01 +01:00
|
|
|
else:
|
|
|
|
text = f"[tt] Next Lesson: {next_lesson['name']!r} with {next_lesson['tutor']} in " \
|
|
|
|
f"{next_lesson['room']} - Starts {discord.utils.format_dt(next_lesson['start_datetime'], 'R')}"
|
2022-10-14 21:27:37 +01:00
|
|
|
else:
|
2022-10-29 21:45:01 +01:00
|
|
|
text = f"[tt] Current Lesson: {lesson['name']!r} with {lesson['tutor']} in {lesson['room']} - " \
|
|
|
|
f"ends {discord.utils.format_dt(lesson['end_datetime'], 'R')}"
|
|
|
|
next_lesson = self.next_lesson(date)
|
|
|
|
if next_lesson:
|
|
|
|
text += "\n[tt] Next lesson: {0[name]!r} with {0[tutor]} in {0[room]} - starts {1}".format(
|
|
|
|
next_lesson,
|
|
|
|
discord.utils.format_dt(next_lesson["start_datetime"], 'R')
|
|
|
|
)
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
if no_prefix:
|
|
|
|
text = text.replace("[tt] ", "")
|
|
|
|
await message.edit(content=text, allowed_mentions=discord.AllowedMentions.none())
|
|
|
|
|
|
|
|
# noinspection DuplicatedCode
|
|
|
|
@tasks.loop(time=schedule_times())
|
|
|
|
async def update_status(self):
|
|
|
|
if not self.bot.is_ready():
|
|
|
|
await self.bot.wait_until_ready()
|
|
|
|
guild: discord.Guild = self.bot.get_guild(994710566612500550)
|
|
|
|
channel = discord.utils.get(guild.text_channels, name="timetable")
|
|
|
|
channel = channel or discord.utils.get(guild.text_channels, name="general")
|
|
|
|
channel: discord.TextChannel
|
|
|
|
|
|
|
|
async for _message in channel.history(limit=20, oldest_first=False):
|
|
|
|
if _message.author == self.bot.user and _message.content.startswith("[tt]"):
|
|
|
|
message = _message
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
message = await channel.send("[tt] (loading)")
|
|
|
|
|
|
|
|
message: discord.Message
|
|
|
|
await self.update_timetable_message(message)
|
|
|
|
|
|
|
|
@commands.slash_command()
|
|
|
|
async def lesson(self, ctx: discord.ApplicationContext, *, date: str = None):
|
|
|
|
"""Shows the current/next lesson."""
|
|
|
|
if date:
|
|
|
|
try:
|
|
|
|
date = datetime.strptime(date, "%d/%m/%Y %H:%M")
|
|
|
|
except ValueError:
|
|
|
|
return await ctx.respond("Invalid date (DD/MM/YYYY HH:MM).")
|
|
|
|
else:
|
|
|
|
date = datetime.now()
|
|
|
|
await ctx.defer()
|
2022-10-14 21:29:42 +01:00
|
|
|
await self.update_timetable_message(ctx, date, no_prefix=True)
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
@commands.slash_command(name="timetable")
|
|
|
|
async def _timetable(self, ctx: discord.ApplicationContext, date: str = None):
|
|
|
|
"""Shows the timetable for today/the specified date"""
|
|
|
|
if date:
|
|
|
|
try:
|
|
|
|
date = datetime.strptime(date, "%d/%m/%Y")
|
|
|
|
except ValueError:
|
|
|
|
return await ctx.respond("Invalid date (DD/MM/YYYY).")
|
|
|
|
else:
|
|
|
|
date = datetime.now()
|
|
|
|
|
2022-10-15 15:33:52 +01:00
|
|
|
lessons = self.timetable.get(date.strftime("%A").lower(), [])
|
2022-10-14 21:27:37 +01:00
|
|
|
if not lessons:
|
|
|
|
return await ctx.respond(f"No lessons on {discord.utils.format_dt(date, 'D')}.")
|
|
|
|
|
|
|
|
blocks = [f"```\nTimetable for {date.strftime('%A')}:\n```"]
|
|
|
|
for lesson in lessons:
|
|
|
|
start_datetime = date.replace(hour=lesson["start"][0], minute=lesson["start"][1])
|
|
|
|
end_datetime = date.replace(hour=lesson["end"][0], minute=lesson["end"][1])
|
|
|
|
text = f"{discord.utils.format_dt(start_datetime, 't')} to {discord.utils.format_dt(end_datetime, 't')}" \
|
2022-10-15 15:24:20 +01:00
|
|
|
f":\n> Lesson Name: {lesson['name']!r}\n" \
|
2022-10-14 21:27:37 +01:00
|
|
|
f"> Tutor: **{lesson['tutor']}**\n> Room: `{lesson['room']}`"
|
|
|
|
blocks.append(text)
|
|
|
|
await ctx.respond("\n\n".join(blocks))
|
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
bot.add_cog(TimeTableCog(bot))
|