CLAUDE: Add get_context_user() helper for hybrid command compatibility

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 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-10 09:07:09 -06:00
parent c0f9466e54
commit 943dcc9b74
8 changed files with 49 additions and 18 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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 *

View File

@ -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 *

View File

@ -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

View File

@ -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