Добавить WL tickets bot/bot_tickets.py
This commit is contained in:
parent
127a280bfb
commit
6f613beb3d
441
WL tickets bot/bot_tickets.py
Normal file
441
WL tickets bot/bot_tickets.py
Normal file
@ -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}")
|
||||
Loading…
Reference in New Issue
Block a user