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)
# Get it from @BotFather on Telegram
BOT_TOKEN=your_bot_token_here
# Database
POSTGRES_USER=
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
# These variables are used by docker-compose to set up the database container
POSTGRES_USER=malenia
POSTGRES_PASSWORD=change_me_to_a_secure_password
POSTGRES_DB=malenia_db
# Telegram bot
BOT_TOKEN=your_telegram_bot_token
# Proxy Configuration (Optional)
# Leave empty if no proxy is needed
PROXY=
# Comma-separated list of Telegram user IDs
ADMINS=[123456789,987654321]
# 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
MAX_DEVICES=12
PER_DEVICE_COST=50
WHITELIST_COST=100
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
.env
*.env
dev.env
.venv
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 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.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.callback_query(F.data == "promotion")
async def promo_init(cb: CallbackQuery, state: FSMContext):
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_data({"ctx": ctx})
@router.message(AdminStorage.promotion_msg)
async def promo_text(msg: Message, state: FSMContext):
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_data({"ctx": ctx})
@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()
await state.clear()
@@ -49,4 +60,6 @@ async def promo_send(cb: CallbackQuery, state: FSMContext, session: AsyncSession
return
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.common import return_to_menu
from bot.states.buy import SubscriptionStorage
from bot.texts import (
CALLBACK_FALLBACK,

View File

@@ -11,6 +11,7 @@ from bot.texts import (
FAQ_TEXT,
GENERAL_PROCESSING,
GENERAL_SUCCESS,
NO_SUBSCRIPTION,
NOTHING_PLACEHOLDER,
)
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)
if not user:
await cb.answer(CALLBACK_FALLBACK)
if not rw_user:
await cb.message.edit_text(NO_SUBSCRIPTION, reply_markup=return_to_menu)
return
if not sub:
if not user:
await cb.answer("1" + CALLBACK_FALLBACK)
return
if rw_user and not sub:
try:
dto = SubscriptionPlan(
devices=rw_user.hwid_device_limit,
@@ -143,7 +148,7 @@ async def sub_info(
plan_price_paid=utils.calculate_price(dto),
)
except Exception:
await cb.answer(CALLBACK_FALLBACK)
await cb.answer("2" + CALLBACK_FALLBACK)
return
await cb.message.edit_text(

View File

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

View File

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

View File

@@ -1,10 +1,6 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
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(
text="⬅️ Назад", callback_data="menu:main"
)

View File

@@ -215,5 +215,7 @@ ADM_MAIN_MENU = (
)
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>"

View File

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

View File

@@ -6,17 +6,17 @@ echo
ruff check --fix
black .
echo "[+] checking psql availability"
if ! pg_isready -h localhost -p 5432 ; then
echo "psql is down." >&2
exit 1
fi
echo "[+] psql is available!"
echo
# echo "[+] checking psql availability"
# if ! pg_isready -h localhost -p 5432 ; then
# echo "psql is down." >&2
# exit 1
# fi
# echo "[+] psql is available!"
# echo
echo "[+] Running migrations..."
echo
alembic upgrade head
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.ext.asyncio import AsyncSession
@@ -17,7 +17,7 @@ class BalanceTXRepository:
balance_after: int,
description: str,
) -> BalanceTransaction:
created_at = datetime.now(timezone.UTC)
created_at = datetime.now(UTC)
tx = BalanceTransaction(
user_id=user_id,
amount=amount,

View File

@@ -7,6 +7,7 @@ from remnawave import RemnawaveSDK
from sqlalchemy.ext.asyncio import AsyncSession
from bot.texts import NO_SUBSCRIPTION, NOTHING_PLACEHOLDER, SUBSCRIPTION_INFORMATION
from config import settings
from db.models.users import User
from misc import utils
from repositories import UserRepository
@@ -14,7 +15,6 @@ from repositories.subscriptions import SubscriptionRepository
from schemas.subscriptions import InternalSquads, SubscriptionPlan, SubscriptionStatus
from schemas.users import NewUserDTO
from services import rw
from config import settings
logger = logging.getLogger(__name__)
@@ -168,6 +168,9 @@ class UserService:
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(
session, user.id
)
@@ -222,11 +225,7 @@ class UserService:
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)
async def launch_promotion(
self,
session: AsyncSession,
promo_msg: Message
) -> int:
async def launch_promotion(self, session: AsyncSession, promo_msg: Message) -> int:
users = await self.user_repository.get_users(session)
sent = 0