2023-11-04 17:18:39 +00:00
|
|
|
import json
|
2023-12-05 17:41:33 +00:00
|
|
|
import logging
|
2022-12-18 14:25:27 +00:00
|
|
|
import random
|
2023-11-04 17:18:39 +00:00
|
|
|
from datetime import datetime, time, timedelta, timezone
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Dict, Optional, Union
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
import discord
|
|
|
|
from discord.ext import commands, tasks
|
2022-11-16 17:28:47 +00:00
|
|
|
|
2023-12-05 21:12:33 +00:00
|
|
|
import config
|
2023-11-04 17:18:39 +00:00
|
|
|
from utils import TimeTableDaySwitcherView, console
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2023-12-05 17:41:33 +00:00
|
|
|
self.log = logging.getLogger("jimmy.cogs.timetable")
|
2022-10-14 21:27:37 +01:00
|
|
|
with (Path.cwd() / "utils" / "timetable.json").open() as file:
|
|
|
|
self.timetable = json.load(file)
|
|
|
|
self.update_status.start()
|
|
|
|
|
2022-11-01 09:35:50 +00:00
|
|
|
def cog_unload(self):
|
|
|
|
self.update_status.stop()
|
|
|
|
|
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:34:26 +00:00
|
|
|
# noinspection PyChainedComparisons
|
2022-10-30 16:03:41 +00:00
|
|
|
if date.timestamp() <= end_date.timestamp() and date.timestamp() >= start_date.timestamp():
|
2022-10-30 16:31:38 +00:00
|
|
|
return {"name": name, "start": start_date, "end": end_date}
|
2022-10-29 21:45:01 +01:00
|
|
|
|
2022-11-06 21:35:14 +00:00
|
|
|
def format_timetable_message(self, date: datetime) -> str:
|
|
|
|
"""Pre-formats the timetable or error message."""
|
|
|
|
if _break := self.are_on_break(date):
|
|
|
|
return f"No lessons on {discord.utils.format_dt(date, 'D')} - On break {_break['name']!r}."
|
|
|
|
|
|
|
|
lessons = self.timetable.get(date.strftime("%A").lower(), [])
|
|
|
|
if not lessons:
|
|
|
|
return f"No lessons on {discord.utils.format_dt(date, 'D')}."
|
|
|
|
|
|
|
|
blocks = [f"```\nTimetable for {date.strftime('%A')} ({date.strftime('%d/%m/%Y')}):\n```"]
|
|
|
|
for lesson in lessons:
|
2023-09-19 10:28:54 +01:00
|
|
|
lesson.setdefault("name", "unknown")
|
|
|
|
lesson.setdefault("tutor", "unknown")
|
|
|
|
lesson.setdefault("room", "unknown")
|
2022-11-06 21:35:14 +00:00
|
|
|
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')}"
|
|
|
|
f":\n> Lesson Name: {lesson['name']!r}\n"
|
|
|
|
f"> Tutor: **{lesson['tutor']}**\n> Room: `{lesson['room']}`"
|
|
|
|
)
|
|
|
|
blocks.append(text)
|
|
|
|
return "\n\n".join(blocks)
|
|
|
|
|
2022-10-14 21:27:37 +01:00
|
|
|
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 16:31:12 +00:00
|
|
|
def absolute_next_lesson(self, date: datetime = None) -> dict:
|
2022-10-14 21:27:37 +01:00
|
|
|
date = date or datetime.now()
|
2022-10-30 16:31:12 +00:00
|
|
|
# Check if there's another lesson today
|
|
|
|
lesson = self.next_lesson(date)
|
|
|
|
# If there's another lesson, great, return that
|
|
|
|
# Otherwise, we need to start looking ahead.
|
2023-04-17 23:37:47 +01:00
|
|
|
if lesson is None or lesson["start_datetime"] < datetime.now():
|
2022-10-30 16:31:12 +00:00
|
|
|
# Loop until we find the next day when it isn't the weekend, and we aren't on break.
|
|
|
|
next_available_date = date.replace(hour=0, minute=0, second=0)
|
2022-10-30 16:31:38 +00:00
|
|
|
while self.are_on_break(next_available_date) or not self.timetable.get(
|
|
|
|
next_available_date.strftime("%A").lower()
|
|
|
|
):
|
2022-10-30 16:31:12 +00:00
|
|
|
next_available_date += timedelta(days=1)
|
|
|
|
if next_available_date.year >= 2024:
|
|
|
|
raise RuntimeError("Failed to fetch absolute next lesson")
|
|
|
|
# NOTE: This could be *even* more efficient but honestly as long as it works it's fine
|
|
|
|
lesson = self.next_lesson(next_available_date) # This *must* be a date given the second part of the
|
|
|
|
# while loop's `or` statement.
|
|
|
|
assert lesson, "Unable to figure out the next lesson."
|
2022-10-14 21:27:37 +01:00
|
|
|
return lesson
|
|
|
|
|
|
|
|
async def update_timetable_message(
|
2022-10-30 16:31:38 +00:00
|
|
|
self,
|
2022-11-06 21:35:14 +00:00
|
|
|
message: Union[discord.Message, discord.ApplicationContext, discord.InteractionMessage],
|
2022-10-30 16:31:38 +00:00
|
|
|
date: datetime = None,
|
|
|
|
*,
|
|
|
|
no_prefix: bool = False,
|
2022-10-14 21:27:37 +01:00
|
|
|
):
|
|
|
|
date = date or datetime.now()
|
2022-10-29 21:45:01 +01:00
|
|
|
_break = self.are_on_break(date)
|
2022-10-30 16:34:26 +00:00
|
|
|
if _break is not None:
|
2022-12-18 14:31:38 +00:00
|
|
|
next_lesson = self.absolute_next_lesson(date + timedelta(days=1))
|
2023-09-19 10:28:54 +01:00
|
|
|
next_lesson.setdefault("name", "unknown")
|
|
|
|
next_lesson.setdefault("tutor", "unknown")
|
|
|
|
next_lesson.setdefault("room", "unknown")
|
|
|
|
next_lesson.setdefault("start_datetime", discord.utils.utcnow())
|
2022-10-30 16:31:38 +00:00
|
|
|
text = (
|
|
|
|
"[tt] On break {!r} from {} until {}. Break ends {}, and the first lesson back is "
|
|
|
|
"{lesson[name]!r} with {lesson[tutor]} in {lesson[room]}.".format(
|
|
|
|
_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,
|
|
|
|
)
|
2022-10-29 21:45:01 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
lesson = self.current_lesson(date)
|
|
|
|
if not lesson:
|
|
|
|
next_lesson = self.next_lesson(date)
|
2023-09-19 10:28:54 +01:00
|
|
|
if next_lesson is None:
|
|
|
|
try:
|
2023-09-19 17:41:41 +01:00
|
|
|
next_lesson = self.absolute_next_lesson(date + timedelta(days=1))
|
2023-09-19 10:28:54 +01:00
|
|
|
except RuntimeError:
|
2023-12-05 17:41:33 +00:00
|
|
|
self.log.critical("Failed to fetch absolute next lesson. Is this the end?")
|
2023-09-19 10:28:54 +01:00
|
|
|
return
|
2023-09-19 09:49:01 +01:00
|
|
|
next_lesson.setdefault("name", "unknown")
|
|
|
|
next_lesson.setdefault("tutor", "unknown")
|
|
|
|
next_lesson.setdefault("room", "unknown")
|
|
|
|
next_lesson.setdefault("start_datetime", discord.utils.utcnow())
|
2022-10-30 16:31:38 +00: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:
|
2023-09-19 09:49:01 +01:00
|
|
|
next_lesson.setdefault("name", "unknown")
|
|
|
|
next_lesson.setdefault("tutor", "unknown")
|
|
|
|
next_lesson.setdefault("room", "unknown")
|
|
|
|
next_lesson.setdefault("start_datetime", discord.utils.utcnow())
|
|
|
|
text = "[tt] Next Lesson: {0[name]!r} with {0[tutor]} in {0[room]} - Starts {1}".format(
|
2023-11-04 17:18:39 +00:00
|
|
|
next_lesson, discord.utils.format_dt(next_lesson["start_datetime"], "R")
|
2022-10-30 16:31:38 +00:00
|
|
|
)
|
2022-10-14 21:27:37 +01:00
|
|
|
else:
|
2023-09-19 09:49:01 +01:00
|
|
|
lesson.setdefault("name", "unknown")
|
|
|
|
lesson.setdefault("tutor", "unknown")
|
|
|
|
lesson.setdefault("room", "unknown")
|
|
|
|
lesson.setdefault("start_datetime", discord.utils.utcnow())
|
|
|
|
if lesson["name"].lower() != "lunch":
|
2023-09-19 10:33:05 +01:00
|
|
|
text = "[tt] Current Lesson: {0[name]!r} with {0[tutor]} in {0[room]} - ends {1}".format(
|
2023-11-04 17:18:39 +00:00
|
|
|
lesson, discord.utils.format_dt(lesson["end_datetime"], "R")
|
2023-09-19 09:49:01 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
text = "[tt] \U0001f37d\U0000fe0f Lunch! {0}-{1}, ends in {2}".format(
|
2023-11-04 17:18:39 +00:00
|
|
|
discord.utils.format_dt(lesson["start_datetime"], "t"),
|
|
|
|
discord.utils.format_dt(lesson["end_datetime"], "t"),
|
|
|
|
discord.utils.format_dt(lesson["end_datetime"], "R"),
|
2023-09-19 09:49:01 +01:00
|
|
|
)
|
2022-10-29 21:45:01 +01:00
|
|
|
next_lesson = self.next_lesson(date)
|
|
|
|
if next_lesson:
|
2023-09-19 09:49:01 +01:00
|
|
|
next_lesson.setdefault("name", "unknown")
|
|
|
|
next_lesson.setdefault("tutor", "unknown")
|
|
|
|
next_lesson.setdefault("room", "unknown")
|
|
|
|
next_lesson.setdefault("start_datetime", discord.utils.utcnow())
|
|
|
|
if lesson["name"].lower() != "lunch":
|
|
|
|
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")
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
text = "[tt] \U0001f37d\U0000fe0f Lunch! {0}-{1}.".format(
|
2023-11-04 17:18:39 +00:00
|
|
|
discord.utils.format_dt(lesson["start_datetime"], "t"),
|
|
|
|
discord.utils.format_dt(lesson["end_datetime"], "t"),
|
2023-09-19 09:49:01 +01:00
|
|
|
)
|
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
|
2022-11-01 09:46:12 +00:00
|
|
|
# @tasks.loop(time=schedule_times())
|
|
|
|
@tasks.loop(minutes=5)
|
2022-10-14 21:27:37 +01:00
|
|
|
async def update_status(self):
|
2022-11-16 17:28:47 +00:00
|
|
|
if config.dev:
|
|
|
|
return
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log("[Timetable Updater Task] Running!")
|
2022-10-14 21:27:37 +01:00
|
|
|
if not self.bot.is_ready():
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log("[Timetable Updater Task] Bot is not ready, waiting until ready.")
|
2022-10-14 21:27:37 +01:00
|
|
|
await self.bot.wait_until_ready()
|
|
|
|
guild: discord.Guild = self.bot.get_guild(994710566612500550)
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log("[Timetable Updater Task] Fetched source server.")
|
2022-10-14 21:27:37 +01:00
|
|
|
channel = discord.utils.get(guild.text_channels, name="timetable")
|
|
|
|
channel = channel or discord.utils.get(guild.text_channels, name="general")
|
2022-10-31 21:48:29 +00:00
|
|
|
if not channel:
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log("[Timetable Updater Task] No channel to update in!!", file=sys.stderr)
|
2022-10-31 21:48:29 +00:00
|
|
|
return
|
2022-10-14 21:27:37 +01:00
|
|
|
channel: discord.TextChannel
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log("[Timetable Updater Task] Updating in channel %r." % channel.name)
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
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:
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log(f"[TimeTable Updater Task] Sending new message in {channel.name!r}.")
|
2022-10-14 21:27:37 +01:00
|
|
|
message = await channel.send("[tt] (loading)")
|
|
|
|
|
|
|
|
message: discord.Message
|
2023-02-23 14:12:59 +00:00
|
|
|
# console.log(f"[TimeTable Updater Task] Updating message: {channel.id}/{message.id}")
|
|
|
|
await self.update_timetable_message(message)
|
|
|
|
# console.log("[Timetable Updater Task] Done! (exit result %r)" % r)
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
@commands.slash_command()
|
|
|
|
async def lesson(self, ctx: discord.ApplicationContext, *, date: str = None):
|
|
|
|
"""Shows the current/next lesson."""
|
|
|
|
if date:
|
|
|
|
try:
|
2023-05-30 19:53:47 +01:00
|
|
|
date = datetime.strptime(date, "%d/%m/%y %H:%M")
|
2022-10-14 21:27:37 +01:00
|
|
|
except ValueError:
|
2023-05-30 19:53:47 +01:00
|
|
|
return await ctx.respond("Invalid date (DD/MM/YY HH:MM).")
|
2022-10-14 21:27:37 +01:00
|
|
|
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-12-18 14:25:27 +00:00
|
|
|
if random.randint(1, 10) == 1:
|
2023-09-19 09:49:01 +01:00
|
|
|
end_date = datetime(2024, 7, 13, 0, 0, 0, tzinfo=timezone.utc)
|
2022-12-18 14:30:25 +00:00
|
|
|
days_left = (end_date - discord.utils.utcnow()).days
|
2022-12-18 14:31:38 +00:00
|
|
|
await ctx.respond("There are only {:,} days left of this academic year.".format(days_left))
|
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:
|
2023-05-30 19:53:47 +01:00
|
|
|
date = datetime.strptime(date, "%d/%m/%y")
|
2022-10-14 21:27:37 +01:00
|
|
|
except ValueError:
|
2023-05-30 19:53:47 +01:00
|
|
|
return await ctx.respond("Invalid date (DD/MM/YY).")
|
2022-10-14 21:27:37 +01:00
|
|
|
else:
|
|
|
|
date = datetime.now()
|
|
|
|
|
2022-11-06 21:35:14 +00:00
|
|
|
text = self.format_timetable_message(date)
|
2022-11-28 17:30:44 +00:00
|
|
|
view = TimeTableDaySwitcherView(ctx.user, self, date)
|
2022-11-06 21:39:18 +00:00
|
|
|
view.update_buttons()
|
2022-11-06 21:35:14 +00:00
|
|
|
await ctx.respond(text, view=view)
|
2023-11-04 17:18:39 +00:00
|
|
|
|
2023-05-29 13:09:59 +01:00
|
|
|
@commands.slash_command(name="exams")
|
|
|
|
async def _exams(self, ctx: discord.ApplicationContext):
|
|
|
|
"""Shows when exams are."""
|
2023-05-29 23:09:34 +01:00
|
|
|
paper_1 = datetime(2023, 6, 14, 12, tzinfo=timezone.utc)
|
|
|
|
paper_2 = datetime(2023, 6, 21, 12, tzinfo=timezone.utc)
|
2023-05-29 13:09:59 +01:00
|
|
|
paper_1_url = "https://classroom.google.com/c/NTQ5MzE5ODg0ODQ2/m/NTUzNjI5NjAyMDQ2/details"
|
|
|
|
paper_2_url = "https://classroom.google.com/c/NTQ5MzE5ODg0ODQ2/m/NjA1Nzk3ODQ4OTg0/details"
|
2023-05-30 19:53:47 +01:00
|
|
|
await ctx.respond(
|
2023-05-29 13:09:59 +01:00
|
|
|
f"Paper A: [{discord.utils.format_dt(paper_1, 'R')}]({paper_1_url})\n"
|
|
|
|
f"Paper B: [{discord.utils.format_dt(paper_2, 'R')}]({paper_2_url})"
|
|
|
|
)
|
2023-05-30 19:53:47 +01:00
|
|
|
message_id = (await ctx.interaction.original_response()).id
|
|
|
|
message = await ctx.channel.fetch_message(message_id)
|
|
|
|
await message.edit(suppress=True)
|
2022-10-14 21:27:37 +01:00
|
|
|
|
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
bot.add_cog(TimeTableCog(bot))
|