feat: add subscribtion scheduler with ai, pagination
This commit is contained in:
5
handlers/__init__.py
Normal file
5
handlers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .common import router as common_router
|
||||
from .registration import router as reg_router
|
||||
from .search import router as search_router
|
||||
|
||||
routers = [reg_router, common_router, search_router]
|
||||
BIN
handlers/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
handlers/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
handlers/__pycache__/common.cpython-314.pyc
Normal file
BIN
handlers/__pycache__/common.cpython-314.pyc
Normal file
Binary file not shown.
BIN
handlers/__pycache__/registration.cpython-314.pyc
Normal file
BIN
handlers/__pycache__/registration.cpython-314.pyc
Normal file
Binary file not shown.
BIN
handlers/__pycache__/search.cpython-314.pyc
Normal file
BIN
handlers/__pycache__/search.cpython-314.pyc
Normal file
Binary file not shown.
42
handlers/common.py
Normal file
42
handlers/common.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import CommandStart, Command
|
||||
from aiogram.types import Message
|
||||
from keyboards import get_start_kb, get_profile_edit_kb
|
||||
from database import Database
|
||||
|
||||
router = Router()
|
||||
db = Database("users.db")
|
||||
|
||||
@router.message(CommandStart())
|
||||
async def command_start_handler(message: Message):
|
||||
text = (
|
||||
"<b>👋 Привет! Я твой персональный агент по Kwork.</b>\n\n"
|
||||
"💻 ⚠️ <b>ВАЖНО:</b> Этот бот предназначен <b>исключительно для IT-специалистов</b>.\n\n"
|
||||
"🔍 Я мониторю биржу 24/7 и мгновенно присылаю тебе свежие заказы."
|
||||
)
|
||||
await message.answer(text, reply_markup=get_start_kb(), disable_web_page_preview=True)
|
||||
|
||||
@router.message(Command("profile"))
|
||||
async def show_profile(message: Message):
|
||||
user_data = await db.get_user(message.from_user.id)
|
||||
|
||||
if not user_data or user_data[0] is None:
|
||||
await message.answer("⚠️ Твой профиль еще не настроен. Нажми /start, чтобы начать.")
|
||||
return
|
||||
|
||||
sphere, lang, prefs = user_data
|
||||
|
||||
text = (
|
||||
"<b>👤 Твой профиль:</b>\n\n"
|
||||
f"<b>🌐 Сфера:</b> {sphere}\n"
|
||||
f"<b>🛠 Стек:</b> {lang}\n"
|
||||
f"<b>⚙️ Предпочтения:</b> {prefs}\n\n"
|
||||
"Хочешь что-то изменить? Нажми кнопку ниже."
|
||||
)
|
||||
|
||||
await message.answer(text, reply_markup=get_profile_edit_kb())
|
||||
|
||||
@router.message(Command("clear"))
|
||||
async def command_clear_handler(message: Message):
|
||||
await db.clear_sent_vacancies(message.from_user.id)
|
||||
await message.answer("🧹 <b>История отправленных вакансий очищена!</b>\n Теперь я снова смогу прислать тебе те заказы, которые ты уже видел.")
|
||||
80
handlers/registration.py
Normal file
80
handlers/registration.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from states import Registration
|
||||
from keyboards import get_spheres_kb, get_skip_kb
|
||||
from database import Database
|
||||
|
||||
router = Router()
|
||||
db = Database("users.db")
|
||||
|
||||
@router.callback_query(F.data == "subscribe")
|
||||
async def subscribe_handler(callback: CallbackQuery, state: FSMContext):
|
||||
await db.add_user(callback.from_user.id)
|
||||
await callback.message.edit_text(
|
||||
"Отлично! Давай настроим профиль.\n<b>В какой сфере IT ты работаешь?</b>",
|
||||
reply_markup=get_spheres_kb()
|
||||
)
|
||||
await state.set_state(Registration.waiting_for_sphere)
|
||||
await callback.answer()
|
||||
|
||||
@router.callback_query(Registration.waiting_for_sphere)
|
||||
async def sphere_chosen(callback: CallbackQuery, state: FSMContext):
|
||||
sphere = callback.data.split("_")[1]
|
||||
if sphere == "other":
|
||||
await callback.message.edit_text("Напиши свою сферу деятельности:")
|
||||
await state.set_state(Registration.waiting_for_custom_sphere)
|
||||
else:
|
||||
await state.update_data(sphere=sphere)
|
||||
await callback.message.edit_text(f"Выбрано: <b>{sphere}</b>\n\nКакой основной стек технологий?")
|
||||
await state.set_state(Registration.waiting_for_language)
|
||||
await callback.answer()
|
||||
|
||||
@router.message(Registration.waiting_for_custom_sphere)
|
||||
async def custom_sphere_input(message: Message, state: FSMContext):
|
||||
await state.update_data(sphere=message.text)
|
||||
await message.answer(f"Принято: <b>{message.text}</b>\n\nКакой основной стек?")
|
||||
await state.set_state(Registration.waiting_for_language)
|
||||
|
||||
@router.message(Registration.waiting_for_language)
|
||||
async def language_chosen(message: Message, state: FSMContext):
|
||||
await state.update_data(language=message.text)
|
||||
await message.answer(
|
||||
"Принято! И последнее: напиши свои <b>предпочтения по заказам</b> (фильтры).\n"
|
||||
"Например: 'чек от 5000р' или 'без правок'.\n\n"
|
||||
"<i>Если не хочешь заполнять сейчас, нажми кнопку ниже.</i>",
|
||||
reply_markup=get_skip_kb()
|
||||
)
|
||||
await state.set_state(Registration.waiting_for_preferences)
|
||||
|
||||
@router.callback_query(Registration.waiting_for_preferences, F.data == "skip_preferences")
|
||||
async def skip_preferences(callback: CallbackQuery, state: FSMContext):
|
||||
await state.update_data(preferences="Не указано")
|
||||
data = await state.get_data()
|
||||
|
||||
await db.update_user_data(callback.from_user.id, data)
|
||||
|
||||
await callback.message.edit_text(
|
||||
"✅ <b>Профиль успешно настроен!</b> (Фильтры пропущены)\n\n"
|
||||
f"🌐 Сфера: {data['sphere']}\n"
|
||||
f"🛠 Стек: {data['language']}\n"
|
||||
f"⚙️ Фильтры: {data['preferences']}"
|
||||
)
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
|
||||
@router.message(Registration.waiting_for_preferences)
|
||||
async def preferences_input(message: Message, state: FSMContext):
|
||||
await state.update_data(preferences=message.text)
|
||||
data = await state.get_data()
|
||||
|
||||
# Сохраняем в базу данных
|
||||
await db.update_user_data(message.from_user.id, data)
|
||||
|
||||
await message.answer(
|
||||
"✅ <b>Профиль успешно настроен!</b>\n\n"
|
||||
f"🌐 Сфера: {data['sphere']}\n"
|
||||
f"🛠 Стек: {data['language']}\n"
|
||||
f"⚙️ Фильтры: {data['preferences']}"
|
||||
)
|
||||
await state.clear()
|
||||
121
handlers/search.py
Normal file
121
handlers/search.py
Normal file
@@ -0,0 +1,121 @@
|
||||
import html
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from database import Database
|
||||
from kwork import get_kwork_projects
|
||||
from ai import filter_vacancies_with_ai
|
||||
from aiogram import F
|
||||
from aiogram.types import CallbackQuery
|
||||
import logging
|
||||
from keyboards import get_pagination_kb
|
||||
|
||||
router = Router()
|
||||
db = Database("users.db")
|
||||
|
||||
@router.message(Command("search"))
|
||||
async def search_projects(message: Message):
|
||||
args = message.text.split()
|
||||
start_p, end_p = 1, 1
|
||||
|
||||
if len(args) == 3:
|
||||
if args[1].isdigit() and args[2].isdigit():
|
||||
start_p, end_p = int(args[1]), int(args[2])
|
||||
elif len(args) == 2:
|
||||
if args[1].isdigit():
|
||||
start_p, end_p = int(args[1]), int(args[1])
|
||||
|
||||
user_data = await db.get_user(message.from_user.id)
|
||||
if not user_data or user_data[0] is None:
|
||||
await message.answer("⚠️ Твой профиль еще не настроен.")
|
||||
return
|
||||
|
||||
sphere, lang, prefs = user_data
|
||||
user_preferences = f"- Сфера: {sphere}\n- Стек: {lang}\n- Доп: {prefs}"
|
||||
|
||||
msg = await message.answer(f"⏳ <b>Собираю свежие проекты и анализирую их нейросетью...</b>\n<i>Это может занять около минуты.</i>")
|
||||
|
||||
try:
|
||||
raw_vacancies = await get_kwork_projects(start_page=start_p, end_page=end_p)
|
||||
|
||||
if not raw_vacancies:
|
||||
await msg.edit_text("❌ Проектов не найдено.")
|
||||
return
|
||||
|
||||
filtered_vacancies = await filter_vacancies_with_ai(raw_vacancies, user_preferences)
|
||||
|
||||
if not filtered_vacancies:
|
||||
await msg.edit_text("😔 Подходящих проектов сейчас нет.")
|
||||
return
|
||||
|
||||
await msg.delete()
|
||||
await message.answer(f"🎯 <b>Найдено {len(filtered_vacancies)} подходящих проектов:</b>")
|
||||
for vac in filtered_vacancies:
|
||||
title = html.escape(vac.get('title', 'Без названия'))
|
||||
price = html.escape(vac.get('price', 'По договоренности'))
|
||||
desc = html.escape(vac.get('description', ''))
|
||||
url = vac.get('url', '#')
|
||||
|
||||
text = (
|
||||
f"💼 <b>{title}</b>\n\n"
|
||||
f"💰 <b>Бюджет:</b> {price}\n"
|
||||
f"📝 <b>Описание:</b> {desc}...\n\n"
|
||||
f"🔗 <a href='{url}'>Смотреть на Kwork</a>"
|
||||
)
|
||||
# Отправляем, отключая превью ссылок, чтобы не захламлять чат
|
||||
await message.answer(text, disable_web_page_preview=True)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка в поиске: {e}")
|
||||
await msg.edit_text("❌ Произошла ошибка при анализе проектов. Попробуй еще раз чуть позже.")
|
||||
|
||||
async def build_pages_text(page_num: int):
|
||||
raw_vacancies = await get_kwork_projects(start_page=page_num, end_page=page_num)
|
||||
|
||||
if not raw_vacancies:
|
||||
return "❌ На этой странице проектов не найдено."
|
||||
|
||||
text = f"📂 <b>Все проекты (Страница {page_num}):</b>\n\n"
|
||||
for i, vac in enumerate(raw_vacancies, 1):
|
||||
title = html.escape(vac.get('title', 'Без названия'))
|
||||
price = html.escape(vac.get('price', 'По договоренности'))
|
||||
url = vac.get('url', '#')
|
||||
desc = html.escape(vac.get('description', ''))[:100] + "..."
|
||||
|
||||
text += f"{i}. <a href='{url}'>{title}</a>\n💰 {price}\n📝 {desc}\n\n"
|
||||
|
||||
return text
|
||||
|
||||
@router.message(Command("all"))
|
||||
async def command_all_vancancies(message: Message):
|
||||
wait_msg = await message.answer("⏳ Загружаю список проектов...")
|
||||
try:
|
||||
page = 1
|
||||
content = await build_pages_text(page)
|
||||
await wait_msg.edit_text(
|
||||
content,
|
||||
reply_markup=get_pagination_kb(page),
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in /all: {e}")
|
||||
await wait_msg.edit_text("⚠️ Ошибка при загрузке данных.")
|
||||
|
||||
# Обработка нажатий на кнопки пагинации
|
||||
@router.callback_query(F.data.startswith("browse_"))
|
||||
async def process_pagination(callback: CallbackQuery):
|
||||
page = int(callback.data.split("_")[1])
|
||||
|
||||
await callback.message.edit_text("🔄 Обновляю список...")
|
||||
|
||||
try:
|
||||
content = await build_pages_text(page)
|
||||
await callback.message.edit_text(
|
||||
content,
|
||||
reply_markup=get_pagination_kb(page),
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
await callback.answer()
|
||||
except Exception as e:
|
||||
logging.error(f"Error in pagination: {e}")
|
||||
await callback.answer("Ошибка при смене страницы", show_alert=True)
|
||||
Reference in New Issue
Block a user