diff --git a/README.md b/README.md index f7d9d00..7a126d8 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,15 @@ Dependencies: - AWS cron job - Generate pairs weekly - Bot asks if a user contacted his/her pair on Wed/Thu -- Inline Menu - - Pause/Resume buttons - - Generate pairs for admins - - Provide Feedback button + send to separate channel +- ~~Inline Menu~~ + - ~~Pause/Resume buttons~~ + - ~~Generate pairs for admins~~ + - ~~Provide Feedback button + send to separate channel~~ ### Nice to have: - Select offline location from a list - Notify members about the next round -- Edit profile from inline menu +- ~~Edit profile from inline menu~~ - Move all strings to a separate file/module diff --git a/bot.py b/bot.py index 83623f6..1b373a7 100644 --- a/bot.py +++ b/bot.py @@ -65,6 +65,8 @@ def debug_main(): def add_handlers(): application.add_handler(CommandHandler(command=Command.start, callback=start_handler)) + application.add_handler(CommandHandler(command=Command.menu, callback=menu_handler)) + application.add_handler(CallbackQueryHandler(callback=pause_handler, pattern=f"^{MenuCallback.pause}$")) application.add_handler(profile_conversation_handler()) @@ -97,7 +99,7 @@ def profile_conversation_handler() -> ConversationHandler: CommandHandler(Command.cancel, cancel_handler) ], FillProfileCallback.city: [ - MessageHandler(filters=filters.LOCATION & ~filters.COMMAND, callback=city_handler), + MessageHandler(filters=filters.LOCATION & ~filters.COMMAND & ~filters.TEXT, callback=city_handler), CommandHandler(Command.cancel, cancel_handler) ], FillProfileCallback.bio: [ diff --git a/src/db_helper.py b/src/db_helper.py index d4081b9..301a065 100644 --- a/src/db_helper.py +++ b/src/db_helper.py @@ -1,15 +1,14 @@ -from collections import namedtuple from typing import Optional -from src.vars import PROD -from src.models.user import User -from src.models.static_models import MeetingFormatCallback import boto3 from boto3.dynamodb.conditions import Key, Attr +from src.models.static_models import MeetingFormatCallback +from src.models.user import User +from src.vars import PROD + class DBHelper(object): - def __new__(cls): if not hasattr(cls, 'instance'): cls.instance = super(DBHelper, cls).__new__(cls) @@ -55,9 +54,9 @@ def update_user_profile( 'bio': user.bio, 'is_paused': user.is_paused } - if user.meeting_format == MeetingFormatCallback.offline: + if user.meeting_format == MeetingFormatCallback.offline.id: item['city'] = user.city - item['country']: user.country + item['country'] = user.country self._usersTable.put_item(Item=item) diff --git a/src/handlers/menu_handlers.py b/src/handlers/menu_handlers.py index 4d1afd9..93ebb9d 100644 --- a/src/handlers/menu_handlers.py +++ b/src/handlers/menu_handlers.py @@ -1,10 +1,8 @@ -import logging - from telegram import ( error, + ChatMember, ChatMemberBanned, ChatMemberLeft, - Update, InlineKeyboardButton, InlineKeyboardMarkup ) @@ -17,32 +15,60 @@ logging.getLogger().setLevel('INFO') db_helper = DBHelper() -current_session_user: User +current_db_user: User +chat_member: ChatMember async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): try: logging.info('Getting chat member..') - member = await context.bot.get_chat_member(chat_id=MEMBERSHIP_CHAT_ID, user_id=update.effective_user.id) + member = await _get_chat_member(update, context) logging.info('Done..') if member.status is not (ChatMemberLeft or ChatMemberBanned): logging.info('Member is fine, show keyboard..') - db_user = db_helper.get_user(user_id=member.user.id) + db_user = _get_db_user(update, context) + + if db_user is None: + await update.message.reply_text( + text="Привет, я Random Coffee bot!\n\n" + "Чтобы начать, заполни профиль ниже. Еще можно временно остановить участие и оставить отзыв", + ) - reply_keyboard: list[list] = [] + menu_markup = _menu_buttons(context, member, db_user) - if db_user is not None: - _add_existing_user_buttons(context, member, db_user, reply_keyboard) + await update.message.reply_text( + text='Меню', + reply_markup=menu_markup + ) else: - reply_keyboard.append( - [InlineKeyboardButton('👤 Заполнить профиль', callback_data=MenuCallback.fill_profile)] + await update.message.reply_text( + text="Ты уже стартовал, вызови /menu, чтобы заполнить или отредактировать профиль" ) + else: + logging.info('Sending membership message request...') + await send_membership_message(update, context) + logging.info('Done') + except error.BadRequest as e: + logging.info('Bad request') + logging.info(e) + await send_membership_message(update, context) + + +async def menu_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + try: + logging.info('Getting chat member..') + member = await _get_chat_member(update, context) + logging.info('Done..') + + if member.status is not (ChatMemberLeft or ChatMemberBanned): + logging.info('Member is fine, show keyboard..') + db_user = _get_db_user(update, context) + reply_keyboard_markup = _menu_buttons(context, member, db_user) await update.message.reply_text( - "Привет, я Random Coffee bot!\n\n" - "Чтобы начать, заполни профиль ниже. Еще можно временно остановить участие и оставить отзыв", - reply_markup=InlineKeyboardMarkup(reply_keyboard) + 'Меню', + reply_markup=reply_keyboard_markup ) logging.info('Keyboard shown') @@ -56,39 +82,36 @@ async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): async def pause_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + query = update.callback_query + await query.answer() + logging.info('Setting pause on/off') - global current_session_user + db_user = _get_db_user(update, context) - try: - current_session_user - except NameError: - current_session_user = db_helper.get_user(str(update.effective_user.id)) + should_pause = not db_user.is_paused + db_user.is_paused = should_pause + context.user_data['is_paused'] = db_user.is_paused - should_pause = not current_session_user.is_paused - current_session_user.is_paused = should_pause + member = await _get_chat_member(update, context) - db_helper.pause_user(should_pause, current_session_user) - inline_keyboard = update.callback_query.message.reply_markup.inline_keyboard + db_helper.pause_user(should_pause, db_user) + inline_markup = _menu_buttons(context, member, db_user) - for idx, inline_buttons in enumerate(inline_keyboard): - if inline_buttons[0].callback_data == MenuCallback.pause: - if not should_pause: - inline_keyboard[idx] = \ - [InlineKeyboardButton(text='⏸️ Поставить на паузу', callback_data=MenuCallback.pause)] - else: - inline_keyboard[idx] = \ - [InlineKeyboardButton(text='▶️ Снять с паузы', callback_data=MenuCallback.pause)] - break + await update.effective_message.edit_reply_markup(inline_markup) - await update.callback_query.message.edit_reply_markup( - InlineKeyboardMarkup(inline_keyboard=inline_keyboard) + await update.effective_message.reply_text( + text='Скрыл твою анкету. Если захочешь участвовать снова, сними с паузы' if should_pause + else 'Открыл твою анкету, ты снова участвуешь' ) logging.info('Done pause') async def feedback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query = update.callback_query + await query.answer() + user_id = update.effective_user.id await context.bot.send_message( @@ -125,33 +148,63 @@ async def send_membership_message(update: Update, context: ContextTypes.DEFAULT_ # Private -def _add_existing_user_buttons(context, member, db_user, reply_keyboard_markup): - context.user_data['is_paused'] = db_user.is_paused - global current_session_user - current_session_user = db_user +def _get_db_user(update, context) -> User: + global current_db_user + + try: + current_db_user + except NameError: + current_db_user = db_helper.get_user(str(update.effective_user.id)) + + if current_db_user is not None: + context.user_data['is_paused'] = current_db_user.is_paused + + return current_db_user + + +async def _get_chat_member(update, context) -> ChatMember: + global chat_member + + try: + chat_member + except NameError: + chat_member = await context.bot.get_chat_member(chat_id=MEMBERSHIP_CHAT_ID, user_id=update.effective_user.id) + + return chat_member - reply_keyboard_markup.append( - [InlineKeyboardButton('👤 Редактировать профиль', callback_data=MenuCallback.fill_profile)] - ) - if not db_user.is_paused: - reply_keyboard_markup.append( - [InlineKeyboardButton(text='⏸️ Поставить на паузу', callback_data=MenuCallback.pause)] +def _menu_buttons(context, member, db_user) -> InlineKeyboardMarkup: + reply_keyboard: list[list[InlineKeyboardButton]] = [] + + if db_user is not None: + reply_keyboard.append( + [InlineKeyboardButton('👤 Редактировать профиль', callback_data=MenuCallback.fill_profile)] + ) + + if not db_user.is_paused: + reply_keyboard.append( + [InlineKeyboardButton(text='⏸️ Поставить на паузу', callback_data=MenuCallback.pause)] + ) + else: + reply_keyboard.append( + [InlineKeyboardButton(text='▶️ Снять с паузы', callback_data=MenuCallback.pause)] + ) + + reply_keyboard.append( + [InlineKeyboardButton(text='💡 Отправить отзыв', callback_data=MenuCallback.send_feedback)] ) else: - reply_keyboard_markup.append( - [InlineKeyboardButton(text='▶️ Снять с паузы', callback_data=MenuCallback.pause)] + reply_keyboard.append( + [InlineKeyboardButton('👤 Заполнить профиль', callback_data=MenuCallback.fill_profile)] ) if str(member.user.id) in ADMIN_ACCOUNTS: logging.info(f'{member.user.id} is admin. adding generate pairs handler...') - reply_keyboard_markup.append( + reply_keyboard.append( [InlineKeyboardButton(text='🎲 Сгенерировать пары', callback_data=MenuCallback.generate_pairs)] ) context.application.add_handler( CallbackQueryHandler(callback=generate_pairs_handler, pattern=f"{MenuCallback.generate_pairs}") ) - reply_keyboard_markup.append( - [InlineKeyboardButton(text='💡 Отправить отзыв', callback_data=MenuCallback.send_feedback)] - ) + return InlineKeyboardMarkup(inline_keyboard=reply_keyboard) diff --git a/src/handlers/pair_handlers.py b/src/handlers/pair_handlers.py index 053758e..09bd131 100644 --- a/src/handlers/pair_handlers.py +++ b/src/handlers/pair_handlers.py @@ -16,6 +16,9 @@ async def generate_pairs(update: Update, context: CallbackContext): + query = update.callback_query + await query.answer() + logging.info('Generating pairs...') pairs: list[list] = [] @@ -67,7 +70,7 @@ async def _send_pair_messages(update, context, pair: (User, User)): chat_id=first_user_id if PROD else update.effective_user.id, text=f"Штош, @{first_user_name}!\n\n" \ f"Твоя пара на эту неделю @{second_user_name}. " \ - f"Вы оба хотели {meeting_format}.\n\n" \ + f"Вы хотели {meeting_format}.\n\n" \ f"Можно начать разговор с обсуждения интересов собеседника: {second_user_bio}" ) await context.bot.send_photo( diff --git a/src/handlers/profile_handlers.py b/src/handlers/profile_handlers.py index 5c42a57..bd060c0 100644 --- a/src/handlers/profile_handlers.py +++ b/src/handlers/profile_handlers.py @@ -24,6 +24,9 @@ # Conversation handlers async def profile_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + query = update.callback_query + await query.answer() + await context.bot.send_chat_action(chat_id=update.effective_chat.id, action=ChatAction.TYPING) logging.info('Member is fine, show keyboard..') @@ -101,7 +104,7 @@ async def meeting_format_handler(update: Update, context: CallbackContext) -> in new_keyboard = [[KeyboardButton(text='Поделиться локацией', request_location=True)]] await context.bot.send_message( chat_id=update.effective_user.id, - text='Где ты находишься? В локации будут использованы только страна и город', + text='Где ты находишься? Нажми кнопку "Поделиться локацией" снизу. \n\nИз локации будут использованы только страна и город', reply_markup=ReplyKeyboardMarkup(new_keyboard, one_time_keyboard=True, resize_keyboard=True) ) return FillProfileCallback.city @@ -178,5 +181,5 @@ async def update_user_in_db(update, context): db_helper.update_user_profile(user) await update.message.reply_text( - "Записано, жди понедельника!" + "Записано, жди понедельника! Можно вызвать меню, чтобы отредактировать профиль, поставить на паузу или оставить отзыв" ) diff --git a/src/models/static_models.py b/src/models/static_models.py index 8fa6472..fb9ee83 100644 --- a/src/models/static_models.py +++ b/src/models/static_models.py @@ -1,9 +1,9 @@ from collections import namedtuple -from dataclasses import dataclass class Command: start = 'start' + menu = 'menu' generate_pairs = 'generate' cancel = 'cancel'