diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 23927eb..c352d08 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -2,9 +2,9 @@ name: build on: push: - branches: [ main, develop, gh-actions ] + branches: [ main, gh-actions ] pull_request: - branches: [ main, develop, gh-actions ] + branches: [ main, gh-actions ] jobs: build: diff --git a/src/__init__.py b/src/__init__.py index 722083b..8c0d369 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,4 @@ from . import keyboards from . import services -import main import create_bot - diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..093c951 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,34 @@ +from aiogram import executor + +from create_bot import dp, logger +from handlers import admin, client, shared +from db import sqlite_db + + +async def on_startup(_) -> None: + logger.info("Bot now is online!") + sqlite_db.start_db() + + +async def on_shutdown(_) -> None: + logger.info("Bot now is offline!") + sqlite_db.stop_db() + + +def main(): + # Регистрация handler-функций. + admin.register_admin_handlers(dp) + client.register_client_handlers(dp) + shared.register_shared_handlers(dp) + + # Запуск бота в режиме опроса. + executor.start_polling( + dispatcher=dp, + skip_updates=True, + on_startup=on_startup, + on_shutdown=on_shutdown, + ) + + +if __name__ == '__main__': + main() diff --git a/src/create_bot.py b/src/create_bot.py index e40f79b..6092942 100644 --- a/src/create_bot.py +++ b/src/create_bot.py @@ -3,12 +3,13 @@ from dotenv import load_dotenv -from aiogram import Bot, Dispatcher, types +from aiogram import Bot, Dispatcher from aiogram.contrib.fsm_storage.memory import MemoryStorage load_dotenv('.env') # Configure logging +logger = logging.getLogger() logging.basicConfig(level=logging.INFO) # Initialize storage for FSM. diff --git a/src/db/init_db.sql b/src/db/init_db.sql index 8d67776..019258a 100644 --- a/src/db/init_db.sql +++ b/src/db/init_db.sql @@ -29,6 +29,6 @@ CREATE TABLE IF NOT EXISTS queues_list CREATE TABLE IF NOT EXISTS queue ( - id INTEGER REFERENCES queues_list (id), - msg_id INTEGER + id INTEGER REFERENCES queues_list (id), + msg_id INTEGER ); \ No newline at end of file diff --git a/src/db/sqlite_db.py b/src/db/sqlite_db.py index e613728..d5d38b1 100644 --- a/src/db/sqlite_db.py +++ b/src/db/sqlite_db.py @@ -1,18 +1,25 @@ import os import sqlite3 +import logging + from datetime import datetime from typing import Tuple -conn = sqlite3.connect('queue_bot.db') +conn = sqlite3.connect( + os.getenv( + 'DATABASE', + default='queue_bot.db', + ) +) cursor = conn.cursor() +logging.basicConfig(level=logging.INFO) -def start_db() -> None: - if os.getenv('DEBUG', 'True') == 'True': - sql_file_path = 'db/init_db.sql' - else: - sql_file_path = '/app/src/db/init_db.sql' +def start_db() -> None: + debug = bool(os.getenv('DEBUG', 'True')) + logging.info(f'DEBUG={debug}') + sql_file_path = 'db/init_db.sql' if debug else '/app/src/db/init_db.sql' with open(sql_file_path, 'r') as sql_file: sql_script = sql_file.read() @@ -20,111 +27,145 @@ def start_db() -> None: conn.commit() if conn: - print("Data base has been connected!") + logging.info("Database has been connected!") + + +def stop_db() -> None: + cursor.close() + conn.close() + logging.info("Database has been disconnected!") def sql_get_queue_list(admin_id_: int) -> list: cursor.execute( - "SELECT id, queue_name, start, chat_id, chat_title FROM queues_list WHERE assignee_id = ?", - (admin_id_,) + "SELECT id, queue_name, start, chat_id, chat_title " + "FROM queues_list " + "WHERE assignee_id = ?", + (admin_id_,), ) - return cursor.fetchall() async def sql_get_queue_from_list(id_: int) -> tuple: cursor.execute( - "SELECT * FROM queues_list WHERE id = ?", - (id_,) + "SELECT * " + "FROM queues_list " + "WHERE id = ?", + (id_,), ) - return cursor.fetchone() def sql_get_chat_title(chat_id_: int) -> tuple: cursor.execute( - "SELECT chat_title FROM chat WHERE chat_id = ?", (chat_id_,) + "SELECT chat_title " + "FROM chat " + "WHERE chat_id = ?", + (chat_id_,), ) - return cursor.fetchone() def sql_get_managed_chats(admin_id_: int) -> list: cursor.execute( - f"SELECT chat_id, chat_title FROM chat WHERE assignee_id = ?", (admin_id_,) + f"SELECT chat_id, chat_title " + f"FROM chat " + f"WHERE assignee_id = ?", + (admin_id_,), ) - return cursor.fetchall() async def sql_add_admin(admin_id_: int, user_name_: str) -> None: cursor.execute( - "INSERT OR IGNORE INTO admin VALUES (?, ?)", - (admin_id_, user_name_) + "INSERT OR IGNORE INTO admin " + "VALUES (?, ?)", + (admin_id_, user_name_), ) conn.commit() async def sql_add_managed_chat(admin_id_: int, chat_id_: int, chat_title_: str) -> None: cursor.execute( - "INSERT INTO chat ('assignee_id', 'chat_id', 'chat_title') VALUES (?, ?, ?)", - (admin_id_, chat_id_, chat_title_) + "INSERT INTO chat " + "('assignee_id', 'chat_id', 'chat_title') " + "VALUES (?, ?, ?)", + (admin_id_, chat_id_, chat_title_), ) conn.commit() async def sql_delete_managed_chat(chat_id_: int) -> None: cursor.execute( - "DELETE FROM chat WHERE chat_id = ?", (chat_id_,) + "DELETE FROM chat " + "WHERE chat_id = ?", + (chat_id_,), ) conn.commit() cursor.execute( - "DELETE FROM queues_list WHERE chat_id = ?", (chat_id_,) + "DELETE FROM queues_list " + "WHERE chat_id = ?", + (chat_id_,), ) conn.commit() async def sql_add_queue(admin_id_: int, queue_name_: str, start_dt: datetime, chat_id_: int, chat_title_: str) -> tuple: cursor.execute( - "INSERT INTO queues_list ('assignee_id', 'queue_name', 'start', 'chat_id', 'chat_title') " - "VALUES (?, ?, ?, ?, ?)", (admin_id_, queue_name_, start_dt, chat_id_, chat_title_) + "INSERT INTO queues_list " + "('assignee_id', 'queue_name', 'start', 'chat_id', 'chat_title') " + "VALUES (?, ?, ?, ?, ?)", + (admin_id_, queue_name_, start_dt, chat_id_, chat_title_), ) conn.commit() cursor.execute( - "SELECT id FROM queues_list WHERE assignee_id = ? AND queue_name = ? AND chat_id = ?", - (admin_id_, queue_name_, chat_id_) + "SELECT id " + "FROM queues_list " + "WHERE assignee_id = ? AND queue_name = ? AND chat_id = ?", + (admin_id_, queue_name_, chat_id_), ) - return cursor.fetchone() async def sql_delete_queue(id_: int) -> Tuple[int, int]: cursor.execute( - "SELECT chat_id FROM queues_list WHERE id = ?", (id_,) + "SELECT chat_id " + "FROM queues_list " + "WHERE id = ?", + (id_,), ) chat_id: tuple = cursor.fetchone() cursor.execute( - "DELETE FROM queues_list WHERE id = ?", (id_,) + "DELETE FROM queues_list " + "WHERE id = ?", + (id_,), ) conn.commit() cursor.execute( - "SELECT msg_id FROM queue WHERE id = ?", (id_,) + "SELECT msg_id " + "FROM queue " + "WHERE id = ?", + (id_,), ) msg_id: tuple = cursor.fetchone() cursor.execute( - "DELETE FROM queue WHERE id = ?", (id_,) + "DELETE FROM queue " + "WHERE id = ?", + (id_,), ) conn.commit() - return chat_id[0], msg_id[0] async def sql_post_queue_msg_id(queue_id_: int, msg_id_: int): cursor.execute( - "INSERT INTO queue ('id', 'msg_id') VALUES (?, ?)", (queue_id_, msg_id_) + "INSERT INTO queue " + "('id', 'msg_id') " + "VALUES (?, ?)", + (queue_id_, msg_id_), ) conn.commit() diff --git a/src/handlers/admin.py b/src/handlers/admin.py index 32824bd..7937052 100644 --- a/src/handlers/admin.py +++ b/src/handlers/admin.py @@ -1,19 +1,35 @@ from datetime import datetime + from aiogram import types, Dispatcher from aiogram.dispatcher import FSMContext from aiogram.dispatcher.filters import Text from aiogram.dispatcher.filters.state import State, StatesGroup from aiogram.types import InlineKeyboardMarkup +from src.db.sqlite_db import ( + sql_get_queue_list, + sql_add_queue, + sql_add_admin, + sql_delete_queue, + sql_get_managed_chats, + sql_get_chat_title, +) +from src.keyboards.client_kb import ( + PLAN_QUEUE_TEXT, + DELETE_QUEUE_TEXT, + PLANNED_QUEUES_TEXT, +) +from src.services.admin_service import ( + EarlierException, + parse_to_datetime, + wait_for_queue_launch, +) from src.create_bot import dp, bot -from src.db.sqlite_db import sql_get_queue_list, sql_add_queue, sql_add_admin, \ - sql_delete_queue, sql_get_managed_chats, sql_get_chat_title from src.keyboards import admin_kb, calendar_kb -from src.keyboards.client_kb import PLAN_QUEUE_TEXT, DELETE_QUEUE_TEXT, PLANNED_QUEUES_TEXT -from src.services.admin_service import EarlierException, parse_to_datetime, wait_for_queue_launch class FSMPlanning(StatesGroup): + """Конечный автомат для планирования очередей.""" choose_chat = State() queue_name = State() start_date = State() @@ -21,31 +37,44 @@ class FSMPlanning(StatesGroup): class FSMDeletion(StatesGroup): + """Конечный автомат удаления очереди.""" queue_choice = State() async def cancel_handler(callback: types.CallbackQuery, state: FSMContext) -> None: + """ + Функция-handler отмены действия. + Прекращает последовательность состояний в конечном автомате, попутно удаляя сообщение. + """ await callback.message.delete() await callback.answer('🚫 Действие отменено') await state.finish() async def queues_list_handler(msg: types.Message) -> tuple: + """ + Функция-handler выдачи списка запланированных очередей. + """ found_queues = sql_get_queue_list(msg.from_user.id) if not found_queues: await bot.send_message( msg.from_user.id, "🙊 У вас пока нет запланированных очередей.\nЗапланируем одну?", - reply_markup=admin_kb.inl_plan_kb + reply_markup=admin_kb.inl_plan_kb, ) + return found_queues, None - out_str = str() - for _, queue_name, dt, _, chat_title in found_queues: - out_str += f"📌«{queue_name}» в чате «{chat_title}» " \ - f"{datetime.strptime(dt, '%Y-%m-%d %H:%M:%S%z').strftime('%d.%m.%Y в %H:%M')}\n" + out_str = '\n'.join([ + f"📌«{queue_name}» в чате «{chat_title}» " + f"{datetime.strptime(dt, '%Y-%m-%d %H:%M:%S%z').strftime('%d.%m.%Y в %H:%M')}" + for _, queue_name, dt, _, chat_title in found_queues + ]) - planned_msg = await bot.send_message(msg.from_user.id, f"⤵️ Вот запланированные вами очереди:\n{out_str}") + planned_msg = await bot.send_message( + msg.from_user.id, + f"⤵️ Вот запланированные вами очереди:\n{out_str}", + ) return found_queues, planned_msg @@ -53,48 +82,64 @@ async def queues_list_handler(msg: types.Message) -> tuple: """ Planning queue zone""" -async def start_planning(action) -> None: - await action.answer('📑 Переходим к планированию очереди...') +async def queue_plan_handler(msg: types.Message) -> None: + await __start_planning(msg) + + +async def queue_plan_inline_handler(callback: types.CallbackQuery) -> None: + await __start_planning(callback) + +async def __start_planning(action: types.Message | types.CallbackQuery) -> None: + """ + Функция старта планирования очередей. + Если бот добавлен вами в групповые чаты, он предложит вам + выбрать, в каком именно вы хотите запланировать очередь. + """ + await action.answer('📑 Переходим к планированию очереди...') managed_chats = sql_get_managed_chats(action.from_user.id) if not managed_chats: await bot.send_message( action.from_user.id, "🙊 Вы пока не добавили меня ни в один групповой чат.\n" - "Я могу организовывать очереди только там 💁‍♂️" + "Я могу организовывать очереди только там 💁‍♂️", ) - return await FSMPlanning.choose_chat.set() - await sql_add_admin(action.from_user.id, action.from_user.username) inl_kb_chat_choices = InlineKeyboardMarkup() for chat_id, chat_title in managed_chats: - inl_kb_chat_choices.add(types.InlineKeyboardButton( - text=chat_title, callback_data=f"choose_chat_{chat_id}") + inl_kb_chat_choices.add( + types.InlineKeyboardButton( + text=chat_title, + callback_data=f"choose_chat_{chat_id}", + ) ) inl_kb_chat_choices.add(admin_kb.cancel_button) - await bot.send_message(action.from_user.id, "⤵️Для начала выберите чат, в который вы добавили бота:", - reply_markup=inl_kb_chat_choices) - - -async def queue_plan_inline_handler(callback: types.CallbackQuery) -> None: - await start_planning(callback) - - -async def queue_plan_handler(msg: types.Message) -> None: - await start_planning(msg) + await bot.send_message( + action.from_user.id, + "⤵️Для начала выберите чат, в который вы добавили бота:", + reply_markup=inl_kb_chat_choices, + ) async def queue_set_chat_handler(callback: types.CallbackQuery, state: FSMContext) -> None: + """ + Функция-handler сохранения выбранного чата. + Переводит в состояние выбора названия очереди если всё хорошо, + иначе отменяет действие. + """ async with state.proxy() as data: chat_id = int(callback.data[len("choose_chat_"):]) chat_title = sql_get_chat_title(chat_id) if not chat_title: + await callback.answer( + "Кажется, бота уже нет в данном чате, попробуйте снова.", + ) await cancel_handler(callback, state) return @@ -102,42 +147,67 @@ async def queue_set_chat_handler(callback: types.CallbackQuery, state: FSMContex data['chat_title'] = chat_title[0] await FSMPlanning.next() - - await bot.send_message(callback.from_user.id, "📝 Задайте название очереди", - reply_markup=admin_kb.inl_cancel_kb) + await bot.send_message( + callback.from_user.id, + "📝 Задайте название очереди", + reply_markup=admin_kb.inl_cancel_kb, + ) async def set_queue_name_handler(msg: types.Message, state: FSMContext) -> None: + """ + Функция-handler сохранения имени очереди. Переводит в состояние выбора даты старта очереди, + иначе выводит ошибку и просит повторить ввод. + """ if not msg.text or msg.text in (PLAN_QUEUE_TEXT, DELETE_QUEUE_TEXT, PLANNED_QUEUES_TEXT): await bot.send_message( - msg.from_user.id, '❌ Кажется, вы ничего не написали! Задайте название очереди', - reply_markup=admin_kb.inl_cancel_kb + msg.from_user.id, + '❌ Кажется, вы ничего не написали! Задайте название очереди', + reply_markup=admin_kb.inl_cancel_kb, ) return + async with state.proxy() as data: data['queue_name'] = msg.text + await FSMPlanning.next() await bot.send_message( msg.from_user.id, '📅 Теперь задайте дату запуска очереди через календарь:', - reply_markup=await calendar_kb.Calendar().start_calendar() + reply_markup=await calendar_kb.Calendar().start_calendar(), ) -async def set_date_handler(callback: types.CallbackQuery, callback_data: dict, state: FSMContext) -> None: - selected, date = await calendar_kb.Calendar().process_selection(callback, callback_data) +async def set_date_handler( + callback: types.CallbackQuery, + callback_data: dict, + state: FSMContext, +) -> None: + """ + Функция-handler сохранения выбранной в календаре даты. + Переводит в состояние выбора времени старта очереди. + """ + selected, date = await calendar_kb.Calendar().process_selection( + query=callback, + data=callback_data, + ) if selected: async with state.proxy() as data: data["selected_date"] = date + await FSMPlanning.next() await bot.send_message( callback.from_user.id, '🕓 Теперь задайте время запуска очереди в формате чч:мм (ex. "15:40")', - reply_markup=admin_kb.inl_cancel_kb + reply_markup=admin_kb.inl_cancel_kb, ) async def set_datetime_handler(msg: types.Message, state: FSMContext) -> None: + """ + Функция-handler сохранения времени и других собранных данных в БД. + Выдаёт информационное сообщение о запуске очереди и начинает ожидание. + """ start_datetime: datetime async with state.proxy() as data: try: @@ -151,8 +221,9 @@ async def set_datetime_handler(msg: types.Message, state: FSMContext) -> None: return except EarlierException as e: await bot.send_message( - msg.from_user.id, e, - reply_markup=admin_kb.inl_cancel_kb + msg.from_user.id, + text=str(e), + reply_markup=admin_kb.inl_cancel_kb, ) return @@ -160,30 +231,43 @@ async def set_datetime_handler(msg: types.Message, state: FSMContext) -> None: # Добавление собранных данных в бд. queue_name = data['queue_name'] - chat_id, chat_title = data['chat_id'], data['chat_title'] - queue_id = await sql_add_queue(msg.from_user.id, queue_name, start_datetime, chat_id, chat_title) + chat_id = data['chat_id'] + chat_title = data['chat_title'] + queue_id = await sql_add_queue( + msg.from_user.id, + queue_name, + start_datetime, + chat_id, + chat_title, + ) await bot.send_message( msg.from_user.id, - f"✅Очередь «{queue_name}» запланирована в чате «{chat_title}»!\n" - f"Начало очереди: {start_datetime.strftime('%d.%m.%Y в %H:%M')}" + f"✅ Очередь «{queue_name}» запланирована в чате «{chat_title}»!\n" + f"Начало очереди: {start_datetime.strftime('%d.%m.%Y в %H:%M')}", ) await bot.send_message( chat_id, - f"✅Очередь «{queue_name}» запланирована!\n" - f"Начало очереди: {start_datetime.strftime('%d.%m.%Y в %H:%M')}" + f"✅ Очередь «{queue_name}» запланирована!\n" + f"Начало очереди: {start_datetime.strftime('%d.%m.%Y в %H:%M')}", ) await state.finish() - - await wait_for_queue_launch(start_datetime, chat_id, queue_id[0]) + await wait_for_queue_launch( + start_datetime, + chat_id, + queue_id[0], + ) """ Deleting queue zone""" async def choose_queue_to_delete_handler(msg: types.Message) -> None: + """ + Функция-handler выбора запланированной очереди для удаления. + """ planned_queues, del_msg = await queues_list_handler(msg) if not planned_queues or del_msg is None: @@ -197,26 +281,37 @@ async def choose_queue_to_delete_handler(msg: types.Message) -> None: inl_kb_choices.add(admin_kb.cancel_button) global messages_tuple - messages_tuple = (del_msg, await bot.send_message(msg.from_user.id, '🗑 Выберите очередь, которую хотите удалить:', - reply_markup=inl_kb_choices)) + messages_tuple = ( + del_msg, + await bot.send_message( + msg.from_user.id, + '🗑 Выберите очередь, которую хотите удалить:', + reply_markup=inl_kb_choices, + ) + ) await FSMDeletion.queue_choice.set() +@dp.callback_query_handler(Text(startswith='delete_queue_'), state=FSMDeletion.queue_choice) async def delete_queue_handler(callback: types.CallbackQuery, state: FSMContext): + """ + Функция-handler удаления запланированной очереди. + """ chat_id, msg_id = await sql_delete_queue(int(callback.data[len("delete_queue_"):])) - await bot.delete_message(chat_id, msg_id) - await callback.answer('💥 Очередь удалена') - await messages_tuple[0].delete() - await messages_tuple[1].delete() - await state.finish() + try: + await bot.delete_message(chat_id, msg_id) + await messages_tuple[0].delete() + await messages_tuple[1].delete() + except TypeError: + pass + finally: + await callback.answer('💥 Очередь удалена') + await state.finish() def register_admin_handlers(dp_: Dispatcher) -> None: - """ - Function for registration all handlers for admin. - :return: None - """ + """Регистрация всех handler-функций для админа.""" dp_.register_callback_query_handler( cancel_handler, text="cancel_call", state="*" ) diff --git a/src/handlers/client.py b/src/handlers/client.py index 4f06ddc..fbf4327 100644 --- a/src/handlers/client.py +++ b/src/handlers/client.py @@ -7,26 +7,24 @@ from src.create_bot import dp, bot from src.keyboards.client_kb import main_kb, queue_inl_kb from src.services import client_service +from src.services.client_service import QueueStatus async def start_handler(message: types.Message): - """ - Handler for `/start` command. - """ - await bot.send_message(message.from_user.id, - f"Привет, {message.from_user.first_name} (@{message.from_user.username})!\n" - f"Я IU8-QueueBot - бот для создания очередей.\n" - "Давайте начнём: можете использовать команды (/help) " - f"или кнопки клавиатуры для работы со мной. В случае возникновения проблем, пишите " - f"@aaaaaaaalesha", - reply_markup=main_kb - ) + """Функция-handler для команды `/start`.""" + await bot.send_message( + message.from_user.id, + f"Привет, {message.from_user.first_name} (@{message.from_user.username})!\n" + f"Я IU8-QueueBot - бот для создания очередей.\n" + "Давайте начнём: можете использовать команды (/help) " + f"или кнопки клавиатуры для работы со мной. В случае возникновения проблем, пишите " + f"@aaaaaaaalesha", + reply_markup=main_kb, + ) async def help_handler(message: types.Message): - """ - Handler for `/help` command. - """ + """Функция-handler для команды `/help`.""" await bot.send_message( message.from_user.id, "/start - Начало работы с ботом \n" @@ -39,106 +37,95 @@ async def help_handler(message: types.Message): async def flood_handler(update: types.Update, exception: RetryAfter): - await update.message.answer(f"Не так быстро! Подождите {exception.timeout} секунд") + answer_msg = f"Не так быстро! Подождите {exception.timeout} секунд" + if update.message is not None: + await update.message.answer(answer_msg) + elif update.callback_query is not None: + await update.message.answer(answer_msg) async def sign_in_queue_handler(callback: types.CallbackQuery): user = callback.from_user - done, _ = await asyncio.wait( - (client_service.add_queuer_text(callback.message.text, user.first_name, user.username),) + new_text, status_code = await client_service.add_queuer_text( + callback.message.text, + user.first_name, + user.username, ) - for future in done: - new_text, status_code = future.result() - if status_code != client_service.STATUS_OK: - if status_code == client_service.STATUS_ALREADY_IN: - await callback.answer("❕ Вы уже в очереди.") - return - await asyncio.wait((callback.message.edit_text(text=new_text, reply_markup=queue_inl_kb),)) + match status_code: + case QueueStatus.OK: + await callback.message.edit_text( + text=new_text, + reply_markup=queue_inl_kb, + ) + case QueueStatus.EXISTS: + await callback.answer("❕ Вы уже в очереди.") async def sign_out_queue_handler(callback: types.CallbackQuery): user = callback.from_user - done, _ = await asyncio.wait( - (client_service.delete_queuer_text(callback.message.text, user.first_name, user.username),) + new_text, status_code = await client_service.delete_queuer_text( + callback.message.text, + user.first_name, + user.username, ) - for future in done: - new_text, status_code = future.result() - if status_code != client_service.STATUS_OK: - if status_code == client_service.STATUS_NO_QUEUERS: - await callback.answer("❕ В очереди ещё нет участников.") - return - if status_code == client_service.STATUS_NOT_QUEUER: - await callback.answer(f"❕ @{callback.from_user.username} ещё не участник очереди.") - return - - await asyncio.wait((callback.message.edit_text(text=new_text, reply_markup=queue_inl_kb),)) + match status_code: + case QueueStatus.OK: + await callback.message.edit_text( + text=new_text, + reply_markup=queue_inl_kb, + ) + case QueueStatus.EMPTY | QueueStatus.NOT_QUEUER as answer: + await callback.answer(answer) async def skip_ahead_handler(callback: types.CallbackQuery): - new_text, status_code = str(), -1 - user = callback.from_user - done, _ = await asyncio.wait( - (client_service.skip_ahead(callback.message.text, user.first_name, user.username),) + new_text, status_code = await client_service.skip_ahead( + callback.message.text, + user.first_name, + user.username, ) - - for future in done: - new_text, status_code = future.result() - - if status_code != client_service.STATUS_OK: - if status_code == client_service.STATUS_NO_QUEUERS: - await callback.answer("❕ В очереди ещё нет участников.") - return - if status_code == client_service.STATUS_ONE_QUEUER: - await callback.answer("❕ В очереди только один участник.") - return - if status_code == client_service.STATUS_NOT_QUEUER: - await callback.answer("❕ Вы ещё не участник очереди.") - return - if status_code == client_service.STATUS_NO_AFTER: - await callback.answer("❕ Вы крайний в очереди.") - return - await callback.answer("❕ Что-то пошло не так.") - return - - await callback.message.edit_text(text=new_text, reply_markup=queue_inl_kb) + match status_code: + case QueueStatus.OK: + await callback.message.edit_text( + text=new_text, + reply_markup=queue_inl_kb, + ) + case (QueueStatus.EMPTY + | QueueStatus.ONE_QUEUER + | QueueStatus.NOT_QUEUER + | QueueStatus.NO_AFTER) as answer: + await callback.answer(answer) + case _: + await callback.answer("❕ Что-то пошло не так") async def push_tail_handler(callback: types.CallbackQuery): - new_text, status_code = str(), -1 - user = callback.from_user - done, _ = await asyncio.wait( - (client_service.push_tail(callback.message.text, user.first_name, user.username),) + new_text, status_code = await client_service.push_tail( + callback.message.text, + user.first_name, + user.username, ) - for future in done: - new_text, status_code = future.result() - - if status_code != client_service.STATUS_OK: - if status_code == client_service.STATUS_NO_QUEUERS: - await callback.answer("❕ В очереди ещё нет участников.") - return - if status_code == client_service.STATUS_ONE_QUEUER: - await callback.answer("❕ В очереди только один участник.") - return - if status_code == client_service.STATUS_NOT_QUEUER: - await callback.answer("❕ Вы ещё не участник очереди.") - return - if status_code == client_service.STATUS_NO_AFTER: - await callback.answer("❕ Вы крайний в очереди.") - return - await callback.answer("❕ Что-то пошло не так.") - return - - await callback.message.edit_text(text=new_text, reply_markup=queue_inl_kb) + match status_code: + case QueueStatus.OK: + await callback.message.edit_text( + text=new_text, + reply_markup=queue_inl_kb, + ) + case (QueueStatus.EMPTY + | QueueStatus.ONE_QUEUER + | QueueStatus.NOT_QUEUER + | QueueStatus.NO_AFTER) as answer: + await callback.answer(answer) + case _: + await callback.answer("❕ Что-то пошло не так") def register_client_handlers(dp_: Dispatcher) -> None: - """ - Function registers all handlers for client. - """ + """Регистрация всех handler-функций для клиента.""" dp_.register_message_handler(start_handler, commands='start', state=None) dp_.register_message_handler(help_handler, commands="help", state=None) dp_.register_errors_handler(flood_handler, exception=RetryAfter) diff --git a/src/handlers/shared.py b/src/handlers/shared.py index 01993ad..199aa07 100644 --- a/src/handlers/shared.py +++ b/src/handlers/shared.py @@ -4,27 +4,32 @@ from src.db.sqlite_db import sql_add_admin, sql_add_managed_chat, sql_delete_managed_chat -async def new_chat_handler(message: types.Message): +async def new_chat_handler(message: types.Message) -> None: + """ + Функция-handler обработки добавления бота в групповой чат. + """ # Check that bot has been added to chat. if any(bot.id == member.id for member in message.new_chat_members): user = message.from_user await sql_add_admin(user.id, user.username) await sql_add_managed_chat(user.id, message.chat.id, message.chat.title) - await message.reply(f"Привет! Теперь {user.first_name} (@{user.username}) – " - "администратор очередей в этом чате.\n" - "Запланировать её можно в личном чате со мной. Приятной работы!") + await message.reply( + f"Привет! Теперь {user.first_name} (@{user.username}) – " + "администратор очередей в этом чате.\n" + "Запланировать её можно в личном чате со мной. Приятной работы!" + ) -async def left_chat_handler(message: types.Message): +async def left_chat_handler(message: types.Message) -> None: + """ + Функция-handler обработки удаления бота из группового чата. + """ # Check that bot has been deleted from chat. if bot.id == message.left_chat_member.id: await sql_delete_managed_chat(message.chat.id) def register_shared_handlers(dp_: Dispatcher) -> None: - """ - Function for registration all handlers for everyone. - """ - # dp.register_message_handler(echo, state=None) + """Регистрация всех публичных handler-функций.""" dp_.register_message_handler(new_chat_handler, content_types=types.ContentTypes.NEW_CHAT_MEMBERS) dp_.register_message_handler(left_chat_handler, content_types=types.ContentTypes.LEFT_CHAT_MEMBER) diff --git a/src/keyboards/admin_kb.py b/src/keyboards/admin_kb.py index e5b6744..1f8b9f8 100644 --- a/src/keyboards/admin_kb.py +++ b/src/keyboards/admin_kb.py @@ -1,8 +1,13 @@ from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton -cancel_button = InlineKeyboardButton(text='🚫 Отмена', callback_data='cancel_call', ) +cancel_button = InlineKeyboardButton( + text='🚫 Отмена', + callback_data='cancel_call', +) inl_cancel_kb = InlineKeyboardMarkup().add(cancel_button) - inl_plan_kb = InlineKeyboardMarkup().add( - InlineKeyboardButton(text='🗓 Запланировать очередь', callback_data='plan_queue') + InlineKeyboardButton( + text='🗓 Запланировать очередь', + callback_data='plan_queue', + ) ) diff --git a/src/keyboards/calendar_kb.py b/src/keyboards/calendar_kb.py index cdf39ae..049d128 100644 --- a/src/keyboards/calendar_kb.py +++ b/src/keyboards/calendar_kb.py @@ -13,8 +13,11 @@ class Calendar: """Calendar inline keyboard.""" - async def start_calendar(self, year: int = datetime.now(timezone('Europe/Moscow')).year, - month: int = datetime.now(timezone('Europe/Moscow')).month) -> InlineKeyboardMarkup: + async def start_calendar( + self, + year: int = datetime.now(timezone('Europe/Moscow')).year, + month: int = datetime.now(timezone('Europe/Moscow')).month, + ) -> InlineKeyboardMarkup: """ Creates an inline keyboard with the provided year and month :param int year: Year to use in the calendar, if None the current year is used. @@ -56,25 +59,35 @@ async def start_calendar(self, year: int = datetime.now(timezone('Europe/Moscow' dt_now = datetime.now(timezone('Europe/Moscow')) if dt_now > datetime(year, month, day, tzinfo=timezone('Europe/Moscow')) and day != dt_now.day: inline_kb.insert(InlineKeyboardButton( - self.__strike_through(day), callback_data=ignore_callback + self.__strike_through(day), + callback_data=ignore_callback, )) continue inline_kb.insert(InlineKeyboardButton( - str(day), callback_data=calendar_callback.new("DAY", year, month, day) + str(day), + callback_data=calendar_callback.new("DAY", year, month, day), )) # Last row - Buttons. inline_kb.row() inline_kb.insert(InlineKeyboardButton( - "◀️", callback_data=calendar_callback.new("PREV-MONTH", year, month, day) + "◀️", + callback_data=calendar_callback.new("PREV-MONTH", year, month, day), )) - inline_kb.insert(InlineKeyboardButton(" ", callback_data=ignore_callback)) inline_kb.insert(InlineKeyboardButton( - "▶️", callback_data=calendar_callback.new("NEXT-MONTH", year, month, day) + " ", + callback_data=ignore_callback, + )) + inline_kb.insert(InlineKeyboardButton( + "▶️", + callback_data=calendar_callback.new("NEXT-MONTH", year, month, day), )) # For cancelling plan queue. - inline_kb.add(InlineKeyboardButton(text='🚫 Отмена', callback_data='cancel_call')) + inline_kb.add(InlineKeyboardButton( + text='🚫 Отмена', + callback_data='cancel_call'), + ) return inline_kb @@ -95,7 +108,12 @@ async def process_selection(self, query: CallbackQuery, data: dict) -> tuple: and returning the date if so. """ return_data = (False, None) - temp_date = datetime(int(data['year']), int(data['month']), 1, tzinfo=timezone('Europe/Moscow')) + temp_date = datetime( + int(data['year']), + int(data['month']), + day=1, + tzinfo=timezone('Europe/Moscow'), + ) # Processing empty buttons, answering with no action. if data['act'] == "IGNORE": @@ -103,23 +121,47 @@ async def process_selection(self, query: CallbackQuery, data: dict) -> tuple: # User picked a day button, return date. if data['act'] == "DAY": await query.message.delete_reply_markup() # removing inline keyboard - return_data = True, datetime(int(data['year']), int(data['month']), int(data['day']), - tzinfo=timezone('Europe/Moscow')) + return_data = True, datetime( + int(data['year']), + int(data['month']), + int(data['day']), + tzinfo=timezone('Europe/Moscow'), + ) # User navigates to previous year, editing message with new calendar. if data['act'] == "PREV-YEAR": prev_date = temp_date - timedelta(days=365) - await query.message.edit_reply_markup(await self.start_calendar(int(prev_date.year), int(prev_date.month))) + await query.message.edit_reply_markup( + await self.start_calendar( + int(prev_date.year), + int(prev_date.month), + ) + ) # User navigates to next year, editing message with new calendar. if data['act'] == "NEXT-YEAR": next_date = temp_date + timedelta(days=365) - await query.message.edit_reply_markup(await self.start_calendar(int(next_date.year), int(next_date.month))) + await query.message.edit_reply_markup( + await self.start_calendar( + int(next_date.year), + int(next_date.month), + ) + ) # User navigates to previous month, editing message with new calendar. if data['act'] == "PREV-MONTH": prev_date = temp_date - timedelta(days=1) - await query.message.edit_reply_markup(await self.start_calendar(int(prev_date.year), int(prev_date.month))) + await query.message.edit_reply_markup( + await self.start_calendar( + int(prev_date.year), + int(prev_date.month), + ) + ) # User navigates to next month, editing message with new calendar. if data['act'] == "NEXT-MONTH": next_date = temp_date + timedelta(days=31) - await query.message.edit_reply_markup(await self.start_calendar(int(next_date.year), int(next_date.month))) + await query.message.edit_reply_markup( + await self.start_calendar( + int(next_date.year), + int(next_date.month), + ) + ) # At some point user clicks DAY button, returning date. return return_data diff --git a/src/keyboards/client_kb.py b/src/keyboards/client_kb.py index 7d6681a..d697fa7 100644 --- a/src/keyboards/client_kb.py +++ b/src/keyboards/client_kb.py @@ -11,10 +11,22 @@ queue_inl_kb = InlineKeyboardMarkup(row_width=2) queue_inl_kb.row( - InlineKeyboardButton(text='⤴️ Встать в очередь', callback_data='sign_in'), - InlineKeyboardButton(text='↩️ Покинуть очередь', callback_data='sign_out') + InlineKeyboardButton( + text='⤴️ Встать в очередь', + callback_data='sign_in', + ), + InlineKeyboardButton( + text='↩️ Покинуть очередь', + callback_data='sign_out', + ) ) queue_inl_kb.add( - InlineKeyboardButton(text='🔃 Пропустить вперёд', callback_data='skip_ahead'), - InlineKeyboardButton(text='↪️ В хвост очереди', callback_data='in_tail') + InlineKeyboardButton( + text='🔃 Пропустить вперёд', + callback_data='skip_ahead', + ), + InlineKeyboardButton( + text='↪️ В хвост очереди', + callback_data='in_tail', + ) ) diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 0de6e71..0000000 --- a/src/main.py +++ /dev/null @@ -1,21 +0,0 @@ -from aiogram import executor - -from create_bot import dp -from handlers import admin, client, shared -from db import sqlite_db - - -async def on_startup(_) -> None: - print("Bot is online!") - sqlite_db.start_db() - - -def main(): - admin.register_admin_handlers(dp) - client.register_client_handlers(dp) - shared.register_shared_handlers(dp) - executor.start_polling(dp, skip_updates=True, on_startup=on_startup) - - -if __name__ == '__main__': - main() diff --git a/src/services/admin_service.py b/src/services/admin_service.py index 3049898..729aa35 100644 --- a/src/services/admin_service.py +++ b/src/services/admin_service.py @@ -1,6 +1,4 @@ -# Copyright 2021 aaaaaaaalesha - -from datetime import datetime, timedelta +from datetime import datetime from pytz import timezone import asyncio from aiogram.utils.exceptions import BadRequest @@ -15,20 +13,27 @@ class EarlierException(Exception): async def wait_for_queue_launch(start_dt: datetime, chat_id: int, queue_id: int) -> None: - await asyncio.sleep((start_dt - datetime.now(timezone('Europe/Moscow'))).seconds) - # Check that queue has not been deleted. + """ + Ожидание начала запуска очереди в групповом чате. + """ + await asyncio.sleep( + (start_dt - datetime.now(timezone('Europe/Moscow'))).seconds + ) + + # Проверим, что очередь не была удалена. queue_data = await sql_get_queue_from_list(queue_id) if not queue_data: - await bot.send_message(chat_id, - f"🗑 Кажется, запланированную на это время очередь, удалили :(") + await bot.send_message( + chat_id=chat_id, + text=f"🗑 Кажется, запланированную на это время очередь, удалили :(", + ) return - msg = await bot.send_message(chat_id, - f"🆕 🅠🅤🅔🅤🅔 🆕\n" - f"Очередь «{queue_data[2]}» запущена!\n" - f"", - reply_markup=client_kb.queue_inl_kb - ) + msg = await bot.send_message( + chat_id, + f"🆕 🅠🅤🅔🅤🅔 🆕\n Очередь «{queue_data[2]}» запущена!\n\n", + reply_markup=client_kb.queue_inl_kb + ) try: await msg.pin(disable_notification=False) except BadRequest: @@ -37,12 +42,17 @@ async def wait_for_queue_launch(start_dt: datetime, chat_id: int, queue_id: int) await sql_post_queue_msg_id(queue_id, msg.message_id) -def parse_to_datetime(date: datetime, text: str) -> datetime: +def parse_to_datetime(date: datetime, input_time: str) -> datetime: + """ + Парсинг времени в формате hh:mm, а также проверка, что введённое время позже указанного. + """ dt_now = datetime.now(timezone('Europe/Moscow')) - h, m = tuple(map(int, text.split(':'))) + h, m = tuple(map(int, input_time.split(':'))) resulted_date = date if resulted_date.replace(hour=h, minute=m, second=0) < dt_now: - raise EarlierException(f"❌ Введённое время раньше текущего!\nСейчас {dt_now.strftime('%H:%M')}") + raise EarlierException( + f"❌ Введённое время раньше текущего!\nСейчас {dt_now.strftime('%H:%M')}" + ) return date.replace(hour=h, minute=m, second=0, tzinfo=dt_now.tzinfo) diff --git a/src/services/client_service.py b/src/services/client_service.py index 0c6ea40..9c66c22 100644 --- a/src/services/client_service.py +++ b/src/services/client_service.py @@ -1,34 +1,33 @@ -# Copyright 2022 aaaaaaaalesha +from enum import StrEnum -from typing import Tuple -STATUS_OK = 0 -STATUS_ALREADY_IN = 1 -STATUS_NO_QUEUERS = 2 -STATUS_ONE_QUEUER = 3 -STATUS_NOT_QUEUER = 4 -STATUS_NO_AFTER = 5 +class QueueStatus(StrEnum): + OK = '👍' + EXISTS = '❕ Вы уже в очереди' + EMPTY = '❕ В очереди ещё нет участников' + ONE_QUEUER = '❕ В очереди только один участник' + NOT_QUEUER = '❕ Вы ещё не участник очереди' + NO_AFTER = '❕ Вы крайний в очереди' -async def add_queuer_text(old_text: str, first_name: str, username: str) -> Tuple[str, int]: +async def add_queuer_text(old_text: str, first_name: str, username: str) -> tuple[str, QueueStatus]: lines = old_text.split('\n') - match_str = f"{first_name} (@{username})" for i in range(2, len(lines)): if lines[i].rfind(match_str) != -1: - return str(), STATUS_ALREADY_IN + return str(), QueueStatus.EXISTS lines.append(f"{len(lines) - 1}. {match_str}") - return '\n'.join(lines), STATUS_OK + return '\n'.join(lines), QueueStatus.OK -async def delete_queuer_text(old_text: str, first_name: str, username: str) -> Tuple[str, int]: +async def delete_queuer_text(old_text: str, first_name: str, username: str) -> tuple[str, QueueStatus]: lines = old_text.split('\n') match_str = f"{first_name} (@{username})" if len(lines) == 2: - return str(), STATUS_NO_QUEUERS + return str(), QueueStatus.EMPTY else: index_changer = -1 for i in range(2, len(lines)): @@ -38,38 +37,38 @@ async def delete_queuer_text(old_text: str, first_name: str, username: str) -> T break if index_changer == -1: - return str(), STATUS_NOT_QUEUER + return str(), QueueStatus.NOT_QUEUER # If queuer is last in queue. if index_changer == len(lines): - return '\n'.join(lines), STATUS_OK + return '\n'.join(lines), QueueStatus.OK for i in range(index_changer, len(lines)): lines[i] = lines[i].replace(f"{i}. ", f"{i - 1}. ", 1) - return '\n'.join(lines), STATUS_OK + return '\n'.join(lines), QueueStatus.OK -async def skip_ahead(old_text: str, first_name: str, username: str) -> Tuple[str, int]: +async def skip_ahead(old_text: str, first_name: str, username: str) -> tuple[str, QueueStatus]: lines = old_text.split('\n') if len(lines) == 2: - return str(), STATUS_NO_QUEUERS + return str(), QueueStatus.EMPTY if len(lines) == 3: - return str(), STATUS_ONE_QUEUER + return str(), QueueStatus.ONE_QUEUER index_changer = -1 match_str = f"{first_name} (@{username})" for i in range(2, len(lines)): if lines[i].rfind(match_str) != -1: if i == len(lines) - 1: - return str(), STATUS_NO_AFTER + return str(), QueueStatus.NO_AFTER index_changer = i break if index_changer == -1: - return str(), STATUS_NOT_QUEUER + return str(), QueueStatus.NOT_QUEUER lines[index_changer] = lines[index_changer].replace(f"{index_changer - 1}. ", f"{index_changer}. ", 1) lines[index_changer + 1] = lines[index_changer + 1].replace(f"{index_changer}. ", f"{index_changer - 1}. ", 1) @@ -77,17 +76,17 @@ async def skip_ahead(old_text: str, first_name: str, username: str) -> Tuple[str # Swap. lines[index_changer], lines[index_changer + 1] = lines[index_changer + 1], lines[index_changer] - return '\n'.join(lines), STATUS_OK + return '\n'.join(lines), QueueStatus.OK -async def push_tail(old_text: str, first_name: str, username: str) -> Tuple[str, int]: +async def push_tail(old_text: str, first_name: str, username: str) -> tuple[str, QueueStatus]: lines = old_text.split('\n') if len(lines) == 2: - return str(), STATUS_NO_QUEUERS + return str(), QueueStatus.EMPTY if len(lines) == 3: - return str(), STATUS_ONE_QUEUER + return str(), QueueStatus.ONE_QUEUER index_changer = -1 del_queuer = str() @@ -96,17 +95,17 @@ async def push_tail(old_text: str, first_name: str, username: str) -> Tuple[str, for i in range(2, len(lines)): if lines[i].rfind(match_str) != -1: if i + 1 == len(lines): - return str(), STATUS_NO_AFTER + return str(), QueueStatus.NO_AFTER del_queuer = lines.pop(i) index_changer = i break if index_changer == -1: - return str(), STATUS_NOT_QUEUER + return str(), QueueStatus.NOT_QUEUER for i in range(index_changer, len(lines)): lines[i] = lines[i].replace(f"{i}. ", f"{i - 1}. ", 1) lines.append(del_queuer.replace(f"{index_changer - 1}. ", f"{len(lines) - 1}. ", 1)) - return '\n'.join(lines), STATUS_OK + return '\n'.join(lines), QueueStatus.OK