diff --git a/WL voice bots/bot_voice.py b/WL voice bots/bot_voice.py new file mode 100644 index 0000000..ed76f46 --- /dev/null +++ b/WL voice bots/bot_voice.py @@ -0,0 +1,394 @@ +#!/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.voice_states = True +intents.guilds = True +intents.message_content = True +intents.members = True + +class VoiceBot(commands.Bot): + def __init__(self): + super().__init__(command_prefix='!', intents=intents) + self.voice_data = {\} # owner_id -> {voice_id, panel_channel_id, panel_msg_id\} + self.modal_owner_cache = {\} # user_id -> owner_id + + async def setup_hook(self): + await self.tree.sync() + logger.info("✅ Слэш-команды синхронизированы!") + +bot = VoiceBot() + +@bot.event +async def on_ready(): + logger.info(f'✅ Бот {bot.user\} запущен!') + await bot.change_presence(activity=discord.Activity( + type=discord.ActivityType.listening, + name="🎵 /voice-setup" + )) + await cleanup_old_channels() + +# ===================================================== +# 🧹 ОЧИСТКА ПРИ ЗАПУСКЕ (удаляем каналы без людей) +# ===================================================== + +def has_humans(channel): + """Проверяет, есть ли в голосовом канале хотя бы один человек (не бот)""" + return any(not member.bot for member in channel.members) + +async def cleanup_old_channels(): + """Удаляет голосовые каналы, в которых нет людей, и все текстовые каналы, созданные ботом""" + for guild in bot.guilds: + category_id = get_setting('voice_category') + if not category_id: + continue + category = guild.get_channel(int(category_id)) + if not category: + continue + + deleted_count = 0 + for channel in category.channels: + if channel.name.startswith(('🎧 Комната', '💬-чат-')): + if isinstance(channel, discord.VoiceChannel): + # Удаляем только если нет людей + if not has_humans(channel): + try: + await channel.delete() + deleted_count += 1 + logger.info(f"Удалён голосовой канал без людей: {channel.name\}") + except Exception as e: + logger.error(f"Не удалось удалить {channel.name\}: {e\}") + elif isinstance(channel, discord.TextChannel): + # Все текстовые каналы удаляем смело (они не содержат пользователей) + try: + await channel.delete() + deleted_count += 1 + logger.info(f"Удалён текстовый канал: {channel.name\}") + except Exception as e: + logger.error(f"Не удалось удалить {channel.name\}: {e\}") + + if deleted_count > 0: + logger.info(f"Очищено {deleted_count\} каналов в категории {category.name\}") + + bot.voice_data.clear() + +# ===================================================== +# 🔧 КОМАНДЫ НАСТРОЙКИ +# ===================================================== + +@bot.tree.command(name="voice-setup", description="Настройка модуля временных каналов") +@app_commands.default_permissions(administrator=True) +@app_commands.describe( + trigger="Голосовой канал-триггер", + category="Категория для временных каналов" +) +async def voice_setup(interaction: discord.Interaction, + trigger: discord.VoiceChannel, + category: discord.CategoryChannel): + set_setting('voice_trigger', str(trigger.id)) + set_setting('voice_category', str(category.id)) + await interaction.response.send_message( + f'✅ Триггер: {trigger.mention\}, категория: {category.name\}', + ephemeral=True + ) + logger.info(f"Voice setup: trigger={trigger.id\}, category={category.id\}") + +@bot.tree.command(name="voice-panel-channel", description="Установить канал для всех панелей (общий)") +@app_commands.default_permissions(administrator=True) +@app_commands.describe(channel="Текстовый канал для панелей (None = отдельные каналы)") +async def voice_panel_channel(interaction: discord.Interaction, channel: discord.TextChannel = None): + if channel: + set_setting('voice_panel_channel', str(channel.id)) + await interaction.response.send_message( + f'✅ Панели будут отправляться в {channel.mention\}', + ephemeral=True + ) + else: + set_setting('voice_panel_channel', None) + await interaction.response.send_message( + '✅ Панели будут создаваться в отдельных каналах', + ephemeral=True + ) + logger.info(f"Panel channel set to {channel.id if channel else 'None'\}") + +# ===================================================== +# 🎤 СОЗДАНИЕ ВРЕМЕННОГО КАНАЛА + ПАНЕЛИ +# ===================================================== + +async def get_panel_channel(guild): + """Возвращает канал для панели (общий или None для отдельного)""" + panel_channel_id = get_setting('voice_panel_channel') + if panel_channel_id: + channel = guild.get_channel(int(panel_channel_id)) + if channel and isinstance(channel, discord.TextChannel): + return channel + return None + +@bot.event +async def on_voice_state_update(member, before, after): + trigger_id = get_setting('voice_trigger') + if not trigger_id: + return + trigger_id = int(trigger_id) + + # Вход в триггерный канал + if after.channel and after.channel.id == trigger_id and before.channel != after.channel: + guild = member.guild + category_id = get_setting('voice_category') + if category_id: + category = guild.get_channel(int(category_id)) + else: + category = await guild.create_category('🎙️ Временные каналы') + set_setting('voice_category', str(category.id)) + + # Проверяем существование каналов для этого пользователя + if member.id in bot.voice_data: + data = bot.voice_data[member.id] + vc = guild.get_channel(data['voice_id']) + if vc: + await member.move_to(vc) + return + else: + # Если голосовой удалён, удаляем панель и данные + panel_channel = guild.get_channel(data['panel_channel_id']) + if panel_channel: + try: + msg = await panel_channel.fetch_message(data['panel_msg_id']) + await msg.delete() + except: + pass + del bot.voice_data[member.id] + + # Создаём голосовой канал + voice = await guild.create_voice_channel( + f'🎧 Комната {member.display_name\}', + category=category, + user_limit=5, + overwrites={ + guild.default_role: discord.PermissionOverwrite(view_channel=True, connect=True), + member: discord.PermissionOverwrite(manage_channels=True, mute_members=True, deafen_members=True) + \} + ) + + # Определяем канал для панели + panel_channel = await get_panel_channel(guild) + if panel_channel is None: + # Создаём отдельный текстовый канал + panel_channel = await guild.create_text_channel( + f'💬-чат-{member.display_name\}', + category=category, + overwrites={ + guild.default_role: discord.PermissionOverwrite(view_channel=False), + member: discord.PermissionOverwrite(view_channel=True, send_messages=True) + \} + ) + + # Формируем embed с названием канала сверху + embed = discord.Embed( + title=f'🎛️ Управление: {voice.name\}', + description=f'Ваш голосовой канал: {voice.mention\}\\nИспользуйте кнопки ниже.', + color=discord.Color.green() + ) + embed.set_footer(text=f'Владелец: {member.display_name\}') + + view = discord.ui.View() + view.add_item(discord.ui.Button(label='Изменить название', style=discord.ButtonStyle.primary, custom_id='voice_name')) + view.add_item(discord.ui.Button(label='Лимит участников', style=discord.ButtonStyle.secondary, custom_id='voice_limit')) + view.add_item(discord.ui.Button(label='Приватный/Публичный', style=discord.ButtonStyle.success, custom_id='voice_private')) + view.add_item(discord.ui.Button(label='Забрать владельца', style=discord.ButtonStyle.danger, custom_id='voice_claim')) + + msg = await panel_channel.send(embed=embed, view=view) + + # Сохраняем данные + bot.voice_data[member.id] = { + 'voice_id': voice.id, + 'panel_channel_id': panel_channel.id, + 'panel_msg_id': msg.id + \} + + await member.move_to(voice) + logger.info(f"Создан канал {voice.name\} для {member\} (панель в {panel_channel.name\})") + + # ====== ОЧИСТКА ПУСТЫХ КАНАЛОВ (ТОЛЬКО ЕСЛИ НЕТ ЛЮДЕЙ) ====== + guild = member.guild + to_delete = [] + for owner_id, data in bot.voice_data.items(): + vc = guild.get_channel(data['voice_id']) + # Удаляем, если канал пуст или в нём только боты + if vc and not has_humans(vc): + to_delete.append(owner_id) + + for owner_id in to_delete: + data = bot.voice_data[owner_id] + vc = guild.get_channel(data['voice_id']) + panel_channel = guild.get_channel(data['panel_channel_id']) + if vc: + await vc.delete() + if panel_channel: + try: + msg = await panel_channel.fetch_message(data['panel_msg_id']) + await msg.delete() + except: + pass + del bot.voice_data[owner_id] + logger.info(f"Удалены каналы для {owner_id\} (в канале не было людей)") + +# ===================================================== +# 🎛️ ОБРАБОТКА КНОПОК И МОДАЛЬНЫХ ОКОН +# ===================================================== + +@bot.event +async def on_interaction(interaction: discord.Interaction): + if interaction.type == discord.InteractionType.component: + custom_id = interaction.data.get('custom_id') + if not custom_id: + return + + if custom_id.startswith('voice_'): + # Находим владельца по ID сообщения панели + owner_id = None + for uid, data in bot.voice_data.items(): + if data['panel_msg_id'] == interaction.message.id: + owner_id = uid + break + if not owner_id: + await interaction.response.send_message('❌ Панель не найдена.', ephemeral=True) + return + if interaction.user.id != owner_id: + await interaction.response.send_message('❌ Только владелец канала может управлять.', ephemeral=True) + return + + data = bot.voice_data[owner_id] + voice = interaction.guild.get_channel(data['voice_id']) + if not voice: + await interaction.response.send_message('❌ Голосовой канал удалён.', ephemeral=True) + return + + if custom_id == 'voice_name': + bot.modal_owner_cache[interaction.user.id] = owner_id + modal = discord.ui.Modal(title='Изменить название') + modal.add_item(discord.ui.TextInput(label='Новое название', default=voice.name)) + await interaction.response.send_modal(modal) + + elif custom_id == 'voice_limit': + bot.modal_owner_cache[interaction.user.id] = owner_id + modal = discord.ui.Modal(title='Изменить лимит') + modal.add_item(discord.ui.TextInput(label='Лимит (0 — безлимит)', default=str(voice.user_limit))) + await interaction.response.send_modal(modal) + + elif custom_id == 'voice_private': + perms = voice.overwrites_for(interaction.guild.default_role) + is_private = perms.connect is False + if is_private: + await voice.set_permissions(interaction.guild.default_role, connect=None) + await interaction.response.send_message('🔓 Канал теперь публичный.', ephemeral=True) + else: + await voice.set_permissions(interaction.guild.default_role, connect=False) + await interaction.response.send_message('🔒 Канал теперь приватный.', ephemeral=True) + + elif custom_id == 'voice_claim': + await interaction.response.send_message('Используйте команду `/voice-transfer @пользователь`', ephemeral=True) + + elif interaction.type == discord.InteractionType.modal_submit: + custom_id = interaction.custom_id + owner_id = bot.modal_owner_cache.get(interaction.user.id) + if not owner_id: + # Фолбэк: поиск по сообщению панели + for uid, data in bot.voice_data.items(): + if data['panel_msg_id'] == interaction.message.id: + owner_id = uid + break + if not owner_id: + await interaction.response.send_message('❌ Не удалось определить владельца.', ephemeral=True) + return + + voice = interaction.guild.get_channel(bot.voice_data[owner_id]['voice_id']) + if not voice: + await interaction.response.send_message('❌ Голосовой канал удалён.', ephemeral=True) + return + + if custom_id == 'voice_name': + new_name = interaction.data['components'][0]['components'][0]['value'] + await voice.edit(name=new_name) + # Обновляем заголовок в сообщении панели + data = bot.voice_data[owner_id] + panel_channel = interaction.guild.get_channel(data['panel_channel_id']) + if panel_channel: + try: + msg = await panel_channel.fetch_message(data['panel_msg_id']) + embed = msg.embeds[0] + embed.title = f'🎛️ Управление: {new_name\}' + await msg.edit(embed=embed) + except: + pass + await interaction.response.send_message(f'✅ Название изменено на **{new_name\}**.', ephemeral=True) + + elif custom_id == 'voice_limit': + try: + limit = int(interaction.data['components'][0]['components'][0]['value']) + if limit < 0: + raise ValueError + except: + await interaction.response.send_message('❌ Введите число >= 0.', ephemeral=True) + return + await voice.edit(user_limit=limit) + await interaction.response.send_message(f'✅ Лимит установлен на **{limit\}**.', ephemeral=True) + + # Очищаем кэш + if interaction.user.id in bot.modal_owner_cache: + del bot.modal_owner_cache[interaction.user.id] + +# ===================================================== +# 🚀 ЗАПУСК +# ===================================================== + +if __name__ == "__main__": + try: + bot.run(TOKEN) + except discord.LoginFailure: + logger.error("❌ Неверный токен!") + except Exception as e: + logger.error(f"❌ Ошибка: {e\}")$0 \ No newline at end of file