Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35d798a429 | ||
|
|
b34eb733b9 | ||
|
|
fcd456dce9 | ||
|
|
8246e9a590 | ||
|
|
14f244ab79 |
+2
-2
@@ -92,8 +92,8 @@ PANEL_WEBHOOK_SECRET= # secret used to verify panel webhook signatures
|
|||||||
USER_TRAFFIC_LIMIT_GB=0
|
USER_TRAFFIC_LIMIT_GB=0
|
||||||
USER_TRAFFIC_STRATEGY="NO_RESET"
|
USER_TRAFFIC_STRATEGY="NO_RESET"
|
||||||
|
|
||||||
# Default Inbounds for Users (Optional, comma-separated UUIDs)
|
# Default Internal Squads for Users (Optional, comma-separated UUIDs)
|
||||||
USER_INBOUND_UUIDS=uuid1,uuid2,uuid3
|
USER_SQUAD_UUIDS=uuid1,uuid2,uuid3
|
||||||
|
|
||||||
# Trial Settings
|
# Trial Settings
|
||||||
TRIAL_ENABLED=True
|
TRIAL_ENABLED=True
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* Supports **Crypto Pay** for payments with fiat currency (RUB by default).
|
* Supports **Crypto Pay** for payments with fiat currency (RUB by default).
|
||||||
* Automatic subscription activation/extension upon successful payment.
|
* Automatic subscription activation/extension upon successful payment.
|
||||||
* Link and syncs users with a **Remnawave panel** account, primarily matching by Telegram ID.
|
* Link and syncs users with a **Remnawave panel** account, primarily matching by Telegram ID.
|
||||||
* Updates user status, expiration dates, traffic limits, and inbounds on the Remnawave panel.
|
* Updates user status, expiration dates, traffic limits, and internal squads on the Remnawave panel.
|
||||||
* **Admin Panel:**
|
* **Admin Panel:**
|
||||||
* Protected by `ADMIN_IDS` (supports multiple administrators).
|
* Protected by `ADMIN_IDS` (supports multiple administrators).
|
||||||
* **Statistics:** View bot usage (total users, banned, active subscriptions), recent payments, and panel sync status.
|
* **Statistics:** View bot usage (total users, banned, active subscriptions), recent payments, and panel sync status.
|
||||||
@@ -104,7 +104,7 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* `PANEL_API_URL`: Full URL to your Remnawave panel's API (e.g., `http://remnawave:3000/api` or `https://panel.yourdomain.com/api`).
|
* `PANEL_API_URL`: Full URL to your Remnawave panel's API (e.g., `http://remnawave:3000/api` or `https://panel.yourdomain.com/api`).
|
||||||
* `PANEL_API_KEY`: API Key for authenticating with the Remnawave panel.
|
* `PANEL_API_KEY`: API Key for authenticating with the Remnawave panel.
|
||||||
* `PANEL_WEBHOOK_SECRET`: Secret key for verifying webhooks from the Remnawave panel.
|
* `PANEL_WEBHOOK_SECRET`: Secret key for verifying webhooks from the Remnawave panel.
|
||||||
* `USER_INBOUND_UUIDS`: (Optional) Comma-separated list of inbound UUIDs from your panel to assign to users. If empty, `activateAllInbounds: true` (panel default) is used for new users.
|
* `USER_SQUAD_UUIDS`: (Optional) Comma-separated list of internal squad UUIDs from your panel to assign to users during creation.
|
||||||
* `USER_TRAFFIC_LIMIT_GB` and `USER_TRAFFIC_STRATEGY`: Default traffic limit in gigabytes (0 for unlimited) and the reset strategy applied when updating users on the panel.
|
* `USER_TRAFFIC_LIMIT_GB` and `USER_TRAFFIC_STRATEGY`: Default traffic limit in gigabytes (0 for unlimited) and the reset strategy applied when updating users on the panel.
|
||||||
* `TRIAL_ENABLED`, `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`: Settings for the trial period.
|
* `TRIAL_ENABLED`, `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`: Settings for the trial period.
|
||||||
* `WEB_SERVER_HOST`, `WEB_SERVER_PORT`: Host and port for the bot's internal webhook server.
|
* `WEB_SERVER_HOST`, `WEB_SERVER_PORT`: Host and port for the bot's internal webhook server.
|
||||||
|
|||||||
+33
-5
@@ -193,11 +193,38 @@ async def on_startup_configured(dispatcher: Dispatcher):
|
|||||||
async def on_shutdown_configured(dispatcher: Dispatcher):
|
async def on_shutdown_configured(dispatcher: Dispatcher):
|
||||||
logging.warning("SHUTDOWN: on_shutdown_configured executing...")
|
logging.warning("SHUTDOWN: on_shutdown_configured executing...")
|
||||||
|
|
||||||
|
async def close_service(key: str) -> None:
|
||||||
|
service = dispatcher.get(key)
|
||||||
|
if not service:
|
||||||
|
return
|
||||||
|
close_coro = getattr(service, "close", None)
|
||||||
|
if callable(close_coro):
|
||||||
|
try:
|
||||||
|
await close_coro()
|
||||||
|
logging.info(f"{key} closed on shutdown.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Failed to close {key}: {e}")
|
||||||
|
else:
|
||||||
|
close_session = getattr(service, "close_session", None)
|
||||||
|
if callable(close_session):
|
||||||
|
try:
|
||||||
|
await close_session()
|
||||||
|
logging.info(f"{key} session closed on shutdown.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Failed to close session for {key}: {e}")
|
||||||
|
|
||||||
panel_service: Optional[PanelApiService] = dispatcher.get("panel_service")
|
for service_key in (
|
||||||
if panel_service and hasattr(panel_service, "close_session"):
|
"panel_service",
|
||||||
await panel_service.close_session()
|
"cryptopay_service",
|
||||||
logging.info("Panel API service session closed on shutdown.")
|
"tribute_service",
|
||||||
|
"panel_webhook_service",
|
||||||
|
"yookassa_service",
|
||||||
|
"promo_code_service",
|
||||||
|
"stars_service",
|
||||||
|
"subscription_service",
|
||||||
|
"referral_service",
|
||||||
|
):
|
||||||
|
await close_service(service_key)
|
||||||
|
|
||||||
bot: Bot = dispatcher["bot_instance"]
|
bot: Bot = dispatcher["bot_instance"]
|
||||||
if bot and bot.session:
|
if bot and bot.session:
|
||||||
@@ -314,7 +341,8 @@ async def run_bot(settings_param: Settings):
|
|||||||
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param))
|
dp.update.outer_middleware(ActionLoggerMiddleware(settings=settings_param))
|
||||||
|
|
||||||
dp.startup.register(on_startup_configured)
|
dp.startup.register(on_startup_configured)
|
||||||
dp.shutdown.register(lambda: on_shutdown_configured(dp))
|
# Register shutdown callback directly so Dispatcher instance is provided
|
||||||
|
dp.shutdown.register(on_shutdown_configured)
|
||||||
|
|
||||||
await register_all_routers(dp, settings_param)
|
await register_all_routers(dp, settings_param)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ class CryptoPayService:
|
|||||||
self.client = None
|
self.client = None
|
||||||
self.configured = False
|
self.configured = False
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Close underlying AioCryptoPay session if initialized."""
|
||||||
|
if self.client:
|
||||||
|
try:
|
||||||
|
await self.client.close()
|
||||||
|
logging.info("CryptoPay client session closed.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Failed to close CryptoPay client: {e}")
|
||||||
|
|
||||||
async def create_invoice(
|
async def create_invoice(
|
||||||
self,
|
self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class PanelApiService:
|
|||||||
self._session = None
|
self._session = None
|
||||||
logging.info("Panel API service HTTP session closed.")
|
logging.info("Panel API service HTTP session closed.")
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""Alias for close_session for API consistency."""
|
||||||
|
await self.close_session()
|
||||||
|
|
||||||
async def _prepare_headers(self) -> Dict[str, str]:
|
async def _prepare_headers(self) -> Dict[str, str]:
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -325,8 +329,7 @@ class PanelApiService:
|
|||||||
default_expire_days: int = 1,
|
default_expire_days: int = 1,
|
||||||
default_traffic_limit_bytes: int = 0,
|
default_traffic_limit_bytes: int = 0,
|
||||||
default_traffic_limit_strategy: str = "NO_RESET",
|
default_traffic_limit_strategy: str = "NO_RESET",
|
||||||
specific_inbound_uuids: Optional[List[str]] = None,
|
specific_squad_uuids: Optional[List[str]] = None,
|
||||||
activate_all_inbounds_default_flag: bool = True,
|
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
tag: Optional[str] = None,
|
tag: Optional[str] = None,
|
||||||
status: str = "ACTIVE",
|
status: str = "ACTIVE",
|
||||||
@@ -357,11 +360,8 @@ class PanelApiService:
|
|||||||
"trafficLimitStrategy": default_traffic_limit_strategy.upper(),
|
"trafficLimitStrategy": default_traffic_limit_strategy.upper(),
|
||||||
"trafficLimitBytes": default_traffic_limit_bytes,
|
"trafficLimitBytes": default_traffic_limit_bytes,
|
||||||
}
|
}
|
||||||
if specific_inbound_uuids:
|
if specific_squad_uuids:
|
||||||
payload["activeUserInbounds"] = specific_inbound_uuids
|
payload["activeInternalSquads"] = specific_squad_uuids
|
||||||
payload["activateAllInbounds"] = False
|
|
||||||
else:
|
|
||||||
payload["activateAllInbounds"] = activate_all_inbounds_default_flag
|
|
||||||
if telegram_id is not None: payload["telegramId"] = telegram_id
|
if telegram_id is not None: payload["telegramId"] = telegram_id
|
||||||
if email: payload["email"] = email
|
if email: payload["email"] = email
|
||||||
if description: payload["description"] = description
|
if description: payload["description"] = description
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class SubscriptionService:
|
|||||||
creation_response = await self.panel_service.create_panel_user(
|
creation_response = await self.panel_service.create_panel_user(
|
||||||
username_on_panel=panel_username_on_panel_standard,
|
username_on_panel=panel_username_on_panel_standard,
|
||||||
telegram_id=user_id,
|
telegram_id=user_id,
|
||||||
specific_inbound_uuids=self.settings.parsed_user_inbound_uuids,
|
specific_squad_uuids=self.settings.parsed_user_squad_uuids,
|
||||||
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||||
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
)
|
)
|
||||||
@@ -128,7 +128,7 @@ class SubscriptionService:
|
|||||||
creation_response = await self.panel_service.create_panel_user(
|
creation_response = await self.panel_service.create_panel_user(
|
||||||
username_on_panel=panel_username_on_panel_standard,
|
username_on_panel=panel_username_on_panel_standard,
|
||||||
telegram_id=user_id,
|
telegram_id=user_id,
|
||||||
specific_inbound_uuids=self.settings.parsed_user_inbound_uuids,
|
specific_squad_uuids=self.settings.parsed_user_squad_uuids,
|
||||||
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||||
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
)
|
)
|
||||||
@@ -357,12 +357,10 @@ class SubscriptionService:
|
|||||||
"trafficLimitBytes": self.settings.trial_traffic_limit_bytes,
|
"trafficLimitBytes": self.settings.trial_traffic_limit_bytes,
|
||||||
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
}
|
}
|
||||||
if self.settings.parsed_user_inbound_uuids:
|
if self.settings.parsed_user_squad_uuids:
|
||||||
panel_update_payload["activeUserInbounds"] = (
|
panel_update_payload["activeInternalSquads"] = (
|
||||||
self.settings.parsed_user_inbound_uuids
|
self.settings.parsed_user_squad_uuids
|
||||||
)
|
)
|
||||||
elif panel_user_created_now:
|
|
||||||
panel_update_payload["activateAllInbounds"] = True
|
|
||||||
|
|
||||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||||
panel_user_uuid, panel_update_payload
|
panel_user_uuid, panel_update_payload
|
||||||
@@ -506,12 +504,10 @@ class SubscriptionService:
|
|||||||
"trafficLimitBytes": self.settings.user_traffic_limit_bytes,
|
"trafficLimitBytes": self.settings.user_traffic_limit_bytes,
|
||||||
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
}
|
}
|
||||||
if self.settings.parsed_user_inbound_uuids:
|
if self.settings.parsed_user_squad_uuids:
|
||||||
panel_update_payload["activeUserInbounds"] = (
|
panel_update_payload["activeInternalSquads"] = (
|
||||||
self.settings.parsed_user_inbound_uuids
|
self.settings.parsed_user_squad_uuids
|
||||||
)
|
)
|
||||||
elif panel_user_created_now:
|
|
||||||
panel_update_payload["activateAllInbounds"] = True
|
|
||||||
|
|
||||||
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
updated_panel_user = await self.panel_service.update_user_details_on_panel(
|
||||||
panel_user_uuid, panel_update_payload
|
panel_user_uuid, panel_update_payload
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ class TributeService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return web.Response(status=400, text="bad_request")
|
return web.Response(status=400, text="bad_request")
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
"Tribute webhook data: %s",
|
||||||
|
json.dumps(payload, ensure_ascii=False),
|
||||||
|
)
|
||||||
|
|
||||||
event_name = payload.get('name')
|
event_name = payload.get('name')
|
||||||
data = payload.get('payload', {})
|
data = payload.get('payload', {})
|
||||||
user_id = data.get('telegram_user_id')
|
user_id = data.get('telegram_user_id')
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class YooKassaService:
|
|||||||
f"Amount: {amount} {currency}. Metadata: {metadata}. Receipt: {receipt_data_dict}"
|
f"Amount: {amount} {currency}. Metadata: {metadata}. Receipt: {receipt_data_dict}"
|
||||||
)
|
)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
response = await loop.run_in_executor(
|
response = await loop.run_in_executor(
|
||||||
None, lambda: YooKassaPayment.create(payment_request,
|
None, lambda: YooKassaPayment.create(payment_request,
|
||||||
idempotence_key))
|
idempotence_key))
|
||||||
@@ -196,7 +196,7 @@ class YooKassaService:
|
|||||||
f"Fetching payment info from YooKassa for ID: {payment_id_in_yookassa}"
|
f"Fetching payment info from YooKassa for ID: {payment_id_in_yookassa}"
|
||||||
)
|
)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
payment_info_yk = await loop.run_in_executor(
|
payment_info_yk = await loop.run_in_executor(
|
||||||
None, lambda: YooKassaPayment.find_one(payment_id_in_yookassa))
|
None, lambda: YooKassaPayment.find_one(payment_id_in_yookassa))
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -95,10 +95,10 @@ class Settings(BaseSettings):
|
|||||||
PANEL_API_KEY: Optional[str] = None
|
PANEL_API_KEY: Optional[str] = None
|
||||||
USER_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=0.0)
|
USER_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=0.0)
|
||||||
USER_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
|
USER_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
|
||||||
USER_INBOUND_UUIDS: Optional[str] = Field(
|
USER_SQUAD_UUIDS: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description=
|
description=
|
||||||
"Comma-separated UUIDs of inbounds to activate for new panel users")
|
"Comma-separated UUIDs of internal squads to assign to new panel users")
|
||||||
|
|
||||||
TRIAL_ENABLED: bool = Field(default=True)
|
TRIAL_ENABLED: bool = Field(default=True)
|
||||||
TRIAL_DURATION_DAYS: int = Field(default=3)
|
TRIAL_DURATION_DAYS: int = Field(default=3)
|
||||||
@@ -156,11 +156,11 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def parsed_user_inbound_uuids(self) -> Optional[List[str]]:
|
def parsed_user_squad_uuids(self) -> Optional[List[str]]:
|
||||||
if self.USER_INBOUND_UUIDS:
|
if self.USER_SQUAD_UUIDS:
|
||||||
return [
|
return [
|
||||||
uuid.strip()
|
uuid.strip()
|
||||||
for uuid in self.USER_INBOUND_UUIDS.split(',')
|
for uuid in self.USER_SQUAD_UUIDS.split(',')
|
||||||
if uuid.strip()
|
if uuid.strip()
|
||||||
]
|
]
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user