diff --git a/WL tickets bot/bot_tickets.py b/WL tickets bot/bot_tickets.py new file mode 100644 index 0000000..e92a14c --- /dev/null +++ b/WL tickets bot/bot_tickets.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +TOKEN = "Ваш токен" + +import discord +from discord.ext import commands +from discord import app_commands +import json +import os +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ===================================================== +# 📁 НАСТРОЙКИ (JSON) +# ===================================================== + +SETTINGS_FILE = "settings.json" + +def load_settings(): + if not os.path.exists(SETTINGS_FILE): + return {} + with open(SETTINGS_FILE, 'r', encoding='utf-8') as f: + try: + return json.load(f) + except: + return {} + +def save_settings(data): + with open(SETTINGS_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=4, ensure_ascii=False) + +def get_setting(key): + return load_settings().get(key) + +def set_setting(key, value): + data = load_settings() + data[key] = value + save_settings(data) + +# ===================================================== +# 🤖 БОТ +# ===================================================== + +intents = discord.Intents.default() +intents.guilds = True +intents.message_content = True +intents.members = True + +class TicketsBot(commands.Bot): + def __init__(self): + super().__init__(command_prefix='!', intents=intents) + self.tickets = {} # channel_id -> owner_id + self.reason_cache = {} # user_id -> reason_name (для панели) + + async def setup_hook(self): + await self.tree.sync() + logger.info("✅ Слэш-команды синхронизированы!") + +bot = TicketsBot() + +@bot.event +async def on_ready(): + logger.info(f'✅ Бот {bot.user} запущен!') + await bot.change_presence(activity=discord.Activity( + type=discord.ActivityType.watching, + name="🎫 /ticket-panel" + )) + +# ===================================================== +# 🔧 КОМАНДЫ НАСТРОЙКИ (основные) +# ===================================================== + +@bot.tree.command(name="ticket-setup", description="Настройка системы тикетов (категория, роль модеров, лог)") +@app_commands.default_permissions(administrator=True) +@app_commands.describe( + category="Категория для тикетов (по умолчанию)", + mod_role="Роль модераторов", + log_channel="Канал для логов" +) +async def ticket_setup(interaction: discord.Interaction, + category: discord.CategoryChannel, + mod_role: discord.Role, + log_channel: discord.TextChannel): + set_setting('tickets_category', str(category.id)) + set_setting('tickets_mod_role', str(mod_role.id)) + set_setting('tickets_log_channel', str(log_channel.id)) + await interaction.response.send_message( + f'✅ Категория по умолчанию: {category.name}, роль: {mod_role.mention}, лог: {log_channel.mention}', + ephemeral=True + ) + logger.info(f"Ticket setup: category={category.id}, mod_role={mod_role.id}, log={log_channel.id}") + +# ===================================================== +# 📋 УПРАВЛЕНИЕ ПРИЧИНАМИ (админ) +# ===================================================== + +@bot.tree.command(name="ticket-reason-add", description="Добавить причину для тикета с привязкой к категории") +@app_commands.default_permissions(administrator=True) +@app_commands.describe( + name="Название причины (например, 'Техпомощь')", + category="Категория, куда будут создаваться тикеты с этой причиной", + emoji="Эмодзи для отображения (опционально)" +) +async def ticket_reason_add(interaction: discord.Interaction, + name: str, + category: discord.CategoryChannel, + emoji: str = "📌"): + reasons = load_settings().get('ticket_reasons', {}) + if name in reasons: + await interaction.response.send_message(f'❌ Причина **{name}** уже существует!', ephemeral=True) + return + reasons[name] = { + 'category_id': str(category.id), + 'emoji': emoji + } + set_setting('ticket_reasons', reasons) + await interaction.response.send_message( + f'✅ Причина **{name}** добавлена (категория: {category.name}, эмодзи: {emoji})', + ephemeral=True + ) + logger.info(f"Added reason: {name} -> category {category.id}") + +@bot.tree.command(name="ticket-reason-remove", description="Удалить причину") +@app_commands.default_permissions(administrator=True) +@app_commands.describe(name="Название причины") +async def ticket_reason_remove(interaction: discord.Interaction, name: str): + reasons = load_settings().get('ticket_reasons', {}) + if name not in reasons: + await interaction.response.send_message(f'❌ Причина **{name}** не найдена!', ephemeral=True) + return + del reasons[name] + set_setting('ticket_reasons', reasons) + await interaction.response.send_message(f'✅ Причина **{name}** удалена.', ephemeral=True) + logger.info(f"Removed reason: {name}") + +@bot.tree.command(name="ticket-reason-list", description="Показать список всех причин") +async def ticket_reason_list(interaction: discord.Interaction): + reasons = load_settings().get('ticket_reasons', {}) + if not reasons: + await interaction.response.send_message('📭 Причины не добавлены.', ephemeral=True) + return + embed = discord.Embed( + title="📋 Список причин тикетов", + color=discord.Color.blue() + ) + for name, data in reasons.items(): + category = interaction.guild.get_channel(int(data['category_id'])) + cat_name = category.name if category else "❌ Удалена" + embed.add_field( + name=f"{data.get('emoji', '📌')} {name}", + value=f"Категория: {cat_name}", + inline=False + ) + await interaction.response.send_message(embed=embed, ephemeral=True) + +# ===================================================== +# 🎫 СОЗДАНИЕ ПАНЕЛИ ТИКЕТОВ +# ===================================================== + +@bot.tree.command(name="ticket-panel", description="Создать панель тикетов в указанном канале") +@app_commands.default_permissions(administrator=True) +@app_commands.describe(channel="Канал, куда отправить панель") +async def ticket_panel(interaction: discord.Interaction, channel: discord.TextChannel): + reasons = load_settings().get('ticket_reasons', {}) + if not reasons: + await interaction.response.send_message( + '❌ Нет ни одной причины. Добавьте их через `/ticket-reason-add`.', + ephemeral=True + ) + return + + # Создаём Select menu с причинами + options = [] + for name, data in reasons.items(): + emoji = data.get('emoji', '📌') + options.append( + discord.SelectOption( + label=name, + description=f"Тикет по причине '{name}'", + emoji=emoji, + value=name + ) + ) + select = discord.ui.Select( + placeholder="Выберите причину обращения", + options=options, + custom_id="ticket_reason_select" + ) + button = discord.ui.Button( + label="📩 Создать тикет", + style=discord.ButtonStyle.primary, + custom_id="ticket_create_button" + ) + view = discord.ui.View() + view.add_item(select) + view.add_item(button) + + embed = discord.Embed( + title="🎫 Система тикетов", + description="Выберите причину обращения и нажмите кнопку, чтобы создать тикет.\nМодераторы свяжутся с вами в ближайшее время.", + color=discord.Color.blue() + ) + embed.set_footer(text="Нажимайте только при необходимости") + + await channel.send(embed=embed, view=view) + await interaction.response.send_message(f"✅ Панель отправлена в {channel.mention}", ephemeral=True) + +# ===================================================== +# 📥 ОБРАБОТКА ВЗАИМОДЕЙСТВИЙ (селект и кнопка) +# ===================================================== + +@bot.event +async def on_interaction(interaction: discord.Interaction): + # Селект причины + if interaction.type == discord.InteractionType.component: + custom_id = interaction.data.get('custom_id') + if custom_id == "ticket_reason_select": + reason_name = interaction.data["values"][0] + bot.reason_cache[interaction.user.id] = reason_name + await interaction.response.send_message( + f"✅ Выбрана причина: **{reason_name}**", + ephemeral=True + ) + return + + # Кнопка "Создать тикет" + if custom_id == "ticket_create_button": + reason_name = bot.reason_cache.get(interaction.user.id) + if not reason_name: + await interaction.response.send_message( + "❌ Сначала выберите причину в выпадающем меню!", + ephemeral=True + ) + return + + # Проверяем, существует ли ещё причина + reasons = load_settings().get('ticket_reasons', {}) + if reason_name not in reasons: + await interaction.response.send_message( + f"❌ Причина **{reason_name}** была удалена. Выберите другую.", + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + + await create_ticket(interaction, reason_name) + return + + # Обработка кнопок закрытия и добавления пользователя + if custom_id == "close_ticket": + await close_ticket(interaction) + return + if custom_id == "add_user_ticket": + await interaction.response.send_message( + "Используйте `/ticket-add @пользователь` в канале тикета", + ephemeral=True + ) + return + + # Модальные окна (если добавим позже) — пока нет + +# ===================================================== +# 🎫 ФУНКЦИЯ СОЗДАНИЯ ТИКЕТА +# ===================================================== + +async def create_ticket(interaction: discord.Interaction, reason_name: str): + guild = interaction.guild + member = interaction.user + reasons = load_settings().get('ticket_reasons', {}) + reason_data = reasons.get(reason_name) + if not reason_data: + await interaction.response.send_message( + f'❌ Причина **{reason_name}** не найдена.', + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + + # Определяем категорию для тикета + category_id = reason_data.get('category_id') + if not category_id: + # Если не указана — используем дефолтную + category_id = get_setting('tickets_category') + if not category_id: + await interaction.response.send_message( + '❌ Категория не настроена. Используйте `/ticket-setup` или укажите категорию в причине.', + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + + category = guild.get_channel(int(category_id)) + if not category: + await interaction.response.send_message( + f'❌ Категория не найдена (ID: {category_id})', + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + + # Проверяем, нет ли уже открытого тикета + for ch_id, uid in bot.tickets.items(): + if uid == member.id: + ch = guild.get_channel(ch_id) + if ch: + await interaction.response.send_message( + f'❌ У вас уже есть открытый тикет: {ch.mention}', + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + else: + del bot.tickets[ch_id] + + # Роль модераторов + mod_role_id = get_setting('tickets_mod_role') + if not mod_role_id: + await interaction.response.send_message( + '❌ Роль модераторов не настроена. Используйте `/ticket-setup`.', + ephemeral=True + ) + del bot.reason_cache[interaction.user.id] + return + mod_role = guild.get_role(int(mod_role_id)) + + # Счётчик + counter = int(get_setting('tickets_counter') or 0) + 1 + set_setting('tickets_counter', str(counter)) + + # Название канала: причина-номер + safe_name = reason_name.lower().replace(' ', '-') + ticket_name = f"{safe_name}-{counter:03d}" + + # Права доступа + overwrites = { + guild.default_role: discord.PermissionOverwrite(view_channel=False), + member: discord.PermissionOverwrite(view_channel=True, send_messages=True, attach_files=True), + mod_role: discord.PermissionOverwrite(view_channel=True, send_messages=True), + } + for role in guild.roles: + if role.permissions.administrator: + overwrites[role] = discord.PermissionOverwrite(view_channel=True, send_messages=True) + + channel = await guild.create_text_channel(ticket_name, category=category, overwrites=overwrites) + bot.tickets[channel.id] = member.id + + # Приветственное сообщение с причиной + embed = discord.Embed( + title=f'🎫 Тикет #{counter}', + description=f"**Причина:** {reason_name}\n" + f"Привет, {member.mention}!\n" + f"Опишите вашу проблему. Модераторы скоро подключатся.", + color=discord.Color.green() + ) + view = discord.ui.View() + view.add_item(discord.ui.Button(label='🔒 Закрыть тикет', style=discord.ButtonStyle.danger, custom_id='close_ticket')) + view.add_item(discord.ui.Button(label='➕ Добавить пользователя', style=discord.ButtonStyle.primary, custom_id='add_user_ticket')) + + await channel.send(embed=embed, view=view) + + # Лог + log_channel_id = get_setting('tickets_log_channel') + if log_channel_id: + log_channel = guild.get_channel(int(log_channel_id)) + if log_channel: + await log_channel.send(f'📝 Тикет {ticket_name} (причина: {reason_name}) открыт {member.mention}') + + # Удаляем кэш причины + if interaction.user.id in bot.reason_cache: + del bot.reason_cache[interaction.user.id] + + await interaction.response.send_message(f'✅ Тикет создан: {channel.mention}', ephemeral=True) + +# ===================================================== +# 🔒 ЗАКРЫТИЕ ТИКЕТА +# ===================================================== + +async def close_ticket(interaction): + channel = interaction.channel + if channel.id not in bot.tickets: + await interaction.response.send_message('❌ Это не тикет-канал.', ephemeral=True) + return + + owner_id = bot.tickets[channel.id] + member = interaction.user + mod_role_id = get_setting('tickets_mod_role') + is_mod = mod_role_id and member.get_role(int(mod_role_id)) is not None + is_owner = member.id == owner_id + is_admin = member.guild_permissions.manage_channels + + if not (is_owner or is_mod or is_admin): + await interaction.response.send_message('❌ У вас нет прав закрыть этот тикет.', ephemeral=True) + return + + log_channel_id = get_setting('tickets_log_channel') + if log_channel_id: + log_channel = interaction.guild.get_channel(int(log_channel_id)) + if log_channel: + await log_channel.send(f'🔒 Тикет {channel.name} закрыт {member.mention}') + + await interaction.response.send_message('⏳ Тикет закрывается...') + del bot.tickets[channel.id] + await channel.delete() + +# ===================================================== +# ➕ ДОБАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯ +# ===================================================== + +@bot.tree.command(name="ticket-add", description="Добавить пользователя в тикет") +@app_commands.describe(user="Пользователь для добавления") +async def ticket_add(interaction: discord.Interaction, user: discord.Member): + if interaction.channel.id not in bot.tickets: + await interaction.response.send_message('❌ Это не тикет-канал.', ephemeral=True) + return + owner_id = bot.tickets[interaction.channel.id] + member = interaction.user + mod_role_id = get_setting('tickets_mod_role') + is_mod = mod_role_id and member.get_role(int(mod_role_id)) is not None + is_owner = member.id == owner_id + is_admin = member.guild_permissions.manage_channels + if not (is_owner or is_mod or is_admin): + await interaction.response.send_message('❌ Недостаточно прав.', ephemeral=True) + return + + await interaction.channel.set_permissions(user, view_channel=True, send_messages=True, attach_files=True) + await interaction.response.send_message(f'✅ {user.mention} добавлен в тикет.', ephemeral=True) + +# ===================================================== +# 🚀 ЗАПУСК +# ===================================================== + +if __name__ == "__main__": + try: + bot.run(TOKEN) + except discord.LoginFailure: + logger.error("❌ Неверный токен!") + except Exception as e: + logger.error(f"❌ Ошибка: {e}") \ No newline at end of file