first commit

This commit is contained in:
VPN SaaS Dev
2026-05-12 03:52:13 +09:00
commit d93c88c751
44 changed files with 4108 additions and 0 deletions

1
bot/__init__.py Normal file
View File

@@ -0,0 +1 @@

40
bot/api_client.py Normal file
View File

@@ -0,0 +1,40 @@
from typing import Any
import httpx
from app.core.config import settings
class ApiClient:
def __init__(self) -> None:
self.base_url = settings.api_base_url.rstrip("/")
async def upsert_user(self, telegram_user: Any) -> dict[str, Any]:
payload = {
"telegram_id": telegram_user.id,
"username": telegram_user.username,
"first_name": telegram_user.first_name,
"last_name": telegram_user.last_name,
}
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.post("/api/users", json=payload)
response.raise_for_status()
return response.json()
async def list_cars(self, owner_id: int) -> list[dict[str, Any]]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.get("/api/cars", params={"owner_id": owner_id})
response.raise_for_status()
return response.json()
async def create_car(self, owner_id: int, name: str) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.post("/api/cars", json={"owner_id": owner_id, "name": name})
response.raise_for_status()
return response.json()
async def stats(self, car_id: int) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.get(f"/api/cars/{car_id}/stats")
response.raise_for_status()
return response.json()

114
bot/main.py Normal file
View File

@@ -0,0 +1,114 @@
import asyncio
import logging
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command, CommandObject
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
Message,
ReplyKeyboardMarkup,
WebAppInfo,
)
from app.core.config import settings
from bot.api_client import ApiClient
logging.basicConfig(level=logging.INFO)
dp = Dispatcher()
api = ApiClient()
def main_keyboard() -> ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Открыть гараж", web_app=WebAppInfo(url=settings.webapp_url))],
[KeyboardButton(text="Мои авто"), KeyboardButton(text="Помощь")],
],
resize_keyboard=True,
)
@dp.message(Command("start"))
async def start(message: Message) -> None:
user = await api.upsert_user(message.from_user)
text = (
f"Готово, {user.get('first_name') or 'водитель'}.\n\n"
"Здесь можно вести заправки, обслуживание, ремонты и смотреть стоимость владения. "
"Основная работа идет в mini app, а бот остается быстрым входом."
)
await message.answer(text, reply_markup=main_keyboard())
@dp.message(Command("add_car"))
async def add_car(message: Message, command: CommandObject) -> None:
user = await api.upsert_user(message.from_user)
name = command.args.strip() if command.args else ""
if not name:
await message.answer("Напиши так: /add_car Toyota Camry")
return
car = await api.create_car(user["id"], name)
await message.answer(f"Добавил авто: {car['name']}")
@dp.message(Command("cars"))
@dp.message(F.text == "Мои авто")
async def cars(message: Message) -> None:
user = await api.upsert_user(message.from_user)
items = await api.list_cars(user["id"])
if not items:
await message.answer("Автомобилей пока нет. Добавь через mini app или командой /add_car Название.")
return
buttons = [
[InlineKeyboardButton(text=car["name"], callback_data=f"stats:{car['id']}")] for car in items
]
await message.answer("Твой гараж:", reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@dp.callback_query(F.data.startswith("stats:"))
async def show_stats(callback: CallbackQuery) -> None:
car_id = int(callback.data.split(":", 1)[1])
stats = await api.stats(car_id)
consumption = stats["avg_consumption_l_per_100km"]
cost_per_km = stats["cost_per_km"]
await callback.message.answer(
"\n".join(
[
"Статистика авто:",
f"Расходы всего: {stats['total_cost']}",
f"Топливо: {stats['fuel_cost']}",
f"Сервис и ремонты: {stats['service_cost']}",
f"Пробег по записям: {stats['distance_km']} км",
f"Средний расход: {consumption:.2f} л/100 км" if consumption else "Средний расход: нет данных",
f"Стоимость 1 км: {cost_per_km:.2f}" if cost_per_km else "Стоимость 1 км: нет данных",
]
)
)
await callback.answer()
@dp.message(F.text == "Помощь")
@dp.message(Command("help"))
async def help_message(message: Message) -> None:
await message.answer(
"Команды:\n"
"/add_car Название - быстро добавить авто\n"
"/cars - список авто и статистика\n\n"
"Заправки, ремонты и обслуживание удобнее вести через кнопку «Открыть гараж».",
reply_markup=main_keyboard(),
)
async def main() -> None:
if not settings.bot_token:
raise RuntimeError("BOT_TOKEN is empty")
bot = Bot(settings.bot_token)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())