Major expansion: - All 8 modules fully expanded with advanced features - New: Embed Builder (webhook-based, templates, scheduling, variables) - New: Dashboard Integration (web config for all modules) - Serverlog: 30+ event types, invite tracking, per-event channels - Moderation: quarantine, reputation, warning decay, staff leaderboard, anti-raid, anti-nuke, 6 automod modules, appeals, cross-server bans - Economy: roulette, dice, fishing, mining, crafting, achievements, income roles, auction house, pets (6 types), heists, gambling stats - Tickets: auto-close, tags, HTML transcripts, staff tracking, reopen - Applications: staff voting, bulk actions, webhook notifications, templates - Suggestions: edit, merge, 10 statuses, tag system, auto-approve/deny - Reaction Roles: verification panels, exclusive groups, sticky/temp roles - Giveaways: drop mode, bonus entries, templates, DM notifications - All views use custom_id for persistence across restarts - Full syntax verification on all 13 Python files
1965 lines
96 KiB
Python
1965 lines
96 KiB
Python
"""NexusCore v2.0.0 — The ultimate all-in-one Red-DiscordBot cog.
|
||
|
||
Modules: Tickets, Applications, Suggestions, Reaction Roles, Giveaways,
|
||
Server Logging, Moderation, Economy, Embed Builder, Dashboard Integration.
|
||
|
||
100+ commands, persistent views, background tasks, full dashboard support.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import datetime
|
||
from typing import Optional
|
||
|
||
import discord
|
||
from redbot.core import Config, commands, checks
|
||
from redbot.core.bot import Red
|
||
|
||
from .utils import (
|
||
Clr, ok_embed, err_embed, info_embed, short_id, ts_now, ts_relative,
|
||
ts_full, duration_str, parse_duration, safe_send, safe_dm,
|
||
ConfirmView, Paginator, chunk_list,
|
||
)
|
||
from .tickets import TicketsMixin
|
||
from .applications import ApplicationsMixin
|
||
from .suggestions import SuggestionsMixin
|
||
from .reactionroles import ReactionRolesMixin
|
||
from .giveaways import GiveawaysMixin
|
||
from .serverlog import ServerLogMixin, EVENT_TYPES
|
||
from .moderation import ModerationMixin
|
||
from .economy import EconomyMixin, ShopView, new_deck, bj_value, card_str
|
||
from .embedbuilder import EmbedBuilderMixin
|
||
from .dashboard_integration import DashboardMixin
|
||
|
||
|
||
class NexusCore(
|
||
TicketsMixin,
|
||
ApplicationsMixin,
|
||
SuggestionsMixin,
|
||
ReactionRolesMixin,
|
||
GiveawaysMixin,
|
||
ServerLogMixin,
|
||
ModerationMixin,
|
||
EconomyMixin,
|
||
EmbedBuilderMixin,
|
||
DashboardMixin,
|
||
commands.Cog,
|
||
):
|
||
"""🔮 NexusCore — All-in-one server management mega-cog."""
|
||
|
||
__version__ = "2.0.0"
|
||
__author__ = "EveCogs"
|
||
|
||
def __init__(self, bot: Red):
|
||
self.bot = bot
|
||
self._init_tickets(bot)
|
||
self._init_applications(bot)
|
||
self._init_suggestions(bot)
|
||
self._init_reaction_roles(bot)
|
||
self._init_giveaways(bot)
|
||
self._init_serverlog(bot)
|
||
self._init_moderation(bot)
|
||
self._init_economy(bot)
|
||
self._init_embed_builder(bot)
|
||
try:
|
||
self._init_dashboard(bot)
|
||
except Exception:
|
||
pass
|
||
|
||
async def cog_load(self):
|
||
self._bg_tasks = []
|
||
self._bg_tasks.append(asyncio.create_task(self._start_auto_close_loop()))
|
||
self._bg_tasks.append(asyncio.create_task(self._start_gw_loop()))
|
||
self._bg_tasks.append(asyncio.create_task(self._warning_decay_loop()))
|
||
self._bg_tasks.append(asyncio.create_task(self._income_role_loop()))
|
||
self._bg_tasks.append(asyncio.create_task(self._scheduled_embed_loop()))
|
||
await self._load_rr_panels()
|
||
for guild in self.bot.guilds:
|
||
await self._cache_invites(guild)
|
||
|
||
async def cog_unload(self):
|
||
for task in self._bg_tasks:
|
||
task.cancel()
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# LISTENERS
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.Cog.listener()
|
||
async def on_message(self, message: discord.Message):
|
||
if message.author.bot or not message.guild:
|
||
return
|
||
await self._update_ticket_activity(message)
|
||
await self._check_automod(message)
|
||
self._cache_message(message)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_message_delete(self, message: discord.Message):
|
||
if message.author.bot:
|
||
return
|
||
await self._log_message_delete(message)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_message_edit(self, before: discord.Message, after: discord.Message):
|
||
if after.author.bot:
|
||
return
|
||
await self._log_message_edit(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_bulk_message_delete(self, messages):
|
||
await self._log_bulk_delete(messages)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_member_join(self, member: discord.Member):
|
||
await self._log_member_join(member)
|
||
await self._check_raid(member)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_member_remove(self, member: discord.Member):
|
||
await self._log_member_leave(member)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||
await self._log_member_update(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_member_ban(self, guild: discord.Guild, user: discord.User):
|
||
await self._log_member_ban(guild, user)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_member_unban(self, guild: discord.Guild, user: discord.User):
|
||
await self._log_member_unban(guild, user)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_channel_create(self, channel):
|
||
await self._log_channel_create(channel)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_channel_delete(self, channel):
|
||
await self._log_channel_delete(channel)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_channel_update(self, before, after):
|
||
await self._log_channel_update(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_role_create(self, role):
|
||
await self._log_role_create(role)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_role_delete(self, role):
|
||
await self._log_role_delete(role)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_role_update(self, before, after):
|
||
await self._log_role_update(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_voice_state_update(self, member, before, after):
|
||
await self._log_voice_update(member, before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_invite_create(self, invite):
|
||
await self._log_invite_create(invite)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_invite_delete(self, invite):
|
||
await self._log_invite_delete(invite)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_thread_create(self, thread):
|
||
await self._log_thread_create(thread)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_thread_delete(self, thread):
|
||
await self._log_thread_delete(thread)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_thread_update(self, before, after):
|
||
await self._log_thread_update(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_guild_update(self, before, after):
|
||
await self._log_guild_update(before, after)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
|
||
if payload.guild_id:
|
||
await self._handle_reaction_add(payload)
|
||
|
||
@commands.Cog.listener()
|
||
async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent):
|
||
if payload.guild_id:
|
||
await self._handle_reaction_remove(payload)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# NEXUS — TOP LEVEL GROUP
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="nexus", aliases=["nx"])
|
||
@commands.guild_only()
|
||
async def nexus(self, ctx: commands.Context):
|
||
"""🔮 NexusCore — All-in-one server management."""
|
||
if ctx.invoked_subcommand is None:
|
||
modules = [
|
||
("🎫 Tickets", "`[p]ticket`"),
|
||
("📋 Applications", "`[p]apply`"),
|
||
("💡 Suggestions", "`[p]suggest`"),
|
||
("🎭 Reaction Roles", "`[p]roles`"),
|
||
("🎉 Giveaways", "`[p]giveaway`"),
|
||
("📋 Server Logging", "`[p]serverlog`"),
|
||
("🛡️ Moderation", "`[p]nmod`"),
|
||
("🪙 Economy", "`[p]eco`"),
|
||
("📨 Embed Builder", "`[p]embedbuilder`"),
|
||
]
|
||
embed = discord.Embed(
|
||
title="🔮 NexusCore v2.0.0",
|
||
description="All-in-one server management with 100+ commands.",
|
||
colour=Clr.PRIMARY,
|
||
)
|
||
for name, cmd in modules:
|
||
embed.add_field(name=name, value=cmd, inline=True)
|
||
embed.set_footer(text="Use [p]help <command> for details")
|
||
await ctx.send(embed=embed)
|
||
|
||
@nexus.command(name="version")
|
||
async def nx_version(self, ctx):
|
||
"""Show NexusCore version."""
|
||
embed = discord.Embed(
|
||
title="🔮 NexusCore",
|
||
description=f"**Version:** {self.__version__}\n**Author:** {self.__author__}\n**Modules:** 10\n**Commands:** 100+",
|
||
colour=Clr.PRIMARY,
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@nexus.command(name="stats")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nx_stats(self, ctx):
|
||
"""Show NexusCore statistics."""
|
||
embed = discord.Embed(title="🔮 NexusCore Stats", colour=Clr.PRIMARY)
|
||
|
||
# Tickets
|
||
td = await self.ticket_config.guild(ctx.guild).all()
|
||
open_t = sum(1 for t in td["open_tickets"].values() if not t.get("closed"))
|
||
embed.add_field(name="🎫 Tickets", value=f"Open: {open_t} · Total: {td['counter']}", inline=True)
|
||
|
||
# Apps
|
||
ad = await self.app_config.guild(ctx.guild).all()
|
||
pending_a = sum(1 for s in ad["submissions"].values() if s["status"] == "pending")
|
||
embed.add_field(name="📋 Applications", value=f"Pending: {pending_a} · Total: {len(ad['submissions'])}", inline=True)
|
||
|
||
# Suggestions
|
||
sd = await self.suggest_config.guild(ctx.guild).all()
|
||
embed.add_field(name="💡 Suggestions", value=f"Total: {sd['counter']}", inline=True)
|
||
|
||
# RR
|
||
rrd = await self.rr_config.guild(ctx.guild).panels()
|
||
embed.add_field(name="🎭 Role Panels", value=str(len(rrd)), inline=True)
|
||
|
||
# Giveaways
|
||
gd = await self.give_config.guild(ctx.guild).all()
|
||
active_gw = sum(1 for g in gd["giveaways"].values() if not g["ended"])
|
||
embed.add_field(name="🎉 Giveaways", value=f"Active: {active_gw}", inline=True)
|
||
|
||
# Mod
|
||
md = await self.mod_config.guild(ctx.guild).all()
|
||
embed.add_field(name="🛡️ Cases", value=str(md["case_counter"]), inline=True)
|
||
|
||
# Economy
|
||
ed = await self.eco_config.guild(ctx.guild).all()
|
||
embed.add_field(name="🪙 Economy", value=f"{ed['currency_emoji']} {ed['currency_name']} · {len(ed.get('shop_items', {}))} items", inline=True)
|
||
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# TICKETS
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="ticket", aliases=["tickets"])
|
||
@commands.guild_only()
|
||
async def ticket(self, ctx: commands.Context):
|
||
"""🎫 Ticket system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@ticket.command(name="setup")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_setup(self, ctx, category: discord.CategoryChannel, log_channel: discord.TextChannel):
|
||
"""Setup the ticket system."""
|
||
await self.ticket_config.guild(ctx.guild).enabled.set(True)
|
||
await self.ticket_config.guild(ctx.guild).category_id.set(category.id)
|
||
await self.ticket_config.guild(ctx.guild).log_channel.set(log_channel.id)
|
||
await ctx.send(embed=ok_embed(f"Tickets enabled!\nCategory: {category.mention}\nLog: {log_channel.mention}"))
|
||
|
||
@ticket.command(name="panel")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_panel(self, ctx, channel: discord.TextChannel = None):
|
||
"""Send a ticket creation panel."""
|
||
channel = channel or ctx.channel
|
||
embed = discord.Embed(
|
||
title="🎫 Support Tickets",
|
||
description="Click the button below to create a ticket.\nA staff member will assist you shortly.",
|
||
colour=Clr.TICKET,
|
||
)
|
||
embed.set_footer(text="NexusCore Ticket System")
|
||
await channel.send(embed=embed, view=self._ticket_panel_view)
|
||
await ctx.send(embed=ok_embed(f"Panel sent to {channel.mention}"))
|
||
|
||
@ticket.command(name="addcategory")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_addcategory(self, ctx, name: str, *, description: str = ""):
|
||
"""Add a ticket category."""
|
||
async with self.ticket_config.guild(ctx.guild).categories() as cats:
|
||
cats[name.lower()] = {
|
||
"description": description, "roles": [],
|
||
"questions": [], "emoji": "🎫",
|
||
"channel_name_fmt": None, "greeting": None,
|
||
"default_priority": "medium",
|
||
}
|
||
await ctx.send(embed=ok_embed(f"Category **{name}** added."))
|
||
|
||
@ticket.command(name="addquestion")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_addquestion(self, ctx, category: str, *, question: str):
|
||
"""Add a question to a ticket category."""
|
||
async with self.ticket_config.guild(ctx.guild).categories() as cats:
|
||
cat = cats.get(category.lower())
|
||
if not cat:
|
||
return await ctx.send(embed=err_embed("Category not found."))
|
||
cat.setdefault("questions", []).append(question)
|
||
await ctx.send(embed=ok_embed(f"Question added to **{category}**."))
|
||
|
||
@ticket.command(name="addrole")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_addrole(self, ctx, category: str, role: discord.Role):
|
||
"""Add a staff role to a ticket category."""
|
||
async with self.ticket_config.guild(ctx.guild).categories() as cats:
|
||
cat = cats.get(category.lower())
|
||
if not cat:
|
||
return await ctx.send(embed=err_embed("Category not found."))
|
||
cat.setdefault("roles", []).append(role.id)
|
||
await ctx.send(embed=ok_embed(f"{role.mention} added to **{category}**."))
|
||
|
||
@ticket.command(name="transcript")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_transcript(self, ctx, channel: discord.TextChannel):
|
||
"""Set the transcript channel."""
|
||
await self.ticket_config.guild(ctx.guild).transcript_channel.set(channel.id)
|
||
await ctx.send(embed=ok_embed(f"Transcripts → {channel.mention}"))
|
||
|
||
@ticket.command(name="archive")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_archive(self, ctx, category: discord.CategoryChannel):
|
||
"""Set archive category (closed tickets move here instead of being deleted)."""
|
||
await self.ticket_config.guild(ctx.guild).archive_channel.set(category.id)
|
||
await ctx.send(embed=ok_embed(f"Archive category → {category.mention}"))
|
||
|
||
@ticket.command(name="autoclose")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_autoclose(self, ctx, hours: int):
|
||
"""Set auto-close inactivity timer (0 = disabled)."""
|
||
await self.ticket_config.guild(ctx.guild).auto_close_hours.set(hours)
|
||
await ctx.send(embed=ok_embed(f"Auto-close: {hours}h" if hours else "Auto-close disabled."))
|
||
|
||
@ticket.command(name="maxperuser")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_maxperuser(self, ctx, count: int):
|
||
"""Set max open tickets per user."""
|
||
await self.ticket_config.guild(ctx.guild).max_per_user.set(count)
|
||
await ctx.send(embed=ok_embed(f"Max per user: {count}"))
|
||
|
||
@ticket.command(name="addtag")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_addtag(self, ctx, name: str, *, description: str = ""):
|
||
"""Add a ticket tag."""
|
||
async with self.ticket_config.guild(ctx.guild).tags() as tags:
|
||
tags[name.lower()] = {"description": description, "colour": Clr.TICKET.value}
|
||
await ctx.send(embed=ok_embed(f"Tag **{name}** added."))
|
||
|
||
@ticket.command(name="add")
|
||
@checks.mod_or_permissions(manage_channels=True)
|
||
async def ticket_add(self, ctx, user: discord.Member):
|
||
"""Add a user to the current ticket."""
|
||
success = await self._add_user_to_ticket(ctx, user)
|
||
if not success:
|
||
await ctx.send(embed=err_embed("This isn't a ticket channel."))
|
||
|
||
@ticket.command(name="remove")
|
||
@checks.mod_or_permissions(manage_channels=True)
|
||
async def ticket_remove(self, ctx, user: discord.Member):
|
||
"""Remove a user from the current ticket."""
|
||
success = await self._remove_user_from_ticket(ctx, user)
|
||
if not success:
|
||
await ctx.send(embed=err_embed("This isn't a ticket channel."))
|
||
|
||
@ticket.command(name="rename")
|
||
@checks.mod_or_permissions(manage_channels=True)
|
||
async def ticket_rename(self, ctx, *, new_name: str):
|
||
"""Rename the current ticket channel."""
|
||
success = await self._rename_ticket(ctx, new_name)
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"Renamed to **{new_name}**"))
|
||
else:
|
||
await ctx.send(embed=err_embed("Not a ticket or renaming disabled."))
|
||
|
||
@ticket.command(name="reopen")
|
||
@checks.mod_or_permissions(manage_channels=True)
|
||
async def ticket_reopen(self, ctx, channel: discord.TextChannel = None):
|
||
"""Reopen a closed ticket."""
|
||
channel = channel or ctx.channel
|
||
success = await self._reopen_ticket(ctx, channel)
|
||
if not success:
|
||
await ctx.send(embed=err_embed("Not a closed ticket or reopen is disabled."))
|
||
|
||
@ticket.command(name="close")
|
||
async def ticket_close_cmd(self, ctx):
|
||
"""Close the current ticket."""
|
||
from .tickets import TicketControlView
|
||
class _FI:
|
||
guild = ctx.guild
|
||
user = ctx.author
|
||
channel = ctx.channel
|
||
async def response_send_message(self, *a, **k): await ctx.send(*a, **k)
|
||
response = type("R", (), {"send_message": lambda s, *a, **k: ctx.send(*a, **k), "defer": lambda s: None})()
|
||
async def followup_send(self, *a, **k): await ctx.send(*a, **k)
|
||
followup = type("F", (), {"send": lambda s, *a, **k: ctx.send(*a, **k)})()
|
||
fake = _FI()
|
||
fake.guild = ctx.guild
|
||
fake.user = ctx.author
|
||
fake.channel = ctx.channel
|
||
await self._close_ticket(fake)
|
||
|
||
@ticket.command(name="blacklist")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_blacklist(self, ctx, user: discord.Member):
|
||
"""Toggle blacklist a user from creating tickets."""
|
||
async with self.ticket_config.guild(ctx.guild).blacklisted() as bl:
|
||
if user.id in bl:
|
||
bl.remove(user.id)
|
||
await ctx.send(embed=ok_embed(f"{user.mention} unblacklisted."))
|
||
else:
|
||
bl.append(user.id)
|
||
await ctx.send(embed=ok_embed(f"{user.mention} blacklisted from tickets."))
|
||
|
||
@ticket.command(name="stats")
|
||
@checks.mod_or_permissions(manage_channels=True)
|
||
async def ticket_stats(self, ctx):
|
||
"""View ticket statistics."""
|
||
closed = await self.ticket_config.guild(ctx.guild).closed_tickets()
|
||
stats = self._get_ticket_stats(closed)
|
||
embed = discord.Embed(title="🎫 Ticket Statistics", colour=Clr.TICKET)
|
||
embed.add_field(name="Total Closed", value=str(stats["total"]), inline=True)
|
||
embed.add_field(name="Avg First Response", value=stats.get("avg_first_response", "N/A"), inline=True)
|
||
if stats.get("categories"):
|
||
cats = "\n".join(f"**{k}:** {v}" for k, v in stats["categories"].items())
|
||
embed.add_field(name="By Category", value=cats, inline=False)
|
||
await ctx.send(embed=embed)
|
||
|
||
@ticket.command(name="settings")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def ticket_settings(self, ctx):
|
||
"""View ticket settings."""
|
||
data = await self.ticket_config.guild(ctx.guild).all()
|
||
embed = discord.Embed(title="🎫 Ticket Settings", colour=Clr.TICKET)
|
||
embed.add_field(name="Enabled", value="✅" if data["enabled"] else "❌", inline=True)
|
||
embed.add_field(name="Max Per User", value=str(data["max_per_user"]), inline=True)
|
||
embed.add_field(name="Auto-Close", value=f"{data['auto_close_hours']}h" if data["auto_close_hours"] else "Off", inline=True)
|
||
embed.add_field(name="Claim", value="✅" if data["claim_enabled"] else "❌", inline=True)
|
||
embed.add_field(name="Feedback", value="✅" if data["feedback_enabled"] else "❌", inline=True)
|
||
embed.add_field(name="DM On Open", value="✅" if data["dm_on_open"] else "❌", inline=True)
|
||
cats = data.get("categories", {})
|
||
if cats:
|
||
embed.add_field(name="Categories", value=", ".join(cats.keys()), inline=False)
|
||
tags = data.get("tags", {})
|
||
if tags:
|
||
embed.add_field(name="Tags", value=", ".join(tags.keys()), inline=False)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# APPLICATIONS
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="apply", aliases=["application", "apps"])
|
||
@commands.guild_only()
|
||
async def apply(self, ctx: commands.Context):
|
||
"""📋 Application system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@apply.command(name="setup")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_setup(self, ctx, review_channel: discord.TextChannel):
|
||
"""Setup the application system."""
|
||
await self.app_config.guild(ctx.guild).enabled.set(True)
|
||
await self.app_config.guild(ctx.guild).review_channel.set(review_channel.id)
|
||
await ctx.send(embed=ok_embed(f"Applications enabled! Review → {review_channel.mention}"))
|
||
|
||
@apply.command(name="panel")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_panel(self, ctx, channel: discord.TextChannel = None):
|
||
"""Send an application panel."""
|
||
channel = channel or ctx.channel
|
||
embed = discord.Embed(
|
||
title="📋 Applications",
|
||
description="Click below to apply!",
|
||
colour=Clr.APP,
|
||
)
|
||
await channel.send(embed=embed, view=self._app_panel_view)
|
||
await ctx.send(embed=ok_embed(f"Panel sent to {channel.mention}"))
|
||
|
||
@apply.command(name="addtype")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_addtype(self, ctx, name: str, *, description: str = ""):
|
||
"""Add an application type."""
|
||
async with self.app_config.guild(ctx.guild).types() as types:
|
||
types[name.lower()] = {
|
||
"description": description, "enabled": True, "emoji": "📋",
|
||
"questions": [{"label": "Why are you applying?", "style": "long"}],
|
||
"role_on_accept": None, "role_on_deny": None,
|
||
"review_channel": None, "cooldown": None,
|
||
"accept_msg": None, "deny_msg": None,
|
||
"auto_thread": False,
|
||
"require_account_age_days": 0, "require_server_days": 0,
|
||
}
|
||
await ctx.send(embed=ok_embed(f"Type **{name}** added. Add questions: `[p]apply addquestion {name} <question>`"))
|
||
|
||
@apply.command(name="addquestion")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_addquestion(self, ctx, type_name: str, *, question: str):
|
||
"""Add a question to an application type."""
|
||
async with self.app_config.guild(ctx.guild).types() as types:
|
||
td = types.get(type_name.lower())
|
||
if not td:
|
||
return await ctx.send(embed=err_embed("Type not found."))
|
||
td.setdefault("questions", []).append({"label": question, "style": "long"})
|
||
await ctx.send(embed=ok_embed(f"Question added to **{type_name}**."))
|
||
|
||
@apply.command(name="setrole")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_setrole(self, ctx, type_name: str, accept_role: discord.Role, deny_role: discord.Role = None):
|
||
"""Set roles given on accept/deny."""
|
||
async with self.app_config.guild(ctx.guild).types() as types:
|
||
td = types.get(type_name.lower())
|
||
if not td:
|
||
return await ctx.send(embed=err_embed("Type not found."))
|
||
td["role_on_accept"] = accept_role.id
|
||
if deny_role:
|
||
td["role_on_deny"] = deny_role.id
|
||
await ctx.send(embed=ok_embed(f"Roles set for **{type_name}**."))
|
||
|
||
@apply.command(name="voting")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_voting(self, ctx, enabled: bool, threshold: int = 3):
|
||
"""Enable/disable voting on applications."""
|
||
await self.app_config.guild(ctx.guild).voting_enabled.set(enabled)
|
||
await self.app_config.guild(ctx.guild).voting_threshold.set(threshold)
|
||
await ctx.send(embed=ok_embed(f"Voting {'enabled' if enabled else 'disabled'} (threshold: {threshold})"))
|
||
|
||
@apply.command(name="webhook")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_webhook(self, ctx, url: str):
|
||
"""Set a webhook URL for application notifications."""
|
||
await self.app_config.guild(ctx.guild).webhook_url.set(url)
|
||
await ctx.send(embed=ok_embed("Webhook URL set."))
|
||
|
||
@apply.command(name="bulk")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_bulk(self, ctx, type_name: str = None):
|
||
"""Bulk accept/deny pending applications."""
|
||
data = await self.app_config.guild(ctx.guild).all()
|
||
pending = {k: v for k, v in data["submissions"].items() if v["status"] == "pending"}
|
||
if type_name:
|
||
pending = {k: v for k, v in pending.items() if v["type"] == type_name.lower()}
|
||
if not pending:
|
||
return await ctx.send(embed=info_embed("No pending applications."))
|
||
from .applications import BulkActionView
|
||
view = BulkActionView(self, list(pending.keys()))
|
||
await ctx.send(f"**{len(pending)}** pending applications. Choose action:", view=view)
|
||
|
||
@apply.command(name="savetemplate")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_savetemplate(self, ctx, type_name: str, template_name: str):
|
||
"""Save an application type as a template."""
|
||
success = await self._save_app_template(ctx.guild, template_name, type_name.lower())
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"Template **{template_name}** saved."))
|
||
else:
|
||
await ctx.send(embed=err_embed("Type not found."))
|
||
|
||
@apply.command(name="loadtemplate")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_loadtemplate(self, ctx, template_name: str, type_name: str):
|
||
"""Load a template into an application type."""
|
||
success = await self._load_app_template(ctx.guild, template_name, type_name.lower())
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"Template **{template_name}** → **{type_name}**"))
|
||
else:
|
||
await ctx.send(embed=err_embed("Template not found."))
|
||
|
||
@apply.command(name="stats")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def apply_stats(self, ctx):
|
||
"""View application statistics."""
|
||
data = await self.app_config.guild(ctx.guild).all()
|
||
stats = self._get_app_stats(data)
|
||
embed = discord.Embed(title="📋 Application Stats", colour=Clr.APP)
|
||
embed.add_field(name="Total", value=str(stats["total"]), inline=True)
|
||
for status, count in stats["by_status"].items():
|
||
embed.add_field(name=status.title(), value=str(count), inline=True)
|
||
s = stats.get("stats", {})
|
||
if s.get("avg_review_time"):
|
||
embed.add_field(name="Avg Review Time", value=duration_str(s["avg_review_time"]), inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@apply.command(name="settings")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def apply_settings(self, ctx):
|
||
"""View application settings."""
|
||
data = await self.app_config.guild(ctx.guild).all()
|
||
embed = discord.Embed(title="📋 Application Settings", colour=Clr.APP)
|
||
embed.add_field(name="Enabled", value="✅" if data["enabled"] else "❌", inline=True)
|
||
embed.add_field(name="Voting", value="✅" if data.get("voting_enabled") else "❌", inline=True)
|
||
embed.add_field(name="DM Results", value="✅" if data["dm_results"] else "❌", inline=True)
|
||
types = data.get("types", {})
|
||
if types:
|
||
embed.add_field(name="Types", value=", ".join(types.keys()), inline=False)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# SUGGESTIONS
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="suggest", aliases=["suggestion", "suggestions"])
|
||
@commands.guild_only()
|
||
async def suggest(self, ctx: commands.Context):
|
||
"""💡 Suggestion system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@suggest.command(name="setup")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def suggest_setup(self, ctx, channel: discord.TextChannel):
|
||
"""Setup the suggestion system."""
|
||
await self.suggest_config.guild(ctx.guild).enabled.set(True)
|
||
await self.suggest_config.guild(ctx.guild).channel.set(channel.id)
|
||
await ctx.send(embed=ok_embed(f"Suggestions enabled! Channel: {channel.mention}"))
|
||
|
||
@suggest.command(name="panel")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def suggest_panel(self, ctx, channel: discord.TextChannel = None):
|
||
"""Send a suggestion panel."""
|
||
channel = channel or ctx.channel
|
||
embed = discord.Embed(
|
||
title="💡 Suggestions", description="Submit your ideas!",
|
||
colour=Clr.SUGGEST,
|
||
)
|
||
await channel.send(embed=embed, view=self._suggest_panel_view)
|
||
await ctx.send(embed=ok_embed(f"Panel sent to {channel.mention}"))
|
||
|
||
@suggest.command(name="new")
|
||
async def suggest_new(self, ctx, *, content: str):
|
||
"""Submit a suggestion via command."""
|
||
class _FI:
|
||
guild = ctx.guild
|
||
user = ctx.author
|
||
async def response_defer(self, **k): pass
|
||
response = type("R", (), {"defer": lambda s, **k: None})()
|
||
async def followup_send(self, *a, **k): await ctx.send(*a, **k)
|
||
followup = type("F", (), {"send": lambda s, *a, **k: ctx.send(*a, **k)})()
|
||
fake = _FI()
|
||
fake.guild = ctx.guild
|
||
fake.user = ctx.author
|
||
await self._create_suggestion(fake, content)
|
||
await ctx.send(embed=ok_embed("Suggestion submitted!"))
|
||
|
||
@suggest.command(name="status")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def suggest_status(self, ctx, suggestion_id: str, *, status: str):
|
||
"""Set suggestion status."""
|
||
from .suggestions import STATUS_MAP
|
||
if status.lower() not in STATUS_MAP:
|
||
valid = ", ".join(STATUS_MAP.keys())
|
||
return await ctx.send(embed=err_embed(f"Valid statuses: {valid}"))
|
||
class _FI:
|
||
guild = ctx.guild
|
||
user = ctx.author
|
||
async def response_send_message(self, *a, **k): await ctx.send(*a, **k)
|
||
response = type("R", (), {"send_message": lambda s, *a, **k: ctx.send(*a, **k)})()
|
||
await self._set_status(_FI(), suggestion_id, status.lower())
|
||
|
||
@suggest.command(name="respond")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def suggest_respond(self, ctx, suggestion_id: str, *, response: str):
|
||
"""Add a staff response to a suggestion."""
|
||
conf = self.suggest_config.guild(ctx.guild)
|
||
async with conf.suggestions() as subs:
|
||
s = subs.get(suggestion_id)
|
||
if not s:
|
||
return await ctx.send(embed=err_embed("Not found."))
|
||
s["staff_response"] = response
|
||
s.setdefault("staff_responses", []).append({
|
||
"text": response, "author": ctx.author.id, "at": ts_now()
|
||
})
|
||
data = await conf.all()
|
||
channel = ctx.guild.get_channel(data["channel"])
|
||
if channel and s.get("message_id"):
|
||
try:
|
||
msg = await channel.fetch_message(s["message_id"])
|
||
if msg.embeds:
|
||
embed = msg.embeds[0]
|
||
embed.add_field(name=f"Staff Response — {ctx.author.display_name}", value=response[:1024], inline=False)
|
||
await msg.edit(embed=embed)
|
||
except discord.HTTPException:
|
||
pass
|
||
await ctx.send(embed=ok_embed("Response added."))
|
||
|
||
@suggest.command(name="edit")
|
||
async def suggest_edit(self, ctx, suggestion_id: str):
|
||
"""Edit your suggestion (within edit window)."""
|
||
data = await self.suggest_config.guild(ctx.guild).all()
|
||
s = data["suggestions"].get(suggestion_id)
|
||
if not s:
|
||
return await ctx.send(embed=err_embed("Not found."))
|
||
if s["user_id"] != ctx.author.id:
|
||
return await ctx.send(embed=err_embed("You can only edit your own suggestions."))
|
||
from .suggestions import EditSuggestionModal
|
||
# Can't send modal from a prefix command; give instructions
|
||
await ctx.send(embed=info_embed(f"To edit, use the suggestion panel button or DM-reply feature. Edit window: {data.get('edit_window', 300)}s."))
|
||
|
||
@suggest.command(name="merge")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def suggest_merge(self, ctx, target_id: str, source_id: str):
|
||
"""Merge a duplicate suggestion into another."""
|
||
success = await self._merge_suggestions(ctx.guild, target_id, source_id)
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"Suggestion #{source_id} merged into #{target_id}."))
|
||
else:
|
||
await ctx.send(embed=err_embed("One or both suggestions not found."))
|
||
|
||
@suggest.command(name="tag")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def suggest_tag(self, ctx, suggestion_id: str, *, tag: str):
|
||
"""Add a tag to a suggestion."""
|
||
success = await self._tag_suggestion(ctx.guild, suggestion_id, tag.lower())
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"Tag **{tag}** added."))
|
||
else:
|
||
await ctx.send(embed=err_embed("Not found."))
|
||
|
||
@suggest.command(name="addcategory")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def suggest_addcategory(self, ctx, *, category: str):
|
||
"""Add a suggestion category."""
|
||
async with self.suggest_config.guild(ctx.guild).categories() as cats:
|
||
if category.lower() not in cats:
|
||
cats.append(category.lower())
|
||
await ctx.send(embed=ok_embed(f"Category **{category}** added."))
|
||
|
||
@suggest.command(name="stats")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def suggest_stats(self, ctx):
|
||
"""View suggestion statistics."""
|
||
data = await self.suggest_config.guild(ctx.guild).all()
|
||
stats = self._get_suggestion_stats(data)
|
||
embed = discord.Embed(title="💡 Suggestion Stats", colour=Clr.SUGGEST)
|
||
embed.add_field(name="Total", value=str(stats["total"]), inline=True)
|
||
embed.add_field(name="Total Upvotes", value=str(stats["total_upvotes"]), inline=True)
|
||
embed.add_field(name="Total Downvotes", value=str(stats["total_downvotes"]), inline=True)
|
||
for status, count in stats["by_status"].items():
|
||
embed.add_field(name=status.title(), value=str(count), inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@suggest.command(name="settings")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def suggest_settings(self, ctx):
|
||
"""View suggestion settings."""
|
||
data = await self.suggest_config.guild(ctx.guild).all()
|
||
embed = discord.Embed(title="💡 Suggestion Settings", colour=Clr.SUGGEST)
|
||
ch = ctx.guild.get_channel(data["channel"]) if data["channel"] else None
|
||
embed.add_field(name="Enabled", value="✅" if data["enabled"] else "❌", inline=True)
|
||
embed.add_field(name="Channel", value=ch.mention if ch else "Not set", inline=True)
|
||
embed.add_field(name="Anonymous", value="✅" if data["anonymous_allowed"] else "❌", inline=True)
|
||
embed.add_field(name="Auto Thread", value="✅" if data["auto_thread"] else "❌", inline=True)
|
||
embed.add_field(name="Button Voting", value="✅" if data["voting_buttons"] else "❌", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# REACTION ROLES
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="roles", aliases=["rr", "reactionroles"])
|
||
@commands.guild_only()
|
||
@checks.admin_or_permissions(manage_roles=True)
|
||
async def roles(self, ctx: commands.Context):
|
||
"""🎭 Reaction role panels."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@roles.command(name="create")
|
||
async def rr_create(self, ctx, channel: discord.TextChannel, mode: str, *, title: str):
|
||
"""Create a role panel: [p]roles create #channel button My Panel Title
|
||
Modes: button, select, reaction"""
|
||
if mode not in ("button", "select", "reaction"):
|
||
return await ctx.send(embed=err_embed("Mode must be `button`, `select`, or `reaction`."))
|
||
panel_id = await self._create_rr_panel(ctx, channel, title, "Select your roles below!", mode)
|
||
await ctx.send(embed=ok_embed(f"Panel created! ID: `{panel_id}`\nNow add roles: `[p]roles add {panel_id} @Role`"))
|
||
|
||
@roles.command(name="add")
|
||
async def rr_add(self, ctx, panel_id: str, role: discord.Role, emoji: str = None, *, label: str = None):
|
||
"""Add a role to a panel."""
|
||
success = await self._add_role_to_panel(ctx.guild, panel_id, role, label=label, emoji=emoji)
|
||
if success:
|
||
await ctx.send(embed=ok_embed(f"**{role.name}** added to panel `{panel_id}`"))
|
||
else:
|
||
await ctx.send(embed=err_embed("Panel not found."))
|
||
|
||
@roles.command(name="exclusive")
|
||
async def rr_exclusive(self, ctx, panel_id: str, group_name: str, max_picks: int = 1):
|
||
"""Make a group exclusive: [p]roles exclusive panel_id colours 1"""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
panel = panels.get(panel_id)
|
||
if not panel:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panel.setdefault("exclusive_groups", {})[group_name] = max_picks
|
||
await ctx.send(embed=ok_embed(f"Group **{group_name}** is exclusive (max {max_picks})."))
|
||
|
||
@roles.command(name="setgroup")
|
||
async def rr_setgroup(self, ctx, panel_id: str, role: discord.Role, group_name: str):
|
||
"""Assign a role on a panel to a group."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
panel = panels.get(panel_id)
|
||
if not panel:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
for r in panel.get("roles", []):
|
||
if r["role_id"] == role.id:
|
||
r["group"] = group_name
|
||
break
|
||
await self._refresh_rr_panel(ctx.guild, panel_id)
|
||
await ctx.send(embed=ok_embed(f"**{role.name}** → group **{group_name}**"))
|
||
|
||
@roles.command(name="maxroles")
|
||
async def rr_maxroles(self, ctx, panel_id: str, count: int):
|
||
"""Set max roles a user can pick from a panel."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
if panel_id not in panels:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panels[panel_id]["max_roles"] = count
|
||
await ctx.send(embed=ok_embed(f"Max roles: {count}"))
|
||
|
||
@roles.command(name="sticky")
|
||
async def rr_sticky(self, ctx, panel_id: str):
|
||
"""Toggle sticky mode (roles can't be removed)."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
if panel_id not in panels:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panels[panel_id]["sticky"] = not panels[panel_id].get("sticky", False)
|
||
sticky = panels[panel_id]["sticky"]
|
||
await ctx.send(embed=ok_embed(f"Sticky mode {'enabled' if sticky else 'disabled'}"))
|
||
|
||
@roles.command(name="temp")
|
||
async def rr_temp(self, ctx, panel_id: str, minutes: int):
|
||
"""Set temporary role duration (0 = permanent)."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
if panel_id not in panels:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panels[panel_id]["temp_minutes"] = minutes
|
||
await ctx.send(embed=ok_embed(f"Temp role: {minutes}m" if minutes else "Roles are now permanent"))
|
||
|
||
@roles.command(name="require")
|
||
async def rr_require(self, ctx, panel_id: str, role: discord.Role):
|
||
"""Require a role to use a panel."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
if panel_id not in panels:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panels[panel_id]["require_role"] = role.id
|
||
await ctx.send(embed=ok_embed(f"Panel `{panel_id}` requires {role.mention}"))
|
||
|
||
@roles.command(name="clone")
|
||
async def rr_clone(self, ctx, panel_id: str, channel: discord.TextChannel):
|
||
"""Clone a role panel to another channel."""
|
||
new_id = await self._clone_panel(ctx.guild, panel_id, channel)
|
||
if new_id:
|
||
await ctx.send(embed=ok_embed(f"Cloned! New panel ID: `{new_id}`"))
|
||
else:
|
||
await ctx.send(embed=err_embed("Panel not found."))
|
||
|
||
@roles.command(name="verify")
|
||
async def rr_verify(self, ctx, channel: discord.TextChannel, role: discord.Role, *, title: str = "Verification"):
|
||
"""Create a verification panel."""
|
||
vpid = await self._create_verification_panel(ctx, channel, role, title=title)
|
||
await ctx.send(embed=ok_embed(f"Verification panel created! ID: `{vpid}`"))
|
||
|
||
@roles.command(name="list")
|
||
async def rr_list(self, ctx):
|
||
"""List all role panels."""
|
||
panels = await self.rr_config.guild(ctx.guild).panels()
|
||
if not panels:
|
||
return await ctx.send(embed=info_embed("No role panels."))
|
||
embed = discord.Embed(title="🎭 Role Panels", colour=Clr.ROLES)
|
||
for pid, p in panels.items():
|
||
ch = ctx.guild.get_channel(p["channel_id"])
|
||
embed.add_field(
|
||
name=f"`{pid}` — {p.get('title', 'Untitled')}",
|
||
value=f"Mode: {p.get('mode', '?')} · Roles: {len(p.get('roles', []))} · Channel: {ch.mention if ch else '?'}",
|
||
inline=False,
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@roles.command(name="delete")
|
||
async def rr_delete(self, ctx, panel_id: str):
|
||
"""Delete a role panel."""
|
||
async with self.rr_config.guild(ctx.guild).panels() as panels:
|
||
if panel_id not in panels:
|
||
return await ctx.send(embed=err_embed("Panel not found."))
|
||
panel = panels.pop(panel_id)
|
||
ch = ctx.guild.get_channel(panel["channel_id"])
|
||
if ch and panel.get("message_id"):
|
||
try:
|
||
msg = await ch.fetch_message(panel["message_id"])
|
||
await msg.delete()
|
||
except discord.HTTPException:
|
||
pass
|
||
await ctx.send(embed=ok_embed(f"Panel `{panel_id}` deleted."))
|
||
|
||
@roles.command(name="stats")
|
||
async def rr_stats(self, ctx):
|
||
"""View reaction role stats."""
|
||
stats = await self.rr_config.guild(ctx.guild).stats()
|
||
if not stats:
|
||
return await ctx.send(embed=info_embed("No role stats yet."))
|
||
embed = discord.Embed(title="🎭 Role Stats", colour=Clr.ROLES)
|
||
for rid, data in list(stats.items())[:20]:
|
||
role = ctx.guild.get_role(int(rid))
|
||
name = role.name if role else f"ID:{rid}"
|
||
embed.add_field(name=name, value=f"➕ {data.get('added', 0)} · ➖ {data.get('removed', 0)}", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# GIVEAWAYS
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="giveaway", aliases=["gw"])
|
||
@commands.guild_only()
|
||
async def giveaway(self, ctx: commands.Context):
|
||
"""🎉 Giveaway system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@giveaway.command(name="start")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_start(self, ctx, channel: discord.TextChannel, duration: str, winners: int, *, prize: str):
|
||
"""Start a giveaway: [p]gw start #channel 1d 1 Nitro Classic"""
|
||
dur = parse_duration(duration)
|
||
if not dur:
|
||
return await ctx.send(embed=err_embed("Invalid duration. Use e.g. `1d`, `12h`, `30m`."))
|
||
gw_id = await self._create_giveaway(ctx, channel, prize, dur, winners)
|
||
await ctx.send(embed=ok_embed(f"Giveaway started! ID: `{gw_id}`"))
|
||
|
||
@giveaway.command(name="drop")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_drop(self, ctx, channel: discord.TextChannel, winners: int, *, prize: str):
|
||
"""Start a drop giveaway (first N to click win)."""
|
||
gw_id = await self._create_giveaway(ctx, channel, prize, 86400, winners, drop_mode=True)
|
||
await ctx.send(embed=ok_embed(f"Drop giveaway started! ID: `{gw_id}`"))
|
||
|
||
@giveaway.command(name="end")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_end(self, ctx, gw_id: str):
|
||
"""End a giveaway early."""
|
||
await self._end_giveaway(ctx.guild, gw_id)
|
||
await ctx.send(embed=ok_embed("Giveaway ended."))
|
||
|
||
@giveaway.command(name="reroll")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_reroll_cmd(self, ctx, gw_id: str):
|
||
"""Reroll a giveaway winner."""
|
||
class _FI:
|
||
guild = ctx.guild
|
||
user = ctx.author
|
||
async def response_send_message(s, *a, **k): await ctx.send(*a, **k)
|
||
response = type("R", (), {"send_message": lambda s, *a, **k: ctx.send(*a, **k)})()
|
||
await self._reroll_giveaway(_FI(), gw_id)
|
||
|
||
@giveaway.command(name="list")
|
||
async def gw_list(self, ctx):
|
||
"""List active giveaways."""
|
||
data = await self.give_config.guild(ctx.guild).all()
|
||
active = {k: v for k, v in data["giveaways"].items() if not v["ended"]}
|
||
if not active:
|
||
return await ctx.send(embed=info_embed("No active giveaways."))
|
||
embed = discord.Embed(title="🎉 Active Giveaways", colour=Clr.GIVE)
|
||
for gid, gw in active.items():
|
||
embed.add_field(
|
||
name=f"`{gid}` — {gw['prize']}",
|
||
value=f"Ends: {ts_relative(gw['ends_at'])} · Entries: {len(gw['entries'])} · Winners: {gw['winners_count']}",
|
||
inline=False,
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@giveaway.command(name="require")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_require(self, ctx, gw_id: str, role: discord.Role):
|
||
"""Set a required role for a giveaway."""
|
||
async with self.give_config.guild(ctx.guild).giveaways() as gws:
|
||
if gw_id not in gws:
|
||
return await ctx.send(embed=err_embed("Not found."))
|
||
gws[gw_id]["require_role"] = role.id
|
||
await ctx.send(embed=ok_embed(f"Giveaway `{gw_id}` now requires {role.mention}"))
|
||
|
||
@giveaway.command(name="bonus")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_bonus(self, ctx, gw_id: str, role: discord.Role, entries: int):
|
||
"""Add bonus entries for a role."""
|
||
async with self.give_config.guild(ctx.guild).giveaways() as gws:
|
||
if gw_id not in gws:
|
||
return await ctx.send(embed=err_embed("Not found."))
|
||
gws[gw_id].setdefault("bonus_roles", {})[str(role.id)] = entries
|
||
await ctx.send(embed=ok_embed(f"{role.mention} gets +{entries} bonus entries in `{gw_id}`"))
|
||
|
||
@giveaway.command(name="savetemplate")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_savetemplate(self, ctx, name: str, channel: discord.TextChannel, duration: str, winners: int, *, prize: str):
|
||
"""Save a giveaway template."""
|
||
dur = parse_duration(duration)
|
||
if not dur:
|
||
return await ctx.send(embed=err_embed("Invalid duration."))
|
||
await self._save_gw_template(ctx.guild, name.lower(), {
|
||
"prize": prize, "duration": dur, "winners_count": winners, "channel_id": channel.id,
|
||
})
|
||
await ctx.send(embed=ok_embed(f"Template **{name}** saved."))
|
||
|
||
@giveaway.command(name="usetemplate")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def gw_usetemplate(self, ctx, name: str):
|
||
"""Start a giveaway from a template."""
|
||
template = await self._load_gw_template(ctx.guild, name.lower())
|
||
if not template:
|
||
return await ctx.send(embed=err_embed("Template not found."))
|
||
channel = ctx.guild.get_channel(template["channel_id"])
|
||
if not channel:
|
||
return await ctx.send(embed=err_embed("Template channel not found."))
|
||
gw_id = await self._create_giveaway(ctx, channel, template["prize"], template["duration"], template["winners_count"])
|
||
await ctx.send(embed=ok_embed(f"Giveaway started from template! ID: `{gw_id}`"))
|
||
|
||
@giveaway.command(name="stats")
|
||
async def gw_stats(self, ctx):
|
||
"""View giveaway statistics."""
|
||
data = await self.give_config.guild(ctx.guild).all()
|
||
stats = data.get("stats", {})
|
||
embed = discord.Embed(title="🎉 Giveaway Stats", colour=Clr.GIVE)
|
||
embed.add_field(name="Total Hosted", value=str(stats.get("total_hosted", 0)), inline=True)
|
||
embed.add_field(name="Total Entries", value=str(stats.get("total_entries", 0)), inline=True)
|
||
embed.add_field(name="Total Winners", value=str(stats.get("total_winners", 0)), inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# SERVER LOGGING
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="serverlog", aliases=["slog"])
|
||
@commands.guild_only()
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def serverlog(self, ctx: commands.Context):
|
||
"""📋 Server logging configuration."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@serverlog.command(name="enable")
|
||
async def slog_enable(self, ctx, channel: discord.TextChannel):
|
||
"""Enable logging with a default channel."""
|
||
await self.log_config.guild(ctx.guild).enabled.set(True)
|
||
await self.log_config.guild(ctx.guild).default_channel.set(channel.id)
|
||
await ctx.send(embed=ok_embed(f"Logging enabled! Default channel: {channel.mention}"))
|
||
|
||
@serverlog.command(name="disable")
|
||
async def slog_disable(self, ctx):
|
||
"""Disable logging."""
|
||
await self.log_config.guild(ctx.guild).enabled.set(False)
|
||
await ctx.send(embed=ok_embed("Logging disabled."))
|
||
|
||
@serverlog.command(name="set")
|
||
async def slog_set(self, ctx, event_type: str, channel: discord.TextChannel):
|
||
"""Set a specific channel for an event type."""
|
||
if event_type not in EVENT_TYPES:
|
||
return await ctx.send(embed=err_embed(f"Valid types: {', '.join(EVENT_TYPES[:10])}... ({len(EVENT_TYPES)} total)"))
|
||
async with self.log_config.guild(ctx.guild).channels() as channels:
|
||
channels[event_type] = channel.id
|
||
await ctx.send(embed=ok_embed(f"`{event_type}` → {channel.mention}"))
|
||
|
||
@serverlog.command(name="toggle")
|
||
async def slog_toggle(self, ctx, event_type: str):
|
||
"""Toggle an event type on/off."""
|
||
if event_type not in EVENT_TYPES:
|
||
return await ctx.send(embed=err_embed("Invalid event type."))
|
||
async with self.log_config.guild(ctx.guild).enabled_events() as events:
|
||
events[event_type] = not events.get(event_type, True)
|
||
state = events[event_type]
|
||
await ctx.send(embed=ok_embed(f"`{event_type}` {'enabled' if state else 'disabled'}"))
|
||
|
||
@serverlog.command(name="ignore")
|
||
async def slog_ignore(self, ctx, target: discord.TextChannel | discord.Role | discord.Member):
|
||
"""Ignore a channel, role, or user from logging."""
|
||
if isinstance(target, discord.TextChannel):
|
||
async with self.log_config.guild(ctx.guild).ignore_channels() as ic:
|
||
if target.id in ic:
|
||
ic.remove(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} unignored."))
|
||
else:
|
||
ic.append(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} ignored."))
|
||
elif isinstance(target, discord.Role):
|
||
async with self.log_config.guild(ctx.guild).ignore_roles() as ir:
|
||
if target.id in ir:
|
||
ir.remove(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} unignored."))
|
||
else:
|
||
ir.append(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} ignored."))
|
||
elif isinstance(target, discord.Member):
|
||
async with self.log_config.guild(ctx.guild).ignore_users() as iu:
|
||
if target.id in iu:
|
||
iu.remove(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} unignored."))
|
||
else:
|
||
iu.append(target.id)
|
||
await ctx.send(embed=ok_embed(f"{target.mention} ignored."))
|
||
|
||
@serverlog.command(name="events")
|
||
async def slog_events(self, ctx):
|
||
"""List all event types and their status."""
|
||
data = await self.log_config.guild(ctx.guild).all()
|
||
events = data.get("enabled_events", {})
|
||
lines = []
|
||
for et in EVENT_TYPES:
|
||
status = "✅" if events.get(et, True) else "❌"
|
||
lines.append(f"{status} `{et}`")
|
||
pages = []
|
||
for chunk in chunk_list(lines, 15):
|
||
embed = discord.Embed(title="📋 Log Events", description="\n".join(chunk), colour=Clr.LOG)
|
||
pages.append(embed)
|
||
pag = Paginator(pages, author_id=ctx.author.id)
|
||
await pag.send(ctx)
|
||
|
||
@serverlog.command(name="settings")
|
||
async def slog_settings(self, ctx):
|
||
"""View logging settings."""
|
||
data = await self.log_config.guild(ctx.guild).all()
|
||
embed = discord.Embed(title="📋 Logging Settings", colour=Clr.LOG)
|
||
embed.add_field(name="Enabled", value="✅" if data["enabled"] else "❌", inline=True)
|
||
dc = ctx.guild.get_channel(data["default_channel"]) if data["default_channel"] else None
|
||
embed.add_field(name="Default Channel", value=dc.mention if dc else "Not set", inline=True)
|
||
embed.add_field(name="Ignore Bots", value="✅" if data["ignore_bots"] else "❌", inline=True)
|
||
embed.add_field(name="Events Tracked", value=f"{sum(1 for v in data.get('enabled_events', {}).values() if v)}/{len(EVENT_TYPES)}", inline=True)
|
||
overrides = []
|
||
for evt, ch_id in data.get("channels", {}).items():
|
||
ch = ctx.guild.get_channel(ch_id)
|
||
if ch:
|
||
overrides.append(f"`{evt}` → {ch.mention}")
|
||
if overrides:
|
||
embed.add_field(name="Channel Overrides", value="\n".join(overrides[:10]), inline=False)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# MODERATION
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="nmod", aliases=["nexusmod"])
|
||
@commands.guild_only()
|
||
async def nmod(self, ctx: commands.Context):
|
||
"""🛡️ NexusCore Moderation."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@nmod.command(name="setup")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nmod_setup(self, ctx, modlog: discord.TextChannel):
|
||
"""Set the modlog channel."""
|
||
await self.mod_config.guild(ctx.guild).modlog_channel.set(modlog.id)
|
||
await ctx.send(embed=ok_embed(f"Modlog: {modlog.mention}"))
|
||
|
||
@nmod.command(name="warn")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_warn(self, ctx, user: discord.Member, *, reason: str = "No reason"):
|
||
"""Warn a user."""
|
||
if user.top_role >= ctx.author.top_role and ctx.author != ctx.guild.owner:
|
||
return await ctx.send(embed=err_embed("Can't warn someone with equal or higher role."))
|
||
case_id = await self._warn_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"⚠️ {user.mention} warned (Case #{case_id}): {reason}"))
|
||
|
||
@nmod.command(name="mute")
|
||
@checks.mod_or_permissions(moderate_members=True)
|
||
async def nmod_mute(self, ctx, user: discord.Member, duration: str, *, reason: str = "No reason"):
|
||
"""Mute (timeout) a user."""
|
||
dur = parse_duration(duration)
|
||
if not dur:
|
||
return await ctx.send(embed=err_embed("Invalid duration."))
|
||
case_id = await self._mute_user(ctx, user, dur, reason)
|
||
await ctx.send(embed=ok_embed(f"🔇 {user.mention} muted for {duration_str(dur)} (Case #{case_id})"))
|
||
|
||
@nmod.command(name="unmute")
|
||
@checks.mod_or_permissions(moderate_members=True)
|
||
async def nmod_unmute(self, ctx, user: discord.Member, *, reason: str = "Unmuted"):
|
||
"""Unmute a user."""
|
||
case_id = await self._unmute_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"🔊 {user.mention} unmuted (Case #{case_id})"))
|
||
|
||
@nmod.command(name="kick")
|
||
@checks.mod_or_permissions(kick_members=True)
|
||
async def nmod_kick(self, ctx, user: discord.Member, *, reason: str = "No reason"):
|
||
"""Kick a user."""
|
||
await self._kick_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"👢 {user.mention} kicked: {reason}"))
|
||
|
||
@nmod.command(name="ban")
|
||
@checks.mod_or_permissions(ban_members=True)
|
||
async def nmod_ban(self, ctx, user: discord.User, *, reason: str = "No reason"):
|
||
"""Ban a user."""
|
||
await self._ban_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"🔨 {user.mention} banned: {reason}"))
|
||
|
||
@nmod.command(name="softban")
|
||
@checks.mod_or_permissions(ban_members=True)
|
||
async def nmod_softban(self, ctx, user: discord.Member, *, reason: str = "No reason"):
|
||
"""Softban (ban + unban to purge messages)."""
|
||
await self._softban_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"🧹 {user.mention} softbanned: {reason}"))
|
||
|
||
@nmod.command(name="tempban")
|
||
@checks.mod_or_permissions(ban_members=True)
|
||
async def nmod_tempban(self, ctx, user: discord.Member, duration: str, *, reason: str = "No reason"):
|
||
"""Temporarily ban a user."""
|
||
dur = parse_duration(duration)
|
||
if not dur:
|
||
return await ctx.send(embed=err_embed("Invalid duration."))
|
||
await self._tempban_user(ctx, user, dur, reason)
|
||
await ctx.send(embed=ok_embed(f"⏰ {user.mention} temp-banned for {duration_str(dur)}: {reason}"))
|
||
|
||
@nmod.command(name="unban")
|
||
@checks.mod_or_permissions(ban_members=True)
|
||
async def nmod_unban(self, ctx, user_id: int, *, reason: str = "Unbanned"):
|
||
"""Unban a user by ID."""
|
||
user = await self.bot.fetch_user(user_id)
|
||
await self._unban_user(ctx, user, reason)
|
||
await ctx.send(embed=ok_embed(f"🔓 {user} unbanned: {reason}"))
|
||
|
||
@nmod.command(name="note")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_note(self, ctx, user: discord.User, *, text: str):
|
||
"""Add a staff note on a user."""
|
||
await self._add_note(ctx.guild, user, ctx.author, text)
|
||
await ctx.send(embed=ok_embed(f"📝 Note added for {user.mention}"))
|
||
|
||
@nmod.command(name="notes")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_notes(self, ctx, user: discord.User):
|
||
"""View notes on a user."""
|
||
notes = await self.mod_config.guild(ctx.guild).notes()
|
||
user_notes = notes.get(str(user.id), [])
|
||
if not user_notes:
|
||
return await ctx.send(embed=info_embed(f"No notes for {user}."))
|
||
embed = discord.Embed(title=f"📝 Notes — {user}", colour=Clr.MOD)
|
||
for n in user_notes[-10:]:
|
||
embed.add_field(
|
||
name=f"<@{n['author_id']}> · {ts_relative(n['timestamp'])}",
|
||
value=n["text"][:1024], inline=False,
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@nmod.command(name="warnings", aliases=["warns"])
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_warnings(self, ctx, user: discord.User):
|
||
"""View a user's warnings."""
|
||
warnings = await self.mod_config.guild(ctx.guild).warnings()
|
||
user_warns = warnings.get(str(user.id), [])
|
||
if not user_warns:
|
||
return await ctx.send(embed=info_embed(f"No warnings for {user}."))
|
||
embed = discord.Embed(title=f"⚠️ Warnings — {user}", colour=Clr.MOD)
|
||
for w in user_warns[-15:]:
|
||
embed.add_field(
|
||
name=f"Case #{w['id']} · {ts_relative(w['timestamp'])}",
|
||
value=f"{w['reason']} (by <@{w['mod_id']}>)", inline=False,
|
||
)
|
||
embed.set_footer(text=f"Total: {len(user_warns)} warnings")
|
||
await ctx.send(embed=embed)
|
||
|
||
@nmod.command(name="clearwarns")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_clearwarns(self, ctx, user: discord.User):
|
||
"""Clear all warnings for a user."""
|
||
async with self.mod_config.guild(ctx.guild).warnings() as warnings:
|
||
warnings.pop(str(user.id), None)
|
||
await ctx.send(embed=ok_embed(f"Warnings cleared for {user.mention}"))
|
||
|
||
@nmod.command(name="history")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_history(self, ctx, user: discord.User):
|
||
"""View a user's full moderation history."""
|
||
cases = await self.mod_config.guild(ctx.guild).cases()
|
||
user_cases = {cid: c for cid, c in cases.items() if c["user_id"] == user.id}
|
||
if not user_cases:
|
||
return await ctx.send(embed=info_embed(f"No cases for {user}."))
|
||
pages = []
|
||
for chunk in chunk_list(list(user_cases.items()), 5):
|
||
embed = discord.Embed(title=f"📋 History — {user}", colour=Clr.MOD)
|
||
for cid, c in chunk:
|
||
embed.add_field(
|
||
name=f"#{cid} {c['type'].upper()} · {ts_relative(c['timestamp'])}",
|
||
value=f"{c['reason'][:200]} (by <@{c['mod_id']}>)", inline=False,
|
||
)
|
||
pages.append(embed)
|
||
pag = Paginator(pages, author_id=ctx.author.id)
|
||
await pag.send(ctx)
|
||
|
||
@nmod.command(name="quarantine")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nmod_quarantine(self, ctx, user: discord.Member, *, reason: str = "Quarantined"):
|
||
"""Quarantine a user (assigns quarantine role)."""
|
||
case_id = await self._quarantine_user(ctx, user, reason)
|
||
if case_id:
|
||
await ctx.send(embed=ok_embed(f"🔒 {user.mention} quarantined (Case #{case_id})"))
|
||
else:
|
||
await ctx.send(embed=err_embed("Set quarantine role first: `[p]nmod setquarantine @role`"))
|
||
|
||
@nmod.command(name="unquarantine")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nmod_unquarantine(self, ctx, user: discord.Member, *, reason: str = "Released"):
|
||
"""Release a user from quarantine."""
|
||
case_id = await self._unquarantine_user(ctx, user, reason)
|
||
if case_id:
|
||
await ctx.send(embed=ok_embed(f"🔓 {user.mention} released (Case #{case_id})"))
|
||
else:
|
||
await ctx.send(embed=err_embed("No quarantine role set."))
|
||
|
||
@nmod.command(name="setquarantine")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_setquarantine(self, ctx, role: discord.Role):
|
||
"""Set the quarantine role."""
|
||
await self.mod_config.guild(ctx.guild).quarantine_role.set(role.id)
|
||
await ctx.send(embed=ok_embed(f"Quarantine role: {role.mention}"))
|
||
|
||
@nmod.command(name="reputation", aliases=["rep"])
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_reputation(self, ctx, user: discord.Member):
|
||
"""View a user's reputation score."""
|
||
rep = await self._get_reputation(ctx.guild, user)
|
||
embed = discord.Embed(title=f"📊 Reputation — {user.display_name}", colour=Clr.MOD)
|
||
embed.add_field(name="Score", value=str(rep), inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@nmod.command(name="lockdown")
|
||
@checks.admin_or_permissions(manage_channels=True)
|
||
async def nmod_lockdown(self, ctx, channel: discord.TextChannel = None):
|
||
"""Lock a channel (or current channel)."""
|
||
channel = channel or ctx.channel
|
||
await self._lockdown_channel(channel, f"Locked by {ctx.author}")
|
||
await ctx.send(embed=ok_embed(f"🔒 {channel.mention} locked."))
|
||
|
||
@nmod.command(name="unlock")
|
||
@checks.admin_or_permissions(manage_channels=True)
|
||
async def nmod_unlock(self, ctx, channel: discord.TextChannel = None):
|
||
"""Unlock a channel."""
|
||
channel = channel or ctx.channel
|
||
await self._unlock_channel(channel, f"Unlocked by {ctx.author}")
|
||
await ctx.send(embed=ok_embed(f"🔓 {channel.mention} unlocked."))
|
||
|
||
@nmod.command(name="serverlockdown")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_serverlockdown(self, ctx, *, reason: str = "Server lockdown"):
|
||
"""Lock ALL text channels in the server."""
|
||
view = ConfirmView(ctx.author.id)
|
||
msg = await ctx.send("⚠️ This will lock **all** text channels. Continue?", view=view)
|
||
await view.wait()
|
||
if view.value:
|
||
await self._lockdown_server(ctx.guild, ctx.author, reason)
|
||
await ctx.send(embed=ok_embed(f"🔒 Server lockdown activated: {reason}"))
|
||
else:
|
||
await ctx.send(embed=info_embed("Cancelled."))
|
||
|
||
@nmod.command(name="antiraid")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_antiraid(self, ctx, enabled: bool, threshold: int = 10, window: int = 10, action: str = "lockdown"):
|
||
"""Configure anti-raid: [p]nmod antiraid true 10 10 lockdown"""
|
||
await self.mod_config.guild(ctx.guild).anti_raid.set({
|
||
"enabled": enabled, "join_threshold": threshold, "join_window": window,
|
||
"action": action, "notify_channel": ctx.channel.id,
|
||
})
|
||
await ctx.send(embed=ok_embed(f"Anti-raid {'enabled' if enabled else 'disabled'}: {threshold} joins in {window}s → {action}"))
|
||
|
||
@nmod.command(name="antinuke")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_antinuke(self, ctx, enabled: bool, action: str = "strip_roles"):
|
||
"""Configure anti-nuke: [p]nmod antinuke true strip_roles"""
|
||
async with self.mod_config.guild(ctx.guild).anti_nuke() as an:
|
||
an["enabled"] = enabled
|
||
an["action"] = action
|
||
await ctx.send(embed=ok_embed(f"Anti-nuke {'enabled' if enabled else 'disabled'}: action → {action}"))
|
||
|
||
@nmod.command(name="automod")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nmod_automod(self, ctx, module: str, enabled: bool):
|
||
"""Toggle auto-mod modules: anti_spam, anti_caps, anti_invite, anti_links, anti_mention, anti_newlines."""
|
||
valid = ["anti_spam", "anti_caps", "anti_invite", "anti_links", "anti_mention", "anti_newlines"]
|
||
if module not in valid:
|
||
return await ctx.send(embed=err_embed(f"Valid modules: {', '.join(valid)}"))
|
||
async with self.mod_config.guild(ctx.guild).auto_mod() as am:
|
||
if module in am:
|
||
am[module]["enabled"] = enabled
|
||
await ctx.send(embed=ok_embed(f"`{module}` {'enabled' if enabled else 'disabled'}"))
|
||
|
||
@nmod.command(name="escalation")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_escalation(self, ctx, warn_count: int, action: str):
|
||
"""Set an escalation threshold: [p]nmod escalation 3 mute_1h"""
|
||
async with self.mod_config.guild(ctx.guild).escalation() as esc:
|
||
esc.setdefault("thresholds", {})[str(warn_count)] = action
|
||
await ctx.send(embed=ok_embed(f"{warn_count} warnings → `{action}`"))
|
||
|
||
@nmod.command(name="warndecay")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_warndecay(self, ctx, days: int, amount: int = 1):
|
||
"""Set warning decay (days until old warnings are removed)."""
|
||
await self.mod_config.guild(ctx.guild).warn_decay_days.set(days)
|
||
await self.mod_config.guild(ctx.guild).warn_decay_amount.set(amount)
|
||
await ctx.send(embed=ok_embed(f"Warning decay: {amount} warning(s) removed after {days} days" if days else "Decay disabled."))
|
||
|
||
@nmod.command(name="appeal")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_appeal(self, ctx, channel: discord.TextChannel):
|
||
"""Set the appeal channel and enable appeals."""
|
||
await self.mod_config.guild(ctx.guild).appeal_channel.set(channel.id)
|
||
await self.mod_config.guild(ctx.guild).appeal_enabled.set(True)
|
||
await ctx.send(embed=ok_embed(f"Appeals enabled! Channel: {channel.mention}"))
|
||
|
||
@nmod.command(name="appealpanel")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_appealpanel(self, ctx, channel: discord.TextChannel = None):
|
||
"""Send an appeal button panel."""
|
||
channel = channel or ctx.channel
|
||
embed = discord.Embed(
|
||
title="📨 Appeals",
|
||
description="Click below to submit an appeal for a moderation action.",
|
||
colour=Clr.MOD,
|
||
)
|
||
from .moderation import AppealButtonView
|
||
await channel.send(embed=embed, view=AppealButtonView(self))
|
||
await ctx.send(embed=ok_embed(f"Appeal panel sent to {channel.mention}"))
|
||
|
||
@nmod.command(name="purge")
|
||
@checks.mod_or_permissions(manage_messages=True)
|
||
async def nmod_purge(self, ctx, count: int, user: discord.Member = None):
|
||
"""Purge messages (optionally from a specific user)."""
|
||
if count > 500:
|
||
return await ctx.send(embed=err_embed("Max 500 messages."))
|
||
def check(m):
|
||
if user:
|
||
return m.author.id == user.id
|
||
return True
|
||
deleted = await ctx.channel.purge(limit=count, check=check)
|
||
await ctx.send(embed=ok_embed(f"🗑️ Purged {len(deleted)} messages."), delete_after=5)
|
||
|
||
@nmod.command(name="massban")
|
||
@checks.admin_or_permissions(ban_members=True)
|
||
async def nmod_massban(self, ctx, *user_ids: int):
|
||
"""Ban multiple users by ID."""
|
||
if not user_ids:
|
||
return await ctx.send(embed=err_embed("Provide user IDs."))
|
||
banned = 0
|
||
for uid in user_ids[:50]:
|
||
try:
|
||
user = await self.bot.fetch_user(uid)
|
||
await ctx.guild.ban(user, reason=f"Massban by {ctx.author}")
|
||
banned += 1
|
||
except discord.HTTPException:
|
||
pass
|
||
await ctx.send(embed=ok_embed(f"Banned {banned}/{len(user_ids)} users."))
|
||
|
||
@nmod.command(name="stafflb", aliases=["staffleaderboard"])
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def nmod_stafflb(self, ctx):
|
||
"""View staff moderation leaderboard."""
|
||
stats = await self.mod_config.guild(ctx.guild).staff_stats()
|
||
if not stats:
|
||
return await ctx.send(embed=info_embed("No staff stats."))
|
||
sorted_stats = sorted(stats.items(), key=lambda x: sum(x[1].values()), reverse=True)
|
||
embed = discord.Embed(title="🛡️ Staff Leaderboard", colour=Clr.MOD)
|
||
for i, (mod_id, s) in enumerate(sorted_stats[:15], 1):
|
||
member = ctx.guild.get_member(int(mod_id))
|
||
name = member.display_name if member else f"ID:{mod_id}"
|
||
total = sum(s.values())
|
||
embed.add_field(
|
||
name=f"#{i} {name} — {total} actions",
|
||
value=f"⚠️ {s.get('warns', 0)} · 🔇 {s.get('mutes', 0)} · 👢 {s.get('kicks', 0)} · 🔨 {s.get('bans', 0)}",
|
||
inline=False,
|
||
)
|
||
await ctx.send(embed=embed)
|
||
|
||
@nmod.command(name="crossban")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def nmod_crossban(self, ctx, enabled: bool, webhook_url: str = None):
|
||
"""Enable cross-server ban sync via webhook."""
|
||
await self.mod_config.guild(ctx.guild).cross_server_ban.set({
|
||
"enabled": enabled, "webhook_url": webhook_url, "log_only": True,
|
||
})
|
||
await ctx.send(embed=ok_embed(f"Cross-server ban sync {'enabled' if enabled else 'disabled'}"))
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# ECONOMY
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="eco", aliases=["economy"])
|
||
@commands.guild_only()
|
||
async def eco(self, ctx: commands.Context):
|
||
"""🪙 Economy system."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@eco.command(name="balance", aliases=["bal"])
|
||
async def eco_bal(self, ctx, user: discord.Member = None):
|
||
"""Check your (or someone's) balance."""
|
||
user = user or ctx.author
|
||
wallet, bank = await self._get_balance(user)
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
emoji = data["currency_emoji"]
|
||
embed = discord.Embed(title=f"{emoji} {user.display_name}'s Balance", colour=Clr.ECO)
|
||
embed.add_field(name="Wallet", value=f"{emoji} {wallet:,}", inline=True)
|
||
embed.add_field(name="Bank", value=f"{emoji} {bank:,}", inline=True)
|
||
embed.add_field(name="Total", value=f"{emoji} {wallet + bank:,}", inline=True)
|
||
embed.set_thumbnail(url=user.display_avatar.url if user.display_avatar else None)
|
||
await ctx.send(embed=embed)
|
||
await self._check_millionaire(user)
|
||
|
||
@eco.command(name="daily")
|
||
async def eco_daily(self, ctx):
|
||
"""Claim your daily reward."""
|
||
await self._daily(ctx)
|
||
|
||
@eco.command(name="weekly")
|
||
async def eco_weekly(self, ctx):
|
||
"""Claim your weekly reward."""
|
||
await self._weekly(ctx)
|
||
|
||
@eco.command(name="work")
|
||
async def eco_work(self, ctx):
|
||
"""Work for coins."""
|
||
await self._work(ctx)
|
||
|
||
@eco.command(name="crime")
|
||
async def eco_crime(self, ctx):
|
||
"""Attempt a crime (risky!)."""
|
||
await self._crime(ctx)
|
||
|
||
@eco.command(name="rob")
|
||
async def eco_rob(self, ctx, target: discord.Member):
|
||
"""Rob another user."""
|
||
await self._rob(ctx, target)
|
||
|
||
@eco.command(name="deposit", aliases=["dep"])
|
||
async def eco_deposit(self, ctx, amount: str):
|
||
"""Deposit coins to bank."""
|
||
if amount.lower() == "all":
|
||
amount = await self.eco_config.member(ctx.author).wallet()
|
||
else:
|
||
amount = int(amount)
|
||
await self._deposit(ctx, amount)
|
||
|
||
@eco.command(name="withdraw", aliases=["with"])
|
||
async def eco_withdraw(self, ctx, amount: str):
|
||
"""Withdraw coins from bank."""
|
||
if amount.lower() == "all":
|
||
amount = await self.eco_config.member(ctx.author).bank()
|
||
else:
|
||
amount = int(amount)
|
||
await self._withdraw(ctx, amount)
|
||
|
||
@eco.command(name="pay", aliases=["give", "transfer"])
|
||
async def eco_pay(self, ctx, user: discord.Member, amount: int):
|
||
"""Pay another user."""
|
||
if user.id == ctx.author.id:
|
||
return await ctx.send(embed=err_embed("Can't pay yourself."))
|
||
if amount <= 0:
|
||
return await ctx.send(embed=err_embed("Amount must be positive."))
|
||
wallet = await self.eco_config.member(ctx.author).wallet()
|
||
if amount > wallet:
|
||
return await ctx.send(embed=err_embed("Not enough coins."))
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
tax = int(amount * data["tax_rate"] / 100)
|
||
received = amount - tax
|
||
await self._remove_balance(ctx.author, amount)
|
||
await self._add_balance(user, received)
|
||
await self._add_transaction(ctx.author, -amount, f"Paid {user}")
|
||
await self._add_transaction(user, received, f"Received from {ctx.author}")
|
||
desc = f"Sent **{await self._format_amount(ctx.guild, received)}** to {user.mention}"
|
||
if tax:
|
||
desc += f"\n(Tax: {tax:,})"
|
||
await ctx.send(embed=ok_embed(desc))
|
||
|
||
@eco.command(name="leaderboard", aliases=["lb", "top"])
|
||
async def eco_lb(self, ctx, page: int = 1):
|
||
"""View the richest users."""
|
||
all_members = await self.eco_config.all_members(ctx.guild)
|
||
rankings = []
|
||
for uid, data in all_members.items():
|
||
total = data.get("wallet", 0) + data.get("bank", 0)
|
||
if total > 0:
|
||
rankings.append((uid, total))
|
||
rankings.sort(key=lambda x: x[1], reverse=True)
|
||
per_page = 10
|
||
start = (page - 1) * per_page
|
||
chunk = rankings[start:start + per_page]
|
||
if not chunk:
|
||
return await ctx.send(embed=info_embed("No data."))
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
emoji = data["currency_emoji"]
|
||
lines = []
|
||
for i, (uid, total) in enumerate(chunk, start=start + 1):
|
||
medal = {1: "🥇", 2: "🥈", 3: "🥉"}.get(i, f"**{i}.**")
|
||
lines.append(f"{medal} <@{uid}> — {emoji} {total:,}")
|
||
embed = discord.Embed(title=f"{emoji} Leaderboard", description="\n".join(lines), colour=Clr.ECO)
|
||
embed.set_footer(text=f"Page {page} · {len(rankings)} total users")
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="coinflip", aliases=["cf"])
|
||
async def eco_coinflip(self, ctx, bet: int, choice: str):
|
||
"""Coinflip! [p]eco cf 100 heads"""
|
||
await self._coinflip(ctx, bet, choice)
|
||
|
||
@eco.command(name="slots")
|
||
async def eco_slots(self, ctx, bet: int):
|
||
"""Play the slot machine!"""
|
||
await self._slots(ctx, bet)
|
||
|
||
@eco.command(name="blackjack", aliases=["bj"])
|
||
async def eco_blackjack(self, ctx, bet: int):
|
||
"""Play blackjack!"""
|
||
await self._blackjack(ctx, bet)
|
||
|
||
@eco.command(name="roulette")
|
||
async def eco_roulette(self, ctx, bet: int, *, choice: str):
|
||
"""Play roulette! Choices: red, black, even, odd, high, low, or 0-36"""
|
||
await self._roulette(ctx, bet, choice)
|
||
|
||
@eco.command(name="dice")
|
||
async def eco_dice(self, ctx, bet: int, guess: int):
|
||
"""Roll dice! Guess the total (2-12)."""
|
||
await self._dice(ctx, bet, guess)
|
||
|
||
@eco.command(name="fish")
|
||
async def eco_fish(self, ctx):
|
||
"""Go fishing!"""
|
||
await self._fish(ctx)
|
||
|
||
@eco.command(name="mine")
|
||
async def eco_mine(self, ctx):
|
||
"""Go mining!"""
|
||
await self._mine(ctx)
|
||
|
||
@eco.command(name="craft")
|
||
async def eco_craft(self, ctx, *, recipe: str):
|
||
"""Craft an item from materials."""
|
||
await self._craft(ctx, recipe.lower().replace(" ", "_"))
|
||
|
||
@eco.command(name="materials", aliases=["mats"])
|
||
async def eco_materials(self, ctx, user: discord.Member = None):
|
||
"""View your gathered materials."""
|
||
user = user or ctx.author
|
||
mats = await self.eco_config.member(user).materials()
|
||
if not mats:
|
||
return await ctx.send(embed=info_embed(f"{user.display_name} has no materials."))
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
mining_types = data.get("mining", {}).get("ore_types", {})
|
||
embed = discord.Embed(title=f"⛏️ {user.display_name}'s Materials", colour=Clr.ECO)
|
||
for mat, count in mats.items():
|
||
mt = mining_types.get(mat, {})
|
||
emoji = mt.get("emoji", "📦")
|
||
embed.add_field(name=f"{emoji} {mat.replace('_', ' ').title()}", value=f"x{count}", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="shop")
|
||
async def eco_shop(self, ctx):
|
||
"""Browse the server shop."""
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
items = data.get("shop_items", {})
|
||
if not items:
|
||
return await ctx.send(embed=info_embed("Shop is empty!"))
|
||
emoji = data["currency_emoji"]
|
||
embed = discord.Embed(title="🛒 Shop", colour=Clr.ECO)
|
||
for iid, item in items.items():
|
||
stock_text = f"Stock: {item['stock']}" if item.get("stock", -1) >= 0 else "∞"
|
||
embed.add_field(
|
||
name=f"{item.get('emoji', '📦')} {item['name']} — {emoji} {item['price']:,}",
|
||
value=f"{item.get('description', 'No description')}\n{stock_text}",
|
||
inline=False,
|
||
)
|
||
view = ShopView(self, ctx.guild, items)
|
||
await ctx.send(embed=embed, view=view)
|
||
|
||
@eco.command(name="additem")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_additem(self, ctx, name: str, price: int, *, description: str = ""):
|
||
"""Add a shop item."""
|
||
item_id = short_id(8)
|
||
async with self.eco_config.guild(ctx.guild).shop_items() as items:
|
||
items[item_id] = {
|
||
"name": name, "description": description, "price": price,
|
||
"emoji": "📦", "role_id": None, "stock": -1,
|
||
"max_per_user": 0, "usable": False, "type": "item",
|
||
}
|
||
await ctx.send(embed=ok_embed(f"Item **{name}** added (ID: `{item_id}`, Price: {price:,})"))
|
||
|
||
@eco.command(name="addroleitem")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_addroleitem(self, ctx, name: str, price: int, role: discord.Role, *, description: str = ""):
|
||
"""Add a role shop item."""
|
||
item_id = short_id(8)
|
||
async with self.eco_config.guild(ctx.guild).shop_items() as items:
|
||
items[item_id] = {
|
||
"name": name, "description": description, "price": price,
|
||
"emoji": "🏷️", "role_id": role.id, "stock": -1,
|
||
"max_per_user": 1, "usable": False, "type": "role",
|
||
}
|
||
await ctx.send(embed=ok_embed(f"Role item **{name}** added ({role.mention}, {price:,})"))
|
||
|
||
@eco.command(name="removeitem")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_removeitem(self, ctx, item_id: str):
|
||
"""Remove a shop item."""
|
||
async with self.eco_config.guild(ctx.guild).shop_items() as items:
|
||
if item_id not in items:
|
||
return await ctx.send(embed=err_embed("Item not found."))
|
||
removed = items.pop(item_id)
|
||
await ctx.send(embed=ok_embed(f"Removed **{removed['name']}**"))
|
||
|
||
@eco.command(name="inventory", aliases=["inv"])
|
||
async def eco_inventory(self, ctx, user: discord.Member = None):
|
||
"""View inventory."""
|
||
user = user or ctx.author
|
||
inv = await self.eco_config.member(user).inventory()
|
||
if not inv:
|
||
return await ctx.send(embed=info_embed(f"{user.display_name} has no items."))
|
||
items_data = await self.eco_config.guild(ctx.guild).shop_items()
|
||
embed = discord.Embed(title=f"🎒 {user.display_name}'s Inventory", colour=Clr.ECO)
|
||
for iid, count in inv.items():
|
||
item = items_data.get(iid, {})
|
||
name = item.get("name", iid)
|
||
emoji = item.get("emoji", "📦")
|
||
embed.add_field(name=f"{emoji} {name}", value=f"x{count}", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="heist")
|
||
async def eco_heist(self, ctx, bet: int):
|
||
"""Start a heist! Others can join."""
|
||
await self._start_heist(ctx, bet)
|
||
|
||
@eco.command(name="pet")
|
||
async def eco_pet(self, ctx):
|
||
"""View your pets."""
|
||
pets = await self.eco_config.member(ctx.author).pets()
|
||
if not pets:
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
types = data.get("pets", {}).get("types", {})
|
||
available = "\n".join(f"{v['emoji']} **{k}** — {v['base_price']:,} coins" for k, v in types.items())
|
||
return await ctx.send(embed=info_embed(f"You have no pets.\n\nAvailable:\n{available}\n\nBuy: `[p]eco buypet <type> <name>`"))
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
types = data.get("pets", {}).get("types", {})
|
||
embed = discord.Embed(title=f"🐾 {ctx.author.display_name}'s Pets", colour=Clr.ECO)
|
||
for name, pet in pets.items():
|
||
pt = types.get(pet["type"], {})
|
||
emoji = pt.get("emoji", "🐾")
|
||
embed.add_field(
|
||
name=f"{emoji} {name}",
|
||
value=f"Type: {pet['type']} · Lv.{pet['level']} · XP: {pet['xp']} · ❤️ {pet['happiness']}%",
|
||
inline=False,
|
||
)
|
||
embed.set_footer(text="Feed: [p]eco feed <name> · Collect: [p]eco collect")
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="buypet")
|
||
async def eco_buypet(self, ctx, pet_type: str, *, name: str):
|
||
"""Buy a pet."""
|
||
await self._buy_pet(ctx, pet_type.lower(), name)
|
||
|
||
@eco.command(name="feed")
|
||
async def eco_feed(self, ctx, *, name: str):
|
||
"""Feed a pet."""
|
||
await self._feed_pet(ctx, name)
|
||
|
||
@eco.command(name="collect")
|
||
async def eco_collect(self, ctx):
|
||
"""Collect earnings from pets."""
|
||
await self._pet_collect(ctx)
|
||
|
||
@eco.command(name="gamblestats")
|
||
async def eco_gamblestats(self, ctx, user: discord.Member = None):
|
||
"""View gambling statistics."""
|
||
user = user or ctx.author
|
||
stats = await self.eco_config.member(user).gambling_stats()
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
emoji = data["currency_emoji"]
|
||
embed = discord.Embed(title=f"🎰 {user.display_name}'s Gambling Stats", colour=Clr.ECO)
|
||
embed.add_field(name="Won", value=f"{emoji} {stats.get('won', 0):,}", inline=True)
|
||
embed.add_field(name="Lost", value=f"{emoji} {stats.get('lost', 0):,}", inline=True)
|
||
embed.add_field(name="Total Wagered", value=f"{emoji} {stats.get('total_wagered', 0):,}", inline=True)
|
||
embed.add_field(name="Biggest Win", value=f"{emoji} {stats.get('biggest_win', 0):,}", inline=True)
|
||
net = stats.get("won", 0) - stats.get("lost", 0)
|
||
embed.add_field(name="Net", value=f"{emoji} {net:,}", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="achievements", aliases=["ach"])
|
||
async def eco_achievements(self, ctx, user: discord.Member = None):
|
||
"""View achievements."""
|
||
user = user or ctx.author
|
||
earned = await self.eco_config.member(user).achievements()
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
all_achs = data.get("achievements", {})
|
||
embed = discord.Embed(title=f"🏆 {user.display_name}'s Achievements", colour=Clr.ECO)
|
||
for aid, ach in all_achs.items():
|
||
status = "✅" if aid in earned else "🔒"
|
||
embed.add_field(
|
||
name=f"{status} {ach.get('emoji', '🏆')} {ach['name']}",
|
||
value=f"{ach.get('description', '')}" + (f"\nReward: {ach.get('reward', 0):,}" if ach.get("reward") else ""),
|
||
inline=True,
|
||
)
|
||
embed.set_footer(text=f"{len(earned)}/{len(all_achs)} unlocked")
|
||
await ctx.send(embed=embed)
|
||
|
||
@eco.command(name="auction")
|
||
async def eco_auction(self, ctx):
|
||
"""View the auction house."""
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
auction_data = data.get("auction", {})
|
||
if not auction_data.get("enabled"):
|
||
return await ctx.send(embed=err_embed("Auction house is disabled."))
|
||
listings = auction_data.get("listings", {})
|
||
active = {k: v for k, v in listings.items() if not v.get("ended")}
|
||
if not active:
|
||
return await ctx.send(embed=info_embed("No active auctions."))
|
||
embed = discord.Embed(title="🏛️ Auction House", colour=Clr.ECO)
|
||
for lid, listing in list(active.items())[:10]:
|
||
embed.add_field(
|
||
name=f"`{lid}` — {listing['item_name']}",
|
||
value=f"Current bid: {listing.get('current_bid', listing.get('starting_price', 0)):,}\nSeller: <@{listing['seller_id']}>",
|
||
inline=True,
|
||
)
|
||
from .economy import AuctionListView
|
||
view = AuctionListView(self, ctx.guild, active)
|
||
await ctx.send(embed=embed, view=view)
|
||
|
||
@eco.command(name="setcurrency")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_setcurrency(self, ctx, name: str, emoji: str, symbol: str = "$"):
|
||
"""Set currency name and emoji."""
|
||
await self.eco_config.guild(ctx.guild).currency_name.set(name)
|
||
await self.eco_config.guild(ctx.guild).currency_emoji.set(emoji)
|
||
await self.eco_config.guild(ctx.guild).currency_symbol.set(symbol)
|
||
await ctx.send(embed=ok_embed(f"Currency: {emoji} {name} ({symbol})"))
|
||
|
||
@eco.command(name="setbalance")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def eco_setbalance(self, ctx, user: discord.Member, wallet: int, bank: int = 0):
|
||
"""Set a user's balance (admin)."""
|
||
await self.eco_config.member(user).wallet.set(wallet)
|
||
await self.eco_config.member(user).bank.set(bank)
|
||
await ctx.send(embed=ok_embed(f"Balance set — {user.mention}: Wallet {wallet:,}, Bank {bank:,}"))
|
||
|
||
@eco.command(name="addmoney")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def eco_addmoney(self, ctx, user: discord.Member, amount: int):
|
||
"""Add coins to a user's wallet (admin)."""
|
||
await self._add_balance(user, amount)
|
||
await ctx.send(embed=ok_embed(f"Added {amount:,} to {user.mention}'s wallet."))
|
||
|
||
@eco.command(name="removemoney")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def eco_removemoney(self, ctx, user: discord.Member, amount: int):
|
||
"""Remove coins from a user (admin)."""
|
||
await self._remove_balance(user, amount)
|
||
await ctx.send(embed=ok_embed(f"Removed {amount:,} from {user.mention}."))
|
||
|
||
@eco.command(name="reset")
|
||
@checks.admin_or_permissions(administrator=True)
|
||
async def eco_reset(self, ctx, user: discord.Member):
|
||
"""Reset a user's economy data (admin)."""
|
||
await self.eco_config.member(user).clear()
|
||
await ctx.send(embed=ok_embed(f"{user.mention}'s economy data reset."))
|
||
|
||
@eco.command(name="incomerole")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_incomerole(self, ctx, role: discord.Role, amount: int, hours: int = 1):
|
||
"""Set passive income for a role."""
|
||
async with self.eco_config.guild(ctx.guild).income_roles() as ir:
|
||
ir[str(role.id)] = {"amount": amount, "interval_hours": hours}
|
||
await ctx.send(embed=ok_embed(f"{role.mention} earns {amount:,} every {hours}h."))
|
||
|
||
@eco.command(name="settings")
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def eco_settings(self, ctx):
|
||
"""View economy settings."""
|
||
data = await self.eco_config.guild(ctx.guild).all()
|
||
embed = discord.Embed(title="🪙 Economy Settings", colour=Clr.ECO)
|
||
embed.add_field(name="Currency", value=f"{data['currency_emoji']} {data['currency_name']}", inline=True)
|
||
embed.add_field(name="Daily", value=f"{data['daily_amount']:,}", inline=True)
|
||
embed.add_field(name="Weekly", value=f"{data['weekly_amount']:,}", inline=True)
|
||
embed.add_field(name="Work", value=f"{data['work_min']}-{data['work_max']}", inline=True)
|
||
embed.add_field(name="Crime", value=f"{data['crime_min']}-{data['crime_max']} ({data['crime_fail_chance']}% fail)", inline=True)
|
||
embed.add_field(name="Rob", value="✅" if data['rob_enabled'] else "❌", inline=True)
|
||
embed.add_field(name="Tax", value=f"{data['tax_rate']}%", inline=True)
|
||
embed.add_field(name="Interest", value=f"{data['interest_rate']}%", inline=True)
|
||
embed.add_field(name="Shop Items", value=str(len(data.get('shop_items', {}))), inline=True)
|
||
embed.add_field(name="Fishing", value="✅" if data.get("fishing", {}).get("enabled") else "❌", inline=True)
|
||
embed.add_field(name="Mining", value="✅" if data.get("mining", {}).get("enabled") else "❌", inline=True)
|
||
embed.add_field(name="Auction", value="✅" if data.get("auction", {}).get("enabled") else "❌", inline=True)
|
||
await ctx.send(embed=embed)
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
# EMBED BUILDER
|
||
# ══════════════════════════════════════════════════════════════════════════
|
||
|
||
@commands.group(name="embedbuilder", aliases=["eb"])
|
||
@commands.guild_only()
|
||
@checks.admin_or_permissions(manage_guild=True)
|
||
async def embedbuilder(self, ctx: commands.Context):
|
||
"""📨 Embed Builder — Create custom embeds and send via webhooks."""
|
||
if ctx.invoked_subcommand is None:
|
||
await ctx.send_help(ctx.command)
|
||
|
||
@embedbuilder.command(name="create")
|
||
async def eb_create(self, ctx):
|
||
"""Open the interactive embed builder."""
|
||
await self._open_embed_builder(ctx)
|
||
|
||
@embedbuilder.command(name="send")
|
||
async def eb_send(self, ctx, channel: discord.TextChannel, *, json_data: str = None):
|
||
"""Send an embed via JSON. Supports Discohook JSON format."""
|
||
if not json_data:
|
||
return await ctx.send(embed=info_embed("Paste Discohook JSON: `[p]eb send #channel {\"embeds\": [...]}`"))
|
||
await self._send_json_embed(ctx, channel, json_data)
|
||
|
||
@embedbuilder.command(name="webhook")
|
||
async def eb_webhook(self, ctx, channel: discord.TextChannel, *, name: str = "NexusCore"):
|
||
"""Create/set a webhook for a channel."""
|
||
await self._setup_webhook(ctx, channel, name)
|
||
|
||
@embedbuilder.command(name="templates")
|
||
async def eb_templates(self, ctx):
|
||
"""List saved embed templates."""
|
||
await self._list_templates(ctx)
|
||
|
||
@embedbuilder.command(name="save")
|
||
async def eb_save(self, ctx, name: str, *, json_data: str):
|
||
"""Save an embed template."""
|
||
await self._save_template(ctx, name, json_data)
|
||
|
||
@embedbuilder.command(name="load")
|
||
async def eb_load(self, ctx, name: str, channel: discord.TextChannel):
|
||
"""Load and send a saved template."""
|
||
await self._load_and_send_template(ctx, name, channel)
|
||
|
||
@embedbuilder.command(name="schedule")
|
||
async def eb_schedule(self, ctx, channel: discord.TextChannel, interval: str, *, json_data: str):
|
||
"""Schedule a recurring embed. Interval: 1h, 6h, 1d, etc."""
|
||
dur = parse_duration(interval)
|
||
if not dur:
|
||
return await ctx.send(embed=err_embed("Invalid interval."))
|
||
await self._schedule_embed(ctx, channel, dur, json_data)
|
||
await ctx.send(embed=ok_embed(f"Embed scheduled every {duration_str(dur)} in {channel.mention}"))
|