diff --git a/main.py b/main.py index 1fa5f16..09f8b56 100644 --- a/main.py +++ b/main.py @@ -17,8 +17,19 @@ dp = Dispatcher(bot) @dp.message_handler(commands=["start", "help"]) @services.auth -async def send_welcome(message: types.Message): - """Справка по командам.""" +async def send_welcome(message: types.Message) -> None: + r"""Справка по командам. + + Команды бота: `/start`, `/help'. + Возвращает справку по командам. Текст берет из файла + `static/help_msg.txt`. Доступна только пользователям из белого + списка. + + Parameters + ---------- + message: types.Message + Сообщение пользователя. Библиотека aiogram. + """ await message.reply( services.get_static("static/help_msg.txt"), reply=False, @@ -122,7 +133,6 @@ async def add_me(message: types.Message) -> None: Когда бот получает токен, он проверяет его на валидность (есть ли такой вообще, не истек ли) и высылает разовую ссылку на вступление в группу. - """ user_token = services.get_text_from_command(message) @@ -154,9 +164,11 @@ async def add_me(message: types.Message) -> None: async def stop_and_panic(message: types.Message) -> None: """Panic-button для бота. + Команда `/stop` + Доступна белому списку. При нажатии: - - кидает юзера в чс чата и удаляет его, - - удаляет из белого списка. + - кидает юзера в чс чата и удаляет его из чата + - удаляет из белого списка. """ u_id = message["from"]["id"] services.delete_user_from_allowed_list(u_id) @@ -168,7 +180,12 @@ async def stop_and_panic(message: types.Message) -> None: @dp.message_handler(commands=["wlc"]) @services.only_admins async def wlc(message: types.Message) -> None: - """Тестирование приветственного сообщения.""" + """Тестирование приветственного сообщения. + + Команда `/wlc` + Доступна только админам. + Команда для тестирования привественного сообщения. + """ await message.reply( services.get_static("static/welcome_msg.txt"), reply=False, @@ -180,7 +197,12 @@ async def wlc(message: types.Message) -> None: @dp.message_handler(commands=["info", "version"]) @services.only_admins async def get_info(message: types.Message) -> None: - """Вовзращает версию бота.""" + """Выводит информацию о боте и окружении. + + Команды: `/info`, `/version` + Доступна только админам. + Бот присылает свою версию, время жизни постов и вопросов. + """ msg = f"Версия бота: *{settings.VERSION}*\n" msg += f"Время жизни постов: *{settings.DELAY_TIME_POST} минут*\n" msg += f"Время жизни вопросов: *{settings.DELAY_TIME_Q} минут*\n" @@ -201,9 +223,16 @@ async def test_admins(message: types.Message): ) -# https://stackoverflow.com/questions/67637631/create-a-background-process-using-aiogram async def messages_cleanup() -> None: - """Зачистка пропущенных сообщений. При запуске бота проверяет, есть ли не удаленные сообщения требующие удаления, удаляет их.""" + """Удаление "забытых" сообщений. + + При запуске вызывает функцию удаления старых сообщений. Постоянно + висит в фоне и вызывает функцию удаления каждые 600 мс. + + See Also + -------- + https://stackoverflow.com/questions/67637631/create-a-background-process-using-aiogram + """ while True: await services.delete_old_messages(bot) await asyncio.sleep(600) diff --git a/services.py b/services.py index 67f3570..7a6f413 100644 --- a/services.py +++ b/services.py @@ -16,17 +16,18 @@ from aiogram import Bot, types from functools import wraps +# не очень красивый способ создание общего объекта ORM для всех +# функций модуля engine = create_engine("sqlite:///" + str(settings.DB_PATH)) Session = sessionmaker(bind=engine) session = Session() def auth(func): - """Аутентификация. + """Декоратор для аутентификации. Проверяем, что у юзера есть полномочия на выполнение действия - он должен быть в белом списке. - """ async def wrapper(message): @@ -47,7 +48,10 @@ def auth(func): def only_admins(func): - """Действия только для админов.""" + """Декоратора аутентификации для администраторов. + + Данные действия с ботом могут выполнять только администраторы. + """ async def wrapper(message): uid = message["from"]["id"] @@ -61,7 +65,21 @@ def only_admins(func): async def _clean_up(message_id: int, delay_min: int, bot: Bot) -> None: - """Удаление сообщенния message_id через delay_min минут.""" + """Поставить таймер на удаление сообщения. + + Внутренняя функция для удаления сообщения. Пишет в базу id + сообщения и время, когда его нужно удалить. Ждет `delay_min` и + пробует удалить сообщение. + + Parameters + ---------- + message_id : int + Идентификатор сообщения, присвоенный ему Телеграмом. + delay_min : int + Количество минут, через которое это сообщение нужно удалить. + bot : Bot + Объект бота, через который сообщение будет отправленно в канал. + """ exp = datetime.now() + timedelta(minutes=delay_min) msg: Messages_to_delete = Messages_to_delete( message_id=message_id, deletion_date=exp @@ -80,10 +98,16 @@ async def _clean_up(message_id: int, delay_min: int, bot: Bot) -> None: def get_new_token() -> str: - """Возвращает токен для приглашения пользователя. + """Получить токен. - Длина токена 16 символов, срок действия 5 минут. Токен записывается в базу. + Возвращает токен для приглашения нового пользователя. + Returns + ------- + token : str + Токен для доступа к группе. Длина - 16 символов. Длина токена + 16 символов, срок действия 5 минут. Токен и его срок годности + записываются в базу. """ token = token_urlsafe(16) exp = datetime.now() + timedelta(minutes=5) @@ -95,7 +119,22 @@ def get_new_token() -> str: def check_token(token: str) -> bool: - """Проверяет, находится ли токен в базе, возвращает True, если токен есть и не истек. затем удаляет токен из базы.""" + """Проверить валидность токена. + + Токен считается валидным если он соотвествует сразу двум условиям: + он есть в базе, срок его действия не истек. После проверки токен + будет удален из базы. + + Parameters + ---------- + token : str + Токен. Длина не проверяется. + + Returns + ------- + result : bool + Результат проверки токена. + """ finded_token = session.query(Token).filter(Token.token == token).one_or_none() if finded_token and not finded_token.expired(): @@ -107,7 +146,16 @@ def check_token(token: str) -> bool: def add_new_user_to_allowed_list(user_id: int) -> None: - """Добавляет пользователя в белый список. Если пользователь в нем уже есть, то игнорирует ошибку и ведет себя так, словно его добавили впервые.""" + """Добавить пользователя в белый список. + + Если пользователь в нем уже есть, то игнорирует ошибку и ведет + себя так, словно его добавили впервые. + + Parameters + ---------- + user_id : int + Идентификатор пользователя. + """ new_user: Allowed_user = Allowed_user(user_id=user_id, date_add=datetime.now()) try: @@ -119,7 +167,13 @@ def add_new_user_to_allowed_list(user_id: int) -> None: def delete_user_from_allowed_list(user_id: str) -> None: - """Удаляет пользователя из белого списка.""" + """Удалить пользователя из белого списка. + + Parameters + ---------- + user_id : int + Идентификатор пользователя. + """ user: Allowed_user = ( session.query(Allowed_user) .filter(Allowed_user.user_id == user_id) @@ -131,14 +185,33 @@ def delete_user_from_allowed_list(user_id: str) -> None: def get_static(name: str) -> str: - """Возвращает содержимое из файла переданного в name.""" + """Считать содержимое файла. + + Возвращает содержимое файла. Поддерживает Markdown. + + Parameters + ---------- + name : str + Имя файла вместе с папкой. Рекомендуется размещать файлы с + текстом в папке `/static`. + + Returns + ------- + msg : str + Содержимое файла, переданное в `name`. + """ with open(name, "r") as f: msg = f.read() return msg async def delete_old_messages(bot: Bot) -> None: - """Удаляет сообещния, если их delete_date меньше чем время сейчас.""" + """Удалить старые сообщения. + + Удаляет сообщения, если их delete_date меньше чем время сейчас. + Если сообщения в чате не найдено - выводит сообщение в лог и + удалет сообщение из базы. + """ msg_list: Messages_to_delete = ( session.query(Messages_to_delete).order_by(Messages_to_delete.id).all() ) @@ -160,7 +233,25 @@ async def delete_old_messages(bot: Bot) -> None: def get_text_from_command(message: types.Message) -> str: - """Вернет текст из команды для бота (но не саму команду). Парсит message["entities"].""" + """Получить текст сообщения, отправленного боту. + + Вернет текст из команды для бота (но не саму команду). + + Parameters + ---------- + message : types.Message + Сообщения, из которого нужно извлечь текст. + + Returns + ------- + text : str + Текст (тело) сообщения без команды. + + Notes + ----- + Парсит message["entities"], поэтому в качестве аргумента не нужна + команда, а только объект сообщения. + """ command_length = int(message["entities"][0]["length"]) return message.text[command_length:].strip() @@ -168,7 +259,22 @@ def get_text_from_command(message: types.Message) -> str: async def send_message_to_chat( msg_date: datetime, delete_after: int, text: str, bot: Bot ) -> None: - """Отправляет сообщение в чат, ставит таймер на удаление через delete_after минут.""" + """Отправить сообщение в чат. + + Отправляет сообщение в чат, ставит таймер на удаление. + + Parameters + ---------- + msg_date : datetime + Время отправки сообщения. На его основе высчитывается время + удаления. + delete_after : int + Количество минут, после которого нужно удалить сообщение. + text : str + Сообщение для отправки в канал. + bot : Bot + Объект бота, через которого будет отправлено сообщение. + """ deletion_time = msg_date + timedelta(minutes=delete_after) text += f"\n\nБудет удалено в *{deletion_time}*"