paper-dynasty-discord/cogs/economy_new/packs.py
Cal Corum 943dcc9b74 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>
2025-11-10 09:07:09 -06:00

336 lines
14 KiB
Python

# Economy Packs Module
# Contains pack opening, daily rewards, and donation commands from the original economy.py
import logging
from discord.ext import commands
from discord import app_commands, Member
import discord
import datetime
# Import specific utilities needed by this module
import random
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_context_user
)
from helpers.discord_utils import get_team_embed, send_to_channel, get_emoji
from discord_ui import SelectView, SelectOpenPack
logger = logging.getLogger('discord_app')
class Packs(commands.Cog):
"""Pack management, daily rewards, and donation system for Paper Dynasty."""
def __init__(self, bot):
self.bot = bot
@commands.hybrid_group(name='donation', help='Mod: Give packs for PD donations')
@commands.has_any_role(PD_PLAYERS_ROLE_NAME)
async def donation(self, ctx: commands.Context):
if ctx.invoked_subcommand is None:
await ctx.send('To buy packs, visit https://ko-fi.com/manticorum/shop and include your discord username!')
@donation.command(name='premium', help='Mod: Give premium packs', aliases=['p', 'prem'])
async def donation_premium(self, ctx: commands.Context, num_packs: int, gm: Member):
if ctx.author.id != self.bot.owner_id:
await ctx.send('Wait a second. You\'re not in charge here!')
return
team = await get_team_by_owner(gm.id)
p_query = await db_get('packtypes', params=[('name', 'Premium')])
if p_query['count'] == 0:
await ctx.send('Oof. I couldn\'t find a Premium Pack')
return
total_packs = await give_packs(team, num_packs, pack_type=p_query['packtypes'][0])
await ctx.send(f'The {team["lname"]} now have {total_packs["count"]} total packs!')
@donation.command(name='standard', help='Mod: Give standard packs', aliases=['s', 'sta'])
async def donation_standard(self, ctx: commands.Context, num_packs: int, gm: Member):
if ctx.author.id != self.bot.owner_id:
await ctx.send('Wait a second. You\'re not in charge here!')
return
team = await get_team_by_owner(gm.id)
p_query = await db_get('packtypes', params=[('name', 'Standard')])
if p_query['count'] == 0:
await ctx.send('Oof. I couldn\'t find a Standard Pack')
return
total_packs = await give_packs(team, num_packs, pack_type=p_query['packtypes'][0])
await ctx.send(f'The {team["lname"]} now have {total_packs["count"]} total packs!')
@commands.hybrid_command(name='lastpack', help='Replay your last pack')
@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(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
p_query = await db_get(
'packs',
params=[('opened', True), ('team_id', team['id']), ('new_to_old', True), ('limit', 1)]
)
if not p_query['count']:
await ctx.send(f'I do not see any packs for you, bub.')
return
pack_name = p_query['packs'][0]['pack_type']['name']
if pack_name == 'Standard':
pack_cover = IMAGES['pack-sta']
elif pack_name == 'Premium':
pack_cover = IMAGES['pack-pre']
else:
pack_cover = None
c_query = await db_get(
'cards',
params=[('pack_id', p_query['packs'][0]['id'])]
)
if not c_query['count']:
await ctx.send(f'Hmm...I didn\'t see any cards in that pack.')
return
await display_cards(c_query['cards'], team, ctx.channel, ctx.author, self.bot, pack_cover=pack_cover)
@app_commands.command(name='comeonmanineedthis', description='Daily check-in for cards, currency, and packs')
@commands.has_any_role(PD_PLAYERS)
@commands.check(legal_channel)
async def daily_checkin(self, interaction: discord.Interaction):
await interaction.response.defer()
team = await get_team_by_owner(interaction.user.id)
if not team:
await interaction.edit_original_response(
content=f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!'
)
return
current = await db_get('current')
now = datetime.datetime.now()
midnight = int_timestamp(datetime.datetime(now.year, now.month, now.day, 0, 0, 0))
daily = await db_get('rewards', params=[
('name', 'Daily Check-in'), ('team_id', team['id']), ('created_after', midnight)
])
logger.debug(f'midnight: {midnight} / now: {int_timestamp(now)}')
logger.debug(f'daily_return: {daily}')
if daily:
await interaction.edit_original_response(
content=f'Looks like you already checked in today - come back at midnight Central!'
)
return
await db_post('rewards', payload={
'name': 'Daily Check-in', 'team_id': team['id'], 'season': current['season'], 'week': current['week'],
'created': int_timestamp(now)
})
current = await db_get('current')
check_ins = await db_get('rewards', params=[
('name', 'Daily Check-in'), ('team_id', team['id']), ('season', current['season'])
])
check_count = check_ins['count'] % 5
# 2nd, 4th, and 5th check-ins
if check_count == 0 or check_count % 2 == 0:
# Every fifth check-in
if check_count == 0:
greeting = await interaction.edit_original_response(
content=f'Hey, you just earned a Standard pack of cards!'
)
pack_channel = get_channel(interaction, 'pack-openings')
p_query = await db_get('packtypes', params=[('name', 'Standard')])
if not p_query:
await interaction.edit_original_response(
content=f'I was not able to pull this pack for you. '
f'Maybe ping {get_cal_user(interaction).mention}?'
)
return
# Every second and fourth check-in
else:
greeting = await interaction.edit_original_response(
content=f'Hey, you just earned a player card!'
)
pack_channel = interaction.channel
p_query = await db_get('packtypes', params=[('name', 'Check-In Player')])
if not p_query:
await interaction.edit_original_response(
content=f'I was not able to pull this card for you. '
f'Maybe ping {get_cal_user(interaction).mention}?'
)
return
await give_packs(team, 1, p_query['packtypes'][0])
p_query = await db_get(
'packs',
params=[('opened', False), ('team_id', team['id']), ('new_to_old', True), ('limit', 1)]
)
if not p_query['count']:
await interaction.edit_original_response(
content=f'I do not see any packs in here. {await get_emoji(interaction, "ConfusedPsyduck")}')
return
pack_ids = await roll_for_cards(p_query['packs'], extra_val=check_ins['count'])
if not pack_ids:
await greeting.edit(
content=f'I was not able to create these cards {await get_emoji(interaction, "slight_frown")}'
)
return
all_cards = []
for p_id in pack_ids:
new_cards = await db_get('cards', params=[('pack_id', p_id)])
all_cards.extend(new_cards['cards'])
if not all_cards:
await interaction.edit_original_response(
content=f'I was not able to pull these cards {await get_emoji(interaction, "slight_frown")}'
)
return
await display_cards(all_cards, team, pack_channel, interaction.user, self.bot)
await refresh_sheet(team, self.bot)
return
# 1st, 3rd check-ins
else:
d_1000 = random.randint(1, 1000)
m_reward = 0
if d_1000 < 500:
m_reward = 10
elif d_1000 < 900:
m_reward = 15
elif d_1000 < 990:
m_reward = 20
else:
m_reward = 25
team = await db_post(f'teams/{team["id"]}/money/{m_reward}')
await interaction.edit_original_response(
content=f'You just earned {m_reward}₼! That brings your wallet to {team["wallet"]}₼!')
@app_commands.command(name='open-packs', description='Open packs from your inventory')
@app_commands.checks.has_any_role(PD_PLAYERS)
async def open_packs_slash(self, interaction: discord.Interaction):
if interaction.channel.name in ['paper-dynasty-chat', 'pd-news-ticker', 'pd-network-news']:
await interaction.response.send_message(
f'Please head to down to {get_channel(interaction, "pd-bot-hole")} to run this command.',
ephemeral=True
)
return
owner_team = await get_team_by_owner(interaction.user.id)
if not owner_team:
await interaction.response.send_message(
f'I don\'t see a team for you, yet. You can sign up with the `/newteam` command!'
)
return
p_query = await db_get('packs', params=[
('team_id', owner_team['id']), ('opened', False)
])
if p_query['count'] == 0:
await interaction.response.send_message(
f'Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by '
f'donating to the league.'
)
return
# Group packs by type and customization (e.g. Standard, Standard-Orioles, Standard-2012, Premium)
p_count = 0
p_data = {
'Standard': [],
'Premium': [],
'Daily': [],
'MVP': [],
'All Star': [],
'Mario': [],
'Team Choice': []
}
logger.debug(f'Parsing packs...')
for pack in p_query['packs']:
p_group = None
logger.debug(f'pack: {pack}')
logger.debug(f'pack cardset: {pack["pack_cardset"]}')
if pack['pack_team'] is None and pack['pack_cardset'] is None:
if pack['pack_type']['name'] in p_data:
p_group = pack['pack_type']['name']
elif pack['pack_team'] is not None:
if pack['pack_type']['name'] == 'Standard':
p_group = f'Standard-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
elif pack['pack_type']['name'] == 'Premium':
p_group = f'Premium-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
elif pack['pack_type']['name'] == 'Team Choice':
p_group = f'Team Choice-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
elif pack['pack_type']['name'] == 'MVP':
p_group = f'MVP-Team-{pack["pack_team"]["id"]}-{pack["pack_team"]["sname"]}'
if pack['pack_cardset'] is not None:
p_group += f'-Cardset-{pack["pack_cardset"]["id"]}'
elif pack['pack_cardset'] is not None:
if pack['pack_type']['name'] == 'Standard':
p_group = f'Standard-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
elif pack['pack_type']['name'] == 'Premium':
p_group = f'Premium-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
elif pack['pack_type']['name'] == 'Team Choice':
p_group = f'Team Choice-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
elif pack['pack_type']['name'] == 'All Star':
p_group = f'All Star-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
elif pack['pack_type']['name'] == 'MVP':
p_group = f'MVP-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
elif pack['pack_type']['name'] == 'Promo Choice':
p_group = f'Promo Choice-Cardset-{pack["pack_cardset"]["id"]}-{pack["pack_cardset"]["name"]}'
logger.info(f'p_group: {p_group}')
if p_group is not None:
p_count += 1
if p_group not in p_data:
p_data[p_group] = [pack]
else:
p_data[p_group].append(pack)
if p_count == 0:
await interaction.response.send_message(
f'Looks like you are clean out of packs, friendo. You can earn them by playing PD games or by '
f'donating to the league.'
)
return
# Display options and ask which group to open
embed = get_team_embed(f'Unopened Packs', team=owner_team)
embed.description = owner_team['lname']
select_options = []
for key in p_data:
if len(p_data[key]) > 0:
pretty_name = None
# Not a specific pack
if '-' not in key:
pretty_name = key
elif 'Team' in key:
pretty_name = f'{key.split("-")[0]} - {key.split("-")[3]}'
elif 'Cardset' in key:
pretty_name = f'{key.split("-")[0]} - {key.split("-")[3]}'
if pretty_name is not None:
embed.add_field(name=pretty_name, value=f'Qty: {len(p_data[key])}')
select_options.append(discord.SelectOption(label=pretty_name, value=key))
view = SelectView(select_objects=[SelectOpenPack(select_options, owner_team)], timeout=15)
await interaction.response.send_message(embed=embed, view=view)
async def setup(bot):
"""Setup function for the Packs cog."""
await bot.add_cog(Packs(bot))