| """
|
| 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_COLORS = {
|
| "default": NEON_CYAN,
|
| "yes_no": NEON_LIME,
|
| "rating": NEON_YELLOW,
|
| "choice": NEON_PURPLE,
|
| "priority": NEON_ORANGE,
|
| "event": NEON_PINK,
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| )
|
|
|
|
|
| embed.add_field(
|
| name=question_label,
|
| value=f"**{self.question}**" if self.question else f"*{not_set}*",
|
| inline=False
|
| )
|
|
|
|
|
| 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_text = f"{self.duration} {minutes}" if self.duration > 0 else no_limit
|
| embed.add_field(name=duration_label, value=duration_text, inline=True)
|
|
|
|
|
| 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_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
|
|
|
|
|
| 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)
|
|
|
|
|
| end_time = time.time() + (self.duration * 60) if self.duration > 0 else None
|
|
|
|
|
| self.cog.poll_counter += 1
|
| poll_id = self.cog.poll_counter
|
|
|
|
|
| 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
|
|
|
|
|
| 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
|
| )
|
|
|
|
|
| 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()))
|
|
|
|
|
| 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))
|
| 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
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]] = {}
|
| self.message: discord.Message | None = None
|
| self._ended = False
|
|
|
|
|
| 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
|
| )
|
|
|
| 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:
|
|
|
| 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:
|
|
|
| if user_id in self.voters:
|
| old_vote = self.voters[user_id][0]
|
| if old_vote == option_idx:
|
|
|
| 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:
|
|
|
| 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
|
| )
|
|
|
|
|
| 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}**"
|
|
|
|
|
| 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
|
|
|
|
|
| bar_filled = int(ratio // 10)
|
| bar = "▓" * bar_filled + "░" * (10 - bar_filled)
|
|
|
|
|
| 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
|
| )
|
|
|
|
|
| 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)
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 <t:{end_time}:R>"
|
| 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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"<t:{int(end_time)}:R>", 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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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()
|
|
|
|
|
| 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))
|
|
|