refactoring: polished some files, .env.example update
This commit is contained in:
46
.env.example
46
.env.example
@@ -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
1
.gitignore
vendored
@@ -130,6 +130,7 @@ celerybeat.pid
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.env
|
||||
dev.env
|
||||
.venv
|
||||
env/
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13.0
|
||||
@@ -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()
|
||||
@@ -31,15 +39,18 @@ async def promo_text(msg: Message, state: FSMContext):
|
||||
ctx: NewPromotionContext | None = data.get("ctx")
|
||||
if ctx:
|
||||
await ctx.msg.delete()
|
||||
|
||||
|
||||
ctx.promotion = msg
|
||||
await edited_msg.edit_text(ADM_VERIFY_PROMOTION, reply_markup=action_confirmation)
|
||||
|
||||
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()
|
||||
|
||||
@@ -47,6 +58,8 @@ async def promo_send(cb: CallbackQuery, state: FSMContext, session: AsyncSession
|
||||
if not (ctx and ctx.promotion and ctx.msg):
|
||||
await cb.message.edit_text(CALLBACK_FALLBACK, reply_markup=return_to_menu)
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
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"
|
||||
)
|
||||
|
||||
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()
|
||||
return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()
|
||||
|
||||
@@ -215,5 +215,7 @@ ADM_MAIN_MENU = (
|
||||
)
|
||||
|
||||
ADM_PROMO_INIT = "<b>📢 Введите/перешлите текст рассылки:</b>"
|
||||
ADM_VERIFY_PROMOTION = "<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>"
|
||||
SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>"
|
||||
ADM_VERIFY_PROMOTION = (
|
||||
"<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>"
|
||||
)
|
||||
SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -238,5 +237,5 @@ class UserService:
|
||||
sent += 1
|
||||
except Exception:
|
||||
logger.exception("caught an exception while launching promo, skipping user...")
|
||||
|
||||
return sent
|
||||
|
||||
return sent
|
||||
|
||||
Reference in New Issue
Block a user