Сод для модерации
Some checks failed
CI / basic-checks (push) Failing after 1m36s

This commit is contained in:
2025-12-08 16:48:17 +07:00
parent 21a93d3844
commit e9c2c456e0

329
bot/cogs/moderation.py Normal file
View File

@@ -0,0 +1,329 @@
from __future__ import annotations
from typing import Callable, Awaitable, Optional
import datetime
import discord
from discord.ext import commands
from discord.utils import get
from configs import settings
from ..storage import storage
# Тип предиката для check (для читаемости, но не обязателен)
CheckPredicate = Callable[[commands.Context], Awaitable[bool]]
def is_admin():
"""
Создаёт декоратор проверки прав администратора:
либо есть роль с именем settings.ADMIN_ROLE_NAME,
либо у пользователя есть флаг администратора сервера.
"""
async def predicate(ctx: commands.Context) -> bool:
author = ctx.author
if not isinstance(author, discord.Member):
return False
admin_role = get(author.roles, name=settings.ADMIN_ROLE_NAME)
return bool(admin_role) or author.guild_permissions.administrator
return commands.check(predicate)
def require_guild(ctx: commands.Context) -> Optional[discord.Guild]:
"""
Безопасно получить guild из контекста.
Если команда вызвана не на сервере (в ЛС), возвращает None.
"""
return ctx.guild
class Moderation(commands.Cog):
"""
Cog с модерационными командами:
rules, kick, ban, unban, mute, unmute, warn, warnings, clear.
"""
def __init__(self, bot: commands.Bot) -> None:
"""
:param bot: Экземпляр бота, к которому привязан cog.
"""
self.bot: commands.Bot = bot
@commands.command()
@is_admin()
async def rules(self, ctx: commands.Context) -> None:
"""
Показать правила сервера.
:param ctx: Контекст команды.
"""
rules_text: str = (
"**Правила сервера:**\n"
"1. Уважайте других участников.\n"
"2. Запрещена реклама и спам.\n"
"3. Не используйте запрещённые слова.\n"
"4. Соблюдайте тематику каналов.\n"
"5. Выполняйте указания модераторов.\n"
)
await ctx.send(rules_text)
@commands.command()
@is_admin()
async def kick(
self,
ctx: commands.Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
"""
Исключить участника с сервера.
:param ctx: Контекст команды.
:param member: Участник для исключения.
:param reason: Причина исключения.
"""
try:
await member.kick(reason=reason)
await ctx.send(f"{member} был исключён. Причина: {reason}")
except discord.Forbidden:
await ctx.send("Недостаточно прав для исключения этого участника.")
except discord.HTTPException:
await ctx.send(f"Не удалось исключить {member} из-за ошибки Discord.")
@commands.command()
@is_admin()
async def ban(
self,
ctx: commands.Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
"""
Забанить участника на сервере.
:param ctx: Контекст команды.
:param member: Участник для бана.
:param reason: Причина бана.
"""
try:
await member.ban(reason=reason)
await ctx.send(f"{member} был забанен. Причина: {reason}")
except discord.Forbidden:
await ctx.send("Недостаточно прав для бана этого участника.")
except discord.HTTPException:
await ctx.send(f"Не удалось забанить {member} из-за ошибки Discord.")
@commands.command()
@commands.has_permissions(ban_members=True)
async def unban(self, ctx: commands.Context, *, member_name: str) -> None:
"""
Разбанить пользователя по имени или тегу.
:param ctx: Контекст команды.
:param member_name: Имя или имя#дискриминатор.
"""
guild = require_guild(ctx)
if guild is None:
await ctx.send("Команду можно использовать только на сервере.")
return
banned_users: list[discord.guild.BanEntry] = [
ban_entry async for ban_entry in guild.bans()
]
# Вариант с полным тегом
if "#" in member_name:
try:
name, discriminator = member_name.split("#", maxsplit=1)
except ValueError:
await ctx.send("Неверный формат пользователя. Используйте Имя#Тег.")
return
for ban_entry in banned_users:
user = ban_entry.user
if (user.name, user.discriminator) == (name, discriminator):
try:
await guild.unban(user)
await ctx.send(f"Пользователь {user} разбанен.")
except discord.HTTPException:
await ctx.send("Ошибка при разбане пользователя.")
return
await ctx.send(f"Пользователь {member_name} не найден в бан-листе.")
return
# Вариант только с именем
matching = [
ban_entry.user
for ban_entry in banned_users
if ban_entry.user.name.lower() == member_name.lower()
]
if not matching:
await ctx.send(
f"Пользователь с именем `{member_name}` не найден в бан-листе."
)
return
if len(matching) == 1:
user = matching[0]
try:
await guild.unban(user)
await ctx.send(f"Пользователь {user} разбанен.")
except discord.HTTPException:
await ctx.send("Ошибка при разбане пользователя.")
return
msg_lines: list[str] = [
"Найдено несколько пользователей с таким именем. "
"Укажите полный тег для разбанивания:",
]
for user in matching:
msg_lines.append(f"- {user.name}#{user.discriminator}")
await ctx.send("\n".join(msg_lines))
@commands.command()
@is_admin()
async def mute(
self,
ctx: commands.Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
"""
Выдать участнику мут (роль Muted).
:param ctx: Контекст команды.
:param member: Участник, которому выдаётся мут.
:param reason: Причина мута.
"""
guild = require_guild(ctx)
if guild is None:
await ctx.send("Команду можно использовать только на сервере.")
return
muted_role: Optional[discord.Role] = get(guild.roles, name="Muted")
if muted_role is None:
await ctx.send("Роль Muted не найдена.")
return
try:
await member.add_roles(muted_role, reason=reason)
await ctx.send(f"{member} заглушен. Причина: {reason}")
except discord.Forbidden:
await ctx.send("Недостаточно прав для выдачи мута.")
except discord.HTTPException:
await ctx.send("Не удалось выдать мут из-за ошибки Discord.")
@commands.command()
@is_admin()
async def unmute(self, ctx: commands.Context, member: discord.Member) -> None:
"""
Снять мут с участника.
:param ctx: Контекст команды.
:param member: Участник, с которого снимается мут.
"""
guild = require_guild(ctx)
if guild is None:
await ctx.send("Команду можно использовать только на сервере.")
return
muted_role: Optional[discord.Role] = get(guild.roles, name="Muted")
if muted_role is None:
await ctx.send("Роль Muted не найдена.")
return
try:
await member.remove_roles(muted_role)
await ctx.send(f"Мут снят с {member}.")
except discord.Forbidden:
await ctx.send("Недостаточно прав для снятия мута.")
except discord.HTTPException:
await ctx.send("Не удалось снять мут из-за ошибки Discord.")
@commands.command()
@is_admin()
async def warn(
self,
ctx: commands.Context,
member: discord.Member,
*,
reason: Optional[str] = None,
) -> None:
"""
Выдать предупреждение участнику.
:param ctx: Контекст команды.
:param member: Участник.
:param reason: Причина предупреждения.
"""
user_id: str = str(member.id)
storage.user_warnings.setdefault(user_id, []).append(
{
"reason": reason or "Без причины",
"date": datetime.datetime.now().isoformat(
sep=" ", timespec="seconds"
),
}
)
storage.save_warnings()
await ctx.send(
f"{member} получил предупреждение. Причина: {reason or 'Без причины'}"
)
@commands.command()
async def warnings(self, ctx: commands.Context, member: discord.Member) -> None:
"""
Показать предупреждения участника.
:param ctx: Контекст команды.
:param member: Участник.
"""
user_id: str = str(member.id)
warns = storage.user_warnings.get(user_id, [])
if not warns:
await ctx.send(f"У пользователя {member} нет предупреждений.")
return
lines: list[str] = [f"Предупреждения пользователя {member}:"]
for i, w in enumerate(warns, 1):
lines.append(f"{i}. {w['reason']} ({w['date']})")
await ctx.send("\n".join(lines))
@commands.command()
@is_admin()
async def clear(self, ctx: commands.Context, amount: int) -> None:
"""
Очистить указанное количество сообщений в канале.
:param ctx: Контекст команды.
:param amount: Количество сообщений для удаления.
"""
if amount <= 0:
await ctx.send("Количество должно быть положительным числом.")
return
deleted = await ctx.channel.purge(limit=amount + 1)
await ctx.send(
f"Удалено сообщений: {len(deleted) - 1}",
delete_after=5,
)
async def setup(bot: commands.Bot) -> None:
"""
Зарегистрировать cog в боте.
:param bot: Экземпляр бота.
"""
await bot.add_cog(Moderation(bot))