""" Community cog: Polls, suggestions, and community engagement features. Enhanced with beautiful panels, dynamic option adding, and multi-language support. """ import asyncio import random import time from datetime import datetime, timedelta import discord from discord.ext import commands, tasks from bot.theme import ( NEON_CYAN, NEON_PINK, NEON_PURPLE, NEON_LIME, NEON_ORANGE, NEON_YELLOW, panel_divider, success_embed, error_embed, info_embed, warning_embed, progress_bar, shimmer, pick_neon_color ) from bot.i18n import get_cmd_desc from bot.emojis import ( ui, E_STAR, E_FIRE, E_SPARKLE, E_TROPHY, E_CROWN, E_GEM, E_DIAMOND ) # ═══════════════════════════════════════════════════════════════════════════════ # POLL EMBED COLORS # ═══════════════════════════════════════════════════════════════════════════════ POLL_COLORS = { "default": NEON_CYAN, "yes_no": NEON_LIME, "rating": NEON_YELLOW, "choice": NEON_PURPLE, "priority": NEON_ORANGE, "event": NEON_PINK, } # ═══════════════════════════════════════════════════════════════════════════════ # INTERACTIVE POLL BUILDER - DYNAMIC OPTION ADDING # ═══════════════════════════════════════════════════════════════════════════════ class PollBuilderView(discord.ui.View): """Interactive poll builder with dynamic option adding.""" def __init__(self, cog: "Community", author_id: int, lang: str = "en") -> None: super().__init__(timeout=None) self.cog = cog self.author_id = author_id self.lang = lang self.question: str = "" self.options: list[str] = [] self.duration: int = 0 self.allow_multiple: bool = False self.poll_type: str = "default" self.message: discord.Message | None = None async def interaction_check(self, interaction: discord.Interaction) -> bool: """Only allow the author to interact.""" if interaction.user.id != self.author_id: if self.lang == "ar": await interaction.response.send_message("❌ فقط صاحب التصويت يمكنه التحكم!", ephemeral=True) else: await interaction.response.send_message("❌ Only the poll creator can use this!", ephemeral=True) return False return True def _build_embed(self) -> discord.Embed: """Build the poll builder embed.""" if self.lang == "ar": title = "🗳️ منشئ التصويت التفاعلي" question_label = "❓ السؤال" options_label = "📋 الخيارات" duration_label = "⏱️ المدة" type_label = "📊 النوع" multiple_label = "🔀 خيارات متعددة" not_set = "غير محدد" minutes = "دقيقة" no_limit = "بدون حد" add_options_hint = "💡 أضف خيارات باستخدام الأزرار أدناه" else: title = "🗳️ Interactive Poll Builder" question_label = "❓ Question" options_label = "📋 Options" duration_label = "⏱️ Duration" type_label = "📊 Type" multiple_label = "🔀 Multiple Choice" not_set = "Not set" minutes = "minutes" no_limit = "No limit" add_options_hint = "💡 Add options using the buttons below" embed = discord.Embed( title=title, description=f"{panel_divider('cyan')}\n{add_options_hint}\n{panel_divider('cyan')}", color=NEON_CYAN ) # Question embed.add_field( name=question_label, value=f"**{self.question}**" if self.question else f"*{not_set}*", inline=False ) # Options if self.options: options_text = "\n".join(f"{i+1}️⃣ {opt}" for i, opt in enumerate(self.options[:10])) if len(self.options) > 10: options_text += f"\n*...and {len(self.options) - 10} more*" else: options_text = f"*{not_set} - click 'Add Option' to add*" embed.add_field(name=options_label, value=options_text, inline=False) # Duration duration_text = f"{self.duration} {minutes}" if self.duration > 0 else no_limit embed.add_field(name=duration_label, value=duration_text, inline=True) # Type type_names = { "default": "🗳️ Standard" if self.lang != "ar" else "🗳️ عادي", "yes_no": "✅ Yes/No" if self.lang != "ar" else "✅ نعم/لا", "rating": "✅ Rating" if self.lang != "ar" else "✅ تقييم", "event": "📅 Event" if self.lang != "ar" else "📅 حدث", } embed.add_field(name=type_label, value=type_names.get(self.poll_type, "Standard"), inline=True) # Multiple choice multiple_text = "✅ Yes" if self.lang != "ar" else "✅ نعم" single_text = "❌ No" if self.lang != "ar" else "❌ لا" embed.add_field(name=multiple_label, value=multiple_text if self.allow_multiple else single_text, inline=True) embed.set_footer(text="Click buttons below to customize your poll • 5 min timeout") return embed @discord.ui.button(label="Set Question", emoji=ui("question"), style=discord.ButtonStyle.success, row=0, custom_id="poll_set_question") async def set_question(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Set the poll question.""" await interaction.response.send_modal(QuestionModal(self)) @discord.ui.button(label="Add Option", emoji=ui("star"), style=discord.ButtonStyle.primary, row=0, custom_id="poll_add_option") async def add_option(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Add a new option.""" if len(self.options) >= 10: if self.lang == "ar": await interaction.response.send_message("⚠️ الحد الأقصى 10 خيارات!", ephemeral=True) else: await interaction.response.send_message("⚠️ Maximum 10 options allowed!", ephemeral=True) return await interaction.response.send_modal(AddOptionModal(self)) @discord.ui.button(label="Remove Option", emoji=ui("no"), style=discord.ButtonStyle.danger, row=0, custom_id="poll_remove_option") async def remove_option(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Remove the last option.""" if not self.options: if self.lang == "ar": await interaction.response.send_message("⚠️ لا توجد خيارات للحذف!", ephemeral=True) else: await interaction.response.send_message("⚠️ No options to remove!", ephemeral=True) return removed = self.options.pop() if self.message: await self.message.edit(embed=self._build_embed(), view=self) if self.lang == "ar": await interaction.response.send_message(f"🗑️ تم حذف: **{removed}**", ephemeral=True) else: await interaction.response.send_message(f"🗑️ Removed: **{removed}**", ephemeral=True) @discord.ui.button(label="Duration", emoji=ui("zap"), style=discord.ButtonStyle.secondary, row=1, custom_id="poll_duration") async def set_duration(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Set poll duration.""" await interaction.response.send_modal(DurationModal(self)) @discord.ui.button(label="Poll Type", emoji=ui("settings"), style=discord.ButtonStyle.secondary, row=1, custom_id="poll_type") async def set_type(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Cycle through poll types.""" types = ["default", "yes_no", "rating", "event"] current_idx = types.index(self.poll_type) if self.poll_type in types else 0 self.poll_type = types[(current_idx + 1) % len(types)] if self.message: await self.message.edit(embed=self._build_embed(), view=self) type_names = { "default": "🗳️ Standard", "yes_no": "✅ Yes/No", "rating": "✅ Rating", "event": "📅 Event", } await interaction.response.send_message( f"📊 Poll type changed to: **{type_names.get(self.poll_type, 'Standard')}**", ephemeral=True ) @discord.ui.button(label="Toggle Multiple", emoji=ui("shuffle"), style=discord.ButtonStyle.secondary, row=1, custom_id="poll_toggle_multiple") async def toggle_multiple(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Toggle multiple choice.""" self.allow_multiple = not self.allow_multiple if self.message: await self.message.edit(embed=self._build_embed(), view=self) status = "enabled" if self.allow_multiple else "disabled" if self.lang == "ar": await interaction.response.send_message( f"🔀 الخيارات المتعددة: **{'مفعلة' if self.allow_multiple else 'معطلة'}**", ephemeral=True ) else: await interaction.response.send_message( f"🔀 Multiple choice: **{status}**", ephemeral=True ) @discord.ui.button(label="Quick Options", emoji=ui("zap"), style=discord.ButtonStyle.blurple, row=2, custom_id="poll_quick_options") async def quick_options(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Add quick preset options.""" view = QuickOptionsView(self) await interaction.response.send_message( "⚡ **Quick Options** - Choose a preset:", view=view, ephemeral=True ) @discord.ui.button(label="Preview", emoji=ui("preview"), style=discord.ButtonStyle.secondary, row=2, custom_id="poll_preview") async def preview(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Preview the poll.""" if not self.question: if self.lang == "ar": await interaction.response.send_message("❌ الرجاء تحديد السؤال أولاً!", ephemeral=True) else: await interaction.response.send_message("❌ Please set a question first!", ephemeral=True) return if len(self.options) < 2: if self.lang == "ar": await interaction.response.send_message("❌ الرجاء إضافة خيارين على الأقل!", ephemeral=True) else: await interaction.response.send_message("❌ Please add at least 2 options!", ephemeral=True) return # Create preview embed preview_embed = self._build_preview_embed() await interaction.response.send_message("👁️ **Poll Preview:**", embed=preview_embed, ephemeral=True) def _build_preview_embed(self) -> discord.Embed: """Build preview embed.""" color = POLL_COLORS.get(self.poll_type, NEON_CYAN) embed = discord.Embed( title=f"🗳️ {self.question}", color=color ) for idx, option in enumerate(self.options[:10]): embed.add_field( name=f"{idx+1}️⃣ {option}", value=f"📊 `0` votes (0.0%)\n`{'░'*10}`", inline=False ) footer_text = "👥 0 voters • 0 votes" if self.duration > 0: footer_text += f" • Ends in {self.duration} min" if self.allow_multiple: footer_text += " • Multiple choice" embed.set_footer(text=footer_text) embed.timestamp = datetime.utcnow() return embed @discord.ui.button(label="Create Poll!", emoji=ui("ok"), style=discord.ButtonStyle.success, row=3, custom_id="poll_create") async def create_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Create the poll.""" if not self.question: if self.lang == "ar": await interaction.response.send_message("❌ الرجاء تحديد السؤال أولاً!", ephemeral=True) else: await interaction.response.send_message("❌ Please set a question first!", ephemeral=True) return if len(self.options) < 2: if self.lang == "ar": await interaction.response.send_message("❌ الرجاء إضافة خيارين على الأقل!", ephemeral=True) else: await interaction.response.send_message("❌ Please add at least 2 options!", ephemeral=True) return if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return await interaction.response.defer(ephemeral=True) # Calculate end time end_time = time.time() + (self.duration * 60) if self.duration > 0 else None # Create the actual poll self.cog.poll_counter += 1 poll_id = self.cog.poll_counter # Check for poll channel row = await self.cog.bot.db.fetchone( "SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", interaction.guild.id ) channel = interaction.guild.get_channel(row[0]) if (row and row[0]) else interaction.channel if not channel: channel = interaction.channel # Create poll view view = PollVoteView( poll_id=poll_id, question=self.question, options=self.options[:10], author_id=self.author_id, poll_type=self.poll_type, end_time=end_time, allow_multiple=self.allow_multiple, lang=self.lang ) # Send poll msg = await channel.send(embed=view._build_embed(), view=view) view.message = msg self.cog.active_polls[poll_id] = view # Schedule end if timed if end_time: self.cog.bot.loop.create_task(self.cog._end_poll_after(poll_id, end_time - time.time())) # Stop this view and edit the message self.stop() if self.message: try: await self.message.edit( content=f"✅ **Poll created in {channel.mention}!**", embed=None, view=None ) except: pass if self.lang == "ar": await interaction.followup.send( f"✅ تم إنشاء التصويت في {channel.mention}!\n" f"📊 **{len(self.options[:10])}** خيارات", ephemeral=True ) else: await interaction.followup.send( f"✅ Poll created in {channel.mention}!\n" f"📊 **{len(self.options[:10])}** options", ephemeral=True ) class QuestionModal(discord.ui.Modal): """Modal for setting poll question.""" question = discord.ui.TextInput( label="Poll Question", placeholder="What do you want to ask?", max_length=200, required=True ) def __init__(self, view: PollBuilderView) -> None: super().__init__(title="Set Poll Question", timeout=None) self.parent_view = view if view.question: self.question.default = view.question async def on_submit(self, interaction: discord.Interaction) -> None: self.parent_view.question = str(self.question.value).strip() if self.parent_view.message: await self.parent_view.message.edit(embed=self.parent_view._build_embed(), view=self.parent_view) await interaction.response.send_message(f"❓ Question set: **{self.parent_view.question}**", ephemeral=True) class AddOptionModal(discord.ui.Modal): """Modal for adding an option.""" option = discord.ui.TextInput( label="Option Text", placeholder="Enter option...", max_length=80, required=True ) def __init__(self, view: PollBuilderView) -> None: super().__init__(title="Add Poll Option", timeout=None) self.parent_view = view async def on_submit(self, interaction: discord.Interaction) -> None: option_text = str(self.option.value).strip() self.parent_view.options.append(option_text) if self.parent_view.message: await self.parent_view.message.edit(embed=self.parent_view._build_embed(), view=self.parent_view) await interaction.response.send_message( f"➕ Added option: **{option_text}** ({len(self.parent_view.options)}/10)", ephemeral=True ) class DurationModal(discord.ui.Modal): """Modal for setting duration.""" duration = discord.ui.TextInput( label="Duration (minutes, 0 = no limit)", placeholder="60", max_length=5, required=True, default="0" ) def __init__(self, view: PollBuilderView) -> None: super().__init__(title="Set Poll Duration", timeout=None) self.parent_view = view self.duration.default = str(view.duration) async def on_submit(self, interaction: discord.Interaction) -> None: try: duration = int(str(self.duration.value).strip() or "0") except ValueError: duration = 0 duration = max(0, min(duration, 10080)) # Max 1 week self.parent_view.duration = duration if self.parent_view.message: await self.parent_view.message.edit(embed=self.parent_view._build_embed(), view=self.parent_view) if self.parent_view.lang == "ar": await interaction.response.send_message( f"⏱️ المدة: **{duration} دقيقة**" if duration > 0 else "⏱️ بدون حد زمني", ephemeral=True ) else: await interaction.response.send_message( f"⏱️ Duration set: **{duration} minutes**" if duration > 0 else "⏱️ No time limit", ephemeral=True ) class QuickOptionsView(discord.ui.View): """Quick preset options view.""" def __init__(self, parent: PollBuilderView) -> None: super().__init__(timeout=60) self.parent = parent @discord.ui.button(label="Yes/No/Maybe", style=discord.ButtonStyle.primary, row=0) async def yes_no_maybe(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["Yes", "No", "Maybe"] await self._apply_options(interaction) @discord.ui.button(label="Agree/Disagree", style=discord.ButtonStyle.primary, row=0) async def agree_disagree(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["Agree", "Disagree", "Neutral"] await self._apply_options(interaction) @discord.ui.button(label="1-5 Rating", style=discord.ButtonStyle.primary, row=1) async def rating(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["1 ✅", "2 ✅✅", "3 ✅✅✅", "4 ✅✅✅✅", "5 ✅✅✅✅✅"] await self._apply_options(interaction) @discord.ui.button(label="Days of Week", style=discord.ButtonStyle.primary, row=1) async def days(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] await self._apply_options(interaction) @discord.ui.button(label="Time Slots", style=discord.ButtonStyle.primary, row=2) async def time_slots(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["Morning", "Afternoon", "Evening", "Night"] await self._apply_options(interaction) @discord.ui.button(label="Priority", style=discord.ButtonStyle.primary, row=2) async def priority(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: self.parent.options = ["🔴 High", "<:animatedarrowyellow:1477261257592668271> Medium", "<:animatedarrowgreen:1477261279428087979> Low"] await self._apply_options(interaction) async def _apply_options(self, interaction: discord.Interaction) -> None: """Apply the selected options.""" if self.parent.message: await self.parent.message.edit(embed=self.parent._build_embed(), view=self.parent) await interaction.response.edit_message( content=f"✅ Applied **{len(self.parent.options)}** options!", view=None ) # ═══════════════════════════════════════════════════════════════════════════════ # POLL VOTE VIEW # ═══════════════════════════════════════════════════════════════════════════════ class PollVoteView(discord.ui.View): """Interactive poll view with vote buttons and controls.""" def __init__(self, *, poll_id: int, question: str, options: list[str], author_id: int, poll_type: str = "default", end_time: float | None = None, allow_multiple: bool = False, lang: str = "en") -> None: super().__init__(timeout=None) self.poll_id = poll_id self.question = question self.options = options self.author_id = author_id self.poll_type = poll_type self.end_time = end_time self.allow_multiple = allow_multiple self.lang = lang self.voters: dict[int, list[int]] = {} # user_id -> [option_indices] self.message: discord.Message | None = None self._ended = False # Add vote buttons self._add_vote_buttons() def _add_vote_buttons(self) -> None: """Add vote buttons for each option.""" number_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] letter_emojis = ["🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭", "🇮", "🇯"] for idx, option in enumerate(self.options[:10]): emoji = number_emojis[idx] if self.poll_type == "default" else letter_emojis[idx] btn = discord.ui.Button( label=option[:80], emoji=emoji, style=discord.ButtonStyle.secondary, custom_id=f"poll:{self.poll_id}:vote:{idx}", row=idx // 5 # 5 buttons per row ) async def _callback(interaction: discord.Interaction, i: int = idx) -> None: await self._handle_vote(interaction, i) btn.callback = _callback self.add_item(btn) async def _handle_vote(self, interaction: discord.Interaction, option_idx: int) -> None: """Handle a vote for an option.""" if self._ended: if self.lang == "ar": await interaction.response.send_message("⚠️ هذا التصويت انتهى!", ephemeral=True) else: await interaction.response.send_message("⚠️ This poll has ended!", ephemeral=True) return user_id = interaction.user.id if self.allow_multiple: # Multiple choice - toggle vote if user_id not in self.voters: self.voters[user_id] = [] if option_idx in self.voters[user_id]: self.voters[user_id].remove(option_idx) if not self.voters[user_id]: del self.voters[user_id] if self.lang == "ar": await interaction.response.send_message( f"↩️ تم إلغاء تصويتك: **{self.options[option_idx]}**", ephemeral=True ) else: await interaction.response.send_message( f"↩️ Removed vote: **{self.options[option_idx]}**", ephemeral=True ) else: self.voters[user_id].append(option_idx) if self.lang == "ar": await interaction.response.send_message( f"✅ تم التصويت: **{self.options[option_idx]}**", ephemeral=True ) else: await interaction.response.send_message( f"✅ Voted: **{self.options[option_idx]}**", ephemeral=True ) else: # Single choice - replace vote if user_id in self.voters: old_vote = self.voters[user_id][0] if old_vote == option_idx: # Remove vote del self.voters[user_id] if self.lang == "ar": await interaction.response.send_message( f"↩️ تم إلغاء تصويتك.", ephemeral=True ) else: await interaction.response.send_message( f"↩️ Vote removed.", ephemeral=True ) else: # Change vote self.voters[user_id] = [option_idx] if self.lang == "ar": await interaction.response.send_message( f"🔄 تم تغيير التصويت إلى: **{self.options[option_idx]}**", ephemeral=True ) else: await interaction.response.send_message( f"🔄 Changed vote to: **{self.options[option_idx]}**", ephemeral=True ) else: self.voters[user_id] = [option_idx] if self.lang == "ar": await interaction.response.send_message( f"✅ تم التصويت: **{self.options[option_idx]}**", ephemeral=True ) else: await interaction.response.send_message( f"✅ Voted: **{self.options[option_idx]}**", ephemeral=True ) # Update embed if self.message: try: await self.message.edit(embed=self._build_embed(), view=self) except discord.NotFound: pass def _get_vote_counts(self) -> list[int]: """Get vote counts for each option.""" counts = [0] * len(self.options) for user_votes in self.voters.values(): for vote_idx in user_votes: if 0 <= vote_idx < len(counts): counts[vote_idx] += 1 return counts def _build_embed(self) -> discord.Embed: """Build the poll embed.""" total_votes = sum(len(v) for v in self.voters.values()) unique_voters = len(self.voters) color = POLL_COLORS.get(self.poll_type, NEON_CYAN) if self.lang == "ar": title = f"🗳️ تصويت: {self.question}" footer_text = f"👥 {unique_voters} مصوت • {total_votes} صوت" ended_text = "⏰ انتهى التصويت!" time_left = "⏰ الوقت المتبقي" else: title = f"🗳️ Poll: {self.question}" footer_text = f"👥 {unique_voters} voters • {total_votes} votes" ended_text = "⏰ Poll ended!" time_left = "⏰ Time remaining" embed = discord.Embed( title=title, color=color if not self._ended else discord.Color.greyple() ) if self._ended: embed.description = f"**{ended_text}**" # Add results counts = self._get_vote_counts() for idx, option in enumerate(self.options): count = counts[idx] ratio = 0 if total_votes == 0 else (count / total_votes) * 100 # Progress bar bar_filled = int(ratio // 10) bar = "▓" * bar_filled + "░" * (10 - bar_filled) # Winner indicator max_count = max(counts) if counts else 0 winner_emoji = " 🏆" if count == max_count and count > 0 and self._ended else "" embed.add_field( name=f"**{option}**{winner_emoji}", value=f"📊 `{count}` votes ({ratio:.1f}%)\n`{bar}`", inline=False ) # Add time info if self.end_time and not self._ended: now = time.time() remaining = max(0, self.end_time - now) if remaining > 0: hours = int(remaining // 3600) mins = int((remaining % 3600) // 60) secs = int(remaining % 60) if hours > 0: time_str = f"{hours}h {mins}m {secs}s" elif mins > 0: time_str = f"{mins}m {secs}s" else: time_str = f"{secs}s" embed.add_field(name=time_left, value=f"⏱️ **{time_str}**", inline=False) # Multiple choice indicator if self.allow_multiple: indicator = "🔀 Multiple Choice" if self.lang != "ar" else "🔀 خيارات متعددة" footer_text += f" • {indicator}" embed.set_footer(text=footer_text + f" • by <@{self.author_id}>") embed.timestamp = datetime.utcnow() return embed # ═══════════════════════════════════════════════════════════════════════════════ # SUGGESTION SYSTEM # ═══════════════════════════════════════════════════════════════════════════════ class SuggestionView(discord.ui.View): """View for suggestion voting.""" def __init__(self, cog: "Community", suggestion_id: int, author_id: int, lang: str = "en") -> None: super().__init__(timeout=None) self.cog = cog self.suggestion_id = suggestion_id self.author_id = author_id self.lang = lang self.message: discord.Message | None = None async def _set_vote(self, user_id: int, vote_type: str | None) -> None: if vote_type is None: await self.cog.bot.db.execute( "DELETE FROM suggestion_votes WHERE suggestion_id = ? AND user_id = ?", self.suggestion_id, user_id, ) return await self.cog.bot.db.execute( "INSERT INTO suggestion_votes(suggestion_id, user_id, vote_type) VALUES (?, ?, ?) " "ON CONFLICT(suggestion_id, user_id) DO UPDATE SET vote_type = excluded.vote_type", self.suggestion_id, user_id, vote_type, ) async def _vote_totals(self) -> tuple[int, int]: rows = await self.cog.bot.db.fetchall( "SELECT vote_type, COUNT(*) FROM suggestion_votes WHERE suggestion_id = ? GROUP BY vote_type", self.suggestion_id, ) mapping = {str(v): int(c) for v, c in rows} return mapping.get("up", 0), mapping.get("down", 0) @discord.ui.button(label="Upvote", emoji=ui("arrow_green"), style=discord.ButtonStyle.success, custom_id="suggestion:upvote") async def upvote(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: user_id = interaction.user.id row = await self.cog.bot.db.fetchone( "SELECT vote_type FROM suggestion_votes WHERE suggestion_id = ? AND user_id = ?", self.suggestion_id, user_id, ) if row and row[0] == "up": await self._set_vote(user_id, None) if self.lang == "ar": await interaction.response.send_message("↩️ تم إزالة صوتك الإيجابي.", ephemeral=True) else: await interaction.response.send_message("↩️ Upvote removed.", ephemeral=True) else: await self._set_vote(user_id, "up") if self.lang == "ar": await interaction.response.send_message("👍 تم التصويت إيجابياً!", ephemeral=True) else: await interaction.response.send_message("👍 Upvoted!", ephemeral=True) if self.message: try: await self._update_message_embed() except discord.NotFound: pass @discord.ui.button(label="Downvote", emoji=ui("arrow_red"), style=discord.ButtonStyle.danger, custom_id="suggestion:downvote") async def downvote(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: user_id = interaction.user.id row = await self.cog.bot.db.fetchone( "SELECT vote_type FROM suggestion_votes WHERE suggestion_id = ? AND user_id = ?", self.suggestion_id, user_id, ) if row and row[0] == "down": await self._set_vote(user_id, None) if self.lang == "ar": await interaction.response.send_message("↩️ تم إزالة صوتك السلبي.", ephemeral=True) else: await interaction.response.send_message("↩️ Downvote removed.", ephemeral=True) else: await self._set_vote(user_id, "down") if self.lang == "ar": await interaction.response.send_message("👎 تم التصويت سلبياً.", ephemeral=True) else: await interaction.response.send_message("👎 Downvoted!", ephemeral=True) if self.message: try: await self._update_message_embed() except discord.NotFound: pass @discord.ui.button(label="Voters", emoji=ui("members"), style=discord.ButtonStyle.secondary, custom_id="suggestion:voters") async def voters(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: rows = await self.cog.bot.db.fetchall( "SELECT user_id, vote_type FROM suggestion_votes WHERE suggestion_id = ? ORDER BY vote_type DESC, user_id ASC", self.suggestion_id, ) if not rows: await interaction.response.send_message("No voters yet.", ephemeral=True) return up = [f"<@{uid}>" for uid, vote_type in rows if vote_type == "up"][:20] down = [f"<@{uid}>" for uid, vote_type in rows if vote_type == "down"][:20] embed = discord.Embed(title=f"Suggestion #{self.suggestion_id} voters", color=NEON_CYAN) embed.add_field(name="👍 Upvotes", value="\n".join(up) if up else "—", inline=True) embed.add_field(name="👎 Downvotes", value="\n".join(down) if down else "—", inline=True) await interaction.response.send_message(embed=embed, ephemeral=True) async def _build_embed(self) -> discord.Embed: up_count, down_count = await self._vote_totals() total = up_count + down_count score = up_count - down_count if self.lang == "ar": embed = discord.Embed( title="💡 اقتراح", color=NEON_LIME if score >= 0 else discord.Color.red() ) embed.add_field(name="📊 النتيجة", value=f"👍 {up_count} | 👎 {down_count} | مجموع: **{score}**", inline=False) embed.set_footer(text=f"👥 {total} مصوت") else: embed = discord.Embed( title="💡 Suggestion", color=NEON_LIME if score >= 0 else discord.Color.red() ) embed.add_field(name="📊 Score", value=f"👍 {up_count} | 👎 {down_count} | Total: **{score}**", inline=False) embed.set_footer(text=f"👥 {total} voters") return embed async def _update_message_embed(self) -> None: if not self.message: return up_count, down_count = await self._vote_totals() total = up_count + down_count score = up_count - down_count source = self.message.embeds[0] if self.message.embeds else discord.Embed(title="💡 Suggestion", color=NEON_CYAN) rebuilt = discord.Embed(title=source.title, description=source.description, color=source.color, url=source.url) if source.author and source.author.name: rebuilt.set_author(name=source.author.name, icon_url=source.author.icon_url) for field in source.fields: if field.name in {"📊 Score", "📊 النتيجة"}: continue rebuilt.add_field(name=field.name, value=field.value, inline=field.inline) if self.lang == "ar": rebuilt.add_field(name="📊 النتيجة", value=f"👍 {up_count} | 👎 {down_count} | مجموع: **{score}**", inline=False) rebuilt.set_footer(text=f"👥 {total} مصوت") else: rebuilt.add_field(name="📊 Score", value=f"👍 {up_count} | 👎 {down_count} | Total: **{score}**", inline=False) rebuilt.set_footer(text=f"👥 {total} voters") await self.message.edit(embed=rebuilt, view=self) # ═══════════════════════════════════════════════════════════════════════════════ # COMMUNITY COG # ═══════════════════════════════════════════════════════════════════════════════ class Community(commands.Cog): """Community features: polls, suggestions, events.""" def __init__(self, bot: commands.Bot) -> None: self.bot = bot self.poll_counter = 0 self.active_polls: dict[int, PollVoteView] = {} self.suggestion_counter = 0 async def cog_load(self) -> None: """Register persistent views.""" self.bot.add_view(PollVoteView( poll_id=0, question="", options=[], author_id=0 )) self.bot.add_view(TicketCloseView()) self.bot.add_view(TicketPanelView(self, 0)) self.bot.add_view(PollPanelView(self)) self.bot.add_view(SuggestionView(self, 0, "")) self.bot.add_view(GiveawayJoinView(self, 0)) self.giveaway_watcher.start() def cog_unload(self) -> None: if self.giveaway_watcher.is_running(): self.giveaway_watcher.cancel() async def _end_poll_after(self, poll_id: int, delay: float) -> None: """End a poll after a delay.""" await asyncio.sleep(max(0, delay)) view = self.active_polls.get(poll_id) if view and not view._ended: view._ended = True if view.message: try: await view.message.edit(embed=view._build_embed(), view=view) except discord.NotFound: pass @tasks.loop(minutes=1) async def giveaway_watcher(self) -> None: now_ts = int(time.time()) rows = await self.bot.db.fetchall( "SELECT id, guild_id, channel_id, message_id, prize FROM giveaways WHERE ended = 0 AND end_time <= ?", now_ts, ) for giveaway_id, guild_id, channel_id, message_id, prize in rows: guild = self.bot.get_guild(guild_id) if not guild: continue entries = await self.bot.db.fetchall("SELECT user_id FROM giveaway_entries WHERE giveaway_id = ?", giveaway_id) users = [uid for (uid,) in entries] winner_id = random.choice(users) if users else None await self.bot.db.execute( "UPDATE giveaways SET ended = 1, winner_id = ? WHERE id = ?", winner_id, giveaway_id, ) channel = guild.get_channel(channel_id) if not isinstance(channel, discord.TextChannel): continue try: msg = await channel.fetch_message(message_id) except Exception: msg = None if msg is not None: embed = msg.embeds[0] if msg.embeds else discord.Embed(title="🎁 Giveaway") embed.color = NEON_ORANGE if winner_id: embed.add_field(name="Winner", value=f"<@{winner_id}>", inline=False) else: embed.add_field(name="Winner", value="No entrants", inline=False) await msg.edit(embed=embed, view=None) if winner_id: await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**") log_channel_id = await self.bot.db.fetchone("SELECT log_channel_id FROM guild_config WHERE guild_id = ?", guild.id) if log_channel_id and log_channel_id[0]: log_channel = guild.get_channel(log_channel_id[0]) if log_channel: log_embed = discord.Embed( title="🎁 Giveaway Ended", description=f"Giveaway **#{giveaway_id}** has ended.", color=NEON_GOLD, ) log_embed.add_field(name="Winner", value=f"<@{winner_id}>", inline=True) log_embed.add_field(name="Prize", value=prize, inline=True) log_embed.add_field(name="Giveaway ID", value=str(giveaway_id), inline=True) await log_channel.send(embed=log_embed) @commands.hybrid_command(name="setsuggestionchannel", description=get_cmd_desc("commands.tools.setsuggestionchannel_desc")) @commands.has_permissions(manage_guild=True) async def setsuggestionchannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: """Set the channel for suggestions.""" await self.bot.db.execute( "INSERT INTO guild_config(guild_id, suggestion_channel_id) VALUES (?, ?) " "ON CONFLICT(guild_id) DO UPDATE SET suggestion_channel_id = excluded.suggestion_channel_id", ctx.guild.id, channel.id ) lang = await self.bot.get_guild_language(ctx.guild.id) if lang == "ar": await ctx.reply(f"💡 تم تعيين قناة الاقتراحات: {channel.mention}") else: await ctx.reply(f"💡 Suggestion channel set to {channel.mention}") @commands.hybrid_command(name="setpollchannel", description=get_cmd_desc("commands.tools.setpollchannel_desc")) @commands.has_permissions(manage_guild=True) async def setpollchannel(self, ctx: commands.Context, channel: discord.TextChannel) -> None: """Set the default channel for polls.""" await self.bot.db.execute( "INSERT INTO guild_config(guild_id, poll_channel_id) VALUES (?, ?) " "ON CONFLICT(guild_id) DO UPDATE SET poll_channel_id = excluded.poll_channel_id", ctx.guild.id, channel.id ) lang = await self.bot.get_guild_language(ctx.guild.id) if lang == "ar": await ctx.reply(f"🗳️ تم تعيين قناة التصويت: {channel.mention}") else: await ctx.reply(f"🗳️ Poll channel set to {channel.mention}") @commands.hybrid_command(name="ticket_panel", description=get_cmd_desc("commands.tools.ticket_panel_desc"), with_app_command=True) @commands.has_permissions(manage_guild=True) async def ticket_panel(self, ctx: commands.Context) -> None: if not ctx.guild: await ctx.reply("Server only.") return row = await self.bot.db.fetchone("SELECT ticket_category_id FROM guild_config WHERE guild_id = ?", ctx.guild.id) category_id = row[0] if row else None category = ctx.guild.get_channel(category_id) if category_id else discord.utils.get(ctx.guild.categories, name="tickets") if not isinstance(category, discord.CategoryChannel): category = await ctx.guild.create_category("tickets") await self.bot.db.execute( "INSERT INTO guild_config(guild_id, ticket_category_id) VALUES (?, ?) ON CONFLICT(guild_id) DO UPDATE SET ticket_category_id = excluded.ticket_category_id", ctx.guild.id, category.id, ) view = TicketPanelView(self, category.id) embed = discord.Embed( title="🎫 Support Tickets", description="Open a private ticket with the button below. Staff can close it from inside the ticket.", color=NEON_PURPLE, ) await ctx.reply(embed=embed, view=view) @commands.hybrid_group(name="giveaway", description="Giveaway commands") @commands.has_permissions(manage_guild=True) async def giveaway_group(self, ctx: commands.Context) -> None: if ctx.invoked_subcommand is None: await ctx.reply("Use: `/giveaway start`, `/giveaway end`, `/giveaway reroll`.") async def giveaway_create(self, ctx_or_interaction: commands.Context | discord.Interaction, minutes: int, winner_count: int, *, prize: str) -> None: guild = ctx_or_interaction.guild channel = ctx_or_interaction.channel if not guild or not isinstance(channel, discord.TextChannel): if isinstance(ctx_or_interaction, commands.Context): await ctx_or_interaction.reply("Server text channel only.") else: await ctx_or_interaction.followup.send("Server text channel only.", ephemeral=True) return end_time = int(time.time()) + max(1, minutes) * 60 view = GiveawayJoinView(self, guild.id) view.winners_count = max(1, winner_count) embed = view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False) msg = await channel.send(embed=embed, view=view) await self.bot.db.execute( "INSERT INTO giveaways(guild_id, channel_id, message_id, prize, end_time, winner_id, ended) VALUES (?, ?, ?, ?, ?, NULL, 0)", guild.id, channel.id, msg.id, prize[:250], end_time, ) row = await self.bot.db.fetchone("SELECT id FROM giveaways WHERE message_id = ?", msg.id) view.giveaway_id = int(row[0]) if row else 0 if view.giveaway_id: await msg.edit(embed=view.build_embed(prize=prize, end_time=end_time, winners=view.winners_count, entrants=0, ended=False), view=view) response = f"✅ Giveaway created (ID: `{view.giveaway_id}`) • ends " if isinstance(ctx_or_interaction, commands.Context): await ctx_or_interaction.reply(response) else: await ctx_or_interaction.followup.send(response, ephemeral=True) async def giveaway_end(self, ctx_or_interaction: commands.Context | discord.Interaction, giveaway_id: int) -> None: row = await self.bot.db.fetchone( "SELECT guild_id, channel_id, message_id, prize, ended FROM giveaways WHERE id = ?", giveaway_id, ) if not row: msg = "Giveaway not found." if isinstance(ctx_or_interaction, commands.Context): await ctx_or_interaction.reply(msg) else: await ctx_or_interaction.followup.send(msg, ephemeral=True) return guild_id, channel_id, message_id, prize, ended = row if int(ended or 0) == 1: msg = "Giveaway already ended." if isinstance(ctx_or_interaction, commands.Context): await ctx_or_interaction.reply(msg) else: await ctx_or_interaction.followup.send(msg, ephemeral=True) return entries = await self.bot.db.fetchall("SELECT user_id FROM giveaway_entries WHERE giveaway_id = ?", giveaway_id) users = [uid for (uid,) in entries] winner_id = random.choice(users) if users else None await self.bot.db.execute("UPDATE giveaways SET ended = 1, winner_id = ? WHERE id = ?", winner_id, giveaway_id) guild = self.bot.get_guild(int(guild_id)) channel = guild.get_channel(int(channel_id)) if guild else None if isinstance(channel, discord.TextChannel): try: msg_obj = await channel.fetch_message(int(message_id)) embed = msg_obj.embeds[0] if msg_obj.embeds else discord.Embed(title="🎁 Giveaway") embed.color = NEON_ORANGE embed.add_field(name="Winner", value=(f"<@{winner_id}>" if winner_id else "No entrants"), inline=False) await msg_obj.edit(embed=embed, view=None) except Exception: pass if winner_id: await channel.send(f"🎉 Giveaway **#{giveaway_id}** winner: <@{winner_id}> • Prize: **{prize}**") else: await channel.send(f"🎉 Giveaway **#{giveaway_id}** ended with no entrants.") reply = f"✅ Giveaway `{giveaway_id}` ended." if isinstance(ctx_or_interaction, commands.Context): await ctx_or_interaction.reply(reply) else: await ctx_or_interaction.followup.send(reply, ephemeral=True) @giveaway_group.command(name="start", description="Start a giveaway in current channel") async def giveaway_start(self, ctx: commands.Context, minutes: int, winners: int, *, prize: str) -> None: await self.giveaway_create(ctx, minutes, winners, prize=prize) @giveaway_group.command(name="end", description="End a giveaway by ID") async def giveaway_end_cmd(self, ctx: commands.Context, giveaway_id: int) -> None: await self.giveaway_end(ctx, giveaway_id) @giveaway_group.command(name="reroll", description="Reroll winner for ended giveaway") async def giveaway_reroll(self, ctx: commands.Context, giveaway_id: int) -> None: row = await self.bot.db.fetchone("SELECT ended FROM giveaways WHERE id = ?", giveaway_id) if not row or int(row[0] or 0) != 1: await ctx.reply("Giveaway must be ended before reroll.") return await self.bot.db.execute("UPDATE giveaways SET ended = 0, winner_id = NULL WHERE id = ?", giveaway_id) await self.giveaway_end(ctx, giveaway_id) @commands.hybrid_group(name="ticket", description="Ticket commands") async def ticket_group(self, ctx: commands.Context) -> None: if ctx.invoked_subcommand is None: await ctx.reply("Use: `/ticket close` or `/ticket delete` in a ticket channel.") @ticket_group.command(name="close", description="Close current ticket channel") async def ticket_close_cmd(self, ctx: commands.Context) -> None: channel = ctx.channel if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"): await ctx.reply("This is not a ticket channel.") return await self.bot.db.execute("UPDATE tickets SET status = 'closed' WHERE channel_id = ?", channel.id) await ctx.reply("🔒 Closing ticket in 5 seconds...") await asyncio.sleep(5) await channel.delete(reason=f"Closed by {ctx.author}") @ticket_group.command(name="delete", description="Delete current ticket channel") @commands.has_permissions(manage_channels=True) async def ticket_delete_cmd(self, ctx: commands.Context) -> None: channel = ctx.channel if not isinstance(channel, discord.TextChannel) or not channel.name.startswith("ticket-"): await ctx.reply("This is not a ticket channel.") return await self.bot.db.execute("DELETE FROM tickets WHERE channel_id = ?", channel.id) await ctx.reply("🗑️ Deleting ticket in 3 seconds...") await asyncio.sleep(3) await channel.delete(reason=f"Deleted by {ctx.author}") # ═══════════════════════════════════════════════════════════════════════════════ # GLOBAL TICKETS + GIVEAWAYS # ═══════════════════════════════════════════════════════════════════════════════ class GiveawayPanelView(discord.ui.View): def __init__(self, cog: "Community") -> None: super().__init__(timeout=None) self.cog = cog @discord.ui.button(label="🚀 Create Giveaway", style=discord.ButtonStyle.success, custom_id="giveaway_panel:create") async def create_giveaway(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: await interaction.response.send_modal(GiveawayCreateModal(self.cog)) @discord.ui.button(label="🛑 End Giveaway", style=discord.ButtonStyle.danger, custom_id="giveaway_panel:end") async def end_giveaway(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: await interaction.response.send_modal(GiveawayEndModal(self.cog)) class GiveawayCreateModal(discord.ui.Modal, title="🎁 Create Giveaway"): prize = discord.ui.TextInput(label="Prize", placeholder="Enter the prize", required=True) duration = discord.ui.TextInput(label="Duration (minutes)", placeholder="e.g. 60", required=True, default="60") winners = discord.ui.TextInput(label="Winners", placeholder="Number of winners", required=True, default="1") def __init__(self, cog: "Community") -> None: super().__init__() self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: try: minutes = int(self.duration.value) winner_count = int(self.winners.value) except ValueError: await interaction.response.send_message("❌ Invalid duration or winners.", ephemeral=True) return await interaction.response.defer() await self.cog.giveaway_create(interaction, minutes, winner_count, prize=self.prize.value) class GiveawayEndModal(discord.ui.Modal, title="🛑 End Giveaway"): giveaway_id = discord.ui.TextInput(label="Giveaway ID", placeholder="Enter the giveaway ID", required=True) def __init__(self, cog: "Community") -> None: super().__init__() self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: try: gid = int(self.giveaway_id.value) except ValueError: await interaction.response.send_message("❌ Invalid ID.", ephemeral=True) return await interaction.response.defer() await self.cog.giveaway_end(interaction, gid) class GiveawayJoinView(discord.ui.View): def __init__(self, cog: "Community", guild_id: int = 0) -> None: super().__init__(timeout=None) self.cog = cog self.guild_id = guild_id self.giveaway_id: int = 0 self.winners_count: int = 1 async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: try: if not interaction.response.is_done(): await interaction.response.send_message("⚠️ Giveaway interaction failed. Please retry.", ephemeral=True) else: await interaction.followup.send("⚠️ Giveaway interaction failed. Please retry.", ephemeral=True) except Exception: pass def build_embed(self, *, prize: str, end_time: int, winners: int, entrants: int, ended: bool) -> discord.Embed: status = "Ended" if ended else "Open" embed = discord.Embed(title=f"{ui('gift')} Giveaway", description=f"**{prize}**", color=NEON_YELLOW if not ended else NEON_ORANGE) embed.add_field(name="Status", value=status, inline=True) embed.add_field(name="Winners", value=str(winners), inline=True) embed.add_field(name="Entrants", value=str(entrants), inline=True) embed.add_field(name="Ends", value=f"", inline=False) embed.set_footer(text=f"Giveaway ID: {self.giveaway_id or 'pending'}") return embed async def _refresh_message(self, interaction: discord.Interaction) -> None: if not interaction.message or not self.giveaway_id: return row = await self.cog.bot.db.fetchone("SELECT prize, end_time, ended FROM giveaways WHERE id = ?", self.giveaway_id) if not row: return prize, end_time, ended = row count_row = await self.cog.bot.db.fetchone("SELECT COUNT(*) FROM giveaway_entries WHERE giveaway_id = ?", self.giveaway_id) entrants = int(count_row[0] if count_row else 0) await interaction.message.edit(embed=self.build_embed(prize=prize, end_time=int(end_time), winners=self.winners_count, entrants=entrants, ended=bool(ended)), view=self) @discord.ui.button(label="Join", emoji=ui("partytime"), style=discord.ButtonStyle.success, custom_id="giveaway:join") async def join(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: if not self.giveaway_id: await interaction.response.send_message("Giveaway not ready.", ephemeral=True) return status_row = await self.cog.bot.db.fetchone("SELECT ended FROM giveaways WHERE id = ?", self.giveaway_id) if status_row and int(status_row[0]) == 1: await interaction.response.send_message("This giveaway is already ended.", ephemeral=True) return await self.cog.bot.db.execute("INSERT OR IGNORE INTO giveaway_entries(giveaway_id, user_id) VALUES (?, ?)", self.giveaway_id, interaction.user.id) await interaction.response.send_message("✅ Joined giveaway.", ephemeral=True) await self._refresh_message(interaction) @discord.ui.button(label="Leave", emoji=ui("leave"), style=discord.ButtonStyle.danger, custom_id="giveaway:leave") async def leave(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: if not self.giveaway_id: await interaction.response.send_message("Giveaway not ready.", ephemeral=True) return status_row = await self.cog.bot.db.fetchone("SELECT ended FROM giveaways WHERE id = ?", self.giveaway_id) if status_row and int(status_row[0]) == 1: await interaction.response.send_message("This giveaway is already ended.", ephemeral=True) return await self.cog.bot.db.execute("DELETE FROM giveaway_entries WHERE giveaway_id = ? AND user_id = ?", self.giveaway_id, interaction.user.id) await interaction.response.send_message("↩️ Left giveaway.", ephemeral=True) await self._refresh_message(interaction) @discord.ui.button(label="Voters", emoji=ui("members"), style=discord.ButtonStyle.secondary, custom_id="giveaway:voters") async def voters(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: rows = await self.cog.bot.db.fetchall("SELECT user_id FROM giveaway_entries WHERE giveaway_id = ? LIMIT 50", self.giveaway_id) if not rows: await interaction.response.send_message("No entrants yet.", ephemeral=True) return await interaction.response.send_message("\n".join(f"• <@{uid}>" for (uid,) in rows), ephemeral=True) class TicketCloseView(discord.ui.View): def __init__(self) -> None: super().__init__(timeout=None) async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: try: if not interaction.response.is_done(): await interaction.response.send_message("⚠️ Ticket interaction failed. Retry.", ephemeral=True) else: await interaction.followup.send("⚠️ Ticket interaction failed. Retry.", ephemeral=True) except Exception: pass @discord.ui.button(label="Close Ticket", emoji=ui("lock"), style=discord.ButtonStyle.danger, custom_id="ticket:close") async def close_ticket(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: if not isinstance(interaction.channel, discord.TextChannel): await interaction.response.send_message("Ticket channel only.", ephemeral=True) return await interaction.response.send_message("🔒 Ticket will be deleted in 5 seconds.") await asyncio.sleep(5) await interaction.channel.delete(reason=f"Closed by {interaction.user}") class TicketPanelView(discord.ui.View): def __init__(self, cog: "Community", category_id: int) -> None: super().__init__(timeout=None) self.cog = cog self.category_id = category_id async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: try: if not interaction.response.is_done(): await interaction.response.send_message("⚠️ Ticket panel failed. Retry please.", ephemeral=True) else: await interaction.followup.send("⚠️ Ticket panel failed. Retry please.", ephemeral=True) except Exception: pass @discord.ui.button(label="Open Ticket", emoji=ui("ticket"), style=discord.ButtonStyle.success, custom_id="ticket:open") async def open_ticket(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return category = interaction.guild.get_channel(self.category_id) if not isinstance(category, discord.CategoryChannel): await interaction.response.send_message("Ticket category missing. Re-run /ticket_panel", ephemeral=True) return overwrites = { interaction.guild.default_role: discord.PermissionOverwrite(view_channel=False), interaction.user: discord.PermissionOverwrite(view_channel=True, send_messages=True, attach_files=True), interaction.guild.me: discord.PermissionOverwrite(view_channel=True, send_messages=True, manage_channels=True), } channel = await interaction.guild.create_text_channel( name=f"ticket-{interaction.user.name[:20].lower()}", category=category, overwrites=overwrites, reason="Global ticket panel", ) await self.cog.bot.db.execute( "INSERT INTO tickets(guild_id, channel_id, user_id, status, created_at) VALUES (?, ?, ?, 'open', ?)", interaction.guild.id, channel.id, interaction.user.id, int(time.time()), ) await channel.send(f"{interaction.user.mention} ticket opened.", view=TicketCloseView()) await interaction.response.send_message(f"✅ Ticket created: {channel.mention}", ephemeral=True) @discord.ui.button(label="Close Ticket", emoji=ui("lock"), style=discord.ButtonStyle.danger, row=0, custom_id="ticket_close_btn") async def close_ticket(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: channel = interaction.channel if not channel or not isinstance(channel, discord.TextChannel): await interaction.response.send_message("This button only works in a ticket channel.", ephemeral=True) return if not channel.name.startswith("ticket-"): await interaction.response.send_message("This is not a ticket channel.", ephemeral=True) return await interaction.response.send_message("🔒 Ticket will be closed in 5 seconds.", ephemeral=True) await asyncio.sleep(5) await channel.edit(archived=True) if hasattr(channel, 'archived') else None await self.cog.bot.db.execute( "UPDATE tickets SET status = 'closed' WHERE channel_id = ?", channel.id, ) await interaction.followup.send("✅ Ticket closed.", ephemeral=True) @discord.ui.button(label="Delete Ticket", emoji=ui("trash"), style=discord.ButtonStyle.danger, row=0, custom_id="ticket_delete_btn") async def delete_ticket(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: channel = interaction.channel if not channel or not isinstance(channel, discord.TextChannel): await interaction.response.send_message("This button only works in a ticket channel.", ephemeral=True) return if not channel.name.startswith("ticket-"): await interaction.response.send_message("This is not a ticket channel.", ephemeral=True) return if not interaction.user.guild_permissions.manage_channels: await interaction.response.send_message("Manage Channels permission required.", ephemeral=True) return await interaction.response.send_message("🗑️ Ticket will be deleted in 5 seconds.", ephemeral=True) await asyncio.sleep(5) await self.cog.bot.db.execute( "DELETE FROM tickets WHERE channel_id = ?", channel.id, ) await channel.delete(reason=f"Ticket deleted by {interaction.user}") # ═══════════════════════════════════════════════════════════════════════════════ # COMMUNITY COG # ═══════════════════════════════════════════════════════════════════════════════ class PollPanelView(discord.ui.View): """Main poll panel with creation options.""" def __init__(self, cog: "Community", lang: str = "en") -> None: super().__init__(timeout=None) self.cog = cog self.lang = lang async def on_error(self, interaction: discord.Interaction, error: Exception, item: discord.ui.Item) -> None: try: if not interaction.response.is_done(): await interaction.response.send_message("⚠️ Poll panel interaction failed. Retry.", ephemeral=True) else: await interaction.followup.send("⚠️ Poll panel interaction failed. Retry.", ephemeral=True) except Exception: pass @discord.ui.button(label="Interactive Builder", emoji=ui("settings"), style=discord.ButtonStyle.success, row=0, custom_id="poll_interactive") async def interactive_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Open the interactive poll builder.""" if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return lang = await self.cog.bot.get_guild_language(interaction.guild.id) view = PollBuilderView(self.cog, interaction.user.id, lang) embed = view._build_embed() await interaction.response.send_message(embed=embed, view=view, ephemeral=True) msg = await interaction.original_response() view.message = msg @discord.ui.button(label="Quick Poll", emoji=ui("zap"), style=discord.ButtonStyle.primary, row=0, custom_id="poll_quick") async def quick_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Create a quick poll.""" await interaction.response.send_modal(QuickPollModal(self.cog)) @discord.ui.button(label="Yes/No Poll", emoji=ui("ok"), style=discord.ButtonStyle.secondary, row=0, custom_id="poll_yesno") async def yesno_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Create a Yes/No poll.""" await interaction.response.send_modal(YesNoPollModal(self.cog)) @discord.ui.button(label="Rating Poll", emoji=ui("star"), style=discord.ButtonStyle.secondary, row=1, custom_id="poll_rating") async def rating_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Create a rating poll.""" await interaction.response.send_modal(RatingPollModal(self.cog)) @discord.ui.button(label="Event Poll", emoji=ui("calendar"), style=discord.ButtonStyle.primary, row=1, custom_id="poll_event") async def event_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Create an event poll.""" await interaction.response.send_modal(EventPollModal(self.cog)) @discord.ui.button(label="Help", emoji=ui("question"), style=discord.ButtonStyle.secondary, row=2, custom_id="poll_help") async def help_btn(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Show poll help.""" lang = await self.cog.bot.get_guild_language(interaction.guild.id if interaction.guild else None) if lang == "ar": embed = discord.Embed( title="❓ مساعدة التصويت", description=( f"{panel_divider('cyan')}\n" "📋 **أنواع التصويت:**\n" "• 🗳️ **منشئ تفاعلي** - خيارات ديناميكية\n" "• ⚡ **سريع** - نعم/لا/ربما\n" "• ✅ **نعم/لا** - تصويت ثنائي\n" "• ✅ **تقييم** - 1-5 نجوم\n" "• 📅 **حدث** - جدولة مواعيد\n" f"{panel_divider('cyan')}\n\n" "💡 **نصيحة:** يمكنك تغيير صوتك في أي وقت!" ), color=NEON_CYAN ) else: embed = discord.Embed( title="❓ Poll Help", description=( f"{panel_divider('cyan')}\n" "📋 **Poll Types:**\n" "• 🗳️ **Interactive** - Dynamic options\n" "• ⚡ **Quick** - Yes/No/Maybe\n" "• ✅ **Yes/No** - Binary vote\n" "• ✅ **Rating** - 1-5 stars\n" "• 📅 **Event** - Schedule times\n" f"{panel_divider('cyan')}\n\n" "💡 **Tip:** You can change your vote at any time!" ), color=NEON_CYAN ) await interaction.response.send_message(embed=embed, ephemeral=True) @discord.ui.button(label="End Poll", emoji=ui("stop"), style=discord.ButtonStyle.danger, row=2, custom_id="poll_end") async def end_poll(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """End a poll by message ID.""" if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return if not interaction.user.guild_permissions.manage_channels: await interaction.response.send_message("Manage Channels permission required.", ephemeral=True) return await interaction.response.send_modal(EndPollModal(self.cog, interaction.guild.id)) @discord.ui.button(label="Poll Stats", emoji=ui("stats"), style=discord.ButtonStyle.blurple, row=2, custom_id="poll_stats") async def poll_stats(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: """Show poll statistics.""" if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return await interaction.response.defer(ephemeral=True) rows = await self.cog.bot.db.fetchall( "SELECT s.content, s.status, COUNT(sv.user_id) AS votes FROM suggestions s LEFT JOIN suggestion_votes sv ON s.id = sv.suggestion_id WHERE s.guild_id = ? GROUP BY s.id ORDER BY s.id DESC LIMIT 10", interaction.guild.id, ) if not rows: await interaction.followup.send("No polls or suggestions found.", ephemeral=True) return lines = [] for content, status, votes in rows: status_emoji = {"pending": "⏳", "approved": "✅", "rejected": "❌"}.get(status, "❓") lines.append(f"{status_emoji} **{(content or '')[:60]}** — {votes} votes") embed = discord.Embed( title="📊 Poll/Suggestion Stats", description="\n".join(lines), color=NEON_CYAN, ) await interaction.followup.send(embed=embed, ephemeral=True) # ═══════════════════════════════════════════════════════════════════════════════ # POLL MODALS # ═══════════════════════════════════════════════════════════════════════════════ class QuickPollModal(discord.ui.Modal): """Modal for quick poll.""" question = discord.ui.TextInput( label="Poll Question", placeholder="What do you want to ask?", max_length=200, required=True ) def __init__(self, cog: "Community") -> None: super().__init__(title="Quick Poll", timeout=None) self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return lang = await self.cog.bot.get_guild_language(interaction.guild.id) question = str(self.question.value).strip() # Check for poll channel row = await self.cog.bot.db.fetchone( "SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", interaction.guild.id ) channel = interaction.guild.get_channel(row[0]) if (row and row[0]) else interaction.channel self.cog.poll_counter += 1 poll_id = self.cog.poll_counter options = ["✅ Yes", "❌ No", "🤔 Maybe"] view = PollVoteView( poll_id=poll_id, question=question, options=options, author_id=interaction.user.id, poll_type="yes_no", lang=lang ) await interaction.response.defer(ephemeral=True) msg = await channel.send(embed=view._build_embed(), view=view) view.message = msg self.cog.active_polls[poll_id] = view if lang == "ar": await interaction.followup.send(f"⚡ تم إنشاء تصويت سريع في {channel.mention}!", ephemeral=True) else: await interaction.followup.send(f"⚡ Quick poll created in {channel.mention}!", ephemeral=True) class YesNoPollModal(discord.ui.Modal): """Modal for Yes/No poll.""" question = discord.ui.TextInput( label="Poll Question", placeholder="What do you want to ask?", max_length=200, required=True ) duration = discord.ui.TextInput( label="Duration (minutes, 0 = no limit)", placeholder="60", max_length=10, required=False, default="0" ) def __init__(self, cog: "Community") -> None: super().__init__(title="Yes/No Poll", timeout=None) self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return lang = await self.cog.bot.get_guild_language(interaction.guild.id) question = str(self.question.value).strip() try: duration = int(str(self.duration.value).strip() or "0") except ValueError: duration = 0 duration = max(0, min(duration, 10080)) end_time = time.time() + (duration * 60) if duration > 0 else None row = await self.cog.bot.db.fetchone( "SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", interaction.guild.id ) channel = interaction.guild.get_channel(row[0]) if (row and row[0]) else interaction.channel self.cog.poll_counter += 1 poll_id = self.cog.poll_counter options = ["✅ Yes", "❌ No"] view = PollVoteView( poll_id=poll_id, question=question, options=options, author_id=interaction.user.id, poll_type="yes_no", end_time=end_time, lang=lang ) await interaction.response.defer(ephemeral=True) msg = await channel.send(embed=view._build_embed(), view=view) view.message = msg self.cog.active_polls[poll_id] = view if end_time: self.cog.bot.loop.create_task(self.cog._end_poll_after(poll_id, end_time - time.time())) if lang == "ar": await interaction.followup.send(f"✅ تم إنشاء التصويت في {channel.mention}!", ephemeral=True) else: await interaction.followup.send(f"✅ Poll created in {channel.mention}!", ephemeral=True) class RatingPollModal(discord.ui.Modal): """Modal for rating poll.""" question = discord.ui.TextInput( label="What do you want to rate?", placeholder="Rate this...", max_length=200, required=True ) duration = discord.ui.TextInput( label="Duration (minutes, 0 = no limit)", placeholder="60", max_length=10, required=False, default="0" ) def __init__(self, cog: "Community") -> None: super().__init__(title="Rating Poll", timeout=None) self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return lang = await self.cog.bot.get_guild_language(interaction.guild.id) question = str(self.question.value).strip() try: duration = int(str(self.duration.value).strip() or "0") except ValueError: duration = 0 duration = max(0, min(duration, 10080)) end_time = time.time() + (duration * 60) if duration > 0 else None row = await self.cog.bot.db.fetchone( "SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", interaction.guild.id ) channel = interaction.guild.get_channel(row[0]) if (row and row[0]) else interaction.channel self.cog.poll_counter += 1 poll_id = self.cog.poll_counter options = ["✅ 1 Star", "✅✅ 2 Stars", "✅✅✅ 3 Stars", "✅✅✅✅ 4 Stars", "✅✅✅✅✅ 5 Stars"] view = PollVoteView( poll_id=poll_id, question=question, options=options, author_id=interaction.user.id, poll_type="rating", end_time=end_time, lang=lang ) await interaction.response.defer(ephemeral=True) msg = await channel.send(embed=view._build_embed(), view=view) view.message = msg self.cog.active_polls[poll_id] = view if end_time: self.cog.bot.loop.create_task(self.cog._end_poll_after(poll_id, end_time - time.time())) if lang == "ar": await interaction.followup.send(f"✅ تم إنشاء تصويت التقييم في {channel.mention}!", ephemeral=True) else: await interaction.followup.send(f"✅ Rating poll created in {channel.mention}!", ephemeral=True) class EventPollModal(discord.ui.Modal): """Modal for event poll.""" question = discord.ui.TextInput( label="Event Question", placeholder="When should we have the event?", max_length=200, required=True ) options = discord.ui.TextInput( label="Time Options (separate with |)", placeholder="Saturday|Sunday|Monday", max_length=500, required=False ) duration = discord.ui.TextInput( label="Poll Duration (minutes)", placeholder="60", max_length=10, required=False, default="0" ) def __init__(self, cog: "Community") -> None: super().__init__(title="Event Poll", timeout=None) self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return lang = await self.cog.bot.get_guild_language(interaction.guild.id) question = str(self.question.value).strip() options_str = str(self.options.value).strip() if self.options.value else "" if options_str: parsed_options = [opt.strip() for opt in options_str.split("|") if opt.strip()] else: parsed_options = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] parsed_options = parsed_options[:10] try: duration = int(str(self.duration.value).strip() or "0") except ValueError: duration = 0 duration = max(0, min(duration, 10080)) end_time = time.time() + (duration * 60) if duration > 0 else None row = await self.cog.bot.db.fetchone( "SELECT poll_channel_id FROM guild_config WHERE guild_id = ?", interaction.guild.id ) channel = interaction.guild.get_channel(row[0]) if (row and row[0]) else interaction.channel self.cog.poll_counter += 1 poll_id = self.cog.poll_counter view = PollVoteView( poll_id=poll_id, question=question, options=parsed_options, author_id=interaction.user.id, poll_type="event", end_time=end_time, lang=lang ) await interaction.response.defer(ephemeral=True) msg = await channel.send(embed=view._build_embed(), view=view) view.message = msg self.cog.active_polls[poll_id] = view if end_time: self.cog.bot.loop.create_task(self.cog._end_poll_after(poll_id, end_time - time.time())) if lang == "ar": await interaction.followup.send(f"📅 تم إنشاء تصويت الحدث في {channel.mention}!", ephemeral=True) else: await interaction.followup.send(f"📅 Event poll created in {channel.mention}!", ephemeral=True) class EndPollModal(discord.ui.Modal, title="⏹️ End a Poll"): message_id = discord.ui.TextInput( label="Poll Message ID", placeholder="1234567890123456789", required=True, max_length=32, ) def __init__(self, cog: "Community", guild_id: int) -> None: super().__init__(timeout=None) self.cog = cog self.guild_id = guild_id async def on_submit(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) try: mid = int(self.message_id.value.strip()) except ValueError: await interaction.followup.send("Invalid message ID.", ephemeral=True) return poll_id = None for pid, view in self.cog.active_polls.items(): if view.message and view.message.id == mid: poll_id = pid break if poll_id is None: await interaction.followup.send("Active poll with that message ID not found.", ephemeral=True) return view = self.cog.active_polls[poll_id] view._ended = True if view.message: await view.message.edit(embed=view._build_embed(), view=None) self.cog.active_polls.pop(poll_id, None) await interaction.followup.send("✅ Poll ended and results posted.", ephemeral=True) async def setup(bot: commands.Bot) -> None: await bot.add_cog(Community(bot))