@@ -45,15 +45,49 @@ async def cog_unload(self):
4545 logger .exception ("Failed to close RankCardGenerator" )
4646
4747 async def _send (self , ctx : commands .Context , * args , ** kwargs ):
48- """Send helper that avoids passing interaction-only kwargs in prefix mode."""
49- if getattr (ctx , "interaction" , None ) is None :
48+ """Send helper that works for both prefix and interaction invocations.
49+
50+ For interactions, responses must be sent within ~3s unless deferred.
51+ If an interaction has expired (error 10062), we fall back to a normal
52+ channel message as a last resort.
53+ """
54+ interaction = getattr (ctx , "interaction" , None )
55+
56+ if interaction is None :
5057 kwargs .pop ("ephemeral" , None )
51- return await ctx .send (* args , ** kwargs )
5258
53- async def _maybe_defer (self , ctx : commands .Context ):
54- """Defer only when invoked as a slash command."""
55- if getattr (ctx , "interaction" , None ) is not None :
56- await ctx .defer ()
59+ try :
60+ return await ctx .send (* args , ** kwargs )
61+ except discord .NotFound as e :
62+ # Slash interaction expired/invalid (common if not deferred quickly).
63+ # Fall back to a plain channel send (cannot be ephemeral).
64+ if interaction is not None and getattr (e , "code" , None ) == 10062 and getattr (ctx , "channel" , None ) is not None :
65+ kwargs .pop ("ephemeral" , None )
66+ return await ctx .channel .send (* args , ** kwargs ) # type: ignore[union-attr]
67+ raise
68+ except discord .InteractionResponded :
69+ # If something already responded via interaction, use follow-up.
70+ if interaction is not None :
71+ return await interaction .followup .send (* args , ** kwargs )
72+ raise
73+
74+ async def _maybe_defer (self , ctx : commands .Context , * , ephemeral : bool = False ):
75+ """Defer only when invoked as a slash command and not already acknowledged."""
76+ interaction = getattr (ctx , "interaction" , None )
77+ if interaction is None :
78+ return
79+
80+ # Avoid double-acknowledging the interaction.
81+ if interaction .response .is_done ():
82+ return
83+
84+ try :
85+ await ctx .defer (ephemeral = ephemeral )
86+ except discord .NotFound :
87+ # Interaction already expired; caller should rely on _send() fallback.
88+ return
89+ except discord .HTTPException :
90+ return
5791
5892 # ========================================================================
5993 # Leveling Formula
@@ -307,6 +341,8 @@ async def leaderboard(self, ctx: commands.Context, page: int = 1):
307341 if ctx .guild is None :
308342 return
309343
344+ await self ._maybe_defer (ctx )
345+
310346 if page < 1 :
311347 page = 1
312348
@@ -364,6 +400,8 @@ async def xp(self, ctx: commands.Context, user: Optional[discord.Member] = None)
364400 if target .bot :
365401 await self ._send (ctx , "Bots don't have XP!" , ephemeral = True )
366402 return
403+
404+ await self ._maybe_defer (ctx )
367405
368406 user_data = await db .get_user_data (target .id , ctx .guild .id )
369407
@@ -414,6 +452,8 @@ async def setlevel(self, ctx: commands.Context, user: discord.Member, level: int
414452 if ctx .guild is None :
415453 return
416454
455+ await self ._maybe_defer (ctx )
456+
417457 if level < 0 :
418458 await self ._send (ctx , " Level must be 0 or higher!" , ephemeral = True )
419459 return
@@ -445,6 +485,8 @@ async def addxp(self, ctx: commands.Context, user: discord.Member, amount: int):
445485 if ctx .guild is None :
446486 return
447487
488+ await self ._maybe_defer (ctx )
489+
448490 user_data = await db .get_user_data (user .id , ctx .guild .id )
449491
450492 if user_data :
@@ -476,6 +518,8 @@ async def resetlevel(self, ctx: commands.Context, user: discord.Member):
476518 if ctx .guild is None :
477519 return
478520
521+ await self ._maybe_defer (ctx )
522+
479523 await db .reset_user_data (user .id , ctx .guild .id )
480524
481525 embed = discord .Embed (
@@ -497,6 +541,8 @@ async def resetalllevels(self, ctx: commands.Context, confirm: Optional[str] = N
497541 if ctx .guild is None :
498542 return
499543
544+ await self ._maybe_defer (ctx )
545+
500546 if confirm != "CONFIRM" :
501547 embed = discord .Embed (
502548 title = " Warning" ,
@@ -532,6 +578,8 @@ async def setlevelchannel(self, ctx: commands.Context, channel: Optional[discord
532578 if ctx .guild is None :
533579 return
534580
581+ await self ._maybe_defer (ctx )
582+
535583 if channel :
536584 await db .set_levelup_channel (ctx .guild .id , channel .id )
537585 embed = discord .Embed (
@@ -561,6 +609,8 @@ async def addrole(self, ctx: commands.Context, level: int, role: discord.Role):
561609 if ctx .guild is None :
562610 return
563611
612+ await self ._maybe_defer (ctx )
613+
564614 if level < 1 :
565615 await self ._send (ctx , " Level must be 1 or higher!" , ephemeral = True )
566616 return
@@ -596,6 +646,8 @@ async def removerole(self, ctx: commands.Context, level: int):
596646 if ctx .guild is None :
597647 return
598648
649+ await self ._maybe_defer (ctx )
650+
599651 result = await db .remove_role_reward (ctx .guild .id , level )
600652
601653 if result :
@@ -624,6 +676,8 @@ async def rolerewards(self, ctx: commands.Context):
624676 if ctx .guild is None :
625677 return
626678
679+ await self ._maybe_defer (ctx )
680+
627681 role_rewards = await db .get_role_rewards (ctx .guild .id )
628682
629683 if not role_rewards :
0 commit comments