test / bot /cogs /community.py
mtaaz's picture
Upload 94 files
91c7f83 verified
"""
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 <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}")
# ═══════════════════════════════════════════════════════════════════════════════
# 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"<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}")
# ═══════════════════════════════════════════════════════════════════════════════
# 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))