Files
seoulmate_bot_v1/services/bot/app/handlers/conversation.py
Andrey K. Choi 9af84db429
Some checks reported errors
continuous-integration/drone/push Build encountered an error
MVP ready. Fully functional (registration? moderation, profiles vew)
2025-08-12 21:55:56 +09:00

349 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from datetime import datetime, date
from typing import List
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, InputMediaPhoto
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters
from app.core.db import db_session
from app.models.candidate import Candidate
from app.repositories.admin_repo import AdminRepository
from app.utils.common import csv_to_list, list_to_csv, calc_age
from app.services.admin_bootstrap import ensure_primary_admin
(
S_NAME, S_GENDER, S_BIRTH, S_HEIGHT, S_WEIGHT,
S_COUNTRY, S_CITY, S_CITIZEN, S_VISA, S_LANGS,
S_EDU, S_OCCUP, S_INCOME, S_RELIGION, S_MARITAL, S_CHILDREN, S_CHILDREN_NOTES,
S_SMOKING, S_ALCOHOL, S_HEALTH,
S_HOBBIES_TAGS, S_HOBBIES_FREE,
S_GOAL, S_PARTNER,
S_AVATAR, S_GALLERY, S_CONSENTS, S_CONFIRM
) = range(28)
GENDER_KB = [["жен", "муж"], ["другое"]]
YESNO_KB = [["да", "нет"]]
SMOKE_ALC_KB = [["нет", "иногда", "да"]]
MARITAL_KB = [["никогда не был(а) в браке"], ["в разводе"], ["вдова/вдовец"]]
INCOME_KB = [["до 2 млн KRW", "24 млн"], ["46 млн", "6+ млн"], ["не указывать"]]
def build_conversation() -> ConversationHandler:
return ConversationHandler(
entry_points=[CommandHandler("start", start), CommandHandler("new", start)],
states={
S_NAME: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_name)],
S_GENDER: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_gender)],
S_BIRTH: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_birth)],
S_HEIGHT: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_height)],
S_WEIGHT: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_weight)],
S_COUNTRY: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_country)],
S_CITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_city)],
S_CITIZEN: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_citizen)],
S_VISA: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_visa)],
S_LANGS: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_langs)],
S_EDU: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_edu)],
S_OCCUP: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_occup)],
S_INCOME: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_income)],
S_RELIGION: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_religion)],
S_MARITAL: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_marital)],
S_CHILDREN: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_children)],
S_CHILDREN_NOTES: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_children_notes)],
S_SMOKING: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_smoking)],
S_ALCOHOL: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_alcohol)],
S_HEALTH: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_health)],
S_HOBBIES_TAGS: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_hobbies_tags)],
S_HOBBIES_FREE: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_hobbies_free)],
S_GOAL: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_goal)],
S_PARTNER: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_partner)],
S_AVATAR: [
MessageHandler(filters.PHOTO, s_avatar),
MessageHandler(filters.TEXT & ~filters.COMMAND, s_avatar),
],
S_GALLERY: [
MessageHandler(filters.PHOTO, s_gallery),
MessageHandler(filters.TEXT & ~filters.COMMAND, s_gallery),
],
S_CONSENTS: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_consents)],
S_CONFIRM: [MessageHandler(filters.TEXT & ~filters.COMMAND, s_confirm)],
},
fallbacks=[CommandHandler("cancel", cancel)],
allow_reentry=True,
)
async def start(update, context):
with db_session() as s:
ensure_primary_admin(s)
exists = s.query(Candidate).filter(Candidate.telegram_id == update.effective_user.id).first()
if exists:
await update.message.reply_text(
"Анкета уже существует. Посмотреть — /my, редактирование — /edit (скоро), начать заново — /new",
reply_markup=ReplyKeyboardRemove(),
)
return ConversationHandler.END
await update.message.reply_text(
"Привет! Это анкета для брачного сервиса с сопровождением до визы F6 🇰🇷\nКак тебя зовут?",
reply_markup=ReplyKeyboardRemove(),
)
return S_NAME
async def s_name(update, context):
context.user_data["full_name"] = update.message.text.strip()
await update.message.reply_text("Твой пол?", reply_markup=ReplyKeyboardMarkup(GENDER_KB, one_time_keyboard=True, resize_keyboard=True))
return S_GENDER
async def s_gender(update, context):
context.user_data["gender"] = update.message.text.strip().lower()
await update.message.reply_text("Дата рождения (ДД.ММ.ГГГГ):", reply_markup=ReplyKeyboardRemove())
return S_BIRTH
async def s_birth(update, context):
try:
d = datetime.strptime(update.message.text.strip(), "%d.%m.%Y").date()
except Exception:
await update.message.reply_text("Неверный формат. Пример: 31.12.1995")
return S_BIRTH
context.user_data["birth_date"] = d
await update.message.reply_text("Рост (см):")
return S_HEIGHT
async def s_height(update, context):
try:
context.user_data["height_cm"] = float(update.message.text.replace(",", "."))
except Exception:
await update.message.reply_text("Введи число, например 168")
return S_HEIGHT
await update.message.reply_text("Вес (кг):")
return S_WEIGHT
async def s_weight(update, context):
try:
context.user_data["weight_kg"] = float(update.message.text.replace(",", "."))
except Exception:
await update.message.reply_text("Введи число, например 55")
return S_WEIGHT
await update.message.reply_text("Страна проживания:")
return S_COUNTRY
async def s_country(update, context):
context.user_data["country"] = update.message.text.strip()
await update.message.reply_text("Город:")
return S_CITY
async def s_city(update, context):
context.user_data["city"] = update.message.text.strip()
await update.message.reply_text("Гражданство (РФ/Казахстан/Узбекистан и т.п.):")
return S_CITIZEN
async def s_citizen(update, context):
context.user_data["citizenship"] = update.message.text.strip()
await update.message.reply_text("Текущий визовый статус в Корее (нет/F4/H1 и т.п.):")
return S_VISA
async def s_visa(update, context):
context.user_data["visa_status"] = update.message.text.strip()
await update.message.reply_text("Языки (через запятую, напр.: ru, ko, en):")
return S_LANGS
async def s_langs(update, context):
context.user_data["languages"] = list_to_csv(csv_to_list(update.message.text))
await update.message.reply_text("Образование (высшее/колледж/иное):")
return S_EDU
async def s_edu(update, context):
context.user_data["education"] = update.message.text.strip()
await update.message.reply_text("Профессия:")
return S_OCCUP
async def s_occup(update, context):
context.user_data["occupation"] = update.message.text.strip()
await update.message.reply_text(
"Доход (диапазон):",
reply_markup=ReplyKeyboardMarkup(INCOME_KB, one_time_keyboard=True, resize_keyboard=True),
)
return S_INCOME
async def s_income(update, context):
context.user_data["income_range"] = update.message.text.strip()
await update.message.reply_text("Религия (если нет, напиши «нет»):", reply_markup=ReplyKeyboardRemove())
return S_RELIGION
async def s_religion(update, context):
context.user_data["religion"] = update.message.text.strip()
await update.message.reply_text(
"Семейное положение:",
reply_markup=ReplyKeyboardMarkup(MARITAL_KB, one_time_keyboard=True, resize_keyboard=True),
)
return S_MARITAL
async def s_marital(update, context):
context.user_data["marital_status"] = update.message.text.strip()
await update.message.reply_text("Есть дети?", reply_markup=ReplyKeyboardMarkup(YESNO_KB, one_time_keyboard=True, resize_keyboard=True))
return S_CHILDREN
async def s_children(update, context):
ans = update.message.text.strip().lower()
context.user_data["has_children"] = (ans == "да")
if ans == "да":
await update.message.reply_text("Коротко про детей (кол-во/возраст/с кем проживают):", reply_markup=ReplyKeyboardRemove())
return S_CHILDREN_NOTES
context.user_data["children_notes"] = None
await update.message.reply_text("Курение:", reply_markup=ReplyKeyboardMarkup(SMOKE_ALC_KB, one_time_keyboard=True, resize_keyboard=True))
return S_SMOKING
async def s_children_notes(update, context):
context.user_data["children_notes"] = update.message.text.strip()
await update.message.reply_text("Курение:", reply_markup=ReplyKeyboardMarkup(SMOKE_ALC_KB, one_time_keyboard=True, resize_keyboard=True))
return S_SMOKING
async def s_smoking(update, context):
context.user_data["smoking"] = update.message.text.strip().lower()
await update.message.reply_text("Алкоголь:", reply_markup=ReplyKeyboardMarkup(SMOKE_ALC_KB, one_time_keyboard=True, resize_keyboard=True))
return S_ALCOHOL
async def s_alcohol(update, context):
context.user_data["alcohol"] = update.message.text.strip().lower()
await update.message.reply_text("Здоровье/особенности (если есть; иначе «нет»):", reply_markup=ReplyKeyboardRemove())
return S_HEALTH
async def s_health(update, context):
context.user_data["health_notes"] = update.message.text.strip()
await update.message.reply_text("Хобби/интересы (теги через запятую, напр.: пение, путешествия, kdrama):")
return S_HOBBIES_TAGS
async def s_hobbies_tags(update, context):
context.user_data["hobbies_tags"] = list_to_csv(csv_to_list(update.message.text))
await update.message.reply_text("Расскажи подробнее про интересы (свободный текст):")
return S_HOBBIES_FREE
async def s_hobbies_free(update, context):
context.user_data["hobbies_free"] = update.message.text.strip()
await update.message.reply_text("Твоя основная цель (например: «брак с гражданином Кореи (F6)»):")
return S_GOAL
async def s_goal(update, context):
context.user_data["goal"] = update.message.text.strip()
await update.message.reply_text("Пожелания к партнёру (возраст, ценности, город, религия и т.п.):")
return S_PARTNER
async def s_partner(update, context):
context.user_data["partner_prefs"] = update.message.text.strip()
await update.message.reply_text("Загрузи своё фото (аватар):")
return S_AVATAR
async def s_avatar(update, context):
if not update.message.photo:
await update.message.reply_text("Пожалуйста, пришли фотографию.")
return S_AVATAR
context.user_data["avatar_file_id"] = update.message.photo[-1].file_id
context.user_data["gallery_file_ids"] = []
await update.message.reply_text("Можно добавить до 9 дополнительных фото. Пришли их. Когда закончишь — напиши «готово».")
return S_GALLERY
async def s_gallery(update, context):
if update.message.photo:
fid = update.message.photo[-1].file_id
gallery: List[str] = context.user_data.get("gallery_file_ids", [])
if len(gallery) < 9:
gallery.append(fid)
context.user_data["gallery_file_ids"] = gallery
await update.message.reply_text(f"Добавлено фото ({len(gallery)}/9). Ещё? Или напиши «готово».")
else:
await update.message.reply_text("Лимит 9 фото достигнут. Напиши «готово».")
return S_GALLERY
if (update.message.text or "").strip().lower() != "готово":
await update.message.reply_text("Если с фото закончили — напиши «готово».")
return S_GALLERY
await update.message.reply_text("Подтверди согласие на обработку персональных данных. Напиши: «согласен» или «согласна».")
return S_CONSENTS
async def s_consents(update, context):
ans = (update.message.text or "").strip().lower()
if ans not in ("согласен", "согласна"):
await update.message.reply_text("Нужно написать «согласен» или «согласна».")
return S_CONSENTS
context.user_data["consent_personal"] = True
context.user_data["consent_policy"] = True
d = context.user_data
age = calc_age(d.get("birth_date"))
summary = (
f"Проверь данные:\n\n"
f"Имя: {d.get('full_name')}\n"
f"Пол: {d.get('gender')}\n"
f"Дата рождения: {d.get('birth_date')} (возраст: {age})\n"
f"Рост/вес: {d.get('height_cm')} см / {d.get('weight_kg')} кг\n"
f"Страна/город: {d.get('country')}, {d.get('city')}\n"
f"Гражданство: {d.get('citizenship')}\n"
f"Виза/статус: {d.get('visa_status')}\n"
f"Языки: {d.get('languages')}\n"
f"Образование/Профессия: {d.get('education')} / {d.get('occupation')}\n"
f"Доход: {d.get('income_range')}\n"
f"Религия: {d.get('religion')}\n"
f"Семейное положение: {d.get('marital_status')}\n"
f"Дети: {'да' if d.get('has_children') else 'нет'} {'(' + d.get('children_notes') + ')' if d.get('children_notes') else ''}\n"
f"Курение/Алкоголь: {d.get('smoking')} / {d.get('alcohol')}\n"
f"Здоровье: {d.get('health_notes')}\n"
f"Хобби (теги): {d.get('hobbies_tags')}\n"
f"Интересы (текст): {d.get('hobbies_free')}\n"
f"Цель: {d.get('goal')}\n"
f"Пожелания к партнёру: {d.get('partner_prefs')}\n"
f"\nЕсли всё ок — напиши «подтверждаю». Иначе /cancel и /new."
)
await update.message.reply_text(summary)
return S_CONFIRM
async def s_confirm(update, context):
if (update.message.text or "").strip().lower() != "подтверждаю":
await update.message.reply_text('Напиши «подтверждаю» для сохранения, либо /cancel.')
return S_CONFIRM
u = update.effective_user
d = context.user_data
with db_session() as s:
cand = s.query(Candidate).filter(Candidate.telegram_id == u.id).first()
if not cand:
cand = Candidate(telegram_id=u.id, username=u.username or None)
s.add(cand)
cand.full_name = d.get("full_name")
cand.gender = d.get("gender")
cand.birth_date = d.get("birth_date")
cand.height_cm = d.get("height_cm")
cand.weight_kg = d.get("weight_kg")
cand.country = d.get("country")
cand.city = d.get("city")
cand.citizenship = d.get("citizenship")
cand.visa_status = d.get("visa_status")
cand.languages = d.get("languages")
cand.education = d.get("education")
cand.occupation = d.get("occupation")
cand.income_range = d.get("income_range")
cand.religion = d.get("religion")
cand.marital_status = d.get("marital_status")
cand.has_children = d.get("has_children")
cand.children_notes = d.get("children_notes")
cand.smoking = d.get("smoking")
cand.alcohol = d.get("alcohol")
cand.health_notes = d.get("health_notes")
cand.hobbies_tags = d.get("hobbies_tags")
cand.hobbies_free = d.get("hobbies_free")
cand.goal = d.get("goal")
cand.partner_prefs = d.get("partner_prefs")
cand.avatar_file_id = d.get("avatar_file_id")
cand.gallery_file_ids = list_to_csv(d.get("gallery_file_ids", []))
cand.consent_personal = d.get("consent_personal", False)
cand.consent_policy = d.get("consent_policy", False)
cand.is_verified = False
cand.is_active = True
s.commit()
await update.message.reply_text("Готово! Анкета сохранена. Свяжемся после модерации. Спасибо!")
return ConversationHandler.END
async def cancel(update, context):
await update.message.reply_text("Ок, отменил. Для нового старта — /new")
return ConversationHandler.END