Skip to content
Merged
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
2065811
improve existing command
MattyTheHacker Aug 6, 2024
39563cc
stfu ruff
MattyTheHacker Aug 6, 2024
8f5fa3c
add archive-channel command
MattyTheHacker Aug 6, 2024
4902b0c
make bool pretty
MattyTheHacker Aug 6, 2024
98fe613
Merge branch 'main' into archive-upgrade
MattyTheHacker Aug 6, 2024
da57c41
Merge branch 'main' into archive-upgrade
MattyTheHacker Aug 9, 2024
8a958d5
Merge branch 'main' into archive-upgrade
MattyTheHacker Sep 25, 2024
3ebb23c
update deps
MattyTheHacker Sep 25, 2024
963641d
Merge branch 'main' into archive-upgrade
MattyTheHacker Oct 15, 2024
94a3a66
Merge branch 'main' into archive-upgrade
MattyTheHacker Oct 16, 2024
506fb80
fix poetry lock file
MattyTheHacker Oct 16, 2024
a18ccbc
Merge branch 'main' into archive-upgrade
CarrotManMatt Nov 3, 2024
ed76b5c
fix line
MattyTheHacker Nov 3, 2024
274e497
Merge branch 'main' into archive-upgrade
MattyTheHacker Dec 23, 2024
31747e0
fix ruff
MattyTheHacker Dec 23, 2024
4c1ea7e
update lock
MattyTheHacker Dec 23, 2024
9b46d92
Merge branch 'main' into archive-upgrade
MattyTheHacker Jan 1, 2025
ee1c93a
Merge branch 'main' into archive-upgrade
MattyTheHacker Jan 1, 2025
35b4376
change auto-complete
MattyTheHacker Jan 1, 2025
17bc102
Merge branch 'main' into archive-upgrade
MattyTheHacker Jan 1, 2025
94084dd
Remove silent message author induction command
MattyTheHacker Jan 1, 2025
a52ce4e
Merge branch 'yeet-silent-induct-message-command' into archive-upgrade
MattyTheHacker Jan 1, 2025
8778cdb
fix archving time out
MattyTheHacker Jan 2, 2025
4fe84ef
Merge branch 'main' into archive-upgrade
MattyTheHacker Jan 2, 2025
1c684d3
Merge branch 'main' into archive-upgrade
MattyTheHacker Jan 24, 2025
08e968b
Merge branch 'main' into archive-upgrade
MattyTheHacker Feb 28, 2025
f16d5cc
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 1, 2025
2b2e31b
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 1, 2025
d6f1dc4
change perms removal method
MattyTheHacker Mar 1, 2025
e2a9e8f
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 5, 2025
318c32c
Reformat
MattyTheHacker Mar 5, 2025
22e6dee
Fix mypy errors
MattyTheHacker Mar 5, 2025
96053df
Add full stop
MattyTheHacker Mar 6, 2025
4cedd96
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 13, 2025
743b14c
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 17, 2025
655ca83
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 17, 2025
eecf5d5
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 18, 2025
0a77bc0
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 31, 2025
fd55715
improve lgoic
MattyTheHacker Mar 31, 2025
6a85f7f
Simplify id check
MattyTheHacker Mar 31, 2025
380dd6b
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 31, 2025
4f03a9b
Fix typo
MattyTheHacker Mar 31, 2025
5e7fcc5
fix typo
MattyTheHacker Mar 31, 2025
af587d3
fix typo
MattyTheHacker Mar 31, 2025
5e0a4e1
fix
MattyTheHacker Mar 31, 2025
dd4d59b
Merge branch 'main' into archive-upgrade
MattyTheHacker Mar 31, 2025
41ed694
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 1, 2025
5a0f042
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 9, 2025
931c056
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 13, 2025
9eeb05e
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 14, 2025
f76cbd8
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 14, 2025
f5aa088
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 14, 2025
a98e058
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 15, 2025
b158353
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 16, 2025
81ded1a
Merge branch 'main' into archive-upgrade
CarrotManMatt Apr 18, 2025
8094b01
Update cogs/archive.py
MattyTheHacker Apr 18, 2025
69e7ac5
fix some stuff
MattyTheHacker Apr 18, 2025
679991c
Merge branch 'main' into archive-upgrade
MattyTheHacker Apr 18, 2025
cd9ed41
rename autocomplete
MattyTheHacker Apr 20, 2025
3ff6a23
Fix mypy
MattyTheHacker Apr 20, 2025
aa8902e
move the stuff
MattyTheHacker Apr 20, 2025
4bef5a7
check if category full
MattyTheHacker Apr 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 193 additions & 118 deletions cogs/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import discord

