Discord.py按钮交互任务取消问题:角色检测失效求助
Discord.py旅行任务取消问题解决方案
问题背景
我正在开发一款Discord游戏,每个频道对应游戏中的一个地点。设置了按钮菜单,用户点击按钮1时,会通过添加/移除频道关联角色实现频道切换。按钮1的执行包含asyncio.sleep()延迟以模拟旅行耗时。用户点击按钮1后会出现按钮2,按钮2的目标是在按钮1执行过程中取消其任务。
我尝试在按钮1执行结束前添加判断:检查用户是否拥有点击按钮2时添加的'cancel'角色,若有则终止执行,否则完成频道切换。
遇到的问题
通过interaction.user获取的用户对象是交互发生时的快照,不会自动更新后续的角色变化。因此即使点击按钮2添加了'cancel'角色,按钮1的判断仍无法识别,导致任务无法取消。
原始代码
import asyncio import discord from discord.ext import commands # this just imports role names/ids from another file import data.roles as ccroles class cancel(discord.ui.View): def __init__(self): super().__init__(timeout = None) @discord.ui.button(label="Cancel", custom_id="cancel", style=discord.ButtonStyle.red) async def buttoncancel(self, interaction, button): user = interaction.user await interaction.response.send_message("You clicked me!", ephemeral=True,) await user.add_roles(user.guild.get_role(ccroles.cancel)) print('Cancel added.') class Menu(discord.ui.View): def __init__(self): super().__init__(timeout = None) @discord.ui.button(label="Location 2", custom_id="location2", style=discord.ButtonStyle.blurple) async def button1(self, interaction, button): user = interaction.user if ccroles.location_1 in [y.id for y in user.roles]: await interaction.response.send_message("Traveling to Location 2!", ephemeral=True, view=cancel()) await asyncio.sleep(10) print('I waited 10 seconds.') if ccroles.cancel in [y.id for y in user.roles]: print('Cancelled travel.') await user.remove_roles(user.guild.get_role(ccroles.cancel)) else: await asyncio.sleep(1) print('I waited 1 second.') await user.edit(roles=[]) await user.add_roles(user.guild.get_role(ccroles.location_2)) class exampleMenu(commands.Cog): def __init__(self, client: commands.Bot): self.client=client @commands.command() async def exampleMenu(self, ctx): embed = discord.Embed(title="Travel Menu", description="Where would you like to travel?") await ctx.send(embed=embed, view=Menu()) async def setup(client:commands.Bot) -> None: await client.add_cog(exampleMenu(client))
解决方案
方案一:刷新用户角色数据
既然interaction.user是静态快照,我们可以在需要检查角色时,重新从服务器获取用户的最新数据。修改按钮1中的角色检查逻辑:
async def button1(self, interaction, button): user = interaction.user if ccroles.location_1 in [y.id for y in user.roles]: await interaction.response.send_message("Traveling to Location 2!", ephemeral=True, view=cancel()) await asyncio.sleep(10) print('I waited 10 seconds.') # 重新拉取用户最新信息 updated_user = await interaction.guild.fetch_member(user.id) if ccroles.cancel in [y.id for y in updated_user.roles]: print('Cancelled travel.') await updated_user.remove_roles(user.guild.get_role(ccroles.cancel)) else: await asyncio.sleep(1) print('I waited 1 second.') await updated_user.edit(roles=[]) await updated_user.add_roles(user.guild.get_role(ccroles.location_2))
这个方法直接解决了角色检测的问题,但需要额外的API调用,且无法提前终止sleep,必须等延迟结束才能判断是否取消。
方案二:用asyncio.Task直接管理任务(更推荐)
用角色标记取消不够高效,更好的方式是直接管理异步任务,点击取消按钮时立刻终止对应的旅行任务。这种方式不需要依赖角色,还能防止用户重复触发旅行。
修改后的完整代码:
import asyncio import discord from discord.ext import commands import data.roles as ccroles class CancelView(discord.ui.View): def __init__(self, user_id, cog): super().__init__(timeout=None) self.user_id = user_id self.cog = cog @discord.ui.button(label="Cancel", custom_id="cancel", style=discord.ButtonStyle.red) async def buttoncancel(self, interaction, button): if interaction.user.id != self.user_id: await interaction.response.send_message("你无权取消他人的旅行!", ephemeral=True) return # 取消并清理用户的旅行任务 if self.user_id in self.cog.travel_tasks: self.cog.travel_tasks[self.user_id].cancel() del self.cog.travel_tasks[self.user_id] await interaction.response.send_message("旅行已取消!", ephemeral=True) else: await interaction.response.send_message("没有正在进行的旅行!", ephemeral=True) class MenuView(discord.ui.View): def __init__(self, cog): super().__init__(timeout=None) self.cog = cog @discord.ui.button(label="Location 2", custom_id="location2", style=discord.ButtonStyle.blurple) async def button1(self, interaction, button): user = interaction.user # 检查用户当前位置 if ccroles.location_1 not in [y.id for y in user.roles]: await interaction.response.send_message("你当前不在Location 1,无法前往Location 2!", ephemeral=True) return # 防止重复触发旅行 if user.id in self.cog.travel_tasks: await interaction.response.send_message("你已经在旅行中了!", ephemeral=True) return await interaction.response.send_message("正在前往Location 2...", ephemeral=True, view=CancelView(user.id, self.cog)) # 定义旅行任务逻辑 async def travel_task(): try: await asyncio.sleep(10) # 执行地点切换 await user.edit(roles=[]) await user.add_roles(user.guild.get_role(ccroles.location_2)) await interaction.followup.send("已到达Location 2!", ephemeral=True) except asyncio.CancelledError: # 任务被取消时触发 await interaction.followup.send("旅行已取消!", ephemeral=True) finally: # 无论完成还是取消,都清理任务记录 if user.id in self.cog.travel_tasks: del self.cog.travel_tasks[user.id] # 创建并存储任务 task = asyncio.create_task(travel_task()) self.cog.travel_tasks[user.id] = task class exampleMenu(commands.Cog): def __init__(self, client: commands.Bot): self.client = client # 用字典维护每个用户的旅行任务 self.travel_tasks = {} @commands.command() async def exampleMenu(self, ctx): embed = discord.Embed(title="Travel Menu", description="你想去哪里旅行?") await ctx.send(embed=embed, view=MenuView(self)) async def setup(client:commands.Bot) -> None: await client.add_cog(exampleMenu(client))
这个方案的优势:
- 无需依赖角色标记,直接通过异步任务管理,响应更及时
- 可以防止用户重复触发旅行任务
- 取消操作会立刻终止
sleep,不需要等待延迟结束
内容的提问来源于stack exchange,提问作者Alexander Primm
相关产品推荐
相关产品推荐

