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