feat: Implement payment notification system and API structure
- Added notifications for successful payments in `api/core/notifications.py`. - Created main API entry point in `api/main.py` with FastAPI integration. - Established routing structure in `api/routes/__init__.py` and `api/routes/pally.py` for handling payment callbacks. - Developed billing handlers in `bot/handlers/billing.py` and `bot/handlers/buy.py` for subscription management. - Introduced state management for user interactions in `bot/states/`. - Created user interface elements in `bot/keyboards/` for navigation and payment processing. - Set up middleware for dependency injection in `bot/middlewares/di.py`. - Added support for user commands and interactions in `bot/handlers/common.py` and `bot/handlers/support.py`. - Implemented logging and error handling throughout the bot's functionality. - Created entry point script for API server in `entrypoints/api.sh`.
This commit is contained in:
41
Dockerfile.api
Normal file
41
Dockerfile.api
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Stage 1: Builder - Install dependencies
|
||||||
|
FROM python:3.13-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Install build-time dependencies required for compiling packages like asyncpg
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
libpq-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||||
|
|
||||||
|
# Stage 2: Runner - Final lightweight image
|
||||||
|
FROM python:3.13-slim AS runner
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies required by asyncpg
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq5 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy installed Python packages from the builder stage
|
||||||
|
COPY --from=builder /install /usr/local
|
||||||
|
|
||||||
|
# Copy application source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN sed -i 's/\r$//' ./entrypoints/api.sh && \
|
||||||
|
chmod +x ./entrypoints/api.sh
|
||||||
|
|
||||||
|
# Ensure the api script is executable
|
||||||
|
RUN chmod +x ./entrypoints/api.sh
|
||||||
|
|
||||||
|
# Default command runs migrations and starts the API
|
||||||
|
CMD ["sh", "./entrypoints/api.sh"]
|
||||||
18
api/core/deps.py
Normal file
18
api/core/deps.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from aiogram import Bot
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from db.session import async_session
|
||||||
|
from schemas.di import APIDependenciesDTO
|
||||||
|
|
||||||
|
|
||||||
|
def get_bot(request: Request) -> Bot:
|
||||||
|
return request.app.state.bot
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db():
|
||||||
|
async with async_session() as db:
|
||||||
|
yield db
|
||||||
|
|
||||||
|
|
||||||
|
def get_deps(request: Request) -> APIDependenciesDTO:
|
||||||
|
return request.app.state.deps
|
||||||
13
api/core/notifications.py
Normal file
13
api/core/notifications.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import Bot
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
|
||||||
|
SUCCESSFUL_PAYMENT = "<b>💸 Зачислен платёж на {amount}₽</b>"
|
||||||
|
|
||||||
|
close_kb: InlineKeyboardMarkup = InlineKeyboardBuilder(
|
||||||
|
[[InlineKeyboardButton(text="❌", callback_data="delete")]]
|
||||||
|
).as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
async def successful_payment_notification(bot: Bot, user_id: int, amount: int):
|
||||||
|
await bot.send_message(user_id, SUCCESSFUL_PAYMENT.format(amount=amount), reply_markup=close_kb)
|
||||||
52
api/main.py
Normal file
52
api/main.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from api.routes import routers
|
||||||
|
from config import settings
|
||||||
|
from repositories.bills import BillsRepository
|
||||||
|
from repositories.users import UserRepository
|
||||||
|
from schemas.di import APIDependenciesDTO
|
||||||
|
from services.billing_service import BillingService
|
||||||
|
from services.user_service import UserService
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||||
|
# Bot Init
|
||||||
|
bot = Bot(token=settings.bot_token)
|
||||||
|
app.state.bot = bot
|
||||||
|
|
||||||
|
user_repository = UserRepository()
|
||||||
|
user_service = UserService(user_repository)
|
||||||
|
|
||||||
|
bills_repository = BillsRepository()
|
||||||
|
bills_service = BillingService(
|
||||||
|
user_repository=user_repository, bills_repository=bills_repository
|
||||||
|
)
|
||||||
|
|
||||||
|
deps_dto = APIDependenciesDTO(
|
||||||
|
user_repository=user_repository,
|
||||||
|
user_service=user_service,
|
||||||
|
bills_repository=bills_repository,
|
||||||
|
billing_service=bills_service,
|
||||||
|
)
|
||||||
|
app.state.deps = deps_dto
|
||||||
|
|
||||||
|
for r in routers:
|
||||||
|
app.include_router(r)
|
||||||
|
|
||||||
|
yield # App's lifespan
|
||||||
|
|
||||||
|
await bot.session.close() # D IE!!!!!!!!
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Malenia API",
|
||||||
|
description="Payment webhook handler and user balance management",
|
||||||
|
version="1.0.0",
|
||||||
|
lifespan=lifespan,
|
||||||
|
docs_url="",
|
||||||
|
)
|
||||||
3
api/routes/__init__.py
Normal file
3
api/routes/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .pally import router as pally_router
|
||||||
|
|
||||||
|
routers = [pally_router]
|
||||||
78
api/routes/pally.py
Normal file
78
api/routes/pally.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from fastapi import Depends, Form, HTTPException
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from api.core.deps import get_bot, get_db, get_deps
|
||||||
|
from api.core.notifications import successful_payment_notification
|
||||||
|
from config import settings
|
||||||
|
from misc.pally import BillStatus
|
||||||
|
from schemas.di import APIDependenciesDTO
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/checkout/pally")
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/callback")
|
||||||
|
async def pally_callback(
|
||||||
|
id: str = Form(...),
|
||||||
|
bill_id: str = Form(...),
|
||||||
|
status: str = Form(...),
|
||||||
|
req_amount: str = Form(...),
|
||||||
|
currency: str = Form(...),
|
||||||
|
order_id: str | None = Form(None),
|
||||||
|
signature: str = Form(...),
|
||||||
|
session: AsyncSession = Depends(get_db),
|
||||||
|
bot: Bot = Depends(get_bot),
|
||||||
|
deps: APIDependenciesDTO = Depends(get_deps),
|
||||||
|
):
|
||||||
|
oid = order_id or ""
|
||||||
|
raw_string = f"{id}{status}{req_amount}{currency}{oid}{settings.pally_token}"
|
||||||
|
md5_hash = hashlib.md5(raw_string.encode("utf-8")).hexdigest()
|
||||||
|
expected_signature = hashlib.sha1(md5_hash.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
if signature != expected_signature:
|
||||||
|
logger.critical("Invalid signature for bill %s", bill_id)
|
||||||
|
raise HTTPException(403, detail="Invalid signature.")
|
||||||
|
|
||||||
|
if status != BillStatus.SUCCESS or not req_amount.isdigit():
|
||||||
|
logger.info("Bill %s skipped: status=%s, req_amount=%s", bill_id, status, req_amount)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
logger.info("Received successfully paid bill %s", bill_id)
|
||||||
|
|
||||||
|
if not oid.isdigit():
|
||||||
|
logger.critical("Invalid order_id format for bill %s", bill_id)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
bill = await deps.bills_repository.get_bill_by_id(session, int(oid))
|
||||||
|
|
||||||
|
if not bill:
|
||||||
|
logger.critical("Bill %s is not found in db", bill_id)
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
try:
|
||||||
|
amount = int(req_amount)
|
||||||
|
|
||||||
|
await deps.user_repository.increase_user_balance(session, bill.creator_id, amount=amount)
|
||||||
|
await successful_payment_notification(bot, bill.creator_id, amount)
|
||||||
|
|
||||||
|
referal = bill.user.referal_id
|
||||||
|
if referal is not None:
|
||||||
|
referal_amount = math.floor(amount * (settings.referal_bonus / 100))
|
||||||
|
await deps.user_repository.increase_user_balance(session, referal, referal_amount)
|
||||||
|
await successful_payment_notification(bot, referal, referal_amount)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Exception caught while processing balances for bill %s (Creator: %s)",
|
||||||
|
bill_id,
|
||||||
|
bill.creator_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
@@ -5,11 +5,11 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from keyboards.client import pay_url, return_to_menu
|
from bot.keyboards.client import pay_url, return_to_menu
|
||||||
|
from bot.states.buy import BillingStorage
|
||||||
|
from bot.texts import BILL_CREATED, BILL_CREATION, CALLBACK_FALLBACK
|
||||||
from schemas.billing import BillingContext, BillingProviders
|
from schemas.billing import BillingContext, BillingProviders
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from states.buy import BillingStorage
|
|
||||||
from texts import BILL_CREATED, BILL_CREATION, CALLBACK_FALLBACK
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -30,7 +30,9 @@ async def pally_init(
|
|||||||
|
|
||||||
ctx.provider = BillingProviders.PALLY
|
ctx.provider = BillingProviders.PALLY
|
||||||
|
|
||||||
await cb.message.edit_text(BILL_CREATION.format(amount=ctx.amount, provider=ctx.provider.value.title()))
|
await cb.message.edit_text(
|
||||||
|
BILL_CREATION.format(amount=ctx.amount, provider=ctx.provider.value.title())
|
||||||
|
)
|
||||||
|
|
||||||
bill = await deps.billing_service.pally_initiate_payment(
|
bill = await deps.billing_service.pally_initiate_payment(
|
||||||
session, deps.pally_client, user_id=cb.from_user.id, amount=ctx.amount
|
session, deps.pally_client, user_id=cb.from_user.id, amount=ctx.amount
|
||||||
@@ -5,23 +5,14 @@ from aiogram import F, Router
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
|
|
||||||
from config import settings
|
from bot.keyboards.client import (
|
||||||
from keyboards.client import (
|
|
||||||
devices_selector,
|
devices_selector,
|
||||||
duration_selector,
|
duration_selector,
|
||||||
payment_gateways,
|
payment_gateways,
|
||||||
return_to_menu,
|
return_to_menu,
|
||||||
)
|
)
|
||||||
from misc.utils import (
|
from bot.states.buy import BillingStorage, SubscriptionStorage
|
||||||
calculate_price,
|
from bot.texts import (
|
||||||
convert_duration_to_int,
|
|
||||||
convert_int_to_duration,
|
|
||||||
get_discount,
|
|
||||||
)
|
|
||||||
from schemas.billing import BillingContext
|
|
||||||
from schemas.subscriptions import SubscriptionPlan
|
|
||||||
from states.buy import BillingStorage, SubscriptionStorage
|
|
||||||
from texts import (
|
|
||||||
CALLBACK_FALLBACK,
|
CALLBACK_FALLBACK,
|
||||||
CHECKOUT_PAYMENT_CHOICE,
|
CHECKOUT_PAYMENT_CHOICE,
|
||||||
CONTEXT_REINITIATED,
|
CONTEXT_REINITIATED,
|
||||||
@@ -30,6 +21,15 @@ from texts import (
|
|||||||
SUBSCRIPTION_DEVICE_SELECTOR,
|
SUBSCRIPTION_DEVICE_SELECTOR,
|
||||||
SUBSCRIPTION_DURATION_SELECTOR,
|
SUBSCRIPTION_DURATION_SELECTOR,
|
||||||
)
|
)
|
||||||
|
from config import settings
|
||||||
|
from misc.utils import (
|
||||||
|
calculate_price,
|
||||||
|
convert_duration_to_int,
|
||||||
|
convert_int_to_duration,
|
||||||
|
get_discount,
|
||||||
|
)
|
||||||
|
from schemas.billing import BillingContext
|
||||||
|
from schemas.subscriptions import SubscriptionPlan
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -2,7 +2,7 @@ from aiogram import F, Router
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
|
||||||
from texts import UNDEFINED_MESSAGE
|
from bot.texts import UNDEFINED_MESSAGE
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -4,11 +4,11 @@ from aiogram.fsm.context import FSMContext
|
|||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from keyboards.client import close_kb, main_menu, return_to_menu
|
from bot.keyboards.client import close_kb, main_menu, return_to_menu
|
||||||
|
from bot.texts import FAQ_TEXT, GENERAL_PROCESSING
|
||||||
from misc.utils import build_main_menu_text, format_subscription_link
|
from misc.utils import build_main_menu_text, format_subscription_link
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from services.rw import get_user_by_telegram_id
|
from services.rw import get_user_by_telegram_id
|
||||||
from texts import FAQ_TEXT, GENERAL_PROCESSING
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -4,12 +4,10 @@ from aiogram.types import CallbackQuery
|
|||||||
from aiogram.utils.deep_linking import create_start_link
|
from aiogram.utils.deep_linking import create_start_link
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from keyboards.client import referals_kb
|
from bot.keyboards.client import referals_kb
|
||||||
|
from bot.texts import REFERALS_MENU
|
||||||
from misc.utils import format_share_deep_link
|
from misc.utils import format_share_deep_link
|
||||||
from schemas.di import DependenciesDTO
|
from schemas.di import DependenciesDTO
|
||||||
from texts import (
|
|
||||||
REFERALS_MENU,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -5,8 +5,8 @@ from aiogram.filters import Command
|
|||||||
from aiogram.fsm.context import FSMContext
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import CallbackQuery, Message
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
|
||||||
from keyboards.client import support_url_kb
|
from bot.keyboards.client import support_url_kb
|
||||||
from texts import SUPPORT_INIT
|
from bot.texts import SUPPORT_INIT
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
0
bot/keyboards/__init__.py
Normal file
0
bot/keyboards/__init__.py
Normal file
@@ -6,10 +6,10 @@ from aiogram.client.default import DefaultBotProperties
|
|||||||
from aiogram.client.session.aiohttp import AiohttpSession
|
from aiogram.client.session.aiohttp import AiohttpSession
|
||||||
from remnawave import RemnawaveSDK
|
from remnawave import RemnawaveSDK
|
||||||
|
|
||||||
|
from bot.handlers import routers
|
||||||
|
from bot.middlewares import DIMiddleware
|
||||||
from config import settings
|
from config import settings
|
||||||
from db.session import async_session
|
from db.session import async_session
|
||||||
from handlers import routers
|
|
||||||
from middlewares import DIMiddleware
|
|
||||||
from misc.pally import PallyClient
|
from misc.pally import PallyClient
|
||||||
from repositories.bills import BillsRepository
|
from repositories.bills import BillsRepository
|
||||||
from repositories.users import UserRepository
|
from repositories.users import UserRepository
|
||||||
0
bot/states/__init__.py
Normal file
0
bot/states/__init__.py
Normal file
12
config.py
12
config.py
@@ -6,19 +6,23 @@ load_dotenv(override=True)
|
|||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
bot_token: str = Field(alias="BOT_TOKEN")
|
|
||||||
postgres_url: str = Field(
|
postgres_url: str = Field(
|
||||||
"postgresql+asyncpg://malenia:skibidi@localhost:5432/malenia_db", alias="POSTGRES_URL"
|
"postgresql+asyncpg://malenia:skibidi@localhost:5432/malenia_db", alias="POSTGRES_URL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bot_token: str = Field(alias="BOT_TOKEN")
|
||||||
|
|
||||||
|
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
||||||
|
pally_token: str = Field(alias="PALLY_TOKEN")
|
||||||
|
|
||||||
|
referal_bonus: int = Field(10, alias="REFERAL_BONUS")
|
||||||
|
|
||||||
proxy: str | None = Field(None, alias="PROXY")
|
proxy: str | None = Field(None, alias="PROXY")
|
||||||
|
|
||||||
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
remnawave_url: str = Field(alias="REMNAWAVE_URL")
|
||||||
subscription_url: str = Field(alias="SUBSCRIPTION_URL")
|
subscription_url: str = Field(alias="SUBSCRIPTION_URL")
|
||||||
remnawave_token: str = Field(alias="REMNAWAVE_TOKEN")
|
remnawave_token: str = Field(alias="REMNAWAVE_TOKEN")
|
||||||
|
|
||||||
pally_shop_id: str = Field(alias="PALLY_SHOP_ID")
|
|
||||||
pally_token: str = Field(alias="PALLY_TOKEN")
|
|
||||||
|
|
||||||
min_devices: int = Field(3, alias="MIN_DEVICES")
|
min_devices: int = Field(3, alias="MIN_DEVICES")
|
||||||
max_devices: int = Field(12, alias="MAX_DEVICES")
|
max_devices: int = Field(12, alias="MAX_DEVICES")
|
||||||
per_device_cost: int = Field(50, alias="PER_DEVICE_COST")
|
per_device_cost: int = Field(50, alias="PER_DEVICE_COST")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
bot:
|
bot:
|
||||||
build:
|
build:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: maleniabot_app
|
container_name: maleniabot_bot
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -43,5 +43,30 @@ services:
|
|||||||
PALLY_SHOP_ID: ${PALLY_SHOP_ID}
|
PALLY_SHOP_ID: ${PALLY_SHOP_ID}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
dockerfile: Dockerfile.api
|
||||||
|
container_name: maleniabot_api
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
BOT_TOKEN: ${BOT_TOKEN}
|
||||||
|
POSTGRES_URL: postgresql+asyncpg://${POSTGRES_USER:-malenia}:${POSTGRES_PASSWORD:-malenia_password}@postgres:5432/${POSTGRES_DB:-malenia_db}
|
||||||
|
PROXY: ${PROXY:-}
|
||||||
|
REMNAWAVE_URL: ${REMNAWAVE_URL}
|
||||||
|
SUBSCRIPTION_URL: ${SUBSCRIPTION_URL}
|
||||||
|
REMNAWAVE_TOKEN: ${REMNAWAVE_TOKEN}
|
||||||
|
MIN_DEVICES: ${MIN_DEVICES:-3}
|
||||||
|
MAX_DEVICES: ${MAX_DEVICES:-12}
|
||||||
|
PER_DEVICE_COST: ${PER_DEVICE_COST:-50}
|
||||||
|
WHITELIST_COST: ${WHITELIST_COST:-100}
|
||||||
|
WHITELIST_DEVICE_THRESHOLD: ${WHITELIST_DEVICE_THRESHOLD:-6}
|
||||||
|
PALLY_TOKEN: ${PALLY_TOKEN}
|
||||||
|
PALLY_SHOP_ID: ${PALLY_SHOP_ID}
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
5
entrypoints/api.sh
Normal file
5
entrypoints/api.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[+] Starting API server..."
|
||||||
|
python -m uvicorn api.main:app --host 0.0.0.0 --port 8000
|
||||||
@@ -6,4 +6,4 @@ echo
|
|||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
echo "[+] Starting bot..."
|
echo "[+] Starting bot..."
|
||||||
python main.py
|
python -m bot.main
|
||||||
@@ -3,7 +3,7 @@ import urllib.parse
|
|||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
import texts
|
from bot import texts
|
||||||
from config import settings
|
from config import settings
|
||||||
from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan
|
from schemas.subscriptions import SubscriptionDuration, SubscriptionPlan
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ ignore = [
|
|||||||
"PLR0913",
|
"PLR0913",
|
||||||
"RUF001",
|
"RUF001",
|
||||||
"RUF002",
|
"RUF002",
|
||||||
"RUF003"
|
"RUF003",
|
||||||
|
"B008"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
|
|||||||
@@ -8,3 +8,6 @@ python-dotenv
|
|||||||
aiohttp-socks
|
aiohttp-socks
|
||||||
alembic
|
alembic
|
||||||
remnawave>=2.6.1
|
remnawave>=2.6.1
|
||||||
|
fastapi>=0.100.0
|
||||||
|
uvicorn[standard]>=0.24.0
|
||||||
|
python-multipart
|
||||||
@@ -17,3 +17,11 @@ class DependenciesDTO:
|
|||||||
bills_repository: BillsRepository
|
bills_repository: BillsRepository
|
||||||
billing_service: BillingService
|
billing_service: BillingService
|
||||||
pally_client: PallyClient
|
pally_client: PallyClient
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class APIDependenciesDTO:
|
||||||
|
user_repository: UserRepository
|
||||||
|
user_service: UserService
|
||||||
|
bills_repository: BillsRepository
|
||||||
|
billing_service: BillingService
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ from aiogram.types import Message
|
|||||||
from remnawave import RemnawaveSDK
|
from remnawave import RemnawaveSDK
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from bot.texts import (
|
||||||
|
NO_SUBSCRIPTION,
|
||||||
|
NOTHING_PLACEHOLDER,
|
||||||
|
SUBSCRIPTION_INFORMATION,
|
||||||
|
)
|
||||||
from db.models.users import User
|
from db.models.users import User
|
||||||
from misc import utils
|
from misc import utils
|
||||||
from repositories import UserRepository
|
from repositories import UserRepository
|
||||||
from schemas.users import NewUserDTO
|
from schemas.users import NewUserDTO
|
||||||
from services.rw import RWUserInfo, get_user_by_telegram_id
|
from services.rw import RWUserInfo, get_user_by_telegram_id
|
||||||
from texts import (
|
|
||||||
NO_SUBSCRIPTION,
|
|
||||||
NOTHING_PLACEHOLDER,
|
|
||||||
SUBSCRIPTION_INFORMATION,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user