From 943dcc9b74106647222cc3079de0d37113e6338d Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 10 Nov 2025 09:07:09 -0600 Subject: [PATCH] CLAUDE: Add get_context_user() helper for hybrid command compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created get_context_user() helper function to safely extract the user from either Context or Interaction objects. This prevents AttributeError issues when hybrid commands are invoked as slash commands. Hybrid commands receive commands.Context (with .author) when invoked with prefix commands, but discord.Interaction (with .user) when invoked as slash commands. The helper function handles both cases transparently. Updated all affected hybrid commands: - /branding-pd (cogs/players.py, cogs/players_new/team_management.py) - /pullroster (cogs/players.py, cogs/players_new/team_management.py) - /newsheet (cogs/economy_new/team_setup.py) - /lastpack (cogs/economy_new/packs.py) This follows the same pattern as the owner_only() fix and provides a consistent, maintainable solution for all hybrid commands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cogs/economy_new/packs.py | 4 ++-- cogs/economy_new/team_setup.py | 6 +++--- cogs/players.py | 6 +++--- cogs/players_new/team_management.py | 17 +++++++++-------- helpers.py | 2 +- helpers/main.py | 2 +- helpers/utils.py | 15 +++++++++++++++ utils.py | 15 +++++++++++++++ 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/cogs/economy_new/packs.py b/cogs/economy_new/packs.py index e87208b..7add272 100644 --- a/cogs/economy_new/packs.py +++ b/cogs/economy_new/packs.py @@ -13,7 +13,7 @@ from api_calls import db_get, db_post, db_patch from helpers.constants import PD_PLAYERS_ROLE_NAME, PD_PLAYERS, IMAGES from helpers import ( get_team_by_owner, display_cards, give_packs, legal_channel, get_channel, - get_cal_user, refresh_sheet, roll_for_cards, int_timestamp + get_cal_user, refresh_sheet, roll_for_cards, int_timestamp, get_context_user ) from helpers.discord_utils import get_team_embed, send_to_channel, get_emoji from discord_ui import SelectView, SelectOpenPack @@ -68,7 +68,7 @@ class Packs(commands.Cog): @commands.check(legal_channel) @commands.has_any_role(PD_PLAYERS_ROLE_NAME) async def last_pack_command(self, ctx: commands.Context): - team = await get_team_by_owner(ctx.author.id) + team = await get_team_by_owner(get_context_user(ctx).id) if not team: await ctx.send(f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!') return diff --git a/cogs/economy_new/team_setup.py b/cogs/economy_new/team_setup.py index e537fd5..043d52e 100644 --- a/cogs/economy_new/team_setup.py +++ b/cogs/economy_new/team_setup.py @@ -18,7 +18,7 @@ from helpers import ( get_team_by_owner, share_channel, get_role, get_cal_user, get_or_create_role, display_cards, give_packs, get_all_pos, get_sheets, refresh_sheet, post_ratings_guide, team_summary_embed, get_roster_sheet, Question, Confirm, - ButtonOptions, legal_channel, get_channel, create_channel + ButtonOptions, legal_channel, get_channel, create_channel, get_context_user ) from api_calls import team_hash from helpers.discord_utils import get_team_embed, send_to_channel @@ -395,14 +395,14 @@ class TeamSetup(commands.Cog): @commands.has_any_role(PD_PLAYERS) async def share_sheet_command( self, ctx, google_sheet_url: str, team_abbrev: Optional[str], copy_rosters: Optional[bool] = True): - owner_team = await get_team_by_owner(ctx.author.id) + owner_team = await get_team_by_owner(get_context_user(ctx).id) if not owner_team: await ctx.send(f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!') return team = owner_team if team_abbrev and team_abbrev != owner_team['abbrev']: - if ctx.author.id != 258104532423147520: + if get_context_user(ctx).id != 258104532423147520: await ctx.send(f'You can only update the team sheet for your own team, you goober.') return else: diff --git a/cogs/players.py b/cogs/players.py index af6f1cc..14e8570 100644 --- a/cogs/players.py +++ b/cogs/players.py @@ -31,7 +31,7 @@ from api_calls import db_get, db_post, db_patch, get_team_by_abbrev from helpers import ACTIVE_EVENT_LITERAL, PD_PLAYERS_ROLE_NAME, IMAGES, PD_SEASON, random_conf_gif, fuzzy_player_search, ALL_MLB_TEAMS, \ fuzzy_search, get_channel, display_cards, get_card_embeds, get_team_embed, cardset_search, get_blank_team_card, \ get_team_by_owner, get_rosters, get_roster_sheet, legal_channel, random_conf_word, embed_pagination, get_cal_user, \ - team_summary_embed, SelectView, SelectPaperdexCardset, SelectPaperdexTeam + team_summary_embed, SelectView, SelectPaperdexCardset, SelectPaperdexTeam, get_context_user from utilities.buttons import ask_with_buttons from utilities.autocomplete import cardset_autocomplete, player_autocomplete @@ -660,7 +660,7 @@ class Players(commands.Cog): @commands.check(legal_channel) async def branding_command( self, ctx, team_logo_url: str = None, color: str = None, short_name: str = None, full_name: str = None): - owner_team = await get_team_by_owner(ctx.author.id) + owner_team = await get_team_by_owner(get_context_user(ctx).id) if not owner_team: await ctx.send(f'Hmm...I don\'t see a team for you, yet. You can create one with `/newteam`!') return @@ -869,7 +869,7 @@ class Players(commands.Cog): @commands.has_any_role(PD_PLAYERS_ROLE_NAME) @commands.check(legal_channel) async def pull_roster_command(self, ctx: commands.Context, specific_roster_num: Optional[int] = None): - team = await get_team_by_owner(ctx.author.id) + team = await get_team_by_owner(get_context_user(ctx).id) if not team: await ctx.send(f'Do you even have a team? I don\'t know you.') return diff --git a/cogs/players_new/team_management.py b/cogs/players_new/team_management.py index d53b688..b650b4a 100644 --- a/cogs/players_new/team_management.py +++ b/cogs/players_new/team_management.py @@ -13,9 +13,10 @@ import requests from api_calls import db_get, db_post, db_patch, get_team_by_abbrev from helpers import ( PD_PLAYERS_ROLE_NAME, IMAGES, get_channel, get_team_embed, - get_blank_team_card, get_team_by_owner, get_rosters, + get_blank_team_card, get_team_by_owner, get_rosters, legal_channel, team_summary_embed, SelectView, - is_ephemeral_channel, is_restricted_channel, can_send_message + is_ephemeral_channel, is_restricted_channel, can_send_message, + get_context_user ) import helpers from helpers.utils import get_roster_sheet @@ -136,11 +137,11 @@ class TeamManagement(commands.Cog): @commands.hybrid_command(name='branding-pd', help='Update your team branding') @commands.has_any_role(PD_PLAYERS_ROLE_NAME) @commands.check(legal_channel) - async def branding_command(self, ctx, team_logo_url: str = None, color: str = None, + async def branding_command(self, ctx, team_logo_url: str = None, color: str = None, short_name: str = None, full_name: str = None, *, branding_updates: Optional[str] = None): """Update team branding (logo, color, etc.).""" - - team = await get_team_by_owner(ctx.author.id) + + team = await get_team_by_owner(get_context_user(ctx).id) if not team: await ctx.send('You don\'t have a team yet! Use `/newteam` to create one.') return @@ -242,11 +243,11 @@ class TeamManagement(commands.Cog): ) @commands.has_any_role(PD_PLAYERS_ROLE_NAME) @commands.check(legal_channel) - async def pull_roster_command(self, ctx, specific_roster_num: Optional[int] = None, + async def pull_roster_command(self, ctx, specific_roster_num: Optional[int] = None, roster_name: Optional[str] = None): """Pull and display saved rosters from Google Sheets.""" - - team = await get_team_by_owner(ctx.author.id) + + team = await get_team_by_owner(get_context_user(ctx).id) if not team: await ctx.send('You don\'t have a team yet! Use `/newteam` to create one.') return diff --git a/helpers.py b/helpers.py index 387783b..ae42c03 100644 --- a/helpers.py +++ b/helpers.py @@ -22,7 +22,7 @@ from in_game.gameplay_models import Team from constants import * from discord_ui import * from random_content import * -from utils import position_name_to_abbrev, user_has_role, get_roster_sheet_legacy, get_roster_sheet, get_player_url, owner_only, get_cal_user +from utils import position_name_to_abbrev, user_has_role, get_roster_sheet_legacy, get_roster_sheet, get_player_url, owner_only, get_cal_user, get_context_user from search_utils import * from discord_utils import * diff --git a/helpers/main.py b/helpers/main.py index 2fe730b..06519b2 100644 --- a/helpers/main.py +++ b/helpers/main.py @@ -22,7 +22,7 @@ from in_game.gameplay_models import Team from constants import * from discord_ui import * from random_content import * -from utils import position_name_to_abbrev, user_has_role, get_roster_sheet_legacy, get_roster_sheet, get_player_url, owner_only, get_cal_user +from utils import position_name_to_abbrev, user_has_role, get_roster_sheet_legacy, get_roster_sheet, get_player_url, owner_only, get_cal_user, get_context_user from search_utils import * from discord_utils import * diff --git a/helpers/utils.py b/helpers/utils.py index 7d91924..d956435 100644 --- a/helpers/utils.py +++ b/helpers/utils.py @@ -103,6 +103,21 @@ def owner_only(ctx) -> bool: return False +def get_context_user(ctx): + """ + Get the user from either a Context or Interaction object. + + Hybrid commands can receive either commands.Context (from prefix commands) + or discord.Interaction (from slash commands). This helper safely extracts + the user from either type. + + Returns: + discord.User or discord.Member: The user who invoked the command + """ + # Handle both Context (has .author) and Interaction (has .user) objects + return getattr(ctx, 'user', None) or getattr(ctx, 'author', None) + + def get_cal_user(ctx): """Get the Cal user from context. Always returns an object with .mention attribute.""" import logging diff --git a/utils.py b/utils.py index 4b3566d..bb71a96 100644 --- a/utils.py +++ b/utils.py @@ -100,6 +100,21 @@ def owner_only(ctx) -> bool: return False +def get_context_user(ctx): + """ + Get the user from either a Context or Interaction object. + + Hybrid commands can receive either commands.Context (from prefix commands) + or discord.Interaction (from slash commands). This helper safely extracts + the user from either type. + + Returns: + discord.User or discord.Member: The user who invoked the command + """ + # Handle both Context (has .author) and Interaction (has .user) objects + return getattr(ctx, 'user', None) or getattr(ctx, 'author', None) + + def get_cal_user(ctx): """Get the Cal user from context. Always returns an object with .mention attribute.""" import logging