diff --git a/bot.py b/bot.py index 6c15dd8..c5eecaf 100644 --- a/bot.py +++ b/bot.py @@ -2090,6 +2090,350 @@ async def updategiveawaymessage(ctx, giveaway_id: str): logger.error(f"Error updating giveaway message: {e}") await ctx.send(f"❌ Error updating giveaway message: {str(e)}") +@client.hybrid_command() +async def endgiveaway(ctx, giveaway_id: str, force: bool = False): + """Manually end a giveaway and pick winners (Only available for admins) + + Parameters: + - giveaway_id: The giveaway ID (first 8 characters of UUID are enough) + - force: Force end even if not expired (default: False) + """ + guild_id = ctx.guild.id + user_data = load_user_data_sync(ctx.author.id, guild_id) + if user_data["permission"] < 5: + await ctx.send("You don't have permission to end giveaways.") + return + + # Find giveaway by partial ID (check both memory and database) + matching_giveaway = None + matching_id = None + source = "memory" + + # First check memory + for giv_id, giveaway in giveaways.items(): + if giv_id.startswith(giveaway_id) or giveaway_id in giv_id: + matching_giveaway = giveaway + matching_id = giv_id + break + + # If not in memory, check database + if not matching_giveaway: + active_processes = get_active_processes(process_type="giveaway", guild_id=guild_id) + + for process in active_processes: + if str(process["uuid"]).startswith(giveaway_id) or giveaway_id in str(process["uuid"]): + # Load giveaway from database + try: + guild = ctx.guild + channel = guild.get_channel(process["channel_id"]) + + if not channel: + await ctx.send(f"❌ Original channel not found for giveaway `{giveaway_id}`") + return + + # Create minimal context + class MinimalContext: + def __init__(self, channel): + self.channel = channel + self.guild = channel.guild + + temp_ctx = MinimalContext(channel) + matching_giveaway = Giveaway.from_process_data(temp_ctx, process["data"]) + matching_giveaway.end_time = process["end_time"] + matching_giveaway.process_uuid = process["uuid"] + + # Restore participants + stored_participants = process["data"].get("participants", []) + for participant_data in stored_participants: + try: + user = await client.fetch_user(participant_data["id"]) + matching_giveaway.participants.append(user) + except: + pass + + matching_id = str(process["uuid"]) + source = "database" + break + + except Exception as e: + logger.error(f"Error loading giveaway from database: {e}") + await ctx.send(f"❌ Error loading giveaway: {str(e)}") + return + + if not matching_giveaway: + await ctx.send(f"❌ No giveaway found with ID starting with `{giveaway_id}`") + return + + # Check if giveaway can be ended + is_expired = matching_giveaway.is_finished() + + if not is_expired and not force: + remaining = matching_giveaway.end_time - datetime.now() + embed = discord.Embed( + title="⚠️ Giveaway Not Expired", + description=f"This giveaway is still active and not expired yet.", + color=0xffa500, + timestamp=datetime.now() + ) + embed.add_field(name="🆔 Giveaway ID", value=f"`{matching_id[:8]}...`", inline=True) + embed.add_field(name="⏰ Time Left", value=f"", inline=True) + embed.add_field(name="🔧 Force End", value="Use `/endgiveaway True` to force end", inline=False) + + await ctx.send(embed=embed) + return + + # Check if giveaway has participants + if len(matching_giveaway.participants) == 0: + embed = discord.Embed( + title="❌ No Participants", + description=f"This giveaway has no participants, so no winners can be selected.", + color=0xff0000, + timestamp=datetime.now() + ) + embed.add_field(name="🆔 Giveaway ID", value=f"`{matching_id[:8]}...`", inline=True) + embed.add_field(name="👥 Participants", value="0", inline=True) + + # Still mark as completed + matching_giveaway.complete_giveaway() + if source == "memory" and matching_id in giveaways: + del giveaways[matching_id] + + await ctx.send(embed=embed) + return + + try: + # Pick winners + winners = matching_giveaway.pick_winners() + + if not winners: + await ctx.send(f"❌ Could not pick winners from {len(matching_giveaway.participants)} participants.") + return + + # Create winner announcement + winner_embed = discord.Embed( + title="🎉 Giveaway Ended - Winners Announced!", + description=f"**{matching_giveaway.title}** has been manually ended!", + color=0xFFD700, + timestamp=datetime.now() + ) + + # Enhanced prize display with game info + if hasattr(matching_giveaway, 'game_info') and matching_giveaway.game_info and matching_giveaway.game_info.get('name'): + if hasattr(matching_giveaway, 'game_url') and matching_giveaway.game_url: + prize_text = f"**[{matching_giveaway.game_info['name']}]({matching_giveaway.game_url})**" + else: + prize_text = f"**{matching_giveaway.game_info['name']}**" + else: + if hasattr(matching_giveaway, 'game_url') and matching_giveaway.game_url: + prize_text = f"**[{matching_giveaway.prize}]({matching_giveaway.game_url})**" + else: + prize_text = f"**{matching_giveaway.prize}**" + + winner_embed.add_field(name="🎁 Prize", value=prize_text, inline=True) + winner_embed.add_field(name="🎮 Platform", value=f"**{matching_giveaway.platform}**", inline=True) + winner_embed.add_field(name="👥 Total Participants", value=f"**{len(matching_giveaway.participants)}**", inline=True) + + # List winners + winner_list = [] + for i, winner in enumerate(winners, 1): + winner_list.append(f"🏆 **#{i}** {winner.mention}") + + winner_embed.add_field(name="🎊 Winners", value="\n".join(winner_list), inline=False) + + # Add sponsor info if available + if hasattr(matching_giveaway, 'sponsor') and matching_giveaway.sponsor: + winner_embed.add_field(name="💝 Sponsored by", value=f"**{matching_giveaway.sponsor}**", inline=False) + + # Add manual end notice + end_type = "🔧 Manually ended" if not is_expired else "⏰ Expired (manually processed)" + winner_embed.add_field(name="📋 Status", value=f"{end_type} by {ctx.author.mention}", inline=False) + + winner_embed.add_field(name="📧 Next Steps", + value="Winners have been sent a DM with prize claim instructions!", + inline=False) + + # Set game image if available + if hasattr(matching_giveaway, 'game_info') and matching_giveaway.game_info and matching_giveaway.game_info.get('image_url'): + winner_embed.set_image(url=matching_giveaway.game_info['image_url']) + winner_embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png") + else: + winner_embed.set_thumbnail(url="https://cdn.discordapp.com/emojis/1028701098145587302.png") + + winner_embed.set_footer(text="Congratulations to all winners! 🎉") + + await ctx.send(embed=winner_embed) + + # Process winners with enhanced DM + for i, winner in enumerate(winners): + try: + if i < len(matching_giveaway.winner_uuids): + winner_uuid = matching_giveaway.winner_uuids[i] + assign_winner_to_uuid(winner_uuid, winner.id) + + # Enhanced winner DM + dm_embed = discord.Embed( + title="🎉 Congratulations! You Won!", + description=f"You are a winner in the **{matching_giveaway.title}** giveaway!", + color=0xFFD700, + timestamp=datetime.now() + ) + + dm_embed.add_field(name="🎁 Prize", value=prize_text, inline=True) + dm_embed.add_field(name="🎮 Platform", value=f"**{matching_giveaway.platform}**", inline=True) + dm_embed.add_field(name="🏆 Position", value=f"Winner #{i+1}", inline=True) + + dm_embed.add_field(name="📋 Prize Claim Instructions", + value="Please contact the server administrators to claim your prize. " + "Make sure to mention this giveaway and your winner position!", + inline=False) + + if hasattr(matching_giveaway, 'sponsor') and matching_giveaway.sponsor: + dm_embed.add_field(name="💝 Sponsored by", value=f"**{matching_giveaway.sponsor}**", inline=False) + + dm_embed.set_footer(text=f"Server: {ctx.guild.name} • Giveaway ID: {matching_id[:8]}...") + + try: + await winner.send(embed=dm_embed) + logger.info(f"Sent winner DM to {winner.name} for giveaway {matching_id[:8]}") + except discord.Forbidden: + logger.warning(f"Could not send DM to winner {winner.name}") + # Notify in channel that DM failed + await ctx.send(f"⚠️ Could not send DM to {winner.mention}. Please contact them manually!") + + except Exception as e: + logger.error(f"Error processing winner {winner.name}: {e}") + + # Mark giveaway as completed and remove from memory + matching_giveaway.complete_giveaway() + if source == "memory" and matching_id in giveaways: + del giveaways[matching_id] + + logger.info(f"Manually ended giveaway {matching_id[:8]} by {ctx.author.id}, {len(winners)} winners selected") + + except Exception as e: + logger.error(f"Error ending giveaway: {e}") + await ctx.send(f"❌ Error ending giveaway: {str(e)}") + +@client.hybrid_command() +async def expiredgiveaways(ctx): + """List all expired giveaways that haven't been processed yet (Only available for admins)""" + guild_id = ctx.guild.id + user_data = load_user_data_sync(ctx.author.id, guild_id) + if user_data["permission"] < 5: + await ctx.send("You don't have permission to view expired giveaways.") + return + + expired_giveaways = [] + + # Check memory giveaways + for giv_id, giveaway in giveaways.items(): + if giveaway.guild_id == guild_id and giveaway.is_finished(): + expired_giveaways.append((giv_id, giveaway, "memory")) + + # Check database giveaways + try: + active_processes = get_active_processes(process_type="giveaway", guild_id=guild_id) + + for process in active_processes: + process_uuid = str(process["uuid"]) + + # Skip if already in memory + if process_uuid in giveaways: + continue + + # Check if expired + if process["end_time"] <= datetime.now(): + try: + # Create minimal giveaway object for display + class MinimalGiveaway: + def __init__(self, data, end_time, uuid): + self.title = data.get("title", "Unknown Giveaway") + self.prize = data.get("prize", "Unknown Prize") + self.platform = data.get("platform", "Unknown") + self.num_winners = data.get("num_winners", 1) + self.end_time = end_time + self.process_uuid = uuid + self.participants_count = len(data.get("participants", [])) + + temp_giveaway = MinimalGiveaway(process["data"], process["end_time"], process["uuid"]) + expired_giveaways.append((process_uuid, temp_giveaway, "database")) + + except Exception as e: + logger.error(f"Error processing expired giveaway {process_uuid}: {e}") + + except Exception as e: + logger.error(f"Error checking database for expired giveaways: {e}") + + if not expired_giveaways: + embed = discord.Embed( + title="✅ No Expired Giveaways", + description="There are currently no expired giveaways that need processing.", + color=0x00ff00 + ) + await ctx.send(embed=embed) + return + + # Create list embed + embed = discord.Embed( + title="⏰ Expired Giveaways", + description=f"Found **{len(expired_giveaways)}** expired giveaway(s) that need processing:", + color=0xff6b6b, + timestamp=datetime.now() + ) + + for giv_id, giveaway, source in expired_giveaways[:10]: # Limit to 10 + # Calculate how long expired + time_since_expired = datetime.now() - giveaway.end_time + days = time_since_expired.days + hours, remainder = divmod(time_since_expired.seconds, 3600) + + if days > 0: + expired_for = f"{days}d {hours}h ago" + elif hours > 0: + expired_for = f"{hours}h ago" + else: + minutes = remainder // 60 + expired_for = f"{minutes}m ago" + + # Source indicator + source_emoji = "🧠" if source == "memory" else "💾" + + # Participant count + if hasattr(giveaway, 'participants'): + participant_count = len(giveaway.participants) + else: + participant_count = getattr(giveaway, 'participants_count', 0) + + embed.add_field( + name=f"{source_emoji} {giveaway.title}", + value=( + f"**ID:** `{giv_id[:8]}...`\n" + f"**Prize:** {giveaway.prize}\n" + f"**Platform:** {giveaway.platform}\n" + f"**Winners:** {giveaway.num_winners}\n" + f"**Participants:** {participant_count}\n" + f"**Expired:** {expired_for}" + ), + inline=True + ) + + if len(expired_giveaways) > 10: + embed.add_field( + name="📋 Additional Info", + value=f"... and {len(expired_giveaways) - 10} more expired giveaways", + inline=False + ) + + embed.add_field( + name="🔧 How to Process", + value="Use `/endgiveaway ` to manually end and pick winners for any expired giveaway.", + inline=False + ) + + embed.set_footer(text="💾 = Database | 🧠 = Memory | Use /endgiveaway to process") + + await ctx.send(embed=embed) + @client.hybrid_command() async def listgiveaways(ctx): """List all active giveaways in this server (Only available for admins)""" @@ -3268,7 +3612,9 @@ async def modhelp(ctx): "`/processes [type]` - Manage active processes\n" "`/startgiveaway` - Create server giveaways with Steam/Epic integration\n" "`/editgiveaway ` - Edit active giveaways (auto-updates post)\n" + "`/endgiveaway [force]` - Manually end giveaway and pick winners\n" "`/listgiveaways` - List all active giveaways (memory + database)\n" + "`/expiredgiveaways` - Show expired giveaways that need processing\n" "`/loadgiveaway ` - Load specific giveaway from database\n" "`/loadallgiveaways` - Load all giveaways from database\n" "`/updategiveawaymessage ` - Manually refresh giveaway post\n"