Discord.py 抽奖结束命令实现技术求助
Hey there! Let's figure out how to implement that end command for your giveaway bot. The main issue with your current setup is that the asyncio.sleep(timer) call in the start command is blocking, and there's no way to interrupt it early. Here's a step-by-step solution using asyncio Tasks and expanded database storage to fix this:
1. Update Your Database Schema
First, your storingStuff table needs more fields to store all critical giveaway details (since we'll need to access them independently in the end command). Run these SQL statements once to modify your table:
ALTER TABLE storingStuff ADD COLUMN prize TEXT; ALTER TABLE storingStuff ADD COLUMN winners_count INTEGER; ALTER TABLE storingStuff ADD COLUMN channel_id INTEGER; ALTER TABLE storingStuff ADD COLUMN end_timestamp INTEGER;
2. Track Giveaway Tasks in Your Bot
Add a dictionary to your main Bot instance to map giveaway message IDs to their running asyncio Tasks. This lets us cancel the sleep/end process early:
# In your main bot file (e.g., main.py) client = commands.Bot(command_prefix='!', intents=discord.Intents.all()) client.giveaway_tasks = {} # Key: giveaway message ID, Value: asyncio.Task
3. Modify the Start Command to Use Tasks
We'll split the giveaway logic into two parts: starting the giveaway, and a separate async function that handles the sleep and final winner selection. This function will be wrapped in a Task so we can cancel it later.
Here's the updated Start cog code:
import asyncio import time import random import datetime import discord from discord.ext import commands import aiosqlite class Start(commands.Cog): def __init__(self, client): self.client = client def convert(self, timer): pos = ["s", "m", "h", "d"] time_dict = {"s" : 1, "m" : 60, "h" : 3600, "d" : 3600*24} unit = timer[-1] if unit not in pos: return -1 try: val = int(timer[:-1]) except: return -2 return val * time_dict[unit] async def end_giveaway(self, ctx, gaw_msg_id, prize, winners_count, end_timestamp): try: # Wait until the giveaway ends (or get cancelled early) await asyncio.sleep(end_timestamp - time.time()) except asyncio.CancelledError: # This triggers when the end command cancels the task pass # Fetch the giveaway message try: gaw_msg = await ctx.channel.fetch_message(gaw_msg_id) except discord.NotFound: return # Message was deleted, nothing to do # Get valid participants (exclude bots) users = await gaw_msg.reactions[0].users().flatten() users = [user for user in users if not user.bot] # Select winners users_mention = [] if len(users) >= winners_count: winners = random.sample(users, winners_count) users_mention = [winner.mention for winner in winners] else: users_mention = ["No valid participants"] displayed_winners = ",".join(users_mention) endembed = discord.Embed( title=f"{prize}", description=f"Winner: {displayed_winners}\nHosted by: {ctx.author.mention}", color=ctx.author.color, timestamp=datetime.datetime.utcfromtimestamp(end_timestamp) ) endembed.set_footer(text='Ended at \u200b') await gaw_msg.edit( content="<:done:939940228746072096> **GIVEAWAY ENDED** <:done:939940228746072096>", embed=endembed ) await ctx.send(f"Congratulations {displayed_winners}! You won the **{prize}**.\n{gaw_msg.jump_url}") # Clean up: remove task and database entry if gaw_msg_id in self.client.giveaway_tasks: del self.client.giveaway_tasks[gaw_msg_id] async with aiosqlite.connect("main.db") as db: await db.execute(f'DELETE FROM storingStuff WHERE msgID = {gaw_msg_id}') await db.commit() @commands.command() async def start(self, ctx, duration, winners: str, *, prize): timer = self.convert(duration) # Validate input if timer == -1: await ctx.send("Invalid time unit! Use `s` (seconds), `m` (minutes), `h` (hours), or `d` (days).") return if timer == -2: await ctx.send("Invalid time value! Enter a number followed by a unit (e.g., `10m`).") return try: winners_count = int(winners.replace("w","")) if winners_count <= 0: await ctx.send("Number of winners must be at least 1!") return except ValueError: await ctx.send("Invalid winners format! Use something like `1w` or `3w`.") return await ctx.message.delete() end_timestamp = int(time.time() + timer) # Create giveaway embed embed = discord.Embed( title=f"{prize}", description=f'React with 🎉 to enter\nEnds: <t:{end_timestamp}:R> (<t:{end_timestamp}>)\nHosted by {ctx.author.mention}\n', color=ctx.author.color, timestamp=datetime.datetime.utcfromtimestamp(end_timestamp) ) embed.set_footer(text=f'Winners : {winners_count} | Ends at \u200b') gaw_msg = await ctx.send( content="<:spadess:939938117736091678> **GIVEAWAY** <:spadess:939938117736091678>", embed=embed ) await gaw_msg.add_reaction("🎉") # Store giveaway details in database async with aiosqlite.connect("main.db") as db: await db.execute(''' INSERT INTO storingStuff (msgID, guildID, prize, winners_count, channel_id, end_timestamp) VALUES (?, ?, ?, ?, ?, ?) ''', (gaw_msg.id, ctx.guild.id, prize, winners_count, ctx.channel.id, end_timestamp)) await db.commit() # Start the giveaway end task and store it task = asyncio.create_task( self.end_giveaway(ctx, gaw_msg.id, prize, winners_count, end_timestamp) ) self.client.giveaway_tasks[gaw_msg.id] = task def setup(client): client.add_cog(Start(client))
4. Implement the End Command
Create a new Cog to handle ending giveaways early. This command will fetch the giveaway details from the database, cancel the running task, and trigger the winner selection process:
import discord from discord.ext import commands import aiosqlite class End(commands.Cog): def __init__(self, client): self.client = client @commands.command() async def end(self, ctx, msg_id: int): # Fetch giveaway details from database async with aiosqlite.connect("main.db") as db: cursor = await db.execute(''' SELECT prize, winners_count, channel_id, end_timestamp, guildID FROM storingStuff WHERE msgID = ? ''', (msg_id,)) data = await cursor.fetchone() if not data: await ctx.send("No giveaway found with that message ID!") return prize, winners_count, channel_id, end_timestamp, guild_id = data # Verify user is in the same guild as the giveaway if ctx.guild.id != guild_id: await ctx.send("You can only end giveaways hosted in this server!") return # Fetch the giveaway channel and message try: channel = self.client.get_channel(channel_id) or await self.client.fetch_channel(channel_id) gaw_msg = await channel.fetch_message(msg_id) except discord.NotFound: await ctx.send("Giveaway message not found!") return # Check if giveaway is already ended if "GIVEAWAY ENDED" in gaw_msg.content: await ctx.send("This giveaway has already ended!") return # Cancel the running task if it exists if msg_id in self.client.giveaway_tasks: task = self.client.giveaway_tasks[msg_id] task.cancel() await ctx.send("Ending the giveaway early...") else: # If task doesn't exist (e.g., it finished already), trigger end logic manually await self.client.get_cog("Start").end_giveaway( ctx, msg_id, prize, winners_count, end_timestamp ) await ctx.send("Giveaway ended!") def setup(client): client.add_cog(End(client))
Key Explanations:
- Task Management: Using
asyncio.create_tasklets us run the giveaway end logic in the background. We can cancel this task at any time withtask.cancel(), which triggers theasyncio.CancelledErrorwe catch inend_giveaway. - Database Storage: Storing all giveaway details (prize, winners count, channel ID) lets the
endcommand operate independently without needing the originalstartcommand context. - Error Handling: Added checks for invalid inputs, missing messages, and already-ended giveaways to make the bot more robust.
To use this, just add both cogs to your bot, update your database schema, and test with commands like:
!start 10m 1w Free Nitro(starts a 10-minute giveaway for 1 winner)!end 123456789012345678(ends the giveaway with the specified message ID)
内容的提问来源于stack exchange,提问作者user18323078

