Skip to content
This repository was archived by the owner on Nov 2, 2025. It is now read-only.

Commit aca65e5

Browse files
committed
Code cleanup and bug fixes
1 parent cd473dd commit aca65e5

35 files changed

+678
-579
lines changed

README.md

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,18 @@ The bot enables efficient subscription sales with advanced features:
4747
- Format text using HTML
4848
- Preview notifications before sending
4949
- System notifications for the developer and administrators
50-
- **Referral Program**
50+
- **Two-Level Referral Program** (by [@Heimlet](https://github.com/Heimlet))
5151
- View referral statistics
5252
- Reward users for inviting new members
53-
- **Trial Period**
53+
- Support for two-tier referral rewards
54+
- **Trial Period** (by [@Heimlet](https://github.com/Heimlet))
5455
- Provide free trial subscription
56+
- Extend trial period for referred users
5557
- Configure and disable the trial period
5658
- **Flexible Payment System**
5759
- Change the default currency
5860
- Easily extendable architecture for adding new payment gateways
5961
- ~~Add, edit, and delete subscription plans at any time~~
60-
- ~~Enable or disable payment methods~~
6162
- ~~Change the display order of payment options~~
6263
- **~~User Editor~~**
6364
- ~~View user information~~
@@ -83,11 +84,13 @@ Administrators do not have access to server management.
8384

8485

8586
### 🚧 Current Tasks
86-
- [ ] Trial period
87-
- [ ] Referral system
87+
- [x] Trial period
88+
- [x] Referral system
8889
- [ ] Statistics
8990
- [ ] User editor
9091
- [ ] Plans editor
92+
- [ ] Flexible server pool
93+
- [ ] Custom promocodes
9194

9295
<a id="installation-guide"></a>
9396

@@ -140,17 +143,14 @@ Before starting the installation, make sure you have the installed [**Docker**](
140143
| | | |
141144
| SHOP_EMAIL || [email protected] | Email for receipts |
142145
| SHOP_CURRENCY || RUB | Currency for buttons (e.g., RUB, USD, XTR) |
143-
| SHOP_TRIAL_ENABLED || True | Enable trial subscription for new users. |
144-
| SHOP_TRIAL_PERIOD || 3 | Duration of the trial subscription in days. |
145-
| SHOP_REFERRED_TRIAL_ENABLED || False | Enable specific trial subscription for referral users. |
146-
| SHOP_REFERRED_TRIAL_PERIOD || 7 | Trial duration in days for the referred (invited) user |
146+
| SHOP_TRIAL_ENABLED || True | Enable trial subscription for new users |
147+
| SHOP_TRIAL_PERIOD || 3 | Duration of the trial subscription in days |
148+
| SHOP_REFERRED_TRIAL_ENABLED || False | Enable extended trial period for referred users |
149+
| SHOP_REFERRED_TRIAL_PERIOD || 7 | Duration of the extended trial for referred users (in days) |
147150
| SHOP_REFERRER_REWARD_ENABLED || True | Enable the two-level referral reward system |
148-
| ~~SHOP_REFERRER_REWARD_TYPE~~ || DAYS | Type of referrer reward. 'days' only now. Awaits user balance implementation. (e.g. days, money) |
149-
| SHOP_REFERRER_LEVEL_ONE_PERIOD || 10 | Days reward for the first-level referrer (user who invited) |
150-
| SHOP_REFERRER_LEVEL_TWO_PERIOD || 3 | Days reward for the second-level referrer (user invited by the invited) |
151-
| ~~SHOP_REFERRER_LEVEL_ONE_RATE~~ || 50 | Percentage reward for the first-level referrer (user who invited) |
152-
| ~~SHOP_REFERRER_LEVEL_TWO_RATE~~ || 5 | Percentage reward for the second-level referrer (user invited by the invited) |
153-
| SHOP_BONUS_DEVICES_COUNT || 1 | Number of devices by default for trial and referral users (mirrors tariff plans settings) |
151+
| SHOP_REFERRER_LEVEL_ONE_PERIOD || 10 | Reward in days for the first-level referrer (inviter) |
152+
| SHOP_REFERRER_LEVEL_TWO_PERIOD || 3 | Reward in days for the second-level referrer (inviter of the inviter). |
153+
| SHOP_BONUS_DEVICES_COUNT || 1 | Default Device Limit for Promocode, Trial, and Referral Users (Based on Plan Settings) |
154154
| SHOP_PAYMENT_STARS_ENABLED || True | Enable Telegram stars payment |
155155
| SHOP_PAYMENT_CRYPTOMUS_ENABLED || False | Enable Cryptomus payment |
156156
| SHOP_PAYMENT_YOOKASSA_ENABLED || False | Enable Yookassa payment |
@@ -171,12 +171,6 @@ Before starting the installation, make sure you have the installed [**Docker**](
171171
| YOOMONEY_WALLET_ID || - | Wallet ID for Yoomoney payment |
172172
| YOOMONEY_NOTIFICATION_SECRET || - | Notification secret key for Yoomoney payment |
173173
| | | |
174-
| REDIS_HOST || 3xui-shop-redis | Host of the Redis server |
175-
| REDIS_PORT || 6379 | Port of the Redis server |
176-
| REDIS_DB_NAME || 0 | Name of the Redis database |
177-
| REDIS_USERNAME || - | Username for authentication in the Redis server |
178-
| REDIS_PASSWORD || - | Password for authentication in the Redis server |
179-
| | | |
180174
| LOG_LEVEL || DEBUG | Log level (e.g., INFO, DEBUG) |
181175
| LOG_FORMAT || %(asctime)s \| %(name)s \| %(levelname)s \| %(message)s | Log format |
182176
| LOG_ARCHIVE_FORMAT || zip | Log archive format (e.g., zip, gz) |
@@ -262,14 +256,15 @@ To ensure the bot functions correctly, you must configure the 3X-UI panel:
262256
<a id="bugs-and-feature-requests"></a>
263257

264258
### Referral and Trial Rewards Configuration
265-
Shop now supports **trial subscriptions** and a **two-level referral reward system**. Here’s how it works:
266-
All configuration is available via .env (see it above).
267-
268-
| Type of reward | How it works |
269-
|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
270-
| Trial period | A trial subscription is available by 'TRY FOR FREE' button at start menu to any user who opens the bot and does not have an active subscription. |
271-
| Referred user Trial period | This option is just like previous 'trial period', but allows bot admin to configure **extended trial period** for an invited user. |
272-
| Referral 2-level payment rewards | When a referred user pays for a subscription, the referrer and the second-level referrer (the user who invited the referrer) receive ~~a percentage of the payment as a reward~~ fixed count of days at the moment fore each level. |
259+
260+
Bot now supports **trial subscriptions** and a **two-level referral reward system**. Here’s how it works:
261+
All configuration is available via `.env` [(see it above)](#environment-variables-configuration).
262+
263+
| Type of reward | How it works |
264+
| - | - |
265+
| Trial period | A trial subscription is available by 'TRY FOR FREE' button at start menu to any user who opens the bot and does not have an active subscription. |
266+
| Extended Trial period | This option is just like previous 'trial period', but allows to configure **extended trial period** for an invited user. |
267+
| Two-Level Referral Payment Rewards | When a referred user pays for a subscription, the referrer and the second-level referrer (the user who invited the referrer) receive fixed count of days at the moment fore each level. |
273268

274269
## 🐛 Bugs and Feature Requests
275270

app/__main__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ async def on_startup(config: Config, bot: Bot, services: ServicesContainer, db:
5151

5252
tasks.transactions.start_scheduler(db.session)
5353
if config.shop.REFERRER_REWARD_ENABLED:
54-
tasks.referral.start_referral_scheduler(session_factory=db.session, referral_service=services.referral)
54+
tasks.referral.start_scheduler(
55+
session_factory=db.session, referral_service=services.referral
56+
)
5557

5658

5759
async def main() -> None:
@@ -66,6 +68,7 @@ async def main() -> None:
6668

6769
# Initialize database
6870
db = Database(config.database)
71+
await db.initialize()
6972

7073
# Set up storage for FSM (Finite State Machine)
7174
storage = RedisStorage.from_url(url=config.redis.url())
@@ -114,7 +117,7 @@ async def main() -> None:
114117
dispatcher.shutdown.register(on_shutdown)
115118

116119
# Enable Maintenance mode for developing # WARNING: remove before production
117-
MaintenanceMiddleware.set_mode(True)
120+
MaintenanceMiddleware.set_mode(False)
118121

119122
# Register middlewares
120123
middlewares.register(dispatcher=dispatcher, i18n=i18n, session=db.session)
@@ -129,9 +132,6 @@ async def main() -> None:
129132
# Include bot routers
130133
routers.include(app=app, dispatcher=dispatcher)
131134

132-
# Initialize database
133-
await db.initialize()
134-
135135
# Set up bot commands
136136
await commands.setup(bot)
137137

app/bot/middlewares/maintenance.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ async def __call__(
3333

3434
if self.active and not is_admin and user.id != event.bot.id:
3535
logger.info(f"User {user.id} tried to use bot in maintenance")
36-
message = event.message or event.callback_query.message
36+
37+
if event.message:
38+
message = event.message
39+
elif event.callback_query and event.callback_query.message:
40+
message = event.callback_query.message
3741

3842
if message:
3943
await NotificationService.notify_by_message(

app/bot/payment_gateways/_gateway.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
from abc import ABC, abstractmethod
33

4-
import requests
54
from aiogram import Bot
65
from aiogram.fsm.storage.redis import RedisStorage
76
from aiogram.utils.i18n import I18n
@@ -43,7 +42,7 @@ def __init__(
4342
bot: Bot,
4443
i18n: I18n,
4544
services: ServicesContainer,
46-
):
45+
) -> None:
4746
self.app = app
4847
self.config = config
4948
self.session = session
@@ -82,7 +81,7 @@ async def _on_payment_succeeded(self, payment_id: str) -> None:
8281
if self.config.shop.REFERRER_REWARD_ENABLED:
8382
await self.services.referral.add_referrers_rewards_on_payment(
8483
referred_tg_id=data.user_id,
85-
payment_amount=data.price, # todo: (!) add currency unified processing
84+
payment_amount=data.price, # TODO: (!) add currency unified processing
8685
payment_id=payment_id,
8786
)
8887

app/bot/payment_gateways/telegram_stars.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(
3434
bot: Bot,
3535
i18n: I18n,
3636
services: ServicesContainer,
37-
):
37+
) -> None:
3838
self.name = __("payment:gateway:telegram_stars")
3939
self.app = app
4040
self.config = config

app/bot/payment_gateways/yookassa.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def __init__(
4242
bot: Bot,
4343
i18n: I18n,
4444
services: ServicesContainer,
45-
):
45+
) -> None:
4646
self.name = __("payment:gateway:yookassa")
4747
self.app = app
4848
self.config = config

app/bot/payment_gateways/yoomoney.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __init__(
3636
bot: Bot,
3737
i18n: I18n,
3838
services: ServicesContainer,
39-
):
39+
) -> None:
4040
self.name = __("payment:gateway:yoomoney")
4141
self.app = app
4242
self.config = config

app/bot/routers/main_menu/handler.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,36 @@
1414
from app.bot.utils.constants import MAIN_MESSAGE_ID_KEY
1515
from app.bot.utils.navigation import NavMain
1616
from app.config import Config
17-
from app.db.models import User, Referral
17+
from app.db.models import Referral, User
1818

1919
from .keyboard import main_menu_keyboard
2020

2121
logger = logging.getLogger(__name__)
2222
router = Router(name=__name__)
2323

2424

25-
async def process_creating_referral(
26-
session: AsyncSession,
27-
user: User,
28-
referrer_id: int
29-
) -> bool:
25+
async def process_creating_referral(session: AsyncSession, user: User, referrer_id: int) -> bool:
3026
logger.info(f"Assigning user {user.tg_id} as a referred to a referrer user {referrer_id}")
3127
try:
3228
referrer = await User.get(session=session, tg_id=referrer_id)
3329
if not referrer or referrer.tg_id == user.tg_id:
34-
logger.info(f"Failed to assign user {user.tg_id} as a referred to a referrer user {referrer_id}."
35-
f"Invalid string received.")
30+
logger.info(
31+
f"Failed to assign user {user.tg_id} as a referred to a referrer user {referrer_id}."
32+
f"Invalid string received."
33+
)
3634
return False
3735

3836
await Referral.create(
39-
session=session,
40-
referrer_tg_id=referrer.tg_id,
41-
referred_tg_id=user.tg_id
37+
session=session, referrer_tg_id=referrer.tg_id, referred_tg_id=user.tg_id
38+
)
39+
logger.info(
40+
f"User {user.tg_id} assigned as referred to a referrer with tg id {referrer.tg_id}"
4241
)
43-
logger.info(f"User {user.tg_id} assigned as referred to a referrer with tg id {referrer.tg_id}")
4442
return True
45-
except Exception as e:
46-
logger.critical(f"Error creating Referral to a referred {user.tg_id} (start arg: {referrer_id}): {e}")
43+
except Exception as exception:
44+
logger.critical(
45+
f"Referral creation error for {user.tg_id} (arg: {referrer_id}): {exception}"
46+
)
4747
return False
4848

4949

@@ -56,7 +56,7 @@ async def command_main_menu(
5656
config: Config,
5757
session: AsyncSession,
5858
command: CommandObject,
59-
is_new_user: bool
59+
is_new_user: bool,
6060
) -> None:
6161
logger.info(f"User {user.tg_id} opened main menu page.")
6262
previous_message_id = await state.get_value(MAIN_MESSAGE_ID_KEY)
@@ -73,9 +73,7 @@ async def command_main_menu(
7373
received_referrer_id = int(command.args) if command.args and command.args.isdigit() else None
7474
if received_referrer_id and is_new_user:
7575
await process_creating_referral(
76-
session=session,
77-
user=user,
78-
referrer_id=received_referrer_id
76+
session=session, user=user, referrer_id=received_referrer_id
7977
)
8078

8179
is_admin = await IsAdmin()(user_id=user.tg_id)
@@ -93,11 +91,11 @@ async def command_main_menu(
9391

9492
@router.callback_query(F.data == NavMain.MAIN_MENU)
9593
async def callback_main_menu(
96-
callback: CallbackQuery,
97-
user: User,
98-
services: ServicesContainer,
99-
state: FSMContext,
100-
config: Config,
94+
callback: CallbackQuery,
95+
user: User,
96+
services: ServicesContainer,
97+
state: FSMContext,
98+
config: Config,
10199
) -> None:
102100
logger.info(f"User {user.tg_id} returned to main menu page.")
103101
await state.clear()
@@ -142,8 +140,10 @@ async def redirect_to_main_menu(
142140
is_admin,
143141
is_referral_available=config.shop.REFERRER_REWARD_ENABLED,
144142
is_trial_available=await services.subscription.is_trial_available(user),
145-
is_referred_trial_available=await services.referral.is_referred_trial_available(user),
143+
is_referred_trial_available=await services.referral.is_referred_trial_available(
144+
user
145+
),
146146
),
147147
)
148-
except Exception as e:
149-
logger.critical(f"Error redirecting to main menu page: {e}")
148+
except Exception as exception:
149+
logger.error(f"Error redirecting to main menu page: {exception}")

app/bot/routers/main_menu/keyboard.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313

1414
def main_menu_keyboard(
15-
is_admin: bool = False,
16-
is_referral_available: bool = False,
17-
is_trial_available: bool = False,
18-
is_referred_trial_available: bool = False,
15+
is_admin: bool = False,
16+
is_referral_available: bool = False,
17+
is_trial_available: bool = False,
18+
is_referred_trial_available: bool = False,
1919
) -> InlineKeyboardMarkup:
2020
builder = InlineKeyboardBuilder()
2121

@@ -29,8 +29,7 @@ def main_menu_keyboard(
2929
elif is_trial_available:
3030
builder.row(
3131
InlineKeyboardButton(
32-
text=_("subscription:button:get_trial"),
33-
callback_data=NavSubscription.GET_TRIAL
32+
text=_("subscription:button:get_trial"), callback_data=NavSubscription.GET_TRIAL
3433
)
3534
)
3635

@@ -45,12 +44,16 @@ def main_menu_keyboard(
4544
),
4645
)
4746
builder.row(
48-
*([
49-
InlineKeyboardButton(
50-
text=_("main_menu:button:referral"),
51-
callback_data=NavReferral.MAIN,
52-
)
53-
] if is_referral_available else []),
47+
*(
48+
[
49+
InlineKeyboardButton(
50+
text=_("main_menu:button:referral"),
51+
callback_data=NavReferral.MAIN,
52+
)
53+
]
54+
if is_referral_available
55+
else []
56+
),
5457
InlineKeyboardButton(
5558
text=_("main_menu:button:support"),
5659
callback_data=NavSupport.MAIN,

0 commit comments

Comments
 (0)