refactoring: polished some files, .env.example update

This commit is contained in:
2026-05-18 17:00:17 +07:00
parent acb8662a66
commit 281a460974
14 changed files with 88 additions and 55 deletions

View File

@@ -1,25 +1,39 @@
# Telegram Bot Token (Required) # Database
# Get it from @BotFather on Telegram POSTGRES_USER=
BOT_TOKEN=your_bot_token_here POSTGRES_PASSWORD=
POSTGRES_DB=
POSTGRES_URL=postgresql+asyncpg://user:password@localhost:5432/database_name
# Remnawave Configuration (Required)
REMNAWAVE_URL=https://your-remnawave-instance.com
REMNAWAVE_TOKEN=your_remnawave_api_token_here
SUBSCRIPTION_URL=https://your-subscription-page.com
# PostgreSQL Database Configuration # Telegram bot
# These variables are used by docker-compose to set up the database container BOT_TOKEN=your_telegram_bot_token
POSTGRES_USER=malenia
POSTGRES_PASSWORD=change_me_to_a_secure_password
POSTGRES_DB=malenia_db
# Proxy Configuration (Optional) # Comma-separated list of Telegram user IDs
# Leave empty if no proxy is needed ADMINS=[123456789,987654321]
PROXY=
# Pricing and Limits Configuration (Optional - defaults provided in code) # Pally
PALLY_SHOP_ID=your_pally_shop_id
PALLY_TOKEN=your_pally_token
# Referral system
REFERAL_BONUS=10
DEPOSIT_THRESHOLD=50
# Optional proxy
PROXY=http://user:password@host:port
# Remnawave
REMNAWAVE_URL=https://example.com
SUBSCRIPTION_URL=https://example.com/subscription
REMNAWAVE_TOKEN=your_remnawave_token
# Device pricing
MIN_DEVICES=3 MIN_DEVICES=3
MAX_DEVICES=12 MAX_DEVICES=12
PER_DEVICE_COST=50 PER_DEVICE_COST=50
WHITELIST_COST=100 WHITELIST_COST=100
WHITELIST_DEVICE_THRESHOLD=6 WHITELIST_DEVICE_THRESHOLD=6
# Squads
GENERAL_SQUAD=2de0b5ea-49b9-4181-916f-ae67b9c83b80
WHITELIST_SQUAD=e4435baa-64d3-4b29-a18e-81ad1c60d977

1
.gitignore vendored
View File

@@ -130,6 +130,7 @@ celerybeat.pid
# Environments # Environments
.env .env
*.env
dev.env dev.env
.venv .venv
env/ env/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13.0

View File