from exceptions import DiscordMemberNotInMainGuildError
from exceptions.base import BaseDoesNotExistError
from utils import (
CommandChecks,
Expand Down Expand Up @@ -34,70 +33,120 @@ class ArchiveCommandCog(TeXBotBaseCog):
"""Cog class that defines the "/archive" command and its call-back method."""

@staticmethod
async def autocomplete_get_categories(
async def autocomplete_get_non_archival_categories(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""
Autocomplete callable that generates the set of available selectable categories.

The list of available selectable categories is unique to each member and is used in
any of the "archive" slash-command options that have a category input-type.
Returns the set of selectable categories.
Only categories which do not contain the word "archive" are selectable.
"""
if not ctx.interaction.user:
try:
main_guild: discord.Guild = ctx.bot.main_guild
except BaseDoesNotExistError:
return set()

try:
if not await ctx.bot.check_user_has_committee_role(ctx.interaction.user):
return set()
return {
discord.OptionChoice(name=category.name, value=str(category.id))
for category in main_guild.categories
if "archive" not in category.name.lower()
}

@staticmethod
async def autocomplete_get_archival_categories(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""
Autocomplete callable that generates the set of categories to hold archived channels.

The set of categories only includes those that contain the word "archive".
These are the categories that channels are to be placed into for archiving.
It is assumed that the categories have the correct permission configuration.
"""
try:
main_guild: discord.Guild = ctx.bot.main_guild
interaction_user: discord.Member = await ctx.bot.get_main_guild_member(
ctx.interaction.user,
)
except (BaseDoesNotExistError, DiscordMemberNotInMainGuildError):
except BaseDoesNotExistError:
return set()

return {
discord.OptionChoice(name=category.name, value=str(category.id))
for category in main_guild.categories
if category.permissions_for(interaction_user).is_superset(
discord.Permissions(send_messages=True, view_channel=True),
if "archive" in category.name.lower()
}

@staticmethod
async def autocomplete_get_non_archived_channels(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""
Autocomplete callable that generates the set of channels that the user can archive.

The list of channels will include all types of channels and categories, except those
that have not been archived.
"""
try:
main_guild: discord.Guild = ctx.bot.main_guild
except BaseDoesNotExistError:
return set()

interaction_user: discord.Member | discord.User | None = ctx.interaction.user

return {
discord.OptionChoice(name=channel.name, value=str(channel.id))
for channel in main_guild.channels
if (
not isinstance(channel, discord.CategoryChannel)
and channel.category
and "archive" not in channel.category.name.lower()
and isinstance(interaction_user, discord.Member)
Comment thread
CarrotManMatt marked this conversation as resolved.
and channel.permissions_for(interaction_user).read_messages
)
}

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="archive",
name="archive-category",
description="Archives the selected category.",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="category",
description="The category to archive.",
input_type=str,
autocomplete=discord.utils.basic_autocomplete(autocomplete_get_categories), # type: ignore[arg-type]
autocomplete=discord.utils.basic_autocomplete(
autocomplete_get_non_archival_categories # type: ignore[arg-type]
),
required=True,
parameter_name="str_category_id",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="allow-archivist-access",
description="Whether to allow archivists to access the category.",
input_type=bool,
required=True,
parameter_name="allow_archivist",
)
@CommandChecks.check_interaction_user_has_committee_role
@CommandChecks.check_interaction_user_in_main_guild
async def archive(self, ctx: "TeXBotApplicationContext", str_category_id: str) -> None: # type: ignore[misc]
async def archive_category( # type: ignore[misc]
self,
ctx: "TeXBotApplicationContext",
str_category_id: str,
allow_archivist: bool, # noqa: FBT001
) -> None:
"""
Definition & callback response of the "archive" command.
Definition & callback response of the "archive-category" command.

The "archive" command hides a given category from view of casual members unless they
have the "Archivist" role.
have the "Archivist" role. This can be overridden via a boolean parameter to allow
for committee channels to be archived with the same command but not be visible.
"""
# NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent
main_guild: discord.Guild = self.bot.main_guild
interaction_member: discord.Member = await self.bot.get_main_guild_member(ctx.user)
committee_role: discord.Role = await self.bot.committee_role
guest_role: discord.Role = await self.bot.guest_role
member_role: discord.Role = await self.bot.member_role
archivist_role: discord.Role = await self.bot.archivist_role
everyone_role: discord.Role = await self.bot.get_everyone_role()

if not re.fullmatch(r"\A\d{17,20}\Z", str_category_id):
await self.command_send_error(
ctx,
ctx=ctx,
message=f"{str_category_id!r} is not a valid category ID.",
)
return
Expand All @@ -110,7 +159,7 @@ async def archive(self, ctx: "TeXBotApplicationContext", str_category_id: str) -
)
if not category:
await self.command_send_error(
ctx,
ctx=ctx,
message=f"Category with ID {str(category_id)!r} does not exist.",
)
return
Expand All @@ -125,97 +174,123 @@ async def archive(self, ctx: "TeXBotApplicationContext", str_category_id: str) -
)
return

initial_response: discord.Interaction | discord.WebhookMessage = await ctx.respond(
content=f"Archiving {category.name}...", ephemeral=True
)

channel: AllChannelTypes
for channel in category.channels:
try:
CHANNEL_NEEDS_COMMITTEE_ARCHIVING: bool = channel.permissions_for(
committee_role
).is_superset(
discord.Permissions(view_channel=True),
) and not channel.permissions_for(guest_role).is_superset(
discord.Permissions(view_channel=True),
)
CHANNEL_NEEDS_NORMAL_ARCHIVING: bool = channel.permissions_for(
guest_role
).is_superset(
discord.Permissions(view_channel=True),
)
if CHANNEL_NEEDS_COMMITTEE_ARCHIVING:
await channel.set_permissions(
everyone_role,
reason=f'{interaction_member.display_name} used "/archive".',
view_channel=False,
)
await channel.set_permissions(
guest_role,
overwrite=None,
reason=f'{interaction_member.display_name} used "/archive".',
)
await channel.set_permissions(
member_role,
overwrite=None,
reason=f'{interaction_member.display_name} used "/archive".',
)
await channel.set_permissions(
committee_role,
overwrite=None,
reason=f'{interaction_member.display_name} used "/archive".',
)

elif CHANNEL_NEEDS_NORMAL_ARCHIVING:
await channel.set_permissions(
everyone_role,
reason=f'{interaction_member.display_name} used "/archive".',
view_channel=False,
)
await channel.set_permissions(
guest_role,
overwrite=None,
reason=f'{interaction_member.display_name} used "/archive".',
)
await channel.set_permissions(
member_role,
overwrite=None,
reason=f'{interaction_member.display_name} used "/archive".',
)
await channel.set_permissions(
committee_role,
reason=f'{interaction_member.display_name} used "/archive".',
view_channel=False,
)
await channel.set_permissions(
archivist_role,
reason=f'{interaction_member.display_name} used "/archive".',
view_channel=True,
)

else:
await self.command_send_error(
ctx,
message=f"Channel {channel.mention} had invalid permissions",
)
logger.error(
"Channel %s had invalid permissions, so could not be archived.",
channel.name,
)
return

except discord.Forbidden:
await self.command_send_error(
ctx,
message=(
"TeX-Bot does not have access to "
"the channels in the selected category."
),
)
logger.error( # noqa: TRY400
(
"TeX-Bot did not have access to "
"the channels in the selected category: "
"%s."
),
category.name,
)
return

await ctx.respond("Category successfully archived", ephemeral=True)
# NOTE: Categories can not be placed inside other categories, so this will always be false, but is needed due to the typing of the method
if isinstance(channel, discord.CategoryChannel):
continue

await channel.edit(sync_permissions=True)

overwrite: discord.Member | discord.Role
for overwrite in category.overwrites:
Comment thread
MattyTheHacker marked this conversation as resolved.
await category.set_permissions(overwrite, overwrite=None)

if allow_archivist:
await category.set_permissions(
target=archivist_role,
read_messages=True,
read_message_history=True,
)

await category.edit(name=f"archive-{category.name}")

await initial_response.edit(
content=f":white_check_mark: Category '{category.name}' successfully archived."
)

@discord.slash_command( # type: ignore[no-untyped-call, misc]
name="archive-channel",
description="Archives the selected channel.",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="channel",
description="The channel to archive.",
input_type=str,
autocomplete=discord.utils.basic_autocomplete(autocomplete_get_non_archived_channels), # type: ignore[arg-type]
required=True,
parameter_name="str_channel_id",
)
@discord.option( # type: ignore[no-untyped-call, misc]
name="category",
description="The category to move the channel to.",
input_type=str,
autocomplete=discord.utils.basic_autocomplete(autocomplete_get_archival_categories), # type: ignore[arg-type]
required=True,
parameter_name="str_category_id",
)
@CommandChecks.check_interaction_user_has_committee_role
@CommandChecks.check_interaction_user_in_main_guild
async def archive_channel( # type: ignore[misc]
self, ctx: "TeXBotApplicationContext", str_channel_id: str, str_category_id: str
) -> None:
"""
Definition & callback response of the "archive-channel" command.

The "archive-channel" command moves the channel into the selected category
and syncs the permissions to the category's permissions.
"""
# NOTE: Shortcut accessors are placed at the top of the function, so that the exceptions they raise are displayed before any further errors may be sent
main_guild: discord.Guild = self.bot.main_guild
Comment thread
MattyTheHacker marked this conversation as resolved.

if not re.fullmatch(r"\A\d{17,20}\Z", str_channel_id):
await self.command_send_error(
ctx=ctx, message=f"{str_channel_id!r} is not a valid channel ID."
)
return

channel_id: int = int(str_channel_id)

channel: AllChannelTypes | None = discord.utils.get(main_guild.channels, id=channel_id)

if not channel:
await self.command_send_error(
ctx=ctx,
message=f"Channel with ID {str(channel_id)!r} does not exist.",
)
return

if isinstance(channel, discord.CategoryChannel):
await self.command_send_error(
ctx=ctx,
message=(
"Supplied channel to archive is a category - "
"please use the archive-channel command to archive categories."
),
)
return
Comment thread
MattyTheHacker marked this conversation as resolved.

if not re.fullmatch(r"\A\d{17,20}\Z", str_category_id):
await self.command_send_error(
ctx=ctx,
message=f"{str_category_id!r} is not a valid category ID.",
)

category_id: int = int(str_category_id)

category: discord.CategoryChannel | None = discord.utils.get(
main_guild.categories, id=category_id
)

if not category:
await self.command_send_error(
ctx=ctx,
message=f"Category with ID {str(category_id)!r} does not exist.",
)
return

if len(category.channels) >= 50:
await self.command_send_error(
ctx=ctx,
message=f"Category with ID {str(category_id)!r} is full. "
"Please select a different category.",
)
return
Comment thread
CarrotManMatt marked this conversation as resolved.

await channel.edit(category=category, sync_permissions=True)

await ctx.respond(":white_check_mark: Channel successfully archived", ephemeral=True)