Все команды изменены на slash
Some checks failed
CI / basic-checks (push) Failing after 11s

This commit is contained in:
2026-03-07 16:35:52 +07:00
parent b89b30b7c5
commit 6f29d7a5ff
5 changed files with 171 additions and 188 deletions

View File

@@ -6,7 +6,6 @@ ENV POETRY_VIRTUALENVS_CREATE=false
WORKDIR /build WORKDIR /build
# Устанавливаем системные зависимости только в builder
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
ffmpeg \ ffmpeg \
@@ -15,15 +14,12 @@ RUN apt-get update \
build-essential \ build-essential \
curl \ curl \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Обновляем pip
RUN pip install --upgrade pip RUN pip install --upgrade pip
# Устанавливаем Poetry
RUN pip install poetry RUN pip install poetry
# Копируем файлы зависимостей
COPY pyproject.toml poetry.lock* ./ COPY pyproject.toml poetry.lock* ./
# Устанавливаем зависимости
RUN poetry install --no-interaction --no-ansi --no-root RUN poetry install --no-interaction --no-ansi --no-root
# ---------- RUNTIME ---------- # ---------- RUNTIME ----------
@@ -33,20 +29,18 @@ ENV PYTHONUNBUFFERED=1
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* # Устанавливаем runtime зависимости
# Копируем Python зависимости RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
nodejs \
&& rm -rf /var/lib/apt/lists/*
# Копируем python пакеты
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13 COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
COPY --from=builder /usr/local/bin /usr/local/bin COPY --from=builder /usr/local/bin /usr/local/bin
# Копируем ffmpeg бинарник
COPY --from=builder /usr/bin/ffmpeg /usr/bin/ffmpeg
# Копируем node runtime (для yt-dlp)
COPY --from=builder /usr/bin/node /usr/bin/node
COPY --from=builder /usr/bin/npm /usr/bin/npm
# Копируем проект # Копируем проект
COPY . . COPY . .
# Команда запуска CMD ["python", "main.py"]
CMD ["python", "main.py"]

View File

@@ -1,5 +1,5 @@
from discord.ext.commands import Cog, Bot, command, Context from discord.ext.commands import Cog, Bot, command, Context
import discord
from ..storage import storage from ..storage import storage
from .moderation import is_admin from .moderation import is_admin
@@ -12,7 +12,10 @@ class Blacklist(Cog):
def __init__(self, bot: Bot) -> None: def __init__(self, bot: Bot) -> None:
self.bot: Bot = bot self.bot: Bot = bot
@command() @discord.app_commands.command(
name="blacklist_show",
description="Показать текущий чёрный список слов",
)
@is_admin() @is_admin()
async def blacklist_show(self, ctx: Context) -> None: async def blacklist_show(self, ctx: Context) -> None:
""" """
@@ -25,7 +28,10 @@ class Blacklist(Cog):
else: else:
await ctx.send("Чёрный список:\n" + ", ".join(storage.blacklist)) await ctx.send("Чёрный список:\n" + ", ".join(storage.blacklist))
@command() @discord.app_commands.command(
name="blacklist_add",
description="Добавить слово в чёрный список",
)
@is_admin() @is_admin()
async def blacklist_add(self, ctx: Context, *, word: str) -> None: async def blacklist_add(self, ctx: Context, *, word: str) -> None:
""" """
@@ -43,7 +49,10 @@ class Blacklist(Cog):
storage.save_blacklist() storage.save_blacklist()
await ctx.send(f"Слово `{word_lower}` добавлено в чёрный список.") await ctx.send(f"Слово `{word_lower}` добавлено в чёрный список.")
@command() @discord.app_commands.command(
name="blacklist_remove",
description="Удалить слово из чёрного списка",
)
@is_admin() @is_admin()
async def blacklist_remove(self, ctx: Context, *, word: str) -> None: async def blacklist_remove(self, ctx: Context, *, word: str) -> None:
""" """

View File

@@ -55,25 +55,11 @@ class Moderation(Cog):
""" """
self.bot: Bot = bot self.bot: Bot = bot
@command()
@is_admin()
async def rules(self, ctx: Context) -> None:
"""
Показать правила сервера.
:param ctx: Контекст команды. @discord.app_commands.command(
""" name="kick",
rules_text: str = ( description="Исключить участника с сервера",
"**Правила сервера:**\n" )
"1. Уважайте других участников.\n"
"2. Запрещена реклама и спам.\n"
"3. Не используйте запрещённые слова.\n"
"4. Соблюдайте тематику каналов.\n"
"5. Выполняйте указания модераторов.\n"
)
await ctx.send(rules_text)
@command()
@is_admin() @is_admin()
async def kick( async def kick(
self, self,
@@ -97,15 +83,12 @@ class Moderation(Cog):
except discord.HTTPException: except discord.HTTPException:
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.") await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
@command() @discord.app_commands.command(
name="ban",
description="Забанить участника на сервере",
)
@is_admin() @is_admin()
async def ban( async def ban(self,ctx: Context,member: discord.Member,*,reason: Optional[str] = None) -> None:
self,
ctx: Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
""" """
Забанить участника на сервере. Забанить участника на сервере.
@@ -124,7 +107,10 @@ class Moderation(Cog):
except discord.HTTPException: except discord.HTTPException:
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.") await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
@command() @discord.app_commands.command(
name="unban",
description="Разбанить пользователя по имени или тегу",
)
@has_permissions(ban_members=True) @has_permissions(ban_members=True)
async def unban(self, ctx: Context, *, member_name: str) -> None: async def unban(self, ctx: Context, *, member_name: str) -> None:
""" """
@@ -193,7 +179,10 @@ class Moderation(Cog):
msg_lines.append(f"- {user.name}#{user.discriminator}") msg_lines.append(f"- {user.name}#{user.discriminator}")
await ctx.send("\n".join(msg_lines)) await ctx.send("\n".join(msg_lines))
@command() @discord.app_commands.command(
name="mute",
description="Выдать участнику мут",
)
@is_admin() @is_admin()
async def mute( async def mute(
self, self,
@@ -227,7 +216,10 @@ class Moderation(Cog):
except discord.HTTPException: except discord.HTTPException:
await ctx.send("Не удалось выдать мут из-за ошибки Discord.") await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
@command() @discord.app_commands.command(
name="unmute",
description="Снять мут с участника",
)
@is_admin() @is_admin()
async def unmute(self, ctx: Context, member: discord.Member) -> None: async def unmute(self, ctx: Context, member: discord.Member) -> None:
""" """
@@ -254,7 +246,10 @@ class Moderation(Cog):
except discord.HTTPException: except discord.HTTPException:
await ctx.send("Не удалось снять мут из-за ошибки Discord.") await ctx.send("Не удалось снять мут из-за ошибки Discord.")
@command() @discord.app_commands.command(
name="warn",
description="Выдать предупреждение участнику",
)
@is_admin() @is_admin()
async def warn( async def warn(
self, self,
@@ -289,8 +284,10 @@ class Moderation(Cog):
if warns_count >= max_warning: if warns_count >= max_warning:
await self.ban(ctx,member,reason=f"Превышен лимит предупреждений ({warns_count})") await self.ban(ctx,member,reason=f"Превышен лимит предупреждений ({warns_count})")
@discord.app_commands.command(
@command() name="warnings",
description="Показать предупреждения участника",
)
async def warnings(self, ctx: Context, member: discord.Member) -> None: async def warnings(self, ctx: Context, member: discord.Member) -> None:
""" """
Показать предупреждения участника. Показать предупреждения участника.
@@ -309,7 +306,10 @@ class Moderation(Cog):
lines.append(f"{i}. {w['reason']} ({w['date']})") lines.append(f"{i}. {w['reason']} ({w['date']})")
await ctx.send("\n".join(lines)) await ctx.send("\n".join(lines))
@command() @discord.app_commands.command(
name="clear",
description="Очистить указанное количество сообщений в канале",
)
@is_admin() @is_admin()
async def clear(self, ctx: Context, amount: int) -> None: async def clear(self, ctx: Context, amount: int) -> None:
""" """

View File

@@ -1,49 +1,60 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from typing import Optional, Dict, Any, List from typing import Dict, List, Any, Optional
import discord import discord
from discord.ext import commands from discord.ext import commands
import yt_dlp import yt_dlp
YTDL_FORMAT_OPTIONS: Dict[str, Any] = { # yt-dlp конфиг с поддержкой поиска и node
YTDL_OPTIONS: Dict[str, Any] = {
"format": "bestaudio/best", "format": "bestaudio/best",
"noplaylist": True, "noplaylist": True,
"quiet": True, "quiet": True,
"default_search": "ytsearch1",
"source_address": "0.0.0.0",
"js_runtimes": {
"node": {"path": "/usr/bin/node"}
},
"remote_components": {
"ejs:github": "github"
},
"extractor_args": {
"youtube": {
"player_client": ["web_music"]
}
}
} }
FFMPEG_OPTIONS: Dict[str, str] = { FFMPEG_OPTIONS: Dict[str, str] = {
"options": "-vn", "options": "-vn",
"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5"
} }
ytdl: yt_dlp.YoutubeDL = yt_dlp.YoutubeDL(YTDL_FORMAT_OPTIONS) ytdl = yt_dlp.YoutubeDL(YTDL_OPTIONS)
class Music(commands.Cog): class Music(commands.Cog):
def __init__(self, bot: commands.Bot):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot self.bot = bot
self.queue: Dict[int, List[str]] = {} self.queue: Dict[int, List[str]] = {} # guild_id -> список запросов
async def connect_to_voice(self, ctx: commands.Context) -> Optional[discord.VoiceClient]: async def connect_voice(self, interaction: discord.Interaction) -> Optional[discord.VoiceClient]:
if ctx.author.voice is None: if not interaction.user.voice or not interaction.user.voice.channel:
await ctx.send("❌ Вы должны находиться в голосовом канале.") await interaction.response.send_message("❌ Вы должны быть в голосовом канале.", ephemeral=True)
return None return None
voice_client: Optional[discord.VoiceClient] = ctx.voice_client voice_client = interaction.guild.voice_client
if voice_client is None: if voice_client is None:
try: try:
voice_client = await ctx.author.voice.channel.connect() voice_client = await interaction.user.voice.channel.connect()
except Exception: except Exception as e:
await ctx.send("Не удалось подключиться к голосовому каналу.") await interaction.response.send_message(f"Не удалось подключиться: {e}", ephemeral=True)
return None return None
return voice_client return voice_client
async def get_audio_data(self, query: str) -> Optional[Dict[str, Any]]: async def get_audio(self, query: str) -> Optional[Dict[str, Any]]:
"""Получает данные аудио по URL или поисковому запросу."""
try: try:
if query.startswith("http"): if query.startswith("http"):
data = ytdl.extract_info(query, download=False) data = ytdl.extract_info(query, download=False)
@@ -55,100 +66,89 @@ class Music(commands.Cog):
except Exception: except Exception:
return None return None
async def play_next(self, ctx: commands.Context) -> None: async def play_next(self, guild_id: int, channel: discord.TextChannel) -> None:
guild_id = ctx.guild.id if guild_id not in self.queue or not self.queue[guild_id]:
voice_client = ctx.voice_client await channel.send("📭 Очередь пуста.")
return
if guild_id in self.queue and self.queue[guild_id]: next_query = self.queue[guild_id].pop(0)
query = self.queue[guild_id].pop(0) # создаем фиктивный interaction для play
await self.play(ctx, query=query) class DummyInteraction:
else: guild = channel.guild
await ctx.send("📭 Очередь пуста.") user = channel.guild.me
response = type('Resp', (), {"send_message": lambda self, msg, ephemeral=False: asyncio.create_task(channel.send(msg))})()
@commands.command(name="play") await self.play(DummyInteraction(), next_query)
async def play(self, ctx: commands.Context, *, query: str) -> None:
voice_client = await self.connect_to_voice(ctx) @discord.app_commands.command(name="play", description="Воспроизвести трек или добавить в очередь")
async def play(self, interaction: discord.Interaction, query: str):
voice_client = await self.connect_voice(interaction)
if voice_client is None: if voice_client is None:
return return
guild_id = ctx.guild.id guild_id = interaction.guild.id
if guild_id not in self.queue: if guild_id not in self.queue:
self.queue[guild_id] = [] self.queue[guild_id] = []
if voice_client.is_playing(): if voice_client.is_playing():
self.queue[guild_id].append(query) self.queue[guild_id].append(query)
await ctx.send(" Трек добавлен в очередь.") await interaction.response.send_message(" Трек добавлен в очередь.")
return return
data = await self.get_audio_data(query) data = await self.get_audio(query)
if not data:
if data is None: await interaction.response.send_message("Не удалось найти трек.")
await ctx.send("Не удалось найти трек.")
return return
stream_url: str = data["url"] stream_url = data["url"]
title: str = data.get("title", "Неизвестный трек") title = data.get("title", "Неизвестный трек")
try: try:
source = await discord.FFmpegOpusAudio.from_probe( source = discord.FFmpegOpusAudio(
stream_url, stream_url,
executable="ffmpeg", executable="ffmpeg",
**FFMPEG_OPTIONS **FFMPEG_OPTIONS
) )
except Exception: except Exception:
await ctx.send("❌ Ошибка воспроизведения.") await interaction.response.send_message("❌ Ошибка воспроизведения.")
return return
def after_playing(error): def after_playing(error):
fut = asyncio.run_coroutine_threadsafe(self.play_next(ctx), self.bot.loop) fut = asyncio.run_coroutine_threadsafe(self.play_next(guild_id, interaction.channel), self.bot.loop)
try: try:
fut.result() fut.result()
except Exception: except Exception:
pass pass
voice_client.play(source, after=after_playing) voice_client.play(source, after=after_playing)
await interaction.response.send_message(f"▶️ Сейчас играет: **{title}**")
await ctx.send(f"▶️ Сейчас играет: **{title}**") @discord.app_commands.command(name="skip", description="Пропустить текущий трек")
async def skip(self, interaction: discord.Interaction):
@commands.command(name="skip") voice_client = interaction.guild.voice_client
async def skip(self, ctx: commands.Context) -> None: if not voice_client or not voice_client.is_playing():
voice_client: Optional[discord.VoiceClient] = ctx.voice_client await interaction.response.send_message("❌ Сейчас ничего не играет.", ephemeral=True)
if voice_client is None or not voice_client.is_playing():
await ctx.send("❌ Сейчас ничего не играет.")
return return
voice_client.stop() voice_client.stop()
await ctx.send("⏭️ Трек пропущен.") await interaction.response.send_message("⏭️ Трек пропущен.")
@commands.command(name="stop") @discord.app_commands.command(name="stop", description="Остановить музыку и очистить очередь")
async def stop(self, ctx: commands.Context) -> None: async def stop(self, interaction: discord.Interaction):
voice_client: Optional[discord.VoiceClient] = ctx.voice_client voice_client = interaction.guild.voice_client
if voice_client:
guild_id = interaction.guild.id
self.queue[guild_id] = []
if voice_client.is_playing():
voice_client.stop()
await interaction.response.send_message("⏹️ Музыка остановлена.")
if voice_client is None: @discord.app_commands.command(name="leave", description="Отключить бота от голосового канала")
await ctx.send("❌ Бот не подключён.") async def leave(self, interaction: discord.Interaction):
return voice_client = interaction.guild.voice_client
if voice_client:
guild_id = ctx.guild.id await voice_client.disconnect()
self.queue[guild_id] = [] await interaction.response.send_message("👋 Бот вышел из голосового канала.")
if voice_client.is_playing():
voice_client.stop()
await ctx.send("⏹️ Музыка остановлена.")
@commands.command(name="leave")
async def leave(self, ctx: commands.Context) -> None:
voice_client: Optional[discord.VoiceClient] = ctx.voice_client
if voice_client is None:
await ctx.send("❌ Бот не в голосовом канале.")
return
await voice_client.disconnect()
await ctx.send("👋 Бот вышел из голосового канала.")
async def setup(bot: commands.Bot) -> None: async def setup(bot: commands.Bot):
await bot.add_cog(Music(bot)) await bot.add_cog(Music(bot))

View File

@@ -1,94 +1,74 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional
from discord.ext.commands import Cog, Bot, Context, group import discord
from discord.ext import commands
from discord import app_commands
from ..storage import storage, Reminder from ..storage import storage, Reminder
from .moderation import is_admin from .moderation import is_admin
class Reminders(Cog): class Reminders(commands.Cog):
""" """Cog для управления напоминаниями: add, list, remove через slash-команды."""
Cog для управления напоминаниями: add, list, remove.
"""
def __init__(self, bot: Bot) -> None:
self.bot: Bot = bot
@group() def __init__(self, bot: commands.Bot):
self.bot = bot
reminder_group = app_commands.Group(name="reminder", description="Управление напоминаниями")
@reminder_group.command(name="add", description="Добавить новое напоминание")
@is_admin() @is_admin()
async def reminder(self, ctx: Context) -> None: @app_commands.describe(minutes="Через сколько минут сработает напоминание", text="Текст напоминания")
"""
Группа команд напоминаний.
:param ctx: Контекст команды.
"""
if ctx.invoked_subcommand is None:
await ctx.send(
"Используйте `!reminder add <минуты> <текст>`, "
"`!reminder list` или `!reminder remove <номер>`"
)
@reminder.command(name="add")
async def reminder_add( async def reminder_add(
self, ctx: Context, minutes: int, *, text: str self, interaction: discord.Interaction, minutes: int, text: str
) -> None: ) -> None:
"""
Добавить новое напоминание.
:param ctx: Контекст команды.
:param minutes: Через сколько минут сработает напоминание.
:param text: Текст напоминания.
"""
if minutes <= 0: if minutes <= 0:
await ctx.send("Время должно быть положительным числом минут.") await interaction.response.send_message(
"Время должно быть положительным числом минут.", ephemeral=True
)
return return
remind_time: datetime = datetime.now() + timedelta(minutes=minutes) remind_time = datetime.now() + timedelta(minutes=minutes)
storage.reminders.append( storage.reminders.append(
Reminder( Reminder(
time=remind_time.timestamp(), time=remind_time.timestamp(),
channel_id=ctx.channel.id, # type: ignore[assignment] channel_id=interaction.channel.id, # type: ignore
user_mention=ctx.author.mention, # type: ignore[union-attr] user_mention=interaction.user.mention, # type: ignore
text=text, text=text,
) )
) )
storage.save_reminders() storage.save_reminders()
await ctx.send(f"Напоминание добавлено через {minutes} минут: {text}") await interaction.response.send_message(
f"✅ Напоминание добавлено через {minutes} минут: {text}"
)
@reminder.command(name="list") @reminder_group.command(name="list", description="Показать список активных напоминаний")
async def reminder_list(self, ctx: Context) -> None: @is_admin()
""" async def reminder_list(self, interaction: discord.Interaction) -> None:
Показать список активных напоминаний.
:param ctx: Контекст команды.
"""
if not storage.reminders: if not storage.reminders:
await ctx.send("Активных напоминаний нет.") await interaction.response.send_message("Активных напоминаний нет.", ephemeral=True)
return return
msg: str = "Активные напоминания:\n" msg = "📋 Активные напоминания:\n"
for i, rem in enumerate(storage.reminders, 1): for i, rem in enumerate(storage.reminders, 1):
t_str: str = datetime.fromtimestamp(rem.time).strftime( t_str = datetime.fromtimestamp(rem.time).strftime("%Y-%m-%d %H:%M:%S")
"%Y-%m-%d %H:%M:%S"
)
msg += f"{i}. До {t_str}{rem.text} (от {rem.user_mention})\n" msg += f"{i}. До {t_str}{rem.text} (от {rem.user_mention})\n"
await ctx.send(msg)
@reminder.command(name="remove") await interaction.response.send_message(msg)
async def reminder_remove(self, ctx: Context, number: int) -> None:
"""
Удалить напоминание по номеру.
:param ctx: Контекст команды. @reminder_group.command(name="remove", description="Удалить напоминание по номеру")
:param number: Порядковый номер напоминания. @is_admin()
""" @app_commands.describe(number="Номер напоминания из списка")
async def reminder_remove(self, interaction: discord.Interaction, number: int) -> None:
if number <= 0 or number > len(storage.reminders): if number <= 0 or number > len(storage.reminders):
await ctx.send("Неверный номер напоминания.") await interaction.response.send_message("Неверный номер напоминания.", ephemeral=True)
return return
removed: Reminder = storage.reminders.pop(number - 1) removed = storage.reminders.pop(number - 1)
storage.save_reminders() storage.save_reminders()
await ctx.send(f"Удалено напоминание: {removed.text}") await interaction.response.send_message(f"Удалено напоминание: {removed.text}")
async def setup(bot: Bot) -> None: async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Reminders(bot)) await bot.add_cog(Reminders(bot))