Compare commits
23 Commits
e9c2c456e0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ca9dae8106 | |||
| 80ac57e9e8 | |||
| 6f29d7a5ff | |||
| b89b30b7c5 | |||
| 2c6629f7f9 | |||
| cc821bc6e8 | |||
| 1cf1e7455a | |||
| 65176292b8 | |||
| 304937b162 | |||
| 4a3b9a48bb | |||
| ae9b716e29 | |||
| b5b16397a4 | |||
| 452690ffe8 | |||
| 90c09c7550 | |||
| 3c6bb49b4f | |||
| 5150846189 | |||
| 6cae88e362 | |||
| 635a8c0f74 | |||
| 869dd545b7 | |||
| 25df391f32 | |||
| 6cc5ea544e | |||
| 0f7364825a | |||
| 78239b4f9a |
@@ -78,7 +78,7 @@ temp/
|
|||||||
# Конфиденциальные файлы и настройки
|
# Конфиденциальные файлы и настройки
|
||||||
# -------------------------------
|
# -------------------------------
|
||||||
.env
|
.env
|
||||||
env/
|
/env
|
||||||
*.session
|
*.session
|
||||||
*.key
|
*.key
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
@@ -26,23 +26,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.13"
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install Poetry
|
|
||||||
uses: snok/install-poetry@v1
|
|
||||||
with:
|
|
||||||
version: "1.8.3"
|
|
||||||
virtualenvs-create: true
|
|
||||||
virtualenvs-in-project: true
|
|
||||||
installer-parallel: true
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
poetry install --no-interaction --no-root
|
python -m pip install --upgrade pip
|
||||||
|
pip install \
|
||||||
|
"discord>=2.3.2,<3.0.0" \
|
||||||
|
"loguru>=0.7.3,<0.8.0" \
|
||||||
|
"email-validator>=2.3.0,<3.0.0" \
|
||||||
|
"pydantic>=2.12.5,<3.0.0" \
|
||||||
|
"pydantic-settings>=2.12.0,<3.0.0"
|
||||||
|
|
||||||
- name: Basic import checks
|
- name: Basic import checks
|
||||||
run: |
|
run: |
|
||||||
poetry run python -c "import configs; from bot import Bot; print('Bot class ok')"
|
python -c "import configs; from bot import Bot; print('Bot class ok')"
|
||||||
poetry run python -c "from bot import discbot; print('Global bot instance ok')"
|
python -c "from bot import discbot; print('Global bot instance ok')"
|
||||||
|
|
||||||
- name: Load cogs without running bot
|
- name: Load cogs without running bot
|
||||||
run: |
|
run: |
|
||||||
poetry run python -c "from bot import discbot; import asyncio; asyncio.run(discbot.setup()); print('Cogs loaded')"
|
python -c "from bot import discbot; import asyncio; asyncio.run(discbot.setup()); print('Cogs loaded')"
|
||||||
|
|||||||
2
.idea/Bot.iml
generated
2
.idea/Bot.iml
generated
@@ -5,7 +5,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.13 (NotFateKursach)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.12 (bot)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="TemplatesService">
|
<component name="TemplatesService">
|
||||||
|
|||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -3,5 +3,5 @@
|
|||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.13 (Bot)" />
|
<option name="sdkName" value="Python 3.13 (Bot)" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (NotFateKursach)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (bot)" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
53
Dockerfile
53
Dockerfile
@@ -1,26 +1,47 @@
|
|||||||
# Используем официальный образ Python с подходящей версией
|
# ---------- BUILDER ----------
|
||||||
FROM python:3.13-slim
|
FROM python:3.13-slim AS builder
|
||||||
|
|
||||||
# Устанавливаем Poetry
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV POETRY_VIRTUALENVS_CREATE=false
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ffmpeg \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
|
|
||||||
# Устанавливаем рабочую директорию внутри контейнера
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Копируем файлы Poetry
|
|
||||||
COPY pyproject.toml poetry.lock* ./
|
COPY pyproject.toml poetry.lock* ./
|
||||||
|
|
||||||
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера)
|
|
||||||
RUN poetry config virtualenvs.create false
|
|
||||||
|
|
||||||
# Устанавливаем зависимости через Poetry
|
|
||||||
RUN poetry install --no-interaction --no-ansi --no-root
|
RUN poetry install --no-interaction --no-ansi --no-root
|
||||||
|
|
||||||
# Копируем все файлы проекта внутрь контейнера
|
# ---------- RUNTIME ----------
|
||||||
COPY . .
|
FROM python:3.13-slim
|
||||||
|
|
||||||
# Устанавливаем переменную окружения для буферизации
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Команда запуска — запуск скрипта main.py
|
WORKDIR /app
|
||||||
CMD ["python", "main.py"]
|
|
||||||
|
# Устанавливаем runtime зависимости
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ffmpeg \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN npm install -g deno
|
||||||
|
# Копируем python пакеты
|
||||||
|
COPY --from=builder /usr/local/lib/python3.13 /usr/local/lib/python3.13
|
||||||
|
COPY --from=builder /usr/local/bin /usr/local/bin
|
||||||
|
|
||||||
|
# Копируем проект
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
||||||
74
bot/bot.py
74
bot/bot.py
@@ -1,5 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
|
||||||
|
|
||||||
from discord import Intents
|
from discord import Intents
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
@@ -15,8 +14,7 @@ __all__ = ("Bot", "discbot")
|
|||||||
class Bot(commands.Bot):
|
class Bot(commands.Bot):
|
||||||
"""
|
"""
|
||||||
Основной класс Discord-бота с методами настройки и запуска.
|
Основной класс Discord-бота с методами настройки и запуска.
|
||||||
|
Поддерживает префиксные и slash-команды.
|
||||||
Поддерживает передачу token, prefix, intents и help_command в конструктор.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -26,33 +24,26 @@ class Bot(commands.Bot):
|
|||||||
intents: Optional[Intents] = None,
|
intents: Optional[Intents] = None,
|
||||||
help_command: Optional[commands.HelpCommand] = None,
|
help_command: Optional[commands.HelpCommand] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
:param token: Токен бота (если None — берётся из settings.BOT_TOKEN).
|
|
||||||
:param prefix: Префикс команд (если None — берётся из settings.PREFIX или '!').
|
|
||||||
:param intents: Intents (если None — создаются стандартные + privileged).
|
|
||||||
:param help_command: Кастомная команда помощи.
|
|
||||||
"""
|
|
||||||
# Intents по умолчанию
|
|
||||||
if intents is None:
|
if intents is None:
|
||||||
intents = Intents.default()
|
intents = Intents.default()
|
||||||
intents.guilds = True
|
intents.guilds = True
|
||||||
intents.message_content = True # Требует включения в Developer Portal
|
intents.message_content = True
|
||||||
intents.members = True
|
intents.members = True
|
||||||
|
intents.presences = True
|
||||||
|
intents.voice_states = True
|
||||||
|
|
||||||
# Префикс по умолчанию
|
|
||||||
command_prefix: str = prefix or getattr(settings, "PREFIX", "!")
|
command_prefix: str = prefix or getattr(settings, "PREFIX", "!")
|
||||||
|
|
||||||
# Help-команда по умолчанию
|
|
||||||
if help_command is None:
|
if help_command is None:
|
||||||
help_command = MyHelpCommand()
|
help_command = MyHelpCommand()
|
||||||
|
|
||||||
|
# ВАЖНО: tree создаёт сам commands.Bot, мы его не переопределяем
|
||||||
super().__init__(
|
super().__init__(
|
||||||
command_prefix=command_prefix,
|
command_prefix=command_prefix,
|
||||||
intents=intents,
|
intents=intents,
|
||||||
help_command=help_command,
|
help_command=help_command,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Сохраняем токен и хранилище
|
|
||||||
self._token: Optional[str] = token
|
self._token: Optional[str] = token
|
||||||
self.storage = storage # type: ignore[assignment]
|
self.storage = storage # type: ignore[assignment]
|
||||||
|
|
||||||
@@ -65,54 +56,71 @@ class Bot(commands.Bot):
|
|||||||
|
|
||||||
async def setup(self) -> None:
|
async def setup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Инициализация бота: логгер, cogs, логирование discord.py.
|
Инициализация бота: логгер и загрузка cogs.
|
||||||
"""
|
"""
|
||||||
logger.setup(start=True)
|
logger.setup(start=True)
|
||||||
logger.info(text="Настройка бота...", log_type="SYSTEM")
|
logger.info(text="Настройка бота...", log_type="SYSTEM")
|
||||||
|
|
||||||
await self.load_cogs()
|
await self.load_cogs()
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.WARNING,
|
|
||||||
format="%(asctime)s:%(levelname)s:%(name)s: %(message)s",
|
|
||||||
)
|
|
||||||
logging.getLogger("discord").setLevel(logging.INFO)
|
|
||||||
|
|
||||||
async def load_cogs(self) -> None:
|
async def load_cogs(self) -> None:
|
||||||
"""
|
"""
|
||||||
Загрузить все модули cogs.
|
Загрузить все модули cogs.
|
||||||
"""
|
"""
|
||||||
|
logger.info(text="Начинаю загрузку cogs...", log_type="COGS")
|
||||||
|
|
||||||
cogs: list[str] = [
|
cogs: list[str] = [
|
||||||
"cogs.events",
|
"bot.cogs.events",
|
||||||
"cogs.moderation",
|
"bot.cogs.moderation",
|
||||||
"cogs.blacklist",
|
"bot.cogs.blacklist",
|
||||||
"cogs.reminders",
|
"bot.cogs.reminders",
|
||||||
|
"bot.cogs.slash",
|
||||||
|
"bot.cogs.music",
|
||||||
]
|
]
|
||||||
for cog in cogs:
|
for cog in cogs:
|
||||||
try:
|
try:
|
||||||
await self.load_extension(cog)
|
await self.load_extension(cog)
|
||||||
logger.info(f"Загружен cog: {cog}", log_type="COGS")
|
logger.info(text=f"Загружен cog: {cog}", log_type="COGS")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка загрузки {cog}: {e}", log_type="COGS")
|
logger.error(text=f"Ошибка загрузки {cog}: {e!r}", log_type="COGS")
|
||||||
|
|
||||||
|
async def setup_hook(self) -> None:
|
||||||
|
"""
|
||||||
|
Хук discord.py 2.x: вызывается перед подключением к Gateway.
|
||||||
|
Здесь синхронизируем slash-команды.
|
||||||
|
"""
|
||||||
|
await self.tree.sync()
|
||||||
|
logger.info(text="Slash-команды синхронизированы", log_type="SYSTEM")
|
||||||
|
|
||||||
async def start_bot(self, token: Optional[str] = None) -> None:
|
async def start_bot(self, token: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Запуск бота с использованием сохранённого токена или переданного.
|
Запуск бота с использованием сохранённого токена или переданного.
|
||||||
|
|
||||||
:param token: Токен бота (если None — используется self.token).
|
|
||||||
"""
|
"""
|
||||||
use_token: Optional[str] = token or self.token
|
use_token: Optional[str] = token or self.token
|
||||||
if not use_token:
|
if not use_token:
|
||||||
error: str = "BOT_TOKEN не задан (ни в конструкторе, ни в settings)"
|
error: str = "BOT_TOKEN не задан (ни в конструкторе, ни в settings)"
|
||||||
logger.error(error)
|
logger.error(text=error, log_type="START")
|
||||||
raise ValueError(error)
|
raise ValueError(error)
|
||||||
|
|
||||||
logger.info(text="Запуск бота...", log_type="START")
|
logger.info(text="Запуск бота...", log_type="START")
|
||||||
await self.start(use_token)
|
await self.start(use_token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def on_command(ctx: commands.Context) -> None:
|
||||||
|
"""
|
||||||
|
Глобальное логирование всех вызванных префиксных команд.
|
||||||
|
"""
|
||||||
|
logger.info(
|
||||||
|
text=(
|
||||||
|
f"Команда: {ctx.command} | Автор: {ctx.author} | "
|
||||||
|
f"Гильдия: {ctx.guild} | Сообщение: {ctx.message.content}"
|
||||||
|
),
|
||||||
|
log_type="COMMAND",
|
||||||
|
user=str(ctx.author),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Глобальный экземпляр — МОЖНО ПЕРЕДАВАТЬ token/prefix ПРЯМО ЗДЕСЬ
|
|
||||||
discbot: Bot = Bot(
|
discbot: Bot = Bot(
|
||||||
token=settings.BOT_TOKEN, # кастомный токен
|
token=settings.BOT_TOKEN,
|
||||||
prefix=settings.PREFIX, # кастомный префикс
|
prefix=settings.PREFIX,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ from .events import *
|
|||||||
from .blacklist import *
|
from .blacklist import *
|
||||||
from .reminders import *
|
from .reminders import *
|
||||||
from .moderation import *
|
from .moderation import *
|
||||||
|
from .music import *
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
from discord.ext import commands
|
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
|
||||||
|
|
||||||
|
|
||||||
class Blacklist(commands.Cog):
|
class Blacklist(Cog):
|
||||||
"""
|
"""
|
||||||
Cog для управления чёрным списком слов.
|
Cog для управления чёрным списком слов.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
def __init__(self, bot: Bot) -> None:
|
||||||
self.bot: commands.Bot = bot
|
self.bot: Bot = bot
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="blacklist_show",
|
||||||
|
description="Показать текущий чёрный список слов",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def blacklist_show(self, ctx: commands.Context) -> None:
|
async def blacklist_show(self, ctx: Context) -> None:
|
||||||
"""
|
"""
|
||||||
Показать текущий чёрный список слов.
|
Показать текущий чёрный список слов.
|
||||||
|
|
||||||
@@ -25,9 +28,12 @@ class Blacklist(commands.Cog):
|
|||||||
else:
|
else:
|
||||||
await ctx.send("Чёрный список:\n" + ", ".join(storage.blacklist))
|
await ctx.send("Чёрный список:\n" + ", ".join(storage.blacklist))
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="blacklist_add",
|
||||||
|
description="Добавить слово в чёрный список",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def blacklist_add(self, ctx: commands.Context, *, word: str) -> None:
|
async def blacklist_add(self, ctx: Context, *, word: str) -> None:
|
||||||
"""
|
"""
|
||||||
Добавить слово в чёрный список.
|
Добавить слово в чёрный список.
|
||||||
|
|
||||||
@@ -43,9 +49,12 @@ class Blacklist(commands.Cog):
|
|||||||
storage.save_blacklist()
|
storage.save_blacklist()
|
||||||
await ctx.send(f"Слово `{word_lower}` добавлено в чёрный список.")
|
await ctx.send(f"Слово `{word_lower}` добавлено в чёрный список.")
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="blacklist_remove",
|
||||||
|
description="Удалить слово из чёрного списка",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def blacklist_remove(self, ctx: commands.Context, *, word: str) -> None:
|
async def blacklist_remove(self, ctx: Context, *, word: str) -> None:
|
||||||
"""
|
"""
|
||||||
Удалить слово из чёрного списка.
|
Удалить слово из чёрного списка.
|
||||||
|
|
||||||
@@ -62,5 +71,5 @@ class Blacklist(commands.Cog):
|
|||||||
await ctx.send(f"Слово `{word_lower}` удалено из чёрного списка.")
|
await ctx.send(f"Слово `{word_lower}` удалено из чёрного списка.")
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: Bot) -> None:
|
||||||
await bot.add_cog(Blacklist(bot))
|
await bot.add_cog(Blacklist(bot))
|
||||||
|
|||||||
@@ -1,34 +1,98 @@
|
|||||||
import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands, tasks
|
from discord.ext import tasks
|
||||||
|
from discord.ext.commands import Bot, Cog, Context, CommandError
|
||||||
from discord.utils import get
|
from discord.utils import get
|
||||||
|
|
||||||
from configs import settings
|
from configs import settings
|
||||||
from middleware import logger
|
from middleware.loggers import logger
|
||||||
from ..storage import storage, Reminder
|
from ..storage import storage, Reminder
|
||||||
|
|
||||||
|
|
||||||
class Events(commands.Cog):
|
class Events(Cog):
|
||||||
"""
|
"""
|
||||||
Cog с обработчиками событий и фоновой задачей напоминаний.
|
Cog с обработчиками событий и фоновой задачей напоминаний.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
def __init__(self, bot: Bot) -> None:
|
||||||
self.bot: commands.Bot = bot
|
self.bot: Bot = bot
|
||||||
self.check_reminders.start()
|
self.check_reminders.start()
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@Cog.listener()
|
||||||
|
async def on_presence_update(self, before: discord.Member, after: discord.Member) -> None:
|
||||||
|
"""
|
||||||
|
Следим за конкретным пользователем и меняем ник на '[AFK] Ник' при уходе в Idle,
|
||||||
|
а при возвращении восстанавливаем оригинальный ник.
|
||||||
|
"""
|
||||||
|
afk_nickname = getattr(settings, "AFK_NICKNAME", "AFK")
|
||||||
|
# если статус не изменился — выходим
|
||||||
|
if before.status == after.status:
|
||||||
|
return
|
||||||
|
# словарь для хранения оригинального ника
|
||||||
|
if not hasattr(self, 'original_nick'):
|
||||||
|
self.original_nick = {}
|
||||||
|
|
||||||
|
# пользователь ушёл в AFK/Idle
|
||||||
|
if after.status == discord.Status.idle:
|
||||||
|
# если уже AFK, ничего не делаем
|
||||||
|
if after.nick and after.nick.startswith(afk_nickname):
|
||||||
|
return
|
||||||
|
|
||||||
|
# сохраняем оригинальный ник
|
||||||
|
self.original_nick[after.id] = after.nick or after.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
await after.edit(nick=afk_nickname)
|
||||||
|
logger.info(
|
||||||
|
text=f"{after} ушёл в AFK, ник изменён",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
user=str(after),
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав изменить ник {after}",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка смены ника {after}: {e!r}",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
)
|
||||||
|
|
||||||
|
# пользователь вернулся из AFK
|
||||||
|
elif before.status == discord.Status.idle and after.status != discord.Status.idle:
|
||||||
|
original = self.original_nick.get(after.id)
|
||||||
|
if original:
|
||||||
|
try:
|
||||||
|
await after.edit(nick=original)
|
||||||
|
logger.info(
|
||||||
|
text=f"{after} вернулся из AFK, ник восстановлен",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
user=str(after),
|
||||||
|
)
|
||||||
|
del self.original_nick[after.id] # убираем из словаря
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав вернуть ник {after}",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка возврата ника {after}: {e!r}",
|
||||||
|
log_type="PRESENCE",
|
||||||
|
)
|
||||||
|
@Cog.listener()
|
||||||
async def on_ready(self) -> None:
|
async def on_ready(self) -> None:
|
||||||
"""
|
"""
|
||||||
Событие запуска бота.
|
Событие запуска бота.
|
||||||
Загружает данные и создаёт необходимые роли.
|
Загружает данные и создаёт необходимые роли.
|
||||||
"""
|
"""
|
||||||
logger.info(text=f"Бот запущен как {self.bot.user}")
|
logger.info(text=f"Бот запущен как {self.bot.user}", log_type="SYSTEM")
|
||||||
storage.load_all()
|
storage.load_all()
|
||||||
await self.ensure_roles_exist()
|
await self.ensure_roles_exist()
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@Cog.listener()
|
||||||
async def on_member_join(self, member: discord.Member) -> None:
|
async def on_member_join(self, member: discord.Member) -> None:
|
||||||
"""
|
"""
|
||||||
Событие вступления нового участника на сервер.
|
Событие вступления нового участника на сервер.
|
||||||
@@ -37,20 +101,31 @@ class Events(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
new_member_role: discord.Role | None = get(member.guild.roles, name="New Member")
|
new_member_role: discord.Role | None = get(member.guild.roles, name="New Member")
|
||||||
if new_member_role:
|
if new_member_role:
|
||||||
await member.add_roles(new_member_role)
|
try:
|
||||||
|
await member.add_roles(new_member_role)
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав выдать роль New Member пользователю {member}",
|
||||||
|
log_type="EVENT",
|
||||||
|
user=str(member),
|
||||||
|
)
|
||||||
|
|
||||||
channel: discord.abc.MessageableChannel | None = self.bot.get_channel(
|
channel = self.bot.get_channel(settings.WELCOME_CHANNEL_ID)
|
||||||
settings.WELCOME_CHANNEL_ID
|
|
||||||
)
|
|
||||||
if isinstance(channel, discord.TextChannel):
|
if isinstance(channel, discord.TextChannel):
|
||||||
await channel.send(f"Приветствуем {member.mention} на сервере!")
|
try:
|
||||||
|
await channel.send(f"Приветствуем {member.mention} на сервере!")
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Не удалось отправить приветствие для {member}: {e!r}",
|
||||||
|
log_type="EVENT",
|
||||||
|
user=str(member),
|
||||||
|
)
|
||||||
|
|
||||||
@commands.Cog.listener()
|
@Cog.listener()
|
||||||
async def on_message(self, message: discord.Message) -> None:
|
async def on_message(self, message: discord.Message) -> None:
|
||||||
"""
|
"""
|
||||||
Событие получения сообщения. Проверяет чёрный список слов.
|
Событие получения сообщения. Проверяет чёрный список слов
|
||||||
|
и передаёт сообщение обработчику команд.
|
||||||
:param message: Полученное сообщение.
|
|
||||||
"""
|
"""
|
||||||
if message.author.bot or not message.content:
|
if message.author.bot or not message.content:
|
||||||
return
|
return
|
||||||
@@ -62,11 +137,35 @@ class Events(commands.Cog):
|
|||||||
await message.channel.send(
|
await message.channel.send(
|
||||||
f"{message.author.mention}, ваше сообщение содержит запрещённые слова."
|
f"{message.author.mention}, ваше сообщение содержит запрещённые слова."
|
||||||
)
|
)
|
||||||
except Exception:
|
logger.info(
|
||||||
pass
|
text=f"Удалено сообщение с запрещённым словом от {message.author}: {message.content!r}",
|
||||||
|
log_type="BLACKLIST",
|
||||||
|
user=str(message.author),
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав удалить сообщение {message.author}: {message.content!r}",
|
||||||
|
log_type="BLACKLIST",
|
||||||
|
user=str(message.author),
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка при удалении сообщения {message.author}: {e!r}",
|
||||||
|
log_type="BLACKLIST",
|
||||||
|
user=str(message.author),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.bot.process_commands(message)
|
@Cog.listener()
|
||||||
|
async def on_command_error(self, ctx: Context, error: CommandError) -> None:
|
||||||
|
"""
|
||||||
|
Логирование ошибок команд.
|
||||||
|
"""
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка команды: {ctx.command} | Автор: {ctx.author} | Ошибка: {error!r}",
|
||||||
|
log_type="COMMAND",
|
||||||
|
user=str(ctx.author),
|
||||||
|
)
|
||||||
|
|
||||||
async def ensure_roles_exist(self) -> None:
|
async def ensure_roles_exist(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -78,18 +177,40 @@ class Events(commands.Cog):
|
|||||||
try:
|
try:
|
||||||
muted_role = await guild.create_role(name="Muted")
|
muted_role = await guild.create_role(name="Muted")
|
||||||
for channel in guild.channels:
|
for channel in guild.channels:
|
||||||
await channel.set_permissions(
|
try:
|
||||||
muted_role, send_messages=False, speak=False
|
await channel.set_permissions(
|
||||||
)
|
muted_role, send_messages=False, speak=False
|
||||||
except Exception:
|
)
|
||||||
pass
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав настроить права для канала {channel} в {guild}",
|
||||||
|
log_type="ROLES",
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Нет прав создать роль Muted в {guild}",
|
||||||
|
log_type="ROLES",
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка при создании роли Muted в {guild}: {e!r}",
|
||||||
|
log_type="ROLES",
|
||||||
|
)
|
||||||
|
|
||||||
new_member_role: discord.Role | None = get(guild.roles, name="New Member")
|
new_member_role: discord.Role | None = get(guild.roles, name="New Member")
|
||||||
if new_member_role is None:
|
if new_member_role is None:
|
||||||
try:
|
try:
|
||||||
await guild.create_role(name="New Member")
|
await guild.create_role(name="New Member")
|
||||||
except Exception:
|
except discord.Forbidden:
|
||||||
pass
|
logger.warning(
|
||||||
|
text=f"Нет прав создать роль New Member в {guild}",
|
||||||
|
log_type="ROLES",
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка при создании роли New Member в {guild}: {e!r}",
|
||||||
|
log_type="ROLES",
|
||||||
|
)
|
||||||
|
|
||||||
@tasks.loop(seconds=30)
|
@tasks.loop(seconds=30)
|
||||||
async def check_reminders(self) -> None:
|
async def check_reminders(self) -> None:
|
||||||
@@ -97,19 +218,32 @@ class Events(commands.Cog):
|
|||||||
Фоновая задача, которая каждые 30 секунд проверяет напоминания
|
Фоновая задача, которая каждые 30 секунд проверяет напоминания
|
||||||
и отправляет просроченные.
|
и отправляет просроченные.
|
||||||
"""
|
"""
|
||||||
now: float = datetime.datetime.now().timestamp()
|
now: float = datetime.now().timestamp()
|
||||||
to_remove: list[Reminder] = []
|
to_remove: list[Reminder] = []
|
||||||
|
|
||||||
for rem in storage.reminders:
|
for rem in list(storage.reminders):
|
||||||
if rem.time <= now:
|
if rem.time <= now:
|
||||||
channel = self.bot.get_channel(rem.channel_id)
|
channel = self.bot.get_channel(rem.channel_id)
|
||||||
if isinstance(channel, discord.TextChannel):
|
if isinstance(channel, discord.TextChannel):
|
||||||
await channel.send(f"{rem.user_mention} Напоминание: {rem.text}")
|
try:
|
||||||
|
await channel.send(f"{rem.user_mention} Напоминание: {rem.text}")
|
||||||
|
logger.info(
|
||||||
|
text=f"Отправлено напоминание пользователю {rem.user_mention}: {rem.text!r}",
|
||||||
|
log_type="REMINDER",
|
||||||
|
)
|
||||||
|
except discord.HTTPException as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка при отправке напоминания в канал {channel.id}: {e!r}",
|
||||||
|
log_type="REMINDER",
|
||||||
|
)
|
||||||
to_remove.append(rem)
|
to_remove.append(rem)
|
||||||
|
|
||||||
if to_remove:
|
if to_remove:
|
||||||
for rem in to_remove:
|
for rem in to_remove:
|
||||||
storage.reminders.remove(rem)
|
try:
|
||||||
|
storage.reminders.remove(rem)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
storage.save_reminders()
|
storage.save_reminders()
|
||||||
|
|
||||||
@check_reminders.before_loop
|
@check_reminders.before_loop
|
||||||
@@ -120,7 +254,7 @@ class Events(commands.Cog):
|
|||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: Bot) -> None:
|
||||||
"""
|
"""
|
||||||
Функция для загрузки Cog.
|
Функция для загрузки Cog.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Callable, Awaitable, Optional
|
from typing import Callable, Awaitable, Optional
|
||||||
|
from datetime import datetime
|
||||||
import datetime
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.app_commands.checks import has_permissions
|
||||||
|
from discord.ext.commands import Bot, Context, Cog, check, command
|
||||||
from discord.utils import get
|
from discord.utils import get
|
||||||
|
|
||||||
from configs import settings
|
from configs import settings
|
||||||
@@ -13,7 +13,7 @@ from ..storage import storage
|
|||||||
|
|
||||||
|
|
||||||
# Тип предиката для check (для читаемости, но не обязателен)
|
# Тип предиката для check (для читаемости, но не обязателен)
|
||||||
CheckPredicate = Callable[[commands.Context], Awaitable[bool]]
|
CheckPredicate = Callable[[Context], Awaitable[bool]]
|
||||||
|
|
||||||
|
|
||||||
def is_admin():
|
def is_admin():
|
||||||
@@ -23,7 +23,7 @@ def is_admin():
|
|||||||
либо у пользователя есть флаг администратора сервера.
|
либо у пользователя есть флаг администратора сервера.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def predicate(ctx: commands.Context) -> bool:
|
async def predicate(ctx: Context) -> bool:
|
||||||
author = ctx.author
|
author = ctx.author
|
||||||
if not isinstance(author, discord.Member):
|
if not isinstance(author, discord.Member):
|
||||||
return False
|
return False
|
||||||
@@ -31,10 +31,10 @@ def is_admin():
|
|||||||
admin_role = get(author.roles, name=settings.ADMIN_ROLE_NAME)
|
admin_role = get(author.roles, name=settings.ADMIN_ROLE_NAME)
|
||||||
return bool(admin_role) or author.guild_permissions.administrator
|
return bool(admin_role) or author.guild_permissions.administrator
|
||||||
|
|
||||||
return commands.check(predicate)
|
return check(predicate)
|
||||||
|
|
||||||
|
|
||||||
def require_guild(ctx: commands.Context) -> Optional[discord.Guild]:
|
def require_guild(ctx: Context) -> Optional[discord.Guild]:
|
||||||
"""
|
"""
|
||||||
Безопасно получить guild из контекста.
|
Безопасно получить guild из контекста.
|
||||||
|
|
||||||
@@ -43,41 +43,27 @@ def require_guild(ctx: commands.Context) -> Optional[discord.Guild]:
|
|||||||
return ctx.guild
|
return ctx.guild
|
||||||
|
|
||||||
|
|
||||||
class Moderation(commands.Cog):
|
class Moderation(Cog):
|
||||||
"""
|
"""
|
||||||
Cog с модерационными командами:
|
Cog с модерационными командами:
|
||||||
rules, kick, ban, unban, mute, unmute, warn, warnings, clear.
|
rules, kick, ban, unban, mute, unmute, warn, warnings, clear.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
def __init__(self, bot: Bot) -> None:
|
||||||
"""
|
"""
|
||||||
:param bot: Экземпляр бота, к которому привязан cog.
|
:param bot: Экземпляр бота, к которому привязан cog.
|
||||||
"""
|
"""
|
||||||
self.bot: commands.Bot = bot
|
self.bot: Bot = bot
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@is_admin()
|
|
||||||
async def rules(self, ctx: commands.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)
|
|
||||||
|
|
||||||
@commands.command()
|
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def kick(
|
async def kick(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: Context,
|
||||||
member: discord.Member,
|
member: discord.Member,
|
||||||
*,
|
*,
|
||||||
reason: Optional[str] = None,
|
reason: Optional[str] = None,
|
||||||
@@ -97,15 +83,12 @@ class Moderation(commands.Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
|
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
|
||||||
|
|
||||||
@commands.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: commands.Context,
|
|
||||||
member: discord.Member,
|
|
||||||
*,
|
|
||||||
reason: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
"""
|
||||||
Забанить участника на сервере.
|
Забанить участника на сервере.
|
||||||
|
|
||||||
@@ -113,6 +96,9 @@ class Moderation(commands.Cog):
|
|||||||
:param member: Участник для бана.
|
:param member: Участник для бана.
|
||||||
:param reason: Причина бана.
|
:param reason: Причина бана.
|
||||||
"""
|
"""
|
||||||
|
user_id: str = str(member.id)
|
||||||
|
storage.user_warnings.pop(user_id,None)
|
||||||
|
storage.save_warnings()
|
||||||
try:
|
try:
|
||||||
await member.ban(reason=reason)
|
await member.ban(reason=reason)
|
||||||
await ctx.send(f"{member} был забанен. Причина: {reason}")
|
await ctx.send(f"{member} был забанен. Причина: {reason}")
|
||||||
@@ -121,9 +107,12 @@ class Moderation(commands.Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
|
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
@commands.has_permissions(ban_members=True)
|
name="unban",
|
||||||
async def unban(self, ctx: commands.Context, *, member_name: str) -> None:
|
description="Разбанить пользователя по имени или тегу",
|
||||||
|
)
|
||||||
|
@has_permissions(ban_members=True)
|
||||||
|
async def unban(self, ctx: Context, *, member_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Разбанить пользователя по имени или тегу.
|
Разбанить пользователя по имени или тегу.
|
||||||
|
|
||||||
@@ -190,11 +179,14 @@ class Moderation(commands.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))
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="mute",
|
||||||
|
description="Выдать участнику мут",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def mute(
|
async def mute(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: Context,
|
||||||
member: discord.Member,
|
member: discord.Member,
|
||||||
*,
|
*,
|
||||||
reason: Optional[str] = None,
|
reason: Optional[str] = None,
|
||||||
@@ -224,9 +216,12 @@ class Moderation(commands.Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
|
await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="unmute",
|
||||||
|
description="Снять мут с участника",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def unmute(self, ctx: commands.Context, member: discord.Member) -> None:
|
async def unmute(self, ctx: Context, member: discord.Member) -> None:
|
||||||
"""
|
"""
|
||||||
Снять мут с участника.
|
Снять мут с участника.
|
||||||
|
|
||||||
@@ -251,11 +246,14 @@ class Moderation(commands.Cog):
|
|||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
await ctx.send("Не удалось снять мут из-за ошибки Discord.")
|
await ctx.send("Не удалось снять мут из-за ошибки Discord.")
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="warn",
|
||||||
|
description="Выдать предупреждение участнику",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def warn(
|
async def warn(
|
||||||
self,
|
self,
|
||||||
ctx: commands.Context,
|
ctx: Context,
|
||||||
member: discord.Member,
|
member: discord.Member,
|
||||||
*,
|
*,
|
||||||
reason: Optional[str] = None,
|
reason: Optional[str] = None,
|
||||||
@@ -267,22 +265,30 @@ class Moderation(commands.Cog):
|
|||||||
:param member: Участник.
|
:param member: Участник.
|
||||||
:param reason: Причина предупреждения.
|
:param reason: Причина предупреждения.
|
||||||
"""
|
"""
|
||||||
|
max_warning= 3
|
||||||
user_id: str = str(member.id)
|
user_id: str = str(member.id)
|
||||||
storage.user_warnings.setdefault(user_id, []).append(
|
storage.user_warnings.setdefault(user_id, []).append(
|
||||||
{
|
{
|
||||||
"reason": reason or "Без причины",
|
"reason": reason or "Без причины",
|
||||||
"date": datetime.datetime.now().isoformat(
|
"date": datetime.now().isoformat(
|
||||||
sep=" ", timespec="seconds"
|
sep=" ", timespec="seconds"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
storage.save_warnings()
|
storage.save_warnings()
|
||||||
|
warns_count = len(storage.user_warnings[user_id])
|
||||||
await ctx.send(
|
await ctx.send(
|
||||||
f"{member} получил предупреждение. Причина: {reason or 'Без причины'}"
|
f"{member} получил {warns_count} предупреждение. Причина: {reason or 'Без причины'}. До бана осталось {max_warning-warns_count} предупреждения."
|
||||||
)
|
)
|
||||||
|
|
||||||
@commands.command()
|
if warns_count >= max_warning:
|
||||||
async def warnings(self, ctx: commands.Context, member: discord.Member) -> None:
|
await self.ban(ctx,member,reason=f"Превышен лимит предупреждений ({warns_count})")
|
||||||
|
|
||||||
|
@discord.app_commands.command(
|
||||||
|
name="warnings",
|
||||||
|
description="Показать предупреждения участника",
|
||||||
|
)
|
||||||
|
async def warnings(self, ctx: Context, member: discord.Member) -> None:
|
||||||
"""
|
"""
|
||||||
Показать предупреждения участника.
|
Показать предупреждения участника.
|
||||||
|
|
||||||
@@ -300,9 +306,12 @@ class Moderation(commands.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))
|
||||||
|
|
||||||
@commands.command()
|
@discord.app_commands.command(
|
||||||
|
name="clear",
|
||||||
|
description="Очистить указанное количество сообщений в канале",
|
||||||
|
)
|
||||||
@is_admin()
|
@is_admin()
|
||||||
async def clear(self, ctx: commands.Context, amount: int) -> None:
|
async def clear(self, ctx: Context, amount: int) -> None:
|
||||||
"""
|
"""
|
||||||
Очистить указанное количество сообщений в канале.
|
Очистить указанное количество сообщений в канале.
|
||||||
|
|
||||||
@@ -320,7 +329,7 @@ class Moderation(commands.Cog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: Bot) -> None:
|
||||||
"""
|
"""
|
||||||
Зарегистрировать cog в боте.
|
Зарегистрировать cog в боте.
|
||||||
|
|
||||||
|
|||||||
219
bot/cogs/music.py
Normal file
219
bot/cogs/music.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
from middleware import logger
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
COG_TYPE="Music"
|
||||||
|
|
||||||
|
# yt-dlp конфиг с поддержкой поиска и node
|
||||||
|
YTDL_OPTIONS: Dict[str, Any] = {
|
||||||
|
"format": "bestaudio/best",
|
||||||
|
"noplaylist": True,
|
||||||
|
"quiet": True,
|
||||||
|
"default_search": "ytsearch",
|
||||||
|
"source_address": "0.0.0.0",
|
||||||
|
"remote_components": {
|
||||||
|
"ejs:github": "github"
|
||||||
|
},
|
||||||
|
# "js_runtimes": {
|
||||||
|
# "deno": {'path': "/usr/local/bin/deno"}
|
||||||
|
# }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMPEG_OPTIONS: Dict[str, str] = {
|
||||||
|
"options": "-vn",
|
||||||
|
"before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
ytdl = yt_dlp.YoutubeDL(YTDL_OPTIONS)
|
||||||
|
|
||||||
|
|
||||||
|
class Music(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot):
|
||||||
|
self.bot = bot
|
||||||
|
self.queue: Dict[int, List[Dict[str, str]]] = {}
|
||||||
|
logger.info(text="Инициализация Music", log_type="cog")
|
||||||
|
|
||||||
|
@discord.app_commands.command(name="queue", description="Посмотреть очередь")
|
||||||
|
async def getQueue(self, interaction: discord.Interaction):
|
||||||
|
safeQueue = "\n".join(
|
||||||
|
track["title"]
|
||||||
|
for guild_queue in self.queue.values()
|
||||||
|
for track in guild_queue
|
||||||
|
) or "❌ Пустая очередь"
|
||||||
|
|
||||||
|
logger.info(text=f"Текущая очередь:\n{safeQueue}", log_type="cog")
|
||||||
|
|
||||||
|
await interaction.response.send_message(f"Текущая очередь:\n{safeQueue}")
|
||||||
|
|
||||||
|
|
||||||
|
async def connect_voice(self, interaction: discord.Interaction) -> Optional[discord.VoiceClient]:
|
||||||
|
if not interaction.user.voice or not interaction.user.voice.channel:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Юзер не в голосовом канале.\nЮзер: {interaction.user.voice}. Канал: {interaction.user.voice.channel}",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
await interaction.response.send_message("❌ Вы должны быть в голосовом канале.", ephemeral=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
voice_client = interaction.guild.voice_client
|
||||||
|
if voice_client is None:
|
||||||
|
try:
|
||||||
|
voice_client = await interaction.user.voice.channel.connect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(text=f"Не удалось подключиться\nОшибка: {e}", log_type="cog")
|
||||||
|
await interaction.response.send_message(f"❌ Не удалось подключиться: {e}", ephemeral=True)
|
||||||
|
return None
|
||||||
|
return voice_client
|
||||||
|
|
||||||
|
async def get_audio(self, query: str, interaction: discord.Interaction) -> Optional[Dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
text=f"Поиск по ключевому слову {query}...",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
if query.startswith("http"):
|
||||||
|
data = ytdl.extract_info(query, download=False);
|
||||||
|
logger.info(
|
||||||
|
text=f"Предоставлена ссылка {query}",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data = ytdl.extract_info(f"ytsearch:{query}", download=False)
|
||||||
|
|
||||||
|
if "entries" in data:
|
||||||
|
title = data["entries"][0]['title']
|
||||||
|
data = data["entries"][0]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
text=f"Найдено {title}",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
text=f"Ничего не найдено",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
text=f"Ошибка при получении аудио {e}",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
await interaction.followup.send(f":x: Ошибка при получении аудио")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def play_next(self, guild_id: int, channel: discord.TextChannel):
|
||||||
|
|
||||||
|
if guild_id not in self.queue or not self.queue[guild_id]:
|
||||||
|
await channel.send("📭 Очередь пуста.")
|
||||||
|
return
|
||||||
|
|
||||||
|
track = self.queue[guild_id].pop(0)
|
||||||
|
|
||||||
|
voice_client = channel.guild.voice_client
|
||||||
|
if not voice_client:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.start_track(voice_client, guild_id, channel, track)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
return
|
||||||
|
|
||||||
|
guild_id = interaction.guild.id
|
||||||
|
if guild_id not in self.queue:
|
||||||
|
self.queue[guild_id] = []
|
||||||
|
await interaction.response.defer()
|
||||||
|
data = await self.get_audio(query, interaction)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
await interaction.followup.send("❌ Не удалось найти трек.")
|
||||||
|
return
|
||||||
|
|
||||||
|
stream_url = data["url"]
|
||||||
|
title = data.get("title", "Неизвестный трек")
|
||||||
|
|
||||||
|
if voice_client.is_playing():
|
||||||
|
self.queue[guild_id].append({"title" : title, "stream_url" : stream_url})
|
||||||
|
await interaction.followup.send(f"➕ Трек {title} добавлен в очередь.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = discord.FFmpegOpusAudio(
|
||||||
|
stream_url,
|
||||||
|
executable="ffmpeg",
|
||||||
|
**FFMPEG_OPTIONS
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
await interaction.followup.send("❌ Ошибка воспроизведения.")
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.followup.send(f"▶️ Сейчас играет: **{title}**")
|
||||||
|
def after_playing(error):
|
||||||
|
logger.info(text=f"Включаем следующий трек...", log_type="cog")
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(self.play_next(guild_id, interaction.channel), self.bot.loop)
|
||||||
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
voice_client.play(source, after=after_playing)
|
||||||
|
|
||||||
|
@discord.app_commands.command(name="skip", description="Пропустить текущий трек")
|
||||||
|
async def skip(self, interaction: discord.Interaction):
|
||||||
|
logger.info(
|
||||||
|
text=f"Скип...",
|
||||||
|
log_type="cog"
|
||||||
|
)
|
||||||
|
voice_client = interaction.guild.voice_client
|
||||||
|
if not voice_client or not voice_client.is_playing():
|
||||||
|
await interaction.response.send_message("❌ Сейчас ничего не играет.", ephemeral=True)
|
||||||
|
return
|
||||||
|
voice_client.stop()
|
||||||
|
await interaction.response.send_message("⏭️ Трек пропущен.")
|
||||||
|
|
||||||
|
@discord.app_commands.command(name="stop", description="Остановить музыку и очистить очередь")
|
||||||
|
async def stop(self, interaction: discord.Interaction):
|
||||||
|
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("⏹️ Музыка остановлена.")
|
||||||
|
|
||||||
|
@discord.app_commands.command(name="leave", description="Отключить бота от голосового канала")
|
||||||
|
async def leave(self, interaction: discord.Interaction):
|
||||||
|
voice_client = interaction.guild.voice_client
|
||||||
|
if voice_client:
|
||||||
|
await voice_client.disconnect()
|
||||||
|
await interaction.response.send_message("👋 Бот вышел из голосового канала.")
|
||||||
|
|
||||||
|
async def start_track(self, voice_client, guild_id, channel, track):
|
||||||
|
source = discord.FFmpegOpusAudio(
|
||||||
|
track["stream_url"],
|
||||||
|
executable="ffmpeg",
|
||||||
|
**FFMPEG_OPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
await channel.send(f"▶️ Сейчас играет: **{track['title']}**")
|
||||||
|
|
||||||
|
def after_playing(error):
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.play_next(guild_id, channel),
|
||||||
|
self.bot.loop
|
||||||
|
)
|
||||||
|
|
||||||
|
voice_client.play(source, after=after_playing)
|
||||||
|
|
||||||
|
async def setup(bot: commands.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
|
||||||
|
|
||||||
|
import discord
|
||||||
from discord.ext import commands
|
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(commands.Cog):
|
class Reminders(commands.Cog):
|
||||||
"""
|
"""Cog для управления напоминаниями: add, list, remove через slash-команды."""
|
||||||
Cog для управления напоминаниями: add, list, remove.
|
|
||||||
"""
|
|
||||||
def __init__(self, bot: commands.Bot) -> None:
|
|
||||||
self.bot: commands.Bot = bot
|
|
||||||
|
|
||||||
@commands.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: commands.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: commands.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: commands.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: commands.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: commands.Bot) -> None:
|
async def setup(bot: commands.Bot) -> None:
|
||||||
await bot.add_cog(Reminders(bot))
|
await bot.add_cog(Reminders(bot))
|
||||||
47
bot/cogs/slash.py
Normal file
47
bot/cogs/slash.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from middleware.loggers import logger
|
||||||
|
|
||||||
|
|
||||||
|
class Slash(commands.Cog):
|
||||||
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
|
self.bot = bot
|
||||||
|
|
||||||
|
@discord.app_commands.command(
|
||||||
|
name="rules",
|
||||||
|
description="Показать правила сервера",
|
||||||
|
)
|
||||||
|
async def rules_slash(self, interaction: discord.Interaction) -> None:
|
||||||
|
rules_text: str = (
|
||||||
|
"**Правила сервера:**\n"
|
||||||
|
"1. Уважайте других участников.\n"
|
||||||
|
"2. Запрещена реклама и спам.\n"
|
||||||
|
"3. Не используйте запрещённые слова.\n"
|
||||||
|
"4. Соблюдайте тематику каналов.\n"
|
||||||
|
"5. Выполняйте указания модераторов.\n"
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(rules_text, ephemeral=False)
|
||||||
|
logger.info(
|
||||||
|
text=f"Slash /rules вызван пользователем {interaction.user}",
|
||||||
|
log_type="COMMAND",
|
||||||
|
user=str(interaction.user),
|
||||||
|
)
|
||||||
|
|
||||||
|
@discord.app_commands.command(
|
||||||
|
name="ping",
|
||||||
|
description="Проверить отклик бота",
|
||||||
|
)
|
||||||
|
async def ping_slash(self, interaction: discord.Interaction) -> None:
|
||||||
|
await interaction.response.send_message("Pong!", ephemeral=True)
|
||||||
|
logger.info(
|
||||||
|
text=f"Slash /ping вызван пользователем {interaction.user}",
|
||||||
|
log_type="COMMAND",
|
||||||
|
user=str(interaction.user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot: commands.Bot) -> None:
|
||||||
|
await bot.add_cog(Slash(bot))
|
||||||
@@ -22,6 +22,8 @@ class MyHelpCommand(HelpCommand):
|
|||||||
f"`{prefix}warn @пользователь [причина]` — предупреждение\n"
|
f"`{prefix}warn @пользователь [причина]` — предупреждение\n"
|
||||||
f"`{prefix}warnings @пользователь` — список предупреждений\n"
|
f"`{prefix}warnings @пользователь` — список предупреждений\n"
|
||||||
f"`{prefix}clear <кол-во>` — очистить чат\n"
|
f"`{prefix}clear <кол-во>` — очистить чат\n"
|
||||||
f"`{prefix}blacklist_show/add/remove` — чёрный список\n"
|
f"`{prefix}blacklist_show` — показать чёрный список\n"
|
||||||
|
f"`{prefix}blacklist_add` — добавить слово чёрный список\n"
|
||||||
|
f"`{prefix}blacklist_remove` — удалить слово из чёрного списока\n"
|
||||||
)
|
)
|
||||||
await channel.send(help_text)
|
await channel.send(help_text)
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ class _Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
BOT_TOKEN: Optional[str] = None
|
BOT_TOKEN: Optional[str] = None
|
||||||
PREFIX: Optional[str] = '!'
|
PREFIX: Optional[str] = '/'
|
||||||
|
|
||||||
WELCOME_CHANNEL_ID: int = 0
|
WELCOME_CHANNEL_ID: int = 0
|
||||||
ADMIN_ROLE_NAME: str = "Администратор"
|
ADMIN_ROLE_NAME: str = "Администратор"
|
||||||
|
AFK_NICKNAME:str ="AFK"
|
||||||
|
|
||||||
WARNINGS_FILE: Path = Path("warnings.json")
|
WARNINGS_FILE: Path = Path("data/warnings.json")
|
||||||
REMINDERS_FILE: Path = Path("reminders.json")
|
REMINDERS_FILE: Path = Path("data/reminders.json")
|
||||||
BLACKLIST_FILE: Path = Path("blacklist.json")
|
BLACKLIST_FILE: Path = Path("data/blacklist.json")
|
||||||
|
|
||||||
LOG_FILE_NAME: Path = Path("bot.log")
|
LOG_FILE_NAME: Path = Path("bot.log")
|
||||||
LOG_LEVEL: str = "info"
|
LOG_LEVEL: str = "info"
|
||||||
@@ -53,6 +54,13 @@ class _Settings(BaseSettings):
|
|||||||
raise ValueError(f"LOG_LEVEL должен быть одним из: {', '.join(allowed)}")
|
raise ValueError(f"LOG_LEVEL должен быть одним из: {', '.join(allowed)}")
|
||||||
return v.lower()
|
return v.lower()
|
||||||
|
|
||||||
|
@field_validator("AFK_NICKNAME")
|
||||||
|
def validate_afk_nickname(cls, v: str) -> str:
|
||||||
|
# если пустая строка или None, используем дефолт "AFK"
|
||||||
|
if not v or not v.strip():
|
||||||
|
return "AFK"
|
||||||
|
return v.strip()
|
||||||
|
|
||||||
@field_validator("WARNINGS_FILE", "REMINDERS_FILE", "BLACKLIST_FILE", "LOG_FILE_NAME", mode="before")
|
@field_validator("WARNINGS_FILE", "REMINDERS_FILE", "BLACKLIST_FILE", "LOG_FILE_NAME", mode="before")
|
||||||
def validate_paths(cls, v) -> Optional[Path]:
|
def validate_paths(cls, v) -> Optional[Path]:
|
||||||
return Path(v) if isinstance(v, str) else v
|
return Path(v) if isinstance(v, str) else v
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -1,18 +1,18 @@
|
|||||||
from asyncio import run
|
from asyncio import run
|
||||||
|
|
||||||
from bot import discbot
|
from bot import discbot
|
||||||
from configs import settings
|
from middleware.loggers import logger
|
||||||
from middleware import logger
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""
|
"""
|
||||||
Точка входа для асинхронного запуска бота.
|
Точка входа для асинхронного запуска бота.
|
||||||
"""
|
"""
|
||||||
logger.setup()
|
logger.setup() # настройка логера
|
||||||
await discbot.start(settings.BOT_TOKEN)
|
await discbot.setup() # ЗАГРУЗКА COGS + настройка discord-логов
|
||||||
await discbot.start_bot()
|
await discbot.start_bot() # запуск бота (внутри возьмёт token из settings)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run(main())
|
run(main())
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ authors = [
|
|||||||
{name = "NotFate"}
|
{name = "NotFate"}
|
||||||
]
|
]
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.11,<4.0"
|
requires-python = ">=3.11,<4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"discord (>=2.3.2,<3.0.0)",
|
|
||||||
"loguru (>=0.7.3,<0.8.0)",
|
"loguru (>=0.7.3,<0.8.0)",
|
||||||
"email-validator (>=2.3.0,<3.0.0)",
|
"email-validator (>=2.3.0,<3.0.0)",
|
||||||
"pydantic (>=2.12.5,<3.0.0)",
|
"pydantic (>=2.12.5,<3.0.0)",
|
||||||
"pydantic-settings (>=2.12.0,<3.0.0)"
|
"pydantic-settings (>=2.12.0,<3.0.0)",
|
||||||
|
"yt_dlp(>=2026.3.3)",
|
||||||
|
"pynacl(>=1.6.2)",
|
||||||
|
"discord(>=2.3.2,<3.0.0)",
|
||||||
|
"davey(>=0.1.4)"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|||||||
Reference in New Issue
Block a user