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)