From 803cdefe99a8bb405449b7d38c3ec75d6656c228 Mon Sep 17 00:00:00 2001 From: HypoxiE Date: Fri, 25 Jul 2025 23:33:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8F=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=81=D1=81=D1=8B=D0=BB=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 4 + src/CoreMod.py | 98 +++++------- src/cogs/moderators.py | 55 +++++++ src/database/db_classes.py | 8 +- src/managers/old_DataBaseManager.py | 239 ---------------------------- src/test.py | 4 +- 6 files changed, 112 insertions(+), 296 deletions(-) delete mode 100644 src/managers/old_DataBaseManager.py diff --git a/requirements.txt b/requirements.txt index 2106fbf..74a9f0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ aiohappyeyeballs==2.6.1 aiohttp==3.12.12 aiosignal==1.3.2 +aiosqlite==0.21.0 async-timeout==5.0.1 asyncpg==0.30.0 attrs==25.3.0 certifi==2025.4.26 charset-normalizer==3.4.2 disnake==2.10.1 +filelock==3.18.0 frozenlist==1.7.0 greenlet==3.2.3 idna==3.10 @@ -14,7 +16,9 @@ multidict==6.4.4 numpy==2.2.6 propcache==0.3.2 requests==2.32.4 +requests-file==2.1.0 SQLAlchemy==2.0.41 +tldextract==5.3.0 typing_extensions==4.14.0 urllib3==2.4.0 yarl==1.20.1 diff --git a/src/CoreMod.py b/src/CoreMod.py index f56245b..0f9eb8c 100644 --- a/src/CoreMod.py +++ b/src/CoreMod.py @@ -17,7 +17,6 @@ from constants.global_constants import * from data.secrets.TOKENS import TOKENS from database.db_classes import all_data as DataBaseClasses from managers.DataBaseManager import DatabaseManager -from managers.old_DataBaseManager import DatabaseManager as old_DatabaseManager from database.settings import config from sqlalchemy.orm import declarative_base, relationship @@ -25,6 +24,8 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sess from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import CreateTable +import tldextract + class AnyBots(commands.Bot): ''' @@ -291,22 +292,25 @@ class MainBot(AnyBots): ''' - def __init__(self, DataBase, stop_event): + def __init__(self, DataBase, stop_event, task_start = True): super().__init__(DataBase) self.stop_event = stop_event + self.task_start = task_start async def on_ready(self): await super().on_ready() - self.CheckDataBases.cancel() - self.MakeBackups.cancel() + if self.task_start: + self.CheckDataBases.cancel() + self.MakeBackups.cancel() - self.MakeBackups.start() - self.CheckDataBases.start() + self.MakeBackups.start() + self.CheckDataBases.start() async def BotOff(self): - self.CheckDataBases.cancel() - self.MakeBackups.cancel() + if self.task_start: + self.CheckDataBases.cancel() + self.MakeBackups.cancel() self.stop_event.set() @@ -515,9 +519,14 @@ class MainBot(AnyBots): #/преды async def on_message(self, msg): + + if msg.author.bot: + return 0 + if msg.author.id == 479210801891115009 and msg.content == "botsoff": await msg.reply(embed=disnake.Embed(description=f'Бот отключён', colour=0xff9900)) await self.BotOff() + return 0 if type(msg.channel).__name__!="DMChannel" and fnmatch(msg.channel.name, "⚠жалоба-от-*-на-*"): log_reports = disnake.utils.get(msg.guild.channels, id=1242373230384386068) files=[] @@ -527,12 +536,38 @@ class MainBot(AnyBots): f"Автор: `{msg.author.name} ({msg.author.id})`\n" + (f"Сообщение: ```{msg.content}```\n" if msg.content else ""), files = files) + return 0 + + def extract_root_domain(url): + ext = tldextract.extract(url) + if not ext.domain or not ext.suffix: + return None + return f"{ext.domain}.{ext.suffix}".lower() + + log = disnake.utils.get(msg.guild.channels, id=893065482263994378) + + url_pattern = re.compile(r'https?://[^\s]+') + links = re.findall(url_pattern, msg.content) + аllowed_domains_model = self.DataBaseManager.model_classes['аllowed_domains'] + async with self.DataBaseManager.session() as session: + for link in links: + root_domain = extract_root_domain(link) + stmt = self.DataBaseManager.select(аllowed_domains_model).where(аllowed_domains_model.domain == root_domain) + link_in_wl = (await session.execute(stmt)).scalars().first() + + if link_in_wl is None: + print("Нарушение!!!") + await log.send(f"{msg.author.mention}({msg.author.id}) отправил в чат {msg.channel.mention} сомнительную ссылку, которой нет в вайлисте:```{msg.content}```") + mess = await msg.reply(embed=disnake.Embed(description=f'Этой ссылки нет в белом списке. Чтобы её туда добавили, свяжитесь с разработчиком или модераторами.', colour=0xff9900)) + await msg.delete() + await asyncio.sleep(20) + await mess.delete() + return 1 message_words = msg.content.replace("/", " ").split(" ") if "discord.gg" in message_words: for i in range(len(message_words)): if message_words[i]=="discord.gg" and not msg.author.bot: - log = disnake.utils.get(msg.guild.channels, id=893065482263994378) try: inv = await self.fetch_invite(url = "https://discord.gg/"+message_words[i+1]) if inv.guild.id != 490445877903622144: @@ -562,51 +597,6 @@ async def init_db(): return DatabaseManager(DataBaseEngine, DataBaseClasses) -async def db_migration(DB_MANAGER): - new_DataBase = DB_MANAGER - DataBase = await old_DatabaseManager.connect("data/penalties.db") - await DataBase.execute("PRAGMA journal_mode=WAL") - await DataBase.execute("PRAGMA synchronous=NORMAL") - await DataBase.execute("PRAGMA foreign_keys = ON") - try: - async with new_DataBase.engine.begin() as conn: - await conn.run_sync(new_DataBase.metadata.drop_all) - await conn.run_sync(new_DataBase.metadata.create_all) - async with new_DataBase.session() as session: - for penaltid, userid, reason, timend, timewarn in await DataBase.SelectBD('punishment_text_mutes'): - penault = DB_MANAGER.model_classes['punishment_mutes_text'](user_id = userid, reason = reason, time_end = timend if timend else None, time_warn = timewarn if timewarn else None) - async with session.begin(): - session.add(penault) - - for penaltid, userid, reason, timend, timewarn in await DataBase.SelectBD('punishment_voice_mutes'): - penault = DB_MANAGER.model_classes['punishment_mutes_voice'](user_id = userid, reason = reason, time_end = timend if timend else None, time_warn = timewarn if timewarn else None) - async with session.begin(): - session.add(penault) - - for penaltid, userid, reason, timend in await DataBase.SelectBD('punishment_bans'): - penault = DB_MANAGER.model_classes['punishment_bans'](user_id = userid, reason = reason, time_end = timend if timend else None) - async with session.begin(): - session.add(penault) - - for penaltid, userid, reason in await DataBase.SelectBD('punishment_perms'): - penault = DB_MANAGER.model_classes['punishment_perms'](user_id = userid, reason = reason) - async with session.begin(): - session.add(penault) - - for penaltid, userid, reason, timend in await DataBase.SelectBD('punishment_warns'): - penault = DB_MANAGER.model_classes['punishment_warns'](user_id = userid, reason = reason, time_warn = timend) - async with session.begin(): - session.add(penault) - - for penaltid, userid, reason, timend in await DataBase.SelectBD('punishment_reprimands'): - penault = DB_MANAGER.model_classes['punishment_reprimands'](user_id = userid, reason = reason, time_warn = timend) - async with session.begin(): - session.add(penault) - finally: - await DataBase.close() - - raise Exception("Миграция БД завершена. требуется переименовывание файлов") - async def run_bot(bot, token, stop_event): try: await bot.start(token) diff --git a/src/cogs/moderators.py b/src/cogs/moderators.py index 02f60ae..3570570 100644 --- a/src/cogs/moderators.py +++ b/src/cogs/moderators.py @@ -10,6 +10,7 @@ import math import random import json import shutil +import tldextract translate = {"textmute":"Текстовый мут", "voicemute":"Голосовой мут", "ban":"Бан", "warning":"Предупреждение",\ "time":"Время", "reason":"Причина", "changenick":"Сменить ник", "reprimand":"Выговор", "newnick":"Новый ник"} @@ -683,4 +684,58 @@ class ModerModule(commands.Cog): return 0 else: await ctx.send(embed = disnake.Embed(description = f'Данный пользователь не имеет роли в ветке {branchid}', colour = 0xff9900)) + return 1 + + @commands.slash_command(description="Позволяет добавить домен в белый список", name="добавить_ссылку", administrator=True) + async def add_domain(self, ctx: disnake.AppCmdInter, link: str = commands.Param(description="Укажите ссылку или домен", name="ссылка")): + async with self.DataBaseManager.session() as session: + async with session.begin(): + staff_branches_model = self.DataBaseManager.model_classes['staff_branches'] + аllowed_domains_model = self.DataBaseManager.model_classes['аllowed_domains'] + + admin_flag = False + + stmt = ( + self.DataBaseManager.select(staff_branches_model) + .options( + self.DataBaseManager.selectinload(staff_branches_model.users) + ) + .where( + self.DataBaseManager.or_( + staff_branches_model.is_admin == True, + staff_branches_model.is_moder == True + ) + ) + ) + + branches = (await session.execute(stmt)).scalars().all() + + for branch in branches: + for user in branch.users: + if ctx.author.id == user.user_id: + admin_flag = True + break + if admin_flag: + break + + if not admin_flag: + await ctx.send(embed = disnake.Embed(description = f'У вас недостаточно полномочий, чтобы добавлять ссылку в белый лист. Обратитесь к любому модератору или разработчику.', colour = 0xff9900)) + return 1 + + else: + + def extract_root_domain(url): + ext = tldextract.extract(url) + if not ext.domain or not ext.suffix: + return None + return f"{ext.domain}.{ext.suffix}".lower() + new_link = link if not "http" in link else extract_root_domain(link) + if not new_link: + await ctx.send(embed = disnake.Embed(description = f'Некорректная ссылка!', colour = 0xff9900)) + return 1 + + domain = аllowed_domains_model(domain = new_link, initiator_id = ctx.author.id) + session.add(domain) + + await ctx.send(embed = disnake.Embed(description = f'Домен {new_link} успешно добавлен в белый список', colour = 0xff9900)) return 1 \ No newline at end of file diff --git a/src/database/db_classes.py b/src/database/db_classes.py index e774973..119f20a 100644 --- a/src/database/db_classes.py +++ b/src/database/db_classes.py @@ -200,7 +200,13 @@ class StaffCuration(Base): UniqueConstraint('apprentice_id', 'curator_id', 'branch_id', name='uq_apprentice_curator_branch'), ) +class AllowedDomain(Base): + __tablename__ = "аllowed_domains" + id: Mapped[identificator_pk] + domain: Mapped[str] = mapped_column(Text, index=True, nullable=False, unique=True) + initiator_id: Mapped[discord_identificator] all_data = { 'base': Base -} \ No newline at end of file +} + diff --git a/src/managers/old_DataBaseManager.py b/src/managers/old_DataBaseManager.py deleted file mode 100644 index 7012dda..0000000 --- a/src/managers/old_DataBaseManager.py +++ /dev/null @@ -1,239 +0,0 @@ -try: - import aiosqlite - import disnake - from disnake.ext import commands - from disnake.ext import tasks -except: - import pip - - pip.main(['install', 'disnake']) - pip.main(['install', 'aiosqlite']) - import disnake - from disnake.ext import commands - from disnake.ext import tasks - import aiosqlite -from typing import Optional, Union, List, Dict, Any, AsyncIterator, Tuple -from asyncio import Lock -import datetime - -class DatabaseManager: - """Расширенное соединение с БД с поддержкой транзакций и удобных методов""" - - def __init__(self, connection: aiosqlite.Connection): - self._connection = connection - self._transaction_depth = 0 - self._closed = False - self._transaction_lock = Lock() - self.last_error = None - - @classmethod - async def connect(cls, database: str, **kwargs) -> 'DatabaseManager': - """Альтернатива конструктору для подключения""" - connection = await aiosqlite.connect(database, **kwargs) - return cls(connection) - - async def UpdateBD(self, table: str, *, change: dict, where: dict, whereandor = "AND"): - request = () - - change_request = [] - for i in change.keys(): - change_request.append(f"{i} = ?") - request = request + (change[i],) - - where_request = [] - for i in where.keys(): - where_request.append(f"{i} = ?") - request = request + (where[i],) - - await self.execute('UPDATE {table} SET {change} WHERE {where}' - .format(table = table, change = ", ".join(change_request), where = f" {whereandor} ".join(where_request)), request) - return 0 - - async def SelectBD(self, table: str, *, select: list = ["*"], where: dict = None, where_ops: dict = None, whereandor = "AND", order_by: str = None, limit: int = None): - - where_combined = {} - if where: - where_combined.update({f"{k} =": v for k, v in where.items()}) - if where_ops: - where_combined.update(where_ops) - - request = () - where_clauses = "" - - for condition, value in where_combined.items(): - field_op = condition.split() - field = field_op[0] - op = "=" if len(field_op) == 1 else field_op[1] - if where_clauses == "": - where_clauses= where_clauses + f"{field} {op} ? " - else: - where_clauses= where_clauses + f"{whereandor} {field} {op} ? " - request += (value,) - - query = "SELECT {select} FROM {table}".format( - select=", ".join(select), - table=table - ) - - if where_clauses: - query += f" WHERE {where_clauses}" - - if order_by: - query += f" ORDER BY {order_by}" - - if limit: - query += f" LIMIT {limit}" - - async with await self.execute(query, request) as cursor: - return [i for i in await cursor.fetchall()] - - async def GetStaffJoins(): - query = \ - """ - SELECT sur.userid, sur.roleid, sur.description, sur.starttime, sr.staffsalary, sbr.layer as rolelayer, sbr.branchid, sb.layer as branchlayer, sb.purpose - FROM staff_users_roles AS sur - JOIN staff_roles as sr ON sr.roleid = sur.roleid - JOIN staff_branches_roles as sbr ON sbr.roleid = sur.roleid - JOIN staff_branches as sb ON sb.branchid = sbr.branchid - ORDER BY branchlayer ASC, rolelayer ASC; - """ - async with await self.execute(query, request) as cursor: - answer = [i for i in await cursor.fetchall()] - result = [{'userid': userid, 'roleid': roleid, 'description': description, 'starttime': starttime, 'staffsalary': staffsalary, 'rolelayer': rolelayer, 'branchid': branchid, 'branchlayer': branchlayer, 'purpose': purpose} for userid, roleid, description, starttime, staffsalary, rolelayer, branchid, branchlayer, purpose in answer] - return result - - async def DeleteBD(self, table: str, *, where: dict, whereandor = "AND"): - request = () - where_request = [] - for i in where.keys(): - where_request.append(f"{i} = ?") - request = request + (where[i],) - await self.execute("DELETE FROM {table} where {where}" - .format(table = table, where = f" {whereandor} ".join(where_request)), request) - return 0 - - async def InsertBD(self, table: str, *, data: dict): - request = () - keys = list(data.keys()) - qstring = [] - for i in keys: - request = request + (data[i],) - qstring.append("?") - await self.execute("INSERT INTO {table}({keys}) VALUES({values})" - .format(table = table, keys = ", ".join(keys), values = ", ".join(qstring)), request) - return 0 - - async def execute(self, sql: str, parameters: Optional[Union[Tuple, Dict]] = None, **kwargs) -> aiosqlite.Cursor: - """ - Универсальный execute, который автоматически определяет: - - Нужно ли начинать транзакцию (для INSERT/UPDATE/DELETE вне транзакции) - - Работает ли уже внутри транзакции (не создаёт вложенные транзакции) - """ - is_modifying = sql.strip().upper().startswith(("INSERT", "UPDATE", "DELETE")) - - # Если это модифицирующий запрос И мы НЕ внутри транзакции - if is_modifying and self._transaction_depth == 0: - async with self: # Автоматические begin/commit - cursor = await self._connection.execute(sql, parameters or (), **kwargs) - await cursor.close() # Важно: закрываем курсор для COMMIT - return cursor - else: - # Для SELECT или работы внутри существующей транзакции - return await self._connection.execute(sql, parameters or (), **kwargs) - - async def fetch_all(self, sql: str, parameters: Optional[Union[Tuple, Dict]] = None) -> List[Tuple]: - """Выполняет запрос и возвращает все строки""" - async with await self.execute(sql, parameters) as cursor: - return await cursor.fetchall() - - async def fetch_one(self, sql: str, parameters: Optional[Union[Tuple, Dict]] = None) -> Optional[Tuple]: - """Выполняет запрос и возвращает первую строку""" - async with await self.execute(sql, parameters) as cursor: - return await cursor.fetchone() - - async def fetch_val(self, sql: str, parameters: Optional[Union[Tuple, Dict]] = None, column: int = 0) -> Any: - """Возвращает значение из первого столбца""" - row = await self.fetch_one(sql, parameters) - return row[column] if row else None - - async def insert(self, table: str, data: Dict[str, Any], on_conflict: str = None) -> int: - """Упрощенный INSERT с поддержкой ON CONFLICT""" - keys = data.keys() - values = list(data.values()) - - sql = f""" - INSERT INTO {table} ({', '.join(keys)}) - VALUES ({', '.join(['?']*len(keys))}) - """ - - if on_conflict: - sql += f" ON CONFLICT {on_conflict}" - - await self.execute(sql, values) - return self.lastrowid - - async def update(self, table: str, where: Dict[str, Any], changes: Dict[str, Any], where_operator: str = "AND") -> int: - """Упрощенный UPDATE с автоматическим построением WHERE""" - set_clause = ", ".join([f"{k} = ?" for k in changes.keys()]) - where_clause = f" {where_operator} ".join([f"{k} = ?" for k in where.keys()]) - - sql = f""" - UPDATE {table} - SET {set_clause} - WHERE {where_clause} - """ - - result = await self.execute(sql, [*changes.values(), *where.values()]) - return result.rowcount - - async def begin(self): - """Начать транзакцию (с поддержкой вложенности)""" - async with self._transaction_lock: - if self._transaction_depth == 0: - await self._connection.execute("BEGIN IMMEDIATE") - self._transaction_depth += 1 - - async def commit(self): - """Зафиксировать транзакцию""" - async with self._transaction_lock: - if self._transaction_depth == 1: - await self._connection.commit() - self._transaction_depth = max(0, self._transaction_depth - 1) - - async def rollback(self): - """Откатить транзакцию""" - async with self._transaction_lock: - if self._transaction_depth > 0: - await self._connection.rollback() - self._transaction_depth = 0 - - async def close(self) -> None: - """Безопасное закрытие соединения с учётом транзакций""" - async with self._transaction_lock: - try: - # Откатываем активную транзакцию, если есть - if self._transaction_depth > 0: - await self._connection.rollback() - self._transaction_depth = 0 - - # Закрываем соединение - if hasattr(self._connection, '_connection'): # Проверка внутреннего состояния - await self._connection.close() - except Exception as e: - self.last_error = f"{datetime.datetime.now().strftime('%H:%M:%S %d-%m-%Y')}:: Ошибка при закрытии соединения: {e}" - print(f"{datetime.datetime.now().strftime('%H:%M:%S %d-%m-%Y')}:: Ошибка при закрытии соединения: {e}") - finally: - # Помечаем соединение как закрытое - self._closed = True - - async def __aenter__(self): - await self.begin() # Используем собственный метод begin - return self # Возвращаем сам менеджер, а не соединение - - async def __aexit__(self, exc_type, exc_val, exc_tb): - if exc_type is None: - await self.commit() - else: - self.last_error = f"{datetime.datetime.now().strftime('%H:%M:%S %d-%m-%Y')}:: Во время записи в бд произошла ошибка: {exc_type}({exc_val}): {exc_tb.tb_frame.f_code.co_filename}(строка {exc_tb.tb_lineno})!" - print(f"{datetime.datetime.now().strftime('%H:%M:%S %d-%m-%Y')}:: Во время записи в бд произошла ошибка: {exc_type}({exc_val}): {exc_tb.tb_frame.f_code.co_filename}(строка {exc_tb.tb_lineno})!") - await self.rollback() \ No newline at end of file diff --git a/src/test.py b/src/test.py index 3bbb2b2..2b929d4 100644 --- a/src/test.py +++ b/src/test.py @@ -46,8 +46,8 @@ async def main(): try: DataBase = await CoreMod.init_db() - #sup_bot = CoreMod.MainBot(DataBase, stop_event) - sup_bot = CoreMod.AnyBots(DataBase) + sup_bot = CoreMod.MainBot(DataBase, stop_event, task_start = False) + #sup_bot = CoreMod.AnyBots(DataBase) all_bots = [sup_bot] #НЕ СМЕЙ РАСКОММЕНТИРОВАТЬ