This commit is contained in:
26
Dockerfile
26
Dockerfile
@@ -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"]
|
|
||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -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))
|
||||||
Reference in New Issue
Block a user