feat: Introduce core bot components, on-chain wallet watchers, Telegram notifications, and configuration management.
This commit is contained in:
+34
-24
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from loguru import logger
|
||||
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env, parse_telegram_chat_ids
|
||||
|
||||
|
||||
class CommandGuard:
|
||||
@@ -15,7 +16,10 @@ class CommandGuard:
|
||||
|
||||
def __init__(self, io_layer: BotIOLayer, group_chat_id: str | None = None):
|
||||
self.io_layer = io_layer
|
||||
self.group_chat_id = str(group_chat_id or os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
if group_chat_id is None:
|
||||
self.group_chat_ids = get_telegram_chat_ids_from_env()
|
||||
else:
|
||||
self.group_chat_ids = parse_telegram_chat_ids(group_chat_id)
|
||||
self.group_invite_url = str(os.getenv("POLYWEATHER_BOT_GROUP_INVITE_URL") or "").strip()
|
||||
|
||||
def _reply_group_required(self, message: Any) -> None:
|
||||
@@ -30,42 +34,48 @@ class CommandGuard:
|
||||
if user_id is None:
|
||||
return False
|
||||
|
||||
if not self.group_chat_id:
|
||||
if not self.group_chat_ids:
|
||||
logger.error(
|
||||
"group member blocked command={} user_id={} reason=missing_TELEGRAM_CHAT_ID",
|
||||
"group member blocked command={} user_id={} reason=missing_TELEGRAM_CHAT_IDS",
|
||||
command_label,
|
||||
user_id,
|
||||
)
|
||||
self.io_layer.bot.reply_to(
|
||||
message,
|
||||
"⚠️ 机器人未配置群组准入(TELEGRAM_CHAT_ID),请联系管理员。",
|
||||
"⚠️ 机器人未配置群组准入(TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID),请联系管理员。",
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
member = self.io_layer.bot.get_chat_member(self.group_chat_id, int(user_id))
|
||||
status = str(getattr(member, "status", "") or "").strip().lower()
|
||||
except Exception as exc:
|
||||
blocked_statuses: list[str] = []
|
||||
lookup_failures: list[str] = []
|
||||
for chat_id in self.group_chat_ids:
|
||||
try:
|
||||
member = self.io_layer.bot.get_chat_member(chat_id, int(user_id))
|
||||
status = str(getattr(member, "status", "") or "").strip().lower()
|
||||
except Exception as exc:
|
||||
lookup_failures.append(f"{chat_id}:{type(exc).__name__}")
|
||||
continue
|
||||
|
||||
if status in self._ALLOWED_MEMBER_STATUSES:
|
||||
return True
|
||||
blocked_statuses.append(f"{chat_id}:{status or 'unknown'}")
|
||||
|
||||
if blocked_statuses:
|
||||
logger.info(
|
||||
"group member blocked command={} user_id={} chat_id={} reason=lookup_failed error={}",
|
||||
"group member blocked command={} user_id={} chat_ids={} statuses={}",
|
||||
command_label,
|
||||
user_id,
|
||||
self.group_chat_id,
|
||||
exc,
|
||||
",".join(self.group_chat_ids),
|
||||
"|".join(blocked_statuses),
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"group member blocked command={} user_id={} chat_ids={} reason=lookup_failed_all errors={}",
|
||||
command_label,
|
||||
user_id,
|
||||
",".join(self.group_chat_ids),
|
||||
"|".join(lookup_failures) or "unknown",
|
||||
)
|
||||
self._reply_group_required(message)
|
||||
return False
|
||||
|
||||
if status in self._ALLOWED_MEMBER_STATUSES:
|
||||
return True
|
||||
|
||||
logger.info(
|
||||
"group member blocked command={} user_id={} chat_id={} status={}",
|
||||
command_label,
|
||||
user_id,
|
||||
self.group_chat_id,
|
||||
status or "unknown",
|
||||
)
|
||||
self._reply_group_required(message)
|
||||
return False
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
|
||||
def _project_root() -> str:
|
||||
@@ -68,7 +69,7 @@ def start_bot() -> None:
|
||||
config=config,
|
||||
command_access_mode="group_member_only",
|
||||
protected_commands=["/city", "/deb"],
|
||||
required_group_chat_id=str(os.getenv("TELEGRAM_CHAT_ID") or "").strip(),
|
||||
required_group_chat_id=",".join(get_telegram_chat_ids_from_env()),
|
||||
)
|
||||
|
||||
_register_handlers(
|
||||
|
||||
@@ -6,6 +6,8 @@ from datetime import datetime
|
||||
from importlib import import_module
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
raw = os.getenv(name)
|
||||
@@ -148,7 +150,7 @@ class StartupCoordinator:
|
||||
|
||||
def _start_trade_alert_loop(self) -> LoopStatus:
|
||||
enabled = _env_bool("TELEGRAM_ALERT_PUSH_ENABLED", True)
|
||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
mispricing_only = _env_bool("TELEGRAM_ALERT_MISPRICING_ONLY", True)
|
||||
interval = (
|
||||
max(300, _env_int("TELEGRAM_ALERT_MISPRICING_INTERVAL_SEC", 7200))
|
||||
@@ -160,8 +162,9 @@ class StartupCoordinator:
|
||||
"mode": "mispricing-only" if mispricing_only else "full",
|
||||
"interval_sec": interval,
|
||||
"cities_count": cities_count,
|
||||
"chat_targets": len(chat_ids),
|
||||
}
|
||||
validation_error = None if chat_id else "missing_TELEGRAM_CHAT_ID"
|
||||
validation_error = None if chat_ids else "missing_TELEGRAM_CHAT_IDS"
|
||||
return self._start_with_validation(
|
||||
key="trade_alert_push",
|
||||
label="错价雷达推送",
|
||||
@@ -176,7 +179,7 @@ class StartupCoordinator:
|
||||
|
||||
def _start_polygon_wallet_loop(self) -> LoopStatus:
|
||||
enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
rpc_url = str(os.getenv("POLYGON_RPC_URL") or "").strip()
|
||||
wallets_count = _parse_csv_count(os.getenv("POLYGON_WALLET_WATCH_ADDRESSES"))
|
||||
poll = max(3, _env_int("POLYGON_WALLET_WATCH_INTERVAL_SEC", 8))
|
||||
@@ -184,10 +187,11 @@ class StartupCoordinator:
|
||||
"poll_sec": poll,
|
||||
"wallets_count": wallets_count,
|
||||
"polymarket_only": _env_bool("POLYGON_WALLET_WATCH_POLYMARKET_ONLY", True),
|
||||
"chat_targets": len(chat_ids),
|
||||
}
|
||||
validation_error = None
|
||||
if not chat_id:
|
||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
||||
if not chat_ids:
|
||||
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||
elif not rpc_url:
|
||||
validation_error = "missing_POLYGON_RPC_URL"
|
||||
elif wallets_count == 0:
|
||||
@@ -205,17 +209,18 @@ class StartupCoordinator:
|
||||
|
||||
def _start_polymarket_wallet_activity_loop(self) -> LoopStatus:
|
||||
enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
users_count = _parse_csv_count(os.getenv("POLYMARKET_WALLET_ACTIVITY_USERS"))
|
||||
poll = max(5, _env_int("POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC", 20))
|
||||
details = {
|
||||
"poll_sec": poll,
|
||||
"users_count": users_count,
|
||||
"link_preview": _env_bool("POLYMARKET_WALLET_ACTIVITY_LINK_PREVIEW", True),
|
||||
"chat_targets": len(chat_ids),
|
||||
}
|
||||
validation_error = None
|
||||
if not chat_id:
|
||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
||||
if not chat_ids:
|
||||
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||
elif users_count == 0:
|
||||
validation_error = "missing_POLYMARKET_WALLET_ACTIVITY_USERS"
|
||||
return self._start_with_validation(
|
||||
@@ -231,7 +236,7 @@ class StartupCoordinator:
|
||||
|
||||
def _start_weekly_reward_loop(self) -> LoopStatus:
|
||||
enabled = _env_bool("POLYWEATHER_WEEKLY_REWARD_ENABLED", True)
|
||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
settle_weekday = min(
|
||||
7, max(1, _env_int("POLYWEATHER_WEEKLY_REWARD_SETTLE_WEEKDAY", 1))
|
||||
)
|
||||
@@ -250,11 +255,12 @@ class StartupCoordinator:
|
||||
"settle_time": f"{settle_hour:02d}:{settle_minute:02d}",
|
||||
"check_interval_sec": check_interval,
|
||||
"announce": _env_bool("POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED", True),
|
||||
"chat_targets": len(chat_ids),
|
||||
}
|
||||
announce_enabled = bool(details["announce"])
|
||||
validation_error = None
|
||||
if announce_enabled and not chat_id:
|
||||
validation_error = "missing_TELEGRAM_CHAT_ID"
|
||||
if announce_enabled and not chat_ids:
|
||||
validation_error = "missing_TELEGRAM_CHAT_IDS"
|
||||
return self._start_with_validation(
|
||||
key="weekly_reward",
|
||||
label="周榜奖励结算",
|
||||
|
||||
@@ -15,6 +15,7 @@ import requests
|
||||
from loguru import logger
|
||||
|
||||
from src.database.db_manager import DBManager
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
@@ -213,19 +214,20 @@ def _runner(bot: Any) -> None:
|
||||
interval_sec = _env_int("POLYWEATHER_WEEKLY_REWARD_CHECK_INTERVAL_SEC", 300, 30)
|
||||
timeout_sec = _env_int("POLYWEATHER_WEEKLY_REWARD_HTTP_TIMEOUT_SEC", 10, 3)
|
||||
announce = _env_bool("POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED", True)
|
||||
chat_id = str(os.getenv("TELEGRAM_CHAT_ID") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/")
|
||||
service_role_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
|
||||
|
||||
db = DBManager()
|
||||
logger.info(
|
||||
"weekly reward loop started tz={} settle={} {:02d}:{:02d} interval={}s announce={}",
|
||||
"weekly reward loop started tz={} settle={} {:02d}:{:02d} interval={}s announce={} chat_targets={}",
|
||||
tz_name,
|
||||
settle_weekday,
|
||||
settle_hour,
|
||||
settle_minute,
|
||||
interval_sec,
|
||||
announce,
|
||||
len(chat_ids),
|
||||
)
|
||||
|
||||
while True:
|
||||
@@ -313,16 +315,22 @@ def _runner(bot: Any) -> None:
|
||||
len(winners),
|
||||
skipped,
|
||||
)
|
||||
if announce and chat_id:
|
||||
try:
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
_render_settle_report(week_key=week_key, winners=winners, skipped=skipped),
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"weekly reward announcement failed: {exc}")
|
||||
if announce and chat_ids:
|
||||
message = _render_settle_report(week_key=week_key, winners=winners, skipped=skipped)
|
||||
for chat_id in chat_ids:
|
||||
try:
|
||||
bot.send_message(
|
||||
chat_id,
|
||||
message,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"weekly reward announcement failed chat_id={} error={}",
|
||||
chat_id,
|
||||
exc,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"weekly reward cycle failed: {exc}")
|
||||
time.sleep(interval_sec)
|
||||
|
||||
Reference in New Issue
Block a user