diff options
author | 2024-05-13 01:07:59 +0100 | |
---|---|---|
committer | 2024-05-13 15:58:44 +0100 | |
commit | 51e9e3e4dfed044f19e0f56d9924bf75c483f75f (patch) | |
tree | 1b3db10f61d744b8396376f32faa5469dd2f8c92 | |
parent | Add new Talentpool API method for fetching a nomination reason (diff) |
Add new context menu command for nominating users
This command either updates or creates a nomination for the author of
the selected message, including a hyperlink to the selected message.
Hopefully the benefits that this additional context brings as well as
the reduced bar of entry for creating nominations will enhance the
talentpool recruitment system.
-rw-r--r-- | bot/exts/recruitment/talentpool/_cog.py | 154 |
1 files changed, 153 insertions, 1 deletions
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 78084899f..ad232df8c 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -5,12 +5,13 @@ from io import StringIO import discord from async_rediscache import RedisCache -from discord import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User +from discord import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User, app_commands from discord.ext import commands, tasks from discord.ext.commands import BadArgument, Cog, Context, group, has_any_role from pydis_core.site_api import ResponseCodeError from pydis_core.utils.channel import get_or_fetch_channel from pydis_core.utils.members import get_or_fetch_member +from sentry_sdk import new_scope from bot.bot import Bot from bot.constants import Bot as BotConfig, Channels, Emojis, Guild, MODERATION_ROLES, Roles, STAFF_ROLES @@ -30,6 +31,66 @@ DAYS_UNTIL_INACTIVE = 45 log = get_logger(__name__) +class NominationContextModal(discord.ui.Modal, title="New Nomination"): + nomination = discord.ui.TextInput( + label="Additional nomination context:", + style=discord.TextStyle.long, + placeholder="Additional context for nomination...", + required=False, + # Take some length off for the URL that is going to be appended + max_length=REASON_MAX_CHARS - 110 + ) + + def __init__(self, api: NominationAPI, message: discord.Message, noms_channel: discord.TextChannel): + self.message = message + self.api = api + self.noms_channel = noms_channel + + super().__init__() + + async def on_submit(self, interaction: discord.Interaction) -> None: + reason = "" + + if self.nomination.value and self.nomination.value != "": + reason += f"{self.nomination.value}\n\n" + + reason += f"Nominated from: {self.message.jump_url}" + + try: + await self.api.post_nomination(self.message.author.id, interaction.user.id, reason) + except ResponseCodeError as e: + match (e.status, e.response_json): + case (400, {"user": _}): + await interaction.response.send_message( + f":x: {self.target.mention} can't be found in the database tables.", + ephemeral=True + ) + log.warning(f"Could not find {self.target.author} in the site database tables, sync may be broken") + return + + raise e + + await interaction.response.send_message( + f":white_check_mark: The nomination for {self.message.author.mention}" + " has been added to the talent pool", + ephemeral=True + ) + + noms_channel_message = ( + f":star: {interaction.user.mention} has nominated " + f"{self.message.author.mention} from {self.message.jump_url}" + ) + + if self.nomination.value and self.nomination.value != "": + noms_channel_message += "\n```\n" + noms_channel_message += self.nomination.value + noms_channel_message += "```" + + await self.noms_channel.send(noms_channel_message) + + async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: + """Handle any exceptions in the processing of the modal.""" + await TalentPool._nominate_context_error(interaction, error) class TalentPool(Cog, name="Talentpool"): """Used to nominate potential helper candidates.""" @@ -45,6 +106,15 @@ class TalentPool(Cog, name="Talentpool"): # This lock lets us avoid cancelling the reviewer loop while the review code is running. self.autoreview_lock = asyncio.Lock() + # Setup the context menu command for message-based nomination + self.nominate_user_context_menu = app_commands.ContextMenu( + name="Nominate User", + callback=self._nominate_context_callback, + ) + self.nominate_user_context_menu.default_permissions = discord.Permissions.none() + self.nominate_user_context_menu.error(self._nominate_context_error) + self.bot.tree.add_command(self.nominate_user_context_menu, guild=discord.Object(Guild.id)) + async def cog_load(self) -> None: """Start autoreview loop if enabled.""" if await self.autoreview_enabled(): @@ -338,6 +408,82 @@ class TalentPool(Cog, name="Talentpool"): await self._nominate_user(ctx, user, reason) + @app_commands.checks.has_any_role(*STAFF_ROLES) + async def _nominate_context_callback(self, interaction: discord.Interaction, message: discord.Message) -> None: + """Create or update a nomination for the author of the selected message.""" + if message.author.bot: + await interaction.response.send_message( + ":x: I'm afraid I can't do that. Only humans can be nominated.", + ephemeral=True + ) + return + + if isinstance(message.author, Member) and any(role.id in STAFF_ROLES for role in message.author.roles): + await interaction.response.send_message( + ":x: Nominating staff members, eh? Here's a cookie :cookie:", + ephemeral=True + ) + return + + maybe_nom = await self.api.get_nomination_reason(message.author.id, interaction.user.id) + + if maybe_nom: + nomination, reason = maybe_nom + + reason += f"\n\n{message.jump_url}" + + await self.api.edit_nomination_entry( + nomination.id, + actor_id=interaction.user.id, + reason=reason + ) + + await interaction.response.send_message( + ":white_check_mark: Existing nomination updated", + ephemeral=True + ) + return + + nominations_channel = self.bot.get_channel(Channels.nominations) + + await interaction.response.send_modal(NominationContextModal(self.api, message, nominations_channel)) + + @staticmethod + async def _nominate_context_error( + interaction: discord.Interaction, + error: app_commands.AppCommandError + ) -> None: + """Handle any errors that occur with the nomination context command.""" + if isinstance(error, app_commands.errors.MissingAnyRole): + await interaction.response.send_message( + ":x: You do not have permission to use this command", ephemeral=True + ) + return + + message = ( + f":x: An unexpected error occured, Please let us know!\n\n" + f"```{error.__class__.__name__}: {error}```" + ) + + if not interaction.response.is_done(): + await interaction.response.send_message(message, ephemeral=True) + else: + await interaction.followup.send(message, ephemeral=True) + + with new_scope() as scope: + scope.user = { + "id": interaction.user.id, + "username": str(interaction.user) + } + + scope.set_tag("command", interaction.command.name) + scope.set_extra("interaction_data", interaction.data) + + log.error( + f"Error executing application command '{interaction.command.name}' invoked by {interaction.user}", + exc_info=error + ) + async def _nominate_user(self, ctx: Context, user: MemberOrUser, reason: str) -> None: """Adds the given user to the talent pool.""" if user.bot: @@ -660,3 +806,9 @@ class TalentPool(Cog, name="Talentpool"): self.autoreview_loop.cancel() self.prune_talentpool.cancel() + + self.bot.tree.remove_command( + self.nominate_user_context_menu.name, + guild=discord.Object(Guild.id), + type=self.nominate_user_context_menu.type + ) |