Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 32 additions & 6 deletions bot/cogs/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def _tradeup_dupes(collection: list[dict], sport: str, season: int, rarity: str)
return dupes


def _seasons_for(sets: list[dict], sport: str) -> str:
return ", ".join(str(s["season"]) for s in sets if s["sport"] == sport) or "none yet"


def _card_line(c: dict) -> str:
"""One-line label for a pulled/owned card."""
bits = [RARITY_EMOJI.get(c["rarity"], "")]
Expand Down Expand Up @@ -361,26 +365,48 @@ async def pack_open(
await self._grant_achievements(uid, interaction.channel)

@pack.command(name="daily", description="Claim your free daily pack")
async def pack_daily(self, interaction: discord.Interaction):
@app_commands.describe(
sport="Which league to claim (defaults to the newest set)",
season="Season year — required if you pick a sport",
)
@app_commands.choices(sport=SPORT_CHOICES)
async def pack_daily(
self, interaction: discord.Interaction,
sport: app_commands.Choice[str] | None = None, season: int | None = None,
):
await interaction.response.defer()
uid = str(interaction.user.id)
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if await queries.has_claimed_daily_pack(uid, day):
await interaction.followup.send("You already claimed your free pack today. Come back tomorrow!")
claimed = await queries.daily_packs_claimed(uid, day)
if claimed >= queries.DAILY_PACKS_PER_DAY:
await interaction.followup.send(
f"You've claimed all {queries.DAILY_PACKS_PER_DAY} free packs today. Come back tomorrow!"
)
return
sets = [s for s in await queries.list_card_sets(include_closed=False)]
if not sets:
await interaction.followup.send("No card sets are available yet.")
return
# newest season across sports
cset = max(sets, key=lambda s: (s["season"], s["set_id"]))
if sport is not None:
if season is None:
await interaction.followup.send(f"Pick a season too — available: {_seasons_for(sets, sport.value)}")
return
cset = next((s for s in sets if s["sport"] == sport.value and s["season"] == season), None)
if not cset:
await interaction.followup.send(
f"No **{sport.name} {season}** pack. Available {sport.name} seasons: {_seasons_for(sets, sport.value)}"
)
return
else:
cset = max(sets, key=lambda s: (s["season"], s["set_id"])) # newest season across sports
try:
cards_out = await queries.mint_pack(uid, cset["set_id"], 5, "daily", _now_iso())
except ValueError as e:
await interaction.followup.send(f"❌ {e}")
return
await queries.record_daily_pack_claim(uid, day)
title = f"🎁 Free daily pack — {cset['name']}"
left = queries.DAILY_PACKS_PER_DAY - (claimed + 1)
title = f"🎁 Free daily pack — {cset['name']} ({left} left today)"
await self._send_reveal(interaction, cards_out, title, cset["set_id"],
fast=await queries.get_cards_fast_open(uid))
await self._dm_wanters(cards_out, interaction.user)
Expand Down
16 changes: 11 additions & 5 deletions db/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -4747,19 +4747,25 @@ async def get_design_owners(design_id: int) -> tuple[dict | None, list[dict]]:
return {**_design_public(d), "sport": d["sport"], "season": d["season"]}, owners


# --- daily free pack ---
async def has_claimed_daily_pack(user: str, day: str) -> bool:
# --- daily free packs ---
DAILY_PACKS_PER_DAY = 3


async def daily_packs_claimed(user: str, day: str) -> int:
async with aiosqlite.connect(DB_PATH) as db:
cur = await db.execute(
"SELECT 1 FROM card_pack_claims WHERE discord_user = ? AND day = ?", (user, day)
"SELECT claims FROM card_pack_claims WHERE discord_user = ? AND day = ?", (user, day)
)
return await cur.fetchone() is not None
row = await cur.fetchone()
return row[0] if row else 0


async def record_daily_pack_claim(user: str, day: str) -> None:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT OR IGNORE INTO card_pack_claims (discord_user, day) VALUES (?, ?)", (user, day)
"INSERT INTO card_pack_claims (discord_user, day, claims) VALUES (?, ?, 1) "
"ON CONFLICT(discord_user, day) DO UPDATE SET claims = claims + 1",
(user, day),
)
await db.commit()

Expand Down
7 changes: 7 additions & 0 deletions db/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@
CREATE TABLE IF NOT EXISTS card_pack_claims (
discord_user TEXT NOT NULL,
day TEXT NOT NULL,
claims INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (discord_user, day)
);

Expand Down Expand Up @@ -551,6 +552,12 @@ async def init_db() -> None:
await db.commit()
except Exception:
pass # column already exists
# Migration: multi-claim daily packs — existing rows counted as 1 claim used
try:
await db.execute("ALTER TABLE card_pack_claims ADD COLUMN claims INTEGER NOT NULL DEFAULT 1")
await db.commit()
except Exception:
pass # column already exists
await db.execute("CREATE INDEX IF NOT EXISTS idx_games_sport_start ON games(sport, start_time)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_odds_snapshots_game_source_kind_time ON odds_snapshots(game_id, source, kind, captured_at)")
await db.commit()
Expand Down
11 changes: 8 additions & 3 deletions web/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,12 @@ async def daily_status(request: Request):
if not sess:
return {"authenticated": False, "claimed": False}
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
return {"authenticated": True, "claimed": await queries.has_claimed_daily_pack(sess["id"], day)}
claimed = await queries.daily_packs_claimed(sess["id"], day)
return {
"authenticated": True,
"claimed": claimed >= queries.DAILY_PACKS_PER_DAY,
"packs_left": max(0, queries.DAILY_PACKS_PER_DAY - claimed),
}


@router.post("/daily")
Expand All @@ -102,8 +107,8 @@ async def open_daily(request: Request):
return JSONResponse({"error": "sign in to open packs"}, status_code=401)
uid = sess["id"]
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
if await queries.has_claimed_daily_pack(uid, day):
return JSONResponse({"error": "already claimed today"}, status_code=409)
if await queries.daily_packs_claimed(uid, day) >= queries.DAILY_PACKS_PER_DAY:
return JSONResponse({"error": "all free packs claimed today"}, status_code=409)
sets = await queries.list_card_sets(include_closed=False)
if not sets:
return JSONResponse({"error": "no sets available"}, status_code=404)
Expand Down