mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Merge pull request #1427 from Fr1ngg/kbxpuv-bedolaga/add-heleket-balance-top-up-method
Polish Heleket payment integration
This commit is contained in:
16
.env.example
16
.env.example
@@ -261,6 +261,22 @@ CRYPTOBOT_DEFAULT_ASSET=USDT
|
||||
CRYPTOBOT_ASSETS=USDT,TON,BTC,ETH,LTC,BNB,TRX,USDC
|
||||
CRYPTOBOT_INVOICE_EXPIRES_HOURS=24
|
||||
|
||||
# HELEKET
|
||||
HELEKET_ENABLED=false
|
||||
HELEKET_MERCHANT_ID=
|
||||
HELEKET_API_KEY=
|
||||
HELEKET_BASE_URL=https://api.heleket.com/v1
|
||||
HELEKET_DEFAULT_CURRENCY=USDT
|
||||
HELEKET_DEFAULT_NETWORK=
|
||||
HELEKET_INVOICE_LIFETIME=3600
|
||||
HELEKET_MARKUP_PERCENT=0
|
||||
HELEKET_WEBHOOK_PATH=/heleket-webhook
|
||||
HELEKET_WEBHOOK_HOST=0.0.0.0
|
||||
HELEKET_WEBHOOK_PORT=8086
|
||||
HELEKET_CALLBACK_URL=
|
||||
HELEKET_RETURN_URL=
|
||||
HELEKET_SUCCESS_URL=
|
||||
|
||||
# MULENPAY
|
||||
MULENPAY_ENABLED=false
|
||||
MULENPAY_API_KEY=
|
||||
|
||||
13
README.md
13
README.md
@@ -35,7 +35,7 @@
|
||||
|
||||
### ⚡ **Полная автоматизация VPN бизнеса**
|
||||
- 🎯 **Готовое решение** - разверни за 5 минут, начни продавать сегодня
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + CryptoBot + YooKassa (СБП + карты) + MulenPay + PayPalych (СБП + карты) + WATA
|
||||
- 💰 **Многоканальные платежи** - Telegram Stars + Tribute + CryptoBot + Heleket + YooKassa (СБП + карты) + MulenPay + PayPalych (СБП + карты) + WATA
|
||||
- 🔄 **Автоматизация 99%** - от регистрации до продления подписок
|
||||
- - 📱 **MiniApp лк** - личный кабинет с возможностью покупки/продления подписки
|
||||
- 📊 **Детальная аналитика** - полная картина вашего бизнеса
|
||||
@@ -605,6 +605,13 @@ CRYPTOBOT_ENABLED=false
|
||||
CRYPTOBOT_API_TOKEN=
|
||||
CRYPTOBOT_WEBHOOK_PATH=/cryptobot-webhook
|
||||
|
||||
# Heleket
|
||||
HELEKET_ENABLED=false
|
||||
HELEKET_MERCHANT_ID=
|
||||
HELEKET_API_KEY=
|
||||
HELEKET_WEBHOOK_PATH=/heleket-webhook
|
||||
HELEKET_WEBHOOK_PORT=8086
|
||||
|
||||
# MulenPay
|
||||
MULENPAY_ENABLED=false
|
||||
MULENPAY_API_KEY=
|
||||
@@ -710,6 +717,7 @@ LOG_FILE=logs/bot.log
|
||||
- 💳 Tribute
|
||||
- 💳 YooKassa (СБП + банковские карты)
|
||||
- 💰 CryptoBot (USDT, TON, BTC, ETH и др.)
|
||||
- 🪙 Heleket (криптовалюта с наценкой)
|
||||
- 💳 MulenPay (СБП)
|
||||
- 🏦 PayPalych/Pal24 (СБП + карты)
|
||||
- 💳 **WATA**
|
||||
@@ -907,6 +915,7 @@ LOG_FILE=logs/bot.log
|
||||
- **Tribute**: Настрой webhook на `https://your-domain.com/tribute-webhook`
|
||||
- **YooKassa**: Настрой webhook на `https://your-domain.com/yookassa-webhook`
|
||||
- **CryptoBot**: Настрой webhook на `https://your-domain.com/cryptobot-webhook`
|
||||
- **Heleket**: Настрой webhook на `https://your-domain.com/heleket-webhook`
|
||||
- **MulenPay**: Настрой webhook на `https://your-domain.com/mulenpay-webhook`
|
||||
- **PayPalych**: Укажи Result URL `https://your-domain.com/pal24-webhook` в кабинете Pal24
|
||||
- **WATA**: Настрой webhook на `https://your-domain.com/wata-webhook`
|
||||
@@ -1310,7 +1319,7 @@ docker stats
|
||||
|
||||
| Метрика | Значение |
|
||||
|---------|----------|
|
||||
| 💳 **Платёжных систем** | 7 (Stars, YooKassa, Tribute, CryptoBot, MulenPay, Pal24, WATA) |
|
||||
| 💳 **Платёжных систем** | 8 (Stars, YooKassa, Tribute, CryptoBot, Heleket, MulenPay, Pal24, WATA) |
|
||||
| 🌍 **Языков интерфейса** | 2 (RU, EN) с возможностью расширения |
|
||||
| 📊 **Периодов подписки** | 6 (от 14 дней до года) |
|
||||
| 🎁 **Типов промо-акций** | 5 (коды, группы, предложения, скидки, кампании) |
|
||||
|
||||
@@ -31,15 +31,11 @@ async def start_heleket_payment(
|
||||
markup = settings.get_heleket_markup_percent()
|
||||
markup_text: Optional[str]
|
||||
if markup > 0:
|
||||
markup_text = texts.t(
|
||||
"PAYMENT_HELEKET_MARKUP",
|
||||
f"Наценка провайдера: {markup:.0f}%", # fallback
|
||||
)
|
||||
label = texts.t("PAYMENT_HELEKET_MARKUP_LABEL", "Наценка провайдера")
|
||||
markup_text = f"{label}: {markup:.0f}%"
|
||||
elif markup < 0:
|
||||
markup_text = texts.t(
|
||||
"PAYMENT_HELEKET_DISCOUNT",
|
||||
f"Скидка провайдера: {abs(markup):.0f}%",
|
||||
)
|
||||
label = texts.t("PAYMENT_HELEKET_DISCOUNT_LABEL", "Скидка провайдера")
|
||||
markup_text = f"{label}: {abs(markup):.0f}%"
|
||||
else:
|
||||
markup_text = None
|
||||
|
||||
@@ -144,8 +140,13 @@ async def process_heleket_payment_amount(
|
||||
markup_percent = None
|
||||
|
||||
if markup_percent:
|
||||
sign = "+" if markup_percent > 0 else ""
|
||||
details.append(f"📈 Наценка: {sign}{markup_percent}%")
|
||||
label_markup = texts.t("PAYMENT_HELEKET_MARKUP_LABEL", "Наценка провайдера")
|
||||
label_discount = texts.t("PAYMENT_HELEKET_DISCOUNT_LABEL", "Скидка провайдера")
|
||||
absolute = abs(markup_percent)
|
||||
if markup_percent > 0:
|
||||
details.append(f"📈 {label_markup}: +{absolute}%")
|
||||
else:
|
||||
details.append(f"📉 {label_discount}: {absolute}%")
|
||||
|
||||
if payer_amount and payer_currency:
|
||||
try:
|
||||
|
||||
@@ -976,6 +976,7 @@
|
||||
"PAYMENT_CARD_YOOKASSA": "💳 Bank card (YooKassa)",
|
||||
"PAYMENT_CHARGE_ERROR": "⚠️ Failed to charge the payment",
|
||||
"PAYMENT_CRYPTOBOT": "🪙 Cryptocurrency (CryptoBot)",
|
||||
"PAYMENT_HELEKET": "🪙 Cryptocurrency (Heleket)",
|
||||
"PAYMENT_METHODS_FOOTER": "Choose a top-up method:",
|
||||
"PAYMENT_METHODS_ONLY_SUPPORT": "💳 <b>Balance top-up methods</b>\n\n⚠️ Automated payment methods are temporarily unavailable.\nContact support to top up your balance.\n\nChoose a top-up method:",
|
||||
"PAYMENT_METHODS_PROMPT": "Choose the payment method that suits you:",
|
||||
@@ -983,6 +984,8 @@
|
||||
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ Automated payment methods are temporarily unavailable. Contact support to top up your balance.",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "via CryptoBot",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Cryptocurrency</b>",
|
||||
"PAYMENT_METHOD_HELEKET_DESCRIPTION": "via Heleket",
|
||||
"PAYMENT_METHOD_HELEKET_NAME": "🪙 <b>Cryptocurrency (Heleket)</b>",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "via {mulenpay_name}",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Bank card ({mulenpay_name})</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "via Faster Payments System",
|
||||
@@ -999,6 +1002,8 @@
|
||||
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 <b>Bank card</b>",
|
||||
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "via YooKassa Fast Payment System",
|
||||
"PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 <b>SBP (YooKassa)</b>",
|
||||
"PAYMENT_HELEKET_MARKUP_LABEL": "Provider markup",
|
||||
"PAYMENT_HELEKET_DISCOUNT_LABEL": "Provider discount",
|
||||
"PAYMENT_RETURN_HOME_BUTTON": "🏠 Main menu",
|
||||
"PAYMENT_SBP_YOOKASSA": "🏦 Pay via SBP (YooKassa)",
|
||||
"PAYMENT_TELEGRAM_STARS": "⭐ Telegram Stars",
|
||||
|
||||
@@ -976,6 +976,7 @@
|
||||
"PAYMENT_CARD_YOOKASSA": "💳 Банковская карта (YooKassa)",
|
||||
"PAYMENT_CHARGE_ERROR": "⚠️ Ошибка списания средств",
|
||||
"PAYMENT_CRYPTOBOT": "🪙 Криптовалюта (CryptoBot)",
|
||||
"PAYMENT_HELEKET": "🪙 Криптовалюта (Heleket)",
|
||||
"PAYMENT_METHODS_FOOTER": "Выберите способ пополнения:",
|
||||
"PAYMENT_METHODS_ONLY_SUPPORT": "💳 <b>Способы пополнения баланса</b>\n\n⚠️ В данный момент автоматические способы оплаты временно недоступны.\nОбратитесь в техподдержку для пополнения баланса.\n\nВыберите способ пополнения:",
|
||||
"PAYMENT_METHODS_PROMPT": "Выберите удобный для вас способ оплаты:",
|
||||
@@ -983,6 +984,8 @@
|
||||
"PAYMENT_METHODS_UNAVAILABLE_ALERT": "⚠️ В данный момент автоматические способы оплаты временно недоступны. Для пополнения баланса обратитесь в техподдержку.",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_DESCRIPTION": "через CryptoBot",
|
||||
"PAYMENT_METHOD_CRYPTOBOT_NAME": "🪙 <b>Криптовалюта</b>",
|
||||
"PAYMENT_METHOD_HELEKET_DESCRIPTION": "через Heleket",
|
||||
"PAYMENT_METHOD_HELEKET_NAME": "🪙 <b>Криптовалюта (Heleket)</b>",
|
||||
"PAYMENT_METHOD_MULENPAY_DESCRIPTION": "через {mulenpay_name}",
|
||||
"PAYMENT_METHOD_MULENPAY_NAME": "💳 <b>Банковская карта ({mulenpay_name})</b>",
|
||||
"PAYMENT_METHOD_PAL24_DESCRIPTION": "через систему быстрых платежей",
|
||||
@@ -999,6 +1002,8 @@
|
||||
"PAYMENT_METHOD_YOOKASSA_NAME": "💳 <b>Банковская карта</b>",
|
||||
"PAYMENT_METHOD_YOOKASSA_SBP_DESCRIPTION": "через систему быстрых платежей YooKassa",
|
||||
"PAYMENT_METHOD_YOOKASSA_SBP_NAME": "🏦 <b>СБП (YooKassa)</b>",
|
||||
"PAYMENT_HELEKET_MARKUP_LABEL": "Наценка провайдера",
|
||||
"PAYMENT_HELEKET_DISCOUNT_LABEL": "Скидка провайдера",
|
||||
"PAYMENT_RETURN_HOME_BUTTON": "🏠 На главную",
|
||||
"PAYMENT_SBP_YOOKASSA": "🏬 Оплатить по СБП (YooKassa)",
|
||||
"PAYMENT_TELEGRAM_STARS": "⭐ Telegram Stars",
|
||||
|
||||
@@ -75,6 +75,10 @@ services:
|
||||
- "${WEB_API_PORT:-8080}:8080"
|
||||
- "${TRIBUTE_WEBHOOK_PORT:-8081}:8081"
|
||||
- "${YOOKASSA_WEBHOOK_PORT:-8082}:8082"
|
||||
- "${CRYPTOBOT_WEBHOOK_PORT:-8083}:8083"
|
||||
- "${PAL24_WEBHOOK_PORT:-8084}:8084"
|
||||
- "${WATA_WEBHOOK_PORT:-8085}:8085"
|
||||
- "${HELEKET_WEBHOOK_PORT:-8086}:8086"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
|
||||
@@ -78,6 +78,7 @@ services:
|
||||
- "${CRYPTOBOT_WEBHOOK_PORT:-8083}:8083"
|
||||
- "${PAL24_WEBHOOK_PORT:-8084}:8084"
|
||||
- "${WATA_WEBHOOK_PORT:-8085}:8085"
|
||||
- "${HELEKET_WEBHOOK_PORT:-8086}:8086"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
- `app/database/crud/cryptobot.py` — Python-модуль
|
||||
Классы: нет
|
||||
Функции: нет
|
||||
- `app/database/crud/heleket.py` — Python-модуль
|
||||
Классы: нет
|
||||
Функции: нет
|
||||
- `app/database/crud/discount_offer.py` — Python-модуль
|
||||
Классы: нет
|
||||
Функции: нет
|
||||
@@ -158,6 +161,12 @@
|
||||
- `app/external/cryptobot.py` — Python-модуль
|
||||
Классы: `CryptoBotService` (2 методов)
|
||||
Функции: нет
|
||||
- `app/external/heleket.py` — Python-модуль
|
||||
Классы: `HeleketService` (3 методов)
|
||||
Функции: нет
|
||||
- `app/external/heleket_webhook.py` — Python-модуль
|
||||
Классы: `HeleketWebhookHandler` (3 методов)
|
||||
Функции: `create_heleket_app`, `start_heleket_webhook_server`
|
||||
- `app/external/pal24_client.py` — Async client for PayPalych (Pal24) API.
|
||||
Классы: `Pal24APIError` — Base error for Pal24 API operations., `Pal24Response` (2 методов) — Wrapper for Pal24 API responses., `Pal24Client` (5 методов) — Async client implementing PayPalych API methods.
|
||||
Функции: нет
|
||||
@@ -726,6 +735,9 @@
|
||||
- `tests/services/test_payment_service_cryptobot.py` — Тесты сценариев CryptoBot в PaymentService.
|
||||
Классы: `DummySession` (2 методов), `DummyLocalPayment` (1 методов), `StubCryptoBotService` (1 методов)
|
||||
Функции: `anyio_backend`, `_make_service`
|
||||
- `tests/services/test_payment_service_heleket.py` — Тесты сценариев Heleket в PaymentService.
|
||||
Классы: `DummySession` (2 методов), `DummyLocalPayment` (1 методов), `StubHeleketService` (1 методов)
|
||||
Функции: `anyio_backend`, `_make_service`
|
||||
- `tests/services/test_payment_service_mulenpay.py` — Тесты для сценариев MulenPay в PaymentService.
|
||||
Классы: `DummySession`, `DummyLocalPayment` (1 методов), `StubMulenPayService` (1 методов)
|
||||
Функции: `anyio_backend`, `_make_service`
|
||||
|
||||
@@ -11,6 +11,7 @@ if str(ROOT_DIR) not in sys.path:
|
||||
|
||||
from app.services.payment import ( # noqa: E402
|
||||
CryptoBotPaymentMixin,
|
||||
HeleketPaymentMixin,
|
||||
MulenPayPaymentMixin,
|
||||
Pal24PaymentMixin,
|
||||
PaymentCommonMixin,
|
||||
@@ -30,6 +31,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
|
||||
YooKassaPaymentMixin,
|
||||
TributePaymentMixin,
|
||||
CryptoBotPaymentMixin,
|
||||
HeleketPaymentMixin,
|
||||
MulenPayPaymentMixin,
|
||||
Pal24PaymentMixin,
|
||||
WataPaymentMixin,
|
||||
@@ -46,6 +48,7 @@ def test_payment_service_mro_contains_all_mixins() -> None:
|
||||
"create_yookassa_payment",
|
||||
"create_tribute_payment",
|
||||
"create_cryptobot_payment",
|
||||
"create_heleket_payment",
|
||||
"create_mulenpay_payment",
|
||||
"create_pal24_payment",
|
||||
"create_wata_payment",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace, ModuleType
|
||||
from typing import Any, Dict
|
||||
@@ -16,6 +18,7 @@ if str(ROOT_DIR) not in sys.path:
|
||||
|
||||
import app.services.payment_service as payment_service_module # noqa: E402
|
||||
from app.services.payment_service import PaymentService # noqa: E402
|
||||
from app.database.models import PaymentMethod # noqa: E402
|
||||
from app.config import settings # noqa: E402
|
||||
|
||||
|
||||
@@ -256,6 +259,146 @@ async def test_process_cryptobot_webhook_success(monkeypatch: pytest.MonkeyPatch
|
||||
assert admin_calls
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_process_heleket_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
bot = DummyBot()
|
||||
service = _make_service(bot)
|
||||
fake_session = FakeSession()
|
||||
|
||||
payment = SimpleNamespace(
|
||||
uuid="heleket-uuid",
|
||||
order_id="heleket-order",
|
||||
user_id=77,
|
||||
amount="150.00",
|
||||
amount_float=150.0,
|
||||
amount_kopeks=15000,
|
||||
status="check",
|
||||
payer_amount=None,
|
||||
payer_currency=None,
|
||||
exchange_rate=None,
|
||||
discount_percent=None,
|
||||
payment_url=None,
|
||||
transaction_id=None,
|
||||
)
|
||||
|
||||
async def fake_get_by_uuid(db, uuid):
|
||||
return payment if uuid == payment.uuid else None
|
||||
|
||||
async def fake_get_by_order(db, order_id):
|
||||
return payment if order_id == payment.order_id else None
|
||||
|
||||
async def fake_update(
|
||||
db,
|
||||
uuid,
|
||||
*,
|
||||
status=None,
|
||||
payer_amount=None,
|
||||
payer_currency=None,
|
||||
exchange_rate=None,
|
||||
discount_percent=None,
|
||||
paid_at=None,
|
||||
payment_url=None,
|
||||
metadata=None,
|
||||
):
|
||||
if status is not None:
|
||||
payment.status = status
|
||||
if payer_amount is not None:
|
||||
payment.payer_amount = payer_amount
|
||||
if payer_currency is not None:
|
||||
payment.payer_currency = payer_currency
|
||||
if exchange_rate is not None:
|
||||
payment.exchange_rate = exchange_rate
|
||||
if discount_percent is not None:
|
||||
payment.discount_percent = discount_percent
|
||||
if payment_url is not None:
|
||||
payment.payment_url = payment_url
|
||||
payment.paid_at = paid_at
|
||||
if metadata:
|
||||
payment.metadata_json = metadata
|
||||
return payment
|
||||
|
||||
async def fake_link(db, uuid, transaction_id):
|
||||
payment.transaction_id = transaction_id
|
||||
return payment
|
||||
|
||||
heleket_module = ModuleType("app.database.crud.heleket")
|
||||
heleket_module.get_heleket_payment_by_uuid = fake_get_by_uuid
|
||||
heleket_module.get_heleket_payment_by_order_id = fake_get_by_order
|
||||
heleket_module.update_heleket_payment = fake_update
|
||||
heleket_module.link_heleket_payment_to_transaction = fake_link
|
||||
monkeypatch.setitem(sys.modules, "app.database.crud.heleket", heleket_module)
|
||||
|
||||
transactions: list[Dict[str, Any]] = []
|
||||
|
||||
async def fake_create_transaction(db, **kwargs):
|
||||
transactions.append(kwargs)
|
||||
return SimpleNamespace(id=321, **kwargs)
|
||||
|
||||
monkeypatch.setattr(payment_service_module, "create_transaction", fake_create_transaction)
|
||||
|
||||
user = SimpleNamespace(
|
||||
id=77,
|
||||
telegram_id=7700,
|
||||
balance_kopeks=0,
|
||||
has_made_first_topup=False,
|
||||
promo_group=None,
|
||||
subscription=None,
|
||||
referred_by_id=None,
|
||||
referrer=None,
|
||||
language="ru",
|
||||
)
|
||||
|
||||
async def fake_get_user(db, user_id):
|
||||
return user if user_id == user.id else None
|
||||
|
||||
monkeypatch.setattr(payment_service_module, "get_user_by_id", fake_get_user)
|
||||
monkeypatch.setattr("app.services.payment.heleket.format_referrer_info", lambda u: "")
|
||||
|
||||
monkeypatch.setattr(type(settings), "format_price", lambda self, amount: f"{amount / 100:.2f}₽", raising=False)
|
||||
|
||||
referral_stub = SimpleNamespace(process_referral_topup=AsyncMock())
|
||||
monkeypatch.setitem(sys.modules, "app.services.referral_service", referral_stub)
|
||||
|
||||
admin_calls: list[Any] = []
|
||||
|
||||
class DummyAdminService:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
async def send_balance_topup_notification(self, *args, **kwargs):
|
||||
admin_calls.append((args, kwargs))
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"app.services.admin_notification_service",
|
||||
SimpleNamespace(AdminNotificationService=lambda bot: DummyAdminService(bot)),
|
||||
)
|
||||
|
||||
service.build_topup_success_keyboard = AsyncMock(return_value=None)
|
||||
|
||||
payload = {
|
||||
"uuid": "heleket-uuid",
|
||||
"status": "paid",
|
||||
"payer_amount": "2.50",
|
||||
"payer_currency": "USDT",
|
||||
"discount_percent": -5,
|
||||
"payer_amount_exchange_rate": "0.0166",
|
||||
"paid_at": "2024-01-02T12:00:00Z",
|
||||
"url": "https://pay.example",
|
||||
}
|
||||
|
||||
result = await service.process_heleket_webhook(fake_session, payload)
|
||||
|
||||
assert result is True
|
||||
assert transactions and transactions[0]["payment_method"] == PaymentMethod.HELEKET
|
||||
assert payment.transaction_id == 321
|
||||
assert user.balance_kopeks == 15000
|
||||
assert user.has_made_first_topup is True
|
||||
assert fake_session.commits >= 1
|
||||
assert bot.sent_messages
|
||||
assert admin_calls
|
||||
referral_stub.process_referral_topup.assert_awaited_once()
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_process_yookassa_webhook_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
bot = DummyBot()
|
||||
|
||||
Reference in New Issue
Block a user