fix chart refresh and bot command surface
This commit is contained in:
@@ -17,7 +17,6 @@ from src.auth.telegram_group_pricing import TelegramGroupPricing, TELEGRAM_MEMBE
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind"}
|
||||
_BASIC_COMMANDS = {"start", "help", "id", "top", "diag", "bind", "unbind", "markets"}
|
||||
|
||||
|
||||
class BasicCommandHandler:
|
||||
@@ -60,10 +59,6 @@ class BasicCommandHandler:
|
||||
def _unbind(message):
|
||||
self._dispatch(message)
|
||||
|
||||
@self.bot.message_handler(commands=["markets"])
|
||||
def _markets(message):
|
||||
self._dispatch(message)
|
||||
|
||||
if hasattr(self.bot, "chat_join_request_handler"):
|
||||
@self.bot.chat_join_request_handler(func=lambda request: True)
|
||||
def _chat_join_request(request):
|
||||
@@ -123,9 +118,6 @@ class BasicCommandHandler:
|
||||
if command == "unbind":
|
||||
self.handle_unbind(message)
|
||||
return
|
||||
if command == "markets":
|
||||
self.handle_markets(message)
|
||||
return
|
||||
|
||||
def handle_start_help(self, message: Any) -> None:
|
||||
trace = CommandTrace("/start", message)
|
||||
@@ -141,7 +133,6 @@ class BasicCommandHandler:
|
||||
trace.set_status("ok")
|
||||
finally:
|
||||
trace.emit()
|
||||
|
||||
@staticmethod
|
||||
def _is_private_text_fallback(message: Any) -> bool:
|
||||
text = str(getattr(message, "text", "") or "").strip()
|
||||
@@ -553,25 +544,3 @@ class BasicCommandHandler:
|
||||
trace.set_status("ok", "unbound")
|
||||
finally:
|
||||
trace.emit()
|
||||
|
||||
def handle_markets(self, message: Any) -> None:
|
||||
trace = CommandTrace("/markets", message)
|
||||
try:
|
||||
chat_type = str(getattr(getattr(message, "chat", None), "type", "") or "").strip().lower()
|
||||
if chat_type and chat_type != "private":
|
||||
self.bot.reply_to(
|
||||
message,
|
||||
"ℹ️ `/markets` 仅支持私聊机器人查询。",
|
||||
parse_mode="Markdown",
|
||||
)
|
||||
trace.set_status("blocked", f"unsupported_chat_type:{chat_type}")
|
||||
return
|
||||
|
||||
self.bot.reply_to(
|
||||
message,
|
||||
"ℹ️ 市场概览 (Focus Digest) 功能已移除。\n频道继续接收关键市场警报推送;如需查看当前市场状态,请访问 https://polyweather.top/",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
trace.set_status("ok", "removed")
|
||||
finally:
|
||||
trace.emit()
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.bot.command_parser import extract_command_name
|
||||
from src.bot.command_parser import split_command_and_args
|
||||
from src.bot.command_guard import CommandGuard
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.bot.observability import CommandTrace
|
||||
from src.bot.services.city_command_service import CityCommandService
|
||||
from src.bot.settings import CITY_DAILY_FREE_LIMIT, CITY_QUERY_COST
|
||||
|
||||
def _is_city_command(message: Any) -> bool:
|
||||
command = extract_command_name(
|
||||
getattr(message, "text", None),
|
||||
getattr(message, "entities", None),
|
||||
)
|
||||
return command in {"city", "pwcity"}
|
||||
|
||||
|
||||
class CityCommandHandler:
|
||||
def __init__(
|
||||
self,
|
||||
bot: Any,
|
||||
guard: CommandGuard,
|
||||
city_service: CityCommandService,
|
||||
io_layer: BotIOLayer,
|
||||
):
|
||||
self.bot = bot
|
||||
self.guard = guard
|
||||
self.city_service = city_service
|
||||
self.io_layer = io_layer
|
||||
|
||||
def register(self) -> None:
|
||||
@self.bot.message_handler(commands=["city", "pwcity"])
|
||||
def _city_command(message):
|
||||
self.handle(message)
|
||||
|
||||
@self.bot.message_handler(
|
||||
func=lambda message: _is_city_command(message),
|
||||
content_types=["text"],
|
||||
)
|
||||
def _city_text(message):
|
||||
self.handle(message)
|
||||
|
||||
def handle(self, message: Any) -> None:
|
||||
if getattr(message, "_pw_city_handled", False):
|
||||
return
|
||||
if not _is_city_command(message):
|
||||
return
|
||||
setattr(message, "_pw_city_handled", True)
|
||||
setattr(message, "_pw_command_handled", True)
|
||||
trace = CommandTrace("/city", message)
|
||||
try:
|
||||
_, args = split_command_and_args(getattr(message, "text", None))
|
||||
if not args:
|
||||
trace.set_status("bad_request", "missing_city")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
"❌ 请输入城市名称\n\n用法: <code>/city chicago</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
city_input = args.strip().lower()
|
||||
resolved = self.city_service.resolve_city(city_input)
|
||||
if not resolved.ok:
|
||||
city_list = ", ".join(resolved.supported_cities or [])
|
||||
trace.set_status("bad_request", "city_not_supported")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"❌ 未找到城市: <b>{city_input}</b>\n\n支持的城市: {city_list}",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
city_name = str(resolved.city_name)
|
||||
if not self.guard.check_daily_query_limit(message, "city", CITY_DAILY_FREE_LIMIT):
|
||||
trace.set_status("blocked", "daily_query_limit")
|
||||
return
|
||||
if not self.guard.ensure_access_and_points(message, CITY_QUERY_COST, "/city"):
|
||||
trace.set_status("blocked", "guard_rejected")
|
||||
return
|
||||
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"🔍 正在查询 {city_name.title()} 的天气数据...",
|
||||
)
|
||||
report_result = self.city_service.build_report(city_name, CITY_QUERY_COST)
|
||||
if not report_result.ok:
|
||||
trace.set_status("failed", report_result.error or "city_report_failed")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"❌ 查询失败: {report_result.error}",
|
||||
)
|
||||
return
|
||||
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
str(report_result.report),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
trace.set_status("ok", city_name)
|
||||
except Exception as exc:
|
||||
trace.set_status("failed", "unexpected_error")
|
||||
logger.exception("查询 /city 失败")
|
||||
self.io_layer.send_query_message(message, f"❌ 查询失败: {exc}")
|
||||
finally:
|
||||
trace.emit()
|
||||
@@ -1,105 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.bot.command_parser import extract_command_name
|
||||
from src.bot.command_parser import split_command_and_args
|
||||
from src.bot.command_guard import CommandGuard
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.bot.observability import CommandTrace
|
||||
from src.bot.services.deb_command_service import DebCommandService
|
||||
from src.bot.settings import DEB_DAILY_FREE_LIMIT, DEB_QUERY_COST
|
||||
|
||||
def _is_deb_command(message: Any) -> bool:
|
||||
command = extract_command_name(
|
||||
getattr(message, "text", None),
|
||||
getattr(message, "entities", None),
|
||||
)
|
||||
return command in {"deb", "pwdeb"}
|
||||
|
||||
|
||||
class DebCommandHandler:
|
||||
def __init__(
|
||||
self,
|
||||
bot: Any,
|
||||
guard: CommandGuard,
|
||||
deb_service: DebCommandService,
|
||||
io_layer: BotIOLayer,
|
||||
):
|
||||
self.bot = bot
|
||||
self.guard = guard
|
||||
self.deb_service = deb_service
|
||||
self.io_layer = io_layer
|
||||
|
||||
def register(self) -> None:
|
||||
@self.bot.message_handler(commands=["deb", "pwdeb"])
|
||||
def _deb_command(message):
|
||||
self.handle(message)
|
||||
|
||||
@self.bot.message_handler(
|
||||
func=lambda message: _is_deb_command(message),
|
||||
content_types=["text"],
|
||||
)
|
||||
def _deb_text(message):
|
||||
self.handle(message)
|
||||
|
||||
def handle(self, message: Any) -> None:
|
||||
if getattr(message, "_pw_deb_handled", False):
|
||||
return
|
||||
if not _is_deb_command(message):
|
||||
return
|
||||
setattr(message, "_pw_deb_handled", True)
|
||||
setattr(message, "_pw_command_handled", True)
|
||||
trace = CommandTrace("/deb", message)
|
||||
try:
|
||||
_, args = split_command_and_args(getattr(message, "text", None))
|
||||
if not args:
|
||||
trace.set_status("bad_request", "missing_city")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
"❌ 用法: <code>/deb ankara</code>",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
city_input = args.strip().lower()
|
||||
city_name = self.deb_service.resolve_city(city_input)
|
||||
if not self.deb_service.has_history(city_name):
|
||||
trace.set_status("bad_request", "history_missing")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"❌ 暂无 {city_name} 的历史数据。",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
if not self.guard.check_daily_query_limit(message, "deb", DEB_DAILY_FREE_LIMIT):
|
||||
trace.set_status("blocked", "daily_query_limit")
|
||||
return
|
||||
if not self.guard.ensure_access_and_points(message, DEB_QUERY_COST, "/deb"):
|
||||
trace.set_status("blocked", "guard_rejected")
|
||||
return
|
||||
|
||||
report_result = self.deb_service.build_report(city_name, DEB_QUERY_COST)
|
||||
if not report_result.ok:
|
||||
trace.set_status("failed", report_result.error or "deb_report_failed")
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"❌ 查询失败: {report_result.error}",
|
||||
)
|
||||
return
|
||||
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
str(report_result.report),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
trace.set_status("ok", city_name)
|
||||
except Exception as exc:
|
||||
trace.set_status("failed", "unexpected_error")
|
||||
logger.exception("查询 /deb 失败")
|
||||
self.io_layer.send_query_message(message, f"❌ 查询失败: {exc}")
|
||||
finally:
|
||||
trace.emit()
|
||||
+3
-19
@@ -1,14 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.bot.settings import (
|
||||
CITY_DAILY_FREE_LIMIT,
|
||||
DEB_DAILY_FREE_LIMIT,
|
||||
GROUP_MESSAGE_POINTS_ENABLED,
|
||||
MESSAGE_COOLDOWN_SEC,
|
||||
MESSAGE_DAILY_CAP,
|
||||
@@ -194,28 +191,20 @@ class BotIOLayer:
|
||||
|
||||
def build_welcome_text(self) -> str:
|
||||
return (
|
||||
"🚀 <b>PolyWeather 天气查询机器人</b>\n\n"
|
||||
"🚀 <b>PolyWeather 机器人</b>\n\n"
|
||||
"可用指令:\n"
|
||||
f"/city [城市名] 或 /pwcity [城市名] - 查询城市天气预测与实测 (免费, 每日 {CITY_DAILY_FREE_LIMIT} 次)\n"
|
||||
f"/deb [城市名] 或 /pwdeb [城市名] - 查看 DEB 融合预测准确率 (免费, 每日 {DEB_DAILY_FREE_LIMIT} 次)\n"
|
||||
"/markets - 私聊机器人查看当前市场监控摘要\n"
|
||||
"/top - 查看积分排行榜\n"
|
||||
"/id - 获取当前聊天的 Chat ID\n\n"
|
||||
"/diag - 查看 Bot 启动诊断\n\n"
|
||||
"/bind - 绑定 Supabase 账号(可选)\n"
|
||||
"/unbind - 解除当前 Telegram 与网页账号绑定\n\n"
|
||||
"🔗 机器人: <a href=\"https://t.me/polyyuanbot\">@polyyuanbot</a>\n"
|
||||
"👥 社群: <a href=\"https://t.me/+Io5H9oVHFmVjOTQ5\">加入 Telegram 群组</a>\n\n"
|
||||
"📌 <i>私有频道用于接收自动推送;手动查看市场概览请私聊机器人发送 <code>/markets</code>。</i>\n\n"
|
||||
"示例: <code>/city 伦敦</code> 或 <code>/pwcity 伦敦</code>\n"
|
||||
"💡 <i>提示: 积分现在通过邀请制度获得;有效邀请完成首次 Pro 付款后,邀请人获得积分奖励。</i>"
|
||||
"👥 社群: <a href=\"https://t.me/+Io5H9oVHFmVjOTQ5\">加入 Telegram 群组</a>"
|
||||
)
|
||||
|
||||
def build_points_rank_text(self, user: Any) -> str:
|
||||
self.db.upsert_user(user.id, self.display_name(user))
|
||||
user_info = self.db.get_user(user.id)
|
||||
now = datetime.now()
|
||||
today_str = now.strftime("%Y-%m-%d")
|
||||
|
||||
leaderboard = self.db.get_leaderboard(limit=5)
|
||||
rank_text = "🏆 <b>PolyWeather 用户积分排行</b>\n"
|
||||
@@ -227,17 +216,12 @@ class BotIOLayer:
|
||||
rank_text += f"{medal} {username}: <b>{points}</b> 分\n"
|
||||
|
||||
if user_info:
|
||||
daily_queries_date = str(user_info.get("daily_queries_date") or "")
|
||||
city_used = int(user_info.get("daily_city_queries") or 0) if daily_queries_date == today_str else 0
|
||||
deb_used = int(user_info.get("daily_deb_queries") or 0) if daily_queries_date == today_str else 0
|
||||
|
||||
rank_text += "────────────────────\n"
|
||||
rank_text += (
|
||||
"👤 <b>我的状态:</b>\n"
|
||||
f"┣ 累计积分: <code>{user_info['points']}</code>\n"
|
||||
"┣ 积分获取: <code>邀请付费用户</code>\n"
|
||||
"┣ 抵扣规则: <code>500分 = 1 USDC,单笔最多抵3U</code>\n"
|
||||
f"┗ /city 免费 ({city_used}/{CITY_DAILY_FREE_LIMIT}) | /deb 免费 ({deb_used}/{DEB_DAILY_FREE_LIMIT})"
|
||||
"┗ 抵扣规则: <code>500分 = 1 USDC,单笔最多抵3U</code>"
|
||||
)
|
||||
return rank_text
|
||||
|
||||
|
||||
+6
-36
@@ -5,32 +5,21 @@ from typing import Any
|
||||
|
||||
from loguru import logger # type: ignore
|
||||
|
||||
from src.bot.analysis.city_analysis_service import CityAnalysisService
|
||||
from src.bot.analysis.deb_analysis_service import DebAnalysisService
|
||||
from src.bot.command_guard import CommandGuard
|
||||
from src.bot.handlers.activity import ActivityHandler
|
||||
from src.bot.handlers.basic import BasicCommandHandler
|
||||
from src.bot.handlers.city import CityCommandHandler
|
||||
from src.bot.handlers.deb import DebCommandHandler
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.bot.runtime_coordinator import StartupCoordinator
|
||||
from src.bot.services.city_command_service import CityCommandService
|
||||
from src.bot.services.deb_command_service import DebCommandService
|
||||
from src.utils.config_validation import validate_or_raise
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
|
||||
def _project_root() -> str:
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def _register_handlers(
|
||||
bot: Any,
|
||||
config: dict[str, Any],
|
||||
io_layer: BotIOLayer,
|
||||
guard: CommandGuard,
|
||||
city_service: CityCommandService,
|
||||
deb_service: DebCommandService,
|
||||
guard: Any | None,
|
||||
city_service: Any | None,
|
||||
deb_service: Any | None,
|
||||
startup_coordinator: StartupCoordinator,
|
||||
) -> None:
|
||||
BasicCommandHandler(
|
||||
@@ -39,25 +28,12 @@ def _register_handlers(
|
||||
runtime_status_provider=startup_coordinator.get_runtime_status,
|
||||
config=config,
|
||||
).register()
|
||||
CityCommandHandler(
|
||||
bot=bot,
|
||||
guard=guard,
|
||||
city_service=city_service,
|
||||
io_layer=io_layer,
|
||||
).register()
|
||||
DebCommandHandler(
|
||||
bot=bot,
|
||||
guard=guard,
|
||||
deb_service=deb_service,
|
||||
io_layer=io_layer,
|
||||
).register()
|
||||
ActivityHandler(bot=bot, io_layer=io_layer).register()
|
||||
|
||||
|
||||
def start_bot() -> None:
|
||||
import telebot # type: ignore
|
||||
|
||||
from src.data_collection.weather_sources import WeatherDataCollector
|
||||
from src.database.db_manager import DBManager
|
||||
from src.utils.config_loader import load_config
|
||||
|
||||
@@ -70,14 +46,8 @@ def start_bot() -> None:
|
||||
|
||||
bot = telebot.TeleBot(token)
|
||||
db = DBManager()
|
||||
weather = WeatherDataCollector(config)
|
||||
|
||||
io_layer = BotIOLayer(bot=bot, db=db)
|
||||
city_analysis = CityAnalysisService(weather=weather)
|
||||
deb_analysis = DebAnalysisService(project_root=_project_root())
|
||||
guard = CommandGuard(io_layer=io_layer)
|
||||
city_service = CityCommandService(analysis=city_analysis)
|
||||
deb_service = DebCommandService(analysis=deb_analysis)
|
||||
startup_coordinator = StartupCoordinator(
|
||||
bot=bot,
|
||||
config=config,
|
||||
@@ -90,9 +60,9 @@ def start_bot() -> None:
|
||||
bot=bot,
|
||||
config=config,
|
||||
io_layer=io_layer,
|
||||
guard=guard,
|
||||
city_service=city_service,
|
||||
deb_service=deb_service,
|
||||
guard=None,
|
||||
city_service=None,
|
||||
deb_service=None,
|
||||
startup_coordinator=startup_coordinator,
|
||||
)
|
||||
runtime_status = startup_coordinator.start_all()
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from src.bot.analysis.city_analysis_service import CityAnalysisService
|
||||
|
||||
|
||||
@dataclass
|
||||
class CityResolveResult:
|
||||
ok: bool
|
||||
city_name: Optional[str] = None
|
||||
supported_cities: List[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CityReportResult:
|
||||
ok: bool
|
||||
report: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class CityCommandService:
|
||||
def __init__(self, analysis: CityAnalysisService):
|
||||
self.analysis = analysis
|
||||
|
||||
def resolve_city(self, city_input: str) -> CityResolveResult:
|
||||
city_name, supported = self.analysis.resolve_city(city_input)
|
||||
if not city_name:
|
||||
return CityResolveResult(ok=False, supported_cities=supported)
|
||||
return CityResolveResult(ok=True, city_name=city_name, supported_cities=supported)
|
||||
|
||||
def build_report(self, city_name: str, city_query_cost: int) -> CityReportResult:
|
||||
try:
|
||||
report = self.analysis.build_city_report(city_name, city_query_cost)
|
||||
return CityReportResult(ok=True, report=report)
|
||||
except Exception as exc:
|
||||
return CityReportResult(ok=False, error=str(exc))
|
||||
@@ -1,31 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from src.bot.analysis.deb_analysis_service import DebAnalysisService
|
||||
|
||||
|
||||
@dataclass
|
||||
class DebReportResult:
|
||||
ok: bool
|
||||
report: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class DebCommandService:
|
||||
def __init__(self, analysis: DebAnalysisService):
|
||||
self.analysis = analysis
|
||||
|
||||
def resolve_city(self, city_input: str) -> str:
|
||||
return self.analysis.resolve_city(city_input)
|
||||
|
||||
def has_history(self, city_name: str) -> bool:
|
||||
return self.analysis.has_history(city_name)
|
||||
|
||||
def build_report(self, city_name: str, deb_query_cost: int) -> DebReportResult:
|
||||
try:
|
||||
report = self.analysis.build_deb_accuracy_report(city_name, deb_query_cost)
|
||||
return DebReportResult(ok=True, report=report)
|
||||
except Exception as exc:
|
||||
return DebReportResult(ok=False, error=str(exc))
|
||||
@@ -41,7 +41,7 @@ class BotEntitlementService:
|
||||
"POLYWEATHER_BOT_USE_SUPABASE_ENTITLEMENT",
|
||||
SUPABASE_ENTITLEMENT.enabled,
|
||||
)
|
||||
commands = protected_commands or ("/city", "/deb")
|
||||
commands = protected_commands or ()
|
||||
self.protected_commands: Set[str] = {str(c).strip().lower() for c in commands if str(c).strip()}
|
||||
|
||||
def check(self, user_id: int, command_label: str) -> EntitlementDecision:
|
||||
|
||||
@@ -31,10 +31,6 @@ MESSAGE_MIN_LENGTH = _env_int("POLYWEATHER_BOT_MESSAGE_MIN_LENGTH", 3, min_value
|
||||
MESSAGE_COOLDOWN_SEC = _env_int("POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC", 30, min_value=0)
|
||||
# Optional per-chat override map, parsed in BotIOLayer:
|
||||
# POLYWEATHER_BOT_MESSAGE_COOLDOWN_BY_CHAT="-1003586303099:10,-1003539418691:20"
|
||||
CITY_QUERY_COST = _env_int("POLYWEATHER_BOT_CITY_QUERY_COST", 0, min_value=0)
|
||||
DEB_QUERY_COST = _env_int("POLYWEATHER_BOT_DEB_QUERY_COST", 0, min_value=0)
|
||||
CITY_DAILY_FREE_LIMIT = _env_int("POLYWEATHER_BOT_CITY_DAILY_FREE_LIMIT", 10, min_value=1)
|
||||
DEB_DAILY_FREE_LIMIT = _env_int("POLYWEATHER_BOT_DEB_DAILY_FREE_LIMIT", 10, min_value=1)
|
||||
|
||||
FIRST_MESSAGE_BONUS = _env_int("POLYWEATHER_BOT_FIRST_MESSAGE_BONUS", 2, min_value=0)
|
||||
WELCOME_BONUS = _env_int("POLYWEATHER_BOT_WELCOME_BONUS", 20, min_value=0)
|
||||
|
||||
Reference in New Issue
Block a user