@@ -3,15 +3,22 @@ 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 bot.keyboards.admins import action_confirmation
from bot.keyboards.common import return_to_menu
from bot.states.admins import AdminStorage
from bot.texts import (
ADM_PROMO_INIT,
ADM_VERIFY_PROMOTION,
CALLBACK_FALLBACK,
GENERAL_PROCESSING,
SUCCESSFULLY_SENT_PROMO,
)
from schemas.di import DependenciesDTO from schemas.di import DependenciesDTO
from schemas.promotions import NewPromotionContext from schemas.promotions import NewPromotionContext
from bot.texts import ADM_PROMO_INIT, CALLBACK_FALLBACK, GENERAL_PROCESSING, ADM_VERIFY_PROMOTION, SUCCESSFULLY_SENT_PROMO
from bot.keyboards.common import return_to_menu
from bot.keyboards.admins import action_confirmation
from bot.states.admins import AdminStorage
router = Router() router = Router()
@router.callback_query(F.data == "promotion") @router.callback_query(F.data == "promotion")
async def promo_init(cb: CallbackQuery, state: FSMContext): async def promo_init(cb: CallbackQuery, state: FSMContext):
await state.clear() await state.clear()
@@ -22,6 +29,7 @@ async def promo_init(cb: CallbackQuery, state: FSMContext):
await state.set_state(AdminStorage.promotion_msg) await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx}) await state.set_data({"ctx": ctx})
@router.message(AdminStorage.promotion_msg) @router.message(AdminStorage.promotion_msg)
async def promo_text(msg: Message, state: FSMContext): async def promo_text(msg: Message, state: FSMContext):
data = await state.get_data() data = await state.get_data()
@@ -38,8 +46,11 @@ async def promo_text(msg: Message, state: FSMContext):
await state.set_state(AdminStorage.promotion_msg) await state.set_state(AdminStorage.promotion_msg)
await state.set_data({"ctx": ctx}) await state.set_data({"ctx": ctx})
@router.callback_query(AdminStorage.promotion_msg, F.data.in_(("approve", "decline"))) @router.callback_query(AdminStorage.promotion_msg, F.data.in_(("approve", "decline")))
async def promo_send(cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO): async def promo_send(
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
):
data = await state.get_data() data = await state.get_data()
await state.clear() await state.clear()
@@ -49,4 +60,6 @@ async def promo_send(cb: CallbackQuery, state: FSMContext, session: AsyncSession
return return
successful = await deps.user_service.launch_promotion(session, ctx.promotion) successful = await deps.user_service.launch_promotion(session, ctx.promotion)
await cb.message.edit_text(SUCCESSFULLY_SENT_PROMO.format(successful=successful), reply_markup=return_to_menu) await cb.message.edit_text(
SUCCESSFULLY_SENT_PROMO.format(successful=successful), reply_markup=return_to_menu
)

View File

@@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from bot.keyboards.client import deposit_kb, devices_selector, duration_selector from bot.keyboards.client import deposit_kb, devices_selector, duration_selector
from bot.keyboards.common import return_to_menu from bot.keyboards.common import return_to_menu
from bot.states.buy import SubscriptionStorage from bot.states.buy import SubscriptionStorage
from bot.texts import ( from bot.texts import (
CALLBACK_FALLBACK, CALLBACK_FALLBACK,

View File

@@ -11,6 +11,7 @@ from bot.texts import (
FAQ_TEXT, FAQ_TEXT,
GENERAL_PROCESSING, GENERAL_PROCESSING,
GENERAL_SUCCESS, GENERAL_SUCCESS,
NO_SUBSCRIPTION,
NOTHING_PLACEHOLDER, NOTHING_PLACEHOLDER,
) )
from db.models.users import User from db.models.users import User
@@ -122,11 +123,15 @@ async def sub_info(
) )
sub = await deps.subscriptions_repository.get_subscription_by_user_id(session, cb.from_user.id) sub = await deps.subscriptions_repository.get_subscription_by_user_id(session, cb.from_user.id)
if not user: if not rw_user:
await cb.answer(CALLBACK_FALLBACK) await cb.message.edit_text(NO_SUBSCRIPTION, reply_markup=return_to_menu)
return return
if not sub: if not user:
await cb.answer("1" + CALLBACK_FALLBACK)
return
if rw_user and not sub:
try: try:
dto = SubscriptionPlan( dto = SubscriptionPlan(
devices=rw_user.hwid_device_limit, devices=rw_user.hwid_device_limit,
@@ -143,7 +148,7 @@ async def sub_info(
plan_price_paid=utils.calculate_price(dto), plan_price_paid=utils.calculate_price(dto),
) )
except Exception: except Exception:
await cb.answer(CALLBACK_FALLBACK) await cb.answer("2" + CALLBACK_FALLBACK)
return return
await cb.message.edit_text( await cb.message.edit_text(

View File

@@ -4,7 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder( main_menu: InlineKeyboardMarkup = InlineKeyboardBuilder(
[ [
[InlineKeyboardButton(text="📢 Рассылка", callback_data="promotion")], [InlineKeyboardButton(text="📢 Рассылка", callback_data="promotion")],
[InlineKeyboardButton(text="👤 Управление пользователем", callback_data="usermgmt")], # [InlineKeyboardButton(text="👤 Управление пользователем", callback_data="usermgmt")],
] ]
).as_markup() ).as_markup()

View File

@@ -4,6 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import settings from config import settings
from misc.utils import convert_duration_to_int from misc.utils import convert_duration_to_int
from schemas.subscriptions import SubscriptionDuration from schemas.subscriptions import SubscriptionDuration
from .common import _return_to_menu_btn from .common import _return_to_menu_btn
main_menu = InlineKeyboardBuilder( main_menu = InlineKeyboardBuilder(

View File

@@ -1,10 +1,6 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import settings
from misc.utils import convert_duration_to_int
from schemas.subscriptions import SubscriptionDuration
_return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton( _return_to_menu_btn: InlineKeyboardButton = InlineKeyboardButton(
text="⬅️ Назад", callback_data="menu:main" text="⬅️ Назад", callback_data="menu:main"
) )

View File

@@ -215,5 +215,7 @@ ADM_MAIN_MENU = (
) )
ADM_PROMO_INIT = "<b>📢 Введите/перешлите текст рассылки:</b>" ADM_PROMO_INIT = "<b>📢 Введите/перешлите текст рассылки:</b>"
ADM_VERIFY_PROMOTION = "<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>" ADM_VERIFY_PROMOTION = (
"<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>"
)
SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>" SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>"

View File

@@ -1,5 +1,6 @@
services: services:
postgres: postgres:
env_file: prod.env
image: postgres:16-alpine image: postgres:16-alpine
container_name: maleniabot_db container_name: maleniabot_db
environment: environment:
@@ -20,6 +21,7 @@ services:
retries: 5 retries: 5
bot: bot:
env_file: prod.env
build: build:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: maleniabot_bot container_name: maleniabot_bot

View File

@@ -6,17 +6,17 @@ echo
ruff check --fix ruff check --fix
black . black .
echo "[+] checking psql availability" # echo "[+] checking psql availability"
if ! pg_isready -h localhost -p 5432 ; then # if ! pg_isready -h localhost -p 5432 ; then
echo "psql is down." >&2 # echo "psql is down." >&2
exit 1 # exit 1
fi # fi
echo "[+] psql is available!" # echo "[+] psql is available!"
echo # echo
echo "[+] Running migrations..." echo "[+] Running migrations..."
echo echo
alembic upgrade head alembic upgrade head
echo "[+] Starting bot..." echo "[+] Starting bot..."
python main.py python -m bot.main

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timezone from datetime import UTC, datetime
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -17,7 +17,7 @@ class BalanceTXRepository:
balance_after: int, balance_after: int,
description: str, description: str,
) -> BalanceTransaction: ) -> BalanceTransaction:
created_at = datetime.now(timezone.UTC) created_at = datetime.now(UTC)
tx = BalanceTransaction( tx = BalanceTransaction(
user_id=user_id, user_id=user_id,
amount=amount, amount=amount,

View File

@@ -7,6 +7,7 @@ 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 bot.texts import NO_SUBSCRIPTION, NOTHING_PLACEHOLDER, SUBSCRIPTION_INFORMATION
from config import settings
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
@@ -14,7 +15,6 @@ from repositories.subscriptions import SubscriptionRepository
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
from schemas.users import NewUserDTO from schemas.users import NewUserDTO
from services import rw from services import rw
from config import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -168,6 +168,9 @@ class UserService:
squad_uuids=squads, squad_uuids=squads,
) )
if not rw_user:
raise Exception("RW User was NOT created, manual intervention required.")
db_subscription = await self.subscription_repository.get_subscription_by_user_id( db_subscription = await self.subscription_repository.get_subscription_by_user_id(
session, user.id session, user.id
) )
@@ -222,11 +225,7 @@ class UserService:
remaining_days = max(0, (rw_user.expire_at - datetime.datetime.now(datetime.UTC)).days) remaining_days = max(0, (rw_user.expire_at - datetime.datetime.now(datetime.UTC)).days)
return math.floor(db_sub.plan_price_paid * remaining_days / db_sub.duration_days) return math.floor(db_sub.plan_price_paid * remaining_days / db_sub.duration_days)
async def launch_promotion( async def launch_promotion(self, session: AsyncSession, promo_msg: Message) -> int:
self,
session: AsyncSession,
promo_msg: Message
) -> int:
users = await self.user_repository.get_users(session) users = await self.user_repository.get_users(session)
sent = 0 sent = 0