async def _create_course_thread(
            self, course: Course) -> Tuple[str, Optional[discord.Thread]]:
        if course.year_level not in self.course_mappings:
            return (
                f"Base channel for year level (`{course.year_level}`) doesn't exist. Initialize it with `register_base_channel`.",
                None,
            )

        if str(course) in self.course_mappings[
                course.year_level][CURRENT_COURSES_KEY]:
            return (f"Course `{course}` already exists.", None)

        base_channel_id: int = self.course_mappings[
            course.year_level][BASE_CHANNEL_KEY]
        base_channel: discord.TextChannel = self.client.get_channel(
            base_channel_id)
        base_message: discord.Message = await base_channel.send(
            f"Thread for `{course}`")
        created_thread: discord.Thread = await base_message.create_thread(
            name=str(course))
        self.course_mappings[course.year_level][CURRENT_COURSES_KEY][str(
            course)] = created_thread.id
        write_json(THREADS_CONFIG_FILENAME, self.course_mappings)
        return (f"Done! Created thread here: {created_thread.mention}",
                created_thread)
    async def thread_refresher_task(self):
        """
        Threads automatically archive after inactivity. We'll iterate over all the threads
        and unarchive the ones that are archived. This shouldn't be expensive since it doesn't
        make any API calls unless the thread is archived (which was pushed to us by the gateway).

        This task is also equivalent to the one from {CourseThreads.py} but whatever.
        """
        try:
            if self.client.is_ready():
                thread_ids: List[int] = [
                    thread_id
                    for threads in self.thread_mappings.values()
                    for thread_id in threads
                ]

                for thread_id in thread_ids:
                    thread: Union[discord.Thread, None] = self.client.get_channel(
                        thread_id
                    )
                    # If the thread was archived and purged from the bot's cache, the
                    # getter will return None and we'll have to make an API call
                    if thread is None:
                        try:
                            thread: discord.Thread = await self.client.fetch_channel(
                                thread_id
                            )
                        except discord.errors.NotFound:
                            # Thrown if the thread isn't found, which should only happen
                            # if the thread was manually deleted; clean this thread up

                            # NOTE: this should _rarely_ be called. It's a user error if
                            # we ever get to this catch, but we do this defensively.
                            # Also, since it shouldn't get called at all, it's extremely inefficient
                            guild_id_str: Union[None, str] = None
                            for guild, threads in self.thread_mappings.items():
                                if thread_id in threads:
                                    guild_id_str = guild
                                    break
                            if guild_id_str:
                                self.thread_mappings[guild_id_str].remove(thread_id)
                            write_json(THREAD_MANAGER_FILENAME, self.thread_mappings)
                            continue

                    if thread.archived:
                        logging.info(
                            f"Unarchived thread with thread ID: {thread_id}, name: {thread.name}"
                        )
                        await thread.edit(
                            archived=False,
                            auto_archive_duration=AUTO_ARCHIVE_DURATION,
                        )
        except Exception as e:
            logging.info(f"Thread manager refresher error: {e}")
    async def unpin(self, ctx: commands.Context, thread: discord.Thread):
        """
        Unpins a thread; in other words, allow it to get archived.

        **Example(s)**
          `[p]thread unpin #some-thread` - unpins the thread #some-thread.
        """
        guild_id_str: str = str(ctx.guild.id)
        if thread.id in self.thread_mappings[guild_id_str]:
            self.thread_mappings[guild_id_str].remove(thread.id)
            write_json(THREAD_MANAGER_FILENAME, self.thread_mappings)
            return await ctx.reply(
                f"Done! Removed {thread.mention} from pinned threads."
            )
        else:
            return await ctx.reply(f"{thread.mention} isn't currently pinned.")
    async def pin(self, ctx: commands.Context, thread: discord.Thread):
        """
        Pins a thread; in other words, unarchive it when it gets auto-archived.

        **Example(s)**
          `[p]thread pin #some-thread` - pin the thread #some-thread to unarchive automatically.
        """
        guild_id_str: str = str(ctx.guild.id)
        if thread.id in self.thread_mappings.get(guild_id_str, []):
            return await ctx.reply(f"{thread.mention} is already pinned.")
        else:
            if guild_id_str not in self.thread_mappings:
                self.thread_mappings[guild_id_str] = []
            self.thread_mappings[guild_id_str].append(thread.id)
            write_json(THREAD_MANAGER_FILENAME, self.thread_mappings)
            return await ctx.reply(f"Done! Pinned {thread.mention}")
    async def delete_thread(self, ctx: commands.Context, course: Course):
        """
        Locks the thread for a given course. Try not to use it as it is relatively destructive.

        **Example(s)**
          `[p]ct delete CPEN331` - removes the thread mapping for CPEN331 and locks the thread
        """
        async with self.course_modification_lock:
            pre_check: Tuple[str, bool] = self._does_course_exist(course)
            if not pre_check[1]:
                return await ctx.reply(pre_check[0])
            course_thread: discord.Thread = self.client.get_channel(
                self.course_mappings[course.year_level][CURRENT_COURSES_KEY][
                    str(course)])
            await course_thread.edit(locked=True, archived=True)
            del self.course_mappings[course.year_level][CURRENT_COURSES_KEY][
                str(course)]
            write_json(THREADS_CONFIG_FILENAME, self.course_mappings)
            await ctx.send(
                f"Done! Locked {course_thread.mention} and removed the mapping."
            )
    async def register_base_channel(self, ctx: commands.Context,
                                    year_level: str,
                                    channel: discord.TextChannel):
        """
        Registers a base channel for courses. Note that registering a year is immutable;
        that is, once set and courses are created, it cannot be changed.

        **Example(s)**
          `[p]ct register 1 #some-channel` - registers #some-channel as the base thread for all 1xx level courses
        """
        async with self.course_modification_lock:
            try:
                int(year_level)
            except ValueError:
                return await ctx.reply(
                    f"`{year_level}` isn't a valid integer. This should map to the first digit of the course code."
                )
            if year_level in self.course_mappings and len(
                    self.course_mappings[year_level][CURRENT_COURSES_KEY]):
                return await ctx.reply(
                    "There are already courses mapped to this year level; changing this is destructive, thus is manual. Exiting."
                )
            await channel.set_permissions(
                ctx.guild.default_role,
                send_messages=False,
                create_public_threads=False,
                create_private_threads=False,
                send_messages_in_threads=True,
                manage_threads=False,
            )
            self.course_mappings[year_level] = {
                BASE_CHANNEL_KEY: channel.id,
                CURRENT_COURSES_KEY: {},
            }
            write_json(THREADS_CONFIG_FILENAME, self.course_mappings)
            return await ctx.reply(
                f"Done! Added {channel.mention} as the base for year level: `{year_level}`."
            )
    async def thread_refresher_task(self):
        """
        Threads automatically archive after inactivity. We'll iterate over all the threads
        and unarchive the ones that are archived. This shouldn't be expensive since it doesn't
        make any API calls unless the thread is archived (which was pushed to us by the gateway).
        """
        try:
            if self.client.is_ready():
                thread_ids: List[int] = [
                    channel_id
                    for year_metadata in self.course_mappings.values() for
                    channel_id in year_metadata[CURRENT_COURSES_KEY].values()
                ]
                for thread_id in thread_ids:
                    thread: Union[discord.Thread,
                                  None] = self.client.get_channel(thread_id)
                    # If the thread was archived and purged from the bot's cache, the
                    # getter will return None and we'll have to make an API call
                    if thread is None:
                        try:
                            thread: discord.Thread = await self.client.fetch_channel(
                                thread_id)
                        except discord.errors.NotFound:
                            # Thrown if the thread isn't found, which should only happen
                            # if the thread was manually deleted; clean this thread up

                            # NOTE: this should _rarely_ be called. It's a user error if
                            # we ever get to this catch, but we do this defensively.
                            # Also, since it shouldn't get called at all, it's extremely inefficient
                            target_year_level: Union[None, str] = None
                            target_course: Union[None, str] = None
                            for (
                                    year_level,
                                    year_metadata,
                            ) in self.course_mappings.items():
                                for course, course_thread_id in year_metadata[
                                        CURRENT_COURSES_KEY].items():
                                    if course_thread_id == thread_id:
                                        target_year_level = year_level
                                        target_course = course
                                        break
                                if target_year_level and target_course:
                                    break
                            del self.course_mappings[target_year_level][
                                CURRENT_COURSES_KEY][target_course]
                            write_json(
                                THREADS_CONFIG_FILENAME,
                                self.course_mappings,
                            )
                            continue

                    if thread.archived:
                        logging.info(
                            f"Unarchived thread with thread ID: {thread_id}, name: {thread.name}"
                        )
                        await thread.edit(
                            archived=False,
                            auto_archive_duration=AUTO_ARCHIVE_DURATION,
                        )
        except Exception as e:
            logging.error(f"Thread refresher error: {e}")