diff --git a/.env.example b/.env.example
index 39d7999..60bc553 100644
--- a/.env.example
+++ b/.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
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 852a71b..8722fed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,6 +130,7 @@ celerybeat.pid
# Environments
.env
+*.env
dev.env
.venv
env/
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..4eba2a6
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13.0
diff --git a/bot/handlers/admins/ads.py b/bot/handlers/admins/ads.py
index 013ec1a..5894ff3 100644
--- a/bot/handlers/admins/ads.py
+++ b/bot/handlers/admins/ads.py
@@ -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)
\ No newline at end of file
+ await cb.message.edit_text(
+ SUCCESSFULLY_SENT_PROMO.format(successful=successful), reply_markup=return_to_menu
+ )
diff --git a/bot/handlers/client/buy.py b/bot/handlers/client/buy.py
index 67f4a7d..8108d96 100644
--- a/bot/handlers/client/buy.py
+++ b/bot/handlers/client/buy.py
@@ -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,
diff --git a/bot/handlers/client/menus.py b/bot/handlers/client/menus.py
index 32dc6de..bdd24a9 100644
--- a/bot/handlers/client/menus.py
+++ b/bot/handlers/client/menus.py
@@ -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(
diff --git a/bot/keyboards/admins.py b/bot/keyboards/admins.py
index a2de2ac..90c04b4 100644
--- a/bot/keyboards/admins.py
+++ b/bot/keyboards/admins.py
@@ -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()
diff --git a/bot/keyboards/client.py b/bot/keyboards/client.py
index fea1b0d..9ea7c5d 100644
--- a/bot/keyboards/client.py
+++ b/bot/keyboards/client.py
@@ -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(
diff --git a/bot/keyboards/common.py b/bot/keyboards/common.py
index 6fa1322..7848162 100644
--- a/bot/keyboards/common.py
+++ b/bot/keyboards/common.py
@@ -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()
\ No newline at end of file
+return_to_menu: InlineKeyboardMarkup = InlineKeyboardBuilder([[_return_to_menu_btn]]).as_markup()
diff --git a/bot/texts.py b/bot/texts.py
index b6baab2..19d20f5 100644
--- a/bot/texts.py
+++ b/bot/texts.py
@@ -215,5 +215,7 @@ ADM_MAIN_MENU = (
)
ADM_PROMO_INIT = "📢 Введите/перешлите текст рассылки:"
-ADM_VERIFY_PROMOTION = "❓ Отправить рассылку? Это действие будет невозможно отменить."
-SUCCESSFULLY_SENT_PROMO = "💚 Успешно отправлена рассылка.\n⚡ Успешных отправок: {successful}."
\ No newline at end of file
+ADM_VERIFY_PROMOTION = (
+ "❓ Отправить рассылку? Это действие будет невозможно отменить."
+)
+SUCCESSFULLY_SENT_PROMO = "💚 Успешно отправлена рассылка.\n⚡ Успешных отправок: {successful}."
diff --git a/docker-compose.yml b/docker-compose.yml
index 930ef8b..4b3001f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/entrypoints/dev.sh b/entrypoints/dev.sh
index 283f080..480039f 100644
--- a/entrypoints/dev.sh
+++ b/entrypoints/dev.sh
@@ -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
\ No newline at end of file
+python -m bot.main
\ No newline at end of file
diff --git a/repositories/transactions.py b/repositories/transactions.py
index f7126d4..47ea2a3 100644
--- a/repositories/transactions.py
+++ b/repositories/transactions.py
@@ -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,
diff --git a/services/user_service.py b/services/user_service.py
index 0c114e4..c67b7ea 100644
--- a/services/user_service.py
+++ b/services/user_service.py
@@ -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
\ No newline at end of file
+
+ return sent