Compare commits

...

12 Commits

Author SHA1 Message Date
ca9dae8106 Merge pull request 'Fixed queue, skip, adding tracks' (#4) from murprite/bot:murprite/fix-bot into master
Some checks failed
CI / basic-checks (push) Failing after 12s
Reviewed-on: #4
2026-03-10 14:52:13 +03:00
80ac57e9e8 Fixed queue, skip, adding tracks
Some checks failed
CI / basic-checks (pull_request) Has been cancelled
2026-03-10 00:15:05 +07:00
6f29d7a5ff Все команды изменены на slash
Some checks failed
CI / basic-checks (push) Failing after 11s
2026-03-07 16:35:52 +07:00
b89b30b7c5 Добавлено прослушивание музыки
Some checks failed
CI / basic-checks (push) Failing after 10s
2026-03-07 13:11:18 +07:00
2c6629f7f9 Добавлена смена никнейма при уходе в AFK
All checks were successful
CI / basic-checks (push) Successful in 51s
2026-03-06 14:25:57 +07:00
cc821bc6e8 Переделал работу warn
All checks were successful
CI / basic-checks (push) Successful in 12s
2025-12-09 18:45:18 +07:00
1cf1e7455a Merge remote-tracking branch 'origin/master'
All checks were successful
CI / basic-checks (push) Successful in 11s
2025-12-08 21:50:18 +07:00
65176292b8 переписал help 2025-12-08 21:50:06 +07:00
304937b162 Merge pull request 'переписал help' (#2) from valera into master
All checks were successful
CI / basic-checks (push) Successful in 12s
Reviewed-on: #2
2025-12-08 14:46:13 +00:00
4a3b9a48bb переписал help
All checks were successful
CI / basic-checks (pull_request) Successful in 12s
2025-12-08 21:43:11 +07:00
ae9b716e29 Merge remote-tracking branch 'origin/master'
All checks were successful
CI / basic-checks (push) Successful in 11s
2025-12-08 20:35:40 +07:00
b5b16397a4 Исправление пути json 2025-12-08 20:35:28 +07:00
16 changed files with 442 additions and 123 deletions

View File

@@ -78,7 +78,7 @@ temp/
# Конфиденциальные файлы и настройки
# -------------------------------
.env
env/
/env
*.session
*.key
*.pem

2
.idea/Bot.iml generated
View File

@@ -5,7 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (bot1)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.12 (bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TemplatesService">

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.13 (Bot)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (bot1)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (bot)" project-jdk-type="Python SDK" />
</project>

View File

@@ -1,26 +1,47 @@
# Используем официальный образ Python с подходящей версией
FROM python:3.13-slim
# ---------- BUILDER ----------
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
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем файлы Poetry
COPY pyproject.toml poetry.lock* ./
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера)
RUN poetry config virtualenvs.create false
# Устанавливаем зависимости через Poetry
RUN poetry install --no-interaction --no-ansi --no-root
# Копируем все файлы проекта внутрь контейнера
COPY . .
# ---------- RUNTIME ----------
FROM python:3.13-slim
# Устанавливаем переменную окружения для буферизации
ENV PYTHONUNBUFFERED=1
# Команда запуска — запуск скрипта main.py
WORKDIR /app
# Устанавливаем 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"]

View File

@@ -29,6 +29,8 @@ class Bot(commands.Bot):
intents.guilds = True
intents.message_content = True
intents.members = True
intents.presences = True
intents.voice_states = True
command_prefix: str = prefix or getattr(settings, "PREFIX", "!")
@@ -73,6 +75,7 @@ class Bot(commands.Bot):
"bot.cogs.blacklist",
"bot.cogs.reminders",
"bot.cogs.slash",
"bot.cogs.music",
]
for cog in cogs:
try:

View File

@@ -2,3 +2,4 @@ from .events import *
from .blacklist import *
from .reminders import *
from .moderation import *
from .music import *

View File

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

View File

@@ -20,6 +20,69 @@ class Events(Cog):
self.check_reminders.start()
@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:
"""
Событие запуска бота.

View File

@@ -55,25 +55,11 @@ class Moderation(Cog):
"""
self.bot: Bot = bot
@command()
@is_admin()
async def rules(self, ctx: Context) -> None:
"""
Показать правила сервера.
:param ctx: Контекст команды.
"""
rules_text: str = (
"**Правила сервера:**\n"
"1. Уважайте других участников.\n"
"2. Запрещена реклама и спам.\n"
"3. Не используйте запрещённые слова.\n"
"4. Соблюдайте тематику каналов.\n"
"5. Выполняйте указания модераторов.\n"
@discord.app_commands.command(
name="kick",
description="Исключить участника с сервера",
)
await ctx.send(rules_text)
@command()
@is_admin()
async def kick(
self,
@@ -97,15 +83,12 @@ class Moderation(Cog):
except discord.HTTPException:
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
@command()
@discord.app_commands.command(
name="ban",
description="Забанить участника на сервере",
)
@is_admin()
async def ban(
self,
ctx: Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
async def ban(self,ctx: Context,member: discord.Member,*,reason: Optional[str] = None) -> None:
"""
Забанить участника на сервере.
@@ -113,6 +96,9 @@ class Moderation(Cog):
:param member: Участник для бана.
:param reason: Причина бана.
"""
user_id: str = str(member.id)
storage.user_warnings.pop(user_id,None)
storage.save_warnings()
try:
await member.ban(reason=reason)
await ctx.send(f"{member} был забанен. Причина: {reason}")
@@ -121,7 +107,10 @@ class Moderation(Cog):
except discord.HTTPException:
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
@command()
@discord.app_commands.command(
name="unban",
description="Разбанить пользователя по имени или тегу",
)
@has_permissions(ban_members=True)
async def unban(self, ctx: Context, *, member_name: str) -> None:
"""
@@ -190,7 +179,10 @@ class Moderation(Cog):
msg_lines.append(f"- {user.name}#{user.discriminator}")
await ctx.send("\n".join(msg_lines))
@command()
@discord.app_commands.command(
name="mute",
description="Выдать участнику мут",
)
@is_admin()
async def mute(
self,
@@ -224,7 +216,10 @@ class Moderation(Cog):
except discord.HTTPException:
await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
@command()
@discord.app_commands.command(
name="unmute",
description="Снять мут с участника",
)
@is_admin()
async def unmute(self, ctx: Context, member: discord.Member) -> None:
"""
@@ -251,7 +246,10 @@ class Moderation(Cog):
except discord.HTTPException:
await ctx.send("Не удалось снять мут из-за ошибки Discord.")
@command()
@discord.app_commands.command(
name="warn",
description="Выдать предупреждение участнику",
)
@is_admin()
async def warn(
self,
@@ -267,6 +265,7 @@ class Moderation(Cog):
:param member: Участник.
:param reason: Причина предупреждения.
"""
max_warning= 3
user_id: str = str(member.id)
storage.user_warnings.setdefault(user_id, []).append(
{
@@ -277,11 +276,18 @@ class Moderation(Cog):
}
)
storage.save_warnings()
warns_count = len(storage.user_warnings[user_id])
await ctx.send(
f"{member} получил предупреждение. Причина: {reason or 'Без причины'}"
f"{member} получил {warns_count} предупреждение. Причина: {reason or 'Без причины'}. До бана осталось {max_warning-warns_count} предупреждения."
)
@command()
if warns_count >= max_warning:
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,7 +306,10 @@ class Moderation(Cog):
lines.append(f"{i}. {w['reason']} ({w['date']})")
await ctx.send("\n".join(lines))
@command()
@discord.app_commands.command(
name="clear",
description="Очистить указанное количество сообщений в канале",
)
@is_admin()
async def clear(self, ctx: Context, amount: int) -> None:
"""

219
bot/cogs/music.py Normal file
View 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))

View File

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

View File

@@ -41,5 +41,7 @@ class Slash(commands.Cog):
)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Slash(bot))

View File

@@ -22,6 +22,8 @@ class MyHelpCommand(HelpCommand):
f"`{prefix}warn @пользователь [причина]` — предупреждение\n"
f"`{prefix}warnings @пользователь` — список предупреждений\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)

View File

@@ -20,10 +20,11 @@ class _Settings(BaseSettings):
WELCOME_CHANNEL_ID: int = 0
ADMIN_ROLE_NAME: str = "Администратор"
AFK_NICKNAME:str ="AFK"
WARNINGS_FILE: Path = Path("warnings.json")
REMINDERS_FILE: Path = Path("reminders.json")
BLACKLIST_FILE: Path = Path("blacklist.json")
WARNINGS_FILE: Path = Path("data/warnings.json")
REMINDERS_FILE: Path = Path("data/reminders.json")
BLACKLIST_FILE: Path = Path("data/blacklist.json")
LOG_FILE_NAME: Path = Path("bot.log")
LOG_LEVEL: str = "info"
@@ -53,6 +54,13 @@ class _Settings(BaseSettings):
raise ValueError(f"LOG_LEVEL должен быть одним из: {', '.join(allowed)}")
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")
def validate_paths(cls, v) -> Optional[Path]:
return Path(v) if isinstance(v, str) else v

View File

@@ -15,3 +15,4 @@ async def main() -> None:
if __name__ == "__main__":
run(main())

View File

@@ -8,15 +8,16 @@ authors = [
license = {text = "MIT"}
requires-python = ">=3.11,<4.0"
dependencies = [
"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)"
"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]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"