diff --git a/.gitignore b/.gitignore index c41509d..abf027c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ settings.py +tests.py # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/bot.py b/bot.py index 385509a..668e26e 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,17 @@ import logging -from telegram.ext import Updater +from telegram.ext import (CommandHandler, MessageHandler, Filters, Updater, + ConversationHandler) import settings +from calorie_calculation import (calorie_calculation_start, target_selection, target_weight, + сalculate_сalorie_сount) + +from questionnaire import (questionnaire_start, questionnaire_gender, questionnaire_age, + questionnaire_height, questionnaire_current_weight, level_of_physical_activity, + data_validation, questionnaire_dontknow) + +from handlers import greet_user, unknown_command + logging.basicConfig(filename="bot.log", format='%(asctime)s - %(message)s', datefmt='%d-%b-%y %H:%M:%S', level=logging.INFO) @@ -11,6 +21,46 @@ def main(): dp = bot.dispatcher + questionnaire = ConversationHandler( + entry_points=[ + MessageHandler(Filters.regex('^(Заполнить данные)|^(Да, заполнить данные снова)'), questionnaire_start) + ], + states={ + "gender": [MessageHandler(Filters.text, questionnaire_gender)], + "age": [MessageHandler(Filters.text, questionnaire_age)], + "height": [MessageHandler(Filters.text, questionnaire_height)], + "current_weight": [MessageHandler(Filters.text, questionnaire_current_weight)], + "level_of_physical_activity": [MessageHandler(Filters.text, level_of_physical_activity)], + "data_validation": [MessageHandler(Filters.text, data_validation)] + }, + fallbacks=[ + MessageHandler(Filters.video | Filters.photo | Filters.document | Filters.location, + questionnaire_dontknow) + ] + ) + + calorie_calculation = ConversationHandler( + entry_points=[ + MessageHandler(Filters.regex('^(Расчёт калорий)'), calorie_calculation_start) + ], + states={ + "target_selection": [MessageHandler(Filters.text, target_selection)], + "target_weight": [MessageHandler(Filters.text, target_weight)], + "сalculate_сalorie_сount": [MessageHandler(Filters.text, сalculate_сalorie_сount)] + }, + fallbacks=[ + MessageHandler(Filters.video | Filters.photo | Filters.document | Filters.location, + questionnaire_dontknow) + ] + ) + + dp.add_handler(questionnaire) + dp.add_handler(calorie_calculation) + dp.add_handler(CommandHandler("start", greet_user)) + dp.add_handler(MessageHandler( + Filters.text | Filters.video | Filters.photo | Filters.document | Filters.location, + unknown_command)) + logging.info("bot started") bot.start_polling() bot.idle() diff --git a/calorie_calculation.py b/calorie_calculation.py new file mode 100644 index 0000000..53c9da4 --- /dev/null +++ b/calorie_calculation.py @@ -0,0 +1,87 @@ +from untils import (emoji_to_the_number_of, initial_keyboard, calorie_сalculation, + activity_level_multiplier, keypad_with_weight_selection, keypad_with_target_selection, + main_keyboard) +from telegram.ext import ConversationHandler +from db import db, get_or_create_user, add_or_replace_something + + +def calorie_calculation_start(update, context): + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + if 'questionnaire' not in user: + update.message.reply_text("Чтобы расчитать калорий нужно заполнить данные!", + reply_markup=initial_keyboard()) + return ConversationHandler.END + update.message.reply_text("Укажите вашу цель", + reply_markup=keypad_with_target_selection()) + return "target_selection" + + +def target_selection(update, context): + user_response = update.message.text + if "Сбросить вес" in user_response: + update.message.reply_text("Сколько кг вы хотите сбросить? Нажмите на кнопку или введите число с клавиатуры", + reply_markup=keypad_with_weight_selection()) + context.user_data["calorie_calculation"] = {"change_multiplier": -1} + return "target_weight" + elif "Набрать вес" in user_response: + update.message.reply_text("Сколько кг вы хотите набрать? Нажмите на кнопку или введите число с клавиатуры", + reply_markup=keypad_with_weight_selection()) + context.user_data["calorie_calculation"] = {"change_multiplier": 1} + return "target_weight" + elif "Сохранить вес" in user_response: + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + current_weight = user["questionnaire"][-1]["current_weight"] + context.user_data["calorie_calculation"] = {"target_weight": current_weight} + сalculate_сalorie_сount(update, context) + return ConversationHandler.END + else: + update.message.reply_text("Выберите один из вариантов", + reply_markup=keypad_with_target_selection()) + return "target_selection" + + +def target_weight(update, context): + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + current_weight = user["questionnaire"][-1]["current_weight"] + change_multiplier = context.user_data["calorie_calculation"]["change_multiplier"] + user_response = update.message.text + + if user_response.isdigit(): + delta_weight = int(user_response) + target_weight = current_weight + change_multiplier*delta_weight + elif emoji_to_the_number_of(user_response).isdigit(): + delta_weight = int(emoji_to_the_number_of(user_response)) + target_weight = current_weight + change_multiplier*delta_weight + else: + update.message.reply_text("Введите целое число кг", reply_markup=keypad_with_weight_selection()) + return "target_weight" + + if target_weight < 0: + update.message.reply_text("Желаемый вес меньше нуля", reply_markup=keypad_with_weight_selection()) + return "target_weight" + else: + context.user_data["calorie_calculation"]["target_weight"] = target_weight + сalculate_сalorie_сount(update, context) + return ConversationHandler.END + + +def сalculate_сalorie_сount(update, context): + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + user_data = user["questionnaire"][-1] + + gender = user_data["gender"] + age = user_data["age"] + height = user_data["height"] + weight = context.user_data["calorie_calculation"]["target_weight"] + level_of_physical_activity = user_data["level_of_physical_activity"] + multiplier_activity_level = activity_level_multiplier(level_of_physical_activity) + + calorie_count = calorie_сalculation( + gender, age, height, weight, multiplier_activity_level + ) + + add_or_replace_something(db, user['user_id'], "calorie_count", calorie_count) + add_or_replace_something(db, user['user_id'], "target_weight", weight) + + update.message.reply_text(f"Вам нужно потреблять {calorie_count} калорий ежедневно", reply_markup=main_keyboard()) + \ No newline at end of file diff --git a/db.py b/db.py new file mode 100644 index 0000000..efcab24 --- /dev/null +++ b/db.py @@ -0,0 +1,44 @@ +from datetime import datetime +from pymongo import MongoClient +import settings + +client = MongoClient(settings.MONGO_LINK) + +db = client[settings.MONGO_DB] + + +def get_or_create_user(db, effective_user, chat_id): + user = db.users.find_one({"user_id": effective_user.id}) + if not user: + user = { + "user_id": effective_user.id, + "first_name": effective_user.first_name, + "last_name": effective_user.last_name, + "username": effective_user.username, + "chat_id": chat_id + } + db.users.insert_one(user) + return user + + +def save_questionnaire(db, user_id, questionnaire_data): + user = db.users.find_one({"user_id": user_id}) + questionnaire_data['created'] = datetime.now() + if 'questionnaire' not in user: + db.users.update_one( + {'_id': user['_id']}, + {'$set': {'questionnaire': [questionnaire_data]}} + ) + else: + db.users.update_one( + {'_id': user['_id']}, + {'$push': {'questionnaire': questionnaire_data}} + ) + + +def add_or_replace_something(db, user_id, name, value): + user = db.users.find_one({"user_id": user_id}) + db.users.update_one( + {'_id': user['_id']}, + {'$set': {name: value}} + ) diff --git a/handlers.py b/handlers.py index e69de29..c829ae3 100644 --- a/handlers.py +++ b/handlers.py @@ -0,0 +1,19 @@ +from untils import initial_keyboard, main_keyboard, get_emoji + +from db import db, get_or_create_user + + +def greet_user(update, context): + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + if 'questionnaire' not in user: + keyboard = initial_keyboard() + else: + keyboard = main_keyboard() + update.message.reply_text( + f"Здравствуй {user['first_name']}! {get_emoji('waving_hand')}", + reply_markup=keyboard, + ) + + +def unknown_command(update, context): + update.message.reply_text(f"Неизвестная команда {get_emoji('red_question_mark')}", reply_markup=main_keyboard()) diff --git a/questionnaire.py b/questionnaire.py new file mode 100644 index 0000000..2c28793 --- /dev/null +++ b/questionnaire.py @@ -0,0 +1,105 @@ +from telegram import ParseMode, ReplyKeyboardMarkup +from telegram.ext import ConversationHandler +from untils import (data_output, gender_selection_button, main_keyboard, + get_emoji, activity_level_selection_button, + list_of_activity_levels) +from db import db, get_or_create_user, save_questionnaire + + +def questionnaire_start(update, context): + update.message.reply_text( + "Выберете пол", + reply_markup=gender_selection_button() + ) + return "gender" + + +def questionnaire_gender(update, context): + user_gender = update.message.text + if not ("Мужской" in user_gender or "Женский" in user_gender): + update.message.reply_text("Выберете пол") + return "gender" + else: + update.message.reply_text("Введите возраст") + context.user_data["questionnaire"] = {"gender": user_gender} + return "age" + + +def questionnaire_age(update, context): + user_age = update.message.text + if user_age.isdigit() is False or user_age == '0': + update.message.reply_text("Укажите корректный возраст") + return "age" + else: + update.message.reply_text("Введите рост") + context.user_data["questionnaire"]["age"] = int(user_age) + return "height" + + +def questionnaire_height(update, context): + user_height = update.message.text + if user_height.isdigit() is False or user_height == '0': + update.message.reply_text("Укажите корректный рост") + return "height" + else: + update.message.reply_text("Введите текущий вес") + context.user_data["questionnaire"]["height"] = int(user_height) + return "current_weight" + + +def questionnaire_current_weight(update, context): + user_weight = update.message.text + if user_weight.isdigit() is False or user_weight == '0': + update.message.reply_text("Укажите корректный вес") + return "current_weight" + else: + update.message.reply_text( + "Выберете уровень физической активности", + reply_markup=activity_level_selection_button() + ) + context.user_data["questionnaire"]["current_weight"] = int(user_weight) + return "level_of_physical_activity" + + +def level_of_physical_activity(update, context): + user_response = update.message.text + level_of_physical_activity = [level[0] for level in list_of_activity_levels()] + if user_response not in level_of_physical_activity: + update.message.reply_text("Выберете уровень физической активности") + return "level_of_physical_activity" + else: + context.user_data["questionnaire"]["level_of_physical_activity"] = user_response + user_text = data_output(context) + keyboard = [ + [f"Данные верны {get_emoji('check_mark')}", + f"Данные неверны. {get_emoji('cross_mark')}"] + ] + update.message.reply_text(user_text, parse_mode=ParseMode.HTML, + reply_markup=ReplyKeyboardMarkup(keyboard, one_time_keyboard=True, + resize_keyboard=True) + ) + return "data_validation" + + +def data_validation(update, context): + user = get_or_create_user(db, update.effective_user, update.message.chat.id) + user_response = update.message.text + if "Данные верны" in user_response: + save_questionnaire(db, user['user_id'], context.user_data['questionnaire']) + update.message.reply_text(f"Ваши данные сохранены {get_emoji('memo')}") + update.message.reply_text(f"Изменить данные можно в Профиле {get_emoji('bust_in_silhouette')}", + reply_markup=main_keyboard()) + return ConversationHandler.END + else: + keyboard = [ + [f"Да, заполнить данные снова {get_emoji('counterclockwise_arrows_button')}"], + [f"Нет. Вернутсья на главную {get_emoji('BACK_arrow')}"] + ] + update.message.reply_text("Ваши данные не сохранены. Заполнить данные снова?", + reply_markup=ReplyKeyboardMarkup(keyboard, one_time_keyboard=True, + resize_keyboard=True)) + return ConversationHandler.END + + +def questionnaire_dontknow(update, context): + update.message.reply_text("Некорректные данные(") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d1fde19 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +APScheduler==3.6.3 +cachetools==4.2.2 +certifi==2022.12.7 +dnspython==2.3.0 +emoji==0.5.4 +mypy==1.1.1 +mypy-extensions==1.0.0 +pymongo==4.3.3 +python-telegram-bot==13.14 +pytz==2022.7.1 +pytz-deprecation-shim==0.1.0.post0 +six==1.16.0 +tornado==6.1 +typing_extensions==4.5.0 +tzdata==2022.7 +tzlocal==4.2 \ No newline at end of file diff --git a/untils.py b/untils.py index e69de29..f4d72b2 100644 --- a/untils.py +++ b/untils.py @@ -0,0 +1,98 @@ +from telegram import ReplyKeyboardMarkup +from emoji import emojize + + +def list_of_activity_levels(): + return [ + [f"Минимальный {get_emoji('yawning_face')}"], + [f"Низкий {get_emoji('flushed_face')}"], + [f"Умеренный {get_emoji('smiling_face_with_sunglasses')}"], + [f"Высокий {get_emoji('face_with_steam_from_nose')}"], + [f"Экстремальный {get_emoji('smiling_face_with_horns')}"] + ] + + +def activity_level_multiplier(level): + if level == f"Минимальный {get_emoji('yawning_face')}": + return 1.2 + elif level == f"Низкий {get_emoji('flushed_face')}": + return 1.375 + elif level == f"Умеренный {get_emoji('smiling_face_with_sunglasses')}": + return 1.55 + elif level == f"Высокий {get_emoji('face_with_steam_from_nose')}": + return 1.7 + return 1.9 + + +def calorie_сalculation(gender: str, age: int, height: int, weight: int, activity_level_multiplier: float) -> int: + if "Женский" in gender: + calorie_count = 655.1 + (9.563*weight) + (1.85*height) - (4.676*age) + return calorie_count + calorie_count = 66.5 + (13.75*weight) + (5.003*height) - (6.775*age) + return int(calorie_count) + + +def initial_keyboard(): + return ReplyKeyboardMarkup([[f"Заполнить данные {get_emoji('pencil')}"], [f"Расчёт калорий {get_emoji('abacus')}"], + [f"Пищевая ценность {get_emoji('fire')}", f"Дейли рацион {get_emoji('receipt')}"], + [f"Рецепт блюда {get_emoji('man_cook')}", f"Профиль {get_emoji('bust_in_silhouette')}"] + ]) + + +def main_keyboard(): + return ReplyKeyboardMarkup([[f"Расчёт калорий {get_emoji('abacus')}"], + [f"Пищевая ценность {get_emoji('fire')}", f"Дейли рацион {get_emoji('receipt')}"], + [f"Рецепт блюда {get_emoji('man_cook')}", f"Профиль {get_emoji('bust_in_silhouette')}"] + ]) + + +def gender_selection_button(): + return ReplyKeyboardMarkup([ + [f"Мужской {get_emoji('man')}", f"Женский {get_emoji('woman')}"] + ], one_time_keyboard=True, resize_keyboard=True) + + +def activity_level_selection_button(): + buttons = list_of_activity_levels() + return ReplyKeyboardMarkup(buttons, one_time_keyboard=True) + + +def get_emoji(emoji_name: str): + return emojize(f":{emoji_name}:") + + +def data_output(context): + text = f"""Пол: {context.user_data['questionnaire']['gender']} +Возраст: {context.user_data['questionnaire']['age']} +Рост: {context.user_data['questionnaire']['height']} +Текцщий вес: {context.user_data['questionnaire']['current_weight']} +Уровень физической активности: {context.user_data['questionnaire']['level_of_physical_activity']} +""" + return text + + +def emoji_to_the_number_of(emoji): + if f"{get_emoji('keycap_2')}{get_emoji('keycap_0')}" in emoji: + return "20" + elif f"{get_emoji('keycap_1')}{get_emoji('keycap_5')}" in emoji: + return "15" + elif f"{get_emoji('keycap_1')}{get_emoji('keycap_0')}" in emoji: + return "10" + elif f"{get_emoji('keycap_5')}" in emoji: + return "5" + return "unknown emoji or missing emoji" + + +def keypad_with_weight_selection(): + return ReplyKeyboardMarkup([ + [f"{get_emoji('keycap_5')} кг", f"{get_emoji('keycap_1')}{get_emoji('keycap_0')} кг"], + [f"{get_emoji('keycap_1')}{get_emoji('keycap_5')} кг", f"{get_emoji('keycap_2')}{get_emoji('keycap_0')} кг"] + ], one_time_keyboard=True, resize_keyboard=True) + + +def keypad_with_target_selection(): + return ReplyKeyboardMarkup([ + [f"Сбросить вес {get_emoji('down_arrow')}"], + [f"Набрать вес {get_emoji('up_arrow')}"], + [f"Сохранить вес {get_emoji('left-right arrow')}"] + ], one_time_keyboard=True, resize_keyboard=True)