diff --git a/bot/app/bot.py b/bot/app/bot.py index 9a12f10..16e7789 100644 --- a/bot/app/bot.py +++ b/bot/app/bot.py @@ -17,9 +17,11 @@ from datetime import datetime import math import socket import os +from typing import Union # Local imports -from config import token, admins +from config import token +from constants import MAX_QUEUES_OWN, MAX_QUEUES_PARTS_IN, MAX_NAME_LENGTH, MAX_QUEUE_NAME_LENGTH import textbook import keyboards @@ -63,23 +65,44 @@ async def get_queue_from_state_data(call: types.CallbackQuery) -> Queue: return None return queue - -def get_first_queue_user(queue: Queue) -> QueueUser: - arr = sorted(queue.users, key=lambda qu: qu.position) - return arr[0] +async def get_parting_queue_from_state_data(call: types.CallbackQuery) -> Queue: + async with bot.retrieve_data( + user_id=call.from_user.id, chat_id=call.message.chat.id + ) as state_data: + queue_id = state_data.get("part_queue_id", None) + if not queue_id: + await bot.answer_callback_query( + callback_query_id=call.id, text=textbook.queue_operational_error + ) + return None + queue = session.query(Queue).filter_by(id=queue_id).first() + return queue -def proceed_queue(queue: Queue) -> bool: +def get_first_queue_user(queue: Queue) -> Union[QueueUser, None]: + arr = sorted(queue.users, key=lambda qu: qu.position) # TODO: Maybe there is a better solution..? + return arr[0] if len(arr) else None + + +def kick_first(queue: Queue) -> bool: if len(queue.users): - for qu in queue.users: - setattr(qu, "position", qu.position - 1) first_user = get_first_queue_user(queue) session.delete(first_user) session.commit() + normalize_queue(queue) return True return False +def proceed_queue_user(queue: Queue, user: User) -> Union[QueueUser, None]: + first_queue_user = get_first_queue_user(queue) + if user.id == first_queue_user.user_id: + kick_first(queue) + next_queue_user = get_first_queue_user(queue) + return next_queue_user + return None + + async def update_queue_users_message(msg: Message, queue: Queue): users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) await bot.edit_message_text( @@ -90,6 +113,15 @@ async def update_queue_users_message(msg: Message, queue: Queue): parse_mode="html", ) +def normalize_queue(queue: Queue) -> Queue: + # first_user = get_first_queue_user(queue) + # if first_user.position != 0: + # setattr(first_user, "position", 0) + # session.commit() + # queue = session.query(Queue).filter_by(id=queue.id).first() + for i, qu in enumerate(sorted(queue.users, key=lambda qu: qu.position)): + setattr(qu, "position", i) + session.commit() # Basic @@ -116,7 +148,7 @@ async def start(msg: Message): not session.query(QueueUser) .filter_by(queue_id=queue.id, user_id=msg.from_user.id) .first() - ): + ) and not len(user.takes_part_in_queues) > MAX_QUEUES_PARTS_IN: last_user = ( session.query(QueueUser) .filter_by(queue_id=queue.id) @@ -167,6 +199,35 @@ async def to_menu_handler(call: types.CallbackQuery): ) +@bot.callback_query_handler(func=lambda c: c.data[:2] == "p:") +async def proceed_user_handler(call: types.CallbackQuery): + queue_id = call.data[2:] + queue = session.query(Queue).filter_by(id=queue_id).first() + user = session.query(User).filter_by(id=call.from_user.id).first() + if next_queue_user := proceed_queue_user(queue, user): + try: + await bot.send_message( + chat_id=next_queue_user.user_id, + text=textbook.your_turn.format(name=queue.name), + reply_markup=keyboards.your_turn(queue.id), + parse_mode="html", + ) + except: + await bot.send_message( + chat_id=queue.owner.id, + text=textbook.error_turn.format(name=queue.name), + reply_markup=keyboards.your_turn(queue.id), + parse_mode="html", + ) + await bot.edit_message_text( + chat_id=call.message.chat.id, + message_id=call.message.id, + text=textbook.finished_turn.format(name=queue.name), + reply_markup=None, + ) + await bot.answer_callback_query(callback_query_id=call.id) + + # Main menu @@ -174,7 +235,7 @@ async def to_menu_handler(call: types.CallbackQuery): async def new_queue_handler(call: types.CallbackQuery): user = session.query(User).filter_by(id=call.from_user.id).first() if user: - if len(user.owns_queues) < 4: + if len(user.owns_queues) < MAX_QUEUES_OWN: queue = Queue(owner_id=call.from_user.id) session.add(queue) session.commit() @@ -203,6 +264,19 @@ async def my_queues_handler(call: types.CallbackQuery): await bot.answer_callback_query(callback_query_id=call.id) +@bot.callback_query_handler(func=lambda c: c.data == "parts_queues") +async def parts_queues_handler(call: types.CallbackQuery): + user = session.query(User).filter_by(id=call.from_user.id).first() + queues = [qu.queue for qu in user.takes_part_in_queues] + await bot.edit_message_text( + chat_id=call.message.chat.id, + message_id=call.message.id, + text=textbook.parts_queues_menu.format(count=len(queues)), + reply_markup=keyboards.parts_queues(queues), + ) + await bot.answer_callback_query(callback_query_id=call.id) + + @bot.callback_query_handler(func=lambda c: c.data == "settings") async def settings(call: types.CallbackQuery): await bot.set_state(user_id=call.from_user.id, state=States.default) @@ -224,7 +298,67 @@ async def about_handler(call: types.CallbackQuery): ) -# Queue list menu +# Queue parts list menu + + +@bot.callback_query_handler(func=lambda c: c.data[:2] == "t:") +async def queue_parts_handler(call: types.CallbackQuery, queue_id: str = None): + queue_id = call.data[2:] if not queue_id else queue_id + queue = session.query(Queue).filter_by(id=queue_id).first() + if not queue: + await bot.answer_callback_query(callback_query_id=call.id, text=textbook.error) + return None + async with bot.retrieve_data( + user_id=call.from_user.id, chat_id=call.message.chat.id + ) as state_data: + state_data["part_queue_id"] = queue_id + users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) + await bot.edit_message_text( + chat_id=call.message.chat.id, + message_id=call.message.id, + text=textbook.part_queue.format(name=queue.name, users_str=users_str), + reply_markup=keyboards.queue_part_in_menu(), + parse_mode="html", + ) + await bot.answer_callback_query(callback_query_id=call.id) + + +# Queue partin menu + + +@bot.callback_query_handler(func=lambda c: c.data == "leave_queue") +async def leave_queue_handler(call: types.CallbackQuery): + if queue := await get_parting_queue_from_state_data(call): + queueuser = session.query(QueueUser).filter_by(queue_id=queue.id, user_id=call.from_user.id).first() + session.delete(queueuser) + session.commit() + normalize_queue(queue) + await bot.answer_callback_query( + callback_query_id=call.id, + text=textbook.leaved_queue.format(name=queue.name)) + await parts_queues_handler(call) + return None + await bot.answer_callback_query(callback_query_id=call.id) + + +@bot.callback_query_handler(func=lambda c: c.data == "refresh_list") +async def refresh_list_handler(call: types.CallbackQuery): + if queue := await get_parting_queue_from_state_data(call): + users_str = "\n".join([f"{qu.position}. {qu.user.name}" for qu in queue.users]) + try: + await bot.edit_message_text( + chat_id=call.message.chat.id, + message_id=call.message.id, + text=textbook.part_queue.format(name=queue.name, users_str=users_str), + reply_markup=keyboards.queue_part_in_menu(), + parse_mode="html", + ) + except: + await asyncio.sleep(2) + await bot.answer_callback_query(callback_query_id=call.id) + + +# Queue own list menu @bot.callback_query_handler(func=lambda c: c.data[:2] == "q:") @@ -289,15 +423,31 @@ async def queue_settings_handler(call: types.CallbackQuery): @bot.callback_query_handler(func=lambda c: c.data == "start_queue") async def start_queue_handler(call: types.CallbackQuery): - pass + if queue := await get_queue_from_state_data(call): + if first_queue_user := get_first_queue_user(queue): + await bot.send_message( + chat_id=first_queue_user.user_id, + text=textbook.your_turn.format(name=queue.name), + reply_markup=keyboards.your_turn(queue.id), + parse_mode="html", + ) + else: + await bot.answer_callback_query( + callback_query_id=call.id, + text=textbook.kick_first_error, + show_alert=True, + ) + await bot.answer_callback_query(callback_query_id=call.id) # Queue users + + @bot.callback_query_handler(func=lambda c: c.data == "kick_first") async def get_queue_users_handler(call: types.CallbackQuery): if queue := await get_queue_from_state_data(call): if queue.owner_id == call.from_user.id: - if proceed_queue(queue): + if kick_first(queue): await bot.answer_callback_query( callback_query_id=call.id, text=textbook.first_kicked ) @@ -318,7 +468,7 @@ async def get_queue_users_handler(call: types.CallbackQuery): @bot.callback_query_handler(func=lambda c: c.data == "swap_users") -async def swap_users_position(call: types.CallbackQuery): +async def swap_users_handler(call: types.CallbackQuery): await bot.answer_callback_query( callback_query_id=call.id, text=textbook.in_development_plug, @@ -326,6 +476,11 @@ async def swap_users_position(call: types.CallbackQuery): ) +@bot.callback_query_handler(func=lambda c: c.data == "refresh_users") +async def refresh_users_handler(call: types.CallbackQuery): + await get_queue_users_handler(call) + + # Queue settings @@ -336,7 +491,7 @@ async def edit_queue_name_handler(call: types.CallbackQuery): await bot.edit_message_text( chat_id=call.message.chat.id, message_id=call.message.id, - text=textbook.edit_name, + text=textbook.edit_queue_name, reply_markup=keyboards.edit_name(), ) @@ -350,7 +505,7 @@ async def edit_queue_name_cancel_handler(call: types.CallbackQuery): @bot.message_handler(content_types=["text"], state=States.changing_queue_name) async def update_queue_name(msg: Message): - if len(msg.text) > 40 or "\n" in msg.text: + if len(msg.text) > MAX_QUEUE_NAME_LENGTH or "\n" in msg.text: await bot.send_message(chat_id=msg.chat.id, text=textbook.edit_name_error) return None async with bot.retrieve_data( @@ -422,7 +577,7 @@ async def edit_name_cancel_handler(call: types.CallbackQuery): @bot.message_handler(content_types=["text"], state=States.changing_name) async def update_queue_name(msg: Message): - if len(msg.text) > 40 or "\n" in msg.text: + if len(msg.text) > MAX_NAME_LENGTH or "\n" in msg.text: await bot.send_message(chat_id=msg.chat.id, text=textbook.edit_name_error) return None user = session.query(User).filter_by(id=msg.from_user.id).first() diff --git a/bot/app/constants.py b/bot/app/constants.py new file mode 100644 index 0000000..1645671 --- /dev/null +++ b/bot/app/constants.py @@ -0,0 +1,5 @@ +MAX_QUEUES_OWN = 4 +MAX_QUEUES_PARTS_IN = 8 +MAX_NAME_LENGTH = 40 +MAX_QUEUE_NAME_LENGTH = 40 +MAX_QUEUE_PARTICIPANTS = 40 diff --git a/bot/app/keyboards.py b/bot/app/keyboards.py index cb705bb..312ddc7 100644 --- a/bot/app/keyboards.py +++ b/bot/app/keyboards.py @@ -10,6 +10,7 @@ def menu() -> keyboard: keyboard=[ [button(text="➕ Новая очередь", callback_data="new_queue")], [button(text="📋 Мои очереди", callback_data="my_queues")], + [button(text="🕓 Текущие очереди", callback_data="parts_queues")], [button(text="🔧 Настройки", callback_data="settings")], [button(text="ℹ️ О боте", callback_data="about")], ] @@ -22,6 +23,22 @@ def my_queues(queues: list[Queue]) -> keyboard: return keyboard(kb) +def parts_queues(queues: list[Queue]) -> keyboard: + kb = [[button(text=q.name, callback_data=f"t:{q.id}")] for q in queues] + kb.append([button(text="⬅️ В меню", callback_data="to_menu")]) + return keyboard(kb) + + +def queue_part_in_menu() -> keyboard: + return keyboard( + keyboard=[ + [button(text="🚪 Выйти из очереди", callback_data="leave_queue")], + [button(text="🔄 Обновить список", callback_data="refresh_list")], + [button(text="⬅️ Назад", callback_data="parts_queues")], + ] + ) + + def queue_menu() -> keyboard: return keyboard( keyboard=[ @@ -80,6 +97,15 @@ def queue_users(queue_id: str) -> keyboard: keyboard=[ [button(text="🔃 Поменять позиции", callback_data="swap_users")], [button(text="⏩ Кикнуть первого", callback_data="kick_first")], + [button(text="🔄 Обновить список", callback_data="refresh_users")], [button(text="⬅️ Назад", callback_data=f"q:{queue_id}")], ] ) + + +def your_turn(queue_id: str) -> keyboard: + return keyboard( + keyboard=[ + [button(text="Я закончил ⏩", callback_data=f"p:{queue_id}")], + ] + ) diff --git a/bot/app/textbook.py b/bot/app/textbook.py index db1add9..a861356 100644 --- a/bot/app/textbook.py +++ b/bot/app/textbook.py @@ -23,12 +23,19 @@ queue_deleted = "Очередь удалена" queue_users_list = "В очереди {name} следующие участники:\n\n{users_str}" link_template = "https://t.me/queue_senko_bot?start={link}" joined_queue = "Ты присоединился к очереди {name}\nТвоя позиция: {position}\n\nКогда придет твоя очередь, я сообщу" -error_joining_queue = "Ты не можешь присоединиться к очереди {name}, так как ты уже в ней состоишь!" +error_joining_queue = "Ты не можешь присоединиться к очереди {name}, так как ты уже в ней состоишь или достигнут лимит очередей (8)!" +max_participants_reached = "Очередь переполнена, подожди, пока кто-нибудь выйдет!" first_kicked = "Первый пользователь кикнут" -kick_first_error = ( - "Действие не выполнено, возможно вы уже вышли из очереди, или очередь пуста?" -) +kick_first_error = "Действие не выполнено, возможно вы уже вышли из очереди, или очередь пуста?" -about = "Бот для очередей.\n\nРазработчик - ollyhearn.\nЯ всегда открыт для вопросов и предложений: @OllyHearn\n\nv0.1.1-beta" +parts_queues_menu = "Ты принимаешь участие в {count} очереди/ей" +part_queue = "Очередь {name}\n\nУчастники:\n{users_str}" +leaved_queue = "Ты вышел из очереди {name}" + +your_turn = "Наступил твой черед в одчереди {name}. Иди делай свои дела, а когда закончишь - нажми кнопку снизу ⤵️" +finished_turn = "Ты вышел из очереди {name}, удачи!" +error_turn = "Внимание! Не удалось отправить сообщение следующему пользователю очереди {name}! По своему усмотрению ты можешь зайти и кикнуть его вручную" + +about = "Бот для очередей.\n\nРазработчик - ollyhearn.\nЯ всегда открыт для вопросов и предложений: @OllyHearn\n\nv0.1.6-beta" groups_plug = "Всем привет, я бот для очередей! В настоящее время идет активная разработка, так что я пока не могу полностью функционировать в группах, но вы всегда можете запустить меня в личном диалоге, создать очередь, и отправить ссылку на очередь сюда. Функционал будет доработан, а пока пользуйтесь мной в личке:\n\nhttps://t.me/queue_senko_bot" in_development_plug = "Функция в разработке ¯\_(ツ)_/¯"