Make `is_user_nitro_like` an async function to fetch full user objects, enabling banner checks for Nitro detection. Pass the bot instance to the function for this purpose.
576 lines
28 KiB
Python
576 lines
28 KiB
Python
import discord
|
|
from discord.ext import commands, tasks
|
|
from discord import app_commands, ui
|
|
import datetime
|
|
import asyncio
|
|
import random
|
|
import re # For parsing duration
|
|
import json
|
|
import os
|
|
import aiofiles # Import aiofiles
|
|
import aiofiles.os
|
|
|
|
GIVEAWAY_DATA_FILE = "data/giveaways.json"
|
|
DATA_DIR = "data"
|
|
|
|
# --- Helper Functions ---
|
|
async def is_user_nitro_like(user: discord.User | discord.Member, bot: commands.Bot = None) -> bool:
|
|
"""Checks if a user has an animated avatar or a banner, indicating Nitro."""
|
|
# Fetch the full user object to get banner information
|
|
if bot:
|
|
try:
|
|
fetched_user = await bot.fetch_user(user.id)
|
|
user = fetched_user
|
|
except discord.NotFound:
|
|
pass # Use the original user object if fetch fails
|
|
|
|
if isinstance(user, discord.Member): # Member object has guild-specific avatar
|
|
# Check guild avatar first, then global avatar
|
|
if user.guild_avatar and user.guild_avatar.is_animated():
|
|
return True
|
|
if user.avatar and user.avatar.is_animated():
|
|
return True
|
|
elif user.avatar and user.avatar.is_animated(): # User object
|
|
return True
|
|
return user.banner is not None
|
|
|
|
|
|
# --- UI Views and Buttons ---
|
|
class GiveawayEnterButton(ui.Button['GiveawayEnterView']):
|
|
def __init__(self, cog_ref):
|
|
super().__init__(label="Enter Giveaway", style=discord.ButtonStyle.green, custom_id="giveaway_enter_button")
|
|
self.cog: GiveawaysCog = cog_ref # Store a reference to the cog
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
giveaway = self.cog._get_giveaway_by_message_id(interaction.message.id)
|
|
if not giveaway or giveaway.get("ended", False):
|
|
await interaction.response.send_message("This giveaway has ended or is no longer active.", ephemeral=True)
|
|
# Optionally disable the button on the message if possible
|
|
self.disabled = True
|
|
await interaction.message.edit(view=self.view)
|
|
return
|
|
|
|
if giveaway["is_nitro_giveaway"]:
|
|
if not await is_user_nitro_like(interaction.user, bot=self.cog.bot):
|
|
await interaction.response.send_message(
|
|
"This is a Nitro-exclusive giveaway. You don't appear to have Nitro (animated avatar or banner).",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
if interaction.user.id in giveaway["participants"]:
|
|
await interaction.response.send_message("You have already entered this giveaway!", ephemeral=True)
|
|
else:
|
|
giveaway["participants"].add(interaction.user.id)
|
|
await self.cog.save_giveaways() # Save after participant update
|
|
await interaction.response.send_message("You have successfully entered the giveaway!", ephemeral=True)
|
|
|
|
# Update participant count in embed if desired (optional)
|
|
# embed = interaction.message.embeds[0]
|
|
# embed.set_field_at(embed.fields.index(...) or create new field, name="Participants", value=str(len(giveaway["participants"])))
|
|
# await interaction.message.edit(embed=embed)
|
|
|
|
|
|
class GiveawayEnterView(ui.View):
|
|
def __init__(self, cog: 'GiveawaysCog', timeout=None): # Timeout=None for persistent
|
|
super().__init__(timeout=timeout)
|
|
self.cog = cog
|
|
self.add_item(GiveawayEnterButton(cog_ref=self.cog))
|
|
|
|
class GiveawayRerollButton(ui.Button['GiveawayEndView']):
|
|
def __init__(self, cog_ref, original_giveaway_message_id: int):
|
|
super().__init__(label="Reroll Winner", style=discord.ButtonStyle.blurple, custom_id=f"giveaway_reroll_button:{original_giveaway_message_id}")
|
|
self.cog: GiveawaysCog = cog_ref
|
|
self.original_giveaway_message_id = original_giveaway_message_id
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# For reroll, we need to find the *original* giveaway data, which might not be in active_giveaways anymore.
|
|
# We'll need to load it from the JSON file or have a separate store for ended giveaways.
|
|
# For simplicity now, let's assume we can find it or it's passed appropriately.
|
|
|
|
# This custom_id parsing is a common pattern for persistent buttons with dynamic data.
|
|
# msg_id_str = interaction.data["custom_id"].split(":")[1]
|
|
# original_msg_id = int(msg_id_str)
|
|
|
|
# Find the giveaway data (this might need adjustment based on how ended giveaways are stored)
|
|
# We'll search all giveaways loaded, including those marked "ended"
|
|
giveaway_data = self.cog._get_giveaway_by_message_id(self.original_giveaway_message_id, search_all=True)
|
|
|
|
if not giveaway_data:
|
|
await interaction.response.send_message("Could not find the data for this giveaway to reroll.", ephemeral=True)
|
|
return
|
|
|
|
if not interaction.user.guild_permissions.manage_guild:
|
|
await interaction.response.send_message("You don't have permission to reroll winners.", ephemeral=True)
|
|
return
|
|
|
|
await interaction.response.defer(ephemeral=True) # Acknowledge
|
|
|
|
participants_ids = list(giveaway_data.get("participants", []))
|
|
if not participants_ids:
|
|
await interaction.followup.send("There were no participants in this giveaway to reroll from.", ephemeral=True)
|
|
return
|
|
|
|
# Fetch user objects for participants
|
|
entrants_users = []
|
|
for user_id in participants_ids:
|
|
user = interaction.guild.get_member(user_id)
|
|
if not user: # Try fetching if not in cache or left server
|
|
try:
|
|
user = await self.cog.bot.fetch_user(user_id)
|
|
except discord.NotFound:
|
|
continue # Skip if user cannot be found
|
|
if user and not user.bot:
|
|
# Apply Nitro check again if it was a nitro giveaway
|
|
if giveaway_data.get("is_nitro_giveaway", False) and not is_user_nitro_like(user):
|
|
continue
|
|
entrants_users.append(user)
|
|
|
|
if not entrants_users:
|
|
await interaction.followup.send("No eligible participants found for a reroll (e.g., after Nitro check or if users left).", ephemeral=True)
|
|
return
|
|
|
|
num_winners = giveaway_data.get("num_winners", 1)
|
|
new_winners_list = []
|
|
if len(entrants_users) <= num_winners:
|
|
new_winners_list = list(entrants_users)
|
|
else:
|
|
new_winners_list = random.sample(entrants_users, num_winners)
|
|
|
|
if new_winners_list:
|
|
winner_mentions = ", ".join(w.mention for w in new_winners_list)
|
|
# Announce in the original giveaway channel
|
|
original_channel = self.cog.bot.get_channel(giveaway_data["channel_id"])
|
|
if original_channel:
|
|
await original_channel.send(f"🔄 Reroll for **{giveaway_data['prize']}**! Congratulations {winner_mentions}, you are the new winner(s)!")
|
|
await interaction.followup.send(f"Reroll successful. New winner(s) announced in {original_channel.mention}.", ephemeral=True)
|
|
else:
|
|
await interaction.followup.send("Reroll successful, but I couldn't find the original channel to announce.", ephemeral=True)
|
|
else:
|
|
await interaction.followup.send("Could not select any new winners in the reroll.", ephemeral=True)
|
|
|
|
|
|
class GiveawayEndView(ui.View):
|
|
def __init__(self, cog: 'GiveawaysCog', original_giveaway_message_id: int, timeout=None): # Timeout=None for persistent
|
|
super().__init__(timeout=timeout)
|
|
self.cog = cog
|
|
self.add_item(GiveawayRerollButton(cog_ref=self.cog, original_giveaway_message_id=original_giveaway_message_id))
|
|
|
|
|
|
class GiveawaysCog(commands.Cog, name="Giveaways"):
|
|
"""Cog for managing giveaways"""
|
|
|
|
gway = app_commands.Group(name="gway", description="Giveaway related commands")
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.active_giveaways = []
|
|
self.all_loaded_giveaways = [] # To keep ended ones for reroll lookup
|
|
# Structure:
|
|
# {
|
|
# "message_id": int,
|
|
# "channel_id": int,
|
|
# "guild_id": int,
|
|
# "prize": str,
|
|
# "end_time": datetime.datetime, # Stored as ISO string in JSON
|
|
# "num_winners": int,
|
|
# "creator_id": int,
|
|
# "participants": set(), # Store user_ids. Stored as list in JSON.
|
|
# "is_nitro_giveaway": bool,
|
|
# "ended": bool
|
|
# }
|
|
# Ensure data directory exists before loading/saving
|
|
asyncio.create_task(self._ensure_data_dir_exists()) # Run asynchronously
|
|
asyncio.create_task(self.load_giveaways()) # Run asynchronously
|
|
self.check_giveaways_loop.start()
|
|
# Persistent views are added in setup_hook
|
|
|
|
async def cog_load(self): # Changed from setup_hook to cog_load for better timing with bot ready
|
|
asyncio.create_task(self._restore_views())
|
|
|
|
async def _restore_views(self):
|
|
await self.bot.wait_until_ready()
|
|
print("Re-adding persistent giveaway views...")
|
|
temp_loaded_giveaways = [] # Use a temporary list for loading
|
|
try:
|
|
async with aiofiles.open(GIVEAWAY_DATA_FILE, mode='r') as f:
|
|
content = await f.read()
|
|
if not content: # Handle empty file case
|
|
giveaways_data_for_views = []
|
|
else:
|
|
giveaways_data_for_views = await self.bot.loop.run_in_executor(None, json.loads, content)
|
|
|
|
for gw_data in giveaways_data_for_views:
|
|
# We only need to re-add views for messages that should have them
|
|
is_ended = gw_data.get("ended", False)
|
|
# Check if end_time is in the past if "ended" flag isn't perfectly reliable
|
|
end_time_dt = datetime.datetime.fromisoformat(gw_data["end_time"])
|
|
if not is_ended and end_time_dt > datetime.datetime.now(datetime.timezone.utc):
|
|
# Active giveaway, re-add EnterView
|
|
self.bot.add_view(GiveawayEnterView(cog=self), message_id=gw_data["message_id"])
|
|
elif is_ended or end_time_dt <= datetime.datetime.now(datetime.timezone.utc):
|
|
# Ended giveaway, re-add EndView (with Reroll button)
|
|
self.bot.add_view(GiveawayEndView(cog=self, original_giveaway_message_id=gw_data["message_id"]), message_id=gw_data["message_id"])
|
|
temp_loaded_giveaways.append(gw_data) # Keep track for _get_giveaway_by_message_id
|
|
print(f"Attempted to re-add views for {len(temp_loaded_giveaways)} giveaways.")
|
|
except FileNotFoundError:
|
|
print("No giveaway data file found, skipping view re-adding.")
|
|
except json.JSONDecodeError:
|
|
print("Error decoding giveaway data file. Starting with no active giveaways.")
|
|
except Exception as e:
|
|
print(f"Error re-adding persistent views: {e}")
|
|
|
|
|
|
async def _ensure_data_dir_exists(self):
|
|
try:
|
|
await aiofiles.os.makedirs(DATA_DIR, exist_ok=True) # Use aiofiles.os for async mkdir, exist_ok handles if it already exists
|
|
except Exception as e:
|
|
print(f"Error ensuring data directory {DATA_DIR} exists: {e}")
|
|
|
|
def cog_unload(self):
|
|
self.check_giveaways_loop.cancel()
|
|
|
|
async def load_giveaways(self): # Make async
|
|
self.active_giveaways = []
|
|
self.all_loaded_giveaways = []
|
|
try:
|
|
async with aiofiles.open(GIVEAWAY_DATA_FILE, mode='r') as f:
|
|
content = await f.read()
|
|
if not content: # Handle empty file case
|
|
giveaways_data = []
|
|
else:
|
|
giveaways_data = await self.bot.loop.run_in_executor(None, json.loads, content)
|
|
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
for gw_data in giveaways_data:
|
|
gw_data["end_time"] = datetime.datetime.fromisoformat(gw_data["end_time"])
|
|
gw_data["participants"] = set(gw_data.get("participants", []))
|
|
gw_data.setdefault("is_nitro_giveaway", False)
|
|
gw_data.setdefault("ended", gw_data["end_time"] <= now) # Set ended if time has passed
|
|
|
|
self.all_loaded_giveaways.append(gw_data.copy()) # Store all for reroll lookup
|
|
|
|
if not gw_data["ended"]:
|
|
self.active_giveaways.append(gw_data)
|
|
|
|
print(f"Loaded {len(self.all_loaded_giveaways)} total giveaways ({len(self.active_giveaways)} active).")
|
|
except FileNotFoundError:
|
|
print("Giveaway data file not found. Starting with no active giveaways.")
|
|
except json.JSONDecodeError:
|
|
print("Error decoding giveaway data file. Starting with no active giveaways.")
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred loading giveaways: {e}")
|
|
|
|
async def save_giveaways(self): # Make async
|
|
# Save all giveaways (from self.all_loaded_giveaways or by merging active and ended)
|
|
# This ensures that "ended" status and participant lists are preserved.
|
|
try:
|
|
# Create a unified list to save, ensuring all giveaways are present
|
|
# and active_giveaways reflects the most current state for those not yet ended.
|
|
|
|
# Create a dictionary of active giveaways by message_id for quick updates
|
|
active_map = {gw['message_id']: gw for gw in self.active_giveaways}
|
|
|
|
giveaways_to_save = []
|
|
# Iterate through all_loaded_giveaways to maintain the full history
|
|
for gw_hist in self.all_loaded_giveaways:
|
|
# If this giveaway is also in active_map, it means it's still active
|
|
# or just ended in the current session. Use the version from active_map
|
|
# as it might have newer participant data before being marked ended.
|
|
if gw_hist['message_id'] in active_map:
|
|
current_version = active_map[gw_hist['message_id']]
|
|
saved_gw = current_version.copy()
|
|
else: # It's an older, ended giveaway not in the current active list
|
|
saved_gw = gw_hist.copy()
|
|
|
|
saved_gw["end_time"] = saved_gw["end_time"].isoformat()
|
|
saved_gw["participants"] = list(saved_gw["participants"])
|
|
giveaways_to_save.append(saved_gw)
|
|
|
|
# Add any brand new giveaways from self.active_giveaways not yet in self.all_loaded_giveaways
|
|
# This case should ideally be handled by adding to both lists upon creation.
|
|
# For robustness:
|
|
all_saved_ids = {gw['message_id'] for gw in giveaways_to_save}
|
|
for gw_active in self.active_giveaways:
|
|
if gw_active['message_id'] not in all_saved_ids:
|
|
new_gw_to_save = gw_active.copy()
|
|
new_gw_to_save["end_time"] = new_gw_to_save["end_time"].isoformat()
|
|
new_gw_to_save["participants"] = list(new_gw_to_save["participants"])
|
|
giveaways_to_save.append(new_gw_to_save)
|
|
# Also add to all_loaded_giveaways for next time
|
|
self.all_loaded_giveaways.append(gw_active.copy())
|
|
|
|
# Offload json.dumps to executor
|
|
json_string_to_save = await self.bot.loop.run_in_executor(None, json.dumps, giveaways_to_save, indent=4)
|
|
async with aiofiles.open(GIVEAWAY_DATA_FILE, mode='w') as f:
|
|
await f.write(json_string_to_save)
|
|
# print(f"Saved {len(giveaways_to_save)} giveaways to disk.")
|
|
except Exception as e:
|
|
print(f"Error saving giveaways: {e}")
|
|
|
|
def _get_giveaway_by_message_id(self, message_id: int, search_all: bool = False):
|
|
"""Helper to find a giveaway by its message ID."""
|
|
source_list = self.all_loaded_giveaways if search_all else self.active_giveaways
|
|
for giveaway in source_list:
|
|
if giveaway["message_id"] == message_id:
|
|
return giveaway
|
|
return None
|
|
|
|
def parse_duration(self, duration_str: str) -> datetime.timedelta | None:
|
|
"""Parses a duration string (e.g., "1d", "3h", "30m", "1w") into a timedelta."""
|
|
match = re.fullmatch(r"(\d+)([smhdw])", duration_str.lower())
|
|
if not match:
|
|
return None
|
|
|
|
value, unit = int(match.group(1)), match.group(2)
|
|
|
|
if unit == 's':
|
|
return datetime.timedelta(seconds=value)
|
|
elif unit == 'm':
|
|
return datetime.timedelta(minutes=value)
|
|
elif unit == 'h':
|
|
return datetime.timedelta(hours=value)
|
|
elif unit == 'd':
|
|
return datetime.timedelta(days=value)
|
|
elif unit == 'w':
|
|
return datetime.timedelta(weeks=value)
|
|
return None
|
|
|
|
@gway.command(name="create", description="Create a new giveaway.")
|
|
@app_commands.describe(
|
|
prize="What is the prize?",
|
|
duration="How long should the giveaway last? (e.g., 10m, 1h, 2d, 1w)",
|
|
winners="How many winners? (default: 1)",
|
|
nitro_giveaway="Is this a Nitro-only giveaway? (checks for animated avatar/banner)"
|
|
)
|
|
@app_commands.checks.has_permissions(manage_guild=True)
|
|
async def create_giveaway_slash(self, interaction: discord.Interaction, prize: str, duration: str, winners: int = 1, nitro_giveaway: bool = False):
|
|
"""Slash command to create a giveaway using buttons."""
|
|
parsed_duration = self.parse_duration(duration)
|
|
if not parsed_duration:
|
|
await interaction.response.send_message(
|
|
"Invalid duration format. Use s, m, h, d, w (e.g., 10m, 1h, 2d).",
|
|
ephemeral=True
|
|
)
|
|
return
|
|
|
|
if winners < 1:
|
|
await interaction.response.send_message("Number of winners must be at least 1.", ephemeral=True)
|
|
return
|
|
|
|
end_time = datetime.datetime.now(datetime.timezone.utc) + parsed_duration
|
|
|
|
embed = discord.Embed(
|
|
title=f"🎉 Giveaway: {prize} 🎉",
|
|
description=f"Click the button below to enter!\n"
|
|
f"Ends: {discord.utils.format_dt(end_time, style='R')} ({discord.utils.format_dt(end_time, style='F')})\n"
|
|
f"Winners: {winners}",
|
|
color=discord.Color.gold()
|
|
)
|
|
if nitro_giveaway:
|
|
embed.description += "\n*This is a Nitro-exclusive giveaway!*"
|
|
embed.set_footer(text=f"Giveaway started by {interaction.user.display_name}. Entries: 0") # Initial entry count
|
|
|
|
await interaction.response.send_message("Creating giveaway...", ephemeral=True)
|
|
|
|
view = GiveawayEnterView(cog=self)
|
|
giveaway_message = await interaction.channel.send(embed=embed, view=view)
|
|
|
|
giveaway_data = {
|
|
"message_id": giveaway_message.id,
|
|
"channel_id": interaction.channel.id,
|
|
"guild_id": interaction.guild.id,
|
|
"prize": prize,
|
|
"end_time": end_time,
|
|
"num_winners": winners,
|
|
"creator_id": interaction.user.id,
|
|
"participants": set(),
|
|
"is_nitro_giveaway": nitro_giveaway,
|
|
"ended": False
|
|
}
|
|
self.active_giveaways.append(giveaway_data)
|
|
self.all_loaded_giveaways.append(giveaway_data.copy()) # Also add to the comprehensive list
|
|
self.save_giveaways()
|
|
|
|
await interaction.followup.send(f"Giveaway for '{prize}' created successfully!", ephemeral=True)
|
|
|
|
|
|
@tasks.loop(seconds=30)
|
|
async def check_giveaways_loop(self):
|
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
giveaways_processed_in_this_run = False
|
|
|
|
# Iterate over a copy of active_giveaways for safe removal/modification
|
|
for giveaway_data in list(self.active_giveaways):
|
|
if giveaway_data["ended"] or now < giveaway_data["end_time"]:
|
|
continue # Skip already ended or not yet due
|
|
|
|
giveaways_processed_in_this_run = True
|
|
giveaway_data["ended"] = True # Mark as ended
|
|
|
|
channel = self.bot.get_channel(giveaway_data["channel_id"])
|
|
if not channel:
|
|
print(f"Error: Could not find channel {giveaway_data['channel_id']} for giveaway {giveaway_data['message_id']}")
|
|
# Remove from active_giveaways directly as it can't be processed
|
|
self.active_giveaways = [gw for gw in self.active_giveaways if gw["message_id"] != giveaway_data["message_id"]]
|
|
continue
|
|
|
|
try:
|
|
message = await channel.fetch_message(giveaway_data["message_id"])
|
|
except discord.NotFound:
|
|
print(f"Error: Could not find message {giveaway_data['message_id']} in channel {channel.id}")
|
|
self.active_giveaways = [gw for gw in self.active_giveaways if gw["message_id"] != giveaway_data["message_id"]]
|
|
continue
|
|
except discord.Forbidden:
|
|
print(f"Error: Bot lacks permissions to fetch message {giveaway_data['message_id']} in channel {channel.id}")
|
|
# Cannot process, but keep it in active_giveaways for now, maybe perms will be fixed.
|
|
# Or decide to remove it. For now, skip.
|
|
continue
|
|
|
|
# Fetch participants from the giveaway data
|
|
entrants_users = []
|
|
for user_id in giveaway_data["participants"]:
|
|
# Ensure user is still in the guild for Nitro check if applicable
|
|
member = channel.guild.get_member(user_id) # Use guild from channel
|
|
user_to_check = member if member else await self.bot.fetch_user(user_id)
|
|
|
|
if not user_to_check: continue # User not found
|
|
|
|
if user_to_check.bot: continue
|
|
|
|
if giveaway_data["is_nitro_giveaway"] and not is_user_nitro_like(user_to_check):
|
|
continue # Skip non-nitro users for nitro giveaways
|
|
entrants_users.append(user_to_check)
|
|
|
|
winners_list = []
|
|
if entrants_users:
|
|
if len(entrants_users) <= giveaway_data["num_winners"]:
|
|
winners_list = list(entrants_users)
|
|
else:
|
|
winners_list = random.sample(entrants_users, giveaway_data["num_winners"])
|
|
|
|
winner_mentions_str = ", ".join(w.mention for w in winners_list) if winners_list else 'None'
|
|
|
|
if winners_list:
|
|
await channel.send(f"Congratulations {winner_mentions_str}! You won **{giveaway_data['prize']}**!")
|
|
else:
|
|
await channel.send(f"The giveaway for **{giveaway_data['prize']}** has ended, but there were no eligible participants.")
|
|
|
|
new_embed = message.embeds[0]
|
|
new_embed.description = f"Giveaway ended!\nWinners: {winner_mentions_str}"
|
|
new_embed.color = discord.Color.dark_grey()
|
|
new_embed.set_footer(text="Giveaway has concluded.")
|
|
|
|
end_view = GiveawayEndView(cog=self, original_giveaway_message_id=giveaway_data["message_id"])
|
|
|
|
try:
|
|
await message.edit(embed=new_embed, view=end_view)
|
|
except discord.Forbidden:
|
|
print(f"Error: Bot lacks permissions to edit message for {giveaway_data['message_id']}")
|
|
except discord.HTTPException as e:
|
|
print(f"Error editing giveaway message {giveaway_data['message_id']}: {e}")
|
|
|
|
# Remove from active_giveaways after processing
|
|
self.active_giveaways = [gw for gw in self.active_giveaways if gw["message_id"] != giveaway_data["message_id"]]
|
|
|
|
if giveaways_processed_in_this_run:
|
|
self.save_giveaways()
|
|
|
|
@check_giveaways_loop.before_loop
|
|
async def before_check_giveaways_loop(self):
|
|
await self.bot.wait_until_ready()
|
|
|
|
@gway.command(name="rollmanual", description="Manually roll a winner from a message (for old giveaways or specific cases).")
|
|
@app_commands.describe(
|
|
message_id="The ID of the message (giveaway or any message with reactions).",
|
|
winners="How many winners to pick? (default: 1)",
|
|
emoji="Emoji for reaction-based roll (if not a button giveaway, default: 🎉)"
|
|
)
|
|
@app_commands.checks.has_permissions(manage_guild=True)
|
|
async def manual_roll_giveaway_slash(self, interaction: discord.Interaction, message_id: str, winners: int = 1, emoji: str = "🎉"):
|
|
if winners < 1:
|
|
await interaction.response.send_message("Number of winners must be at least 1.", ephemeral=True)
|
|
return
|
|
|
|
try:
|
|
msg_id = int(message_id)
|
|
except ValueError:
|
|
await interaction.response.send_message("Invalid Message ID format. It should be a number.", ephemeral=True)
|
|
return
|
|
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
# Try to find if this message_id corresponds to a known giveaway
|
|
giveaway_info = self._get_giveaway_by_message_id(msg_id, search_all=True)
|
|
entrants = set() # Store user objects
|
|
|
|
message_to_roll = None
|
|
try:
|
|
message_to_roll = await interaction.channel.fetch_message(msg_id)
|
|
except (discord.NotFound, discord.Forbidden):
|
|
# Try searching all channels if not found or no perms in current
|
|
for chan in interaction.guild.text_channels:
|
|
try:
|
|
message_to_roll = await chan.fetch_message(msg_id)
|
|
if message_to_roll: break
|
|
except (discord.NotFound, discord.Forbidden):
|
|
continue
|
|
|
|
if not message_to_roll:
|
|
await interaction.followup.send(f"Could not find message with ID `{msg_id}` in this server or I lack permissions.", ephemeral=True)
|
|
return
|
|
|
|
if giveaway_info and "participants" in giveaway_info:
|
|
# Use stored participants if available (from button-based system)
|
|
for user_id in giveaway_info["participants"]:
|
|
user = interaction.guild.get_member(user_id) or await self.bot.fetch_user(user_id)
|
|
if user and not user.bot:
|
|
if giveaway_info.get("is_nitro_giveaway", False) and not is_user_nitro_like(user):
|
|
continue
|
|
entrants.add(user)
|
|
if not entrants:
|
|
await interaction.followup.send(f"Found giveaway data for message `{msg_id}`, but no eligible stored participants.", ephemeral=True)
|
|
return
|
|
else:
|
|
# Fallback to reactions if no participant data or not a known giveaway
|
|
reaction_found = False
|
|
for reaction in message_to_roll.reactions:
|
|
if str(reaction.emoji) == emoji:
|
|
reaction_found = True
|
|
async for user in reaction.users():
|
|
if not user.bot:
|
|
# For manual reaction roll, we might not know if it was nitro_giveaway
|
|
# Consider adding a parameter to manual_roll for this if needed
|
|
entrants.add(user)
|
|
break
|
|
if not reaction_found:
|
|
await interaction.followup.send(f"No reactions found with {emoji} on message `{msg_id}`.", ephemeral=True)
|
|
return
|
|
if not entrants:
|
|
await interaction.followup.send(f"No valid (non-bot) users reacted with {emoji} on message `{msg_id}`.", ephemeral=True)
|
|
return
|
|
|
|
winners_list = []
|
|
entrants_list = list(entrants)
|
|
if len(entrants_list) <= winners:
|
|
winners_list = entrants_list
|
|
else:
|
|
winners_list = random.sample(entrants_list, winners)
|
|
|
|
if winners_list:
|
|
winner_mentions = ", ".join(w.mention for w in winners_list)
|
|
await interaction.followup.send(f"Manual roll from message `{msg_id}` in {message_to_roll.channel.mention}:\nCongratulations {winner_mentions}!", ephemeral=False)
|
|
if interaction.channel.id != message_to_roll.channel.id:
|
|
try:
|
|
await message_to_roll.channel.send(f"Manual roll for message {message_to_roll.jump_url} concluded. Winner(s): {winner_mentions}")
|
|
except discord.Forbidden:
|
|
await interaction.followup.send(f"(Note: I couldn't announce the winner in {message_to_roll.channel.mention}.)", ephemeral=True)
|
|
else:
|
|
await interaction.followup.send(f"Could not select any winners from message `{msg_id}`.", ephemeral=True)
|
|
|
|
|
|
async def setup(bot: commands.Bot):
|
|
cog = GiveawaysCog(bot)
|
|
await bot.add_cog(cog)
|
|
# The cog_load method will handle re-adding views once the bot is ready.
|