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)
|
# 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
1
.gitignore
vendored
@@ -130,6 +130,7 @@ celerybeat.pid
|
|||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
*.env
|
||||||
dev.env
|
dev.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
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 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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user