i18n, messages are in templates
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Some checks reported errors
continuous-integration/drone/push Build encountered an error
This commit is contained in:
2
.env
2
.env
@@ -11,3 +11,5 @@ DATABASE_URL=mysql+pymysql://match_user:match_password_change_me@db:3306/match_a
|
||||
BOT_TOKEN=7839684534:AAGAUmO756P1T81q8ImndWgIBU9OsZkY06s
|
||||
PRIMARY_ADMIN_TELEGRAM_ID=556399210
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
BOT_LOCALE=ru
|
||||
110
services/bot/app/core/i18n.py
Normal file
110
services/bot/app/core/i18n.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
|
||||
def _lang() -> str:
|
||||
# Язык берём из переменной окружения, по умолчанию RU
|
||||
return (os.getenv("BOT_LOCALE") or "ru").lower()
|
||||
|
||||
def t(key: str, **kwargs) -> str:
|
||||
lang = _lang()
|
||||
base = LOCALES.get("ru", {})
|
||||
s = LOCALES.get(lang, {}).get(key, base.get(key, key))
|
||||
try:
|
||||
return s.format(**kwargs)
|
||||
except Exception:
|
||||
return s # на случай, если забыли передать плейсхолдеры
|
||||
|
||||
LOCALES = {
|
||||
"ru": {
|
||||
# общие
|
||||
"err.not_allowed": "Недостаточно прав.",
|
||||
"err.not_found": "Анкета не найдена.",
|
||||
"ok.done": "Готово!",
|
||||
|
||||
# /candidates и карточка
|
||||
"cand.caption": "#{id} {name} @{username}\nГород: {city} | Цель: {goal}\nСтатус: {verified} | Активна: {active}",
|
||||
"cand.verified_yes": "✅ проверена",
|
||||
"cand.verified_no": "⏳ на проверке",
|
||||
"cand.active_yes": "да",
|
||||
"cand.active_no": "нет",
|
||||
|
||||
"btn.open": "👁 Открыть",
|
||||
"btn.verify": "✅ Проверить",
|
||||
"btn.hide": "🚫 Скрыть",
|
||||
"btn.restore": "♻️ Вернуть",
|
||||
"btn.prev": "◀️ Назад",
|
||||
"btn.next": "Вперёд ▶️",
|
||||
|
||||
# /list фильтры
|
||||
"list.start_gender": "Фильтр по полу:",
|
||||
"list.gender_f": "жен",
|
||||
"list.gender_m": "муж",
|
||||
"list.skip": "Пропустить",
|
||||
"list.ask_city": "Укажи город (или напиши «пропустить»):",
|
||||
"list.ask_verified": "Показывать только проверенные?",
|
||||
"list.ask_active": "Показывать только активные?",
|
||||
"list.searching": "Подбираю анкеты по фильтрам…",
|
||||
"list.empty": "По фильтрам ничего не нашлось.",
|
||||
|
||||
# профайл/визард (пример — можешь расширить)
|
||||
"wiz.hello": "Привет! Это анкета для брачного сервиса с сопровождением до визы F‑6 🇰🇷\nКак тебя зовут?",
|
||||
"wiz.exists": "Анкета уже существует. Посмотреть — /my, редактирование — /edit (скоро), начать заново — /new",
|
||||
},
|
||||
|
||||
# ЗАГОТОВКИ под en/ko — при желании дополни
|
||||
"en": {
|
||||
"err.not_allowed": "Not allowed.",
|
||||
"err.not_found": "Profile not found.",
|
||||
"ok.done": "Done!",
|
||||
"cand.caption": "#{id} {name} @{username}\nCity: {city} | Goal: {goal}\nStatus: {verified} | Active: {active}",
|
||||
"cand.verified_yes": "✅ verified",
|
||||
"cand.verified_no": "⏳ pending",
|
||||
"cand.active_yes": "yes",
|
||||
"cand.active_no": "no",
|
||||
"btn.open": "👁 Open",
|
||||
"btn.verify": "✅ Verify",
|
||||
"btn.hide": "🚫 Hide",
|
||||
"btn.restore": "♻️ Restore",
|
||||
"btn.prev": "◀️ Prev",
|
||||
"btn.next": "Next ▶️",
|
||||
"list.start_gender": "Filter by gender:",
|
||||
"list.gender_f": "female",
|
||||
"list.gender_m": "male",
|
||||
"list.skip": "Skip",
|
||||
"list.ask_city": "City (or type 'skip'):",
|
||||
"list.ask_verified": "Only verified?",
|
||||
"list.ask_active": "Only active?",
|
||||
"list.searching": "Searching…",
|
||||
"list.empty": "No results by filters.",
|
||||
"wiz.hello": "Hi! This is the application for our marriage service (visa F‑6 support). What's your name?",
|
||||
"wiz.exists": "Your profile already exists. View: /my, edit: /edit (soon), restart: /new",
|
||||
},
|
||||
|
||||
"ko": {
|
||||
"err.not_allowed": "권한이 없습니다.",
|
||||
"err.not_found": "프로필을 찾을 수 없습니다.",
|
||||
"ok.done": "완료!",
|
||||
"cand.caption": "#{id} {name} @{username}\n도시: {city} | 목표: {goal}\n상태: {verified} | 활성: {active}",
|
||||
"cand.verified_yes": "✅ 인증됨",
|
||||
"cand.verified_no": "⏳ 검토중",
|
||||
"cand.active_yes": "예",
|
||||
"cand.active_no": "아니오",
|
||||
"btn.open": "👁 열기",
|
||||
"btn.verify": "✅ 인증",
|
||||
"btn.hide": "🚫 숨기기",
|
||||
"btn.restore": "♻️ 복구",
|
||||
"btn.prev": "◀️ 이전",
|
||||
"btn.next": "다음 ▶️",
|
||||
"list.start_gender": "성별 필터:",
|
||||
"list.gender_f": "여",
|
||||
"list.gender_m": "남",
|
||||
"list.skip": "건너뛰기",
|
||||
"list.ask_city": "도시를 입력하거나 '건너뛰기'를 입력하세요:",
|
||||
"list.ask_verified": "인증된 프로필만 표시할까요?",
|
||||
"list.ask_active": "활성 프로필만 표시할까요?",
|
||||
"list.searching": "검색 중…",
|
||||
"list.empty": "필터에 해당하는 결과가 없습니다.",
|
||||
"wiz.hello": "안녕하세요! 혼인 서비스(F‑6 비자 지원) 신청서입니다. 이름이 무엇인가요?",
|
||||
"wiz.exists": "프로필이 이미 있습니다. 보기: /my, 편집: /edit(곧), 다시 시작: /new",
|
||||
},
|
||||
}
|
||||
@@ -1,99 +1,197 @@
|
||||
from __future__ import annotations
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
|
||||
from telegram import (
|
||||
Update,
|
||||
InlineKeyboardMarkup,
|
||||
InlineKeyboardButton,
|
||||
InputMediaPhoto,
|
||||
)
|
||||
from telegram.ext import (
|
||||
ContextTypes,
|
||||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
ConversationHandler,
|
||||
MessageHandler,
|
||||
filters,
|
||||
)
|
||||
from sqlalchemy import desc
|
||||
|
||||
from app.core.db import db_session
|
||||
from app.core.i18n import t
|
||||
from app.repositories.admin_repo import AdminRepository
|
||||
from app.repositories.candidate_repo import CandidateRepository
|
||||
from app.models.candidate import Candidate
|
||||
|
||||
from app.utils.common import csv_to_list
|
||||
|
||||
async def add_admin_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Команда /addadmin <telegram_id> — добавить нового администратора (только для админов)."""
|
||||
with db_session() as s:
|
||||
repo = AdminRepository(s)
|
||||
if not repo.is_admin(update.effective_user.id):
|
||||
await update.message.reply_text("Недостаточно прав.")
|
||||
return
|
||||
PAGE_SIZE = 5 # можно менять
|
||||
|
||||
args = context.args or []
|
||||
if not args:
|
||||
await update.message.reply_text("Формат: /addadmin 123456789")
|
||||
return
|
||||
# ========================== Утилиты отрисовки ===============================
|
||||
|
||||
try:
|
||||
tid = int(args[0])
|
||||
except ValueError:
|
||||
await update.message.reply_text("Неверный telegram_id.")
|
||||
return
|
||||
def _caption(c: Candidate) -> str:
|
||||
return t(
|
||||
"cand.caption",
|
||||
id=c.id,
|
||||
name=(c.full_name or "—"),
|
||||
username=(c.username or ""),
|
||||
city=(c.city or "—"),
|
||||
goal=(c.goal or "—"),
|
||||
verified=(t("cand.verified_yes") if c.is_verified else t("cand.verified_no")),
|
||||
active=(t("cand.active_yes") if c.is_active else t("cand.active_no")),
|
||||
)
|
||||
|
||||
from app.models.admin import Admin
|
||||
if s.query(Admin).filter(Admin.telegram_id == tid).first():
|
||||
await update.message.reply_text("Этот администратор уже добавлен.")
|
||||
return
|
||||
def cand_kb(c: Candidate) -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[
|
||||
InlineKeyboardButton(t("btn.open"), callback_data=f"cand:view:{c.id}"),
|
||||
InlineKeyboardButton(t("btn.verify"), callback_data=f"cand:verify:{c.id}"),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
t("btn.hide") if c.is_active else t("btn.restore"),
|
||||
callback_data=f"cand:toggle:{c.id}",
|
||||
),
|
||||
],
|
||||
]
|
||||
return InlineKeyboardMarkup(buttons)
|
||||
|
||||
s.add(Admin(telegram_id=tid))
|
||||
s.commit()
|
||||
await update.message.reply_text(f"Администратор {tid} добавлен.")
|
||||
def pager_kb(prefix: str, page: int, has_prev: bool, has_next: bool) -> InlineKeyboardMarkup | None:
|
||||
# prefix: "candlist" или "listres"
|
||||
btns = []
|
||||
row = []
|
||||
if has_prev:
|
||||
row.append(InlineKeyboardButton(t("btn.prev"), callback_data=f"{prefix}:page:{page-1}"))
|
||||
if has_next:
|
||||
row.append(InlineKeyboardButton(t("btn.next"), callback_data=f"{prefix}:page:{page+1}"))
|
||||
if row:
|
||||
btns.append(row)
|
||||
return InlineKeyboardMarkup(btns)
|
||||
return None
|
||||
|
||||
# =================== /candidates с пагинацией ===============================
|
||||
|
||||
async def list_candidates_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Команда /candidates — показать последние анкеты (только для админов)."""
|
||||
with db_session() as s:
|
||||
admin_repo = AdminRepository(s)
|
||||
if not admin_repo.is_admin(update.effective_user.id):
|
||||
await update.message.reply_text("Недостаточно прав.")
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
await update.message.reply_text(t("err.not_allowed"))
|
||||
return
|
||||
|
||||
rows = s.query(Candidate).order_by(desc(Candidate.created_at)).limit(30).all()
|
||||
page = 0
|
||||
context.user_data["candlist_page"] = page
|
||||
total = s.query(Candidate).count()
|
||||
rows = (s.query(Candidate)
|
||||
.order_by(desc(Candidate.created_at))
|
||||
.offset(page * PAGE_SIZE).limit(PAGE_SIZE).all())
|
||||
|
||||
if not rows:
|
||||
await update.message.reply_text("Кандидатов пока нет.")
|
||||
await update.message.reply_text(t("list.empty"))
|
||||
return
|
||||
|
||||
out = []
|
||||
for c in rows:
|
||||
out.append(
|
||||
f"#{c.id} {c.full_name or '—'} | {c.city or '—'} | цель: {c.goal or '—'} | @{c.username or ''}"
|
||||
)
|
||||
await update.message.reply_text("Последние анкеты:\n" + "\n".join(out))
|
||||
if c.avatar_file_id:
|
||||
await update.message.reply_photo(photo=c.avatar_file_id, caption=_caption(c), reply_markup=cand_kb(c))
|
||||
else:
|
||||
await update.message.reply_text(text=_caption(c), reply_markup=cand_kb(c))
|
||||
|
||||
kb = pager_kb("candlist", page, has_prev=False, has_next=(total > (page + 1) * PAGE_SIZE))
|
||||
if kb:
|
||||
await update.message.reply_text(" ", reply_markup=kb) # отдельным сообщением панель пагинации
|
||||
|
||||
async def candlist_pager(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
_, _, page_str = q.data.split(":") # candlist:page:<n>
|
||||
page = max(0, int(page_str))
|
||||
context.user_data["candlist_page"] = page
|
||||
|
||||
async def verify_candidate_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Команда /verify <id> — пометить анкету как проверенную (только для админов)."""
|
||||
with db_session() as s:
|
||||
admin_repo = AdminRepository(s)
|
||||
if not admin_repo.is_admin(update.effective_user.id):
|
||||
await update.message.reply_text("Недостаточно прав.")
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
await q.edit_message_text(t("err.not_allowed"))
|
||||
return
|
||||
|
||||
args = context.args or []
|
||||
if not args:
|
||||
await update.message.reply_text("Формат: /verify 12")
|
||||
return
|
||||
total = s.query(Candidate).count()
|
||||
rows = (s.query(Candidate)
|
||||
.order_by(desc(Candidate.created_at))
|
||||
.offset(page * PAGE_SIZE).limit(PAGE_SIZE).all())
|
||||
|
||||
# Обновляем "панель" пагинации (это отдельное сообщение)
|
||||
kb = pager_kb("candlist", page, has_prev=(page > 0), has_next=(total > (page + 1) * PAGE_SIZE))
|
||||
try:
|
||||
cid = int(args[0])
|
||||
except ValueError:
|
||||
await update.message.reply_text("Укажи числовой id анкеты.")
|
||||
await q.edit_message_reply_markup(kb)
|
||||
except Exception:
|
||||
# если не панель, а текст — просто обновим текст
|
||||
await q.edit_message_text(" ", reply_markup=kb)
|
||||
|
||||
# а список карточек отправляем следующими сообщениями
|
||||
chat_id = q.message.chat.id
|
||||
for c in rows:
|
||||
if c.avatar_file_id:
|
||||
await q.bot.send_photo(chat_id=chat_id, photo=c.avatar_file_id, caption=_caption(c), reply_markup=cand_kb(c))
|
||||
else:
|
||||
await q.bot.send_message(chat_id=chat_id, text=_caption(c), reply_markup=cand_kb(c))
|
||||
|
||||
# ======================== Кнопки карточек ===================================
|
||||
|
||||
async def callback_router(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
data = q.data or ""
|
||||
if not data.startswith("cand:"):
|
||||
return
|
||||
_, action, id_str = data.split(":")
|
||||
cid = int(id_str)
|
||||
|
||||
with db_session() as s:
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
if q.message.photo:
|
||||
await q.edit_message_caption(caption=t("err.not_allowed"))
|
||||
else:
|
||||
await q.edit_message_text(t("err.not_allowed"))
|
||||
return
|
||||
|
||||
c = s.get(Candidate, cid)
|
||||
if not c:
|
||||
await update.message.reply_text("Анкета не найдена.")
|
||||
if q.message.photo:
|
||||
await q.edit_message_caption(caption=t("err.not_found"))
|
||||
else:
|
||||
await q.edit_message_text(t("err.not_found"))
|
||||
return
|
||||
|
||||
c.is_verified = True
|
||||
s.commit()
|
||||
await update.message.reply_text(f"Анкета #{cid} помечена как проверенная.")
|
||||
if action == "view":
|
||||
cap = _caption(c)
|
||||
if q.message.photo:
|
||||
await q.edit_message_caption(caption=cap, reply_markup=cand_kb(c))
|
||||
else:
|
||||
await q.edit_message_text(text=cap, reply_markup=cand_kb(c))
|
||||
return
|
||||
|
||||
async def view_candidate_handler(update, context):
|
||||
"""Команда /view <id> — показать карточку анкеты с фото (для админов)."""
|
||||
if action == "verify":
|
||||
c.is_verified = True
|
||||
s.commit()
|
||||
cap = _caption(c)
|
||||
if q.message.photo:
|
||||
await q.edit_message_caption(caption=cap, reply_markup=cand_kb(c))
|
||||
else:
|
||||
await q.edit_message_text(text=cap, reply_markup=cand_kb(c))
|
||||
return
|
||||
|
||||
if action == "toggle":
|
||||
c.is_active = not c.is_active
|
||||
s.commit()
|
||||
cap = _caption(c)
|
||||
if q.message.photo:
|
||||
await q.edit_message_caption(caption=cap, reply_markup=cand_kb(c))
|
||||
else:
|
||||
await q.edit_message_text(text=cap, reply_markup=cand_kb(c))
|
||||
return
|
||||
|
||||
# ============================= /view <id> ===================================
|
||||
|
||||
async def view_candidate_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
with db_session() as s:
|
||||
admin_repo = AdminRepository(s)
|
||||
if not admin_repo.is_admin(update.effective_user.id):
|
||||
await update.message.reply_text("Недостаточно прав.")
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
await update.message.reply_text(t("err.not_allowed"))
|
||||
return
|
||||
|
||||
args = context.args or []
|
||||
@@ -109,33 +207,165 @@ async def view_candidate_handler(update, context):
|
||||
|
||||
c = s.get(Candidate, cid)
|
||||
if not c:
|
||||
await update.message.reply_text("Анкета не найдена.")
|
||||
await update.message.reply_text(t("err.not_found"))
|
||||
return
|
||||
|
||||
# Текстовая карточка
|
||||
caption = (
|
||||
f"#{c.id} {c.full_name or '—'} @{c.username or ''}\n"
|
||||
f"Город: {c.city or '—'} | Цель: {c.goal or '—'}\n"
|
||||
f"Статус: {'✅ проверена' if c.is_verified else '⏳ на проверке'} | Активна: {'да' if c.is_active else 'нет'}"
|
||||
)
|
||||
|
||||
caption = _caption(c)
|
||||
chat_id = update.effective_chat.id
|
||||
|
||||
# Аватар (если есть)
|
||||
if c.avatar_file_id:
|
||||
await context.bot.send_photo(chat_id=chat_id, photo=c.avatar_file_id, caption=caption)
|
||||
await context.bot.send_photo(chat_id=chat_id, photo=c.avatar_file_id, caption=caption, reply_markup=cand_kb(c))
|
||||
else:
|
||||
await context.bot.send_message(chat_id=chat_id, text=caption + "\n(аватар отсутствует)")
|
||||
await context.bot.send_message(chat_id=chat_id, text=caption, reply_markup=cand_kb(c))
|
||||
|
||||
# Галерея (если есть)
|
||||
gallery_ids = csv_to_list(c.gallery_file_ids)
|
||||
if gallery_ids:
|
||||
# Telegram принимает до 10 media за раз
|
||||
chunk = []
|
||||
batch: List[InputMediaPhoto] = []
|
||||
for fid in gallery_ids:
|
||||
chunk.append(InputMediaPhoto(media=fid))
|
||||
if len(chunk) == 10:
|
||||
await context.bot.send_media_group(chat_id=chat_id, media=chunk)
|
||||
chunk = []
|
||||
if chunk:
|
||||
await context.bot.send_media_group(chat_id=chat_id, media=chunk)
|
||||
batch.append(InputMediaPhoto(media=fid))
|
||||
if len(batch) == 10:
|
||||
await context.bot.send_media_group(chat_id=chat_id, media=batch)
|
||||
batch = []
|
||||
if batch:
|
||||
await context.bot.send_media_group(chat_id=chat_id, media=batch)
|
||||
|
||||
# ===================== /list — фильтры + пагинация ==========================
|
||||
|
||||
F_GENDER, F_CITY, F_VERIFIED, F_ACTIVE = range(4)
|
||||
|
||||
@dataclass
|
||||
class ListFilters:
|
||||
gender: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
verified: Optional[bool] = None
|
||||
active: Optional[bool] = True
|
||||
|
||||
def _kb_gender() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton(t("list.gender_f"), callback_data="list:gender:f"),
|
||||
InlineKeyboardButton(t("list.gender_m"), callback_data="list:gender:m")],
|
||||
[InlineKeyboardButton(t("list.skip"), callback_data="list:gender:skip")],
|
||||
])
|
||||
|
||||
def _kb_yesno(prefix: str) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([
|
||||
[InlineKeyboardButton("Да", callback_data=f"list:{prefix}:yes"),
|
||||
InlineKeyboardButton("Нет", callback_data=f"list:{prefix}:no")],
|
||||
[InlineKeyboardButton(t("list.skip"), callback_data=f"list:{prefix}:skip")],
|
||||
])
|
||||
|
||||
async def list_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
with db_session() as s:
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
await update.message.reply_text(t("err.not_allowed"))
|
||||
return ConversationHandler.END
|
||||
context.user_data["list_filters"] = ListFilters()
|
||||
context.user_data["list_page"] = 0
|
||||
await update.message.reply_text(t("list.start_gender"), reply_markup=_kb_gender())
|
||||
return F_GENDER
|
||||
|
||||
async def list_gender(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
_, _, val = q.data.split(":") # list:gender:<val>
|
||||
lf: ListFilters = context.user_data.get("list_filters") # type: ignore
|
||||
if val == "f":
|
||||
lf.gender = t("list.gender_f")
|
||||
elif val == "m":
|
||||
lf.gender = t("list.gender_m")
|
||||
await q.edit_message_text(t("list.ask_city"),
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(t("list.skip"), callback_data="list:city:skip")]]))
|
||||
return F_CITY
|
||||
|
||||
async def list_city(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
lf: ListFilters = context.user_data.get("list_filters") # type: ignore
|
||||
if update.callback_query:
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(t("list.ask_verified"))
|
||||
await update.callback_query.message.reply_text(t("list.ask_verified"), reply_markup=_kb_yesno("verified"))
|
||||
return F_VERIFIED
|
||||
|
||||
city = (update.message.text or "").strip()
|
||||
if city.lower() != "пропустить":
|
||||
lf.city = city
|
||||
await update.message.reply_text(t("list.ask_verified"), reply_markup=_kb_yesno("verified"))
|
||||
return F_VERIFIED
|
||||
|
||||
async def list_verified(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
_, _, val = q.data.split(":")
|
||||
lf: ListFilters = context.user_data.get("list_filters") # type: ignore
|
||||
if val == "yes":
|
||||
lf.verified = True
|
||||
elif val == "no":
|
||||
lf.verified = False
|
||||
await q.edit_message_text(t("list.ask_active"))
|
||||
await q.message.reply_text(t("list.ask_active"), reply_markup=_kb_yesno("active"))
|
||||
return F_ACTIVE
|
||||
|
||||
async def list_active(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
_, _, val = q.data.split(":")
|
||||
lf: ListFilters = context.user_data.get("list_filters") # type: ignore
|
||||
if val == "yes":
|
||||
lf.active = True
|
||||
elif val == "no":
|
||||
lf.active = False
|
||||
|
||||
context.user_data["list_page"] = 0
|
||||
await q.edit_message_text(t("list.searching"))
|
||||
await _list_show_results(update, context)
|
||||
return ConversationHandler.END
|
||||
|
||||
async def _list_show_results(update: Update, context: ContextTypes.DEFAULT_TYPE, page: Optional[int] = None):
|
||||
lf: ListFilters = context.user_data.get("list_filters") # type: ignore
|
||||
page = context.user_data.get("list_page", 0) if page is None else page
|
||||
|
||||
with db_session() as s:
|
||||
if not AdminRepository(s).is_admin(update.effective_user.id):
|
||||
await context.bot.send_message(chat_id=update.effective_chat.id, text=t("err.not_allowed"))
|
||||
return
|
||||
|
||||
q = s.query(Candidate)
|
||||
if lf.gender:
|
||||
q = q.filter(Candidate.gender == lf.gender)
|
||||
if lf.city:
|
||||
q = q.filter(Candidate.city.ilike(lf.city))
|
||||
if lf.verified is not None:
|
||||
q = q.filter(Candidate.is_verified.is_(lf.verified))
|
||||
if lf.active is not None:
|
||||
q = q.filter(Candidate.is_active.is_(lf.active))
|
||||
|
||||
total = q.count()
|
||||
rows = q.order_by(desc(Candidate.created_at)).offset(page * PAGE_SIZE).limit(PAGE_SIZE).all()
|
||||
|
||||
if not rows:
|
||||
await context.bot.send_message(chat_id=update.effective_chat.id, text=t("list.empty"))
|
||||
return
|
||||
|
||||
# Отправим карточки
|
||||
for c in rows:
|
||||
cap = _caption(c)
|
||||
if c.avatar_file_id:
|
||||
await context.bot.send_photo(chat_id=update.effective_chat.id, photo=c.avatar_file_id, caption=cap, reply_markup=cand_kb(c))
|
||||
else:
|
||||
await context.bot.send_message(chat_id=update.effective_chat.id, text=cap, reply_markup=cand_kb(c))
|
||||
|
||||
# Панель пагинации результатов /list
|
||||
kb = pager_kb("listres", page, has_prev=(page > 0), has_next=(total > (page + 1) * PAGE_SIZE))
|
||||
if kb:
|
||||
await context.bot.send_message(chat_id=update.effective_chat.id, text=" ", reply_markup=kb)
|
||||
|
||||
async def list_results_pager(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
q = update.callback_query
|
||||
await q.answer()
|
||||
_, _, page_str = q.data.split(":") # listres:page:<n>
|
||||
page = max(0, int(page_str))
|
||||
context.user_data["list_page"] = page
|
||||
# просто обновляем "панель" и заново выводим карточки
|
||||
try:
|
||||
await q.edit_message_reply_markup(reply_markup=None) # убираем старую панель (по возможности)
|
||||
except Exception:
|
||||
pass
|
||||
await _list_show_results(update, context)
|
||||
|
||||
@@ -1,28 +1,64 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from telegram.ext import Application, CommandHandler
|
||||
from app.core.config import settings
|
||||
from app.handlers.conversation import build_conversation
|
||||
from app.handlers.admin import add_admin_handler, list_candidates_handler, verify_candidate_handler, view_candidate_handler
|
||||
|
||||
from app.handlers.profile import my_profile_handler, edit_hint_handler
|
||||
from app.handlers.admin import (
|
||||
list_candidates_handler, candlist_pager, callback_router,
|
||||
view_candidate_handler, list_start, list_gender, list_city, list_verified, list_active, list_results_pager
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||||
log = logging.getLogger("bot")
|
||||
|
||||
async def start_cmd(update, context):
|
||||
await update.message.reply_text("Бот запущен. Набери /new, чтобы заполнить анкету.")
|
||||
|
||||
def build_app() -> Application:
|
||||
if not settings.BOT_TOKEN:
|
||||
raise RuntimeError("BOT_TOKEN is empty. Set it in .env")
|
||||
app = Application.builder().token(settings.BOT_TOKEN).build()
|
||||
# Хендлеры
|
||||
|
||||
# визард пользователя
|
||||
app.add_handler(build_conversation())
|
||||
|
||||
# базовые
|
||||
app.add_handler(CommandHandler("start", start_cmd))
|
||||
app.add_handler(CommandHandler("my", my_profile_handler))
|
||||
app.add_handler(CommandHandler("edit", edit_hint_handler))
|
||||
app.add_handler(CommandHandler("addadmin", add_admin_handler))
|
||||
|
||||
# админские
|
||||
app.add_handler(CommandHandler("candidates", list_candidates_handler))
|
||||
app.add_handler(CommandHandler("verify", verify_candidate_handler))
|
||||
app.add_handler(CommandHandler("view", view_candidate_handler))
|
||||
app.add_handler(CommandHandler("list", list_start))
|
||||
app.add_handler(CommandHandler("verify", lambda u, c: None)) # заглушка если где-то есть старая команда
|
||||
|
||||
# callback-и
|
||||
from telegram.ext import CallbackQueryHandler, ConversationHandler, MessageHandler, filters
|
||||
app.add_handler(CallbackQueryHandler(candlist_pager, pattern=r"^candlist:page:"))
|
||||
app.add_handler(CallbackQueryHandler(callback_router, pattern=r"^cand:"))
|
||||
list_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("list", list_start)],
|
||||
states={
|
||||
0: [CallbackQueryHandler(list_gender, pattern=r"^list:gender:")],
|
||||
1: [CallbackQueryHandler(list_city, pattern=r"^list:city:"), MessageHandler(filters.TEXT & ~filters.COMMAND, list_city)],
|
||||
2: [CallbackQueryHandler(list_verified, pattern=r"^list:verified:")],
|
||||
3: [CallbackQueryHandler(list_active, pattern=r"^list:active:")],
|
||||
},
|
||||
fallbacks=[],
|
||||
allow_reentry=True,
|
||||
)
|
||||
app.add_handler(list_conv)
|
||||
app.add_handler(CallbackQueryHandler(list_results_pager, pattern=r"^listres:page:"))
|
||||
return app
|
||||
|
||||
if __name__ == "__main__":
|
||||
application = build_app()
|
||||
# В v21 это блокирующий вызов; updater больше не используется.
|
||||
application.run_polling(close_loop=False)
|
||||
try:
|
||||
log.info("Starting bot…")
|
||||
log.info("DB URL masked is set: %s", "***" if settings.DATABASE_URL else "<empty>")
|
||||
application = build_app()
|
||||
application.run_polling(close_loop=False)
|
||||
except Exception as e:
|
||||
log.exception("Bot crashed on startup: %s", e)
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user