Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the archive command #303

Draft
wants to merge 32 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
32 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
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
295 changes: 180 additions & 115 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 @@ -40,33 +39,73 @@ async def autocomplete_get_categories(
"""
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.
The list of categories only includes those that do not contain the word "archive".
"""
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 list 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_channels(
ctx: "TeXBotAutocompleteContext",
) -> "AbstractSet[discord.OptionChoice] | AbstractSet[str]":
"""
Autocpomplete callable that generates the set of channels that the user can archive.

The list of channels will include all types of channels except categories,
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)
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]
Expand All @@ -77,27 +116,35 @@ async def autocomplete_get_categories(
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 overriden 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 +157,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 +172,115 @@ 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
if isinstance(
channel, discord.CategoryChannel
): # 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
continue

await channel.edit(sync_permissions=True)

for overwrite in category.overwrites:
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_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.
"""
main_guild: discord.Guild = self.bot.main_guild

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

category_id: int = int(str_category_id)
channel_id: int = int(str_channel_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

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

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

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