first commit

This commit is contained in:
2026-05-25 01:12:43 +03:00
commit bfc22efe24
83 changed files with 8903 additions and 0 deletions
View File
View File
+221
View File
@@ -0,0 +1,221 @@
# Aiogram
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
from aiogram.exceptions import TelegramBadRequest
# Const
from create_bot import orm
# Keyboards
from keyboards.admin.main_kbs import *
# States
from states.admin_states import AdminStates, AdminBlacklistStates
# Another
from contextlib import suppress
# Init
admin_blacklist_router = Router()
@admin_blacklist_router.message(
F.text == "🚫 Черный список", StateFilter(AdminStates.main)
)
@admin_blacklist_router.message(F.text == "↩️ Назад", StateFilter(AdminBlacklistStates))
async def cmd_blacklist(message: types.Message, state: FSMContext):
msg_text = "🚫 Выберите действие:"
await message.answer(text=msg_text, reply_markup=get_blacklist_kb())
await state.set_state(AdminBlacklistStates.main)
# *############################
# *# ADD #
# *############################
@admin_blacklist_router.message(
F.text == " Добавить", StateFilter(AdminBlacklistStates.main)
)
async def cmd_blacklist_add(message: types.Message, state: FSMContext):
msg_text = f" Введите User ID:"
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminBlacklistStates.add_blacklist)
@admin_blacklist_router.message(F.text, StateFilter(AdminBlacklistStates.add_blacklist))
async def cmd_blacklist_add_finish(message: types.Message, state: FSMContext):
# validation
if not message.text.isdigit():
await message.answer(
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
)
return
user_id = int(message.text)
if not await orm.is_user_exists(user_id):
await message.answer(
text="⛔️ Пользователь не существует в БД! Повторите попытку:",
reply_markup=get_back_kb(),
)
return
await orm.create_blacklist(user_id=user_id)
await message.answer(text=f"✅ Черный список обновлен!")
await cmd_blacklist(message, state)
# *############################
# *# DEL #
# *############################
@admin_blacklist_router.message(
F.text == " Удалить", StateFilter(AdminBlacklistStates.main)
)
async def cmd_blacklist_delete(message: types.Message, state: FSMContext):
msg_text = " Введите User ID:"
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminBlacklistStates.del_blacklist)
@admin_blacklist_router.message(F.text, StateFilter(AdminBlacklistStates.del_blacklist))
async def cmd_blacklist_delete_finish(message: types.Message, state: FSMContext):
# validation
if not message.text.isdigit():
await message.answer(
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
)
return
user_id = int(message.text)
if not await orm.is_blacklisted(user_id):
await message.answer(
text="⛔️ Пользователь не найден в ЧС! Повторите попытку:",
reply_markup=get_back_kb(),
)
return
await orm.delete_blacklist(user_id=user_id)
await message.answer(text=f"✅ Черный список обновлен!")
await cmd_blacklist(message, state)
# *############################
# *# LIST #
# *############################
@admin_blacklist_router.message(
F.text == "👁 Открыть список", StateFilter(AdminBlacklistStates.main)
)
async def cmd_blacklist_list(message: types.Message, state: FSMContext):
await state.update_data(blacklist_offset=0)
items = await orm.get_all_blacklist()
if not items:
await message.answer(text="💭 Список пуст.")
return
offset = 0
max_offset = len(items) // 10 + (1 if len(items) % 10 != 0 else 0)
msg_text = f"<b>🚫 Черный список {offset + 1}/{max_offset}</b>\n\n"
for item in items[offset * 10 : (offset + 1) * 10]:
msg_text += f"✦ <code>{item}</code>\n"
await message.answer(
text=msg_text,
reply_markup=get_bookList_ikb(
prefix="admin_blacklist",
offset=0,
max_offset=max_offset,
items=[],
element_col=10,
),
)
async def cmd_blacklist_list_query(query: types.CallbackQuery, state: FSMContext):
data = await state.get_data()
offset = data.get("blacklist_offset")
items = await orm.get_all_blacklist()
if not items:
await query.answer(text="💭 Список пуст.")
return
max_offset = len(items) // 10 + (1 if len(items) % 10 != 0 else 0)
if offset < 0:
offset = max_offset - 1
await state.update_data(blacklist_offset=offset)
elif offset >= max_offset:
offset = 0
await state.update_data(blacklist_offset=offset)
msg_text = f"<b>🚫 Черный список {offset + 1}/{max_offset}</b>\n\n"
for item in items[offset * 10 : (offset + 1) * 10]:
msg_text += f"✦ <code>{item}</code>\n"
with suppress(TelegramBadRequest):
await query.message.edit_text(
text=msg_text,
reply_markup=get_bookList_ikb(
prefix="admin_blacklist",
offset=offset,
max_offset=max_offset,
items=[],
element_col=10,
),
)
await query.answer()
@admin_blacklist_router.callback_query(
F.data == "admin_blacklist_next", StateFilter(AdminBlacklistStates.main)
)
@admin_blacklist_router.callback_query(
F.data == "admin_blacklist_prev", StateFilter(AdminBlacklistStates.main)
)
@admin_blacklist_router.callback_query(
F.data == "admin_blacklist_status", StateFilter(AdminBlacklistStates.main)
)
async def cmd_blacklist_list_actions(query: types.CallbackQuery, state: FSMContext):
state_data = await state.get_data()
if query.data.endswith("next"):
await state.update_data(
blacklist_offset=state_data.get("blacklist_offset", 0) + 1
)
elif query.data.endswith("prev"):
await state.update_data(
blacklist_offset=state_data.get("blacklist_offset", 0) - 1
)
await cmd_blacklist_list_query(query, state)
+53
View File
@@ -0,0 +1,53 @@
# Aiogram
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
# Const
from create_bot import tz, orm
# States
from states.admin_states import AdminStates
# Another
import shutil, os
from openpyxl import load_workbook
# Init
list_of_users_router = Router()
@list_of_users_router.message(
F.text == "📑 Список пользователей", StateFilter(AdminStates.main)
)
async def cmd_list_of_users(message: types.Message, state: FSMContext):
# copy the table
table_path = shutil.copy(
src="templates/users.xlsx", dst=f"templates/users_list.xlsx"
)
# load table
book = load_workbook(filename=table_path)
sheet = book["users"]
all_clients = await orm.get_all_users()
for row, user in enumerate(all_clients, 2):
sheet.cell(row=row, column=1, value=user.user_id)
sheet.cell(row=row, column=2, value=user.username)
sheet.cell(row=row, column=3, value=user.fullname)
sheet.cell(
row=row,
column=4,
value=user.register_date.astimezone(tz).strftime(r"%d-%m-%y %H:%M %Z"),
)
book.save(table_path)
await message.answer_document(document=types.FSInputFile(table_path))
if os.path.exists(table_path):
os.remove(table_path)
+133
View File
@@ -0,0 +1,133 @@
# Aiogram imports
import logging
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
# Const
from create_bot import bot, orm
# Keyboards
from keyboards.admin.mailer_kbs import *
# Utils
from utils.text_tools import parse_links_to_inline_markup
# States
from states.admin_states import AdminStates, AdminMailerStates
# Funcs
from handlers.admin.main import show_admin_menu
admin_mailer_router = Router()
logger = logging.getLogger(__name__)
@admin_mailer_router.message(F.text == "✉️ Рассылка", StateFilter(AdminStates.main))
@admin_mailer_router.message(F.text == "↩️ Назад", StateFilter(AdminMailerStates))
async def process_mailer_post(message: types.Message, state: FSMContext):
msg_text = "✉️ Отправьте пост одним сообщением:"
await message.answer(text=msg_text, reply_markup=get_back_to_main_kb())
await state.set_state(AdminMailerStates.post)
@admin_mailer_router.message(StateFilter(AdminMailerStates.post))
async def process_mailer_ikb(message: types.Message, state: FSMContext):
await state.update_data(admin_mailer_post=message.message_id)
msg_text = """✉️ Введите кнопки:
<blockquote>Отправьте ссылку(и) в формате:
[Текст кнопки + ссылка]
Пример:
[Переводчик + https://t.me/TransioBot]
Чтобы добавить несколько кнопок в один ряд, пишите ссылки рядом с предыдущими.
Формат:
[Первый текст + первая ссылка][Второй текст + вторая ссылка]
Чтобы добавить несколько кнопок в строчку, пишите новые ссылки с новой строки.
Формат:
[Первый текст + первая ссылка]
[Второй текст + вторая ссылка]</blockquote>"""
await message.answer(
text=msg_text, reply_markup=get_skip_kb(), disable_web_page_preview=True
)
await state.set_state(AdminMailerStates.ikb)
@admin_mailer_router.message(F.text, StateFilter(AdminMailerStates.ikb))
async def process_mailer_preview(message: types.Message, state: FSMContext):
ikb = (
parse_links_to_inline_markup(message.text)
if message.text != "↪️ Пропустить"
else None
)
await state.update_data(admin_mailer_ikb=ikb)
state_data = await state.get_data()
post = state_data.get("admin_mailer_post")
await message.answer(text="✉️ Предпросмотр:", reply_markup=get_mailer_finish_kb())
try:
await bot.copy_message(
chat_id=message.from_user.id,
from_chat_id=message.from_user.id,
message_id=post,
reply_markup=get_mailer_btn_ikb(buttons_preset=ikb),
)
except Exception:
logger.exception("Mailer preview failed")
await message.answer(text="🔴 Ошибка!")
await process_mailer_post(message, state)
return
await state.set_state(AdminMailerStates.preview)
@admin_mailer_router.message(
F.text == "🟢 Начать рассылку", StateFilter(AdminMailerStates.preview)
)
async def process_mailer_finish(message: types.Message, state: FSMContext):
state_data = await state.get_data()
ikb = state_data.get("admin_mailer_ikb")
post = state_data.get("admin_mailer_post")
all_users = await orm.get_all_user_ids()
# info
await message.answer(text="▶️✉️ Рассылка запущена...")
await state.clear()
# back to main menu
await show_admin_menu(message, state)
counter = 0
for user_id in all_users:
try:
await bot.copy_message(
chat_id=user_id,
from_chat_id=message.from_user.id,
message_id=post,
reply_markup=get_mailer_btn_ikb(buttons_preset=ikb),
)
counter += 1
except Exception:
logger.exception("Mailer copy failed for user_id=%s", user_id)
await message.answer(
text=f"✅ Рассылка завершена! Сообщение отправлено {counter}/{len(all_users)}."
)
+67
View File
@@ -0,0 +1,67 @@
# Aiogram
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import Command, StateFilter
from aiogram import Router, F
# Const
from create_bot import orm
# Keyboards
from keyboards.admin.main_kbs import *
# States
from states.admin_states import (
AdminStates,
AdminMailerStates,
AdminManagementStates,
AdminSettingsStates,
AdminBlacklistStates,
)
# Funcs
from handlers.start import cmd_start
# Init
admin_main_router = Router()
@admin_main_router.message(Command("admin"), StateFilter("*"))
async def cmd_login_as_admin(message: types.Message, state: FSMContext):
if message.chat.type != "private":
return
is_admin_exists = await orm.is_admin_exists(user_id=message.from_user.id)
if is_admin_exists:
await show_admin_menu(message, state)
else:
await message.answer(text="🤨")
@admin_main_router.message(F.text == "🔚 Выйти", StateFilter(AdminStates.main))
async def cmd_admin_exit(message: types.Message, state: FSMContext):
await message.answer(text="🚪⠀", reply_markup=types.ReplyKeyboardRemove())
await cmd_start(message, state)
@admin_main_router.message(
F.text == "↩️ Вернуться в меню",
StateFilter(
AdminManagementStates.main,
AdminMailerStates.post,
AdminSettingsStates.main,
AdminBlacklistStates.main,
),
)
async def show_admin_menu(message: types.Message, state: FSMContext):
msg_text = "👮‍♂️ Вы находитесь в админ-панели"
await message.answer(text=msg_text, reply_markup=get_main_menu_kb())
await state.set_state(AdminStates.main)
+142
View File
@@ -0,0 +1,142 @@
# Aiogram imports
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
from aiogram.exceptions import TelegramBadRequest
# Const
from create_bot import bot, storage, StorageKey, orm
# Keyboards
from keyboards.admin.main_kbs import *
# States
from states.admin_states import AdminStates, AdminManagementStates
# Config
from decouple import config
# Another
from contextlib import suppress
# Init
admin_management_router = Router()
@admin_management_router.message(
F.text == "👮‍♂️ Управление админами", StateFilter(AdminStates.main)
)
@admin_management_router.message(
F.text == "↩️ Назад", StateFilter(AdminManagementStates)
)
async def cmd_management(message: types.Message, state: FSMContext):
admins = await orm.get_all_admins()
msg_text = "<i>👮‍♂️ Действующие администраторы</i>\n"
for admin in admins:
msg_text += f"✦ [<code>{admin.user_id}</code>]: {admin.username if admin.username else admin.fullname}\n"
msg_text += f"\n<b>🔽 Выберите действие:</b>"
await message.answer(text=msg_text, reply_markup=get_add_admins_kb())
await state.set_state(AdminManagementStates.main)
# *############################
# *# ADD #
# *############################
@admin_management_router.message(
F.text == " Добавить", StateFilter(AdminManagementStates.main)
)
async def cmd_management_add_id(message: types.Message, state: FSMContext):
msg_text = "➕ Введите User ID нового админа:"
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminManagementStates.add_admin)
@admin_management_router.message(F.text, StateFilter(AdminManagementStates.add_admin))
async def cmd_management_add_finish(message: types.Message, state: FSMContext):
# validation
if not message.text.isdigit():
await message.answer(
text="⛔️ Только цифры! Повторите попытку:", reply_markup=get_back_kb()
)
return
user_id = int(message.text)
if not await orm.is_user_exists(user_id):
await message.answer(
text="⛔️ Пользователь не существует в БД! Повторите попытку:",
reply_markup=get_back_kb(),
)
return
user = await orm.get_user(user_id)
await orm.create_admin(user.user_id, user.username, user.fullname)
await message.answer("✅ Успешно!")
await cmd_management(message, state)
# *############################
# *# DELETE #
# *############################
@admin_management_router.message(
F.text == " Удалить", StateFilter(AdminManagementStates.main)
)
async def cmd_management_delete(message: types.Message, state: FSMContext):
msg_text = "➖ Введите ID админа для удаления:"
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminManagementStates.del_admin)
@admin_management_router.message(F.text, StateFilter(AdminManagementStates.del_admin))
async def cmd_management_delete_finish(message: types.Message, state: FSMContext):
# validation
if not message.text.isdigit():
await message.answer(text="⛔️ Только цифры! Повторите попытку:")
return
user_id = int(message.text)
if user_id == int(config("BASE_ADMIN")):
await message.answer(
text="⛔️ Отказано! Повторите попытку:", reply_markup=get_back_kb()
)
return
if not await orm.is_admin_exists(user_id):
await message.answer(text="⛔️ Админ не найден! Повторите попытку:")
return
# change admin state
with suppress(TelegramBadRequest):
await bot.send_message(
chat_id=user_id,
text="☹️ Вы больше не являетесь админом!",
reply_markup=types.ReplyKeyboardRemove(),
)
await storage.set_state(
key=StorageKey(bot_id=bot.id, chat_id=user_id, user_id=user_id), state=None
)
await orm.delete_admin(user_id)
await message.answer("✅ Успешно!")
await cmd_management(message, state)
+85
View File
@@ -0,0 +1,85 @@
# Aiogram imports
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
# Const
from create_bot import orm
# Keyboards
from keyboards.admin.main_kbs import *
# States
from states.admin_states import AdminStates, AdminSettingsStates
# Init
admin_settings_router = Router()
@admin_settings_router.message(F.text == "↩️ Назад", StateFilter(AdminSettingsStates))
@admin_settings_router.message(F.text == "⚙️ Настройки", StateFilter(AdminStates.main))
async def cmd_settings(message: types.Message, state: FSMContext):
msg_text = "⚙️ Выберите, что хотите изменить:"
await message.answer(text=msg_text, reply_markup=get_settings_kb())
await state.set_state(AdminSettingsStates.main)
# *############################
# *# EDIT PHOTO #
# *############################
@admin_settings_router.message(
F.text.in_({"🖼 ..."}), StateFilter(AdminSettingsStates.main)
)
async def cmd_edit_photo(message: types.Message, state: FSMContext):
x = {"🖼 ...": "..."}
setting_key = x.get(message.text)
await state.update_data(setting_key=setting_key)
photo = await orm.get_setting_value(setting_key)
msg_text = f"""<b>Текущее значение:</b>
<blockquote>{photo}</blockquote>
⌨️ Отправьте фото для изменения:"""
if photo:
await message.answer_photo(
photo=photo, caption=msg_text, reply_markup=get_back_kb()
)
else:
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminSettingsStates.edit_photo)
@admin_settings_router.message(F.photo, StateFilter(AdminSettingsStates.edit_photo))
async def cmd_edit_photo_setup(message: types.Message, state: FSMContext):
photo = message.photo[-1].file_id
state_data = await state.get_data()
setting_key = state_data.get("setting_key")
await orm.update_setting_value(setting_key, photo)
msg_text = f"""<b>Текущее значение:</b>
<blockquote>{photo}</blockquote>
⌨️ Отправьте фото для изменения:"""
if photo:
await message.answer_photo(
photo=photo, caption=msg_text, reply_markup=get_back_kb()
)
else:
await message.answer(text=msg_text, reply_markup=get_back_kb())
await state.set_state(AdminSettingsStates.edit_photo)
+28
View File
@@ -0,0 +1,28 @@
# Aiogram
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import StateFilter
from aiogram import Router, F
# Const
from create_bot import orm
# States
from states.admin_states import AdminStates
# Init
admin_statistic_router = Router()
@admin_statistic_router.message(
F.text == "📊 Статистика", StateFilter(AdminStates.main)
)
async def cmd_statistic(message: types.Message, state: FSMContext):
users_count = await orm.get_users_count()
msg_text = f"""<i>📊 Статистика</i>
🔹 Кол-во пользователей в боте: {users_count:,} чел."""
await message.answer(text=msg_text)
View File
+506
View File
@@ -0,0 +1,506 @@
import aiogram.types as types
from aiogram import F, Router
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from create_bot import orm, tz
from keyboards.reply_keyboards import (
CATEGORY_BY_TEXT,
USER_TYPE_BY_TEXT,
USER_TYPE_LABEL_BY_CODE,
get_back_to_menu_kb,
get_categories_kb,
get_consultation_actions_ikb,
get_consultations_ikb,
get_delete_profile_kb,
get_main_menu_kb,
get_profile_kb,
get_user_types_kb,
)
from states.client_states import AskQuestionStates, MainStates, ProfileStates
from utils.rag_api import ask_rag_answer
from utils.text_tools import format_llm_answer_html, split_plain_text_chunks, to_html
from datetime import datetime, timezone
client_router = Router()
DAILY_CONSULTATION_LIMIT = 5
CONSULTATION_MESSAGE_LIMIT = 5
def get_today_start_utc() -> datetime:
now_local = datetime.now(tz)
start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
return start_local.astimezone(timezone.utc)
def build_start_text() -> str:
return (
"Здравствуйте. Я юридический ИИ-консультант по законам РФ.\n\n"
"Я могу помочь:\n"
"— разобраться в ситуации;\n"
"— найти применимые нормы закона;\n"
"— объяснить их простыми словами;\n"
"— дать базовый план действий.\n\n"
"Ответ носит информационный характер и не заменяет консультацию юриста."
)
def build_help_text() -> str:
return (
"ℹ️ Как пользоваться ботом\n\n"
"1. Нажмите «Задать вопрос».\n"
"2. Выберите категорию.\n"
"3. Опишите ситуацию.\n"
"4. Укажите регион РФ.\n"
"5. Получите ответ с найденными источниками.\n\n"
"Пример хорошего вопроса:\n"
"«Работодатель не выплатил зарплату за апрель. Работаю официально, Москва. Что делать?»\n\n"
"Бот не заменяет юриста и не гарантирует результат спора."
)
def build_profile_text(user) -> str:
return (
"👤 Профиль\n\n"
f"Страна: {to_html(user.country or 'Россия')}\n"
f"Регион: {to_html(user.region or 'не указан')}\n"
f"Тип пользователя: {to_html(USER_TYPE_LABEL_BY_CODE.get(user.user_type, 'Физлицо'))}"
)
def build_consultations_text(consultations: list) -> str:
lines = ["📚 Ваши консультации:\n"]
for index, consultation in enumerate(consultations, start=1):
title = consultation.title or consultation.category
lines.append(
f"{index}. {to_html(title)}{consultation.updated_at.astimezone(tz).strftime('%d.%m.%Y')}"
)
return "\n".join(lines)
def build_consultation_text(consultation, messages: list) -> str:
lines = [
f"📚 Консультация #{consultation.id}",
"",
f"Категория: {to_html(consultation.category)}",
f"Регион: {to_html(consultation.region or 'не указан')}",
f"Статус: {to_html(consultation.status)}",
"",
]
if not messages:
lines.append("Сообщений пока нет.")
return "\n".join(lines)
for message in messages[-6:]:
if message.role == "user":
title = "👤 Вопрос"
elif message.role == "assistant":
title = "🤖 Ответ"
else:
title = "️ Сообщение"
lines.append(title)
if message.role == "assistant":
lines.append(format_llm_answer_html(message.content[:1800]))
else:
lines.append(to_html(message.content[:1800]))
lines.append("")
return "\n".join(lines).strip()
def build_fallback_error_text(error_text: str) -> str:
if "No reliable law chunks" in error_text:
return (
"Я не нашел в базе надежную норму по этому вопросу. "
"Попробуйте уточнить ситуацию, добавить детали или выбрать другую категорию."
)
return error_text
async def show_main_menu(message: types.Message, state: FSMContext) -> None:
await message.answer(build_start_text(), reply_markup=get_main_menu_kb())
await state.set_state(MainStates.main)
async def process_question(
message: types.Message,
state: FSMContext,
*,
category: str,
question: str,
region: str,
consultation_id: int | None = None,
) -> None:
user = await orm.get_user(message.from_user.id)
if user is None:
await show_main_menu(message, state)
return
if consultation_id is None:
consultations_today = await orm.count_user_consultations_since(
user_id=message.from_user.id,
since=get_today_start_utc(),
)
if consultations_today >= DAILY_CONSULTATION_LIMIT:
await message.answer(
"На сегодня достигнут лимит: 5 новых консультаций. Попробуйте завтра.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
else:
message_count = await orm.count_user_messages_in_consultation(consultation_id)
if message_count >= CONSULTATION_MESSAGE_LIMIT:
await message.answer(
"В этой консультации уже достигнут лимит: 5 сообщений пользователя. "
"Создайте новую консультацию.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
history = []
if consultation_id is not None:
history = await orm.get_consultation_messages(consultation_id)
await state.set_state(AskQuestionStates.processing)
await message.answer("⏳ Ищу подходящие нормы закона и готовлю ответ...")
try:
response = await ask_rag_answer(
user_id=message.from_user.id,
consultation_id=consultation_id,
question=question,
category=category,
region=region,
user_type=user.user_type,
history=history,
)
except RuntimeError as exc:
await message.answer(
build_fallback_error_text(str(exc)),
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
if user.region != region:
await orm.set_user_region(message.from_user.id, region)
raw_answer_text = response.get("answer", "Не удалось сформировать ответ.")
answer_chunks = [
format_llm_answer_html(chunk)
for chunk in split_plain_text_chunks(raw_answer_text)
]
saved_consultation_id = response.get("consultation_id")
for index, answer_chunk in enumerate(answer_chunks):
reply_markup = None
if index == len(answer_chunks) - 1 and saved_consultation_id is not None:
reply_markup = get_consultation_actions_ikb(saved_consultation_id)
await message.answer(answer_chunk, reply_markup=reply_markup)
await message.answer(
"Можете задать новый вопрос или открыть историю консультаций.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
@client_router.message(F.text == "↩️ В меню", StateFilter("*"))
async def return_to_main_menu(message: types.Message, state: FSMContext):
await show_main_menu(message, state)
@client_router.message(F.text == "️ Помощь", StateFilter("*"))
async def show_help(message: types.Message, state: FSMContext):
await message.answer(build_help_text(), reply_markup=get_main_menu_kb())
await state.set_state(MainStates.main)
@client_router.message(F.text == "⚖️ Задать вопрос", StateFilter("*"))
async def ask_question_entry(message: types.Message, state: FSMContext):
await message.answer(
"Выберите категорию вопроса:",
reply_markup=get_categories_kb(),
)
await state.set_state(AskQuestionStates.choosing_category)
@client_router.message(
F.text.in_(list(CATEGORY_BY_TEXT.keys())),
AskQuestionStates.choosing_category,
)
async def set_question_category(message: types.Message, state: FSMContext):
await state.update_data(
category_text=message.text,
)
await message.answer(
"Опишите ситуацию одним сообщением.\n\n"
"Постарайтесь указать:\n"
"— что произошло;\n"
"— когда произошло;\n"
"с кем спор;\n"
"— чего вы хотите добиться.",
reply_markup=get_back_to_menu_kb(),
)
await state.set_state(AskQuestionStates.waiting_question)
@client_router.message(AskQuestionStates.waiting_question)
async def set_question_text(message: types.Message, state: FSMContext):
question = (message.text or "").strip()
if not question:
await message.answer("Опишите ситуацию текстом одним сообщением.")
return
user = await orm.get_user(message.from_user.id)
await state.update_data(question=question)
if user and user.region:
await message.answer(
"Укажите регион РФ, где произошла ситуация.\n\n"
f"Сейчас в профиле указан регион: {to_html(user.region)}\n"
"Если он подходит, отправьте символ: -",
reply_markup=get_back_to_menu_kb(),
)
else:
await message.answer(
"Укажите регион РФ, где произошла ситуация.\n\n"
"Например:\n"
"Москва\n"
"Санкт-Петербург\n"
"Краснодарский край\n"
"Республика Татарстан",
reply_markup=get_back_to_menu_kb(),
)
await state.set_state(AskQuestionStates.waiting_region)
@client_router.message(AskQuestionStates.waiting_region)
async def set_region_and_process(message: types.Message, state: FSMContext):
payload = await state.get_data()
user = await orm.get_user(message.from_user.id)
region = (message.text or "").strip()
if region == "-" and user and user.region:
region = user.region
if not region:
await message.answer("Укажите регион РФ текстом.")
return
await process_question(
message,
state,
category=payload["category_text"],
question=payload["question"],
region=region,
)
@client_router.message(F.text == "📚 Мои консультации", StateFilter("*"))
async def show_consultations(message: types.Message, state: FSMContext):
consultations = await orm.list_user_consultations(message.from_user.id)
if not consultations:
await message.answer(
"У вас пока нет консультаций.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
await message.answer(
build_consultations_text(consultations),
reply_markup=get_consultations_ikb(consultations),
)
await state.set_state(MainStates.main)
@client_router.callback_query(F.data.startswith("consultation:open:"))
async def open_consultation(callback: types.CallbackQuery, state: FSMContext):
consultation_id = int(callback.data.rsplit(":", 1)[-1])
consultation = await orm.get_consultation(consultation_id, callback.from_user.id)
if consultation is None:
await callback.answer("Консультация не найдена.", show_alert=True)
return
messages = await orm.get_consultation_messages(consultation_id)
await callback.message.answer(
build_consultation_text(consultation, messages),
reply_markup=get_consultation_actions_ikb(consultation_id),
)
await callback.answer()
await state.set_state(MainStates.main)
@client_router.callback_query(F.data.startswith("consultation:continue:"))
async def continue_consultation(callback: types.CallbackQuery, state: FSMContext):
consultation_id = int(callback.data.rsplit(":", 1)[-1])
consultation = await orm.get_consultation(consultation_id, callback.from_user.id)
if consultation is None:
await callback.answer("Консультация не найдена.", show_alert=True)
return
await state.update_data(
consultation_id=consultation.id,
category_text=consultation.category,
region=consultation.region,
)
await callback.message.answer(
"Напишите следующий вопрос по этой консультации одним сообщением.",
reply_markup=get_back_to_menu_kb(),
)
await callback.answer()
await state.set_state(AskQuestionStates.waiting_continue_question)
@client_router.callback_query(F.data.startswith("consultation:delete:"))
async def delete_consultation(callback: types.CallbackQuery, state: FSMContext):
consultation_id = int(callback.data.rsplit(":", 1)[-1])
await orm.delete_consultation(consultation_id, callback.from_user.id)
consultations = await orm.list_user_consultations(callback.from_user.id)
await callback.answer("Консультация удалена.")
if not consultations:
await callback.message.answer(
"Консультаций больше не осталось.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
await callback.message.answer(
build_consultations_text(consultations),
reply_markup=get_consultations_ikb(consultations),
)
await state.set_state(MainStates.main)
@client_router.message(AskQuestionStates.waiting_continue_question)
async def process_continue_question(message: types.Message, state: FSMContext):
question = (message.text or "").strip()
if not question:
await message.answer("Напишите вопрос текстом одним сообщением.")
return
payload = await state.get_data()
consultation_id = payload.get("consultation_id")
consultation = await orm.get_consultation(consultation_id, message.from_user.id)
if consultation is None:
await message.answer(
"Консультация не найдена. Начните новую.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
return
await process_question(
message,
state,
category=consultation.category,
question=question,
region=consultation.region or payload.get("region"),
consultation_id=consultation.id,
)
@client_router.message(F.text == "👤 Профиль", StateFilter("*"))
async def show_profile(message: types.Message, state: FSMContext):
user = await orm.get_user(message.from_user.id)
if user is None:
await show_main_menu(message, state)
return
await message.answer(build_profile_text(user), reply_markup=get_profile_kb())
await state.set_state(MainStates.main)
@client_router.message(F.text == "🌍 Изменить регион", StateFilter("*"))
async def change_region_start(message: types.Message, state: FSMContext):
await message.answer(
"Напишите новый регион РФ одним сообщением.",
reply_markup=get_back_to_menu_kb(),
)
await state.set_state(ProfileStates.waiting_region)
@client_router.message(ProfileStates.waiting_region)
async def change_region_finish(message: types.Message, state: FSMContext):
region = (message.text or "").strip()
if not region:
await message.answer("Регион не должен быть пустым.")
return
await orm.set_user_region(message.from_user.id, region)
user = await orm.get_user(message.from_user.id)
await message.answer(build_profile_text(user), reply_markup=get_profile_kb())
await state.set_state(MainStates.main)
@client_router.message(F.text == "👔 Изменить тип пользователя", StateFilter("*"))
async def change_user_type_start(message: types.Message, state: FSMContext):
await message.answer(
"Выберите тип пользователя:",
reply_markup=get_user_types_kb(),
)
await state.set_state(ProfileStates.waiting_user_type)
@client_router.message(
F.text.in_(list(USER_TYPE_BY_TEXT.keys())),
ProfileStates.waiting_user_type,
)
async def change_user_type_finish(message: types.Message, state: FSMContext):
await orm.set_user_type(message.from_user.id, USER_TYPE_BY_TEXT[message.text])
user = await orm.get_user(message.from_user.id)
await message.answer(build_profile_text(user), reply_markup=get_profile_kb())
await state.set_state(MainStates.main)
@client_router.message(F.text == "🗑 Удалить мои данные", StateFilter("*"))
async def delete_profile_start(message: types.Message, state: FSMContext):
await message.answer(
"Удалить историю консультаций и сбросить профиль?",
reply_markup=get_delete_profile_kb(),
)
await state.set_state(ProfileStates.confirm_delete)
@client_router.message(F.text == "✅ Да, удалить", ProfileStates.confirm_delete)
async def delete_profile_finish(message: types.Message, state: FSMContext):
await orm.delete_user(message.from_user.id)
await orm.create_user(
user_id=message.from_user.id,
username="@" + message.from_user.username if message.from_user.username else None,
fullname=to_html(message.from_user.full_name),
register_date=datetime.now(timezone.utc),
)
await message.answer(
"Данные удалены. История очищена, профиль сброшен.",
reply_markup=get_main_menu_kb(),
)
await state.set_state(MainStates.main)
@client_router.message(MainStates.main)
async def main_menu_fallback(message: types.Message):
await message.answer(
"Выберите действие в меню ниже.",
reply_markup=get_main_menu_kb(),
)
+59
View File
@@ -0,0 +1,59 @@
# Aiogram
import aiogram.types as types
from aiogram.fsm.context import FSMContext
from aiogram.filters import CommandStart, StateFilter
from aiogram import Router, F
# Utils
from utils.text_tools import to_html
# Const
from create_bot import orm
# Keyboards
from keyboards.reply_keyboards import get_main_menu_kb
# States
from states.client_states import MainStates
# Another
from datetime import datetime, timezone
# Init
start_router = Router()
@start_router.message(CommandStart(), StateFilter("*"))
async def cmd_start(message: types.Message, state: FSMContext):
if message.chat.type != "private":
return
user_id = message.from_user.id
username = (
"@" + message.from_user.username
if message.from_user.username is not None
else None
)
fullname = to_html(message.from_user.full_name)
await orm.create_user(
user_id=user_id,
username=username,
fullname=fullname,
register_date=datetime.now(timezone.utc),
)
msg_text = (
"Здравствуйте. Я юридический ИИ-консультант по законам РФ.\n\n"
"Я могу помочь:\n"
"— разобраться в ситуации;\n"
"— найти применимые нормы закона;\n"
"— объяснить их простыми словами;\n"
"— дать базовый план действий.\n\n"
"Ответ носит информационный характер и не заменяет консультацию юриста."
)
await message.answer(text=msg_text, reply_markup=get_main_menu_kb())
await state.set_state(MainStates.main)